Repository: robb/RBBJSON
Branch: main
Commit: b6a8ac6f2cd8
Files: 26
Total size: 52.7 KB
Directory structure:
gitextract_zivqdtik/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── swift_linux.yml
│ └── swift_macos.yml
├── .gitignore
├── LICENSE.md
├── Package.swift
├── README.md
├── Sources/
│ └── RBBJSON/
│ ├── RBBJSON+Boolean.swift
│ ├── RBBJSON+Comparable.swift
│ ├── RBBJSON+Date.swift
│ ├── RBBJSON+Numbers.swift
│ ├── RBBJSON+PlaygroundSupport.swift
│ ├── RBBJSON+String.swift
│ ├── RBBJSON.swift
│ └── RBBJSONQuery.swift
└── Tests/
├── LinuxMain.swift
└── RBBJSONTests/
├── BooleanTests.swift
├── ComparisonTests.swift
├── DateTests.swift
├── NumbersTests.swift
├── QueryTests.swift
├── RBBJSON+Literals.swift
├── RBBJSONTests.swift
├── READMETests.swift
├── SequenceTests.swift
└── StringTests.swift
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: robb
================================================
FILE: .github/workflows/swift_linux.yml
================================================
name: Swift Linux
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Build
run: swift build -v
- name: Run tests
run: swift test --enable-test-discovery -v
================================================
FILE: .github/workflows/swift_macos.yml
================================================
name: Swift macOS
on: [push]
jobs:
build:
runs-on: macOS-latest
steps:
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_12.3.app
- uses: actions/checkout@v1
- name: Build
run: swift build -v
- name: Run tests
run: swift test --enable-test-discovery -v
================================================
FILE: .gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2020 Robert Böhnke
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Package.swift
================================================
// swift-tools-version:5.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "RBBJSON",
platforms: [
.iOS(.v13),
.macOS(.v10_15)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "RBBJSON",
targets: ["RBBJSON"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "RBBJSON",
dependencies: []),
.testTarget(
name: "RBBJSONTests",
dependencies: ["RBBJSON"]),
]
)
================================================
FILE: README.md
================================================
# RBBJSON
RBBJSON enables flexible JSON traversal at runtime and [JSONPath]-like querying for rapid prototyping.
Use `JSONDecoder` to create an `RBBJSON` struct, then traverse it using [dynamic member lookup][dml]:
```swift
let json = try JSONDecoder().decode(RBBJSON.self, from: data)
json.firstName // RBBJSON.string("John")
json.lastName // RBBJSON.string("Appleseed")
json.age // RBBJSON.number(26)
json.invalidKey // RBBJSON.null
json.phoneNumbers[0] // RBBJSON.string("+14086065775")
```
If you want to access a value that coincides with a Swift-defined property, use a `String` subscript instead:
```swift
json.office.map // Error: Maps to Sequence.map
json.office["map"] // RBBJSON.string("https://maps.apple.com/?q=IL1")
```
To unbox a JSON value, use one of the failable initializers:
```swift
String(json.firstName) // "John"
String(json.lastName) // "Appleseed"
String(json.age) // nil
Int(json.age) // 26
Double(json.age) // 26.0
```
You can also make use of a [JSONPath]-inspired Query syntax to find nested data inside a JSON structure.
For example, given:
```json
{
"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
}
}
}
```
|JSONPath|RBBJSON|Result|
|-|-|-|
|`$.store.book[*].author`|`json.store.book[any: .child].author`|[The authors of all books in the store.](/Tests/RBBJSONTests/READMETests.swift#L46-L51)|
|`$..author`|`json[any: .descendantOrSelf].author`|[All authors.](/Tests/RBBJSONTests/READMETests.swift#L56-L61)|
|`$.store.*`|`json.store[any: .child]`|[All things in the store, a list of books an a red bycicle.](/Tests/RBBJSONTests/READMETests.swift#L66-L99)|
|`$.store..price`|`json.store[any: .descendantOrSelf].price`|[All prices in the store.](/Tests/RBBJSONTests/READMETests.swift#L104-L110)|
|`$..book[2]`|`json[any: .descendantOrSelf].book[2]`|[The second book.](/Tests/RBBJSONTests/READMETests.swift#L115-L123)|
|`$..book[-2]`|`json[any: .descendantOrSelf].book[-2]`|[The second-to-last book.](/Tests/RBBJSONTests/READMETests.swift#L128-L136)|
|`$..book[0,1]`, `$..book[:2]`|`json[any: .descendantOrSelf].book[0, 1])`, `json[any: .descendantOrSelf].book[0...1])`, `json[any: .descendantOrSelf].book[0..<2])`|[The first two books.](/Tests/RBBJSONTests/READMETests.swift#L141-L154)|
|`$..book[?(@.isbn)]`|`json[any: .descendantOrSelf].book[has: \.isbn]`|[All books with an ISBN number.](/Tests/RBBJSONTests/READMETests.swift#L159-L174)|
|`$..book[?(@.price<10)]`|`json.store.book[matches: { $0.price <= 10 }]`|[All books cheaper than `10`.](/Tests/RBBJSONTests/READMETests.swift#L179-L193)|
|`$.store["book", "bicycle"]..["price", "author"]`|`json.store["book", "bicycle"][any: .descendantOrSelf]["price", "author"]`|[The author (where available) and price of every book or bicycle.](/Tests/RBBJSONTests/READMETests.swift#L203-L223)|
Once you query a JSON value using one of the higher order selectors, the resulting type of the expression will be a lazy `RBBJSONQuery`:
```swift
json.store.book[0]["title"] // RBBJSON.string("Sayings of the Century")
json.store.book[0, 1]["title"] // some RBBJSONQuery
```
Because `RBBJSONQuery` conforms to `Sequence`, you can initialize an `Array` with it to obtain the results or use e.g. `compactMap`:
```swift
String(json.store.book[0].title) // "Sayings of the Century"
json.store.book[0, 1].title.compactMap(String.init) // ["Sayings of the Century", "Sword of Honour"]
String(json.store.book[0]["invalid Property"]) // nil
json.store.book[0, 1]["invalid Property"].compactMap(String.init) // []
```
---
[jsonpath]: https://goessner.net/articles/JsonPath/
[dml]: https://oleb.net/blog/2018/06/dynamic-member-lookup/
================================================
FILE: Sources/RBBJSON/RBBJSON+Boolean.swift
================================================
import Foundation
public extension Bool {
init?(_ json: RBBJSON, lenient: Bool = false) {
switch (json, lenient) {
case (.bool(let value), _):
self = value
case (.string("true"), true):
self = true
case (.string("false"), true):
self = false
case (.number(let value), true):
self = value != 0
default:
return nil
}
}
}
================================================
FILE: Sources/RBBJSON/RBBJSON+Comparable.swift
================================================
import Foundation
extension RBBJSON: Comparable {
static func sortKey(value: RBBJSON) -> Int {
switch value {
case .object:
return 1
case .array:
return 2
case .number:
return 3
case .string:
return 4
case .bool:
return 5
case .null:
return 6
}
}
/// Compares two `RBBJSON` values against each other.
///
/// Note that this is not equivalent to JavaScript's comparison of
/// heterogenous values.
public static func < (lhs: RBBJSON, rhs: RBBJSON) -> Bool {
switch (lhs, rhs) {
case (.object(let l), .object(let r)):
return l.count < r.count
case (.array(let l), .array(let r)):
return l.count < r.count
case (.string(let l), .string(let r)):
return l < r
case (.number(let l), .number(let r)):
return l < r
case (.bool(let l), .bool(let r)):
return (l ? 1 : 0) < (r ? 1 : 0)
case (.null, .null):
return false
default:
return sortKey(value: lhs) < sortKey(value: rhs)
}
}
}
================================================
FILE: Sources/RBBJSON/RBBJSON+Date.swift
================================================
import Foundation
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
public extension Date {
enum JSONDateStringDecodingStrategy: Hashable {
fileprivate static let isoParser = ISO8601DateFormatter()
case iso8601
case formatted(DateFormatter)
}
enum JSONDateNumberDecodingStrategy {
case secondsSince1970
case millisecondsSince1970
}
init?(_ json: RBBJSON, stringDecodingStrategoy: JSONDateStringDecodingStrategy = .iso8601, numberDecodingStrategoy: JSONDateNumberDecodingStrategy = .secondsSince1970) {
switch (json, stringDecodingStrategoy, numberDecodingStrategoy) {
case let (.number(value), _, .secondsSince1970):
self = Date(timeIntervalSince1970: value)
case let (.number(value), _, .millisecondsSince1970):
self = Date(timeIntervalSince1970: value / 1000)
case let (.string(value), .iso8601, _):
if let date = JSONDateStringDecodingStrategy.isoParser.date(from: value) {
self = date
} else {
return nil
}
case let (.string(value), .formatted(formatter), _):
if let date = formatter.date(from: value) {
self = date
} else {
return nil
}
default:
return nil
}
}
}
================================================
FILE: Sources/RBBJSON/RBBJSON+Numbers.swift
================================================
import Foundation
#if canImport(CoreGraphics)
import CoreGraphics
public extension CGFloat {
init?(_ json: RBBJSON, lenient: Bool = false) {
switch (json, lenient) {
case (.number(let value), _):
self = Self(value)
case (.string(let string), true):
guard let double = Double(string) else {
return nil
}
self.init(double)
default:
return nil
}
}
}
#endif
public extension Double {
init?(_ json: RBBJSON, lenient: Bool = false) {
switch (json, lenient) {
case (.number(let value), _):
self = Self(value)
case (.string(let string), true):
self.init(string)
default:
return nil
}
}
}
public extension FixedWidthInteger {
init?(_ json: RBBJSON, lenient: Bool = false) {
switch (json, lenient) {
case (.number(let value), _):
self = Self(value)
case (.string(let string), true):
if let value = Self(string) {
self = value
} else if let double = Double(json, lenient: true) {
self.init(double)
} else {
return nil
}
default:
return nil
}
}
}
public extension Float {
init?(_ json: RBBJSON, lenient: Bool = false) {
switch (json, lenient) {
case (.number(let value), _):
self = Self(value)
case (.string(let string), true):
self.init(string)
default:
return nil
}
}
}
================================================
FILE: Sources/RBBJSON/RBBJSON+PlaygroundSupport.swift
================================================
import Foundation
extension RBBJSON: CustomPlaygroundDisplayConvertible {
public var playgroundDescription: Any {
switch self {
case .object(let object):
return object.mapValues(\.playgroundDescription)
case .array(let values):
return values.map(\.playgroundDescription)
case .string(let value):
return value
case .number(let value):
return value
case .bool(let value):
return value
case .null:
return Optional.none as Any
}
}
}
================================================
FILE: Sources/RBBJSON/RBBJSON+String.swift
================================================
import Foundation
public extension String {
init?(_ json: RBBJSON) {
if case .string(let value) = json {
self = value
} else {
return nil
}
}
}
================================================
FILE: Sources/RBBJSON/RBBJSON.swift
================================================
import Foundation
fileprivate struct JSONCodingKeys: CodingKey {
internal var stringValue: String
internal init?(stringValue: String) {
self.stringValue = stringValue
}
internal var intValue: Int?
internal init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
@dynamicMemberLookup
public enum RBBJSON: Hashable, Codable {
case object([String: RBBJSON])
case array([RBBJSON])
case string(String)
case number(Double)
case bool(Bool)
case null
public init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: JSONCodingKeys.self) {
self = try RBBJSON(container: container)
} else if var container = try? decoder.unkeyedContainer() {
self = try RBBJSON(container: &container)
} else if let container = try? decoder.singleValueContainer() {
if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let number = try? container.decode(Double.self) {
self = .number(number)
} else if let string = try? container.decode(String.self) {
self = .string(string)
} else if container.decodeNil() {
self = .null
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: ""))
}
}
else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: ""))
}
}
private init(container: KeyedDecodingContainer) throws {
let values = try container.allKeys.map { codingKey -> RBBJSON in
try container.decode(RBBJSON.self, forKey: codingKey)
}
let zipped = zip(container.allKeys.map(\.stringValue), values)
self = .object(Dictionary(zipped) { a, _ in a })
}
private init(container: inout UnkeyedDecodingContainer) throws {
var values: [RBBJSON] = []
while !container.isAtEnd {
values.append(try container.decode(RBBJSON.self))
}
self = .array(values)
}
public func encode(to encoder: Encoder) throws {
switch self {
case .object(let object):
var container = encoder.container(keyedBy: JSONCodingKeys.self)
for (key, value) in object {
let codingKey = JSONCodingKeys(stringValue: key)!
try container.encode(value, forKey: codingKey)
}
case .array(let array):
var container = encoder.unkeyedContainer()
for value in array {
try container.encode(value)
}
case .string(let string):
var container = encoder.singleValueContainer()
try container.encode(string)
case .number(let number):
var container = encoder.singleValueContainer()
try container.encode(number)
case .bool(let bool):
var container = encoder.singleValueContainer()
try container.encode(bool)
case .null:
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
public subscript(index: Int) -> RBBJSON {
guard case .array(let array) = self else { return .null }
return array[wrapping: index] ?? .null
}
public subscript(key: String) -> RBBJSON {
guard case .object(let object) = self else { return .null }
return object[key] ?? .null
}
public subscript(dynamicMember member: String) -> RBBJSON {
self[member]
}
public static func keys(_ json: RBBJSON) -> [String] {
switch json {
case .object(let object):
return Array(object.keys).sortedIfDebug
default:
return []
}
}
public static func values(_ json: RBBJSON) -> [RBBJSON] {
switch json {
case .object(let object):
return Array(object.values).sortedIfDebug
case .array(let array):
return array
default:
return []
}
}
}
extension RBBJSON: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .object(let object):
return object.debugDescription
case .array(let array):
return array.debugDescription
case .string(let string):
return string.debugDescription
case .number(let number):
return number.debugDescription
case .bool(let bool):
return bool ? "true" : "false"
case .null:
return "null"
}
}
}
================================================
FILE: Sources/RBBJSON/RBBJSONQuery.swift
================================================
import Foundation
@dynamicMemberLookup
public protocol RBBJSONQuery: Sequence, CustomPlaygroundDisplayConvertible where Element == RBBJSON {
}
extension RBBJSONQuery {
var playgroundDescription: Any {
map(\.playgroundDescription)
}
}
public extension RBBJSON {
enum Axis {
/// Matches any immediate child of a JSON object or array.
case child
/// Matches any immediate or transitive child of a JSON object or array as
/// well as itself.
case descendantOrSelf
}
/// Matches multiple indices on a JSON array. Negative indices can be
/// used to index from the end.
subscript(indices: Int...) -> some RBBJSONQuery {
IndicesSequence(base: CollectionOfOne(self), indices: indices)
}
/// Matches a range of indices on a JSON array. Negative indices are not
/// allowed.
subscript(range: Range) -> some RBBJSONQuery {
RangeSequence(range: range, base: CollectionOfOne(self))
}
/// Matches a range of indices on a JSON array. Negative indices are not
/// allowed.
subscript(range: ClosedRange) -> some RBBJSONQuery {
RangeSequence(range: range.lowerBound ..< range.upperBound + 1, base: CollectionOfOne(self))
}
/// Matches values on a JSON object or array that the given `predicate`
/// returns `true` for.
subscript(matches predicate: @escaping (RBBJSON) -> Bool) -> some RBBJSONQuery {
PredicateSequence(predicate: predicate, base: CollectionOfOne(self))
}
/// Matches values on a JSON object or array that the given `keyPath`
/// returns anything but `null` for, this includes values such as `0`,
/// `false` or `""` that Javascript would consider falsy.
subscript(has keyPath: KeyPath) -> some RBBJSONQuery {
self[matches: { $0[keyPath: keyPath] != .null }]
}
subscript(any axis: Axis) -> some RBBJSONQuery {
AxisSequence(axis: axis, base: CollectionOfOne(self))
}
subscript(keys: String...) -> some RBBJSONQuery {
KeysSequence(keys: keys, base: CollectionOfOne(self))
}
}
extension RBBJSON {
@dynamicMemberLookup
struct KeySequence: RBBJSONQuery where Base: Sequence, Base.Element == RBBJSON {
var key: String
var base: Base
public func makeIterator() -> AnyIterator {
let underlying = base.lazy.map { $0[key] }
.filter { $0 != .null }
.makeIterator()
return AnyIterator(underlying)
}
}
@dynamicMemberLookup
struct KeysSequence: RBBJSONQuery where Base: Sequence, Base.Element == RBBJSON {
var keys: [String]
var base: Base
public func makeIterator() -> AnyIterator {
let underlying = base
.lazy
.compactMap { object -> RBBJSON? in
let keysAndValues: [(String, RBBJSON)] = keys.compactMap { key in
let value = object[key]
guard value != .null else { return nil }
return (key, value)
}
if !keysAndValues.isEmpty {
return .object(Dictionary(keysAndValues) { a, _ in a })
} else {
return nil
}
}
.makeIterator()
return AnyIterator(underlying)
}
}
@dynamicMemberLookup
struct AnyChildSequence: RBBJSONQuery where Base: Sequence, Base.Element == RBBJSON {
var base: Base
public func makeIterator() -> AnyIterator {
let underlying = base.lazy.flatMap {
RBBJSON.values($0)
}
.makeIterator()
return AnyIterator(underlying)
}
}
@dynamicMemberLookup
struct IndicesSequence: RBBJSONQuery where Base: Sequence, Base.Element == RBBJSON {
var base: Base
var indices: [Int]
public func makeIterator() -> AnyIterator {
let underlying = base.lazy.flatMap { object -> [RBBJSON] in
let results = indices.map { object[$0] }.filter { $0 != .null }
if results.isEmpty {
return []
} else {
return results
}
}
.makeIterator()
return AnyIterator(underlying)
}
}
@dynamicMemberLookup
struct RangeSequence: RBBJSONQuery where Base: Sequence, Base.Element == RBBJSON {
var range: Range
var base: Base
public func makeIterator() -> AnyIterator {
let underlying = base.lazy.flatMap { object -> AnySequence in
switch object {
case let .array(array):
let clampedRange = range.clamped(to: array.indices)
return AnySequence(array[clampedRange])
default:
return AnySequence(EmptyCollection())
}
}
.makeIterator()
return AnyIterator(underlying)
}
}
@dynamicMemberLookup
struct PredicateSequence: RBBJSONQuery where Base: Sequence, Base.Element == RBBJSON {
var predicate: (RBBJSON) -> Bool
var base: Base
public func makeIterator() -> AnyIterator {
let underlying = base.lazy.flatMap { object -> AnySequence in
switch object {
case let .array(array):
return AnySequence(array.lazy.filter(predicate))
case .object where predicate(object):
return AnySequence(CollectionOfOne(object))
default:
return AnySequence(EmptyCollection())
}
}
.makeIterator()
return AnyIterator(underlying)
}
}
@dynamicMemberLookup
struct AxisSequence: RBBJSONQuery where Base: Sequence, Base.Element == RBBJSON {
var axis: Axis
var base: Base
public func makeIterator() -> AnyIterator {
switch axis {
case .child:
return AnyChildSequence(base: base).makeIterator()
case .descendantOrSelf:
let underlying = base.lazy.flatMap {
RecursiveDescentSequence(json: $0)
}
.makeIterator()
return AnyIterator(underlying)
}
}
}
struct RecursiveDescentSequence: RBBJSONQuery {
var json: RBBJSON
struct Iterator: IteratorProtocol {
typealias Element = RBBJSON
var stack: [RBBJSON]
mutating func next() -> RBBJSON? {
while !stack.isEmpty {
let json = stack.removeLast()
switch json {
case .null, .bool, .string, .number:
continue
case .array(let array):
stack.append(contentsOf: array.reversed())
return json
case .object(let object):
#if DEBUG
stack.append(contentsOf: Array(object.values).sortedIfDebug.reversed())
#else
stack.append(contentsOf: object.values)
#endif
return json
}
}
return nil
}
}
func makeIterator() -> Iterator {
Iterator(stack: [json])
}
}
}
public extension RBBJSONQuery {
/// Matches a particular index on a JSON array. Negative indices can be
/// used to index from the end.
subscript(index: Int) -> some RBBJSONQuery {
RBBJSON.IndicesSequence(base: self, indices: [index])
}
/// Matches multiple indices on a JSON array. Negative indices can be
/// used to index from the end.
subscript(indices: Int...) -> some RBBJSONQuery {
RBBJSON.IndicesSequence(base: self, indices: indices)
}
/// Matches a particular key on a JSON object.
subscript(key: String) -> some RBBJSONQuery {
RBBJSON.KeySequence(key: key, base: self)
}
subscript(keys: String...) -> some RBBJSONQuery {
RBBJSON.KeysSequence(keys: keys, base: self)
}
/// Matches a particular key on a JSON object.
subscript(dynamicMember dynamicMember: String) -> some RBBJSONQuery {
RBBJSON.KeySequence(key: dynamicMember, base: self)
}
/// Matches values on a JSON object or array that the given `keyPath`
/// returns anything but `null` for, this includes values such as `0`,
/// `false` or `""` that Javascript would consider falsy.
subscript(has keyPath: KeyPath) -> some RBBJSONQuery {
RBBJSON.PredicateSequence(predicate: { $0[keyPath: keyPath] != .null }, base: self)
}
/// Matches a range of indices on a JSON array. Negative indices are not
/// allowed.
subscript(range: Range) -> some RBBJSONQuery {
RBBJSON.RangeSequence(range: range, base: self)
}
/// Matches a range of indices on a JSON array. Negative indices are not
/// allowed.
subscript(range: ClosedRange) -> some RBBJSONQuery {
RBBJSON.RangeSequence(range: range.lowerBound ..< range.upperBound + 1, base: self)
}
subscript(any axis: RBBJSON.Axis) -> some RBBJSONQuery {
RBBJSON.AxisSequence(axis: axis, base: self)
}
/// Matches values on a JSON object or array that the given `predicate`
/// returns `true` for.
subscript(matches predicate: @escaping (RBBJSON) -> Bool) -> some RBBJSONQuery {
RBBJSON.PredicateSequence(predicate: predicate, base: self)
}
}
internal extension Array where Element == RBBJSON {
subscript(wrapping index: Int) -> Element? {
if index < 0 {
return self[safe: index + count]
} else {
return self[safe: index]
}
}
subscript(safe index: Int) -> Element? {
indices.contains(index) ? self[index] : nil
}
}
internal extension Sequence where Element: Comparable {
#if DEBUG
var sortedIfDebug: [Element] {
sorted()
}
#else
var sortedIfDebug: Self {
self
}
#endif
}
================================================
FILE: Tests/LinuxMain.swift
================================================
fatalError("Run the tests with `swift test --enable-test-discovery`.")
================================================
FILE: Tests/RBBJSONTests/BooleanTests.swift
================================================
import XCTest
import RBBJSON
@available(iOS 10.0, *)
final class BoolTests: XCTestCase {
func testBooleanConversion() {
XCTAssertEqual(Bool("foo" as RBBJSON), nil)
XCTAssertEqual(Bool("" as RBBJSON), nil)
XCTAssertEqual(Bool("false" as RBBJSON), nil)
XCTAssertEqual(Bool("true" as RBBJSON), nil)
XCTAssertEqual(Bool(0 as RBBJSON), nil)
XCTAssertEqual(Bool(1 as RBBJSON), nil)
XCTAssertEqual(Bool(3 as RBBJSON), nil)
XCTAssertEqual(Bool(true as RBBJSON), true)
XCTAssertEqual(Bool(false as RBBJSON), false)
XCTAssertEqual(Bool("foo" as RBBJSON, lenient: true), nil)
XCTAssertEqual(Bool("" as RBBJSON, lenient: true), nil)
XCTAssertEqual(Bool("false" as RBBJSON, lenient: true), false)
XCTAssertEqual(Bool("true" as RBBJSON, lenient: true), true)
XCTAssertEqual(Bool(0 as RBBJSON, lenient: true), false)
XCTAssertEqual(Bool(1 as RBBJSON, lenient: true), true)
XCTAssertEqual(Bool(3 as RBBJSON, lenient: true), true)
XCTAssertEqual(Bool(true as RBBJSON, lenient: true), true)
XCTAssertEqual(Bool(false as RBBJSON, lenient: true), false)
}
}
================================================
FILE: Tests/RBBJSONTests/ComparisonTests.swift
================================================
import XCTest
import RBBJSON
final class ComparisonTests: XCTestCase {
func testHeterogenousSorting() {
let json = [
false,
"c",
nil,
3, 123, -2, 0,
"b",
[ 1, 2, 3 ],
[ "one": 1, "zero": 0 ],
"a",
true
] as [RBBJSON]
XCTAssertEqual(json.sorted(), [
[ "one": 1, "zero": 0 ],
[ 1, 2, 3 ],
-2,
0,
3,
123,
"a", "b", "c",
false,
true,
nil,
])
}
}
================================================
FILE: Tests/RBBJSONTests/DateTests.swift
================================================
import XCTest
import RBBJSON
@available(iOS 10.0, *)
final class DateTests: XCTestCase {
func testDateConversion() {
XCTAssertEqual(Date("foo" as RBBJSON), nil)
XCTAssertEqual(Date(1604942100 as RBBJSON), Date(timeIntervalSince1970: 1604942100))
XCTAssertEqual(Date(1604942100000 as RBBJSON, numberDecodingStrategoy: .millisecondsSince1970), Date(timeIntervalSince1970: 1604942100))
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "E, d MMM yyyy HH:mm:ss Z"
XCTAssertEqual(Date("2020-11-09T17:15:00Z" as RBBJSON), Date(timeIntervalSince1970: 1604942100))
XCTAssertEqual(Date("Mon, 9 Nov 2020 17:15:00 +0000" as RBBJSON, stringDecodingStrategoy: .formatted(formatter)), Date(timeIntervalSince1970: 1604942100))
}
}
================================================
FILE: Tests/RBBJSONTests/NumbersTests.swift
================================================
import XCTest
import RBBJSON
@available(iOS 10.0, *)
final class NumbersTests: XCTestCase {
func testNumberConversion() {
do {
XCTAssertEqual(Int("foo" as RBBJSON), nil)
XCTAssertEqual(Int("" as RBBJSON), nil)
XCTAssertEqual(Int("123.4" as RBBJSON), nil)
XCTAssertEqual(Int(123.4 as RBBJSON), 123)
XCTAssertEqual(Int(0 as RBBJSON), 0)
XCTAssertEqual(Int("foo" as RBBJSON, lenient: true), nil)
XCTAssertEqual(Int("" as RBBJSON, lenient: true), nil)
XCTAssertEqual(Int("123" as RBBJSON, lenient: true), 123)
XCTAssertEqual(Int("123.4" as RBBJSON, lenient: true), 123)
XCTAssertEqual(Int(123.4 as RBBJSON, lenient: true), 123)
XCTAssertEqual(Int(0 as RBBJSON, lenient: true), 0)
}
do {
XCTAssertEqual(UInt32("foo" as RBBJSON), nil)
XCTAssertEqual(UInt32("" as RBBJSON), nil)
XCTAssertEqual(UInt32("123.4" as RBBJSON), nil)
XCTAssertEqual(UInt32(123.4 as RBBJSON), 123)
XCTAssertEqual(UInt32(0 as RBBJSON), 0)
XCTAssertEqual(UInt32("foo" as RBBJSON, lenient: true), nil)
XCTAssertEqual(UInt32("" as RBBJSON, lenient: true), nil)
XCTAssertEqual(UInt32("123" as RBBJSON, lenient: true), 123)
XCTAssertEqual(UInt32("123.4" as RBBJSON, lenient: true), 123)
XCTAssertEqual(UInt32(123.4 as RBBJSON, lenient: true), 123)
XCTAssertEqual(UInt32(0 as RBBJSON, lenient: true), 0)
}
#if canImport(CoreGraphics)
do {
XCTAssertEqual(CGFloat("foo" as RBBJSON), nil)
XCTAssertEqual(CGFloat("" as RBBJSON), nil)
XCTAssertEqual(CGFloat("123.4" as RBBJSON), nil)
XCTAssertEqual(CGFloat(123.4 as RBBJSON), 123.4)
XCTAssertEqual(CGFloat(0 as RBBJSON), 0)
XCTAssertEqual(CGFloat("foo" as RBBJSON, lenient: true), nil)
XCTAssertEqual(CGFloat("" as RBBJSON, lenient: true), nil)
XCTAssertEqual(CGFloat("123.4" as RBBJSON, lenient: true), 123.4)
XCTAssertEqual(CGFloat(123.4 as RBBJSON, lenient: true), 123.4)
XCTAssertEqual(CGFloat(0 as RBBJSON, lenient: true), 0)
}
#endif
do {
XCTAssertEqual(Double("foo" as RBBJSON), nil)
XCTAssertEqual(Double("" as RBBJSON), nil)
XCTAssertEqual(Double("123.4" as RBBJSON), nil)
XCTAssertEqual(Double(123.4 as RBBJSON), 123.4)
XCTAssertEqual(Double(0 as RBBJSON), 0)
XCTAssertEqual(Double("foo" as RBBJSON, lenient: true), nil)
XCTAssertEqual(Double("" as RBBJSON, lenient: true), nil)
XCTAssertEqual(Double("123.4" as RBBJSON, lenient: true), 123.4)
XCTAssertEqual(Double(123.4 as RBBJSON, lenient: true), 123.4)
XCTAssertEqual(Double(0 as RBBJSON, lenient: true), 0)
}
do {
XCTAssertEqual(Float("foo" as RBBJSON), nil)
XCTAssertEqual(Float("" as RBBJSON), nil)
XCTAssertEqual(Float("123.4" as RBBJSON), nil)
XCTAssertEqual(Float(123.4 as RBBJSON), 123.4)
XCTAssertEqual(Float(0 as RBBJSON), 0)
XCTAssertEqual(Float("foo" as RBBJSON, lenient: true), nil)
XCTAssertEqual(Float("" as RBBJSON, lenient: true), nil)
XCTAssertEqual(Float("123.4" as RBBJSON, lenient: true), 123.4)
XCTAssertEqual(Float(123.4 as RBBJSON, lenient: true), 123.4)
XCTAssertEqual(Float(0 as RBBJSON, lenient: true), 0)
}
}
}
================================================
FILE: Tests/RBBJSONTests/QueryTests.swift
================================================
import XCTest
import RBBJSON
private let json = [
"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
] as RBBJSON
final class QueryTests: XCTestCase {
func testQueriesMultipleIndices() {
// JSONPath: $..book[0,1].price
RBBAssertEqual(json[any: .descendantOrSelf].book[0, 1].price, [
8.95,
12.99
])
}
func testQueriesMultipleNegativeIndices() {
// JSONPath: $..book[-4,-3].price
RBBAssertEqual(json[any: .descendantOrSelf].book[-4, -3].price, [
8.95,
12.99
])
}
func testRange() {
// JSONPath: $..book[1:2]
RBBAssertEqual(json[any: .descendantOrSelf].book[1..<2], [
[
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
]
])
}
func testPredicateOnObject() {
// JSONPath: $.store.bicycle[?(@.price < 20)]
RBBAssertEqual(json.store.bicycle[matches: { $0.price <= 20 }], [
[
"color": "red",
"price": 19.95
]
])
// JSONPath: $.store.bicycle[?(@.price <= 20)].color
RBBAssertEqual(json.store.bicycle[matches: { $0.price <= 20 }].color, [
"red"
])
// JSONPath: $.store[*][?(@.price <= 20)].color
//
// NOTE: This query doesn't seem to work in Gatling but does in Jayway.
RBBAssertEqual(json.store[any: .child][matches: { $0.price <= 20 }].color, [
"red"
])
}
func testTrailingAnyOnArray() {
// JSONPath: $.store.books[*]
RBBAssertEqual(json.store.book[any: .child], [
[
"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
]
])
}
func testLaziness() {
var counter = 0;
let json = [
1, "b", "c", 4
] as RBBJSON
XCTAssertEqual(counter, 0)
let query = json[matches: { _ in
counter += 1;
return true == true
}]
var results = query.lazy.map { $0 }.makeIterator()
XCTAssertEqual(counter, 0)
XCTAssertEqual(results.next(), 1)
XCTAssertEqual(counter, 1)
XCTAssertEqual(results.next(), "b")
XCTAssertEqual(counter, 2)
XCTAssertEqual(results.next(), "c")
XCTAssertEqual(counter, 3)
}
func testEagerness() {
var counter = 0;
let json = [
1, "b", "c", 4
] as RBBJSON
XCTAssertEqual(counter, 0)
let query = json[matches: { _ in
counter += 1;
return true == true
}]
var results = query.map { $0 }.makeIterator()
XCTAssertEqual(counter, 4)
XCTAssertEqual(results.next(), 1)
XCTAssertEqual(counter, 4)
XCTAssertEqual(results.next(), "b")
XCTAssertEqual(counter, 4)
XCTAssertEqual(results.next(), "c")
XCTAssertEqual(counter, 4)
}
func testOrder() {
let json = [
"numbers": [
[1],
[2],
[3],
[4],
]
] as RBBJSON
RBBAssertEqual(json[any: .child][any: .child][any: .child], [1, 2, 3, 4])
RBBAssertEqual(json[any: .child][any: .child][0], [1, 2, 3, 4])
RBBAssertEqual(json[any: .child][0, 1, 2, 3][0], [1, 2, 3, 4])
RBBAssertEqual(json[any: .child][0..<4][0], [1, 2, 3, 4])
RBBAssertEqual(json[any: .child][0...3][0], [1, 2, 3, 4])
RBBAssertEqual(json.numbers[any: .child][0], [1, 2, 3, 4])
RBBAssertEqual(json.numbers[matches: { $0[0] >= 0 }][any: .child], [1, 2, 3, 4])
}
func testEmptyLeaves() {
let json = [
[
[:],
["empty": [:]],
["also_empty": []],
[]
],
[]
] as RBBJSON
let result = Array(json[any: .child][any: .child][any: .child][any: .child])
XCTAssert(result.isEmpty)
}
func testUnsupported() {
do {
let result = Array(json.store.bicycle[1, 2, 3])
XCTAssert(result.isEmpty)
}
do {
let result = Array(json.store.bicycle[1..<3])
XCTAssert(result.isEmpty)
}
do {
let result = Array(json.store.books["a"])
XCTAssert(result.isEmpty)
}
}
func testFilters() throws {
let json = [
[
"name": "a",
"flag": nil
],
[
"name": "b",
"flag": 5
],
[
"name": "c",
"flag": 10
]
] as RBBJSON
RBBAssertEqual(json[has: \.name].name, ["a", "b", "c"])
RBBAssertEqual(json[has: \.flag].name, ["b", "c"])
RBBAssertEqual(json[has: \.flag][matches: { $0.flag >= 7 }].name, [
"c"
])
}
}
func RBBAssertEqual(_ lhs: S, _ result: T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) where S: Sequence, T: Sequence, T.Element == S.Element, S.Element: Equatable {
XCTAssertEqual(Array(lhs), Array(result), message(), file: file, line: line)
}
================================================
FILE: Tests/RBBJSONTests/RBBJSON+Literals.swift
================================================
import RBBJSON
extension RBBJSON: ExpressibleByNilLiteral {
public init(nilLiteral: ()) {
self = .null
}
}
extension RBBJSON: ExpressibleByBooleanLiteral {
public init(booleanLiteral value: BooleanLiteralType) {
self = .bool(value)
}
}
extension RBBJSON: ExpressibleByFloatLiteral {
public init(floatLiteral value: Double) {
self = .number(value)
}
}
extension RBBJSON: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .string(value)
}
}
extension RBBJSON: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: RBBJSON...) {
self = .array(elements)
}
}
extension RBBJSON: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self = .number(Double(value))
}
}
extension RBBJSON: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (String, RBBJSON)...) {
self = .object(Dictionary(elements) { a, _ in a })
}
}
================================================
FILE: Tests/RBBJSONTests/RBBJSONTests.swift
================================================
import XCTest
import RBBJSON
final class RBBJSONTests: XCTestCase {
func testJSONDecoding() throws {
let jsonString = """
{
"results": [
{
"number": -123.45,
"boolean": false,
"string": "Hello World",
},
{},
null
]
}
"""
let json = try JSONDecoder().decode(RBBJSON.self, from: jsonString.data(using: .utf8)!)
XCTAssertEqual(json, [
"results": [
[
"number": -123.45,
"boolean": false,
"string": "Hello World"
],
[:],
nil
]
])
}
func testJSONRoundTripping() throws {
let json = [
"results": [
[
"number": -123.45,
"boolean": false,
"string": "Hello World"
]
]
] as RBBJSON
let jsonData = try JSONEncoder().encode(json)
XCTAssertEqual(json, try JSONDecoder().decode(RBBJSON.self, from: jsonData))
}
func testDebugDescription() {
let json = [
"results": [
[
"number": -123.45,
"boolean": false,
"string": "Hello World"
]
]
] as RBBJSON
XCTAssert(json.debugDescription.contains("Hello World"))
}
func testDynamicKeypath() {
let json = [
"results": [
[
"number": -123.45,
"boolean": false,
"string": "Hello World"
]
]
] as RBBJSON
XCTAssertEqual(json.results[0].number, -123.45)
XCTAssertEqual(json.results[0].boolean, false)
XCTAssertEqual(json.results[0].string, "Hello World")
}
func testKeys() {
XCTAssertEqual(RBBJSON.keys("a"), [])
XCTAssertEqual(RBBJSON.keys(1), [])
XCTAssertEqual(RBBJSON.keys(false), [])
XCTAssertEqual(RBBJSON.keys(nil), [])
XCTAssertEqual(RBBJSON.keys([1, 2, 3]), [])
XCTAssertEqual(RBBJSON.keys(["a": 1, "b": 2]), ["a", "b"])
}
func testValues() {
XCTAssertEqual(RBBJSON.values("a"), [])
XCTAssertEqual(RBBJSON.values(1), [])
XCTAssertEqual(RBBJSON.values(false), [])
XCTAssertEqual(RBBJSON.values(nil), [])
XCTAssertEqual(RBBJSON.values([1, 2, 3]), [1, 2, 3])
XCTAssertEqual(RBBJSON.values(["a": 1, "b": 2]), [1, 2])
}
}
================================================
FILE: Tests/RBBJSONTests/READMETests.swift
================================================
import XCTest
import RBBJSON
private let json = [
"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
] as RBBJSON
final class READMETests: XCTestCase {
func testQueriesAny() {
// JSONPath: $.store.book[*].author
RBBAssertEqual(json.store.book[any: .child].author, [
"Nigel Rees",
"Evelyn Waugh",
"Herman Melville",
"J. R. R. Tolkien"
])
}
func testQueriesDescend() {
// JSONPath: $..author
RBBAssertEqual(json[any: .descendantOrSelf].author, [
"Nigel Rees",
"Evelyn Waugh",
"Herman Melville",
"J. R. R. Tolkien"
])
}
func testQueriesAnyAtEnd() {
// JSONPath: $.store.*
RBBAssertEqual(json.store[any: .child], [
[
"color": "red",
"price": 19.95
],
[
[
"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
]
]
])
}
func testQueriesDescend2() {
// JSONPath: $.store..price
RBBAssertEqual(json.store[any: .descendantOrSelf].price, [
19.95,
8.95,
12.99,
8.99,
22.99
])
}
func testQueriesSingleIndex() {
// JSONPath: $..book[2]
RBBAssertEqual(json[any: .descendantOrSelf].book[2], [
[
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
]
])
}
func testQueriesNegativeIndex() {
// JSONPath: $..book[-2]
RBBAssertEqual(json[any: .descendantOrSelf].book[-2], [
[
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
]
])
}
func testQueriesMultipleIndicesAtEnd() {
// JSONPath: $..book[0,1]
RBBAssertEqual(json[any: .descendantOrSelf].book[0, 1], [
[
"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
]
])
}
func testFilter() {
// JSONPath: $..book[?(@.isbn)]
RBBAssertEqual(json[any: .descendantOrSelf].book[has: \.isbn], [
[
"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
]
])
}
func testBooleanPredicate() {
// JSONPath: $.store.book[?(@.price < 10)]
RBBAssertEqual(json.store.book[matches: { $0.price <= 10 }], [
[
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
],
[
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
]
])
}
func testMultipleKeys() {
// JSONPath: $.store["book", "bicycle"]..["price", "author"]
//
// NOTE: This query doesn't seem to work in Gatling but does in Jayway
// with slightly different semantics: Turning on _Return null for
// missing leaf_ will produce the expected result although with a
// `null` value for the bicycle's author that we're omitting here.
RBBAssertEqual(json.store["book", "bicycle"][any: .descendantOrSelf]["price", "author"], [
[
"price": 19.95,
],
[
"price": 8.95,
"author": "Nigel Rees"
],
[
"price": 12.99,
"author": "Evelyn Waugh"
],
[
"price": 8.99,
"author": "Herman Melville"
],
[
"price": 22.99,
"author": "J. R. R. Tolkien"
],
])
}
func testExample() {
RBBAssertEqual(json.store.book[0].title.compactMap(String.init), [
"Sayings of the Century"
])
RBBAssertEqual(json.store.book[0, 1].title.compactMap(String.init), [
"Sayings of the Century",
"Sword of Honour"
])
RBBAssertEqual(json.store.book[0]["invalid Property"].compactMap(String.init), [])
RBBAssertEqual(json.store.book[0, 1]["invalid Property"].compactMap(String.init), [])
}
}
================================================
FILE: Tests/RBBJSONTests/SequenceTests.swift
================================================
import XCTest
import RBBJSON
final class SequenceTests: XCTestCase {
func testEnumeration() {
let json = [
"results": [
[
"number": -123.45,
"boolean": false,
"string": "Hello World"
]
]
] as RBBJSON
for results in RBBJSON.values(json) {
for result in RBBJSON.values(results) {
XCTAssertEqual(result.number, -123.45)
XCTAssertEqual(result.boolean, false)
XCTAssertEqual(result.string, "Hello World")
}
}
}
}
================================================
FILE: Tests/RBBJSONTests/StringTests.swift
================================================
import XCTest
import RBBJSON
@available(iOS 10.0, *)
final class StringTests: XCTestCase {
func testStringConversion() {
XCTAssertEqual(String("foo" as RBBJSON), "foo")
XCTAssertEqual(String("" as RBBJSON), "")
XCTAssertEqual(String(123.4 as RBBJSON), nil)
XCTAssertEqual(String(0 as RBBJSON), nil)
}
}