package engine import ( "errors" "strconv" "strings" ) // Path identifies a configuration value using YAML field names. Its textual form // is a JSON Pointer: the root is empty, ~ escapes as ~0, and / escapes as ~1. // Paths are semantic rather than filesystem or URL paths: empty, dot, dot-dot, // percent-containing, and numeric keys retain their literal meaning. Parsed // numeric tokens are resolved only when traversal knows their containing type. // // Paths are immutable. The zero value is the root path. type Path struct { segments []pathSegment invalid bool } type pathSegmentKind uint8 const ( pathSegmentKey pathSegmentKind = iota pathSegmentIndex pathSegmentParsed ) type pathSegment struct { kind pathSegmentKind key string index int } // Keys constructs a path of mapping keys. Keys with no arguments returns the // root path. func Keys(keys ...string) Path { p := Path{} for _, key := range keys { p = p.Key(key) } return p } // ParsePath parses a JSON Pointer. Numeric tokens remain unresolved until schema // or document traversal determines whether the containing value is a mapping or // sequence. func ParsePath(pointer string) (Path, error) { if pointer == "" { return Path{}, nil } if pointer[0] != '/' { return Path{}, errors.New("configkit: path must be empty or begin with slash") } parts := strings.Split(pointer[1:], "/") segments := make([]pathSegment, 0, len(parts)) for _, part := range parts { key, ok := unescapePathToken(part) if !ok { return Path{}, errors.New("configkit: path contains an invalid tilde escape") } segments = append(segments, pathSegment{kind: pathSegmentParsed, key: key}) } return Path{segments: segments}, nil } // Key returns a new path with key appended as a mapping-key segment. func (p Path) Key(key string) Path { segments := clonePathSegments(p.segments, 1) segments = append(segments, pathSegment{kind: pathSegmentKey, key: key}) return Path{segments: segments, invalid: p.invalid} } // Index returns a new path with index appended as a sequence-index segment. // A negative index produces an invalid path that configuration operations reject. func (p Path) Index(index int) Path { segments := clonePathSegments(p.segments, 1) segments = append(segments, pathSegment{kind: pathSegmentIndex, index: index}) return Path{segments: segments, invalid: p.invalid || index < 0} } // String returns the path's canonical JSON Pointer spelling. func (p Path) String() string { if len(p.segments) == 0 { return "" } // Include each separator and the extra byte introduced by escaping a token. // Use stack storage for sizing indexes without allocating another string. size := len(p.segments) var indexText [20]byte for _, segment := range p.segments { if segment.kind == pathSegmentIndex { size += len(strconv.AppendInt(indexText[:0], int64(segment.index), 10)) } else { size += len(segment.key) + strings.Count(segment.key, "~") + strings.Count(segment.key, "/") } } var b strings.Builder b.Grow(size) for _, segment := range p.segments { b.WriteByte('/') if segment.kind == pathSegmentIndex { b.WriteString(strconv.Itoa(segment.index)) continue } b.WriteString(escapePathToken(segment.key)) } return b.String() } func (p Path) clone() Path { return Path{segments: clonePathSegments(p.segments, 0), invalid: p.invalid} } func (p Path) valid() bool { return !p.invalid } func clonePathSegments(segments []pathSegment, extra int) []pathSegment { if len(segments) == 0 && extra == 0 { return nil } cloned := make([]pathSegment, len(segments), len(segments)+extra) copy(cloned, segments) return cloned } func escapePathToken(token string) string { token = strings.ReplaceAll(token, "~", "~0") return strings.ReplaceAll(token, "/", "~1") } func unescapePathToken(token string) (string, bool) { if !strings.Contains(token, "~") { return token, true } var b strings.Builder for i := 0; i < len(token); i++ { if token[i] != '~' { b.WriteByte(token[i]) continue } if i+1 >= len(token) { return "", false } i++ switch token[i] { case '0': b.WriteByte('~') case '1': b.WriteByte('/') default: return "", false } } return b.String(), true }