| // Copyright 2025 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // This file implements JSON Pointers. |
| // A JSON Pointer is a path that refers to one JSON value within another. |
| // If the path is empty, it refers to the root value. |
| // Otherwise, it is a sequence of slash-prefixed strings, like "/points/1/x", |
| // selecting successive properties (for JSON objects) or items (for JSON arrays). |
| // For example, when applied to this JSON value: |
| // { |
| // "points": [ |
| // {"x": 1, "y": 2}, |
| // {"x": 3, "y": 4} |
| // ] |
| // } |
| // |
| // the JSON Pointer "/points/1/x" refers to the number 3. |
| // See the spec at https://datatracker.ietf.org/doc/html/rfc6901. |
| |
| package jsonschema |
| |
| import ( |
| "errors" |
| "fmt" |
| "reflect" |
| "strconv" |
| "strings" |
| |
| "golang.org/x/tools/internal/mcp/internal/util" |
| ) |
| |
| var ( |
| jsonPointerEscaper = strings.NewReplacer("~", "~0", "/", "~1") |
| jsonPointerUnescaper = strings.NewReplacer("~0", "~", "~1", "/") |
| ) |
| |
| func escapeJSONPointerSegment(s string) string { |
| return jsonPointerEscaper.Replace(s) |
| } |
| |
| func unescapeJSONPointerSegment(s string) string { |
| return jsonPointerUnescaper.Replace(s) |
| } |
| |
| // parseJSONPointer splits a JSON Pointer into a sequence of segments. It doesn't |
| // convert strings to numbers, because that depends on the traversal: a segment |
| // is treated as a number when applied to an array, but a string when applied to |
| // an object. See section 4 of the spec. |
| func parseJSONPointer(ptr string) (segments []string, err error) { |
| if ptr == "" { |
| return nil, nil |
| } |
| if ptr[0] != '/' { |
| return nil, fmt.Errorf("JSON Pointer %q does not begin with '/'", ptr) |
| } |
| // Unlike file paths, consecutive slashes are not coalesced. |
| // Split is nicer than Cut here, because it gets a final "/" right. |
| segments = strings.Split(ptr[1:], "/") |
| if strings.Contains(ptr, "~") { |
| // Undo the simple escaping rules that allow one to include a slash in a segment. |
| for i := range segments { |
| segments[i] = unescapeJSONPointerSegment(segments[i]) |
| } |
| } |
| return segments, nil |
| } |
| |
| // dereferenceJSONPointer returns the Schema that sptr points to within s, |
| // or an error if none. |
| // This implementation suffices for JSON Schema: pointers are applied only to Schemas, |
| // and refer only to Schemas. |
| func dereferenceJSONPointer(s *Schema, sptr string) (_ *Schema, err error) { |
| defer util.Wrapf(&err, "JSON Pointer %q", sptr) |
| |
| segments, err := parseJSONPointer(sptr) |
| if err != nil { |
| return nil, err |
| } |
| v := reflect.ValueOf(s) |
| for _, seg := range segments { |
| switch v.Kind() { |
| case reflect.Pointer: |
| v = v.Elem() |
| if !v.IsValid() { |
| return nil, errors.New("navigated to nil reference") |
| } |
| fallthrough // if valid, can only be a pointer to a Schema |
| |
| case reflect.Struct: |
| // The segment must refer to a field in a Schema. |
| if v.Type() != reflect.TypeFor[Schema]() { |
| return nil, fmt.Errorf("navigated to non-Schema %s", v.Type()) |
| } |
| v = lookupSchemaField(v, seg) |
| if !v.IsValid() { |
| return nil, fmt.Errorf("no schema field %q", seg) |
| } |
| case reflect.Slice, reflect.Array: |
| // The segment must be an integer without leading zeroes that refers to an item in the |
| // slice or array. |
| if seg == "-" { |
| return nil, errors.New("the JSON Pointer array segment '-' is not supported") |
| } |
| if len(seg) > 1 && seg[0] == '0' { |
| return nil, fmt.Errorf("segment %q has leading zeroes", seg) |
| } |
| n, err := strconv.Atoi(seg) |
| if err != nil { |
| return nil, fmt.Errorf("invalid int: %q", seg) |
| } |
| if n < 0 || n >= v.Len() { |
| return nil, fmt.Errorf("index %d is out of bounds for array of length %d", n, v.Len()) |
| } |
| v = v.Index(n) |
| // Cannot be invalid. |
| case reflect.Map: |
| // The segment must be a key in the map. |
| v = v.MapIndex(reflect.ValueOf(seg)) |
| if !v.IsValid() { |
| return nil, fmt.Errorf("no key %q in map", seg) |
| } |
| default: |
| return nil, fmt.Errorf("value %s (%s) is not a schema, slice or map", v, v.Type()) |
| } |
| } |
| if s, ok := v.Interface().(*Schema); ok { |
| return s, nil |
| } |
| return nil, fmt.Errorf("does not refer to a schema, but to a %s", v.Type()) |
| } |
| |
| // lookupSchemaField returns the value of the field with the given name in v, |
| // or the zero value if there is no such field or it is not of type Schema or *Schema. |
| func lookupSchemaField(v reflect.Value, name string) reflect.Value { |
| if name == "type" { |
| // The "type" keyword may refer to Type or Types. |
| // At most one will be non-zero. |
| if t := v.FieldByName("Type"); !t.IsZero() { |
| return t |
| } |
| return v.FieldByName("Types") |
| } |
| if sf, ok := schemaFieldMap[name]; ok { |
| return v.FieldByIndex(sf.Index) |
| } |
| return reflect.Value{} |
| } |