diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9251ffa --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,158 @@ +# AGENTS.md - JSONPath Project + +This file provides guidance to AI agents working on the JSONPath project. + +## Project Overview + +JSONPath is a Go library for querying JSON data using JSONPath expressions. It provides: + +- **Language**: Go (Golang) 1.25+ +- **Type**: Library/package +- **Purpose**: JSON data querying and manipulation +- **Features**: JSONPath expression parsing and evaluation + +## Key Configuration Files + +- `go.mod` - Go module definition +- `jsonpath.go` - Main library implementation +- `jsonpath_test.go` - Comprehensive test suite +- `.travis.yml` - CI/CD configuration + +## Build and Test Commands + +### Installation +```bash +# Install the package +go get github.com/your-repo/jsonpath + +# Install dependencies +go mod tidy +``` + +### Development +```bash +# Build the project +go build + +# Run the library +go run . +``` + +### Testing +```bash +# Run all tests +go test -v + +# Run tests with coverage +go test -cover + +# Run specific test functions +go test -run TestFunctionName +``` + +### Code Quality +```bash +# Format code +gofmt -w . + +# Check for formatting issues +gofmt -d . +``` + +## Project Structure + +``` +jsonpath.go # Main library implementation +jsonpath_test.go # Test suite +go.mod # Go module definition +readme.md # Project documentation +``` + +## Code Style Guidelines + +- **Go Standards**: Follow official Go code review comments +- **Formatting**: Use `gofmt` for consistent formatting +- **Naming**: Use camelCase for variables, PascalCase for exported types +- **Error Handling**: Explicit error handling (no panic for expected errors) +- **Documentation**: Add godoc comments for exported functions/types +- **Testing**: Comprehensive test coverage for all functionality + +## Testing Instructions + +- **Test Files**: `jsonpath_test.go` contains comprehensive tests +- **Test Patterns**: Table-driven tests for JSONPath expressions +- **Coverage**: Aim for high test coverage of all code paths +- **Edge Cases**: Test malformed JSON, invalid paths, boundary conditions + +## JSONPath Implementation Details + +- **Expression Parsing**: Custom JSONPath parser implementation +- **Query Evaluation**: Efficient JSON traversal algorithms +- **Result Handling**: Support for various return types +- **Error Handling**: Clear error messages for invalid expressions + +## Security Considerations + +- **Input Validation**: Validate JSONPath expressions +- **Memory Safety**: Handle large JSON documents efficiently +- **Error Messages**: Don't expose sensitive information +- **Dependency Management**: Minimal dependencies for security + +## Performance Considerations + +- **Parsing Optimization**: Efficient JSONPath expression parsing +- **Traversal Algorithms**: Optimized JSON document traversal +- **Memory Usage**: Minimize allocations during query execution +- **Benchmarking**: Consider adding performance benchmarks + +## Usage Examples + +```go +// Basic usage example +result, err := jsonpath.Get(pathExpression, jsonData) +if err != nil { + // Handle error +} +// Use result... +``` + +## Git Conventions + +- **Commit Messages**: Clear, descriptive commit messages +- **Branching**: Use feature branches for new development +- **Pull Requests**: Required for merging to main branch +- **Tags**: Use semantic versioning for releases + +## CI/CD + +- **Travis CI**: Configured in `.travis.yml` +- **Automated Testing**: Runs on every push/PR +- **Build Verification**: Ensures project builds successfully +- **Test Coverage**: Reports test coverage metrics + +## Documentation + +- **readme.md**: Contains usage examples and API documentation +- **Godoc**: Use godoc comments for inline documentation +- **Examples**: Include practical usage examples + +## Dependency Management + +- **Minimal Dependencies**: Only standard library dependencies +- **Go Modules**: Uses Go modules for dependency management +- **Updates**: Regularly update Go version in `go.mod` + +## Future Enhancements + +- **Additional Features**: Consider adding more JSONPath features +- **Performance**: Optimize for large JSON documents +- **Compatibility**: Ensure compatibility with JSONPath standards +- **Documentation**: Expand usage examples and tutorials + +## Task Implementation +1. **Analyze Requirements**: Refer to `README.md` for detailed feature specifications and system design. +2. **Implementation**: Modify source code in the respective directories (e.g., `src/`, `internal/`). +3. **Verification**: Run provided build and test commands (see above) to ensure correctness. +4. **Push Changes**: + - Commit changes: `git commit -m "feat: implement "` + - Push to remote: `git push origin ` diff --git a/jsonpath.go b/jsonpath.go index 00dc6fd..45ebd43 100644 --- a/jsonpath.go +++ b/jsonpath.go @@ -69,7 +69,7 @@ func (c *Compiled) String() string { func (c *Compiled) Lookup(obj interface{}) (interface{}, error) { var err error - for _, s := range c.steps { + for i, s := range c.steps { // "key", "idx" switch s.op { case "key": @@ -132,6 +132,24 @@ func (c *Compiled) Lookup(obj interface{}) (interface{}, error) { if err != nil { return nil, err } + case "recursive": + obj = getAllDescendants(obj) + // Heuristic: if next step is key, exclude slices from candidates to avoid double-matching + // (once as container via implicit map, once as individual elements) + if i+1 < len(c.steps) && c.steps[i+1].op == "key" { + if candidates, ok := obj.([]interface{}); ok { + filtered := []interface{}{} + for _, cand := range candidates { + // Filter out Slices (but keep Maps and others) + // because get_key on Slice iterates children, which are already in candidates + v := reflect.ValueOf(cand) + if v.Kind() != reflect.Slice { + filtered = append(filtered, cand) + } + } + obj = filtered + } + } default: return nil, fmt.Errorf("expression don't support in filter") } @@ -161,8 +179,8 @@ func tokenize(query string) ([]string, error) { if token == "." { continue } else if token == ".." { - if tokens[len(tokens)-1] != "*" { - tokens = append(tokens, "*") + if len(tokens) == 0 || tokens[len(tokens)-1] != ".." { + tokens = append(tokens, "..") } token = "." continue @@ -215,7 +233,7 @@ func tokenize(query string) ([]string, error) { } /* - op: "root", "key", "idx", "range", "filter", "scan" +op: "root", "key", "idx", "range", "filter", "scan" */ func parse_token(token string) (op string, key string, args interface{}, err error) { if token == "$" { @@ -224,6 +242,9 @@ func parse_token(token string) (op string, key string, args interface{}, err err if token == "*" { return "scan", "*", nil, nil } + if token == ".." { + return "recursive", "..", nil, nil + } bracket_idx := strings.Index(token, "[") if bracket_idx < 0 { @@ -720,3 +741,37 @@ func cmp_any(obj1, obj2 interface{}, op string) (bool, error) { return false, nil } + +func getAllDescendants(obj interface{}) []interface{} { + res := []interface{}{} + var recurse func(curr interface{}) + recurse = func(curr interface{}) { + res = append(res, curr) + v := reflect.ValueOf(curr) + if !v.IsValid() { + return + } + + kind := v.Kind() + if kind == reflect.Ptr { + v = v.Elem() + if !v.IsValid() { + return + } + kind = v.Kind() + } + + switch kind { + case reflect.Map: + for _, k := range v.MapKeys() { + recurse(v.MapIndex(k).Interface()) + } + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + recurse(v.Index(i).Interface()) + } + } + } + recurse(obj) + return res +} diff --git a/jsonpath_test.go b/jsonpath_test.go index 90f05b7..22469df 100644 --- a/jsonpath_test.go +++ b/jsonpath_test.go @@ -96,7 +96,7 @@ func Test_jsonpath_JsonPathLookup_1(t *testing.T) { if res_v, ok := res.([]interface{}); ok != true || res_v[0].(float64) != 8.95 || res_v[1].(float64) != 12.99 || res_v[2].(float64) != 8.99 || res_v[3].(float64) != 22.99 { t.Errorf("exp: [8.95, 12.99, 8.99, 22.99], got: %v", res) } - + // range res, err = JsonPathLookup(json_data, "$.store.book[0:1].price") t.Log(err, res) @@ -214,6 +214,15 @@ var token_cases = []map[string]interface{}{ "query": "$....author", "tokens": []string{"$", "*", "author"}, }, + // New test cases for recursive descent + map[string]interface{}{ + "query": "$..author", + "tokens": []string{"$", "..", "author"}, + }, + map[string]interface{}{ + "query": "$....author", + "tokens": []string{"$", "..", "author"}, + }, } func Test_jsonpath_tokenize(t *testing.T) { @@ -1243,3 +1252,77 @@ func Test_jsonpath_rootnode_is_nested_array_range(t *testing.T) { // t.Fatal("idx: 0, should be 3.1, got: %v", ares[1]) //} } + +func TestRecursiveDescent(t *testing.T) { + data := ` +{ + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "expensive": 10 +} +` + var json_data interface{} + json.Unmarshal([]byte(data), &json_data) + + // Test case: $..author should return all authors + authors_query := "$..author" + res, err := JsonPathLookup(json_data, authors_query) + if err != nil { + t.Fatalf("Failed to execute recursive query %s: %v", authors_query, err) + } + + authors, ok := res.([]interface{}) + if !ok { + t.Fatalf("Expected []interface{}, got %T", res) + } + + if len(authors) != 4 { + t.Errorf("Expected 4 authors, got %d: %v", len(authors), authors) + } + + // Test case: $..price should return all prices (5 total: 4 books + 1 bicycle) + price_query := "$..price" + res, err = JsonPathLookup(json_data, price_query) + if err != nil { + t.Fatalf("Failed to execute recursive query %s: %v", price_query, err) + } + prices, ok := res.([]interface{}) + if !ok { + t.Fatalf("Expected []interface{}, got %T", res) + } + if len(prices) != 5 { + t.Errorf("Expected 5 prices, got %d: %v", len(prices), prices) + } +}