route_source.go
1package waypoint
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7
8 "git.kilimanjaro.io/ygo"
9)
10
11// extractWaypointFromElement extracts full Waypoint data from an XmlElement.
12func extractWaypointFromElement(elem *ygo.XmlElement, txn *ygo.Transaction) (Waypoint, error) {
13 var wp Waypoint
14
15 // Get all attributes
16 attrIter, err := elem.AttrIter(txn)
17 if err != nil {
18 return wp, fmt.Errorf("failed to get attribute iterator: %w", err)
19 }
20 defer attrIter.Destroy()
21
22 for {
23 attr, err := attrIter.Next()
24 if err != nil {
25 return wp, fmt.Errorf("failed to get next attribute: %w", err)
26 }
27 if attr == nil {
28 break
29 }
30
31 // Use ToInterface to convert value to appropriate Go type
32 val, err := attr.Value.ToInterface()
33 if err != nil {
34 // If conversion fails, skip this attribute
35 attr.Destroy()
36 continue
37 }
38
39 // Map attributes to Waypoint fields
40 switch attr.Name {
41 case "type":
42 if v, ok := val.(string); ok {
43 wp.Type = v
44 }
45 case "id":
46 if v, ok := val.(string); ok {
47 wp.ID = v
48 }
49 case "label":
50 if v, ok := val.(string); ok {
51 wp.Label = v
52 }
53 case "gid":
54 if v, ok := val.(string); ok {
55 wp.GID = v
56 }
57 case "country":
58 if v, ok := val.(string); ok {
59 wp.Country = v
60 }
61 case "point":
62 // Handle point attribute - read raw JSON bytes and unmarshal directly
63 if jsonBytes, ok := attr.Value.JSONBytes(); ok {
64 var point []float64
65 if err := json.Unmarshal(jsonBytes, &point); err == nil && len(point) == 2 {
66 wp.Point = point
67 }
68 }
69 case "cons":
70 // Handle cons attribute - read raw JSON bytes and unmarshal directly
71 if jsonBytes, ok := attr.Value.JSONBytes(); ok {
72 var cons []string
73 if err := json.Unmarshal(jsonBytes, &cons); err == nil {
74 wp.Cons = cons
75 }
76 }
77 case "error":
78 // Handle error attribute - read raw JSON bytes and unmarshal directly
79 if jsonBytes, ok := attr.Value.JSONBytes(); ok {
80 var errors []string
81 if err := json.Unmarshal(jsonBytes, &errors); err == nil {
82 wp.Error = errors
83 }
84 }
85 case "nonroutable":
86 // Handle nonroutable attribute - stored as boolean
87 if v, ok := val.(bool); ok {
88 wp.Nonroutable = v
89 }
90 case "from":
91 // Handle from attribute - read raw JSON bytes and unmarshal directly
92 if jsonBytes, ok := attr.Value.JSONBytes(); ok {
93 var fromMap map[string]Route
94 if err := json.Unmarshal(jsonBytes, &fromMap); err == nil {
95 wp.From = fromMap
96 }
97 }
98 }
99
100 attr.Destroy()
101 }
102
103 return wp, nil
104}
105
106// FindRouteSource traverses the document tree via DFS to find all waypoints
107// and sets the "from" attribute based on document order. The first waypoint
108// gets an empty array, subsequent waypoints get the ID of the previous waypoint.
109// Only sets "from" if it is undefined, null, or absent - never overwrites existing values.
110func FindRouteSource(doc *ygo.Doc) error {
111 // Get the fragment OUTSIDE the transaction
112 frag, err := doc.GetXmlFragment("main")
113 if err != nil {
114 return fmt.Errorf("failed to get xml fragment 'main': %w", err)
115 }
116 defer frag.Destroy()
117
118 // Collect waypoint data: ordered list and map of full waypoint data
119 var waypointData struct {
120 orderedIDs []string
121 waypoints map[string]Waypoint // full waypoint data by ID
122 }
123 waypointData.waypoints = make(map[string]Waypoint)
124
125 err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
126 // Iterate over all children of the fragment
127 childIter, err := frag.ChildIter(txn)
128 if err != nil {
129 return fmt.Errorf("failed to get child iterator: %w", err)
130 }
131 defer childIter.Destroy()
132
133 for {
134 child, err := childIter.Next()
135 if err != nil {
136 return fmt.Errorf("failed to get next child: %w", err)
137 }
138 if child == nil {
139 break
140 }
141
142 // Process element children
143 if child.Tag() == ygo.TagXmlElem {
144 elem, err := child.ToXmlElement()
145 child.Destroy()
146 if err != nil {
147 return fmt.Errorf("failed to convert to XmlElement: %w", err)
148 }
149
150 // Traverse this element and its children
151 err = dfsCollectWaypointData(elem, txn, &waypointData)
152 elem.Destroy()
153 if err != nil {
154 return err
155 }
156 } else {
157 child.Destroy()
158 }
159 }
160
161 return nil
162 })
163
164 if err != nil {
165 return err
166 }
167
168 // Build a map of waypoint ID -> from map (for those that need from set)
169 fromValues := make(map[string]map[string]Route)
170
171 for i, id := range waypointData.orderedIDs {
172 currentWp := waypointData.waypoints[id]
173
174 // Skip if nonroutable
175 if currentWp.Nonroutable {
176 continue
177 }
178
179 // First waypoint gets empty map (or clears SAA routes)
180 if i == 0 {
181 if currentWp.From == nil {
182 fromValues[id] = make(map[string]Route)
183 } else {
184 // Check for SAA routes that should be deleted (first waypoint shouldn't have sources)
185 hasSAARoutes := false
186 for key := range currentWp.From {
187 if strings.HasSuffix(key, ":saa") {
188 hasSAARoutes = true
189 break
190 }
191 }
192 if hasSAARoutes {
193 // Create new map without SAA routes
194 fromValues[id] = make(map[string]Route)
195 for k, v := range currentWp.From {
196 if !strings.HasSuffix(k, ":saa") {
197 fromValues[id][k] = v
198 }
199 }
200 }
201 }
202 continue
203 }
204
205 // Get previous waypoint
206 prevID := waypointData.orderedIDs[i-1]
207 prevWp := waypointData.waypoints[prevID]
208
209 // Generate expected route key with SAA suffix
210 expectedRouteKey, err := ToRouteKey(prevWp, currentWp, true)
211 if err != nil {
212 // Log error but continue - don't block on invalid route key
213 fmt.Printf("Warning: failed to generate route key for waypoint %s: %v\n", id, err)
214 continue
215 }
216
217 // Track routes to delete (SAA routes pointing to wrong source)
218 routesToDelete := []string{}
219
220 // Check existing routes for SAA-tagged routes that need updating
221 if currentWp.From != nil {
222 for existingKey := range currentWp.From {
223 if strings.HasSuffix(existingKey, ":saa") && existingKey != expectedRouteKey {
224 // This SAA route points to wrong source - mark for deletion
225 routesToDelete = append(routesToDelete, existingKey)
226 }
227 }
228 }
229
230 // Check if expected key already exists
231 if currentWp.From == nil {
232 fromValues[id] = make(map[string]Route)
233 fromValues[id][expectedRouteKey] = Route{}
234 } else if _, exists := currentWp.From[expectedRouteKey]; !exists {
235 // Key doesn't exist - copy existing map, add new key, remove stale SAA routes
236 fromValues[id] = make(map[string]Route)
237 for k, v := range currentWp.From {
238 // Skip routes marked for deletion
239 shouldDelete := false
240 for _, delKey := range routesToDelete {
241 if k == delKey {
242 shouldDelete = true
243 break
244 }
245 }
246 if !shouldDelete {
247 fromValues[id][k] = v
248 }
249 }
250 fromValues[id][expectedRouteKey] = Route{}
251 } else {
252 // Key exists - still need to remove stale SAA routes if any
253 if len(routesToDelete) > 0 {
254 fromValues[id] = make(map[string]Route)
255 for k, v := range currentWp.From {
256 shouldDelete := false
257 for _, delKey := range routesToDelete {
258 if k == delKey {
259 shouldDelete = true
260 break
261 }
262 }
263 if !shouldDelete {
264 fromValues[id][k] = v
265 }
266 }
267 }
268 }
269 }
270
271 // If no waypoints need from attribute, nothing to do
272 if len(fromValues) == 0 {
273 return nil
274 }
275
276 // Second pass: update waypoints that need "from" attribute set
277 return doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
278 // Re-traverse to find and update waypoints
279 childIter, err := frag.ChildIter(txn)
280 if err != nil {
281 return fmt.Errorf("failed to get child iterator: %w", err)
282 }
283
284 for {
285 child, err := childIter.Next()
286 if err != nil {
287 return fmt.Errorf("failed to get next child: %w", err)
288 }
289 if child == nil {
290 break
291 }
292
293 // Process element children
294 if child.Tag() == ygo.TagXmlElem {
295 elem, err := child.ToXmlElement()
296 child.Destroy()
297 if err != nil {
298 return fmt.Errorf("failed to convert to XmlElement: %w", err)
299 }
300
301 // Traverse and update waypoints
302 err = dfsUpdateWaypoints(elem, txn, fromValues)
303 elem.Destroy()
304 if err != nil {
305 return err
306 }
307 } else {
308 child.Destroy()
309 }
310 }
311
312 return nil
313 })
314}
315
316// dfsCollectWaypointData recursively traverses XML elements depth-first,
317// collecting waypoint IDs and full waypoint data.
318func dfsCollectWaypointData(elem *ygo.XmlElement, txn *ygo.Transaction, data *struct {
319 orderedIDs []string
320 waypoints map[string]Waypoint
321}) error {
322 // Check if this element is a waypoint
323 tag, err := elem.Tag()
324 if err != nil {
325 return fmt.Errorf("failed to get element tag: %w", err)
326 }
327
328 if tag == "waypoint" {
329 // Extract full waypoint data
330 wp, err := extractWaypointFromElement(elem, txn)
331 if err != nil {
332 return fmt.Errorf("failed to extract waypoint data: %w", err)
333 }
334 if wp.ID != "" {
335 data.orderedIDs = append(data.orderedIDs, wp.ID)
336 data.waypoints[wp.ID] = wp
337 }
338 }
339
340 // Iterate over children and recurse
341 childIter, err := elem.ChildIter(txn)
342 if err != nil {
343 return fmt.Errorf("failed to get child iterator: %w", err)
344 }
345
346 for {
347 child, err := childIter.Next()
348 if err != nil {
349 return fmt.Errorf("failed to get next child: %w", err)
350 }
351 if child == nil {
352 break
353 }
354
355 // Only process XmlElement children (skip text nodes)
356 if child.Tag() != ygo.TagXmlElem {
357 child.Destroy()
358 continue
359 }
360
361 // Convert to XmlElement
362 childElem, err := child.ToXmlElement()
363 child.Destroy()
364 if err != nil {
365 return fmt.Errorf("failed to convert child to XmlElement: %w", err)
366 }
367
368 // Recurse
369 if err := dfsCollectWaypointData(childElem, txn, data); err != nil {
370 childElem.Destroy()
371 return err
372 }
373 childElem.Destroy()
374 }
375
376 return nil
377}
378
379// extractWaypointFromAttr extracts ID and checks for "from" attribute from an XmlElement.
380func extractWaypointFromAttr(elem *ygo.XmlElement, txn *ygo.Transaction) (id string, hasFrom bool, err error) {
381 // Get all attributes
382 attrIter, err := elem.AttrIter(txn)
383 if err != nil {
384 return "", false, fmt.Errorf("failed to get attribute iterator: %w", err)
385 }
386 defer attrIter.Destroy()
387
388 for {
389 attr, err := attrIter.Next()
390 if err != nil {
391 return "", false, fmt.Errorf("failed to get next attribute: %w", err)
392 }
393 if attr == nil {
394 break
395 }
396
397 val, err := attr.Value.ToInterface()
398 if err != nil {
399 attr.Destroy()
400 continue
401 }
402
403 switch attr.Name {
404 case "id":
405 if v, ok := val.(string); ok {
406 id = v
407 }
408 case "from":
409 // "from" attribute exists
410 hasFrom = true
411 }
412
413 attr.Destroy()
414 }
415
416 return id, hasFrom, nil
417}
418
419// dfsUpdateWaypoints recursively traverses XML elements depth-first,
420// updating waypoints that need "from" attribute set.
421func dfsUpdateWaypoints(elem *ygo.XmlElement, txn *ygo.Transaction, fromValues map[string]map[string]Route) error {
422 // Check if this element is a waypoint
423 tag, err := elem.Tag()
424 if err != nil {
425 return fmt.Errorf("failed to get element tag: %w", err)
426 }
427
428 if tag == "waypoint" {
429 // Get waypoint ID
430 id, _, err := extractWaypointFromAttr(elem, txn)
431 if err != nil {
432 return fmt.Errorf("failed to extract waypoint ID: %w", err)
433 }
434
435 // Check if we need to update this waypoint
436 if fromMap, ok := fromValues[id]; ok {
437 // Set the "from" attribute as a JSON object (map)
438 input, err := ygo.JSON(fromMap)
439 if err != nil {
440 return fmt.Errorf("failed to marshal from value for waypoint %s: %w", id, err)
441 }
442
443 if err := elem.SetAttribute(txn, "from", input); err != nil {
444 return fmt.Errorf("failed to set from attribute on waypoint %s: %w", id, err)
445 }
446 }
447 }
448
449 // Iterate over children and recurse
450 childIter, err := elem.ChildIter(txn)
451 if err != nil {
452 return fmt.Errorf("failed to get child iterator: %w", err)
453 }
454
455 for {
456 child, err := childIter.Next()
457 if err != nil {
458 return fmt.Errorf("failed to get next child: %w", err)
459 }
460 if child == nil {
461 break
462 }
463
464 // Only process XmlElement children (skip text nodes)
465 if child.Tag() != ygo.TagXmlElem {
466 child.Destroy()
467 continue
468 }
469
470 // Convert to XmlElement
471 childElem, err := child.ToXmlElement()
472 child.Destroy()
473 if err != nil {
474 return fmt.Errorf("failed to convert child to XmlElement: %w", err)
475 }
476
477 // Recurse
478 if err := dfsUpdateWaypoints(childElem, txn, fromValues); err != nil {
479 childElem.Destroy()
480 return err
481 }
482 childElem.Destroy()
483 }
484
485 return nil
486}