Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ A complete Go implementation of JSONPath that fully complies with [RFC 9535](htt
- Array slices (`[start:end:step]`)
- Array wildcards (`[*]`)
- Multiple indices (`[1,2,3]`)
- Multiple field names (`['name','age']`)
- Filter expressions (`[?(@.price < 10)]`)
- Command Line Tool (`jp`)
- Beautiful colorized output
Expand All @@ -45,6 +46,17 @@ A complete Go implementation of JSONPath that fully complies with [RFC 9535](htt
- Enhanced documentation clarity
- Fixed typos and inconsistencies

### v2.1.0 (Upcoming)
- New Features
- Support for multiple field name extraction (`['name','age']`)
- Enhanced array indexing with mixed field types
- Improvements
- Better error handling for multi-field expressions
- Performance optimizations for complex queries
- Documentation
- Added examples for multi-field extraction
- Updated feature list to include multi-field support

### v2.0.0
- Complete rewrite with RFC 9535 compliance
- Full implementation of JSONPath specification (RFC 9535)
Expand Down Expand Up @@ -295,6 +307,12 @@ func main() {
// Search for books with titles starting with 'S'
"$.store.book[*].title.search('^S.*')"

// Extract multiple fields from objects
"$.store.book[*]['author','price']"

// Extract multiple fields with wildcard
"$.store.book[*]['title','category']"

// Chain multiple functions
"$.store.book[?@.price > 10].title.length()"

Expand Down
18 changes: 18 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- 数组切片(`[start:end:step]`)
- 数组通配符(`[*]`)
- 多重索引(`[1,2,3]`)
- 多字段名称(`['name','age']`)
- 过滤表达式(`[?(@.price < 10)]`)
- 命令行工具(`jp`)
- 精美的彩色输出
Expand Down Expand Up @@ -50,6 +51,17 @@
- 添加更多示例和用例
- 改进 API 文档

### v2.1.0 (即将发布)
- 新特性
- 支持多字段名称提取(`['name','age']`)
- 增强的数组索引支持混合字段类型
- 改进
- 更好的多字段表达式错误处理
- 复杂查询的性能优化
- 文档
- 添加多字段提取示例
- 更新特性列表以包含多字段支持

### v1.0.4

- 集中管理版本号
Expand Down Expand Up @@ -296,6 +308,12 @@ func main() {

// 组合搜索和过滤条件
"$.store.book[?@.title.match('^S.*') && @.price < 10].author"

// 从对象中提取多个字段
"$.store.book[*]['author','price']"

// 使用通配符提取多个字段
"$.store.book[*]['title','category']"
```

### 结果处理方法
Expand Down
79 changes: 79 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -992,3 +992,82 @@ func TestSearchFunction(t *testing.T) {
})
}
}

func TestMultiFieldExtraction(t *testing.T) {
// 测试数据
jsonData := `{
"star": {
"name": "Sun",
"diameter": 1391016,
"age": null,
"planets": [
{
"name": "Mercury",
"Number of Moons": "0",
"diameter": 4879,
"has-moons": false
},
{
"name": "Venus",
"Number of Moons": "0",
"diameter": 12104,
"has-moons": false
},
{
"name": "Earth",
"Number of Moons": "1",
"diameter": 12756,
"has-moons": true
},
{
"name": "Mars",
"Number of Moons": "2",
"diameter": 6792,
"has-moons": true
}
]
}
}`

testCases := []struct {
name string
path string
expected interface{}
wantErr bool
}{
{
name: "extract multiple fields from objects",
path: "$.star.planets.*['name','diameter']",
expected: []interface{}{"Mercury", float64(4879), "Venus", float64(12104), "Earth", float64(12756), "Mars", float64(6792)},
},
{
name: "extract multiple fields with wildcard",
path: "$.star.planets[*]['name','has-moons']",
expected: []interface{}{"Mercury", false, "Venus", false, "Earth", true, "Mars", true},
},
{
name: "extract single field",
path: "$.star.planets[*]['name']",
expected: []interface{}{"Mercury", "Venus", "Earth", "Mars"},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := Query(jsonData, tc.path)
if tc.wantErr {
if err == nil {
t.Errorf("expected error but got none")
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("got %v, want %v", result, tc.expected)
}
})
}
}
133 changes: 128 additions & 5 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,10 @@ func parseBracketSegment(content string) (segment, error) {
return parseFilterSegment(content[1:])
}

// 处理多索引选择
if strings.Contains(content, ",") {
// 处理多索引选择或多字段选择
if strings.Contains(content, ",") ||
((strings.HasPrefix(content, "'") && strings.HasSuffix(content, "'")) && strings.Contains(content[1:len(content)-1], "','")) ||
((strings.HasPrefix(content, "\"") && strings.HasSuffix(content, "\"")) && strings.Contains(content[1:len(content)-1], "\",\"")) {
return parseMultiIndexSegment(content)
}

Expand Down Expand Up @@ -649,15 +651,131 @@ func getFieldValue(obj interface{}, field string) (interface{}, error) {
return current, nil
}

// isValidIdentifier 检查字符串是否是有效的标识符
func isValidIdentifier(s string) bool {
if s == "" {
return false
}

// 检查第一个字符是否是字母或下划线
first := s[0]
if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_') {
return false
}

// 检查其余字符是否是字母、数字或下划线
for i := 1; i < len(s); i++ {
c := s[i]
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
return false
}
}

return true
}

// 解析多索引选择
func parseMultiIndexSegment(content string) (segment, error) {
// 检查前导和尾随逗号
if strings.HasPrefix(content, ",") {
return nil, NewError(ErrInvalidPath, "leading comma in multi-index segment", content)
}
if strings.HasSuffix(content, ",") {
return nil, NewError(ErrInvalidPath, "trailing comma in multi-index segment", content)
}

parts := strings.Split(content, ",")
indices := make([]int, 0, len(parts))

// 检查空索引
for _, part := range parts {
if strings.TrimSpace(part) == "" {
return nil, NewError(ErrInvalidPath, "empty index in multi-index segment", content)
}
}

// 检查是否包含字符串字段名
// 检查是否包含字符串字段名
hasString := false
hasQuotedString := false
for _, part := range parts {
idx, err := strconv.Atoi(strings.TrimSpace(part))
trimmed := strings.TrimSpace(part)
// 检查是否是字符串字段名(带引号)
if (strings.HasPrefix(trimmed, "'") && strings.HasSuffix(trimmed, "'")) ||
(strings.HasPrefix(trimmed, "\"") && strings.HasSuffix(trimmed, "\"")) {
hasString = true
hasQuotedString = true
break
}
// 检查是否是非数字的字段名
if _, err := strconv.Atoi(trimmed); err != nil {
// 如果不是带引号的字符串,但也不是数字,可能是无效索引或不带引号的字段名
// 我们需要进一步判断
hasString = true
break
}
}

// 如果有引号字符串,或者所有非数字部分都是有效的字段名,则作为多字段段处理
if hasString && hasQuotedString {
// 有引号字符串,按多字段段处理
names := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
// 处理带引号的字符串
if (strings.HasPrefix(trimmed, "'") && strings.HasSuffix(trimmed, "'")) ||
(strings.HasPrefix(trimmed, "\"") && strings.HasSuffix(trimmed, "\"")) {
names = append(names, trimmed[1:len(trimmed)-1])
} else {
// 处理不带引号的字段名
names = append(names, trimmed)
}
}
return &multiNameSegment{names: names}, nil
}

// 检查是否所有部分都是有效的字段名(不带引号但也不是纯数字)
if hasString && !hasQuotedString {
// 检查是否所有部分都是数字或者所有部分都是有效的标识符
allNumbers := true
allValidNames := true

for _, part := range parts {
trimmed := strings.TrimSpace(part)
if _, err := strconv.Atoi(trimmed); err != nil {
// 不是数字
allNumbers = false
if !isValidIdentifier(trimmed) {
allValidNames = false
}
} else {
// 是数字,但在混合情况下不应该作为字段名
allValidNames = false
}
}

// 如果混合了数字和非数字,这是无效的
if !allNumbers && !allValidNames {
return nil, NewError(ErrInvalidPath, "cannot mix numeric indices and field names", content)
}

if allValidNames {
// 所有部分都是有效标识符,按多字段段处理
names := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
names = append(names, trimmed)
}
return &multiNameSegment{names: names}, nil
}
}

// 否则解析为多索引段
indices := make([]int, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
idx, err := strconv.Atoi(trimmed)
if err != nil {
return nil, fmt.Errorf("invalid index: %s", part)
return nil, NewError(ErrInvalidPath, fmt.Sprintf("invalid index: %s", trimmed), content)
}
indices = append(indices, idx)
}
Expand Down Expand Up @@ -724,6 +842,11 @@ func parseIndexOrName(content string) (segment, error) {
return &nameSegment{name: content[1 : len(content)-1]}, nil
}

// 处理双引号字符串字面量
if strings.HasPrefix(content, "\"") && strings.HasSuffix(content, "\"") && len(content) > 1 {
return &nameSegment{name: content[1 : len(content)-1]}, nil
}

return &nameSegment{name: content}, nil
}

Expand Down
Loading
Loading