-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathJSON.swift
820 lines (745 loc) · 31.1 KB
/
JSON.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
//
// JSON.swift
// DynamicJSON
//
// Created by Matthias Zenger on 11/02/2024.
// Copyright © 2024 Matthias Zenger. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
///
/// Generic representation of a JSON document.
///
/// Enum `JSON` encodes JSON documents using seven different cases: `null`, `boolean`,
/// `integer`, `float`, `string`, `array`, and `object`. It provides initializers for all
/// relevant Swift literals allowing a natural expression of JSON documents using Swift
/// syntax.
///
/// Enum `JSON` supports encoding and decoding of JSON documents using standard Swift
/// APIs and has convenience methods for dealing with JSON documents provided as a string
/// or `Data` object.
///
/// Besides numerous convenience methods for accessing data encapsulated in values of
/// type `JSON`, there are generic accessors based on Swift key paths, JSON pointers,
/// as well as singular JSON path queries. These can be used to extract data, but also
/// to specify transformations as well as mutations of JSON data.
///
@dynamicMemberLookup
public enum JSON: Hashable,
Codable,
CustomStringConvertible,
CustomDebugStringConvertible,
ExpressibleByNilLiteral,
ExpressibleByBooleanLiteral,
ExpressibleByIntegerLiteral,
ExpressibleByFloatLiteral,
ExpressibleByStringLiteral,
ExpressibleByArrayLiteral,
ExpressibleByDictionaryLiteral {
case null
case boolean(Bool)
case integer(Int64)
case float(Double)
case string(String)
case array([JSON])
case object([String : JSON])
/// Collection of errors raised by functionality provided by enum `JSON`.
public enum Error: LocalizedError, CustomStringConvertible {
case initialization
case erroneousEncoding
case cannotAppend(JSON, JSON)
case cannotInsert(JSON, JSON, Int)
case cannotAssign(String, JSON)
case typeMismatch(JSONType, JSON)
public var description: String {
switch self {
case .initialization:
return "unable to initialize JSON data structure"
case .erroneousEncoding:
return "erroneous JSON encoding"
case .cannotAppend(let json, let array):
return "unable to append \(json) to \(array)"
case .cannotInsert(let json, let array, let index):
return "unable to insert \(json) into \(array) at \(index)"
case .cannotAssign(let member, let json):
return "unable to set/update member '\(member)' of \(json)"
case .typeMismatch(let types, let json):
return "expected \(json) to be of type \(types)"
}
}
public var errorDescription: String? {
return self.description
}
public var failureReason: String? {
switch self {
case .initialization:
return "initialization error"
case .erroneousEncoding:
return "encoding error"
case .cannotAppend(_, _), .cannotInsert(_, _, _), .cannotAssign(_, _):
return "mutation error"
case .typeMismatch(_, _):
return "type mismatch"
}
}
}
// MARK: - Initializers
/// Creates a JSON null value.
public init(nilLiteral: ()) {
self = .null
}
/// Creates a JSON boolean value.
public init(booleanLiteral value: Bool) {
self = .boolean(value)
}
/// Creates a JSON number from an integer literal.
public init(integerLiteral value: Int64) {
self = .integer(value)
}
/// Creates a JSON number from an floating-point literal.
public init(floatLiteral value: Double) {
self = .float(value)
}
/// Creates a JSON string.
public init(stringLiteral value: String) {
self = .string(value)
}
/// Creates a JSON array.
public init(arrayLiteral elements: JSON...) {
self = .array(elements)
}
/// Creates a JSON object from a dictionary.
public init(dictionaryLiteral elements: (String, JSON)...) {
var object: [String:JSON] = [:]
for (k, v) in elements {
object[k] = v
}
self = .object(object)
}
/// Coerces instances of standard Swift data types into JSON values.
public init(_ value: Any) throws {
switch value {
case _ as NSNull:
self = .null
case let opt as Optional<Any> where opt == nil:
self = .null
case let bool as Bool:
self = .boolean(bool)
case let num as Int:
self = .integer(Int64(num))
case let num as Int64:
self = .integer(num)
case let num as Double:
self = .float(num)
case let num as NSNumber:
if num.isBool {
self = .boolean(num.boolValue)
} else if let value = num as? Int {
self = .integer(Int64(value))
} else if let value = num as? Int64 {
self = .integer(value)
} else if let value = num as? Double {
self = .float(value)
} else {
throw Error.initialization
}
case let str as String:
self = .string(str)
case let array as [Any]:
self = .array(try array.map(JSON.init))
case let dict as [String : Any]:
self = .object(try dict.mapValues(JSON.init))
case let obj as Encodable:
self = try .init(encodable: obj)
default:
throw Error.initialization
}
}
/// Initializer used to decode JSON values.
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let object = try? container.decode([String: JSON].self) {
self = .object(object)
} else if let array = try? container.decode([JSON].self) {
self = .array(array)
} else if let string = try? container.decode(String.self) {
self = .string(string)
} else if let bool = try? container.decode(Bool.self) {
self = .boolean(bool)
} else if let number = try? container.decode(Int64.self) {
self = .integer(number)
} else if let number = try? container.decode(Double.self) {
self = .float(number)
} else if container.decodeNil() {
self = .null
} else {
throw DecodingError.dataCorrupted(
DecodingError.Context(codingPath: decoder.codingPath,
debugDescription: "Invalid JSON value"))
}
}
/// Initializer used to map encodable data into JSON values. This initializer can
/// be used to coerce a strongly typed representation of JSON-based data into a
/// generic JSON representation.
public init(encodable: Encodable) throws {
self = try JSONDecoder().decode(JSON.self, from: try JSONEncoder().encode(encodable))
}
/// This initializer decodes the provided data with the given decoding strategies
/// into a JSON value.
public init(data: Data,
dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
floatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy = .throw,
userInfo: [CodingUserInfoKey : Any]? = nil) throws {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .useDefaultKeys
decoder.dateDecodingStrategy = dateDecodingStrategy
decoder.nonConformingFloatDecodingStrategy = floatDecodingStrategy
if let userInfo {
decoder.userInfo = userInfo
}
self = try decoder.decode(JSON.self, from: data)
}
/// This initializer decodes the provided string with the given decoding strategies
/// into a JSON value.
public init(string: String,
dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
floatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy = .throw,
userInfo: [CodingUserInfoKey : Any]? = nil) throws {
guard let data = string.data(using: .utf8) else {
throw Error.erroneousEncoding
}
self = try .init(data: data,
dateDecodingStrategy: dateDecodingStrategy,
floatDecodingStrategy: floatDecodingStrategy,
userInfo: userInfo)
}
/// This initializer decodes the content at the provided URL with the given
/// decoding strategies into a JSON value.
public init(url: URL,
dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
floatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy = .throw,
userInfo: [CodingUserInfoKey : Any]? = nil) throws {
try self.init(data: try Data(contentsOf: url),
dateDecodingStrategy: dateDecodingStrategy,
floatDecodingStrategy: floatDecodingStrategy,
userInfo: userInfo)
}
// MARK: - Exporting and interchanging data
/// Encodes this JSON value using the provided encoding strategies and returns it as
/// a `Data` object.
public func data(formatting: JSONEncoder.OutputFormatting = .init(),
dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate,
floatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy = .throw,
userInfo: [CodingUserInfoKey : Any]? = nil) throws -> Data {
let encoder = JSONEncoder()
encoder.outputFormatting = formatting
encoder.keyEncodingStrategy = .useDefaultKeys
encoder.dateEncodingStrategy = dateEncodingStrategy
encoder.nonConformingFloatEncodingStrategy = floatEncodingStrategy
if let userInfo {
encoder.userInfo = userInfo
}
return try encoder.encode(self)
}
/// Encodes this JSON value using the provided encoding strategies and returns it as
/// a string.
public func string(formatting: JSONEncoder.OutputFormatting = .init(),
dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate,
floatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy = .throw,
userInfo: [CodingUserInfoKey : Any]? = nil) throws -> String? {
return String(data: try self.data(formatting: formatting,
dateEncodingStrategy: dateEncodingStrategy,
floatEncodingStrategy: floatEncodingStrategy,
userInfo: userInfo),
encoding: .utf8)
}
/// Coerces this JSON value into a decodable object. This method can be used to map
/// generic JSON values into a strongly typed representation of JSON-based data.
public func coerce<T: Decodable>() throws -> T {
return try JSONDecoder().decode(T.self, from: try JSONEncoder().encode(self))
}
/// Encodes this JSON value using the provided encoder.
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .null:
try container.encodeNil()
case .boolean(let bool):
try container.encode(bool)
case .integer(let number):
try container.encode(number)
case .float(let number):
try container.encode(number)
case .string(let string):
try container.encode(string)
case .array(let array):
try container.encode(array)
case .object(let object):
try container.encode(object)
}
}
// MARK: - Projections and accessors
/// Returns the type of this JSON value.
public var type: JSONType {
switch self {
case .null:
return .null
case .boolean(_):
return .boolean
case .integer(_):
return .integer
case .float(_):
return .number
case .string(_):
return .string
case .array(_):
return .array
case .object(_):
return .object
}
}
/// Returns `true` if this JSON value represents null.
public var isNull: Bool {
guard case .null = self else {
return false
}
return true
}
/// Returns a boolean if this JSON value represents a boolean value, otherwise
/// `nil` is returned.
public var boolValue: Bool? {
guard case .boolean(let value) = self else {
return nil
}
return value
}
/// Returns an integer if this JSON value represents an integer value in the
/// `Int` range, otherwise `nil` is returned.
public var intValue: Int? {
guard case .integer(let value) = self,
let res = Int(exactly: value) else {
return nil
}
return res
}
/// Returns an integer if this JSON value represents an `Int64` integer value,
/// otherwise `nil` is returned.
public var int64Value: Int64? {
guard case .integer(let value) = self else {
return nil
}
return value
}
/// Returns a double if this JSON value represents a number, otherwise `nil` is returned.
public var doubleValue: Double? {
switch self {
case .integer(let num):
return Double(num)
case .float(let num):
return num
default:
return nil
}
}
/// Returns a string if this value represents a JSON string, otherwise `nil` is returned.
public var stringValue: String? {
guard case .string(let value) = self else {
return nil
}
return value
}
/// Returns an array if this value represents a JSON array, otherwise `nil` is returned.
public var arrayValue: [JSON]? {
guard case .array(let value) = self else {
return nil
}
return value
}
/// Returns a dictionary if this value represents a JSON object, otherwise
/// `nil` is returned.
public var objectValue: [String: JSON]? {
guard case .object(let value) = self else {
return nil
}
return value
}
/// Returns all the direct children of this JSON value in an array. For null, boolean,
/// numeric and string values, an empty array is returned. For JSON arrays, the array
/// itself is returned. For JSON objects, the values of the object (without their
/// corresponding keys) are returned in an undefined order.
public var children: [JSON] {
switch self {
case .null, .boolean(_), .integer(_), .float(_), .string(_):
return []
case .array(let arr):
return arr
case .object(let dict):
return [JSON](dict.values)
}
}
/// Applies the given function to all descendents (i.e. direct and indirect children)
/// of this JSON value.
public func forEachDescendant(_ proc: (JSON) throws -> Void) rethrows {
try proc(self)
for child in self.children {
try child.forEachDescendant(proc)
}
}
// MARK: - Queries and lookup of values
/// Returns the JSON value at the given index in the array represented by this
/// JSON value. If this JSON value does not represent an array or if the index
/// is out of bounds, then `nil` is returned.
public subscript(index: Int) -> JSON? {
guard case .array(let array) = self, array.indices.contains(index) else {
return nil
}
return array[index]
}
/// Returns the JSON value associated with the key `member` by the object represented
/// by this JSON value. If this JSON value does not represent an object or if the object
/// does not have such a member, `nil` will be returned?
public subscript(member: String) -> JSON? {
guard case .object(let dict) = self else {
return nil
}
return dict[member]
}
/// Implements dynamic member lookup.
public subscript(dynamicMember member: String) -> JSON? {
return self[member]
}
/// Returns the JSON value referenced via the JSON reference `ref`. Supported JSON
/// reference implementations are `JSONLocation` (singular JSON path queries) and
/// `JSONPointer`, and every third party implementation of the `JSONReference` protocol.
public subscript(ref ref: JSONReference) -> JSON? {
return ref.get(from: self)
}
/// Returns the JSON value referenced via the JSON reference string `ref`. Both,
/// JSON path syntax as well as JSON pointer syntax are supported.
public subscript(ref ref: String) -> JSON? {
get throws {
return try JSON.reference(from: ref).get(from: self)
}
}
/// Executes the given JSON path query and returns all matching JSON values with their
/// corresponding locations.
public func query(_ path: JSONPath) throws -> [LocatedJSON] {
return try JSONPathEvaluator(value: self).query(path)
}
/// Executes the JSON path query given in JSON path syntax and returns all matching
/// JSON values with their corresponding locations.
public func query(_ path: String) throws -> [LocatedJSON] {
var parser = JSONPathParser(string: path)
return try self.query(parser.parse())
}
/// Executes the given JSON path query and returns all matching JSON values.
public func query(values path: JSONPath) throws -> [JSON] {
return try JSONPathEvaluator(value: self).query(path).values
}
/// Executes the JSON path query given in JSON path syntax and returns all matching
/// JSON values.
public func query(values path: String) throws -> [JSON] {
var parser = JSONPathParser(string: path)
return try self.query(values: parser.parse())
}
/// Executes the given JSON path query and returns the locations of all matching JSON
/// values.
public func query(locations path: JSONPath) throws -> [JSONLocation] {
return try JSONPathEvaluator(value: self).query(path).locations
}
/// Executes the JSON path query given in JSON path syntax and returns the locations
/// of all matching JSON values.
public func query(locations path: String) throws -> [JSONLocation] {
var parser = JSONPathParser(string: path)
return try self.query(locations: parser.parse())
}
// MARK: - Transforming data
/// Merges this JSON value with the given JSON value `patch` recursively. Objects are
/// merged key by key with values from `patch` overriding values of the object represented
/// by this JSON value. All other types of JSON values are not merged and `patch` overrides
/// this JSON value. This implements the following algorithm as specified in RFC 7396 on
/// JSON Merge Patch.
///
/// define MergePatch(Target, Patch):
/// if Patch is an Object:
/// if Target is not an Object:
/// Target = {} // Ignore the contents and set it to an empty Object
/// for each Name/Value pair in Patch:
/// if Value is null:
/// if Name exists in Target:
/// remove the Name/Value pair from Target
/// else:
/// Target[Name] = MergePatch(Target[Name], Value)
/// return Target
/// else:
/// return Patch
public func merging(patch: JSON) -> JSON {
if case .object(let patch) = patch {
var result: [String : JSON]
if case .object(let target) = self {
result = target
} else {
result = [:]
}
for (member, value) in patch {
if value == .null {
result.removeValue(forKey: member)
} else {
result[member] = (result[member] ?? .null).merging(patch: value)
}
}
return .object(result)
} else {
return patch
}
}
/// Returns a new JSON value in which the value referenced by `ref` (any abstraction
/// implementing the `JSONReference` procotol, such as `JSONLocation` and `JSONPointer`)
/// was replaced with `json`.
public func updating(_ ref: JSONReference, with json: JSON) throws -> JSON {
return try ref.set(to: json, in: self)
}
/// Returns a new JSON value in which the value referenced by the JSON reference
/// string `ref` (string representation of either `JSONLocation` or `JSONPointer`
/// references) was replaced with `json`.
public func updating(_ ref: String, with json: JSON) throws -> JSON {
return try JSON.reference(from: ref).set(to: json, in: self)
}
/// Applies the given JSON patch object to this JSON document and returns the result
/// as a separate JSON document.
public func applying(patch: JSONPatch) throws -> JSON {
var value = self
try patch.apply(to: &value)
return value
}
// MARK: - Mutating data
/// Mutates this JSON value if it represents either an array or a string by appending
/// the given JSON value `json`. For arrays, `json` is appended as a new element. For
/// strings it is expected that `json` also refers to a string and `json` gets appended
/// as a string. For all other types of JSON values, an error is thrown.
public mutating func append(_ json: JSON) throws {
if case .array(var arr) = self {
self = .null // remove reference to arr
arr.append(json) // modify arr
self = .array(arr) // restore self
} else if case .string(var str) = self,
case .string(let ext) = json {
self = .null // remove reference to str
str.append(ext) // modify arr
self = .string(str) // restore self
} else {
throw Error.cannotAppend(json, self)
}
}
/// Mutates this JSON value if it represents either an array or a string by inserting
/// the given JSON value `json`. For arrays, `json` is inserted as a new element at
/// `index`. For strings it is expected that `json` also refers to a string and `json`
/// gets inserted into this string at position `index`. For all other types of JSON
/// values, an error is thrown.
public mutating func insert(_ json: JSON, at index: Int) throws {
if case .array(var arr) = self {
self = .null // remove reference to arr
arr.insert(json, at: index) // modify arr
arr.append(json) // modify arr
self = .array(arr) // restore self
} else if case .string(var str) = self,
case .string(let ext) = json {
self = .null // remove reference to str
str.insert(contentsOf: ext, at: str.index(str.startIndex, offsetBy: index))
str.append(ext) // modify arr
self = .string(str) // restore self
} else {
throw Error.cannotInsert(json, self, index)
}
}
/// Adds a new key/value mapping or updates an existing key/value mapping in this
/// JSON object. If this JSON value is not an object, an error is thrown.
public mutating func assign(_ member: String, to json: JSON) throws {
if case .object(var dict) = self {
self = .null // remove reference to dict
dict[member] = json // modify dict
self = .object(dict) // restore self
} else {
throw Error.cannotAppend(json, self)
}
}
/// Replaces the value the location reference `ref` is referring to with `json`. The
/// replacement is done in place, i.e. it mutates this JSON value. `ref` can be
/// implemented by any abstraction implementing the `JSONReference` procotol, such as
/// `JSONLocation` (for singular JSON path queries) and `JSONPointer`.
public mutating func update(_ ref: JSONReference, with json: JSON) throws {
try self.mutate(ref) { $0 = json }
}
/// Replaces the value the location reference string `ref` is referring to with `json`.
/// The replacement is done in place, i.e. it mutates this JSON value. `ref` is a string
/// representation of either `JSONLocation` or `JSONPointer` references.
public mutating func update(_ ref: String, with json: JSON) throws {
try self.mutate(ref) { $0 = json }
}
/// Mutates the JSON value the reference `ref` is referring to with function `proc`.
/// `proc` receives a reference to the JSON value, allowing efficient in place mutations
/// without automatically doing any copying. `ref` can be implemented by any abstraction
/// implementing the `JSONReference` procotol, such as `JSONLocation` (for singular JSON
/// path queries) and `JSONPointer`.
public mutating func mutate(_ ref: JSONReference, with proc: (inout JSON) throws -> Void) throws {
try ref.mutate(&self, with: proc)
}
/// Mutates the JSON value the reference `ref` is referring to with function `arrProc`
/// if the value is an array or `objProc` if the value is an object. For all other
/// cases, an error is thrown. This method allows for efficient in place mutations
/// without automatically doing any copying. `ref` can be implemented by any abstraction
/// implementing the `JSONReference` procotol, such as `JSONLocation` (for singular JSON
/// path queries) and `JSONPointer`.
public mutating func mutate(_ ref: JSONReference,
array arrProc: ((inout [JSON]) throws -> Void)? = nil,
object objProc: ((inout [String : JSON]) throws -> Void)? = nil,
other proc: ((inout JSON) throws -> Void)? = nil) throws {
try ref.mutate(&self, with: Self.mutator(array: arrProc, object: objProc, other: proc))
}
/// Mutates the JSON value the reference string `ref` is referring to with function `proc`.
/// `proc` receives a reference to the JSON value, allowing efficient in place mutations
/// without automatically doing any copying. `ref` is a string representation of either
/// `JSONLocation` or `JSONPointer` references.
public mutating func mutate(_ ref: String, with proc: (inout JSON) throws -> Void) throws {
try self.mutate(try JSON.reference(from: ref), with: proc)
}
/// Mutates the JSON array the reference string `ref` is referring to with function
/// `arrProc` if the value is an array or `objProc` if the value is an object. For
/// all other cases, an error is thrown. This method allows for efficient in place mutations
/// without automatically doing any copying. `ref` is a string representation of either
/// `JSONLocation` or `JSONPointer` references.
public mutating func mutate(_ ref: String,
array arrProc: ((inout [JSON]) throws -> Void)? = nil,
object objProc: ((inout [String : JSON]) throws -> Void)? = nil,
other proc: ((inout JSON) throws -> Void)? = nil) throws {
try self.mutate(try JSON.reference(from: ref), array: arrProc, object: objProc, other: proc)
}
/// Applies the given JSON Patch operation to this JSON document, mutating this JSON
/// document atomically (with transactional semantics).
public mutating func apply(operation: JSONPatchOperation) throws {
var value = self
try operation.apply(to: &value)
self = value
}
/// Applies the given JSON Patch operations to this JSON document, mutating this JSON
/// document atomically (with transactional semantics), i.e. if there is a failure during
/// the processing of the patch operation, this JSON document remains unchanged.
public mutating func apply(patch: JSONPatch) throws {
var value = self
try patch.apply(to: &value)
self = value
}
// MARK: - Schema validation
/// Returns true if this JSON document is valid for the given JSON schema (using
/// `registry` for resolving references to schema referred to from `schema`).
public func valid(for schema: JSONSchema, using registry: JSONSchemaRegistry? = nil) -> Bool {
return (try? self.validate(with: schema, using: registry))?.isValid ?? false
}
/// Returns a schema validation result for this JSON document validated against the
/// JSON schema `schema` (using`registry` for resolving references to schema referred to
/// from `schema`).
public func validate(with schema: JSONSchema, using registry: JSONSchemaRegistry? = nil) throws
-> JSONSchemaValidationResults {
let resource = try JSONSchemaResource(root: schema)
let registry = registry ?? JSONSchemaRegistry()
return try registry.validator(for: resource).validate(LocatedJSON(root: self))
}
// MARK: - String representations
/// Returns a pretty-printed representation of this JSON value with sorted keys in
/// object representations. Dates are encoded using ISO 8601. Floating-point numbers
/// denoting infinity are represented with the term "Infinity" respectively "-Infinity".
/// NaN values are denoted with "NaN".
public var description: String {
return (try? self.string(
formatting: [.prettyPrinted, .sortedKeys],
dateEncodingStrategy: .iso8601,
floatEncodingStrategy: .convertToString(positiveInfinity: "Infinity",
negativeInfinity: "-Infinity",
nan: "NaN"))) ?? "<invalid JSON>"
}
/// Returns a description of this JSON value for debugging purposes.
public var debugDescription: String {
switch self {
case .null:
return "null"
case .boolean(let bool):
return bool.description
case .integer(let number):
return number.description
case .float(let number):
return number.debugDescription
case .string(let str):
return str.debugDescription
default:
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
if let res = try? String(data: encoder.encode(self), encoding: .utf8) {
return res
} else {
return "<error>"
}
}
}
// MARK: - Utilities
/// Parse the given string either as a `JSONLocation` (i.e. using the JSON path
/// syntax) or `JSONPointer` (i.e. using the JSON path syntax). Empty strings or
/// strings starting with "/" are parsed as `JSONPointer` references; all other
/// strings are interpreted as `JSONLocation` references (with some flexibility
/// to omit the initial "$", for backward compatibility purposes).
public static func reference(from str: String) throws -> JSONReference {
if let first = str.first {
if first == "/" {
return try JSONPointer(str)
} else {
let trimmed = str.trimmingCharacters(in: CharacterSet.whitespaces)
if trimmed.first == "." {
return try JSONLocation("$" + trimmed)
} else if trimmed.first == "$" {
return try JSONLocation(trimmed)
} else {
return try JSONLocation("$." + trimmed)
}
}
} else {
return try JSONLocation("$")
}
}
/// Combines the three given closures into one mutator closure. This is useful to create
/// JSON document mutators with minimal copying.
public static func mutator(array arrProc: ((inout [JSON]) throws -> Void)? = nil,
object objProc: ((inout [String : JSON]) throws -> Void)? = nil,
other otherProc: ((inout JSON) throws -> Void)? = nil)
-> ((inout JSON) throws -> Void) {
return { value in
switch value {
case .array(var arr) where arrProc != nil:
value = .null
defer {
value = .array(arr)
}
try arrProc!(&arr)
case .object(var dict) where objProc != nil:
value = .null
defer {
value = .object(dict)
}
try objProc!(&dict)
default:
if let otherProc {
try otherProc(&value)
}
}
}
}
}