Skip to content

Commit 54da09e

Browse files
thomas-manginclaude
andcommitted
refactor(bgp): wire RouteAction and EventKind enums into text event pipeline
FamilyOperation.Action changed from string to bgptypes.RouteAction. MessageInfo.Type changed from string to rpc.EventKind. GetEventType() returns rpc.EventKind with lazy caching via Event.TypeKind field. Added EventKindSent and EventKindNegotiated to the EventKind enum. Event.Type stays string (carries non-BGP types like "cache", "request"). The typed EventKind is cached alongside for dispatch without per-call allocation. Also: server-side tokenize() now rejects backslash (matching CLI tokenizeCommand), and joinTokensWithQuotes no longer attempts to escape embedded quotes (no escape mechanism exists). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e119166 commit 54da09e

18 files changed

Lines changed: 271 additions & 188 deletions

File tree

‎ai/LEARNED-INDEX.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,4 @@ Reusable lessons extracted from gotchas sections across summaries.
192192
- (754) Adding a named test group to `mk/test-unit.mk` requires adding the exclusion pattern to `ZE_GROUP_REST`
193193
- (756) Web commit hook runs synchronously in HTTP handler; slow reload blocks the response (acceptable because reload is <100ms)
194194
- (759) Archive pruning prefix computed by diff of two timestamps at different dates; if filename format has no time token, prefix equals full filename
195+
- (768) Enum-over-string for text event pipeline: Event.Type stays string (non-BGP types), TypeKind caches the parsed EventKind; FamilyOperation.Action typed as RouteAction; local familyOperation types in format/rs/rr/persist are independent; NLRI index map[string] is Go-idiomatic and not improvable

‎internal/component/bgp/event.go‎

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"encoding/json"
1010
"strings"
1111

