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}