Sextant 是一个完整的、高性能的 JSONPath 实现,使用 Swift 编写。它最初是从 SMJJSONPath 移植而来,而 SMJJSONPath 本身又是对 Jayway JsonPath 实现的紧密改编。Sextant 后来经过更新,以使其与其他 JSONPath 实现保持一致(请参阅 issue),因此这个特定实现现在与 SMJJSONPath/Jayway 实现有所不同。
import Sextant
/// Each call to Sextant's query(values: ) will return an array on success and nil on failure
func testSimple0() {
let json = #"["Hello","World"]"#
guard let results = json.query(values: "$[0]") else { return XCTFail() }
XCTAssertEqualAny(results[0], "Hello")
}
/// Works with any existing JSON-like structure
func testSimple2() {
let data = [ "Hello", "World" ]
guard let results = data.query(values: "$[0]") else { return XCTFail() }
XCTAssertEqualAny(results[0], "Hello")
}
/// Automatically covert to simple tuples
func testSimple3() {
let json = #"{"name":"Rocco","age":42}"#
guard let person: (name: String?, age: Int?) = json.query("$.['name','age']") else { return XCTFail() }
XCTAssertEqual(person.name, "Rocco")
XCTAssertEqual(person.age, 42)
}
/// Supports Decodable structs
func testSimple4() {
let json = #"{"data":{"people":[{"name":"Rocco","age":42},{"name":"John","age":12},{"name":"Elizabeth","age":35},{"name":"Victoria","age":85}]}}"#
class Person: Decodable {
let name: String
let age: Int
}
guard let persons: [Person] = json.query("$..[?(@.name)]") else { return XCTFail() }
XCTAssertEqual(persons[0].name, "Rocco")
XCTAssertEqual(persons[0].age, 42)
XCTAssertEqual(persons[2].name, "Elizabeth")
XCTAssertEqual(persons[2].age, 35)
}
/// Easily combine results from multiple queries
func testSimple5() {
let json1 = #"{"error":"Error format 1"}"#
let json2 = #"{"errors":[{"title:":"Error!","detail":"Error format 2"}]}"#
let queries: [String] = [
"$.error",
"$.errors[0].detail",
]
XCTAssertEqualAny(json1.query(string: queries), "Error format 1")
XCTAssertEqualAny(json2.query(string: queries), "Error format 2")
}
/// High performance JSON processing by using .parsed() to get
/// a quick view of the raw json to execute on paths on
func testSimple9() {
let data = #"{"DtCutOff":"2018-01-01 00:00:00","ServiceGroups":[{"ServiceName":"Service1","DtUpdate":"2021-11-22 00:00:00","OrderNumber":"123456","Active":"true"},{"ServiceName":"Service2","DtUpdate":"2021-11-20 00:00:00","OrderNumber":"123456","Active":true},{"ServiceName":"Service3","DtUpdate":"2021-11-10 00:00:00","OrderNumber":"123456","Active":false}]}"#
data.parsed { json in
guard let json = json else { XCTFail(); return }
guard let isActive: Bool = json.query("$.ServiceGroups[*][?(@.ServiceName=='Service1')].Active") else { XCTFail(); return }
XCTAssertEqual(isActive, true)
guard let date: Date = json.query("$.ServiceGroups[*][?(@.ServiceName=='Service1')].DtUpdate") else { XCTFail(); return }
XCTAssertEqual(date, "2021-11-22 00:00:00".date())
}
}
/// Use replace, map, filter, remove and forEach to perform mofications to your json
func testSimple10() {
let json = #"{"data":{"people":[{"name":"Rocco","age":42,"gender":"m"},{"name":"John","age":12,"gender":"m"},{"name":"Elizabeth","age":35,"gender":"f"},{"name":"Victoria","age":85,"gender":"f"}]}}"#
let modifiedJson: String? = json.parsed { root in
guard let root = root else { XCTFail(); return nil }
// Remove all females
root.query(remove: "$..people[?(@.gender=='f')]")
// Incremet all ages by 1
root.query(map: "$..age", {
guard let age = $0.intValue else { return $0 }
return age + 1
})
// Lowercase all names
root.query(map: "$..name", { $0.hitchValue?.lowercase() })
return root.description
}
XCTAssertEqual(modifiedJson, #"{"data":{"people":[{"name":"rocco","age":43,"gender":"m"},{"name":"john","age":13,"gender":"m"}]}}"#)
}
/// Use a single map to accomplish the same task as above but with only one pass through the data
func testSimple11() {
let json = #"{"data":{"people":[{"name":"Rocco","age":42,"gender":"m"},{"name":"John","age":12,"gender":"m"},{"name":"Elizabeth","age":35,"gender":"f"},{"name":"Victoria","age":85,"gender":"f"}]}}"#
let modifiedJson: String? = json.query(map: "$..people[*] ", { person in
// Remove all females, increment age by 1, lowercase all names
guard person["gender"]?.stringValue == "m" else {
return nil
}
if let age = person["age"]?.intValue {
person.set(key: "age", value: age + 1)
}
if let name = person["name"]?.hitchValue {
person.set(key: "name", value: name.lowercase())
}
return person
}) { root in
return root.description
}
XCTAssertEqual(modifiedJson, #"{"data":{"people":[{"name":"rocco","age":43,"gender":"m"},{"name":"john","age":13,"gender":"m"}]}}"#)
}
/// You are not bound to just modify existing elements in your JSON,
/// you can return any json-like structure in your mapping
func testSimple12() {
let oldJson = #"{"someValue": ["elem1", "elem2", "elem3"]}"#
let newJson: String? = oldJson.query(map: "$.someValue", {_ in
return ["elem4", "elem5"]
} ) { root in
return root.description
}
XCTAssertEqual(newJson, #"{"someValue":["elem4","elem5"]}"#)
}
/// For the performance minded, your maps should do as little work as possible
/// per replacement. To improve on the previous example, we could create our
/// replacement element outside of the mapping to reduce unnecessary work.
func testSimple13() {
let oldJson = #"{"someValue": ["elem1", "elem2", "elem3"]}"#
let replacementElement = JsonElement(unknown: ["elem4", "elem5"])
let newJson: String? = oldJson.query(map: "$.someValue", {_ in
return replacementElement
} ) { root in
return root.description
}
XCTAssertEqual(newJson, #"{"someValue":["elem4","elem5"]}"#)
}
/// Example of handling an heterogenous array. The task is to iterate over all
/// operations and perform a dynamic lookup to the operation function, perform
/// the task and coallate the results.
func testSimple14() {
let json = #"[{"name":"add","inputs":[3,4]},{"name":"subtract","inputs":[6,3]},{"echo":"Hello, world"},{"name":"increment","input":41},{"echo":"Hello, world"}]"#
let operations: [HalfHitch: (JsonElement) -> (Int?)] = [
"add": { input in
guard let values = input[element: "inputs"] else { return nil }
guard let lhs = values[int: 0] else { return nil }
guard let rhs = values[int: 1] else { return nil }
return lhs + rhs
},
"subtract": { input in
guard let values = input[element: "inputs"] else { return nil }
guard let lhs = values[int: 0] else { return nil }
guard let rhs = values[int: 1] else { return nil }
return lhs - rhs
},
"increment": { input in
guard let value = input[int: "input"] else { return nil }
return value + 1
}
]
var results = [Int]()
json.query(forEach: #"$[?(@.name)]"#) { operation in
if let opName = operation[halfHitch: "name"],
let opFunc = operations[opName] {
results.append(opFunc(operation) ?? 0)
}
}
XCTAssertEqual(results, [7,3,42])
}
/// You can test a json path for validity by calling .query(validate)
func testSimple17() {
let json = #"[{"title":"Post 1","timestamp":1},{"title":"Post 2","timestamp":2}]"#
XCTAssertEqual(json.query(validate: "$"), nil)
XCTAssertEqual(json.query(validate: ""), "Path must start with $ or @")
XCTAssertEqual(json.query(validate: "$."), "Path must not end with a \'.\' or \'..\'")
XCTAssertEqual(json.query(validate: "$.."), "Path must not end with a \'.\' or \'..\'")
XCTAssertEqual(json.query(validate: "$.store.book[["), "Could not parse token starting at position 12.")
}
Sextant 利用 Hitch(高性能字符串)和 Spanker(高性能、低开销 JSON 反序列化)为 Swift 提供一流的 JSONPath 实现。Hitch 允许快速、utf8 共享内存字符串。Spanker 生成 JSON blob 的低成本视图,Sextant 随后针对该视图查询 JSONPath。在它们作为查询结果返回之前,不会反序列化任何内容,也不会从源 JSON blob 复制任何内存。在您有大量 JSON 和/或大量查询需要对其运行时,Sextant 真正发挥作用。
Sextant 完全兼容 Swift Package Manager
dependencies: [
.package(url: "https://github.com/KittyMac/Sextant.git", .upToNextMinor(from: "0.4.0"))
],
最初的 Stefan Goessner JsonPath 实现于 2007 年发布,并由此衍生出数十种不同的实现。此 JSONPath 比较图表显示了各种可用的实现,并且在撰写本文时,Swift 实现尚不存在(请注意,存在 SwiftPath 项目,但由于 在 Linux 上运行时出现严重错误,因此未包含在该图表中)。
本节的其余部分主要改编自 Jayway JsonPath 入门指南部分。
运算符 | 描述 |
---|---|
$ |
要查询的根元素。这将启动所有路径表达式。 |
@ |
过滤器谓词正在处理的当前节点。 |
* |
通配符。在需要名称或数字的任何地方都可用。 |
.. |
深度扫描。在需要名称的任何地方都可用。 |
.<name> |
点表示法子级 |
['<name>' (, '<name>')] |
括号表示法子级或子级们 |
[<number> (, <number>)] |
数组索引或索引们 |
[start:end] |
数组切片运算符 |
[?(<expression>)] |
过滤器表达式。表达式必须求值为布尔值。 |
函数可以在路径的末尾调用 - 函数的输入是路径表达式的输出。函数输出由函数本身决定。
函数 | 描述 | 输出类型 |
---|---|---|
min() | 提供数字数组的最小值 | Double |
max() | 提供数字数组的最大值 | Double |
avg() | 提供数字数组的平均值 | Double |
stddev() | 提供数字数组的标准偏差值 | Double |
length() | 提供数组的长度 | Integer |
sum() | 提供数字数组的总和值 | Double |
keys() | 提供属性键(终端波浪号 ~ 的替代方案) |
Set<E> |
concat(X) | 提供路径输出与新项的连接版本 | 类似输入 |
append(X) | 向 jsonpath 输出数组添加一个项目 | 类似输入 |
过滤器是用于过滤数组的逻辑表达式。典型的过滤器是 [?(@.age > 18)]
,其中 @
表示正在处理的当前项。可以使用逻辑运算符 &&
和 ||
创建更复杂的过滤器。字符串文字必须用单引号或双引号括起来([?(@.color == 'blue')]
或 [?(@.color == "blue")]
)。
运算符 | 描述 |
---|---|
== | 左侧等于右侧(请注意 1 不等于 '1') |
!= | 左侧不等于右侧 |
< | 左侧小于右侧 |
<= | 左侧小于或等于右侧 |
> | 左侧大于右侧 |
>= | 左侧大于或等于右侧 |
=~ | 左侧匹配正则表达式 [?(@.name =~ /foo.*?/i)] |
in | 左侧存在于右侧 [?(@.size in ['S', 'M'])] |
nin | 左侧不存在于右侧 |
subsetof | 左侧是右侧的子集 [?(@.sizes subsetof ['S', 'M', 'L'])] |
anyof | 左侧与右侧有交集 [?(@.sizes anyof ['M', 'L'])] |
noneof | 左侧与右侧没有交集 [?(@.sizes noneof ['M', 'L'])] |
size | 左侧(数组或字符串)的大小应与右侧匹配 |
empty | 左侧(数组或字符串)应为空 |
给定 json
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"display-price": 8.95,
"bargain": true
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"display-price": 12.99,
"bargain": false
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"display-price": 8.99,
"bargain": true
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"display-price": 22.99,
"bargain": false
}
],
"bicycle": {
"color": "red",
"display-price": 19.95,
"foo:bar": "fooBar",
"dot.notation": "new",
"dash-notation": "dashes"
}
}
}
JsonPath(点击链接尝试) | 结果 |
---|---|
$.store.book[*].author | 所有书籍的作者 |
$..['author','title'] | 所有作者和标题 |
$.store.* | 所有事物,包括书籍和自行车 |
$.store..display-price | 所有事物的价格 |
$..book[2] | 第三本书 |
$..book[-2] | 倒数第二本书 |
$..book[0,1] | 前两本书 |
$..book[:2] | 从索引 0(包含)到索引 2(不包含)的所有书籍 |
$..book[1:2] | 从索引 1(包含)到索引 2(不包含)的所有书籍 |
$..book[-2:] | 最后两本书 |
$..book[2:] | 从末尾开始的第二本书 |
$..book[?(@.isbn)] | 所有带有 ISBN 编号的书籍 |
$.store.book[?(@.display-price < 10)] | 商店中所有价格低于 10 的书籍 |
$..book[?(@.bargain == true)] | 商店中所有特价书籍 |
$..book[?(@.author =~ /.*REES/i)] | 所有匹配正则表达式的书籍(忽略大小写) |
$..* | 给我一切东西 |
$..book.length() | 书籍的数量 |
Sextant 是根据 MIT 许可证条款分发的免费软件,以下为许可证内容。Sextant 可用于任何目的,包括商业目的,且完全免费。无需文书工作、无需版税、没有 GNU 式的“著作权共享”限制。只需下载并享受即可。
版权所有 (c) 2021 Chimera Software, LLC
特此授予许可,免费向任何获得本软件和相关文档文件(“软件”)副本的人员授予许可,以不受限制地处理本软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本的权利,并允许向已获得本软件的人员提供软件,但须符合以下条件
上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于适销性、特定用途的适用性和不侵权的保证。在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是在合同诉讼、侵权诉讼或其他诉讼中,因本软件或本软件的使用或其他交易而产生、出于或与之相关。