12+
bgptypes "codeberg.org/thomas-mangin/ze/internal/component/bgp/types"
1213
"codeberg.org/thomas-mangin/ze/internal/core/family"
1314
"codeberg.org/thomas-mangin/ze/internal/core/slogutil"
1415
"codeberg.org/thomas-mangin/ze/pkg/plugin/rpc"
@@ -81,12 +82,13 @@ func ParseEvent(data []byte) (*Event, error) {
8182
_ = json.Unmarshal(payloadData, &bgpPayload)
8283

8384
// Determine event type: use message.type for ze-bgp JSON format if top-level type is missing.
84-
eventType := bgpPayload.Type
85-
if eventType == "" && bgpPayload.Message != nil && bgpPayload.Message.Type != "" {
86-
eventType = bgpPayload.Message.Type
87-
// For "sent" type, we know the nested data is under "update".
88-
if eventType == "sent" {
89-
eventType = "update"
85+
// jsonKey stays a string for rawPayload map lookup; eventKind is the typed value.
86+
jsonKey := bgpPayload.Type
87+
if jsonKey == "" && bgpPayload.Message != nil && bgpPayload.Message.Type != rpc.EventKindUnspecified {
88+
jsonKey = bgpPayload.Message.Type.String()
89+
// For "sent" type, the nested data is under "update".
90+
if bgpPayload.Message.Type == rpc.EventKindSent {
91+
jsonKey = "update"
9092
}
9193
}
9294

@@ -97,7 +99,7 @@ func ParseEvent(data []byte) (*Event, error) {
9799
}
98100

99101
// For non-state events, merge in the nested event data.
100-
if eventType != "" && eventType != "state" {
102+
if jsonKey != "" && jsonKey != "state" {
101103
var rawPayload map[string]json.RawMessage
102104
if err := json.Unmarshal(payloadData, &rawPayload); err == nil {
103105
// Extract raw wire bytes BEFORE narrowing payloadData.
@@ -106,7 +108,7 @@ func ParseEvent(data []byte) (*Event, error) {
106108
if rawData, ok := rawPayload["raw"]; ok {
107109
parseRawFields(&event, rawData)
108110
}
109-
if nestedData, ok := rawPayload[eventType]; ok && len(nestedData) > 0 {
111+
if nestedData, ok := rawPayload[jsonKey]; ok && len(nestedData) > 0 {
110112
// Only use nested data if it's an object (starts with '{'), not a string.
111113
if len(nestedData) > 0 && nestedData[0] == '{' {
112114
// Merge nested data into event (this adds attr, nlri, message, etc.).
@@ -117,9 +119,12 @@ func ParseEvent(data []byte) (*Event, error) {
117119
}
118120
}
119121

120-
// Preserve the event type.
121-
if eventType != "" {
122-
event.Type = eventType
122+
// Preserve the event type parsed from the outer envelope.
123+
if jsonKey != "" {
124+
event.Type = jsonKey
125+
var ek rpc.EventKind
126+
_ = ek.UnmarshalText([]byte(jsonKey))
127+
event.TypeKind = ek
123128
}
124129
// Preserve peer from bgp level.
125130
if len(bgpPayload.Peer) > 0 {
@@ -294,9 +299,10 @@ func ParseFamilyOps(event *Event, data []byte) {
294299
// Event represents a JSON event from ze.
295300
// Handles both sent events (flat format) and received events (nested format).
296301
type Event struct {
297-
// Sent events use top-level type.
298-
Type string `json:"type"`
299-
MsgID uint64 `json:"msg-id"`
302+
// Sent events use top-level type (string: may carry non-BGP event types like "cache", "request").
303+
Type string `json:"type"`
304+
TypeKind rpc.EventKind `json:"-"`
305+
MsgID uint64 `json:"msg-id"`
300306

301307
// Received events use message wrapper (includes type, id, direction).
302308
Message *MessageInfo `json:"message,omitempty"`
@@ -354,25 +360,29 @@ type Event struct {
354360
// FamilyOperation represents a single add or del operation for a family.
355361
// RFC 7911: nlri items may have path-id when ADD-PATH is negotiated.
356362
type FamilyOperation struct {
357-
NextHop string `json:"next-hop,omitempty"` // Only for "add" operations
358-
Action string `json:"action"` // "add" or "del"
359-
NLRIs []any `json:"nlri"` // Strings or {"prefix":"...", "path-id":N}
363+
NextHop string `json:"next-hop,omitempty"` // Only for "add" operations
364+
Action bgptypes.RouteAction `json:"action"`
365+
NLRIs []any `json:"nlri"` // Strings or {"prefix":"...", "path-id":N}
360366
}
361367

362368
// MessageInfo contains message wrapper for received events.
363369
type MessageInfo struct {
364-
Type string `json:"type"`
370+
Type rpc.EventKind `json:"type"`
365371
ID uint64 `json:"id,omitempty"`
366372
Direction rpc.MessageDirection `json:"direction,omitempty"`
367373
}
368374

369375
// GetEventType returns unified event type.
370-
// For received events, uses message.type. For sent events, uses type.
371-
func (e *Event) GetEventType() string {
372-
if e.Message != nil && e.Message.Type != "" {
376+
// For received events, uses message.type. For sent events, uses cached TypeKind
377+
// (populated by ParseEvent), falling back to parsing Type on first call.
378+
func (e *Event) GetEventType() rpc.EventKind {
379+
if e.Message != nil && e.Message.Type != rpc.EventKindUnspecified {
373380
return e.Message.Type
374381
}
375-
return e.Type
382+
if e.TypeKind == rpc.EventKindUnspecified && e.Type != "" {
383+
_ = e.TypeKind.UnmarshalText([]byte(e.Type))
384+
}
385+
return e.TypeKind
376386
}
377387

378388
// GetMsgID returns message ID from either format.

‎internal/component/bgp/event_test.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"github.com/stretchr/testify/assert"
77
"github.com/stretchr/testify/require"
88

9+
bgptypes "codeberg.org/thomas-mangin/ze/internal/component/bgp/types"
910
"codeberg.org/thomas-mangin/ze/internal/core/family"
11+
"codeberg.org/thomas-mangin/ze/pkg/plugin/rpc"
1012
)
1113

1214
// TestParseEvent_ZeBGPUpdateFormat verifies parsing of ze-bgp JSON update events.
@@ -27,7 +29,7 @@ func TestParseEvent_ZeBGPUpdateFormat(t *testing.T) {
2729
event, err := ParseEvent([]byte(input))
2830
require.NoError(t, err)
2931

30-
assert.Equal(t, "update", event.GetEventType())
32+
assert.Equal(t, rpc.EventKindUpdate, event.GetEventType())
3133
assert.Equal(t, uint64(42), event.GetMsgID())
3234
assert.Equal(t, "received", event.GetDirection())
3335
assert.Equal(t, "10.0.0.1", event.GetPeerAddress())
@@ -38,7 +40,7 @@ func TestParseEvent_ZeBGPUpdateFormat(t *testing.T) {
3840
require.Contains(t, event.FamilyOps, family.IPv4Unicast)
3941
ops := event.FamilyOps[family.IPv4Unicast]
4042
require.Len(t, ops, 1)
41-
assert.Equal(t, "add", ops[0].Action)
43+
assert.Equal(t, bgptypes.RouteActionAdd, ops[0].Action)
4244
assert.Equal(t, "10.0.0.1", ops[0].NextHop)
4345
require.Len(t, ops[0].NLRIs, 1)
4446
assert.Equal(t, "10.0.0.0/24", ops[0].NLRIs[0])
@@ -54,7 +56,7 @@ func TestParseEvent_StateFormat(t *testing.T) {
5456
event, err := ParseEvent([]byte(input))
5557
require.NoError(t, err)
5658

57-
assert.Equal(t, "state", event.GetEventType())
59+
assert.Equal(t, rpc.EventKindState, event.GetEventType())
5860
assert.Equal(t, "10.0.0.1", event.GetPeerAddress())
5961
assert.Equal(t, "up", event.GetPeerState())
6062
}

‎internal/component/bgp/plugins/adj_rib_in/rib.go‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ func (r *AdjRIBInManager) handleReceivedStructured(se *rpc.StructuredEvent) {
210210
ctx := bgpctx.Registry.Get(wu.SourceCtxID())
211211

212212
// Build a bgp.Event with raw hex fields from wire data.
213-
event := &bgp.Event{Type: "update"}
213+
event := &bgp.Event{Type: "update", TypeKind: rpc.EventKindUpdate}
214214

215215
// Raw attributes hex (path attrs without MP_REACH/UNREACH).
216216
if msg.AttrsWire != nil {
@@ -231,7 +231,7 @@ func (r *AdjRIBInManager) handleReceivedStructured(se *rpc.StructuredEvent) {
231231
addPath := ctx != nil && ctx.AddPath(fam)
232232
event.AddPath[fam] = addPath
233233
event.FamilyOps[fam] = append(event.FamilyOps[fam], bgp.FamilyOperation{
234-
Action: "add",
234+
Action: bgptypes.RouteActionAdd,
235235
NLRIs: wireNLRIsToAny(nlriData, addPath, fam),
236236
})
237237
}
@@ -244,7 +244,7 @@ func (r *AdjRIBInManager) handleReceivedStructured(se *rpc.StructuredEvent) {
244244
addPath := ctx != nil && ctx.AddPath(fam)
245245
event.AddPath[fam] = addPath
246246
event.FamilyOps[fam] = append(event.FamilyOps[fam], bgp.FamilyOperation{
247-
Action: "del",
247+
Action: bgptypes.RouteActionDel,
248248
NLRIs: wireNLRIsToAny(wdData, addPath, fam),
249249
})
250250
}
@@ -260,7 +260,7 @@ func (r *AdjRIBInManager) handleReceivedStructured(se *rpc.StructuredEvent) {
260260
event.AddPath[fam] = addPath
261261
nhop := mpReach.NextHop().String()
262262
event.FamilyOps[fam] = append(event.FamilyOps[fam], bgp.FamilyOperation{
263-
Action: "add",
263+
Action: bgptypes.RouteActionAdd,
264264
NextHop: nhop,
265265
NLRIs: wireNLRIsToAny(nlriBytes, addPath, fam),
266266
})
@@ -277,7 +277,7 @@ func (r *AdjRIBInManager) handleReceivedStructured(se *rpc.StructuredEvent) {
277277
addPath := ctx != nil && ctx.AddPath(fam)
278278
event.AddPath[fam] = addPath
279279
event.FamilyOps[fam] = append(event.FamilyOps[fam], bgp.FamilyOperation{
280-
Action: "del",
280+
Action: bgptypes.RouteActionDel,
281281
NLRIs: wireNLRIsToAny(wdBytes, addPath, fam),
282282
})
283283
}
@@ -372,9 +372,9 @@ func (r *AdjRIBInManager) dispatch(event *bgp.Event) {
372372
eventType := event.GetEventType()
373373

374374
switch eventType {
375-
case "update":
375+
case rpc.EventKindUpdate:
376376
r.handleReceived(event)
377-
case "state":
377+
case rpc.EventKindState:
378378
r.handleState(event)
379379
}
380380
}
@@ -407,7 +407,7 @@ func (r *AdjRIBInManager) handleReceived(event *bgp.Event) {
407407

408408
for _, op := range ops {
409409
switch op.Action {
410-
case "add":
410+
case bgptypes.RouteActionAdd:
411411
// Skip adds without essential fields -- routes missing attributes
412412
// or next-hop cannot be replayed correctly via "update hex" commands.
413413
if event.RawAttributes == "" {
@@ -475,7 +475,7 @@ func (r *AdjRIBInManager) handleReceived(event *bgp.Event) {
475475
}
476476
}
477477

478-
case "del":
478+
case bgptypes.RouteActionDel:
479479
for _, nlriVal := range op.NLRIs {
480480
prefix, pathID := bgp.ParseNLRIValue(nlriVal)
481481
if prefix == "" {

‎internal/component/bgp/plugins/adj_rib_in/rib_test.go‎

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import (
1010
"github.com/stretchr/testify/require"
1111

1212
bgp "codeberg.org/thomas-mangin/ze/internal/component/bgp"
13+
bgptypes "codeberg.org/thomas-mangin/ze/internal/component/bgp/types"
1314
"codeberg.org/thomas-mangin/ze/internal/core/family"
1415
"codeberg.org/thomas-mangin/ze/internal/core/seqmap"
16+
"codeberg.org/thomas-mangin/ze/pkg/plugin/rpc"
1517
sdk "codeberg.org/thomas-mangin/ze/pkg/plugin/sdk"
1618
)
1719

@@ -63,13 +65,13 @@ func TestStoreReceivedRoute(t *testing.T) {
6365

6466
// format=full event: ORIGIN IGP (40 01 01 00), 10.0.0.0/24 (18 0a 00 00)
6567
event := &bgp.Event{
66-
Message: &bgp.MessageInfo{Type: "update", ID: 100},
68+
Message: &bgp.MessageInfo{Type: rpc.EventKindUpdate, ID: 100},
6769
Peer: testPeerJSON(t),
6870
RawAttributes: "40010100",
6971
RawNLRI: map[family.Family]string{family.IPv4Unicast: "180a0000"},
7072
FamilyOps: map[family.Family][]bgp.FamilyOperation{
7173
family.IPv4Unicast: {
72-
{NextHop: "10.0.0.1", Action: "add", NLRIs: []any{"10.0.0.0/24"}},
74+
{NextHop: "10.0.0.1", Action: bgptypes.RouteActionAdd, NLRIs: []any{"10.0.0.0/24"}},
7375
},
7476
},
7577
}
@@ -110,13 +112,13 @@ func TestStoreAllFamilies(t *testing.T) {
110112
// The raw blob "deadbeef" must be stored as-is; prefixToWireHex would produce
111113
// bare IPv4 bytes "180a0000" which is wrong for VPN wire format.
112114
event := &bgp.Event{
113-
Message: &bgp.MessageInfo{Type: "update", ID: 200},
115+
Message: &bgp.MessageInfo{Type: rpc.EventKindUpdate, ID: 200},
114116
Peer: testPeerJSON(t),
115117
RawAttributes: "40010100",
116118
RawNLRI: map[family.Family]string{ipv4VPN: "deadbeef"},
117119
FamilyOps: map[family.Family][]bgp.FamilyOperation{
118120
ipv4VPN: {
119-
{NextHop: "10.0.0.1", Action: "add", NLRIs: []any{"10.0.0.0/24"}},
121+
{NextHop: "10.0.0.1", Action: bgptypes.RouteActionAdd, NLRIs: []any{"10.0.0.0/24"}},
120122
},
121123
},
122124
}
@@ -148,13 +150,13 @@ func TestRemoveWithdrawnRoute(t *testing.T) {
148150

149151
// First announce
150152
announce := &bgp.Event{
151-
Message: &bgp.MessageInfo{Type: "update", ID: 100},
153+
Message: &bgp.MessageInfo{Type: rpc.EventKindUpdate, ID: 100},
152154
Peer: peerJSON,
153155
RawAttributes: "40010100",
154156
RawNLRI: map[family.Family]string{family.IPv4Unicast: "180a0000"},
155157
FamilyOps: map[family.Family][]bgp.FamilyOperation{
156158
family.IPv4Unicast: {
157-
{NextHop: "10.0.0.1", Action: "add", NLRIs: []any{"10.0.0.0/24"}},
159+
{NextHop: "10.0.0.1", Action: bgptypes.RouteActionAdd, NLRIs: []any{"10.0.0.0/24"}},
158160
},
159161
},
160162
}
@@ -163,13 +165,13 @@ func TestRemoveWithdrawnRoute(t *testing.T) {
163165

164166
// Then withdraw
165167
withdraw := &bgp.Event{
166-
Message: &bgp.MessageInfo{Type: "update", ID: 101},
168+
Message: &bgp.MessageInfo{Type: rpc.EventKindUpdate, ID: 101},
167169
Peer: peerJSON,
168170
// Withdrawals may have raw-withdrawn but not raw-attributes
169171
RawWithdrawn: map[family.Family]string{family.IPv4Unicast: "180a0000"},
170172
FamilyOps: map[family.Family][]bgp.FamilyOperation{
171173
family.IPv4Unicast: {
172-
{Action: "del", NLRIs: []any{"10.0.0.0/24"}},
174+
{Action: bgptypes.RouteActionDel, NLRIs: []any{"10.0.0.0/24"}},
173175
},
174176
},
175177
}
@@ -279,13 +281,13 @@ func TestSequenceIndexMonotonic(t *testing.T) {
279281
for i, prefix := range []string{"10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"} {
280282
nlriHex := []string{"180a0000", "180a0001", "180a0002"}
281283
event := &bgp.Event{
282-
Message: &bgp.MessageInfo{Type: "update", ID: uint64(100 + i)},
284+
Message: &bgp.MessageInfo{Type: rpc.EventKindUpdate, ID: uint64(100 + i)},
283285
Peer: peerJSON,
284286
RawAttributes: "40010100",
285287
RawNLRI: map[family.Family]string{family.IPv4Unicast: nlriHex[i]},
286288
FamilyOps: map[family.Family][]bgp.FamilyOperation{
287289
family.IPv4Unicast: {
288-
{NextHop: "10.0.0.1", Action: "add", NLRIs: []any{prefix}},
290+
{NextHop: "10.0.0.1", Action: bgptypes.RouteActionAdd, NLRIs: []any{prefix}},
289291
},
290292
},
291293
}
@@ -439,13 +441,13 @@ func TestMultipleNLRIsPerUpdate(t *testing.T) {
439441

440442
// Two NLRIs: 10.0.0.0/24 (18 0a 00 00) + 10.0.1.0/24 (18 0a 00 01)
441443
event := &bgp.Event{
442-
Message: &bgp.MessageInfo{Type: "update", ID: 100},
444+
Message: &bgp.MessageInfo{Type: rpc.EventKindUpdate, ID: 100},
443445
Peer: testPeerJSON(t),
444446
RawAttributes: "40010100",
445447
RawNLRI: map[family.Family]string{family.IPv4Unicast: "180a0000180a0001"},
446448
FamilyOps: map[family.Family][]bgp.FamilyOperation{
447449
family.IPv4Unicast: {
448-
{NextHop: "10.0.0.1", Action: "add", NLRIs: []any{"10.0.0.0/24", "10.0.1.0/24"}},
450+
{NextHop: "10.0.0.1", Action: bgptypes.RouteActionAdd, NLRIs: []any{"10.0.0.0/24", "10.0.1.0/24"}},
449451
},
450452
},
451453
}
@@ -634,13 +636,13 @@ func TestComplexFamilyMultiNLRI(t *testing.T) {
634636
// VPN UPDATE with 2 parsed NLRIs but a single concatenated raw blob.
635637
// The raw blob contains both NLRIs in wire format (RD+labels+prefix).
636638
event := &bgp.Event{
637-
Message: &bgp.MessageInfo{Type: "update", ID: 300},
639+
Message: &bgp.MessageInfo{Type: rpc.EventKindUpdate, ID: 300},
638640
Peer: testPeerJSON(t),
639641
RawAttributes: "40010100",
640642
RawNLRI: map[family.Family]string{ipv4VPN: "aabbccdd11223344"},
641643
FamilyOps: map[family.Family][]bgp.FamilyOperation{
642644
ipv4VPN: {
643-
{NextHop: "10.0.0.1", Action: "add", NLRIs: []any{"10.0.0.0/24", "10.0.1.0/24"}},
645+
{NextHop: "10.0.0.1", Action: bgptypes.RouteActionAdd, NLRIs: []any{"10.0.0.0/24", "10.0.1.0/24"}},
644646
},
645647
},
646648
}

0 commit comments

Comments
 (0)