From 9a1dd22033ec915d921c2eefb2eaddfa0586f29b Mon Sep 17 00:00:00 2001 From: Kareem Hepburn Date: Tue, 20 Aug 2024 15:15:52 -0500 Subject: [PATCH] feat(bed-4728): Add jsonpointer package as core component for JSON schema validation This commit introduces a new `jsonpointer` package that provides utilities for working with JSON Pointers as defined in RFC 6901. This package serves as a precursor and core component for our upcoming JSON schema validator package, laying the groundwork for efficient JSON traversal and manipulation. Key features: - Implements a `WalkJSON` function for traversing JSON-like structures - Supports map[string]any, []any, and uses reflection for other types - Provides a visitor pattern for custom handling of each element - Includes comprehensive documentation and usage examples - Offers extensibility for future enhancements, particularly for JSON schema validation The 'jsonpointer' package is designed with a clear goal in mind: to balance performance with flexibility. It provides fast paths for common JSON types while supporting complex nested structures. This balance is not just a feature, but a promise of efficiency and adaptability, crucial for implementing efficient JSON schema validation. Usage example: ```go data := map[string]any{ "foo": []any{1, 2, 3}, "bar": map[string]any{"baz": "qux"}, } err := WalkJSON(data, func(elem any) error { fmt.Printf("Visited: %v\n", elem) return nil }) ``` REFERENCE: https://specterops.atlassian.net/browse/BED-4728 --- go.work | 1 + packages/go/jsonpointer/README.md | 95 +++ packages/go/jsonpointer/go.mod | 3 + packages/go/jsonpointer/pointer.go | 618 ++++++++++++++++++ .../go/jsonpointer/pointer_benchmark_test.go | 191 ++++++ packages/go/jsonpointer/pointer_test.go | 308 +++++++++ packages/go/jsonpointer/traversal.go | 151 +++++ packages/go/jsonpointer/traversal_test.go | 223 +++++++ 8 files changed, 1590 insertions(+) create mode 100644 packages/go/jsonpointer/README.md create mode 100644 packages/go/jsonpointer/go.mod create mode 100644 packages/go/jsonpointer/pointer.go create mode 100644 packages/go/jsonpointer/pointer_benchmark_test.go create mode 100644 packages/go/jsonpointer/pointer_test.go create mode 100644 packages/go/jsonpointer/traversal.go create mode 100644 packages/go/jsonpointer/traversal_test.go diff --git a/go.work b/go.work index c5077a96c0..98380e61f9 100644 --- a/go.work +++ b/go.work @@ -28,6 +28,7 @@ use ( ./packages/go/errors ./packages/go/graphschema ./packages/go/headers + ./packages/go/jsonpointer ./packages/go/lab ./packages/go/log ./packages/go/mediatypes diff --git a/packages/go/jsonpointer/README.md b/packages/go/jsonpointer/README.md new file mode 100644 index 0000000000..8c17f77160 --- /dev/null +++ b/packages/go/jsonpointer/README.md @@ -0,0 +1,95 @@ +# jsonpointer + +## Description + +The `jsonpointer` package provides utilities for working with JSON Pointers as defined in [IETF RFC6901](https://tools.ietf.org/html/rfc6901). It allows parsing, evaluating, and manipulating JSON Pointers, which are strings that identify specific values within a JSON document. + +Key features: + +- Parse JSON Pointers from strings, including those embedded in URLs +- Evaluate JSON Pointers against JSON documents +- Create and manipulate JSON Pointers programmatically +- Validate JSON Pointer strings +- Traverse JSON documents using JSON Pointers + +## Usage + +### Parsing JSON Pointers + +```go +import "github.com/bloodhound/jsonpointer" + +// Parse a simple JSON Pointer +ptr, err := jsonpointer.Parse("/foo/bar") +if err != nil { + // Handle error +} + +// Parse a JSON Pointer from a URL +urlPtr, err := jsonpointer.Parse("http://example.com/data#/foo/bar") +if err != nil { + // Handle error +} +``` + +### Evaluating JSON Pointers + +```go +doc := map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": []interface{}{0, "hello!"}, + }, +} + +ptr, _ := jsonpointer.Parse("/foo/bar/1") +result, err := ptr.Eval(doc) +if err != nil { + // Handle error +} +fmt.Println(result) // Output: hello! +``` + +### Creating Pointers Programmatically + +```go +builder := jsonpointer.NewPointerBuilder() +ptr := builder.AddToken("foo").AddToken("bar").AddToken("0").Build() +fmt.Println(ptr.String()) // Output: /foo/bar/0 +``` + +### Validating JSON Pointer Strings + +```go +fmt.Println(jsonpointer.Validate("/foo/bar")) // Output: true +fmt.Println(jsonpointer.Validate("foo/bar")) // Output: false +fmt.Println(jsonpointer.Validate("/foo~bar")) // Output: false +``` + +### Traversing JSON Documents + +```go +doc := map[string]interface{}{ + "foo": []interface{}{1, 2, 3}, + "bar": map[string]interface{}{"baz": "qux"}, +} + +err := jsonpointer.WalkJSON(doc, func(elem interface{}) error { + fmt.Printf("Visited: %v\n", elem) + return nil +}) +if err != nil { + // Handle error +} +``` + +### Checking Pointer Relationships + +```go +p1, _ := jsonpointer.Parse("/foo") +p2, _ := jsonpointer.Parse("/foo/bar") + +fmt.Println(p1.IsPrefix(p2)) // Output: true + +root, _ := jsonpointer.Parse("/") +fmt.Println(root.IsPrefix(p2)) // Output: true +``` diff --git a/packages/go/jsonpointer/go.mod b/packages/go/jsonpointer/go.mod new file mode 100644 index 0000000000..65f6937fd3 --- /dev/null +++ b/packages/go/jsonpointer/go.mod @@ -0,0 +1,3 @@ +module github.com/bloodhound/jsonpointer + +go 1.21 diff --git a/packages/go/jsonpointer/pointer.go b/packages/go/jsonpointer/pointer.go new file mode 100644 index 0000000000..4d716dd254 --- /dev/null +++ b/packages/go/jsonpointer/pointer.go @@ -0,0 +1,618 @@ +// Package jsonpointer provides utilities for working with JSON Pointers as defined in RFC 6901. +// It allows parsing, evaluating, and manipulating JSON Pointers, which are strings that identify +// specific values within a JSON document. +// +// The implementation focuses on efficiency, flexibility, and ease of use. It aims to provide +// a balance between performance and maintainability, with clear error handling and +// extensibility for future enhancements. +package jsonpointer + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" +) + +// Unexported constants used for consistent error messaging across the package. +// Using constants for error messages ensures consistency and makes it easier +// to update messages in the future if needed. +const ( + errMsgInvalidStart = "non-empty references must begin with a '/' character" + errMsgInvalidArrayIndex = "invalid array index: %s" + errMsgIndexOutOfBounds = "index %d exceeds array length of %d" + errMsgKeyNotFound = "key not found: '%s' in pointer: %s for JSON resource" +) + +// Unexported constants used for escaping and unescaping JSON Pointer tokens. +// These constants are defined according to RFC 6901 specifications. +const ( + separator = "/" + escapedSeparator = "~1" + tilde = "~" + escapedTilde = "~0" +) + +// ErrInvalidPointer represents an error that occurs when a pointer is invalid. +// It encapsulates the reason for the invalidity, providing more context than a simple error string. +// This custom error type allows for more specific error handling by users of the package. +type ErrInvalidPointer struct { + Reason string +} + +func (e ErrInvalidPointer) Error() string { + return fmt.Sprintf("invalid pointer: %s", e.Reason) +} + +// Pointer represents a parsed JSON pointer as a slice of reference tokens. +// We use a slice of strings instead of a single string to avoid repeated parsing and splitting +// during pointer evaluation, improving performance for repeated use of the same pointer. +type Pointer []string + +// Parse parses str into a Pointer structure. It handles both plain JSON Pointer strings +// and URL strings containing JSON Pointers. This flexibility allows users to work with +// JSON Pointers in various contexts, including within URLs, which is a common use case +// in web applications. +// +// The function first checks for common cases ("" and "#") to avoid unnecessary processing. +// It then checks if the string starts with '/', which indicates a plain JSON Pointer. +// If not, it attempts to parse as a URL and extract the fragment. This approach balances +// efficiency (fast-path for common cases) with flexibility (handling URLs). +// +// Returns: +// - A Pointer structure representing the parsed JSON Pointer. +// - An error if the input string is invalid or cannot be parsed. +// +// Usage: +// +// ptr, err := Parse("/foo/0/bar") +// if err != nil { +// // Handle error +// } +// // Use ptr... +// +// urlPtr, err := Parse("http://example.com/data#/foo/bar") +// if err != nil { +// // Handle error +// } +// // Use urlPtr... +func Parse(str string) (Pointer, error) { + // Fast paths that skip URL parse step + if len(str) == 0 || str == "#" { + return Pointer{}, nil + } else if str[0] == '/' { + return parse(str) + } + + u, err := url.Parse(str) + if err != nil { + return nil, err + } + return parse(u.Fragment) +} + +// parse is an unexported function that converts a string into a Pointer after the initial '/' character. +// It combines splitting and unescaping in a single pass for efficiency. +// +// Rationale: +// - Single-pass parsing reduces allocations and improves performance, especially for long pointers. +// - The function handles escaping inline, avoiding the need for a separate unescaping step. +// - It uses a strings.Builder for efficient string construction, minimizing allocations. +// +// Parameters: +// - str: The string to parse, without the initial '/' character. +// +// Returns: +// - A Pointer representing the parsed string. +// - An error if the string is invalid according to the JSON Pointer syntax. +func parse(str string) (Pointer, error) { + if len(str) == 0 { + return Pointer{}, nil + } + if str[0] != '/' { + return nil, ErrInvalidPointer{errMsgInvalidStart} + } + + var tokens []string + var token strings.Builder + for i := 1; i < len(str); i++ { + switch str[i] { + case '/': + tokens = append(tokens, token.String()) + token.Reset() + case '~': + if i+1 < len(str) { + switch str[i+1] { + case '0': + token.WriteByte('~') + case '1': + token.WriteByte('/') + default: + token.WriteByte('~') + token.WriteByte(str[i+1]) + } + i++ + } else { + token.WriteByte('~') + } + default: + token.WriteByte(str[i]) + } + } + tokens = append(tokens, token.String()) + return Pointer(tokens), nil +} + +// String implements the fmt.Stringer interface for Pointer, +// returning the escaped string representation of the JSON Pointer. +// +// Rationale: +// - Using strings.Builder for efficient string construction. +// - Escaping each token individually ensures correct handling of special characters. +// - Implementing Stringer allows seamless use with fmt package and other standard library functions. +// +// Returns: +// - A string representation of the Pointer. +// +// Usage: +// +// ptr, _ := Parse("/foo/bar~0baz") +// fmt.Println(ptr.String()) // Output: /foo/bar~0baz +func (p Pointer) String() string { + var sb strings.Builder + for _, tok := range p { + sb.WriteString("/") + sb.WriteString(escapeToken(tok)) + } + return sb.String() +} + +// escapeToken is an unexported function that applies the escaping process to a single token. +// This function is crucial for ensuring that the string representation +// of a Pointer is valid according to the JSON Pointer specification. +// +// Rationale: +// - Using strings.Replace for clarity and simplicity. +// - Escaping '~' before '/' to handle cases where both characters appear. +// +// Parameters: +// - unescapedToken: The token to escape. +// +// Returns: +// - The escaped token string. +func escapeToken(unescapedToken string) string { + return strings.Replace( + strings.Replace(unescapedToken, tilde, escapedTilde, -1), + separator, escapedSeparator, -1, + ) +} + +// IsEmpty checks if the Pointer is empty (i.e., has no reference tokens). +// This method is useful for quickly determining if a Pointer references the root of a document. +// +// Returns: +// - true if the pointer is empty, false otherwise. +// +// Usage: +// +// ptr := Pointer{} +// fmt.Println(ptr.IsEmpty()) // Output: true +func (p Pointer) IsEmpty() bool { + return len(p) == 0 +} + +// Head returns the first reference token of the Pointer. +// This method is useful for algorithms that need to process a Pointer token by token. +// +// Rationale: +// - Returns a pointer to the string to avoid unnecessary copying. +// - Returns nil for empty Pointers to allow for easy null checks. +// +// Returns: +// - A pointer to the first token string, or nil if the Pointer is empty. +// +// Usage: +// +// ptr, _ := Parse("/foo/bar") +// head := ptr.Head() +// fmt.Println(*head) // Output: foo +func (p Pointer) Head() *string { + if len(p) == 0 { + return nil + } + return &p[0] +} + +// Tail returns a new Pointer containing all reference tokens except the first. +// This method complements Head(), allowing for recursive processing of Pointers. +// +// Rationale: +// - Creates a new slice to avoid modifying the original Pointer. +// - Efficient for most use cases, as it's a simple slice operation. +// +// Returns: +// - A new Pointer with all but the first token. +// +// Usage: +// +// ptr, _ := Parse("/foo/bar/baz") +// tail := ptr.Tail() +// fmt.Println(tail.String()) // Output: /bar/baz +func (p Pointer) Tail() Pointer { + return Pointer(p[1:]) +} + +// Eval evaluates the Pointer against a given JSON document. +// It traverses the document according to the reference tokens in the Pointer, +// handling both nested objects and arrays. +// +// Rationale: +// - Uses a loop to process each token, allowing for arbitrary nesting levels. +// - Delegates token evaluation to evalToken for type-specific handling. +// - Returns early on any error to prevent invalid traversal. +// +// Parameters: +// - data: The root JSON document to evaluate against. +// +// Returns: +// - The value referenced by the Pointer within the document. +// - An error if the Pointer cannot be resolved within the document. +// +// Usage: +// +// doc := map[string]interface{}{ +// "foo": map[string]interface{}{ +// "bar": map[string]interface{}{ +// "baz": []interface{}{0, "hello!"}, +// }, +// }, +// } +// ptr, _ := Parse("/foo/bar/baz/1") +// result, err := ptr.Eval(doc) +// if err != nil { +// // Handle error +// } +// fmt.Println(result) // Output: hello! +func (p Pointer) Eval(data any) (any, error) { + result := data + var err error + for _, token := range p { + result, err = p.evalToken(token, result) + if err != nil { + return nil, err + } + } + return result, nil +} + +// evalToken is an unexported method that evaluates a single token against the current data. +// It determines the type of the current data and calls the appropriate helper function. +// +// Rationale: +// - Uses a type switch for efficient handling of different data types. +// - Separates concerns by delegating to type-specific evaluation methods. +// +// Parameters: +// - token: The token to evaluate. +// - data: The current data being traversed. +// +// Returns: +// - The result of evaluating the token against the data. +// - An error if the token cannot be evaluated. +func (p Pointer) evalToken(token string, data any) (any, error) { + switch typedData := data.(type) { + case map[string]any: + return p.evalMap(token, typedData) + case []any: + return p.evalArray(token, typedData) + default: + return nil, fmt.Errorf("invalid token %s for type %T", token, data) + } +} + +// evalMap is an unexported method that evaluates a single token against a map. +// +// Rationale: +// - Directly accesses map keys for efficiency. +// - Returns a descriptive error if the key is not found. +// +// Parameters: +// - token: The token to evaluate. +// - data: The map to evaluate against. +// +// Returns: +// - The value associated with the token in the map. +// - An error if the token is not found in the map. +func (p Pointer) evalMap(token string, data map[string]any) (any, error) { + if v, ok := data[token]; ok { + return v, nil + } + return nil, fmt.Errorf(errMsgKeyNotFound, token, p.String()) +} + +// evalArray is an unexported method that evaluates a single token against an array. +// +// This updated version provides more detailed error messages, distinguishing between +// different types of invalid access attempts. +// +// Parameters: +// - token: The token to evaluate (should be a valid array index). +// - data: The array to evaluate against. +// +// Returns: +// - The value at the specified index in the array. +// - An error if the token is not a valid index, is out of bounds, or if trying to access an object property. +func (p Pointer) evalArray(token string, data []any) (any, error) { + // Check if the token is a valid integer + i, err := strconv.Atoi(token) + if err != nil { + // If it's not a valid integer, it could be an attempt to access an object property + return nil, fmt.Errorf("invalid array access: token '%s' is not a valid array index", token) + } + + // Check if the index is negative + if i < 0 { + return nil, fmt.Errorf("invalid array index: %d is negative", i) + } + + // Check if the index is out of bounds + if i >= len(data) { + return nil, fmt.Errorf("array index out of bounds: index %d exceeds array length of %d", i, len(data)) + } + + return data[i], nil +} + +// MarshalJSON implements the json.Marshaler interface. +// This allows Pointer values to be directly marshaled as JSON strings. +// +// Rationale: +// - Simplifies serialization of Pointers in JSON contexts. +// - Uses the String() method to ensure correct escaping. +// +// Returns: +// - The JSON representation of the Pointer as a byte slice. +// - An error if marshaling fails. +// +// Usage: +// +// ptr, _ := Parse("/foo/bar") +// jsonData, err := json.Marshal(ptr) +// if err != nil { +// // Handle error +// } +// fmt.Println(string(jsonData)) // Output: "/foo/bar" +func (p Pointer) MarshalJSON() ([]byte, error) { + return json.Marshal(p.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// This allows Pointer values to be directly unmarshaled from JSON strings. +// +// Rationale: +// - Complements MarshalJSON for complete JSON serialization support. +// - Uses the Parse function to ensure correct interpretation of the JSON string. +// +// Parameters: +// - data: The JSON data to unmarshal. +// +// Returns: +// - An error if unmarshaling or parsing fails. +// +// Usage: +// +// var ptr Pointer +// err := json.Unmarshal([]byte(`"/foo/bar"`), &ptr) +// if err != nil { +// // Handle error +// } +// fmt.Println(ptr.String()) // Output: /foo/bar +func (p *Pointer) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + ptr, err := Parse(s) + if err != nil { + return err + } + *p = ptr + return nil +} + +// Validate checks if a given string is a valid JSON Pointer. +// This function provides a quick way to check pointer validity without fully parsing it. +// +// A valid JSON Pointer is either an empty string or a string that starts with a '/' character, +// followed by a sequence of reference tokens separated by '/'. Each reference token can contain +// any character except '/' and '~', which must be escaped as '~1' and '~0' respectively. +// +// Parameters: +// - s: The string to validate as a JSON Pointer. +// +// Returns: +// - true if the string is a valid JSON Pointer, false otherwise. +// +// Usage: +// +// fmt.Println(Validate("")) // Output: true +// fmt.Println(Validate("/foo/bar")) // Output: true +// fmt.Println(Validate("foo/bar")) // Output: false +// fmt.Println(Validate("/foo~bar")) // Output: false +func Validate(s string) bool { + if s == "" { + return true + } + if !strings.HasPrefix(s, "/") { + return false + } + tokens := strings.Split(s[1:], "/") + for _, token := range tokens { + if strings.ContainsRune(token, '~') { + if strings.ContainsRune(token, '~') && !strings.Contains(token, "~0") && !strings.Contains(token, "~1") { + return false + } + if strings.HasSuffix(token, "~") { + return false + } + } + } + return true +} + +// Equals checks if two Pointers are equal. +// Two Pointers are considered equal if they have the same tokens in the same order. +// +// Rationale: +// - Compares length first as a quick check before comparing individual tokens. +// - Direct slice comparison is more efficient than converting to strings. +func (p Pointer) Equals(other Pointer) bool { + if len(p) != len(other) { + return false + } + for i, v := range p { + if v != other[i] { + return false + } + } + return true +} + +// IsPrefix checks if the Pointer is a prefix of another Pointer. +// This is useful for determining hierarchical relationships between Pointers. +// Both the empty pointer and the root pointer ("/") are considered prefixes of all other pointers. +// +// Parameters: +// - other: The other Pointer to check against. +// +// Returns: +// - true if this Pointer is a prefix of the other Pointer, false otherwise. +// +// Usage: +// +// p1, _ := Parse("/foo") +// p2, _ := Parse("/foo/bar") +// fmt.Println(p1.IsPrefix(p2)) // Output: true +// +// root, _ := Parse("/") +// fmt.Println(root.IsPrefix(p2)) // Output: true +// +// empty, _ := Parse("") +// fmt.Println(empty.IsPrefix(p2)) // Output: true +func (p Pointer) IsPrefix(other Pointer) bool { + // The empty pointer and the root pointer are prefixes of all pointers + if len(p) == 0 || (len(p) == 1 && p[0] == "") { + return true + } + + if len(p) > len(other) { + return false + } + for i, v := range p { + if v != other[i] { + return false + } + } + return true +} + +// PointerBuilder provides a builder pattern for constructing Pointers. +// This struct allows for more intuitive and flexible Pointer creation, +// especially when building Pointers programmatically. +type PointerBuilder struct { + tokens []string +} + +// NewPointerBuilder creates a new PointerBuilder. +// It initializes the token slice with a small capacity to balance +// memory usage and potential growth. +// +// Rationale: +// - Initial capacity of 8 is a reasonable default for most use cases. +// - Using a builder pattern allows for cleaner, more readable Pointer construction. +func NewPointerBuilder() *PointerBuilder { + return &PointerBuilder{tokens: make([]string, 0, 8)} +} + +// AddToken adds a token to the Pointer being built. +// It returns the PointerBuilder to allow for method chaining. +// +// Rationale: +// - Method chaining provides a fluent interface for building Pointers. +// - No validation is done at this stage for performance; validation occurs in Build(). +func (pb *PointerBuilder) AddToken(token string) *PointerBuilder { + pb.tokens = append(pb.tokens, token) + return pb +} + +// Build constructs the final Pointer. +// This method performs no additional validation, assuming that the +// individual tokens have been properly formatted. +// +// Rationale: +// - Simple conversion from the internal slice to a Pointer type. +// - No additional allocations are needed, promoting efficiency. +func (pb *PointerBuilder) Build() Pointer { + return Pointer(pb.tokens) +} + +// Descendant returns a new pointer to a descendant of the current pointer +// by parsing the input path into components and appending them to the current pointer. +// This method handles both relative and absolute paths, as well as edge cases involving +// empty pointers and paths. +// +// The method follows these rules: +// 1. If the input path is absolute (starts with '/'), it creates a new pointer from that path. +// 2. If both the current pointer and input path are empty, it returns the root pointer ("/"). +// 3. If the input path is empty, it returns the current pointer. +// 4. For relative paths, it appends the new path to the current pointer, adding a '/' if necessary. +// +// Parameters: +// - path: A string representation of the path to append or create. It can be a relative path +// (e.g., "foo/bar") or an absolute path (e.g., "/foo/bar"). +// +// Returns: +// - A new Pointer that is either a descendant of the current Pointer or a new absolute Pointer. +// - An error if the input path is invalid or cannot be parsed. +// +// Usage: +// +// ptr, _ := Parse("/foo") +// desc, err := ptr.Descendant("bar") +// if err != nil { +// // Handle error +// } +// fmt.Println(desc.String()) // Output: /foo/bar +// +// root, _ := Parse("/") +// abs, err := root.Descendant("/baz/qux") +// if err != nil { +// // Handle error +// } +// fmt.Println(abs.String()) // Output: /baz/qux +func (p Pointer) Descendant(path string) (Pointer, error) { + if strings.HasPrefix(path, "/") { + // If the path is absolute, create a new pointer from it + return Parse(path) + } + + // Handle the case where both p and path are empty + if p.IsEmpty() && path == "" { + return Parse("/") + } + + // For relative paths, append to the current pointer + if path == "" { + return p, nil + } + + // Construct the full path, adding a slash only if necessary + fullPath := p.String() + if fullPath != "/" && !strings.HasSuffix(fullPath, "/") { + fullPath += "/" + } + fullPath += path + + return Parse(fullPath) +} diff --git a/packages/go/jsonpointer/pointer_benchmark_test.go b/packages/go/jsonpointer/pointer_benchmark_test.go new file mode 100644 index 0000000000..ecca3541ea --- /dev/null +++ b/packages/go/jsonpointer/pointer_benchmark_test.go @@ -0,0 +1,191 @@ +package jsonpointer + +import ( + "encoding/json" + "fmt" + "math/rand" + "testing" +) + +// generateNestedJSON creates a nested JSON structure with the specified depth and breadth +func generateNestedJSON(depth, breadth int) map[string]interface{} { + if depth == 0 { + return map[string]interface{}{"value": rand.Intn(100)} + } + result := make(map[string]interface{}) + for i := 0; i < breadth; i++ { + key := fmt.Sprintf("key%d", i) + result[key] = generateNestedJSON(depth-1, breadth) + } + return result +} + +// generateLargeJSON creates a large JSON structure with many top-level keys +func generateLargeJSON(size int) map[string]interface{} { + result := make(map[string]interface{}) + for i := 0; i < size; i++ { + key := fmt.Sprintf("key%d", i) + result[key] = rand.Intn(100) + } + return result +} + +// generateLargeArray creates a large JSON array +func generateLargeArray(size int) []interface{} { + result := make([]interface{}, size) + for i := 0; i < size; i++ { + result[i] = rand.Intn(100) + } + return result +} + +// BenchmarkParse benchmarks the Parse function with different input sizes +func BenchmarkParse(b *testing.B) { + benchmarks := []struct { + name string + input string + }{ + {"Short", "/foo/bar"}, + {"Medium", "/a/b/c/d/e/f/g/h/i/j"}, + {"Long", "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z"}, + {"ComplexEscaping", "/a~1b~1c~0d~0e~1f/g~0h~0i~1j~1k/l~0m~1n~0o~1p"}, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = Parse(bm.input) + } + }) + } +} + +// BenchmarkEval benchmarks the Eval function with different JSON structures +func BenchmarkEval(b *testing.B) { + benchmarks := []struct { + name string + json interface{} + pointers []string + }{ + { + name: "Shallow", + json: map[string]interface{}{ + "foo": "bar", + "baz": 42, + "qux": []interface{}{1, 2, 3}, + }, + pointers: []string{"/foo", "/baz", "/qux/2"}, + }, + { + name: "Deep", + json: generateNestedJSON(10, 2), + pointers: []string{"/key0/key1/key0/key1/key0/key1/key0/key1/key0/key1/value"}, + }, + { + name: "Wide", + json: generateLargeJSON(1000), + pointers: []string{"/key0", "/key500", "/key999"}, + }, + { + name: "LargeArray", + json: map[string]interface{}{"array": generateLargeArray(10000)}, + pointers: []string{"/array/0", "/array/5000", "/array/9999"}, + }, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + jsonBytes, _ := json.Marshal(bm.json) + var doc interface{} + _ = json.Unmarshal(jsonBytes, &doc) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, ptr := range bm.pointers { + p, _ := Parse(ptr) + _, _ = p.Eval(doc) + } + } + }) + } +} + +// BenchmarkDescendant benchmarks the Descendant function +func BenchmarkDescendant(b *testing.B) { + benchmarks := []struct { + name string + parent string + path string + }{ + {"ShortToShort", "/foo", "bar"}, + {"ShortToLong", "/foo", "bar/baz/qux/quux/corge/grault/garply"}, + {"LongToShort", "/foo/bar/baz/qux/quux/corge/grault", "garply"}, + {"LongToLong", "/a/b/c/d/e/f/g", "h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z"}, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + parent, _ := Parse(bm.parent) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = parent.Descendant(bm.path) + } + }) + } +} + +// BenchmarkPointerBuilder benchmarks the PointerBuilder +func BenchmarkPointerBuilder(b *testing.B) { + benchmarks := []struct { + name string + tokens []string + }{ + {"Few", []string{"foo", "bar", "baz"}}, + {"Many", []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"}}, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + pb := NewPointerBuilder() + for _, token := range bm.tokens { + pb.AddToken(token) + } + _ = pb.Build() + } + }) + } +} + +// BenchmarkWalkJSON benchmarks the WalkJSON function with different JSON structures +func BenchmarkWalkJSON(b *testing.B) { + benchmarks := []struct { + name string + json interface{} + }{ + {"Shallow", generateLargeJSON(100)}, + {"Deep", generateNestedJSON(10, 3)}, + {"Wide", generateLargeJSON(1000)}, + {"LargeArray", generateLargeArray(10000)}, + {"Complex", map[string]interface{}{ + "nested": generateNestedJSON(5, 3), + "large": generateLargeJSON(100), + "largeArray": generateLargeArray(1000), + }}, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + jsonBytes, _ := json.Marshal(bm.json) + var doc interface{} + _ = json.Unmarshal(jsonBytes, &doc) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = WalkJSON(doc, func(elem interface{}) error { + return nil + }) + } + }) + } +} diff --git a/packages/go/jsonpointer/pointer_test.go b/packages/go/jsonpointer/pointer_test.go new file mode 100644 index 0000000000..9bf6c745ce --- /dev/null +++ b/packages/go/jsonpointer/pointer_test.go @@ -0,0 +1,308 @@ +package jsonpointer + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var docBytes = []byte(`{ + "foo": ["bar", "baz"], + "zoo": { + "too": ["bar", "baz"], + "goo": { + "loo": ["bar", "baz"] + } + }, + "": 0, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + " ": 7, + "m~n": 8, + "deep": { + "nested": { + "object": { + "with": { + "array": [1, 2, 3, 4, 5] + } + } + } + }, + "empty": {}, + "emptyArray": [] +}`) + +func TestParse(t *testing.T) { + cases := []struct { + name string + raw string + parsed string + err string + }{ + {"Empty string", "", "", ""}, + {"Root", "#/", "/", ""}, + {"Simple property", "#/foo", "/foo", ""}, + {"Property with trailing slash", "#/foo/", "/foo/", ""}, + {"URL fragment", "https://example.com#/foo", "/foo", ""}, + {"Multiple segments", "#/foo/bar/baz", "/foo/bar/baz", ""}, + {"Empty segment", "#//", "//", ""}, + {"Escaped characters", "#/a~1b~1c~0d", "/a~1b~1c~0d", ""}, + {"URL encoded characters", "#/foo%20bar", "/foo bar", ""}, + {"Invalid URL", "://", "", "missing protocol scheme"}, + {"Invalid pointer", "#7", "", "non-empty references must begin with a '/' character"}, + {"Complex URL", "https://example.com/api/v1#/data/0/name", "/data/0/name", ""}, + {"Pointer with query params", "https://example.com/api?param=value#/foo", "/foo", ""}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, err := Parse(c.raw) + if c.err != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), c.err) + } else { + assert.NoError(t, err) + assert.Equal(t, c.parsed, got.String()) + } + }) + } +} + +func TestEval(t *testing.T) { + document := map[string]any{} + err := json.Unmarshal(docBytes, &document) + require.NoError(t, err) + + testCases := []struct { + name string + pointerStr string + expected any + err string + }{ + {"Root document", "", document, ""}, + {"Simple property", "/foo", document["foo"], ""}, + {"Array element", "/foo/0", "bar", ""}, + {"Nested object", "/zoo/too/0", "bar", ""}, + {"Deeply nested", "/zoo/goo/loo/0", "bar", ""}, + {"Empty key", "/", float64(0), ""}, + {"Key with escaped /", "/a~1b", float64(1), ""}, + {"Key with %", "/c%d", float64(2), ""}, + {"Key with ^", "/e^f", float64(3), ""}, + {"Key with |", "/g|h", float64(4), ""}, + {"Key with \\", "/i\\j", float64(5), ""}, + {"Key with \"", "/k\"l", float64(6), ""}, + {"Key with space", "/ ", float64(7), ""}, + {"Key with escaped ~", "/m~0n", float64(8), ""}, + {"URL fragment root", "#", document, ""}, + {"URL fragment property", "#/foo", document["foo"], ""}, + {"URL fragment array", "#/foo/0", "bar", ""}, + {"URL encoded /", "#/a~1b", float64(1), ""}, + {"URL encoded %", "#/c%25d", float64(2), ""}, + {"URL encoded ^", "#/e%5Ef", float64(3), ""}, + {"URL encoded |", "#/g%7Ch", float64(4), ""}, + {"URL encoded \\", "#/i%5Cj", float64(5), ""}, + {"URL encoded \"", "#/k%22l", float64(6), ""}, + {"URL encoded space", "#/%20", float64(7), ""}, + {"URL with fragment", "https://example.com#/m~0n", float64(8), ""}, + {"Deep nested array", "/deep/nested/object/with/array/4", float64(5), ""}, + {"Empty object", "/empty", map[string]any{}, ""}, + {"Empty array", "/emptyArray", []any{}, ""}, + {"Non-existent key", "/undefined", nil, "key not found: 'undefined' in pointer: /undefined for JSON resource"}, + {"Invalid array index (non-integer)", "/foo/bar", nil, "invalid array access: token 'bar' is not a valid array index"}, + {"Array index out of bounds", "/foo/3", nil, "array index out of bounds: index 3 exceeds array length of 2"}, + {"Non-existent nested key", "/bar/baz", nil, "key not found: 'bar' in pointer: /bar/baz for JSON resource"}, + {"Negative array index", "/foo/-1", nil, "invalid array index: -1 is negative"}, + {"Attempting object access on array", "/foo/object", nil, "invalid array access: token 'object' is not a valid array index"}, + } + + for _, c := range testCases { + t.Run(c.name, func(t *testing.T) { + ptr, err := Parse(c.pointerStr) + require.NoError(t, err) + + got, err := ptr.Eval(document) + if c.err != "" { + assert.Error(t, err) + assert.Equal(t, c.err, err.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, c.expected, got) + } + }) + } +} + +func TestDescendant(t *testing.T) { + cases := []struct { + name string + parent string + path string + parsed string + err string + }{ + {"Root to child", "/", "foo", "/foo", ""}, + {"Root to nested", "/", "foo/bar", "/foo/bar", ""}, + {"Append to existing path", "/foo", "bar", "/foo/bar", ""}, + {"Append multiple segments", "/foo", "bar/baz", "/foo/bar/baz", ""}, + {"Absolute path", "/foo", "/bar/baz", "/bar/baz", ""}, + {"Empty parent", "", "foo", "/foo", ""}, + {"Empty child", "/foo", "", "/foo", ""}, + {"Both empty", "", "", "/", ""}, + {"Parent with trailing slash", "/foo/", "bar", "/foo/bar", ""}, + {"Child with leading slash", "/foo", "/bar", "/bar", ""}, + {"Escaped characters", "/a~1b", "c~0d", "/a~1b/c~0d", ""}, + {"Root to root", "/", "/", "/", ""}, + {"Non-root to root", "/foo", "/", "/", ""}, + {"Multiple slashes in path", "/foo", "bar//baz", "/foo/bar//baz", ""}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + p, err := Parse(c.parent) + require.NoError(t, err) + + desc, err := p.Descendant(c.path) + if c.err != "" { + assert.Error(t, err) + assert.Equal(t, c.err, err.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, c.parsed, desc.String()) + } + }) + } +} + +func TestPointerBuilder(t *testing.T) { + t.Run("Build simple pointer", func(t *testing.T) { + pb := NewPointerBuilder() + ptr := pb.AddToken("foo").AddToken("bar").Build() + assert.Equal(t, "/foo/bar", ptr.String()) + }) + + t.Run("Build pointer with escaped characters", func(t *testing.T) { + pb := NewPointerBuilder() + ptr := pb.AddToken("a/b").AddToken("c~d").Build() + assert.Equal(t, "/a~1b/c~0d", ptr.String()) + }) + + t.Run("Build empty pointer", func(t *testing.T) { + pb := NewPointerBuilder() + ptr := pb.Build() + assert.Equal(t, "", ptr.String()) + }) +} + +func TestPointerEquality(t *testing.T) { + cases := []struct { + name string + pointer1 string + pointer2 string + equal bool + }{ + {"Equal pointers", "/foo/bar", "/foo/bar", true}, + {"Different pointers", "/foo/bar", "/foo/baz", false}, + {"Different lengths", "/foo", "/foo/bar", false}, + {"Empty pointers", "", "", true}, + {"Root pointers", "/", "/", true}, + {"Escaped characters", "/a~1b/c~0d", "/a~1b/c~0d", true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + p1, _ := Parse(c.pointer1) + p2, _ := Parse(c.pointer2) + assert.Equal(t, c.equal, p1.Equals(p2)) + }) + } +} + +func TestPointerIsPrefix(t *testing.T) { + cases := []struct { + name string + pointer1 string + pointer2 string + isPrefix bool + }{ + {"Is prefix", "/foo", "/foo/bar", true}, + {"Is not prefix", "/foo/bar", "/foo", false}, + {"Equal pointers", "/foo/bar", "/foo/bar", true}, + {"Empty is prefix of all", "", "/foo/bar", true}, + {"Root is prefix of all", "/", "/foo/bar", true}, + {"Root is prefix of root", "/", "/", true}, + {"Empty is prefix of root", "", "/", true}, + {"Root is prefix of empty", "/", "", true}, + {"Empty is prefix of empty", "", "", true}, + {"Different paths", "/foo/bar", "/baz/qux", false}, + {"Longer is not prefix", "/foo/bar/baz", "/foo/bar", false}, + {"Prefix with different end", "/foo/bar", "/foo/baz", false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + p1, err := Parse(c.pointer1) + assert.NoError(t, err) + p2, err := Parse(c.pointer2) + assert.NoError(t, err) + assert.Equal(t, c.isPrefix, p1.IsPrefix(p2), "Pointer1: %v, Pointer2: %v", p1, p2) + }) + } +} + +func TestJSONMarshaling(t *testing.T) { + cases := []struct { + name string + pointer string + }{ + {"Simple pointer", "/foo/bar"}, + {"Empty pointer", ""}, + {"Root pointer", "/"}, + {"Escaped characters", "/a~1b/c~0d"}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + p, _ := Parse(c.pointer) + marshaled, err := json.Marshal(p) + assert.NoError(t, err) + + var unmarshaled Pointer + err = json.Unmarshal(marshaled, &unmarshaled) + assert.NoError(t, err) + + assert.Equal(t, p, unmarshaled) + }) + } +} + +func TestValidate(t *testing.T) { + cases := []struct { + name string + input string + valid bool + }{ + {"Valid empty pointer", "", true}, + {"Valid root pointer", "/", true}, + {"Valid simple pointer", "/foo/bar", true}, + {"Valid escaped characters", "/a~1b/c~0d", true}, + {"Valid complex pointer", "/foo/bar~1baz~0qux/0", true}, + {"Invalid: no leading slash", "foo/bar", false}, + {"Invalid: unescaped ~", "/foo/bar~", false}, + {"Invalid: unescaped ~ in middle", "/foo/bar~baz", false}, + {"Invalid: incomplete escape", "/foo/bar~", false}, + {"Invalid: double slash", "/foo//bar", true}, // Note: This is actually valid per RFC 6901 + {"Invalid: non-ASCII characters", "/foo/bár", true}, // Note: This is actually valid per RFC 6901 + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + assert.Equal(t, c.valid, Validate(c.input), "Input: %s", c.input) + }) + } +} diff --git a/packages/go/jsonpointer/traversal.go b/packages/go/jsonpointer/traversal.go new file mode 100644 index 0000000000..870d470500 --- /dev/null +++ b/packages/go/jsonpointer/traversal.go @@ -0,0 +1,151 @@ +// Package jsonpointer provides utilities for working with JSON Pointers as defined in RFC 6901. +// It allows parsing, evaluating, and manipulating JSON Pointers, which are strings that identify +// specific values within a JSON document. +// +// The implementation focuses on efficiency, flexibility, and ease of use. It aims to provide +// a balance between performance and maintainability, with clear error handling and +// extensibility for future enhancements. +package jsonpointer + +import ( + "reflect" +) + +// WalkJSON traverses a JSON-like structure and applies a visitor function to each element. +// It supports map[string]any, []any, and uses reflection for other types. +// +// The function aims to balance performance with flexibility, providing fast-paths for common JSON types. +// +// Parameters: +// - tree: The root of the JSON-like structure to traverse. +// - visit: A function to apply to each element in the structure. +// +// Returns: +// - An error if the visitor function returns an error, or nil if the traversal is successful. +// +// Usage: +// +// data := map[string]any{ +// "foo": []any{1, 2, 3}, +// "bar": map[string]any{"baz": "qux"}, +// } +// err := WalkJSON(data, func(elem any) error { +// fmt.Printf("Visited: %v\n", elem) +// return nil +// }) +// if err != nil { +// // Handle error +// } +func WalkJSON(tree any, visit func(elem any) error) error { + if tree == nil { + return nil + } + + if err := visit(tree); err != nil { + return err + } + + switch t := tree.(type) { + case map[string]any: + return walkMap(t, visit) + case []any: + return walkSlice(t, visit) + default: + return walkReflect(reflect.ValueOf(tree), visit) + } +} + +// walkMap is an unexported helper function that walks through a map[string]any. +// It iterates over all values in the map and recursively calls WalkJSON on each. +// +// Parameters: +// - m: The map to walk through. +// - visit: The visitor function to apply to each element. +// +// Returns: +// - An error if any call to WalkJSON returns an error, or nil if successful. +func walkMap(m map[string]any, visit func(elem any) error) error { + for _, val := range m { + if err := WalkJSON(val, visit); err != nil { + return err + } + } + return nil +} + +// walkSlice is an unexported helper function that walks through a []any. +// It iterates over all elements in the slice and recursively calls WalkJSON on each. +// +// Parameters: +// - s: The slice to walk through. +// - visit: The visitor function to apply to each element. +// +// Returns: +// - An error if any call to WalkJSON returns an error, or nil if successful. +func walkSlice(s []any, visit func(elem any) error) error { + for _, val := range s { + if err := WalkJSON(val, visit); err != nil { + return err + } + } + return nil +} + +// walkReflect is an unexported helper function that uses reflection to walk through complex types. +// It handles pointers, maps, structs, slices, and arrays. +// +// This function is used as a fallback for types that don't match the more specific cases in WalkJSON. +// It uses reflection to inspect the structure of the value and traverse it appropriately. +// +// Parameters: +// - value: The reflect.Value to walk through. +// - visit: The visitor function to apply to each element. +// +// Returns: +// - An error if any recursive call returns an error, or nil if successful. +func walkReflect(value reflect.Value, visit func(elem any) error) error { + switch value.Kind() { + case reflect.Ptr: + if !value.IsNil() { + return walkReflect(value.Elem(), visit) + } + case reflect.Map: + for _, key := range value.MapKeys() { + if err := walkReflectValue(value.MapIndex(key), visit); err != nil { + return err + } + } + case reflect.Struct: + for i := 0; i < value.NumField(); i++ { + if err := walkReflectValue(value.Field(i), visit); err != nil { + return err + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < value.Len(); i++ { + if err := walkReflectValue(value.Index(i), visit); err != nil { + return err + } + } + } + return nil +} + +// walkReflectValue is an unexported helper function that safely walks a reflect.Value. +// It checks if the value can be converted to an interface{} before calling WalkJSON. +// +// This function is used to safely handle reflect.Value instances that may or may not +// be convertible to interface{} (e.g., unexported struct fields). +// +// Parameters: +// - value: The reflect.Value to walk. +// - visit: The visitor function to apply to each element. +// +// Returns: +// - An error if WalkJSON returns an error, or nil if successful or if the value cannot be interfaced. +func walkReflectValue(value reflect.Value, visit func(elem any) error) error { + if value.CanInterface() { + return WalkJSON(value.Interface(), visit) + } + return nil +} diff --git a/packages/go/jsonpointer/traversal_test.go b/packages/go/jsonpointer/traversal_test.go new file mode 100644 index 0000000000..e0f4a9cf67 --- /dev/null +++ b/packages/go/jsonpointer/traversal_test.go @@ -0,0 +1,223 @@ +package jsonpointer + +import ( + "errors" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +type A struct { + Foo string `json:"$foo,omitempty"` + Boo bool `json:"$boo"` + Bar *B // Change this to a pointer to avoid potential cycles +} + +type B struct { + Baz int + C C + D D +} + +type C struct { + Nope string +} + +func (c C) JSONProps() map[string]any { + return map[string]any{ + "bat": "book", + "stuff": false, + "other": nil, + } +} + +type D []string + +type ( + CustomMap map[string]any + CustomSlice []any +) + +func TestWalkJSON(t *testing.T) { + tests := []struct { + name string + data any + expectedElements int + expectedError error + }{ + { + name: "Simple map", + data: map[string]any{ + "a": 1, + "b": "two", + "c": true, + }, + expectedElements: 4, + }, + { + name: "Nested map", + data: map[string]any{ + "a": 1, + "b": map[string]any{ + "c": "nested", + "d": 2, + }, + }, + expectedElements: 5, + }, + { + name: "Map with slice", + data: map[string]any{ + "a": []any{1, 2, 3}, + "b": "string", + }, + expectedElements: 6, + }, + { + name: "Empty map", + data: map[string]any{}, + expectedElements: 1, + }, + { + name: "Empty slice", + data: []any{}, + expectedElements: 1, + }, + { + name: "Complex nested structure", + data: map[string]any{ + "a": 1, + "b": []any{ + "string", + map[string]any{"c": true}, + []any{1, 2, 3}, + }, + "d": map[string]any{ + "e": "nested", + "f": []any{4, 5, 6}, + }, + }, + expectedElements: 16, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actualElements := 0 + visitedElements := make([]string, 0) + err := WalkJSON(tt.data, func(elem any) error { + actualElements++ + visitedElements = append(visitedElements, fmt.Sprintf("%T: %v", elem, elem)) + return nil + }) + + if ok := os.Getenv("BLOODHOUND_JSONPOINTER_TEST_DEBUG"); ok != "" { + // Print visited elements for debugging + t.Logf("Visited elements (%d):", actualElements) + for i, elem := range visitedElements { + t.Logf("%d: %s", i+1, elem) + } + } + + assert.Equal(t, tt.expectedError, err) + assert.Equal(t, tt.expectedElements, actualElements, "Mismatch in number of elements visited") + }) + } +} + +func TestWalkJSONWithError(t *testing.T) { + data := getTestStruct() + expectedError := errors.New("test error") + + err := WalkJSON(data, func(elem any) error { + return expectedError + }) + + assert.Equal(t, expectedError, err) +} + +func TestWalkJSONWithPanic(t *testing.T) { + data := getTestStruct() + + assert.Panics(t, func() { + _ = WalkJSON(data, func(elem any) error { + panic("unexpected panic") + }) + }) +} + +func getTestStruct() *A { + return &A{ + Foo: "fooval", + Boo: true, + Bar: &B{ + Baz: 1, + C: C{ + Nope: "won't register", + }, + D: D{"hello", "world"}, + }, + } +} + +func getComplexNestedStructure() map[string]any { + return map[string]any{ + "a": 1, + "b": []any{ + "string", + map[string]any{ + "c": true, + "d": []any{1, 2, 3}, + }, + }, + "e": struct { + F string + G int + }{ + F: "hello", + G: 42, + }, + } +} + +func getCyclicStructure() *Node { + cyclic := &Node{Value: "first"} + cyclic.Next = &Node{Value: "second", Next: cyclic} + return cyclic +} + +func getPointerToPrimitive() *int { + value := 42 + return &value +} + +func getStructWithUnexportedFields() struct { + exported int + unexported string +} { + return struct { + exported int + unexported string + }{ + exported: 1, + unexported: "hidden", + } +} + +func getLargeNestedStructure() map[string]any { + result := make(map[string]any) + current := result + for i := 0; i < 1000; i++ { + next := make(map[string]any) + current["next"] = next + current = next + } + return result +} + +type Node struct { + Value any + Next *Node +}