From fd62738faa13fc279147ff2a3faf0b0591402e18 Mon Sep 17 00:00:00 2001 From: Cloorc Date: Tue, 23 Sep 2025 01:14:14 +0100 Subject: [PATCH] enhance: using mermaid output Signed-off-by: Cloorc --- dump.go | 35 +++++++++++++++++------------------ forest.go | 54 +++++++++++++++++++++++++++--------------------------- matcher.go | 4 ++-- 3 files changed, 46 insertions(+), 47 deletions(-) diff --git a/dump.go b/dump.go index 193ac27..54f262e 100644 --- a/dump.go +++ b/dump.go @@ -108,8 +108,7 @@ func dumpMultiLevelCacheToFile(cache *MultiLevelCache, filename string) error { // DumpForestToFile dumps the forest in concise graph format to a file func DumpForestToFile(m *InMemoryMatcher, filename string) error { // We'll produce two files per requested filename: - // - .graph : concise graph listing edges between node keys (dimension|value+match) - // - .mapping : mapping of node_key -> comma-separated rule IDs (for lookup) + // - .mermaid : concise graph listing edges between node keys (dimension#value+match) m.mu.RLock() // Snapshot the forestIndexes keys to avoid holding matcher lock while writing files @@ -134,8 +133,6 @@ func DumpForestToFile(m *InMemoryMatcher, filename string) error { forest := forestIndex.RuleForest // Tenant header - graphLines = append(graphLines, fmt.Sprintf("# Tenant: %s", tenantKey)) - mappingLines = append(mappingLines, fmt.Sprintf("# Tenant: %s", tenantKey)) graphs, relationship := make(map[string]any), "" // Snapshot NodeRelationships under forest lock @@ -143,15 +140,20 @@ func DumpForestToFile(m *InMemoryMatcher, filename string) error { for current, trans := range forest.NodeRelationships { b := strings.Builder{} for rid, next := range trans { - relationship = fmt.Sprintf("%s %s", current, next) - if _, ok := graphs[relationship]; !ok { - graphs[relationship] = nil - graphLines = append(graphLines, relationship) + if next != "" { + // Not the last node + relationship = fmt.Sprintf(" %s --> %s", current, next) + if _, ok := graphs[relationship]; !ok { + graphs[relationship] = nil + graphLines = append(graphLines, relationship) + } + } + if b.Len() > 0 { + b.WriteString(",") } b.WriteString(rid) - b.WriteString(",") } - mappingLines = append(mappingLines, fmt.Sprintf("%s %s", current, b.String())) + mappingLines = append(mappingLines, fmt.Sprintf(" %s[%s<%s>]", current, current, b.String())) } forest.mu.RUnlock() @@ -161,16 +163,13 @@ func DumpForestToFile(m *InMemoryMatcher, filename string) error { } // Write graph file - graphFile := filename + ".graph" - if err := os.WriteFile(graphFile, []byte(strings.Join(graphLines, "\n")), 0644); err != nil { + graphFile := filename + ".mermaid" + graph := append([]string{}, "flowchart TD") + graph = append(graph, mappingLines...) + graph = append(graph, graphLines...) + if err := os.WriteFile(graphFile, []byte(strings.Join(graph, "\n")), 0644); err != nil { return fmt.Errorf("failed to write forest graph file: %w", err) } - // Write mapping file - mappingFile := filename + ".mapping" - if err := os.WriteFile(mappingFile, []byte(strings.Join(mappingLines, "\n")), 0644); err != nil { - return fmt.Errorf("failed to write forest mapping file: %w", err) - } - return nil } diff --git a/forest.go b/forest.go index 6d9397d..9844399 100644 --- a/forest.go +++ b/forest.go @@ -129,20 +129,16 @@ func CreateForestIndexCompat() *RuleForest { // generateNodeName generates a unique node name in the pattern 'dimension|value+match_type' func generateNodeName(dimensionName, value string, matchType MatchType) string { - var matchTypeStr string switch matchType { case MatchTypeEqual: - matchTypeStr = "" case MatchTypeAny: - matchTypeStr = "*" + value = "*" case MatchTypePrefix: - matchTypeStr = "*" value = value + "*" case MatchTypeSuffix: - matchTypeStr = "*" value = "*" + value } - return fmt.Sprintf("%s|%s%s", dimensionName, value, matchTypeStr) + return fmt.Sprintf("%s#%s", dimensionName, value) } // cleanupNodeRelationshipsForRule removes relationships for a specific rule from a specific node's relationships. @@ -223,55 +219,54 @@ func (rf *RuleForest) AddRule(rule *Rule) (*Rule, error) { ruleNodes = append(ruleNodes, rootNode) // Track parent node for relationship building - var parentNodeName string + var parentNodeName, childNodeName string // Traverse/create path for remaining dimensions - current := rootNode + parent := rootNode + parentDim := firstDim + parentNodeName = generateNodeName(parent.DimensionName, parent.Value, parentDim.MatchType) for i := 1; i < len(sorted); i++ { - dim := completeRule.GetDimensionValue(sorted[i]) + currentDim := completeRule.GetDimensionValue(sorted[i]) // Use the original match type from the rule definition - do NOT change it - matchType := dim.MatchType + currentMatchType := currentDim.MatchType // Get or create the match branch for the CURRENT dimension's match type - branch, exists := current.Branches[matchType] + branch, exists := parent.Branches[currentMatchType] if !exists { branch = &MatchBranch{ - MatchType: matchType, + MatchType: currentMatchType, Rules: []*Rule{}, Children: make(map[string]*SharedNode), } - current.Branches[matchType] = branch + parent.Branches[currentMatchType] = branch } - // Get or create child node for this dimension value within the match branch - child, exists := branch.Children[dim.Value] + // Get or create current node for this dimension value within the match branch + current, exists := branch.Children[currentDim.Value] if !exists { - child = CreateSharedNode(i, dim.DimensionName, dim.Value) - branch.Children[dim.Value] = child + current = CreateSharedNode(i, currentDim.DimensionName, currentDim.Value) + branch.Children[currentDim.Value] = current // MAINTAIN RELATIONSHIPS: Track parent-child relationship for efficient dumping - parentNodeName = generateNodeName(current.DimensionName, current.Value, dim.MatchType) - childNodeName := generateNodeName(child.DimensionName, child.Value, matchType) - - // MAINTAIN NODE RELATIONSHIPS: Track rule transitions for efficient dumping + childNodeName = generateNodeName(current.DimensionName, current.Value, currentMatchType) if rf.NodeRelationships[parentNodeName] == nil { rf.NodeRelationships[parentNodeName] = make(map[string]string) } rf.NodeRelationships[parentNodeName][completeRule.ID] = childNodeName } else { // Even if child exists, still record the rule transition - parentNodeName = generateNodeName(current.DimensionName, current.Value, dim.MatchType) - childNodeName := generateNodeName(child.DimensionName, child.Value, matchType) - + childNodeName = generateNodeName(current.DimensionName, current.Value, currentMatchType) if rf.NodeRelationships[parentNodeName] == nil { rf.NodeRelationships[parentNodeName] = make(map[string]string) } rf.NodeRelationships[parentNodeName][completeRule.ID] = childNodeName } - ruleNodes = append(ruleNodes, child) - current = child + ruleNodes = append(ruleNodes, current) + parent = current + parentDim = currentDim + parentNodeName = generateNodeName(parent.DimensionName, parent.Value, parentDim.MatchType) } // Add rule to the final node (the node for the last dimension the rule specifies) @@ -283,7 +278,12 @@ func (rf *RuleForest) AddRule(rule *Rule) (*Rule, error) { finalMatchType = lastDim.MatchType } } - current.AddRule(completeRule, finalMatchType) + // Last dimension node + if _, ok := rf.NodeRelationships[childNodeName]; !ok { + rf.NodeRelationships[childNodeName] = make(map[string]string) + } + rf.NodeRelationships[childNodeName][completeRule.ID] = "" + parent.AddRule(completeRule, finalMatchType) // Index the rule for quick removal rf.RuleIndex[completeRule.ID] = ruleNodes diff --git a/matcher.go b/matcher.go index bb153ed..7f74d63 100644 --- a/matcher.go +++ b/matcher.go @@ -12,7 +12,7 @@ import ( "time" ) -const snapshotFileName = "snapshot" // .graph, .cache, .mapping +const snapshotFileName = "snapshot" // .mermaid, .cache // InMemoryMatcher implements the core matching logic using forest indexes type InMemoryMatcher struct { @@ -124,7 +124,7 @@ func (m *InMemoryMatcher) startSnapshotMonitor() { } else { slog.Info("Snapshot dump complete, check snapshot.*.") } - } else if st, err := os.Stat(snapshotFileName + ".graph"); err != nil || st.Size() <= 0 { + } else if st, err := os.Stat(snapshotFileName + ".mermaid"); err != nil || st.Size() <= 0 { // First time snapshot generation slog.Info("No snapshot file found, triggering initial snapshot") atomic.CompareAndSwapInt64(&m.snapshotChanged, 0, 1)