-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpath_match.go
More file actions
137 lines (111 loc) · 3.6 KB
/
Copy pathpath_match.go
File metadata and controls
137 lines (111 loc) · 3.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package farp
import "strings"
// MatchPath checks whether a URL path matches a glob pattern.
//
// Pattern syntax:
// - Literal segments match exactly: "/api/v1/users" matches "/api/v1/users"
// - "*" matches exactly one path segment: "/api/*/status" matches "/api/users/status"
// - "**" matches zero or more path segments: "/api/**" matches "/api", "/api/v1", "/api/v1/users/123"
func MatchPath(pattern, path string) bool {
patternParts := splitPath(pattern)
pathParts := splitPath(path)
return matchParts(patternParts, pathParts)
}
func splitPath(p string) []string {
p = strings.Trim(p, "/")
if p == "" {
return nil
}
return strings.Split(p, "/")
}
func matchParts(pattern, path []string) bool {
pi, pa := 0, 0
for pi < len(pattern) && pa < len(path) {
seg := pattern[pi]
switch {
case seg == "**":
// If ** is the last pattern segment, it matches everything remaining
if pi == len(pattern)-1 {
return true
}
// Try matching ** against zero or more path segments
for tryPa := pa; tryPa <= len(path); tryPa++ {
if matchParts(pattern[pi+1:], path[tryPa:]) {
return true
}
}
return false
case seg == "*":
// Matches exactly one segment — just advance both
pi++
pa++
case strings.Contains(seg, "*"):
// Segment contains inline wildcard (e.g., "openapi*" matches "openapi.json")
if !matchSegment(seg, path[pa]) {
return false
}
pi++
pa++
default:
if seg != path[pa] {
return false
}
pi++
pa++
}
}
// Handle trailing ** which can match zero segments
for pi < len(pattern) && pattern[pi] == "**" {
pi++
}
return pi == len(pattern) && pa == len(path)
}
// matchSegment matches a single path segment against a pattern segment
// containing inline wildcards (e.g., "openapi*" matches "openapi.json").
func matchSegment(pattern, segment string) bool {
// Simple prefix/suffix matching for inline *
if prefix, ok := strings.CutSuffix(pattern, "*"); ok {
return strings.HasPrefix(segment, prefix)
}
if suffix, ok := strings.CutPrefix(pattern, "*"); ok {
return strings.HasSuffix(segment, suffix)
}
// * in the middle: split and check prefix + suffix
if prefix, suffix, ok := strings.Cut(pattern, "*"); ok {
return strings.HasPrefix(segment, prefix) && strings.HasSuffix(segment, suffix) && len(segment) >= len(prefix)+len(suffix)
}
return pattern == segment
}
// ShouldIncludePath evaluates path rules in order and returns whether the
// path should be included. First matching rule wins. If no rule matches,
// the path is included by default.
func ShouldIncludePath(path string, rules []PathRule) bool {
for _, rule := range rules {
if MatchPath(rule.Pattern, path) {
return rule.Action == PathRuleInclude
}
}
return true // default: include
}
// InternalPathRules returns the default rules for excluding framework
// introspection endpoints. Prepend these to user rules when
// ExcludeInternalPaths is enabled.
func InternalPathRules() []PathRule {
return []PathRule{
{Pattern: "/", Action: PathRuleExclude},
{Pattern: "/_farp/**", Action: PathRuleExclude},
{Pattern: "/_/**", Action: PathRuleExclude},
{Pattern: "/docs/**", Action: PathRuleExclude},
{Pattern: "/docs", Action: PathRuleExclude},
{Pattern: "/openapi*", Action: PathRuleExclude},
{Pattern: "/asyncapi*", Action: PathRuleExclude},
}
}
// BuildPathRules combines internal rules (if enabled) with user-defined rules.
// Internal rules are prepended so they run first.
func BuildPathRules(excludeInternal bool, userRules []PathRule) []PathRule {
if !excludeInternal {
return userRules
}
return append(InternalPathRules(), userRules...)
}