package pathmatcher import ( "strings" "sync" ) type MountedPath struct { path string name string stripPath bool } func NewMountedPath(path string, opts ...MountedPathOption) *MountedPath { // Normalize: remove trailing slash (except for root) normalizedPath := strings.TrimSuffix(path, "/") if normalizedPath == "" { normalizedPath = "" } m := &MountedPath{ path: normalizedPath, name: normalizedPath, stripPath: true, } for _, opt := range opts { opt(m) } if m.name == "" { m.name = normalizedPath } return m } type MountedPathOption func(*MountedPath) func WithName(name string) MountedPathOption { return func(m *MountedPath) { m.name = name } } func WithStripPath(strip bool) MountedPathOption { return func(m *MountedPath) { m.stripPath = strip } } func (m *MountedPath) Path() string { return m.path } func (m *MountedPath) Name() string { return m.name } func (m *MountedPath) StripPath() bool { return m.stripPath } func (m *MountedPath) Matches(requestPath string) bool { // Empty or "/" mount matches everything if m.path == "" || m.path == "/" { return true } // Request path must be at least as long as mount path if len(requestPath) < len(m.path) { return false } // Check if request path starts with mount path if !strings.HasPrefix(requestPath, m.path) { return false } // If paths are equal length, it's a match if len(requestPath) == len(m.path) { return true } // Otherwise, next char must be '/' to prevent /api matching /api-v2 return requestPath[len(m.path)] == '/' } func (m *MountedPath) GetModifiedPath(requestPath string) string { if !m.stripPath { return requestPath } // Root mount doesn't strip anything if m.path == "" || m.path == "/" { return requestPath } // Strip the prefix modified := strings.TrimPrefix(requestPath, m.path) // Ensure result starts with / if modified == "" || modified[0] != '/' { modified = "/" + modified } return modified } type MountManager struct { mounts []*MountedPath mu sync.RWMutex } func NewMountManager() *MountManager { return &MountManager{ mounts: make([]*MountedPath, 0), } } func (mm *MountManager) AddMount(mount *MountedPath) { mm.mu.Lock() defer mm.mu.Unlock() // Insert in sorted order (longer paths first) inserted := false for i, existing := range mm.mounts { if len(mount.path) > len(existing.path) { // Insert at position i mm.mounts = append(mm.mounts[:i], append([]*MountedPath{mount}, mm.mounts[i:]...)...) inserted = true break } } if !inserted { mm.mounts = append(mm.mounts, mount) } } func (mm *MountManager) RemoveMount(path string) bool { mm.mu.Lock() defer mm.mu.Unlock() normalizedPath := strings.TrimSuffix(path, "/") for i, mount := range mm.mounts { if mount.path == normalizedPath { mm.mounts = append(mm.mounts[:i], mm.mounts[i+1:]...) return true } } return false } func (mm *MountManager) GetMount(requestPath string) *MountedPath { mm.mu.RLock() defer mm.mu.RUnlock() // Mounts are sorted by path length (longest first) // so the first match is the best match for _, mount := range mm.mounts { if mount.Matches(requestPath) { return mount } } return nil } func (mm *MountManager) MountCount() int { mm.mu.RLock() defer mm.mu.RUnlock() return len(mm.mounts) } func (mm *MountManager) Mounts() []*MountedPath { mm.mu.RLock() defer mm.mu.RUnlock() result := make([]*MountedPath, len(mm.mounts)) copy(result, mm.mounts) return result } func (mm *MountManager) ListMounts() []map[string]interface{} { mm.mu.RLock() defer mm.mu.RUnlock() result := make([]map[string]interface{}, len(mm.mounts)) for i, mount := range mm.mounts { result[i] = map[string]interface{}{ "path": mount.path, "name": mount.name, "strip_path": mount.stripPath, } } return result } // Utility functions func PathMatchesPrefix(requestPath, prefix string) bool { // Normalize prefix prefix = strings.TrimSuffix(prefix, "/") // Empty or "/" prefix matches everything if prefix == "" || prefix == "/" { return true } // Request path must be at least as long as prefix if len(requestPath) < len(prefix) { return false } // Check if request path starts with prefix if !strings.HasPrefix(requestPath, prefix) { return false } // If paths are equal length, it's a match if len(requestPath) == len(prefix) { return true } // Otherwise, next char must be '/' return requestPath[len(prefix)] == '/' } func StripPathPrefix(requestPath, prefix string) string { // Normalize prefix prefix = strings.TrimSuffix(prefix, "/") // Empty or "/" prefix doesn't strip anything if prefix == "" || prefix == "/" { return requestPath } // Strip the prefix modified := strings.TrimPrefix(requestPath, prefix) // Ensure result starts with / if modified == "" || modified[0] != '/' { modified = "/" + modified } return modified } func MatchAndModifyPath(requestPath, prefix string, stripPath bool) (matches bool, modifiedPath string) { if !PathMatchesPrefix(requestPath, prefix) { return false, "" } if stripPath { return true, StripPathPrefix(requestPath, prefix) } return true, requestPath }