506 lines
16 KiB
Swift
506 lines
16 KiB
Swift
import Foundation
|
|
|
|
/// A representation of ambigious JSON.
|
|
///
|
|
/// # Heterogeneous Arrays.
|
|
///
|
|
/// Take for exmaple the JSON below:
|
|
///
|
|
/// {
|
|
/// "users": [
|
|
/// {
|
|
/// "age": 18,
|
|
/// "name": {
|
|
/// "first": "Caleb",
|
|
/// "last": "Kleveter"
|
|
/// }
|
|
/// }
|
|
/// ],
|
|
/// "metadata": [42, 3.14, true, "fizz buzz"]
|
|
/// }
|
|
///
|
|
/// The `users` data is pretty normal, but the `metadata` array can't be represented by standard arrays.
|
|
///
|
|
/// The `metadata` array, when decoded to a `JSON` instance, will result in this case:
|
|
///
|
|
/// JSON.array([.number(.int(42)), .number(.double(3.14)), .bool(true), .string("fizz buzz")])
|
|
///
|
|
/// # Value Unwrapping
|
|
///
|
|
/// Getting the associated value from an enum case can be a real pain,
|
|
/// so there are properties for each case to unwrap the value.
|
|
///
|
|
/// If you have the example JSON above, it is an `.object` case.
|
|
/// You can use the `.object` property to get the `[String: JSON]` value it wraps:
|
|
///
|
|
/// json.object // [String: JSON]?
|
|
///
|
|
/// There are also properties for `.null`, `.string`, `.bool`, `.int`, `.float`, `.double`, and `.array`.
|
|
/// Be sure to read the docs for the `.null` property.
|
|
///
|
|
/// These properties also have setters. They let you set the value of the current `JSON` case.
|
|
/// They will set the case regardless the current type, so if the case is a `.bool`,
|
|
/// you can use `.string` and the case will be changed to a `.string` case.
|
|
///
|
|
/// var json = JSON.bool(true)
|
|
/// json.string = "Fizz Buzz"
|
|
/// print(json) // "\"Fizz Buzz\""
|
|
///
|
|
/// # Dynamic Access
|
|
///
|
|
/// `JSON` supports `@dynamicMemberLookup`, so it is really easy to access values your JSON objects/arrays:
|
|
///
|
|
/// let firstname = json.users.0.name.first.string
|
|
///
|
|
/// This also works for setting JSON values:
|
|
///
|
|
/// json.users.0.name.first = "Tanner"
|
|
@dynamicMemberLookup
|
|
public enum JSON: Hashable, CustomStringConvertible {
|
|
|
|
// MARK: - Properties
|
|
|
|
/// Gets the value of a `.null` case.
|
|
///
|
|
/// You will always get `nil` from this property. This is because the
|
|
/// JSON case is itself `.null`, or it isn't so conversion failed and you get `nil`.
|
|
///
|
|
/// The `Wrapped` type is `Never` so the compiler can gurantee that the value is `nil`.
|
|
public var null: Optional<Never> {
|
|
get { return nil }
|
|
set { self = .null }
|
|
}
|
|
|
|
/// Accesses the value of a `.bool` case.
|
|
///
|
|
/// The setter will set the JSON case regardles the current type.
|
|
public var bool: Bool? {
|
|
get {
|
|
guard case let .bool(value) = self else { return nil }
|
|
return value
|
|
}
|
|
set {
|
|
self = newValue.map(JSON.bool) ?? .null
|
|
}
|
|
}
|
|
|
|
/// Accesses the value of a `.string` case.
|
|
///
|
|
/// The setter will set the JSON case regardles the current type.
|
|
public var string: String? {
|
|
get {
|
|
guard case let .string(value) = self else { return nil }
|
|
return value
|
|
}
|
|
set {
|
|
self = newValue.map(JSON.string) ?? .null
|
|
}
|
|
}
|
|
|
|
/// Accesses the value of a `Number.int` case wrapped in a `.number` case.
|
|
///
|
|
/// The setter will set the JSON case regardles the current type.
|
|
public var int: Int? {
|
|
get {
|
|
guard case let .number(.int(value)) = self else { return nil }
|
|
return value
|
|
}
|
|
set {
|
|
self = newValue.map(Number.int).map(JSON.number) ?? .null
|
|
}
|
|
}
|
|
|
|
/// Accesses the value of a `Number.float` case wrapped in a `.number` case.
|
|
///
|
|
/// The setter will set the JSON case regardles the current type.
|
|
public var float: Float? {
|
|
get {
|
|
guard case let .number(.float(value)) = self else { return nil }
|
|
return value
|
|
}
|
|
set {
|
|
self = newValue.map(Number.float).map(JSON.number) ?? .null
|
|
}
|
|
}
|
|
|
|
/// Accesses the value of a `Number.double` case wrapped in a `.number` case.
|
|
///
|
|
/// The setter will set the JSON case regardles the current type.
|
|
public var double: Double? {
|
|
get {
|
|
guard case let .number(.double(value)) = self else { return nil }
|
|
return value
|
|
}
|
|
set {
|
|
self = newValue.map(Number.double).map(JSON.number) ?? .null
|
|
}
|
|
}
|
|
|
|
/// Accesses the value of a `Number.decimal` case wrapped in a `.number` case.
|
|
///
|
|
/// The setter will set the JSON case regardles the current type.
|
|
public var decimal: Decimal? {
|
|
get {
|
|
guard case let .number(.decimal(value)) = self else { return nil }
|
|
return value
|
|
}
|
|
set {
|
|
self = newValue.map(Number.decimal).map(JSON.number) ?? .null
|
|
}
|
|
}
|
|
|
|
/// Accesses the value of an `.array` case.
|
|
///
|
|
/// The setter will set the JSON case regardles the current type.
|
|
public var array: [JSON]? {
|
|
get {
|
|
guard case let .array(value) = self else { return nil }
|
|
return value
|
|
}
|
|
set {
|
|
self = newValue.map(JSON.array) ?? .null
|
|
}
|
|
}
|
|
|
|
/// Accesses the value of an `.object` case.
|
|
///
|
|
/// The setter will set the JSON case regardles the current type.
|
|
public var object: [String: JSON]? {
|
|
get {
|
|
guard case let .object(value) = self else { return nil }
|
|
return value
|
|
}
|
|
set {
|
|
self = newValue.map(JSON.object) ?? .null
|
|
}
|
|
}
|
|
|
|
/// Converts a non-optiona JSON case to an optional JSON case.
|
|
///
|
|
/// If the JSON case is `.null`, then `nil` will be returned. Otherwise the JSON case is returned.
|
|
/// This allows you to unwrap a case to make sure it isn't `null`:
|
|
///
|
|
/// if let value = json.optional {
|
|
/// // ...
|
|
/// }
|
|
public var optional: Optional<JSON> {
|
|
switch self {
|
|
case .null: return nil
|
|
default: return self
|
|
}
|
|
}
|
|
|
|
// MARK: - Cases
|
|
|
|
/// A `null` JSON value.
|
|
///
|
|
/// This represents an explicit `null` value in the JSON. A non-existant key or index has no representation.
|
|
///
|
|
/// {
|
|
/// "foo": null
|
|
/// }
|
|
case null
|
|
|
|
/// A boolean JSON value.
|
|
///
|
|
/// {
|
|
/// "fizz": true,
|
|
/// "buzz": false
|
|
/// }
|
|
case bool(Bool)
|
|
|
|
/// A string JSON value.
|
|
///
|
|
/// {
|
|
/// "foo": "bar"
|
|
/// }
|
|
case string(String)
|
|
|
|
/// A number JSON value.
|
|
///
|
|
/// This case wraps a `Number` enum case, which will be an int, float, or double.
|
|
///
|
|
/// {
|
|
/// "answer": 42,
|
|
/// "pi": 3.1415
|
|
/// }
|
|
case number(Number)
|
|
|
|
/// A JSON array of values.
|
|
///
|
|
/// [
|
|
/// "foo",
|
|
/// "bar",
|
|
/// 1997,
|
|
/// ]
|
|
case array([JSON])
|
|
|
|
/// A JSON object which maps string keys to JSON values.
|
|
///
|
|
/// {
|
|
/// "foo: "bar",
|
|
/// "fizz": true,
|
|
/// "bar": [98, 97, 114]
|
|
/// }
|
|
case object([String: JSON])
|
|
|
|
|
|
// MARK: - Initializers
|
|
|
|
/// Creates a `JSON` instance with the `.null` case.
|
|
public init() {
|
|
self = .null
|
|
}
|
|
|
|
/// Creates a `JSON` instance with the `.string` case.
|
|
///
|
|
/// - Parameter string: The `String` held by the `.string` case.
|
|
public init(_ string: String) {
|
|
self = .string(string)
|
|
}
|
|
|
|
/// Creates a `JSON` instance with the `.bool` case.
|
|
///
|
|
/// - Parameter bool: The `Bool` held by the `.bool` case.
|
|
public init(_ bool: Bool) {
|
|
self = .bool(bool)
|
|
}
|
|
|
|
/// Creates a `JSON` instance with the `.number(Number.int)` case.
|
|
///
|
|
/// - Parameter int: The `Int` held by the `.int` case.
|
|
public init(_ int: Int) {
|
|
self = .number(.int(int))
|
|
}
|
|
|
|
/// Creates a `JSON` instance with the `.number(Number.float)` case.
|
|
///
|
|
/// - Parameter float: The `Float` held by the `.float` case.
|
|
public init(_ float: Float) {
|
|
self = .number(.float(float))
|
|
}
|
|
|
|
/// Creates a `JSON` instance with the `.number(Number.double)` case.
|
|
///
|
|
/// - Parameter double: The `Double` held by the `.double` case.
|
|
public init(_ double: Double) {
|
|
self = .number(.double(double))
|
|
}
|
|
|
|
/// Creates a `JSON` instance with the `.number(Number.decimal)` case.
|
|
///
|
|
/// - Parameter decimal: The `Decimal` held by the `.decimal` case.
|
|
public init(_ decimal: Decimal) {
|
|
self = .number(.decimal(decimal))
|
|
}
|
|
|
|
/// Creates a `JSON` instance with the `.array` case.
|
|
///
|
|
/// - Parameter array: The `JSON` values held by the `.array` case.
|
|
public init(_ array: [JSON]) {
|
|
self = .array(array)
|
|
}
|
|
|
|
/// Creates a `JSON` instance with the `.object` case.
|
|
///
|
|
/// - Parameter array: The `JSON` key/values map held by the `.object` case.
|
|
public init(_ object: [String: JSON]) {
|
|
self = .object(object)
|
|
}
|
|
|
|
/// Creates a `JSON` instance with the `.number` case.
|
|
///
|
|
/// - Parameter fwi: A fixed-width integer which is converted to an `Int` and then passed into a `.number(.int)` case.
|
|
public init<I>(_ fwi: I) where I: FixedWidthInteger {
|
|
self = .number(.int(Int(fwi)))
|
|
}
|
|
|
|
#if canImport(Foundation)
|
|
|
|
/// Creates a `JSON` instance from raw JSON data.
|
|
///
|
|
/// - Parameter data: The JSON data to decode to `JSON`.
|
|
public init(data: Data)throws {
|
|
self = try JSONDecoder().decode(JSON.self, from: data)
|
|
}
|
|
#endif
|
|
|
|
// MARK: - Public Methods
|
|
|
|
/// Gets the JSON value for a given key.
|
|
///
|
|
/// - Complexity: _O(n)_, where _n_ is the number of elements in the JSON array you are accessing object values from.
|
|
/// More often though, you are accessing object values via key, and that is _O(1)_.
|
|
///
|
|
/// Most of the time, the member passed in with be a `String` key. This will get a value for an object key:
|
|
///
|
|
/// json.user.first_name // .string("Tanner")
|
|
///
|
|
/// However, `Int` members are also supported in that case of a `JSON` array:
|
|
///
|
|
/// json.users.0.age // .number(.int(42))
|
|
///
|
|
/// - Parameter member: The key for JSON objects or index for JSON arrays of the value to get.
|
|
///
|
|
/// - Returns: The JSON value for the given key or index. `.null` is returned if the value is not found or
|
|
/// the JSON value this subscript is called on does not have nested data, i.e. `.string`, `.bool`, etc.
|
|
public subscript(dynamicMember member: String) -> JSON {
|
|
get {
|
|
switch self {
|
|
case let .object(object): return object[member] ?? .null
|
|
case let .array(array) where Int(member) != nil:
|
|
guard let index = Int(member), index >= array.startIndex && index < array.endIndex else { return .null }
|
|
return array[index]
|
|
default: return .null
|
|
}
|
|
}
|
|
set {
|
|
switch self {
|
|
case var .object(object):
|
|
object[member] = newValue
|
|
self = .object(object)
|
|
case var .array(array) where Int(member) != nil:
|
|
guard let index = Int(member) else { return }
|
|
array[index] = newValue
|
|
self = .array(array)
|
|
default: self = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Accesses the `JSON` value at a given key/index path.
|
|
///
|
|
/// - Complexity: _O(n)_ where _n_ is the number of elements in the path.
|
|
///
|
|
/// To get the value, `.get(_:)` is used. To set the value, `.set(_:to:)` is used.
|
|
///
|
|
/// - Parameter path: The key/index path to access.
|
|
/// - Returns: Thw JSON value(s) found at the path passed in.
|
|
public subscript (path: String...) -> JSON {
|
|
get {
|
|
return self.get(path)
|
|
}
|
|
set {
|
|
self.set(path, to: newValue)
|
|
}
|
|
}
|
|
|
|
/// Gets the JSON at a given path.
|
|
///
|
|
/// - Complexity: _O(n)_ where _n_ is the number of elements in the path.
|
|
///
|
|
/// If an `.array` case is found, the path key will be converted to an index and
|
|
/// the array element at that index will be returned. If key to index conversion fails,
|
|
/// or the index it outside the range of the array, `.null` is returned.
|
|
///
|
|
/// - Parameter path: The keys and indexes to the desired JSON value(s).
|
|
/// - Returns: Thw JSON value(s) found at the path passed in. You will get a `.null` case
|
|
/// if no JSON is found at the given path.
|
|
public func get(_ path: [String]) -> JSON {
|
|
return path.reduce(self) { json, key in
|
|
switch json {
|
|
case let .object(object): return object[key] ?? .null
|
|
case let .array(array) where Int(key) != nil:
|
|
guard let index = Int(key), index >= array.startIndex && index < array.endIndex else { return .null }
|
|
return array[index]
|
|
default: return .null
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sets the value of an object key or array index.
|
|
///
|
|
/// - Complexity: _O(n)_, where _n_ is the number of elements in the `path`. This method is
|
|
/// recursive, so you may have adverse performance for long paths.
|
|
///
|
|
/// - Parameters:
|
|
/// - path: The path of the value to set.
|
|
/// - json: The JSON value to set the index or key to.
|
|
public mutating func set<Path>(_ path: Path, to json: JSON) where Path: Collection, Path.Element == String {
|
|
if let key = path.first {
|
|
switch self {
|
|
case var .object(object):
|
|
if object[key] == nil { object[key] = .null }
|
|
object[key]?.set(path.dropFirst(), to: json)
|
|
self = .object(object)
|
|
case var .array(array) where Int(key) != nil:
|
|
guard let index = Int(key) else { return }
|
|
array[index].set(path.dropFirst(), to: json)
|
|
self = .array(array)
|
|
default:
|
|
var value = JSON.null
|
|
value.set(path.dropFirst(), to: json)
|
|
if let index = Int(key) {
|
|
self = .array(Array(repeating: .null, count: index) + [value])
|
|
} else {
|
|
self = .object([key: value])
|
|
}
|
|
}
|
|
} else {
|
|
self = json
|
|
}
|
|
}
|
|
|
|
/// Removes a key/value pair from an object at a given path.
|
|
///
|
|
/// The `JSON` type converts `nil` to it's `.null` case, so if you try to remove a value like this:
|
|
///
|
|
/// json["foo", "bar"] = nil
|
|
///
|
|
/// You just set the object's property to `null`:
|
|
///
|
|
/// {
|
|
/// "foo": {
|
|
/// "bar": null
|
|
/// }
|
|
/// }
|
|
///
|
|
/// To actually remove a property from an object, you use `.remove(_:)` with the path to the property to remove:
|
|
///
|
|
/// json.remove(["foo", "bar"])
|
|
///
|
|
/// Will result in this json structure:
|
|
///
|
|
/// {
|
|
/// "foo": {}
|
|
/// }
|
|
///
|
|
/// - Parameter path: The key path to the json property to remove.
|
|
///
|
|
/// - Complexity: _O(n)_, where _n_ is the number of elements in the path to remove.
|
|
/// Keep in mind that this method is recursive, so each succesive eleemnt in the path will
|
|
/// add another call to the stack.
|
|
public mutating func remove<Path>(_ path: Path) where Path: Collection, Path.Element == String {
|
|
guard path.count > 0 else { return }
|
|
if let key = path.first {
|
|
guard var object = self.object else { return }
|
|
|
|
if path.count == 1 {
|
|
object[key] = nil
|
|
self.object = object
|
|
} else {
|
|
if var json = object[key] {
|
|
json.remove(path.dropFirst())
|
|
self[key] = json
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// See `CustomStringConvertible.description`.
|
|
///
|
|
/// This textal representation is compressed, so you might need to prettify it to read it.
|
|
public var description: String {
|
|
switch self {
|
|
case .null: return "null"
|
|
case let .string(string): return #""\#(string)""#
|
|
case let .number(number): return number.description
|
|
case let .bool(bool): return bool.description
|
|
case let .array(array): return "[" + array.map { $0.description }.joined(separator: ",") + "]"
|
|
case let .object(object):
|
|
let data = object.map { "\"" + $0.key + "\":" + $0.value.description }.joined(separator: ",")
|
|
return "{" + data + "}"
|
|
}
|
|
}
|
|
}
|