diff --git a/.swift-version b/.swift-version index 9f55b2c..5186d07 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -3.0 +4.0 diff --git a/PMJSON.podspec b/PMJSON.podspec index 85d2098..de336bd 100644 --- a/PMJSON.podspec +++ b/PMJSON.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "PMJSON" - s.version = "2.0.3" + s.version = "3.0.0" s.summary = "Pure Swift JSON encoding/decoding library" s.description = "PMJSON provides a pure-Swift strongly-typed JSON encoder/decoder as well as a set of convenience methods for converting to/from Foundation objects and for decoding JSON structures." @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/postmates/PMJSON.git", :tag => "v#{s.version}" } - s.source_files = "Sources/*.{swift,h,m}", + s.source_files = "Sources/**/*.{swift,h,m}", s.ios.deployment_target = '8.0' s.osx.deployment_target = '10.9' diff --git a/PMJSON.xcodeproj/project.pbxproj b/PMJSON.xcodeproj/project.pbxproj index c29cbc7..1a61c98 100644 --- a/PMJSON.xcodeproj/project.pbxproj +++ b/PMJSON.xcodeproj/project.pbxproj @@ -9,6 +9,13 @@ /* Begin PBXBuildFile section */ 0A0F2BAA1BF1BBE50095D290 /* JSONError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0F2BA91BF1BBE50095D290 /* JSONError.swift */; }; 0A0F2BAC1BF30D1B0095D290 /* JSONObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0F2BAB1BF30D1B0095D290 /* JSONObject.swift */; }; + 0A550CCE203A28A700941526 /* SwiftEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A550CCD203A28A700941526 /* SwiftEncoderTests.swift */; }; + 0AE408A4203413AE00A73EBC /* SwiftDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE408A3203413AE00A73EBC /* SwiftDecoder.swift */; }; + 0AE408A62036A36200A73EBC /* SwiftDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE408A52036A36200A73EBC /* SwiftDecoderTests.swift */; }; + 0AE408A82036BEEF00A73EBC /* Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE408A72036BEEF00A73EBC /* Codable.swift */; }; + 0AE408AA2036C6A000A73EBC /* SwiftCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE408A92036C6A000A73EBC /* SwiftCodableTests.swift */; }; + 0AE408AD2038024E00A73EBC /* JSONKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE408AC2038024E00A73EBC /* JSONKey.swift */; }; + 0AE408B62038063500A73EBC /* SwiftEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE408B52038063500A73EBC /* SwiftEncoder.swift */; }; 9E0317C51D664B9000096D9E /* sample.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E0317C41D664B9000096D9E /* sample.json */; }; 9E0AD0C31C69B3360038B4F5 /* DecimalNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0AD0C21C69B3360038B4F5 /* DecimalNumber.swift */; }; 9E1B24441BC73D7600E5BC19 /* PMJSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E1B24431BC73D7600E5BC19 /* PMJSON.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -40,8 +47,16 @@ /* Begin PBXFileReference section */ 0A0F2BA91BF1BBE50095D290 /* JSONError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONError.swift; sourceTree = ""; }; 0A0F2BAB1BF30D1B0095D290 /* JSONObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONObject.swift; sourceTree = ""; }; + 0A550CCD203A28A700941526 /* SwiftEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftEncoderTests.swift; sourceTree = ""; }; 0A79B5071E8AFE6700A8C727 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 0A79B5091E8B0BF500A8C727 /* LinuxMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinuxMain.swift; sourceTree = ""; }; + 0AE408A3203413AE00A73EBC /* SwiftDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDecoder.swift; sourceTree = ""; }; + 0AE408A52036A36200A73EBC /* SwiftDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDecoderTests.swift; sourceTree = ""; }; + 0AE408A72036BEEF00A73EBC /* Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Codable.swift; sourceTree = ""; }; + 0AE408A92036C6A000A73EBC /* SwiftCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftCodableTests.swift; sourceTree = ""; }; + 0AE408AC2038024E00A73EBC /* JSONKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONKey.swift; sourceTree = ""; }; + 0AE408AE2038028100A73EBC /* PMJSON.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = PMJSON.podspec; sourceTree = ""; }; + 0AE408B52038063500A73EBC /* SwiftEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftEncoder.swift; sourceTree = ""; }; 9E0317C41D664B9000096D9E /* sample.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = sample.json; sourceTree = ""; }; 9E0317C61D664D7D00096D9E /* README.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.txt; sourceTree = ""; }; 9E0AD0C21C69B3360038B4F5 /* DecimalNumber.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecimalNumber.swift; sourceTree = ""; }; @@ -95,10 +110,21 @@ path = Tests; sourceTree = ""; }; + 0AE408AB2038023D00A73EBC /* Coders */ = { + isa = PBXGroup; + children = ( + 0AE408A3203413AE00A73EBC /* SwiftDecoder.swift */, + 0AE408B52038063500A73EBC /* SwiftEncoder.swift */, + 0AE408AC2038024E00A73EBC /* JSONKey.swift */, + ); + path = Coders; + sourceTree = ""; + }; 9E1B24271BC73D1900E5BC19 = { isa = PBXGroup; children = ( 0A79B5071E8AFE6700A8C727 /* Package.swift */, + 0AE408AE2038028100A73EBC /* PMJSON.podspec */, 9E49DEF31C1F9E2F004FDCAD /* README.md */, 9E69BE0B1C6E843D00D92762 /* LICENSE-APACHE */, 9E49DEF21C1F9E2F004FDCAD /* LICENSE-MIT */, @@ -126,6 +152,8 @@ 0A0F2BA91BF1BBE50095D290 /* JSONError.swift */, 9E87937B1BC8341500BC6A15 /* Foundation.swift */, 9E0AD0C21C69B3360038B4F5 /* DecimalNumber.swift */, + 0AE408A72036BEEF00A73EBC /* Codable.swift */, + 0AE408AB2038023D00A73EBC /* Coders */, 9E1B246A1BC7463D00E5BC19 /* Parser.swift */, 9E8793771BC7547800BC6A15 /* Decoder.swift */, 9E5983DB1C60282F008495F6 /* Encoder.swift */, @@ -144,6 +172,9 @@ 9EC4CD951DB83DE8003A4268 /* JSONParserTests.swift */, 9E4997EF1DC96E110049EECD /* JSONEncoderTests.swift */, 9ECD04F61DC15CAD00DBE7CA /* JSONTestSuite.swift */, + 0AE408A92036C6A000A73EBC /* SwiftCodableTests.swift */, + 0AE408A52036A36200A73EBC /* SwiftDecoderTests.swift */, + 0A550CCD203A28A700941526 /* SwiftEncoderTests.swift */, 9E0317C61D664D7D00096D9E /* README.txt */, 9E0317C41D664B9000096D9E /* sample.json */, 9ECD04F41DC15C9800DBE7CA /* JSONTestSuite */, @@ -210,15 +241,15 @@ attributes = { LastSwiftUpdateCheck = 0700; LastUpgradeCheck = 0900; - ORGANIZATIONNAME = Postmates; + ORGANIZATIONNAME = "Kevin Ballard"; TargetAttributes = { 9E1B24401BC73D7600E5BC19 = { CreatedOnToolsVersion = 7.0.1; - LastSwiftMigration = 0800; + LastSwiftMigration = 0920; }; 9E1B24491BC73D7600E5BC19 = { CreatedOnToolsVersion = 7.0.1; - LastSwiftMigration = 0800; + LastSwiftMigration = 0920; }; }; }; @@ -270,9 +301,13 @@ 9E8793781BC7547800BC6A15 /* Decoder.swift in Sources */, 0A0F2BAA1BF1BBE50095D290 /* JSONError.swift in Sources */, 9E0AD0C31C69B3360038B4F5 /* DecimalNumber.swift in Sources */, + 0AE408A82036BEEF00A73EBC /* Codable.swift in Sources */, 9E1B24591BC73DA000E5BC19 /* JSON.swift in Sources */, 9E87937E1BC835F500BC6A15 /* Accessors.swift in Sources */, + 0AE408A4203413AE00A73EBC /* SwiftDecoder.swift in Sources */, 0A0F2BAC1BF30D1B0095D290 /* JSONObject.swift in Sources */, + 0AE408B62038063500A73EBC /* SwiftEncoder.swift in Sources */, + 0AE408AD2038024E00A73EBC /* JSONKey.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -282,8 +317,11 @@ files = ( 9EC4CD961DB83DE8003A4268 /* JSONParserTests.swift in Sources */, 9ECD04F71DC15CAD00DBE7CA /* JSONTestSuite.swift in Sources */, + 0A550CCE203A28A700941526 /* SwiftEncoderTests.swift in Sources */, + 0AE408AA2036C6A000A73EBC /* SwiftCodableTests.swift in Sources */, 9E4997F01DC96E110049EECD /* JSONEncoderTests.swift in Sources */, 9EC52A6A1DCBC2E900658723 /* JSONAccessorTests.swift in Sources */, + 0AE408A62036A36200A73EBC /* SwiftDecoderTests.swift in Sources */, 9E1B24501BC73D7600E5BC19 /* JSONDecoderTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -421,7 +459,7 @@ SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos watchsimulator watchos appletvsimulator appletvos"; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -475,7 +513,7 @@ SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos watchsimulator watchos appletvsimulator appletvos"; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -529,7 +567,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -572,7 +610,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/Package.swift b/Package.swift index 49d6b48..7896bc7 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:3.0 +// swift-tools-version:4.0 import PackageDescription diff --git a/README.md b/README.md index 95dc9a2..42e8707 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PMJSON -[![Version](https://img.shields.io/badge/version-v2.0.3-blue.svg)](https://github.com/postmates/PMJSON/releases/latest) +[![Version](https://img.shields.io/badge/version-v3.0.0-blue.svg)](https://github.com/postmates/PMJSON/releases/latest) ![Platforms](https://img.shields.io/badge/platforms-ios%20%7C%20osx%20%7C%20watchos%20%7C%20tvos-lightgrey.svg) ![Languages](https://img.shields.io/badge/languages-swift-orange.svg) ![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg) @@ -71,6 +71,8 @@ struct Config { } ``` +This library also provides support for `Swift.Encoder` and `Swift.Decoder`. See [this section](#swiftencoder-and-swiftdecoder) for details. + ### Parsing The JSON decoder is split into separate parser and decoder stages. The parser consums any sequence of unicode scalars, and produces a sequence of JSON "events" (similar to a SAX XML parser). The decoder accepts a sequence of JSON events and produces a `JSON` value. This architecture is designed such that you can use just the parser alone in order to decode directly to your own data structures and bypass the `JSON` representation entirely if desired. However, most clients are expected to use both components, and this is exposed via a simple method `JSON.decode(_:options:)`. @@ -177,6 +179,18 @@ The `JSON` type has static methods `map()` and `flatMap()` for working with arra There are also helpers for converting to/from Foundation objects. `JSON` offers an initializer `init(ns: AnyObject) throws` that converts from any JSON-compatible object to a `JSON`. `JSON` and `JSONObject` both offer the property `.ns`, which returns a Foundation object equivalent to the `JSON`, and `.nsNoNull` which does the same but omits any `null` values instead of using `NSNull`. +### Codable support + +The `JSON` type conforms to `Codable`, so it can be encoded to a `Swift.Encoder` and decoded from a `Swift.Decoder`. This has been tested against the standard library-provided `JSONEncoder` and `JSONDecoder`. Due to limitations in the decoding protocol, decoding a `JSON` must attempt to decode multiple different types of values, so it's possible that a poorly-written `Swift.Decoder` may produce surprising results when decoding a `JSON`. + +Encoding to a `JSON.Encoder` and decoding from a `JSON.Decoder` is optimized to avoid unnecessary work. + +### `Swift.Encoder` and `Swift.Decoder` + +This library provides an implementation of `Swift.Encoder` called `JSON.Encoder`. This can encode any `Encodable` to a `JSON`, a `String`, or a `Data`. It's used similarly to `Swift.JSONEncoder` (except at this time it doesn't have options to control encoding of specific types). + +This library provides an implementation of `Swift.Decoder` called `JSON.Decoder`. This can decode any `Decodable` from a `JSON`, a `String`, or a `Data`. It's used similar to `Swift.JSONDecoder` (except at this time it doesn't have options to control decoding of specific types). + ### Performance The test suite includes some basic performance tests. Decoding ~70KiB of JSON using PMJSON takes about 2.5-3x the time that `NSJSONSerialization` does, though I haven't tested this with different distributions of inputs and it's possible this performance is specific to the characteristics of the test input. However, encoding the same JSON back to a `Data` is actually faster with PMJSON, taking around 75% of the time that `NSJSONSerialization` does. @@ -197,7 +211,7 @@ The [Swift Package Manager][] may be used to install PMJSON by adding it to your let package = Package( name: "YourPackage", dependencies: [ - .Package(url: "https://github.com/postmates/PMJSON.git", majorVersion: 2) + .Package(url: "https://github.com/postmates/PMJSON.git", majorVersion: 3) ] ) ``` @@ -209,13 +223,13 @@ let package = Package( To install using [Carthage][], add the following to your Cartfile: ``` -github "postmates/PMJSON" ~> 2.0 +github "postmates/PMJSON" ~> 3.0 ``` -This release supports Swift 3. If you want Swift 2.3 support, you can use +This release supports Swift 4. If you want Swift 3.x support, you can use ``` -github "postmates/PMJSON" ~> 0.9.4 +github "postmates/PMJSON" ~> 2.0 ``` ### CocoaPods @@ -223,13 +237,13 @@ github "postmates/PMJSON" ~> 0.9.4 To install using [CocoaPods][], add the following to your Podfile: ``` -pod 'PMJSON', '~> 2.0' +pod 'PMJSON', '~> 3.0' ``` -This release supports Swift 3. If you want Swift 2.3 support, you can use +This release supports Swift 4. If you want Swift 3.x support, you can use ``` -pod 'PMJSON', '~> 0.9.4' +pod 'PMJSON', '~> 2.0' ``` [CocoaPods]: https://cocoapods.org @@ -248,6 +262,13 @@ Unless you explicitly state otherwise, any contribution intentionally submitted ## Version History +#### v3.0.0 (2018-02-18) + +* Convert to Swift 4. +* Implement `Codable` on `JSON`. +* Add a `Swift.Decoder` implementation called `JSON.Decoder`. +* Add a `Swift.Encoder` implementation called `JSON.Encoder`. + #### v2.0.3 (2017-09-12) * Add Linux support for `Decimal` (on Swift 3.1 and later). NOTE: Decimal support is still buggy in Swift 3.1, and the workarounds we employ to get the correct values on Apple platforms don't work on Linux. You probably shouldn't rely on this working correctly on Linux until Swift fixes its Decimal implementation. diff --git a/Sources/Accessors.swift b/Sources/Accessors.swift index 4d7d85d..831fe2e 100644 --- a/Sources/Accessors.swift +++ b/Sources/Accessors.swift @@ -12,10 +12,8 @@ // except according to those terms. // -#if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - import class Foundation.NSDecimalNumber - import struct Foundation.Decimal -#endif +import class Foundation.NSDecimalNumber +import struct Foundation.Decimal public extension JSON { /// Returns `true` iff the receiver is `.null`. @@ -71,13 +69,7 @@ public extension JSON { /// On platforms where `.decimal` is a dummy value, it's not a treated as a number. var isNumber: Bool { switch self { - case .int64, .double: return true - case .decimal: - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - return true - #else - return false - #endif + case .int64, .double, .decimal: return true default: return false } } @@ -169,7 +161,7 @@ public extension JSON { var int: Int? { get { guard let value = self.int64 else { return nil} - let truncated = Int(truncatingBitPattern: value) + let truncated = Int(truncatingIfNeeded: value) guard Int64(truncated) == value else { return nil } return truncated } @@ -192,12 +184,8 @@ public extension JSON { case .int64(let i): return Double(i) case .double(let d): return d case .decimal(let d): - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - // NB: Decimal does not have any accessor to produce a Double - return NSDecimalNumber(decimal: d).doubleValue - #else - return nil - #endif + // NB: Decimal does not have any accessor to produce a Double + return NSDecimalNumber(decimal: d).doubleValue default: return nil } } @@ -304,16 +292,18 @@ internal func convertDoubleToInt64(_ d: Double) -> Int64? { return Int64(d) } -#if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - internal func convertDecimalToInt64(_ d: Decimal) -> Int64? { - if d > Int64.maxDecimal || d < Int64.minDecimal { - return nil - } - // NB: Decimal does not have any appropriate accessor - return NSDecimalNumber(decimal: d).int64Value +internal func convertDecimalToInt64(_ d: Decimal) -> Int64? { + if d > Int64.maxDecimal || d < Int64.minDecimal { + return nil } -#else - internal func convertDecimalToInt64(_ d: DecimalPlaceholder) -> Int64? { + // NB: Decimal does not have any appropriate accessor + return NSDecimalNumber(decimal: d).int64Value +} + +internal func convertDecimalToUInt64(_ d: Decimal) -> UInt64? { + if d > UInt64.maxDecimal || d < UInt64.minDecimal { return nil } -#endif + // NB: Decimal does not have any appropriate accessor + return NSDecimalNumber(decimal: d).uint64Value +} diff --git a/Sources/Codable.swift b/Sources/Codable.swift new file mode 100644 index 0000000..575c992 --- /dev/null +++ b/Sources/Codable.swift @@ -0,0 +1,94 @@ +// +// Codable.swift +// PMJSON +// +// Created by Kevin Ballard on 2/15/18. +// Copyright © 2018 Kevin Ballard. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// + +import struct Foundation.Decimal + +extension JSON: Codable { + public init(from decoder: Swift.Decoder) throws { + if let decoder = decoder as? _JSONDecoder { + self = decoder.value + } else { + let container = try decoder.singleValueContainer() + if let value = try? container.decode(String.self) { + self = .string(value) + } else if let value = try? container.decode(Bool.self) { + // NB: We must attempt to decode booleans before numbers because JSONDecoder will + // convert booleans into numbers (but not vice versa). + self = .bool(value) + } else if let value = try? container.decode(Double.self) { + if value.rounded(.down) == value, + let intValue = try? container.decode(Int64.self) { + self = .int64(intValue) + } else { + self = .double(value) + } + } else if let value = try? container.decode(Int64.self) { + self = .int64(value) + } else if let value = try? container.decode(JSONObject.self) { + self = .object(value) + } else if let value = try? container.decode([JSON].self) { + self = .array(JSONArray(value)) + } else if container.decodeNil() { + self = .null + } else if let value = try? container.decode(Decimal.self) { + self = .decimal(value) + } else { + throw DecodingError.typeMismatch(JSON.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode value of type JSON")) + } + } + } + + public func encode(to encoder: Swift.Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .null: + try container.encodeNil() + case .bool(let value): + try container.encode(value) + case .string(let value): + try container.encode(value) + case .int64(let value): + try container.encode(value) + case .double(let value): + try container.encode(value) + case .decimal(let value): + try container.encode(value) + case .object(let value): + try container.encode(value) + case .array(let value): + try container.encode(Array(value)) + } + } +} + +extension JSONObject: Codable { + public init(from decoder: Decoder) throws { + if let decoder = decoder as? _JSONDecoder { + do { + self.init() + self = try decoder.value.getObject() + } catch let JSONError.missingOrInvalidType(path, expected, actual) { + throw DecodingError.typeMismatch(JSONObject.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected JSONObject, found \(actual as Any)", underlyingError: JSONError.missingOrInvalidType(path: path, expected: expected, actual: actual))) + } + } else { + let container = try decoder.singleValueContainer() + let dict = try container.decode([String: JSON].self) + self.init(dict) + } + } + + public func encode(to encoder: Encoder) throws { + try dictionary.encode(to: encoder) + } +} diff --git a/Sources/Coders/JSONKey.swift b/Sources/Coders/JSONKey.swift new file mode 100644 index 0000000..cf4cad8 --- /dev/null +++ b/Sources/Coders/JSONKey.swift @@ -0,0 +1,42 @@ +// +// JSONKey.swift +// PMJSON +// +// Created by Kevin Ballard on 2/16/18. +// Copyright © 2018 Kevin Ballard. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// + +internal enum JSONKey: CodingKey { + static let `super` = JSONKey.string("super") + + case int(Int) + case string(String) + + var stringValue: String { + switch self { + case .int(let x): return String(x) + case .string(let s): return s + } + } + + var intValue: Int? { + switch self { + case .int(let x): return x + case .string: return nil + } + } + + init?(stringValue: String) { + self = .string(stringValue) + } + + init?(intValue: Int) { + self = .int(intValue) + } +} diff --git a/Sources/Coders/SwiftDecoder.swift b/Sources/Coders/SwiftDecoder.swift new file mode 100644 index 0000000..ce9fe56 --- /dev/null +++ b/Sources/Coders/SwiftDecoder.swift @@ -0,0 +1,574 @@ +// +// Codable.swift +// PMJSON +// +// Created by Kevin Ballard on 2/13/18. +// Copyright © 2018 Kevin Ballard. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// + +import Foundation + +extension JSON { + /// An object that decodes instances of data types that conform to `Decodable` from JSON + /// streams. + public struct Decoder { + /// A dictionary you use to customize the decoding process by providing contextual information. + public var userInfo: [CodingUserInfoKey: Any] = [:] + + /// Creates a new, reusable JSON decoder. + public init() {} + + /// Returns a value of the type you specify, decoded from JSON. + /// + /// - Parameter type: The type of the object to decode. + /// - Parameter data: The data containing JSON to decode. + /// - Parameter options: An optional set of options to control the JSON decoder. + /// - Returns: An instance of `type`. + /// - Throws: `DecodingError.dataCorrupted` if the JSON fails to decode (where the + /// `underlyingError` on the context is a `JSONParserError`), or any of the other + /// `DecoderError`s if the object decode fails. + public func decode(_ type: T.Type, from data: Data, options: JSONOptions = []) throws -> T { + let json: JSON + do { + json = try JSON.decode(data, options: options) + } catch { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error)) + } + return try decode(type, from: json) + } + + /// Returns a value of the type you specify, decoded from JSON. + /// + /// - Parameter type: The type of the object to decode. + /// - Parameter string: The string containing JSON to decode. + /// - Parameter options: An optional set of options to control the JSON decoder. + /// - Returns: An instance of `type`. + /// - Throws: `DecodingError.dataCorrupted` if the JSON fails to decode (where the + /// `underlyingError` on the context is a `JSONParserError`), or any of the other + /// `DecoderError`s if the object decode fails. + public func decode(_ type: T.Type, from string: String, options: JSONOptions = []) throws -> T { + let json: JSON + do { + json = try JSON.decode(string, options: options) + } catch { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given string was not valid JSON.", underlyingError: error)) + } + return try decode(type, from: json) + } + + /// Returns a value of the type you specify, decoded from JSON. + /// + /// - Parameter type: The type of the object to decode. + /// - Parameter json: The JSON to decode. + /// - Returns: An instance of `type`. + /// - Throws: `DecodingError` if the object decode fails. + public func decode(_ type: T.Type, from json: JSON) throws -> T { + let data = DecoderData() + data.userInfo = userInfo + let decoder = _JSONDecoder(data: data, value: json) + return try T(from: decoder) + } + } +} + +private class DecoderData { + var codingPath: [CodingKey] = [] + var userInfo: [CodingUserInfoKey: Any] = [:] + + func copy() -> DecoderData { + let result = DecoderData() + result.codingPath = codingPath + result.userInfo = userInfo + return result + } +} + +internal struct _JSONDecoder: Decoder { + fileprivate init(data: DecoderData, value: JSON) { + _data = data + self.value = value + } + + private let _data: DecoderData + let value: JSON + + // MARK: Decoder + + var codingPath: [CodingKey] { + return _data.codingPath + } + + var userInfo: [CodingUserInfoKey: Any] { + return _data.userInfo + } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { + let object = try _wrapTypeMismatch({ try value.getObject() }, data: _data) + return KeyedDecodingContainer(_JSONKeyedDecoder(data: _data, value: object)) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + let array = try _wrapTypeMismatch({ try value.getArray() }, data: _data) + return _JSONUnkeyedDecoder(data: _data, value: array) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + return self + } +} + +// MARK: - + +extension _JSONDecoder: SingleValueDecodingContainer { + private func wrapTypeMismatch(_ f: @autoclosure () throws -> T) throws -> T { + return try _wrapTypeMismatch(f, data: _data) + } + + private func castNumber(_ value: U) throws -> T { + return try _castNumber(value, data: _data) + } + + func decodeNil() -> Bool { + return value.isNull + } + + func decode(_ type: Bool.Type) throws -> Bool { + return try wrapTypeMismatch(value.getBool()) + } + + func decode(_ type: Int.Type) throws -> Int { + return try wrapTypeMismatch(value.getInt()) + } + + func decode(_ type: Int8.Type) throws -> Int8 { + return try castNumber(wrapTypeMismatch(value.getInt())) + } + + func decode(_ type: Int16.Type) throws -> Int16 { + return try castNumber(wrapTypeMismatch(value.getInt())) + } + + func decode(_ type: Int32.Type) throws -> Int32 { + return try castNumber(wrapTypeMismatch(value.getInt())) + } + + func decode(_ type: Int64.Type) throws -> Int64 { + return try wrapTypeMismatch(value.getInt64()) + } + + func decode(_ type: UInt.Type) throws -> UInt { + return try castNumber(_getUInt64(from: value, data: _data)) + } + + func decode(_ type: UInt8.Type) throws -> UInt8 { + return try castNumber(wrapTypeMismatch(value.getInt())) + } + + func decode(_ type: UInt16.Type) throws -> UInt16 { + return try castNumber(wrapTypeMismatch(value.getInt())) + } + + func decode(_ type: UInt32.Type) throws -> UInt32 { + return try castNumber(wrapTypeMismatch(value.getInt64())) + } + + func decode(_ type: UInt64.Type) throws -> UInt64 { + return try _getUInt64(from: value, data: _data) + } + + func decode(_ type: Float.Type) throws -> Float { + return try Float(wrapTypeMismatch(value.getDouble())) + } + + func decode(_ type: Double.Type) throws -> Double { + return try wrapTypeMismatch(value.getDouble()) + } + + func decode(_ type: String.Type) throws -> String { + return try wrapTypeMismatch(value.getString()) + } + + func decode(_ type: T.Type) throws -> T where T : Decodable { + return try T(from: self) + } +} + +private struct _JSONKeyedDecoder: KeyedDecodingContainerProtocol { + init(data: DecoderData, value: JSONObject) { + _data = data + self.value = value + } + + let _data: DecoderData + let value: JSONObject + + typealias Key = K + + var codingPath: [CodingKey] { + return _data.codingPath + } + + var allKeys: [K] { + return Array(value.keys.flatMap(K.init(stringValue:))) + } + + func contains(_ key: K) -> Bool { + return value[key.stringValue] != nil + } + + private func wrapTypeMismatch(forKey key: K, _ f: @autoclosure () throws -> T) throws -> T { + return try _wrapTypeMismatch(key: key, f, data: _data) + } + + private func castNumber(_ value: U) throws -> T { + return try _castNumber(value, data: _data) + } + + func decodeNil(forKey key: K) throws -> Bool { + guard let value = value[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: _data.codingPath, debugDescription: "No value associated with key \(key) (\(String(reflecting: key.stringValue)))")) + } + return value.isNull + } + + func decode(_ type: Bool.Type, forKey key: K) throws -> Bool { + return try wrapTypeMismatch(forKey: key, try value.getBool(key.stringValue)) + } + + func decode(_ type: Int.Type, forKey key: K) throws -> Int { + return try wrapTypeMismatch(forKey: key, value.getInt(key.stringValue)) + } + + func decode(_ type: Int8.Type, forKey key: K) throws -> Int8 { + return try castNumber(wrapTypeMismatch(forKey: key, value.getInt(key.stringValue))) + } + + func decode(_ type: Int16.Type, forKey key: K) throws -> Int16 { + return try castNumber(wrapTypeMismatch(forKey: key, value.getInt(key.stringValue))) + } + + func decode(_ type: Int32.Type, forKey key: K) throws -> Int32 { + return try castNumber(wrapTypeMismatch(forKey: key, value.getInt(key.stringValue))) + } + + func decode(_ type: Int64.Type, forKey key: K) throws -> Int64 { + return try wrapTypeMismatch(forKey: key, value.getInt64(key.stringValue)) + } + + func decode(_ type: UInt.Type, forKey key: K) throws -> UInt { + guard let value = value[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: _data.codingPath, debugDescription: "No value associated with key \(key) (\(String(reflecting: key.stringValue)))")) + } + return try castNumber(_getUInt64(from: value, data: _data)) + } + + func decode(_ type: UInt8.Type, forKey key: K) throws -> UInt8 { + return try castNumber(wrapTypeMismatch(forKey: key, value.getInt(key.stringValue))) + } + + func decode(_ type: UInt16.Type, forKey key: K) throws -> UInt16 { + return try castNumber(wrapTypeMismatch(forKey: key, value.getInt(key.stringValue))) + } + + func decode(_ type: UInt32.Type, forKey key: K) throws -> UInt32 { + return try castNumber(wrapTypeMismatch(forKey: key, value.getInt64(key.stringValue))) + } + + func decode(_ type: UInt64.Type, forKey key: K) throws -> UInt64 { + guard let value = value[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: _data.codingPath, debugDescription: "No value associated with key \(key) (\(String(reflecting: key.stringValue)))")) + } + return try _getUInt64(from: value, data: _data) + } + + func decode(_ type: Float.Type, forKey key: K) throws -> Float { + return try Float(wrapTypeMismatch(forKey: key, value.getDouble(key.stringValue))) + } + + func decode(_ type: Double.Type, forKey key: K) throws -> Double { + return try wrapTypeMismatch(forKey: key, value.getDouble(key.stringValue)) + } + + func decode(_ type: String.Type, forKey key: K) throws -> String { + return try wrapTypeMismatch(forKey: key, value.getString(key.stringValue)) + } + + func decode(_ type: T.Type, forKey key: K) throws -> T where T : Decodable { + guard let value = value[key.stringValue] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: _data.codingPath, debugDescription: "No value associated with key \(key) (\(String(reflecting: key.stringValue)))")) + } + _data.codingPath.append(key) + defer { _data.codingPath.removeLast() } + return try T(from: _JSONDecoder(data: _data, value: value)) + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: K) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + let object = try wrapTypeMismatch(forKey: key, value.getObject(key.stringValue)) + let data = _data.copy() + data.codingPath.append(key) + return KeyedDecodingContainer(_JSONKeyedDecoder(data: data, value: object)) + } + + func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer { + let array = try wrapTypeMismatch(forKey: key, value.getArray(key.stringValue)) + let data = _data.copy() + data.codingPath.append(key) + return _JSONUnkeyedDecoder(data: data, value: array) + } + + func superDecoder() throws -> Decoder { + return try _superDecoder(forKey: JSONKey.super) + } + + func superDecoder(forKey key: K) throws -> Decoder { + return try _superDecoder(forKey: key) + } + + private func _superDecoder(forKey key: CodingKey) throws -> Decoder { + let data = _data.copy() + data.codingPath.append(key) + return _JSONDecoder(data: data, value: value[key.stringValue] ?? .null) + } +} + +private struct _JSONUnkeyedDecoder: UnkeyedDecodingContainer { + private let _data: DecoderData + private let value: JSONArray + + init(data: DecoderData, value: JSONArray) { + _data = data + self.value = value + } + + var codingPath: [CodingKey] { + return _data.codingPath + } + + var count: Int? { + return value.count + } + + var isAtEnd: Bool { + return currentIndex == value.count + } + + private(set) var currentIndex: Int = 0 + + private func wrapTypeMismatch(_ f: @autoclosure () throws -> T) throws -> T { + return try _wrapTypeMismatch(f, data: _data) + } + + private func castNumber(_ value: U) throws -> T { + return try _castNumber(value, data: _data) + } + + private func assertNotAtEnd(_ expectedType: T.Type) throws { + if isAtEnd { + throw DecodingError.valueNotFound(expectedType, DecodingError.Context(codingPath: _data.codingPath + [JSONKey.int(currentIndex)], debugDescription: "Unkeyed container is at end.")) + } + } + + mutating func decodeNil() throws -> Bool { + try assertNotAtEnd(JSON.self) + if value[currentIndex].isNull { + currentIndex += 1 + return true + } else { + return false + } + } + + mutating func decode(_ type: Bool.Type) throws -> Bool { + try assertNotAtEnd(type) + let result = try wrapTypeMismatch(value[currentIndex].getBool()) + currentIndex += 1 + return result + } + + mutating func decode(_ type: Int.Type) throws -> Int { + try assertNotAtEnd(type) + let result = try wrapTypeMismatch(value[currentIndex].getInt()) + currentIndex += 1 + return result + } + + mutating func decode(_ type: Int8.Type) throws -> Int8 { + try assertNotAtEnd(type) + let result: Int8 = try castNumber(wrapTypeMismatch(value[currentIndex].getInt())) + currentIndex += 1 + return result + } + + mutating func decode(_ type: Int16.Type) throws -> Int16 { + try assertNotAtEnd(type) + let result: Int16 = try castNumber(wrapTypeMismatch(value[currentIndex].getInt())) + currentIndex += 1 + return result + } + + mutating func decode(_ type: Int32.Type) throws -> Int32 { + try assertNotAtEnd(type) + let result: Int32 = try castNumber(wrapTypeMismatch(value[currentIndex].getInt())) + currentIndex += 1 + return result + } + + mutating func decode(_ type: Int64.Type) throws -> Int64 { + try assertNotAtEnd(type) + let result = try wrapTypeMismatch(value[currentIndex].getInt64()) + currentIndex += 1 + return result + } + + mutating func decode(_ type: UInt.Type) throws -> UInt { + try assertNotAtEnd(type) + let result: UInt = try castNumber(_getUInt64(from: value[currentIndex], data: _data)) + currentIndex += 1 + return result + } + + mutating func decode(_ type: UInt8.Type) throws -> UInt8 { + try assertNotAtEnd(type) + let result: UInt8 = try castNumber(wrapTypeMismatch(value[currentIndex].getInt())) + currentIndex += 1 + return result + } + + mutating func decode(_ type: UInt16.Type) throws -> UInt16 { + try assertNotAtEnd(type) + let result: UInt16 = try castNumber(wrapTypeMismatch(value[currentIndex].getInt())) + currentIndex += 1 + return result + } + + mutating func decode(_ type: UInt32.Type) throws -> UInt32 { + try assertNotAtEnd(type) + let result: UInt32 = try castNumber(wrapTypeMismatch(value[currentIndex].getInt64())) + currentIndex += 1 + return result + } + + mutating func decode(_ type: UInt64.Type) throws -> UInt64 { + try assertNotAtEnd(type) + let result = try _getUInt64(from: value[currentIndex], data: _data) + currentIndex += 1 + return result + } + + mutating func decode(_ type: Float.Type) throws -> Float { + try assertNotAtEnd(type) + let result = try Float(wrapTypeMismatch(value[currentIndex].getDouble())) + currentIndex += 1 + return result + } + + mutating func decode(_ type: Double.Type) throws -> Double { + try assertNotAtEnd(type) + let result = try wrapTypeMismatch(value[currentIndex].getDouble()) + currentIndex += 1 + return result + } + + mutating func decode(_ type: String.Type) throws -> String { + try assertNotAtEnd(type) + let result = try wrapTypeMismatch(value[currentIndex].getString()) + currentIndex += 1 + return result + } + + mutating func decode(_ type: T.Type) throws -> T where T : Decodable { + try assertNotAtEnd(type) + _data.codingPath.append(JSONKey.int(currentIndex)) + defer { _data.codingPath.removeLast() } + let result = try T(from: _JSONDecoder(data: _data, value: value[currentIndex])) + currentIndex += 1 + return result + } + + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + try assertNotAtEnd(type) + let object = try wrapTypeMismatch(value[currentIndex].getObject()) + let data = _data.copy() + data.codingPath.append(JSONKey.int(currentIndex)) + currentIndex += 1 + return KeyedDecodingContainer(_JSONKeyedDecoder(data: data, value: object)) + } + + mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + try assertNotAtEnd(JSONArray.self) + let array = try wrapTypeMismatch(value[currentIndex].getArray()) + let data = _data.copy() + data.codingPath.append(JSONKey.int(currentIndex)) + currentIndex += 1 + return _JSONUnkeyedDecoder(data: data, value: array) + } + + mutating func superDecoder() throws -> Decoder { + try assertNotAtEnd(JSON.self) + let data = _data.copy() + data.codingPath.append(JSONKey.int(currentIndex)) + let decoder = _JSONDecoder(data: data, value: value[currentIndex]) + currentIndex += 1 + return decoder + } +} + +// MARK: - + +private func _wrapTypeMismatch(key: CodingKey? = nil, _ f: () throws -> T, data: DecoderData) throws -> T { + do { + return try f() + } catch let error as JSONError { + let prefix = key.map({ "Failed to decode value for key \($0) (\(String(reflecting: $0.stringValue))) - " }) ?? "" + switch error { + case .missingOrInvalidType(path: _, let expected, let actual): + if actual == nil, let key = key { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: data.codingPath, debugDescription: "\(prefix)Expected to decode \(expected) but found missing value", underlyingError: error)) + } else { + throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: data.codingPath, debugDescription: "\(prefix)Expected to decode \(expected) but found \(actual.map(String.init(describing:)) ?? "nil")", underlyingError: error)) + } + case .outOfRangeInt64(path: _, let value, let expected): + throw DecodingError.typeMismatch(expected, DecodingError.Context(codingPath: data.codingPath, debugDescription: "\(prefix)Expected to decode \(expected) but found out of range integer \(value)", underlyingError: error)) + case .outOfRangeDouble(path: _, let value, let expected): + throw DecodingError.typeMismatch(expected, DecodingError.Context(codingPath: data.codingPath, debugDescription: "\(prefix)Expected to decode \(expected) but found out of range double \(value)", underlyingError: error)) + case .outOfRangeDecimal(path: _, let value, let expected): + throw DecodingError.typeMismatch(expected, DecodingError.Context(codingPath: data.codingPath, debugDescription: "\(prefix)Expected to decode \(expected) but found out of range decimal \(value)", underlyingError: error)) + } + } catch { + // We shouldn't get any other error type + let prefix = key.map({ "Failed to decode value for key \($0) (\(String(reflecting: $0.stringValue))) - " }) ?? "" + throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: data.codingPath, debugDescription: "\(prefix)Expected to decode \(T.self) but got error \(error)", underlyingError: error)) + } +} + +private func _castNumber(_ value: U, data: DecoderData) throws -> T { + guard let result = T(exactly: value) else { + throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: data.codingPath, debugDescription: "Expected to decode \(T.self) but found out of range integer \(value)")) + } + return result +} + +private func _getUInt64(from value: JSON, data: DecoderData) throws -> UInt64 { + switch value { + case .int64(let value): + return try _castNumber(value, data: data) + case .double(let value): + guard let result = UInt64(exactly: value) else { + throw DecodingError.typeMismatch(UInt64.self, DecodingError.Context(codingPath: data.codingPath, debugDescription: "Expected to decode UInt64 but found out of range double \(value)")) + } + return result + case .decimal(let value): + guard let result = convertDecimalToUInt64(value) else { + throw DecodingError.typeMismatch(UInt64.self, DecodingError.Context(codingPath: data.codingPath, debugDescription: "Expected to decode UInt64 but found out of range decimal \(value)")) + } + return result + default: + throw DecodingError.typeMismatch(UInt64.self, DecodingError.Context(codingPath: data.codingPath, debugDescription: "Expected to decode UInt64 but found \(JSONError.JSONType.forValue(value))")) + } +} diff --git a/Sources/Coders/SwiftEncoder.swift b/Sources/Coders/SwiftEncoder.swift new file mode 100644 index 0000000..42b4336 --- /dev/null +++ b/Sources/Coders/SwiftEncoder.swift @@ -0,0 +1,684 @@ +// +// SwiftEncoder.swift +// PMJSON +// +// Created by Kevin Ballard on 2/16/18. +// Copyright © 2018 Kevin Ballard. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// + +import Foundation + +// There are two reasonable approaches to encoding here that are compatible with the fact that +// encoders aren't strictly scoped-based (because of nested keyed encoders, and super encoders, and +// the fact that you can ask for multiple container encoders from a single Encoder). +// +// The first is to build up a parallel JSON-like enum that boxes up objets/arrays so they can be +// shared with the containers. This approach is relatively simple, but the downside is if we want to +// create a `JSON` we need to deep-copy the whole thing (though if we write a streaming encoder we +// can serialize to `String`/`Data` without the deep copy). +// +// The second approach is to have the encoder hold an enum that contains either a `JSON` primitive +// or a boxed object/array, and have it write this value into its parent when it deinits. Because +// each container holds onto its parent, we ensure we've always written any nested values before we +// try to write our own value to our parent. The upside is we don't build up a parallel JSON +// structure, so we end up with a JSON without deep-copying. The downside here is this is a fair +// amount more complicated, and there's a lot of edge cases involved that need to be handled +// correctly, including some that I don't believe can be handled correctly, such as creating a +// nested container, writing some values to it, creating a second nested container for the same key, +// writing the same nested keys to that, dropping that container, then dropping the first container. +// The values from the first container will overwrite the ones from the second, even though that's +// not the order we wrote them in. +// +// We're going to go with approach #1 because of the edge cases in #2. + +private enum EncodedJSON { + /// An unboxed JSON value. + /// + /// This should always contain a primitive, with the sole exception of when the encoder is asked + /// to encode a `JSON` directly, which it will store unboxed. If we then ask for a nested + /// container for the same key, and the previously-stored unboxed `JSON` is an object/array, we + /// will box it at that point. + case unboxed(JSON) + case object(BoxedObject) + case array(BoxedArray) + /// A special-case for super encoders. We need to box a value but we don't know what the type of + /// the value is yet. If the wrapped value is `nil` when we go to unbox this, we'll just assume + /// an empty object. + /// + /// - Requires: This case should never contain `Box(.super(…))`. + case `super`(Box) + + typealias BoxedObject = Box<[String: EncodedJSON]> + typealias BoxedArray = Box<[EncodedJSON]> + + class Box { + var value: Value + + init(_ value: Value) { + self.value = value + } + } + + var isObject: Bool { + switch self { + case .unboxed(let json): return json.isObject + case .object: return true + case .array: return false + case .super(let box): return box.value?.isObject ?? false + } + } + + var isArray: Bool { + switch self { + case .unboxed(let json): return json.isArray + case .object: return false + case .array: return true + case .super(let box): return box.value?.isArray ?? false + } + } + + func unbox() -> JSON { + switch self { + case .unboxed(let value): return value + case .object(let box): + return .object(JSONObject(dict: box.value.mapValues({ $0.unbox() }))) + case .array(let box): + return .array(JSONArray(box.value.map({ $0.unbox() }))) + case .super(let box): + return box.value?.unbox() ?? .object(JSONObject()) + } + } + + /// Extracts the boxed object from the given json. + /// + /// If the json contains `.unboxed(.object)`, the object is boxed first and stored back in the json. + /// If the json contains `nil`, it's initialized to an empty object. + static func boxObject(json: inout EncodedJSON?) -> BoxedObject? { + switch json { + case nil: + let box = BoxedObject([:]) + json = .object(box) + return box + case .unboxed(.object(let object))?: + let box = BoxedObject(object.dictionary.mapValues(EncodedJSON.init(boxing:))) + json = .object(box) + return box + case .unboxed?: + return nil + case .object(let box)?: + return box + case .array?: + return nil + case .super(let box)?: + return boxObject(json: &box.value) + } + } + + /// Extracts the boxed array from the given json. + /// + /// If the json contains `.unboxed(.array)`, the array is boxed first and stored back in the json. + /// If the json contains `nil`, it's initialized to an empty array. + static func boxArray(json: inout EncodedJSON?) -> BoxedArray? { + switch json { + case nil: + let box = BoxedArray([]) + json = .array(box) + return box + case .unboxed(.array(let array))?: + let box = BoxedArray(array.map(EncodedJSON.init(boxing:))) + json = .array(box) + return box + case .unboxed?, .object?: + return nil + case .array(let box)?: + return box + case .super(let box)?: + return boxArray(json: &box.value) + } + } + + init(boxing json: JSON) { + switch json { + case .object(let object): self = .object(Box(object.dictionary.mapValues(EncodedJSON.init(boxing:)))) + case .array(let array): self = .array(Box(array.map(EncodedJSON.init(boxing:)))) + default: self = .unboxed(json) + } + } +} + +extension JSON { + /// An object that encodes instances of data types that conform to `Encodable` to JSON streams. + public struct Encoder { + /// A dictionary you use to customize the encoding process by providing contextual information. + public var userInfo: [CodingUserInfoKey: Any] = [:] + + /// Creates a new, reusable JSON encoder. + public init() {} + + /// Returns a JSON-encoded representation of the value you supply. + /// + /// - Parameter value: The value to encode. + /// - Returns: Data containing the JSON encoding of the value. + /// - Throws: Any error thrown by a value's `encode(to:)` method. + public func encodeAsData(_ value: T, options: JSONEncoderOptions = []) throws -> Data { + return try JSON.encodeAsData(encodeAsJSON(value), options: options) + } + + /// Returns a JSON-encoded representation of the value you supply. + /// + /// - Parameter value: The value to encode. + /// - Returns: A string containing the JSON encoding of the value. + /// - Throws: Any error thrown by a value's `encode(to:)` method. + public func encodeAsString(_ value: T, options: JSONEncoderOptions = []) throws -> String { + return try JSON.encodeAsString(encodeAsJSON(value), options: options) + } + + /// Returns a JSON-encoded representation of the value you supply. + /// + /// - Parameter value: The value to encode. + /// - Returns: The JSON encoding of the value. + /// - Throws: Any error thrown by a value's `encode(to:)` method, or + /// `EncodingError.invalidValue` if the value doesn't encode anything. + public func encodeAsJSON(_ value: T) throws -> JSON { + let data = EncoderData() + data.userInfo = userInfo + let encoder = _JSONEncoder(data: data) + try value.encode(to: encoder) + guard let json = encoder.json else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(type(of: value)) did not encode any values.")) + } + return json.unbox() + } + + @available(*, unavailable, renamed: "encodeAsData(_:)") + public func encode(_ value: T) throws -> Data { + return try encodeAsData(value) + } + } +} + +private class EncoderData { + var codingPath: [CodingKey] = [] + var userInfo: [CodingUserInfoKey: Any] = [:] + + func copy() -> EncoderData { + let result = EncoderData() + result.codingPath = codingPath + result.userInfo = userInfo + return result + } +} + +private class _JSONEncoder: Encoder { + init(data: EncoderData, json: EncodedJSON? = nil) { + _data = data + value = json.map(Value.json) + } + + init(data: EncoderData, box: EncodedJSON.Box) { + _data = data + value = .box(box) + } + + private let _data: EncoderData + private var value: Value? + + private enum Value { + case json(EncodedJSON) + case box(EncodedJSON.Box) + + var isEmpty: Bool { + switch self { + case .json(.super(let box)): return box.value == nil + case .json: return false + case .box(let box): return box.value == nil + } + } + } + + var json: EncodedJSON? { + get { + switch value { + case .json(let json)?: return json + case .box(let box)?: return box.value + case nil: return nil + } + } + set { + switch value { + case nil, .json?: value = newValue.map(Value.json) + case .box(let box)?: box.value = newValue + } + } + } + + var codingPath: [CodingKey] { + return _data.codingPath + } + + var userInfo: [CodingUserInfoKey: Any] { + return _data.userInfo + } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + let box_: EncodedJSON.BoxedObject? + switch value { + case .json(let json_)?: + var json: EncodedJSON? = json_ + box_ = EncodedJSON.boxObject(json: &json) + if box_ != nil, let json = json { + value = .json(json) + } + case .box(let box)?: + box_ = EncodedJSON.boxObject(json: &box.value) + case nil: + let box = EncodedJSON.BoxedObject([:]) + value = .json(.object(box)) + box_ = box + } + guard let box = box_ else { + fatalError("Attempted to create a keyed encoding container when existing encoded value is not a JSON object.") + } + + return KeyedEncodingContainer(_JSONKeyedEncoder(data: _data, box: box)) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + let box_: EncodedJSON.BoxedArray? + switch value { + case .json(let json_)?: + var json: EncodedJSON? = json_ + box_ = EncodedJSON.boxArray(json: &json) + if box_ != nil, let json = json { + value = .json(json) + } + case .box(let box)?: + box_ = EncodedJSON.boxArray(json: &box.value) + case nil: + let box = EncodedJSON.BoxedArray([]) + value = .json(.array(box)) + box_ = box + } + guard let box = box_ else { + fatalError("Attempted to create an unkeyed encoding container when existing encoded value is not a JSON array.") + } + + return _JSONUnkeyedEncoder(data: _data, box: box) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + return self + } +} + +// MARK: - + +extension _JSONEncoder: SingleValueEncodingContainer { + private func assertCanWriteValue() { + precondition(value?.isEmpty ?? true, "Attempted to encode value through single value container when previous value already encoded.") + } + + func encodeNil() throws { + assertCanWriteValue() + json = .unboxed(.null) + } + + func encode(_ value: Bool) throws { + assertCanWriteValue() + json = .unboxed(.bool(value)) + } + + func encode(_ value: Int) throws { + assertCanWriteValue() + json = .unboxed(.int64(Int64(value))) + } + + func encode(_ value: Int8) throws { + assertCanWriteValue() + json = .unboxed(.int64(Int64(value))) + } + + func encode(_ value: Int16) throws { + assertCanWriteValue() + json = .unboxed(.int64(Int64(value))) + } + + func encode(_ value: Int32) throws { + assertCanWriteValue() + json = .unboxed(.int64(Int64(value))) + } + + func encode(_ value: Int64) throws { + assertCanWriteValue() + json = .unboxed(.int64(value)) + } + + func encode(_ value: UInt) throws { + assertCanWriteValue() + guard let intValue = Int64(exactly: value) else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "Encoded value is out of range for JSON integer.")) + } + json = .unboxed(.int64(intValue)) + } + + func encode(_ value: UInt8) throws { + assertCanWriteValue() + json = .unboxed(.int64(Int64(value))) + } + + func encode(_ value: UInt16) throws { + assertCanWriteValue() + json = .unboxed(.int64(Int64(value))) + } + + func encode(_ value: UInt32) throws { + assertCanWriteValue() + json = .unboxed(.int64(Int64(value))) + } + + func encode(_ value: UInt64) throws { + assertCanWriteValue() + guard let intValue = Int64(exactly: value) else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "Encoded value is out of range for JSON integer.")) + } + json = .unboxed(.int64(intValue)) + } + + func encode(_ value: Float) throws { + assertCanWriteValue() + json = .unboxed(.double(Double(value))) + } + + func encode(_ value: Double) throws { + assertCanWriteValue() + json = .unboxed(.double(value)) + } + + func encode(_ value: String) throws { + assertCanWriteValue() + json = .unboxed(.string(value)) + } + + func encode(_ value: T) throws where T : Encodable { + switch value { + case let json as JSON: + self.json = .unboxed(json) + case let decimal as Decimal: + json = .unboxed(.decimal(decimal)) + default: + try value.encode(to: self) + } + } +} + +private class _JSONUnkeyedEncoder: UnkeyedEncodingContainer { + init(data: EncoderData, box: EncodedJSON.BoxedArray) { + _data = data + self.box = box + } + + private let _data: EncoderData + private let box: EncodedJSON.BoxedArray + + var codingPath: [CodingKey] { + return _data.codingPath + } + + var count: Int { + return box.value.count + } + + private func append(unboxed json: JSON) { + box.value.append(.unboxed(json)) + } + + func encodeNil() throws { + append(unboxed: .null) + } + + func encode(_ value: Bool) throws { + append(unboxed: .bool(value)) + } + + func encode(_ value: Int8) throws { + append(unboxed: .int64(Int64(value))) + } + + func encode(_ value: Int16) throws { + append(unboxed: .int64(Int64(value))) + } + + func encode(_ value: Int32) throws { + append(unboxed: .int64(Int64(value))) + } + + func encode(_ value: Int64) throws { + append(unboxed: .int64(value)) + } + + func encode(_ value: Int) throws { + append(unboxed: .int64(Int64(value))) + } + + func encode(_ value: UInt8) throws { + append(unboxed: .int64(Int64(value))) + } + + func encode(_ value: UInt16) throws { + append(unboxed: .int64(Int64(value))) + } + + func encode(_ value: UInt32) throws { + append(unboxed: .int64(Int64(value))) + } + + func encode(_ value: UInt64) throws { + guard let intValue = Int64(exactly: value) else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + [JSONKey.int(count)], debugDescription: "Encoded value is out of range for JSON integer.")) + } + append(unboxed: .int64(intValue)) + } + + func encode(_ value: UInt) throws { + guard let intValue = Int64(exactly: value) else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + [JSONKey.int(count)], debugDescription: "Encoded value is out of range for JSON integer.")) + } + append(unboxed: .int64(intValue)) + } + + func encode(_ value: Float) throws { + append(unboxed: .double(Double(value))) + } + + func encode(_ value: Double) throws { + append(unboxed: .double(value)) + } + + func encode(_ value: String) throws { + append(unboxed: .string(value)) + } + + func encode(_ value: T) throws where T : Encodable { + switch value { + case let json as JSON: + append(unboxed: json) + case let decimal as Decimal: + append(unboxed: .decimal(decimal)) + default: + _data.codingPath.append(JSONKey.int(count)) + defer { _data.codingPath.removeLast() } + let encoder = _JSONEncoder(data: _data) + try value.encode(to: encoder) + guard let json = encoder.json else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "\(type(of: value)) did not encode any values.")) + } + box.value.append(json) + } + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { + let data = _data.copy() + data.codingPath.append(JSONKey.int(count)) + let box = EncodedJSON.BoxedObject([:]) + self.box.value.append(.object(box)) + return KeyedEncodingContainer(_JSONKeyedEncoder(data: data, box: box)) + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + let data = _data.copy() + data.codingPath.append(JSONKey.int(count)) + let box = EncodedJSON.BoxedArray([]) + self.box.value.append(.array(box)) + return _JSONUnkeyedEncoder(data: data, box: box) + } + + func superEncoder() -> Encoder { + let data = _data.copy() + data.codingPath.append(JSONKey.int(count)) + let box: EncodedJSON.Box = EncodedJSON.Box(nil) + self.box.value.append(.super(box)) + return _JSONEncoder(data: data, box: box) + } +} + +private class _JSONKeyedEncoder: KeyedEncodingContainerProtocol { + typealias Key = K + + init(data: EncoderData, box: EncodedJSON.BoxedObject) { + _data = data + self.box = box + } + + private let _data: EncoderData + private let box: EncodedJSON.BoxedObject + + var codingPath: [CodingKey] { + return _data.codingPath + } + + private func store(unboxed json: JSON, forKey key: K) { + box.value[key.stringValue] = .unboxed(json) + } + + func encodeNil(forKey key: K) throws { + store(unboxed: .null, forKey: key) + } + + func encode(_ value: Bool, forKey key: K) throws { + store(unboxed: .bool(value), forKey: key) + } + + func encode(_ value: Int, forKey key: K) throws { + store(unboxed: .int64(Int64(value)), forKey: key) + } + + func encode(_ value: Int8, forKey key: K) throws { + store(unboxed: .int64(Int64(value)), forKey: key) + } + + func encode(_ value: Int16, forKey key: K) throws { + store(unboxed: .int64(Int64(value)), forKey: key) + } + + func encode(_ value: Int32, forKey key: K) throws { + store(unboxed: .int64(Int64(value)), forKey: key) + } + + func encode(_ value: Int64, forKey key: K) throws { + store(unboxed: .int64(value), forKey: key) + } + + func encode(_ value: UInt8, forKey key: K) throws { + store(unboxed: .int64(Int64(value)), forKey: key) + } + + func encode(_ value: UInt16, forKey key: K) throws { + store(unboxed: .int64(Int64(value)), forKey: key) + } + + func encode(_ value: UInt32, forKey key: K) throws { + store(unboxed: .int64(Int64(value)), forKey: key) + } + + func encode(_ value: UInt64, forKey key: K) throws { + guard let intValue = Int64(exactly: value) else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + [key], debugDescription: "Encoded value is out of range for JSON integer.")) + } + store(unboxed: .int64(intValue), forKey: key) + } + + func encode(_ value: UInt, forKey key: K) throws { + guard let intValue = Int64(exactly: value) else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + [key], debugDescription: "Encoded value is out of range for JSON integer.")) + } + store(unboxed: .int64(intValue), forKey: key) + } + + func encode(_ value: Float, forKey key: K) throws { + store(unboxed: .double(Double(value)), forKey: key) + } + + func encode(_ value: Double, forKey key: K) throws { + store(unboxed: .double(value), forKey: key) + } + + func encode(_ value: String, forKey key: K) throws { + store(unboxed: .string(value), forKey: key) + } + + func encode(_ value: T, forKey key: K) throws where T : Encodable { + switch value { + case let json as JSON: + store(unboxed: json, forKey: key) + case let decimal as Decimal: + store(unboxed: .decimal(decimal), forKey: key) + default: + _data.codingPath.append(key) + defer { _data.codingPath.removeLast() } + let encoder = _JSONEncoder(data: _data) + try value.encode(to: encoder) + guard let json = encoder.json else { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "\(type(of: value)) did not encode any values.")) + } + box.value[key.stringValue] = json + } + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: K) -> KeyedEncodingContainer where NestedKey : CodingKey { + let data = _data.copy() + data.codingPath.append(key) + let box = EncodedJSON.BoxedObject([:]) + self.box.value[key.stringValue] = .object(box) + return KeyedEncodingContainer(_JSONKeyedEncoder(data: data, box: box)) + } + + func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer { + let data = _data.copy() + data.codingPath.append(key) + let box = EncodedJSON.BoxedArray([]) + self.box.value[key.stringValue] = .array(box) + return _JSONUnkeyedEncoder(data: data, box: box) + } + + func superEncoder() -> Encoder { + return _superEncoder(forKey: JSONKey.super) + } + + func superEncoder(forKey key: K) -> Encoder { + return _superEncoder(forKey: key) + } + + private func _superEncoder(forKey key: CodingKey) -> Encoder { + let data = _data.copy() + data.codingPath.append(key) + let box: EncodedJSON.Box = EncodedJSON.Box(nil) + self.box.value[key.stringValue] = .super(box) + return _JSONEncoder(data: data, box: box) + } +} diff --git a/Sources/DecimalNumber.swift b/Sources/DecimalNumber.swift index c821709..8b466a9 100644 --- a/Sources/DecimalNumber.swift +++ b/Sources/DecimalNumber.swift @@ -12,467 +12,472 @@ // except according to those terms. // -#if os(iOS) || os(OSX) || os(tvOS) || os(watchOS) || swift(>=3.1) - - import Foundation - - // MARK: Basic accessors - - public extension JSON { - /// Returns the numeric value as a `Decimal` if the receiver is `.int64`, `.double`, or `.decimal`, otherwise `nil`. - /// - /// When setting, replaces the receiver with the given decimal value, or with - /// null if the value is `nil`. - var decimal: Decimal? { - get { - switch self { - case .int64(let i): return Decimal(workaround: i) - case .double(let d): return Decimal(workaround: d) - case .decimal(let d): return d - default: return nil - } - } - set { - self = newValue.map(JSON.decimal) ?? nil - } - } - - /// Returns the receiver as a `Decimal` if possible. - /// - Returns: A `Decimal` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` - /// that contains a valid decimal number representation, otherwise `nil`. - /// - Note: Whitespace is not allowed in the string representation. - var asDecimal: Decimal? { +import Foundation + +// MARK: Basic accessors + +public extension JSON { + /// Returns the numeric value as a `Decimal` if the receiver is `.int64`, `.double`, or `.decimal`, otherwise `nil`. + /// + /// When setting, replaces the receiver with the given decimal value, or with + /// null if the value is `nil`. + var decimal: Decimal? { + get { switch self { case .int64(let i): return Decimal(workaround: i) case .double(let d): return Decimal(workaround: d) case .decimal(let d): return d - case .string(let s) where !s.isEmpty: - // Decimal(string:locale:) uses Scanner, but it will skip whitespace and allow trailing characters, - // neither of which are appropriate (SR-3128) - let scanner = Scanner(string: s) - scanner.charactersToBeSkipped = nil - var decimal = Decimal() - if !scanner.scanDecimal(&decimal) { - return nil - } - #if os(iOS) || os(OSX) || os(tvOS) || os(watchOS) - if !scanner.isAtEnd { return nil } - #else - // Linux in Swift 3.1 doesn't have isAtEnd - // Instead we'll just rely on knowing that scanLocation is in NSString indices (i.e. UTF-16 indices) - if scanner.scanLocation != s.utf16.count { return nil } - #endif - return decimal default: return nil } } - - /// Returns the receiver as a `Decimal` if it is `.int64`, `.double`, or `.decimal`. - /// - Returns: A `Decimal`. - /// - Throws: `JSONError` if the receiver is not `.int64`, `.double`, or `.decimal`. - func getDecimal() throws -> Decimal { - switch self { - case .int64(let i): return Decimal(workaround: i) - case .double(let d): return Decimal(workaround: d) - case .decimal(let d): return d - default: throw hideThrow(JSONError.missingOrInvalidType(path: nil, expected: .required(.number), actual: .forValue(self))) - } + set { + self = newValue.map(JSON.decimal) ?? nil } - - /// Returns the receiver as a `Decimal` if it is `.int64`, `.double`, or `.decimal`. - /// - Returns: A `Decimal`, or `nil` if the receiver is `null`. - /// - Throws: `JSONError` if the receiver is not `.int64`, `.double`, or `.decimal`. - func getDecimalOrNil() throws -> Decimal? { - switch self { - case .int64(let i): return Decimal(workaround: i) - case .double(let d): return Decimal(workaround: d) - case .decimal(let d): return d - case .null: return nil - default: throw hideThrow(JSONError.missingOrInvalidType(path: nil, expected: .required(.number), actual: .forValue(self))) - } - } - - /// Returns the receiver as a `Decimal` if possible. - /// - Returns: A `Decimal` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` - /// that contains a valid decimal number representation. - /// - Throws: `JSONError` if the receiver is the wrong type, or is a `.string` that does not contain - /// a valid decimal number representation. - /// - Note: Whitespace is not allowed in the string representation. - func toDecimal() throws -> Decimal { - guard let value = asDecimal else { - throw hideThrow(JSONError.missingOrInvalidType(path: nil, expected: .required(.number), actual: .forValue(self))) + } + + /// Returns the receiver as a `Decimal` if possible. + /// - Returns: A `Decimal` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` + /// that contains a valid decimal number representation, otherwise `nil`. + /// - Note: Whitespace is not allowed in the string representation. + var asDecimal: Decimal? { + switch self { + case .int64(let i): return Decimal(workaround: i) + case .double(let d): return Decimal(workaround: d) + case .decimal(let d): return d + case .string(let s) where !s.isEmpty: + // Decimal(string:locale:) uses Scanner, but it will skip whitespace and allow trailing characters, + // neither of which are appropriate (SR-3128) + let scanner = Scanner(string: s) + scanner.charactersToBeSkipped = nil + var decimal = Decimal() + if !scanner.scanDecimal(&decimal) { + return nil } - return value - } - - /// Returns the receiver as a `Decimal` if possible. - /// - Returns: A `Decimal` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` - /// that contains a valid decimal number representation, or `nil` if the receiver is `null`. - /// - Throws: `JSONError` if the receiver is the wrong type, or is a `.string` that does not contain - /// a valid decimal number representation. - /// - Note: Whitespace is not allowed in the string representation. - func toDecimalOrNil() throws -> Decimal? { - if let value = asDecimal { return value } - else if isNull { return nil } - else { throw hideThrow(JSONError.missingOrInvalidType(path: nil, expected: .optional(.number), actual: .forValue(self))) } - } - - // MARK: - Deprecated - - /// Returns the receiver as an `NSDecimalNumber` if possible. (Deprecated) - /// - Note: Deprecated in favor of `asDecimal`. - /// - Returns: An `NSDecimalNumber` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` - /// that contains a valid decimal number representation, otherwise `nil`. - /// - Note: Whitespace is not allowed in the string representation. - @available(*, deprecated, message: "use asDecimal instead") - var asDecimalNumber: NSDecimalNumber? { - return asDecimal.map(NSDecimalNumber.init(decimal:)) - } - - /// Returns the receiver as an `NSDecimalNumber` if it is `.int64`, `.double`, or `.decimal`. (Deprecated) - /// - Note: Deprecated in favor of `getDecimal()`. - /// - Returns: An `NSDecimalNumber`. - /// - Throws: `JSONError` if the receiver is not `.int64`, `.double`, or `.decimal`. - @available(*, deprecated, message: "use getDecimal() instead") - func getDecimalNumber() throws -> NSDecimalNumber { - return try NSDecimalNumber(decimal: getDecimal()) - } - - /// Returns the receiver as an `NSDecimalNumber` if it is `.int64`, `.double`, or `.decimal`. (Deprecated) - /// - Note: Deprecated in favor of `getDecimalOrNil()`. - /// - Returns: An `NSDecimalNumber`, or `nil` if the receiver is `null`. - /// - Throws: `JSONError` if the receiver is not `.int64`, `.double`, or `.decimal`. - @available(*, deprecated, message: "use getDecimalOrNil() instead") - func getDecimalNumberOrNil() throws -> NSDecimalNumber? { - return try getDecimalOrNil().map(NSDecimalNumber.init(decimal:)) + #if os(iOS) || os(OSX) || os(tvOS) || os(watchOS) + if !scanner.isAtEnd { return nil } + #else + // Linux in Swift 3.1 doesn't have isAtEnd + // Instead we'll just rely on knowing that scanLocation is in NSString indices (i.e. UTF-16 indices) + if scanner.scanLocation != s.utf16.count { return nil } + #endif + return decimal + default: return nil } - - /// Returns the receiver as an `NSDecimalNumber` if possible. (Deprecated) - /// - Note: Deprecated in favor of `toDecimal()`. - /// - Returns: An `NSDecimalNumber` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` - /// that contains a valid decimal number representation. - /// - Throws: `JSONError` if the receiver is the wrong type, or is a `.string` that does not contain - /// a valid decimal number representation. - /// - Note: Whitespace is not allowed in the string representation. - @available(*, deprecated, message: "use toDecimal() instead") - func toDecimalNumber() throws -> NSDecimalNumber { - return try NSDecimalNumber(decimal: toDecimal()) + } + + /// Returns the receiver as a `Decimal` if it is `.int64`, `.double`, or `.decimal`. + /// - Returns: A `Decimal`. + /// - Throws: `JSONError` if the receiver is not `.int64`, `.double`, or `.decimal`. + func getDecimal() throws -> Decimal { + switch self { + case .int64(let i): return Decimal(workaround: i) + case .double(let d): return Decimal(workaround: d) + case .decimal(let d): return d + default: throw hideThrow(JSONError.missingOrInvalidType(path: nil, expected: .required(.number), actual: .forValue(self))) } - - /// Returns the receiver as an `NSDecimalNumber` if possible. (Deprecated) - /// - Note: Deprecated in favor of `toDecimalOrNil()`. - /// - Returns: An `NSDecimalNumber` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` - /// that contains a valid decimal number representation, or `nil` if the receiver is `null`. - /// - Throws: `JSONError` if the receiver is the wrong type, or is a `.string` that does not contain - /// a valid decimal number representation. - /// - Note: Whitespace is not allowed in the string representation. - @available(*, deprecated, message: "use toDecimalOrNil() instead") - func toDecimalNumberOrNil() throws -> NSDecimalNumber? { - return try toDecimalOrNil().map(NSDecimalNumber.init(decimal:)) + } + + /// Returns the receiver as a `Decimal` if it is `.int64`, `.double`, or `.decimal`. + /// - Returns: A `Decimal`, or `nil` if the receiver is `null`. + /// - Throws: `JSONError` if the receiver is not `.int64`, `.double`, or `.decimal`. + func getDecimalOrNil() throws -> Decimal? { + switch self { + case .int64(let i): return Decimal(workaround: i) + case .double(let d): return Decimal(workaround: d) + case .decimal(let d): return d + case .null: return nil + default: throw hideThrow(JSONError.missingOrInvalidType(path: nil, expected: .required(.number), actual: .forValue(self))) } } - // MARK: - Keyed accessors + /// Returns the receiver as a `Decimal` if possible. + /// - Returns: A `Decimal` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` + /// that contains a valid decimal number representation. + /// - Throws: `JSONError` if the receiver is the wrong type, or is a `.string` that does not contain + /// a valid decimal number representation. + /// - Note: Whitespace is not allowed in the string representation. + func toDecimal() throws -> Decimal { + guard let value = asDecimal else { + throw hideThrow(JSONError.missingOrInvalidType(path: nil, expected: .required(.number), actual: .forValue(self))) + } + return value + } - public extension JSON { - /// Subscripts the receiver with `key` and returns the result as a `Decimal`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: A `Decimal`. - /// - Throws: `JSONError` if the key doesn't exist or the value is the wrong type, or if - /// the receiver is not an object. - func getDecimal(_ key: String) throws -> Decimal { - let dict = try getObject() - let value = try getRequired(dict, key: key, type: .number) - return try scoped(key) { try value.getDecimal() } - } - - /// Subscripts the receiver with `key` and returns the result as a `Decimal`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: A `Decimal`, or `nil` if the key doesn't exist or the value is `null`. - /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is - /// not an object. - func getDecimalOrNil(_ key: String) throws -> Decimal? { - let dict = try getObject() - guard let value = dict[key] else { return nil } - return try scoped(key) { try value.getDecimalOrNil() } - } - - /// Subscripts the receiver with `key` and returns the result as a `Decimal`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: A `Decimal`. - /// - Throws: `JSONError` if the key doesn't exist or the value is `null`, a boolean, an object, - /// an array, or a string that cannot be coerced to a decimal number, or if the - /// receiver is not an object. - func toDecimal(_ key: String) throws -> Decimal { - let dict = try getObject() - let value = try getRequired(dict, key: key, type: .number) - return try scoped(key) { try value.toDecimal() } - } - - /// Subscripts the receiver with `key` and returns the result as a `Decimal`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: A `Decimal`, or `nil` if the key doesn't exist or the value is `null`. - /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that - /// cannot be coerced to a decimal number, or if the receiver is not an object. - func toDecimalOrNil(_ key: String) throws -> Decimal? { - let dict = try getObject() - guard let value = dict[key] else { return nil } - return try scoped(key) { try value.toDecimalOrNil() } - } - - // MARK: - Deprecated - - /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `getDecimal()`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`. - /// - Throws: `JSONError` if the key doesn't exist or the value is the wrong type, or if - /// the receiver is not an object. - @available(*, deprecated, message: "use getDecimal() instead") - func getDecimalNumber(_ key: String) throws -> NSDecimalNumber { - return try NSDecimalNumber(decimal: getDecimal(key)) - } - - /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `getDecimalOrNil()`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`, or `nil` if the key doesn't exist or the value is `null`. - /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is - /// not an object. - @available(*, deprecated, message: "use getDecimalOrNil() instead") - func getDecimalNumberOrNil(_ key: String) throws -> NSDecimalNumber? { - return try getDecimalOrNil(key).map(NSDecimalNumber.init(decimal:)) - } - - /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `toDecimal()`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`. - /// - Throws: `JSONError` if the key doesn't exist or the value is `null`, a boolean, an object, - /// an array, or a string that cannot be coerced to a decimal number, or if the - /// receiver is not an object. - @available(*, deprecated, message: "use toDecimal() instead") - func toDecimalNumber(_ key: String) throws -> NSDecimalNumber { - return try NSDecimalNumber(decimal: toDecimal(key)) - } - - /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `toDecimalOrNil()`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`, or `nil` if the key doesn't exist or the value is `null`. - /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that - /// cannot be coerced to a decimal number, or if the receiver is not an object. - @available(*, deprecated, message: "use toDecimalOrNil() instead") - func toDecimalNumberOrNil(_ key: String) throws -> NSDecimalNumber? { - return try toDecimalOrNil(key).map(NSDecimalNumber.init(decimal:)) - } + /// Returns the receiver as a `Decimal` if possible. + /// - Returns: A `Decimal` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` + /// that contains a valid decimal number representation, or `nil` if the receiver is `null`. + /// - Throws: `JSONError` if the receiver is the wrong type, or is a `.string` that does not contain + /// a valid decimal number representation. + /// - Note: Whitespace is not allowed in the string representation. + func toDecimalOrNil() throws -> Decimal? { + if let value = asDecimal { return value } + else if isNull { return nil } + else { throw hideThrow(JSONError.missingOrInvalidType(path: nil, expected: .optional(.number), actual: .forValue(self))) } } - // MARK: - Indexed accessors + // MARK: - Deprecated - public extension JSON { - /// Subscripts the receiver with `index` and returns the result as a `Decimal`. - /// - Parameter index: The index that's used to subscript the receiver. - /// - Returns: A `Decimal`. - /// - Throws: `JSONError` if the index is out of bounds or the value is the wrong type, or if - /// the receiver is not an array. - func getDecimal(_ index: Int) throws -> Decimal { - let array = try getArray() - let value = try getRequired(array, index: index, type: .number) - return try scoped(index) { try value.getDecimal() } - } - - /// Subscripts the receiver with `index` and returns the result as a `Decimal`. - /// - Parameter index: The index that's used to subscript the receiver. - /// - Returns: A `Decimal`, or `nil` if the index is out of bounds or the value is `null`. - /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is not an array. - func getDecimalOrNil(_ index: Int) throws -> Decimal? { - let array = try getArray() - guard let value = array[safe: index] else { return nil } - return try scoped(index) { try value.getDecimalOrNil() } - } - - /// Subscripts the receiver with `index` and returns the result as a `Decimal`. - /// - Parameter index: The index that's used to subscript the receiver. - /// - Returns: A `Decimal`. - /// - Throws: `JSONError` if the index is out of bounds or the value is `null`, a boolean, - /// an object, an array, or a string that cannot be coerced to a decimal number, or - /// if the receiver is not an array. - func toDecimal(_ index: Int) throws -> Decimal { - let array = try getArray() - let value = try getRequired(array, index: index, type: .number) - return try scoped(index) { try value.toDecimal() } - } - - /// Subscripts the receiver with `index` and returns the result as a `Decimal`. - /// - Parameter index: The index that's used to subscript the receiver. - /// - Returns: A `Decimal`, or `nil` if the index is out of bounds or the value is `null`. - /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that - /// cannot be coerced to a decimal number, or if the receiver is not an array. - func toDecimalOrNil(_ index: Int) throws -> Decimal? { - let array = try getArray() - guard let value = array[safe: index] else { return nil } - return try scoped(index) { try value.toDecimalOrNil() } - } - - // MARK: - Deprecated - - /// Subscripts the receiver with `index` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `getDecimal()`. - /// - Parameter index: The index that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`. - /// - Throws: `JSONError` if the index is out of bounds or the value is the wrong type, or if - /// the receiver is not an array. - @available(*, deprecated, message: "use getDecimal() instead") - func getDecimalNumber(_ index: Int) throws -> NSDecimalNumber { - return try NSDecimalNumber(decimal: getDecimal(index)) - } - - /// Subscripts the receiver with `index` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `getDecimalOrNil()`. - /// - Parameter index: The index that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`, or `nil` if the index is out of bounds or the value is `null`. - /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is not an array. - @available(*, deprecated, message: "use getDecimalOrNil() instead") - func getDecimalNumberOrNil(_ index: Int) throws -> NSDecimalNumber? { - return try getDecimalOrNil(index).map(NSDecimalNumber.init(decimal:)) - } - - /// Subscripts the receiver with `index` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `toDecimal()`. - /// - Parameter index: The index that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`. - /// - Throws: `JSONError` if the index is out of bounds or the value is `null`, a boolean, - /// an object, an array, or a string that cannot be coerced to a decimal number, or - /// if the receiver is not an array. - @available(*, deprecated, message: "use toDecimal() instead") - func toDecimalNumber(_ index: Int) throws -> NSDecimalNumber { - return try NSDecimalNumber(decimal: toDecimal(index)) - } - - /// Subscripts the receiver with `index` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `toDecimalOrNil()`. - /// - Parameter index: The index that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`, or `nil` if the index is out of bounds or the value is `null`. - /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that - /// cannot be coerced to a decimal number, or if the receiver is not an array. - @available(*, deprecated, message: "use toDecimalOrNil() instead") - func toDecimalNumberOrNil(_ index: Int) throws -> NSDecimalNumber? { - return try toDecimalOrNil(index).map(NSDecimalNumber.init(decimal:)) - } + /// Returns the receiver as an `NSDecimalNumber` if possible. (Deprecated) + /// - Note: Deprecated in favor of `asDecimal`. + /// - Returns: An `NSDecimalNumber` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` + /// that contains a valid decimal number representation, otherwise `nil`. + /// - Note: Whitespace is not allowed in the string representation. + @available(*, deprecated, message: "use asDecimal instead") + var asDecimalNumber: NSDecimalNumber? { + return asDecimal.map(NSDecimalNumber.init(decimal:)) } - // MARK: - + /// Returns the receiver as an `NSDecimalNumber` if it is `.int64`, `.double`, or `.decimal`. (Deprecated) + /// - Note: Deprecated in favor of `getDecimal()`. + /// - Returns: An `NSDecimalNumber`. + /// - Throws: `JSONError` if the receiver is not `.int64`, `.double`, or `.decimal`. + @available(*, deprecated, message: "use getDecimal() instead") + func getDecimalNumber() throws -> NSDecimalNumber { + return try NSDecimalNumber(decimal: getDecimal()) + } - public extension JSONObject { - /// Subscripts the receiver with `key` and returns the result as a `Decimal`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: A `Decimal`. - /// - Throws: `JSONError` if the key doesn't exist or the value is the wrong type, or if - /// the receiver is not an object. - func getDecimal(_ key: String) throws -> Decimal { - let value = try getRequired(self, key: key, type: .number) - return try scoped(key) { try value.getDecimal() } - } - - /// Subscripts the receiver with `key` and returns the result as a `Decimal`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: A `Decimal`, or `nil` if the key doesn't exist or the value is `null`. - /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is - /// not an object. - func getDecimalOrNil(_ key: String) throws -> Decimal? { - guard let value = self[key] else { return nil } - return try scoped(key) { try value.getDecimalOrNil() } - } - - /// Subscripts the receiver with `key` and returns the result as a `Decimal`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: A `Decimal`. - /// - Throws: `JSONError` if the key doesn't exist or the value is `null`, a boolean, an object, - /// an array, or a string that cannot be coerced to a decimal number, or if the - /// receiver is not an object. - func toDecimal(_ key: String) throws -> Decimal { - let value = try getRequired(self, key: key, type: .number) - return try scoped(key) { try value.toDecimal() } - } - - /// Subscripts the receiver with `key` and returns the result as a `Decimal`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: A `Decimal`, or `nil` if the key doesn't exist or the value is `null`. - /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that - /// cannot be coerced to a decimal number, or if the receiver is not an object. - func toDecimalOrNil(_ key: String) throws -> Decimal? { - guard let value = self[key] else { return nil } - return try scoped(key) { try value.toDecimalOrNil() } - } - - // MARK: - Deprecated - - /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `getDecimal()`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`. - /// - Throws: `JSONError` if the key doesn't exist or the value is the wrong type, or if - /// the receiver is not an object. - @available(*, deprecated, message: "use getDecimal() instead") - func getDecimalNumber(_ key: String) throws -> NSDecimalNumber { - return try NSDecimalNumber(decimal: getDecimal(key)) - } - - /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `getDecimalOrNil()`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`, or `nil` if the key doesn't exist or the value is `null`. - /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is - /// not an object. - @available(*, deprecated, message: "use getDecimalOrNil() instead") - func getDecimalNumberOrNil(_ key: String) throws -> NSDecimalNumber? { - return try getDecimalOrNil(key).map(NSDecimalNumber.init(decimal:)) - } - - /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `toDecimal()`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`. - /// - Throws: `JSONError` if the key doesn't exist or the value is `null`, a boolean, an object, - /// an array, or a string that cannot be coerced to a decimal number, or if the - /// receiver is not an object. - @available(*, deprecated, message: "use toDecimal() instead") - func toDecimalNumber(_ key: String) throws -> NSDecimalNumber { - return try NSDecimalNumber(decimal: toDecimal(key)) - } - - /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) - /// - Note: Deprecated in favor of `toDecimalOrNil()`. - /// - Parameter key: The key that's used to subscript the receiver. - /// - Returns: An `NSDecimalNumber`, or `nil` if the key doesn't exist or the value is `null`. - /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that - /// cannot be coerced to a decimal number, or if the receiver is not an object. - @available(*, deprecated, message: "use toDecimalOrNil() instead") - func toDecimalNumberOrNil(_ key: String) throws -> NSDecimalNumber? { - return try toDecimalOrNil(key).map(NSDecimalNumber.init(decimal:)) - } + /// Returns the receiver as an `NSDecimalNumber` if it is `.int64`, `.double`, or `.decimal`. (Deprecated) + /// - Note: Deprecated in favor of `getDecimalOrNil()`. + /// - Returns: An `NSDecimalNumber`, or `nil` if the receiver is `null`. + /// - Throws: `JSONError` if the receiver is not `.int64`, `.double`, or `.decimal`. + @available(*, deprecated, message: "use getDecimalOrNil() instead") + func getDecimalNumberOrNil() throws -> NSDecimalNumber? { + return try getDecimalOrNil().map(NSDecimalNumber.init(decimal:)) } - // MARK: - Internal Helpers + /// Returns the receiver as an `NSDecimalNumber` if possible. (Deprecated) + /// - Note: Deprecated in favor of `toDecimal()`. + /// - Returns: An `NSDecimalNumber` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` + /// that contains a valid decimal number representation. + /// - Throws: `JSONError` if the receiver is the wrong type, or is a `.string` that does not contain + /// a valid decimal number representation. + /// - Note: Whitespace is not allowed in the string representation. + @available(*, deprecated, message: "use toDecimal() instead") + func toDecimalNumber() throws -> NSDecimalNumber { + return try NSDecimalNumber(decimal: toDecimal()) + } - internal extension Int64 { - static let maxDecimal: Decimal = Decimal(workaround: Int64.max) - static let minDecimal: Decimal = Decimal(workaround: Int64.min) + /// Returns the receiver as an `NSDecimalNumber` if possible. (Deprecated) + /// - Note: Deprecated in favor of `toDecimalOrNil()`. + /// - Returns: An `NSDecimalNumber` if the receiver is `.int64`, `.double`, or `.decimal`, or is a `.string` + /// that contains a valid decimal number representation, or `nil` if the receiver is `null`. + /// - Throws: `JSONError` if the receiver is the wrong type, or is a `.string` that does not contain + /// a valid decimal number representation. + /// - Note: Whitespace is not allowed in the string representation. + @available(*, deprecated, message: "use toDecimalOrNil() instead") + func toDecimalNumberOrNil() throws -> NSDecimalNumber? { + return try toDecimalOrNil().map(NSDecimalNumber.init(decimal:)) + } +} + +// MARK: - Keyed accessors + +public extension JSON { + /// Subscripts the receiver with `key` and returns the result as a `Decimal`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: A `Decimal`. + /// - Throws: `JSONError` if the key doesn't exist or the value is the wrong type, or if + /// the receiver is not an object. + func getDecimal(_ key: String) throws -> Decimal { + let dict = try getObject() + let value = try getRequired(dict, key: key, type: .number) + return try scoped(key) { try value.getDecimal() } } - internal extension Decimal { - // NB: As of Swift 3.0.1, Decimal(_: Int64) incorrectly passes through Double first (SR-3125) - // and Decimal(_: Double) can produce incorrect results (SR-3130), so for now we're going to - // always go through NSNumber. - init(workaround value: Int64) { - self = NSNumber(value: value).decimalValue - } - - // NB: As of Swift 3.0.1, Decimal(_: Double) can produce incorrect results (SR-3130) - init(workaround value: Double) { - self = NSNumber(value: value).decimalValue - } + /// Subscripts the receiver with `key` and returns the result as a `Decimal`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: A `Decimal`, or `nil` if the key doesn't exist or the value is `null`. + /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is + /// not an object. + func getDecimalOrNil(_ key: String) throws -> Decimal? { + let dict = try getObject() + guard let value = dict[key] else { return nil } + return try scoped(key) { try value.getDecimalOrNil() } } -#endif // os(iOS) || os(OSX) || os(tvOS) || os(watchOS) || swift(>=3.1) + /// Subscripts the receiver with `key` and returns the result as a `Decimal`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: A `Decimal`. + /// - Throws: `JSONError` if the key doesn't exist or the value is `null`, a boolean, an object, + /// an array, or a string that cannot be coerced to a decimal number, or if the + /// receiver is not an object. + func toDecimal(_ key: String) throws -> Decimal { + let dict = try getObject() + let value = try getRequired(dict, key: key, type: .number) + return try scoped(key) { try value.toDecimal() } + } + + /// Subscripts the receiver with `key` and returns the result as a `Decimal`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: A `Decimal`, or `nil` if the key doesn't exist or the value is `null`. + /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that + /// cannot be coerced to a decimal number, or if the receiver is not an object. + func toDecimalOrNil(_ key: String) throws -> Decimal? { + let dict = try getObject() + guard let value = dict[key] else { return nil } + return try scoped(key) { try value.toDecimalOrNil() } + } + + // MARK: - Deprecated + + /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `getDecimal()`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`. + /// - Throws: `JSONError` if the key doesn't exist or the value is the wrong type, or if + /// the receiver is not an object. + @available(*, deprecated, message: "use getDecimal() instead") + func getDecimalNumber(_ key: String) throws -> NSDecimalNumber { + return try NSDecimalNumber(decimal: getDecimal(key)) + } + + /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `getDecimalOrNil()`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`, or `nil` if the key doesn't exist or the value is `null`. + /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is + /// not an object. + @available(*, deprecated, message: "use getDecimalOrNil() instead") + func getDecimalNumberOrNil(_ key: String) throws -> NSDecimalNumber? { + return try getDecimalOrNil(key).map(NSDecimalNumber.init(decimal:)) + } + + /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `toDecimal()`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`. + /// - Throws: `JSONError` if the key doesn't exist or the value is `null`, a boolean, an object, + /// an array, or a string that cannot be coerced to a decimal number, or if the + /// receiver is not an object. + @available(*, deprecated, message: "use toDecimal() instead") + func toDecimalNumber(_ key: String) throws -> NSDecimalNumber { + return try NSDecimalNumber(decimal: toDecimal(key)) + } + + /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `toDecimalOrNil()`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`, or `nil` if the key doesn't exist or the value is `null`. + /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that + /// cannot be coerced to a decimal number, or if the receiver is not an object. + @available(*, deprecated, message: "use toDecimalOrNil() instead") + func toDecimalNumberOrNil(_ key: String) throws -> NSDecimalNumber? { + return try toDecimalOrNil(key).map(NSDecimalNumber.init(decimal:)) + } +} + +// MARK: - Indexed accessors + +public extension JSON { + /// Subscripts the receiver with `index` and returns the result as a `Decimal`. + /// - Parameter index: The index that's used to subscript the receiver. + /// - Returns: A `Decimal`. + /// - Throws: `JSONError` if the index is out of bounds or the value is the wrong type, or if + /// the receiver is not an array. + func getDecimal(_ index: Int) throws -> Decimal { + let array = try getArray() + let value = try getRequired(array, index: index, type: .number) + return try scoped(index) { try value.getDecimal() } + } + + /// Subscripts the receiver with `index` and returns the result as a `Decimal`. + /// - Parameter index: The index that's used to subscript the receiver. + /// - Returns: A `Decimal`, or `nil` if the index is out of bounds or the value is `null`. + /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is not an array. + func getDecimalOrNil(_ index: Int) throws -> Decimal? { + let array = try getArray() + guard let value = array[safe: index] else { return nil } + return try scoped(index) { try value.getDecimalOrNil() } + } + + /// Subscripts the receiver with `index` and returns the result as a `Decimal`. + /// - Parameter index: The index that's used to subscript the receiver. + /// - Returns: A `Decimal`. + /// - Throws: `JSONError` if the index is out of bounds or the value is `null`, a boolean, + /// an object, an array, or a string that cannot be coerced to a decimal number, or + /// if the receiver is not an array. + func toDecimal(_ index: Int) throws -> Decimal { + let array = try getArray() + let value = try getRequired(array, index: index, type: .number) + return try scoped(index) { try value.toDecimal() } + } + + /// Subscripts the receiver with `index` and returns the result as a `Decimal`. + /// - Parameter index: The index that's used to subscript the receiver. + /// - Returns: A `Decimal`, or `nil` if the index is out of bounds or the value is `null`. + /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that + /// cannot be coerced to a decimal number, or if the receiver is not an array. + func toDecimalOrNil(_ index: Int) throws -> Decimal? { + let array = try getArray() + guard let value = array[safe: index] else { return nil } + return try scoped(index) { try value.toDecimalOrNil() } + } + + // MARK: - Deprecated + + /// Subscripts the receiver with `index` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `getDecimal()`. + /// - Parameter index: The index that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`. + /// - Throws: `JSONError` if the index is out of bounds or the value is the wrong type, or if + /// the receiver is not an array. + @available(*, deprecated, message: "use getDecimal() instead") + func getDecimalNumber(_ index: Int) throws -> NSDecimalNumber { + return try NSDecimalNumber(decimal: getDecimal(index)) + } + + /// Subscripts the receiver with `index` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `getDecimalOrNil()`. + /// - Parameter index: The index that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`, or `nil` if the index is out of bounds or the value is `null`. + /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is not an array. + @available(*, deprecated, message: "use getDecimalOrNil() instead") + func getDecimalNumberOrNil(_ index: Int) throws -> NSDecimalNumber? { + return try getDecimalOrNil(index).map(NSDecimalNumber.init(decimal:)) + } + + /// Subscripts the receiver with `index` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `toDecimal()`. + /// - Parameter index: The index that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`. + /// - Throws: `JSONError` if the index is out of bounds or the value is `null`, a boolean, + /// an object, an array, or a string that cannot be coerced to a decimal number, or + /// if the receiver is not an array. + @available(*, deprecated, message: "use toDecimal() instead") + func toDecimalNumber(_ index: Int) throws -> NSDecimalNumber { + return try NSDecimalNumber(decimal: toDecimal(index)) + } + + /// Subscripts the receiver with `index` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `toDecimalOrNil()`. + /// - Parameter index: The index that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`, or `nil` if the index is out of bounds or the value is `null`. + /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that + /// cannot be coerced to a decimal number, or if the receiver is not an array. + @available(*, deprecated, message: "use toDecimalOrNil() instead") + func toDecimalNumberOrNil(_ index: Int) throws -> NSDecimalNumber? { + return try toDecimalOrNil(index).map(NSDecimalNumber.init(decimal:)) + } +} + +// MARK: - + +public extension JSONObject { + /// Subscripts the receiver with `key` and returns the result as a `Decimal`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: A `Decimal`. + /// - Throws: `JSONError` if the key doesn't exist or the value is the wrong type, or if + /// the receiver is not an object. + func getDecimal(_ key: String) throws -> Decimal { + let value = try getRequired(self, key: key, type: .number) + return try scoped(key) { try value.getDecimal() } + } + + /// Subscripts the receiver with `key` and returns the result as a `Decimal`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: A `Decimal`, or `nil` if the key doesn't exist or the value is `null`. + /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is + /// not an object. + func getDecimalOrNil(_ key: String) throws -> Decimal? { + guard let value = self[key] else { return nil } + return try scoped(key) { try value.getDecimalOrNil() } + } + + /// Subscripts the receiver with `key` and returns the result as a `Decimal`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: A `Decimal`. + /// - Throws: `JSONError` if the key doesn't exist or the value is `null`, a boolean, an object, + /// an array, or a string that cannot be coerced to a decimal number, or if the + /// receiver is not an object. + func toDecimal(_ key: String) throws -> Decimal { + let value = try getRequired(self, key: key, type: .number) + return try scoped(key) { try value.toDecimal() } + } + + /// Subscripts the receiver with `key` and returns the result as a `Decimal`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: A `Decimal`, or `nil` if the key doesn't exist or the value is `null`. + /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that + /// cannot be coerced to a decimal number, or if the receiver is not an object. + func toDecimalOrNil(_ key: String) throws -> Decimal? { + guard let value = self[key] else { return nil } + return try scoped(key) { try value.toDecimalOrNil() } + } + + // MARK: - Deprecated + + /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `getDecimal()`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`. + /// - Throws: `JSONError` if the key doesn't exist or the value is the wrong type, or if + /// the receiver is not an object. + @available(*, deprecated, message: "use getDecimal() instead") + func getDecimalNumber(_ key: String) throws -> NSDecimalNumber { + return try NSDecimalNumber(decimal: getDecimal(key)) + } + + /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `getDecimalOrNil()`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`, or `nil` if the key doesn't exist or the value is `null`. + /// - Throws: `JSONError` if the value is the wrong type, or if the receiver is + /// not an object. + @available(*, deprecated, message: "use getDecimalOrNil() instead") + func getDecimalNumberOrNil(_ key: String) throws -> NSDecimalNumber? { + return try getDecimalOrNil(key).map(NSDecimalNumber.init(decimal:)) + } + + /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `toDecimal()`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`. + /// - Throws: `JSONError` if the key doesn't exist or the value is `null`, a boolean, an object, + /// an array, or a string that cannot be coerced to a decimal number, or if the + /// receiver is not an object. + @available(*, deprecated, message: "use toDecimal() instead") + func toDecimalNumber(_ key: String) throws -> NSDecimalNumber { + return try NSDecimalNumber(decimal: toDecimal(key)) + } + + /// Subscripts the receiver with `key` and returns the result as an `NSDecimalNumber`. (Deprecated) + /// - Note: Deprecated in favor of `toDecimalOrNil()`. + /// - Parameter key: The key that's used to subscript the receiver. + /// - Returns: An `NSDecimalNumber`, or `nil` if the key doesn't exist or the value is `null`. + /// - Throws: `JSONError` if the value is a boolean, an object, an array, or a string that + /// cannot be coerced to a decimal number, or if the receiver is not an object. + @available(*, deprecated, message: "use toDecimalOrNil() instead") + func toDecimalNumberOrNil(_ key: String) throws -> NSDecimalNumber? { + return try toDecimalOrNil(key).map(NSDecimalNumber.init(decimal:)) + } +} + +// MARK: - Internal Helpers + +internal extension Int64 { + static let maxDecimal: Decimal = Decimal(workaround: Int64.max) + static let minDecimal: Decimal = Decimal(workaround: Int64.min) +} + +internal extension UInt64 { + static let maxDecimal: Decimal = Decimal(workaround: UInt64.max) + static let minDecimal: Decimal = Decimal(workaround: UInt64.min) +} + +internal extension Decimal { + // NB: As of Swift 3.0.1, Decimal(_: Int64) incorrectly passes through Double first (SR-3125) + // and Decimal(_: Double) can produce incorrect results (SR-3130), so for now we're going to + // always go through NSNumber. + init(workaround value: Int64) { + self = NSNumber(value: value).decimalValue + } + + init(workaround value: UInt64) { + self = NSNumber(value: value).decimalValue + } + + // NB: As of Swift 3.0.1, Decimal(_: Double) can produce incorrect results (SR-3130) + init(workaround value: Double) { + self = NSNumber(value: value).decimalValue + } +} diff --git a/Sources/Decoder.swift b/Sources/Decoder.swift index 0e4c8ae..9891d79 100644 --- a/Sources/Decoder.swift +++ b/Sources/Decoder.swift @@ -66,7 +66,7 @@ extension JSON { /// - Parameter scalars: A sequence of `UnicodeScalar`s to parse as a JSON stream. /// - Parameter options: Options that controls JSON parsing. Defaults to no options. See `JSONOptions` for details. /// - Returns: A `JSONStreamDecoder`. - public static func decodeStream(_ scalars: Seq, options: JSONOptions = []) -> JSONStreamDecoder> where Seq.Iterator.Element == UnicodeScalar { + public static func decodeStream(_ scalars: Seq, options: JSONOptions = []) -> JSONStreamDecoder> { var parserOptions = options.parserOptions parserOptions.streaming = true let parser = JSONParser(scalars, options: parserOptions) @@ -74,7 +74,7 @@ extension JSON { } @available(*, deprecated, message: "Use JSON.decodeStream(_:options:) instead") - public static func decodeStream(_ scalars: Seq, strict: Bool) -> JSONStreamDecoder> where Seq.Iterator.Element == UnicodeScalar { + public static func decodeStream(_ scalars: Seq, strict: Bool) -> JSONStreamDecoder> { return decodeStream(scalars, options: JSONOptions(strict: strict)) } } @@ -99,8 +99,6 @@ public struct JSONOptions { /// instead of as `Double` values. /// /// The default value is `false`. - /// - /// - Note: This option is ignored on platforms without `Decimal`. public var useDecimals: Bool = false /// Returns a new `JSONOptions` with default values. diff --git a/Sources/Encoder.swift b/Sources/Encoder.swift index 7fb65fd..d026bf0 100644 --- a/Sources/Encoder.swift +++ b/Sources/Encoder.swift @@ -12,9 +12,7 @@ // except according to those terms. // -#if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - import struct Foundation.Decimal -#endif +import struct Foundation.Decimal extension JSON { /// Encodes a `JSON` to a `String`. @@ -79,16 +77,9 @@ extension JSON { stream.write(String(value)) } - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) private static func encodeDecimal(_ value: Decimal, toStream stream: inout Target) { stream.write(String(describing: value)) } - #else - private static func encodeDecimal(_ value: DecimalPlaceholder, toStream stream: inout Target) { - // This is a dummy value. Lets just encode it as null for the time being. - stream.write("null") - } - #endif private static func encodeString(_ value: String, toStream stream: inout Target) { stream.write("\"") diff --git a/Sources/Info.plist b/Sources/Info.plist index 0de69c1..e0f4bf7 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.0.3 + 3.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/Sources/JSON.swift b/Sources/JSON.swift index f9a079c..bec3dd8 100644 --- a/Sources/JSON.swift +++ b/Sources/JSON.swift @@ -12,16 +12,7 @@ // except according to those terms. // -#if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - import struct Foundation.Decimal -#else - /// A placeholder used for platforms that don't support `Decimal`. - public struct DecimalPlaceholder: Equatable { - public static func ==(lhs: DecimalPlaceholder, rhs: DecimalPlaceholder) -> Bool { - return true - } - } -#endif +import struct Foundation.Decimal /// A single JSON-compatible value. public enum JSON { @@ -37,18 +28,10 @@ public enum JSON { /// When decoding, any integer that doesn't fit in 64 bits and any floating-point number /// is decoded as a `Double`. case double(Double) - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) /// A decimal number. /// When the decoding option `.useDecimals` is used, any value that would otherwise be /// decoded as a `Double` is decoded as a `Decimal` instead. case decimal(Decimal) - #else - /// A placeholder for decimal number support. - /// This exists purely to work around Swift's poor support for conditionally-compiled - /// enum variants. At such time as Linux gains `Decimal` support, this will turn - /// into a real case. In the meantime, this case should be ignored. - case decimal(DecimalPlaceholder) - #endif /// An object. case object(JSONObject) /// An array. @@ -70,11 +53,9 @@ public enum JSON { public init(_ d: Double) { self = .double(d) } - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) public init(_ d: Decimal) { self = .decimal(d) } - #endif /// Initializes `self` as an object with the value `obj`. public init(_ obj: JSONObject) { self = .object(obj) @@ -121,17 +102,9 @@ extension JSON: Equatable { case (.decimal(let a), .decimal(let b)): return a == b case (.int64(let a), .double(let b)): return Double(a) == b case (.int64(let a), .decimal(let b)): - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - return Decimal(workaround: a) == b - #else - return false - #endif + return Decimal(workaround: a) == b case (.double(let a), .decimal(let b)): - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - return Decimal(workaround: a) == b - #else - return false - #endif + return Decimal(workaround: a) == b case (.double, .int64), (.decimal, .int64), (.decimal, .double): return rhs == lhs case (.object(let a), .object(let b)): return a == b case (.array(let a), .array(let b)): return a == b diff --git a/Sources/JSONError.swift b/Sources/JSONError.swift index 7a4614b..775f38f 100644 --- a/Sources/JSONError.swift +++ b/Sources/JSONError.swift @@ -12,10 +12,8 @@ // except according to those terms. // -#if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - import struct Foundation.Decimal - import class Foundation.NSDecimalNumber -#endif +import struct Foundation.Decimal +import class Foundation.NSDecimalNumber // MARK: JSONError @@ -38,22 +36,12 @@ public enum JSONError: Error, CustomStringConvertible { /// - Parameter value: The actual value at that path. /// - Parameter expected: The type that the value doesn't fit in, e.g. `Int.self`. case outOfRangeDouble(path: String?, value: Double, expected: Any.Type) - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) /// Thrown when a decimal value is coerced to a smaller type (e.g. `Decimal` to `Int`) /// and the value doesn't fit in the smaller type. /// - Parameter path: The path of the value that cuased the error. /// - Parameter value: The actual value at that path. /// - Parameter expected: The type that the value doesn't fit in, e.g. `Int.self`. case outOfRangeDecimal(path: String?, value: Decimal, expected: Any.Type) - #else - /// Thrown when a decimal value is coerced to a smaller type (e.g. `Decimal` to `Int`) - /// and the value doesn't fit in the smaller type. - /// - Note: This error is never actually thrown for platforms that do not support `Decimal`. - /// - Parameter path: The path of the value that cuased the error. - /// - Parameter value: The actual value at that path. - /// - Parameter expected: The type that the value doesn't fit in, e.g. `Int.self`. - case outOfRangeDecimal(path: String?, value: DecimalPlaceholder, expected: Any.Type) - #endif public var description: String { switch self { @@ -104,22 +92,13 @@ public enum JSONError: Error, CustomStringConvertible { case number = "number" case object = "object" case array = "array" - #if !os(iOS) && !os(OSX) && !os(watchOS) && !os(tvOS) && !swift(>=3.1) - case decimalPlaceholder = "decimalPlaceholder" - #endif internal static func forValue(_ value: JSON) -> JSONType { switch value { case .null: return .null case .bool: return .bool case .string: return .string - case .int64, .double: return .number - case .decimal: - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - return .number - #else - return .decimalPlaceholder - #endif + case .int64, .double, .decimal: return .number case .object: return .object case .array: return .array } @@ -190,7 +169,7 @@ public extension JSON { /// is too large to fit in an `Int`. func getInt() throws -> Int { guard let val = self.int64 else { throw hideThrow(JSONError.missingOrInvalidType(path: nil, expected: .required(.number), actual: .forValue(self))) } - let truncated = Int(truncatingBitPattern: val) + let truncated = Int(truncatingIfNeeded: val) guard Int64(truncated) == val else { throw hideThrow(JSONError.outOfRangeInt64(path: nil, value: val, expected: Int.self)) } return truncated } @@ -201,7 +180,7 @@ public extension JSON { /// is too large to fit in an `Int`. func getIntOrNil() throws -> Int? { if let val = self.int64 { - let truncated = Int(truncatingBitPattern: val) + let truncated = Int(truncatingIfNeeded: val) guard Int64(truncated) == val else { throw hideThrow(JSONError.outOfRangeInt64(path: nil, value: val, expected: Int.self)) } return truncated } else if isNull { return nil } @@ -282,12 +261,7 @@ public extension JSON { case .bool(let b): return String(b) case .int64(let i): return String(i) case .double(let d): return String(d) - case .decimal(let d): - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - return String(describing: d) - #else - break - #endif + case .decimal(let d): return String(describing: d) default: break } throw hideThrow(JSONError.missingOrInvalidType(path: nil, expected: expected, actual: .forValue(self))) @@ -358,7 +332,7 @@ public extension JSON { /// or a floating-point value that does not fit in an `Int`. func toInt() throws -> Int { let val = try toInt64() - let truncated = Int(truncatingBitPattern: val) + let truncated = Int(truncatingIfNeeded: val) guard Int64(truncated) == val else { throw hideThrow(JSONError.outOfRangeInt64(path: nil, value: val, expected: Int.self)) } return truncated } @@ -372,7 +346,7 @@ public extension JSON { /// or a floating-point value that does not fit in an `Int`. func toIntOrNil() throws -> Int? { guard let val = try toInt64OrNil() else { return nil } - let truncated = Int(truncatingBitPattern: val) + let truncated = Int(truncatingIfNeeded: val) guard Int64(truncated) == val else { throw hideThrow(JSONError.outOfRangeInt64(path: nil, value: val, expected: Int.self)) } return truncated } @@ -401,12 +375,8 @@ public extension JSON { case .int64(let i): return Double(i) case .double(let d): return d case .decimal(let d): - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - // NB: Decimal does not have any appropriate accessor - return NSDecimalNumber(decimal: d).doubleValue - #else - break - #endif + // NB: Decimal does not have any appropriate accessor + return NSDecimalNumber(decimal: d).doubleValue case .string(let s): return Double(s) case .null: return nil default: break diff --git a/Sources/JSONObject.swift b/Sources/JSONObject.swift index dc484c7..d07426e 100644 --- a/Sources/JSONObject.swift +++ b/Sources/JSONObject.swift @@ -31,6 +31,14 @@ public struct JSONObject { } } + /// Creates an object from a dictionary. + /// + /// - Note: This is not `public` because `JSONObject` is not guaranteed to be a `Dictionary` + /// wrapper in the future and we don't want anyone to assume this has O(1) behavior. + internal init(dict: [String: JSON]) { + dictionary = dict + } + /// The JSON object represented as a `[String: JSON]`. public fileprivate(set) var dictionary: [String: JSON] diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 82dff55..9bcc7f3 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -19,9 +19,7 @@ import Darwin #endif -#if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - import struct Foundation.Decimal -#endif +import struct Foundation.Decimal /// A streaming JSON parser that consumes a sequence of unicode scalars. public struct JSONParser: Sequence where Seq.Iterator.Element == UnicodeScalar { @@ -362,13 +360,13 @@ public struct JSONParserIterator: JSONEventIterator wher tempBuffer.reserveCapacity(12) } defer { self.tempBuffer = tempBuffer } - tempBuffer.append(Int8(truncatingBitPattern: c.value)) + tempBuffer.append(Int8(truncatingIfNeeded: c.value)) if options.strict { let digit: UnicodeScalar if c == "-" { guard let c2 = bump(), case "0"..."9" = c2 else { throw error(.invalidNumber) } digit = c2 - tempBuffer.append(Int8(truncatingBitPattern: digit.value)) + tempBuffer.append(Int8(truncatingIfNeeded: digit.value)) } else { digit = c } @@ -377,31 +375,29 @@ public struct JSONParserIterator: JSONEventIterator wher throw error(.invalidNumber) } } - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - @inline(__always) func parseDecimal(from buffer: UnsafeBufferPointer) throws -> Decimal { - guard let baseAddress = buffer.baseAddress, - // NB: For some reason String(bytesNoCopy:length:encoding:freeWhenDone:) takes a mutable pointer, - // even though it doesn't mutate the data. - let str = String(bytesNoCopy: UnsafeMutableRawPointer(mutating: UnsafeRawPointer(baseAddress)), length: buffer.count, encoding: .utf8, freeWhenDone: false) - else { - // It shouldn't be possible to fail, we already know it's valid utf-8 - return .nan - } - guard let decimal = Decimal(string: str, locale: nil) else { - // The above shouldn't fail, we only pass valid numbers to Decimal - throw error(.invalidNumber) - } - return decimal + @inline(__always) func parseDecimal(from buffer: UnsafeBufferPointer) throws -> Decimal { + guard let baseAddress = buffer.baseAddress, + // NB: For some reason String(bytesNoCopy:length:encoding:freeWhenDone:) takes a mutable pointer, + // even though it doesn't mutate the data. + let str = String(bytesNoCopy: UnsafeMutableRawPointer(mutating: UnsafeRawPointer(baseAddress)), length: buffer.count, encoding: .utf8, freeWhenDone: false) + else { + // It shouldn't be possible to fail, we already know it's valid utf-8 + return .nan + } + guard let decimal = Decimal(string: str, locale: nil) else { + // The above shouldn't fail, we only pass valid numbers to Decimal + throw error(.invalidNumber) } - #endif + return decimal + } /// Invoke this after parsing the "e" character. func parseExponent() throws -> JSONEvent { let c = try bumpRequired() - tempBuffer.append(Int8(truncatingBitPattern: c.value)) + tempBuffer.append(Int8(truncatingIfNeeded: c.value)) switch c { case "-", "+": guard let c = bump(), case "0"..."9" = c else { throw error(.invalidNumber) } - tempBuffer.append(Int8(truncatingBitPattern: c.value)) + tempBuffer.append(Int8(truncatingIfNeeded: c.value)) case "0"..."9": break default: throw error(.invalidNumber) } @@ -409,16 +405,14 @@ public struct JSONParserIterator: JSONEventIterator wher switch c { case "0"..."9": bump() - tempBuffer.append(Int8(truncatingBitPattern: c.value)) + tempBuffer.append(Int8(truncatingIfNeeded: c.value)) default: break loop } } - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - if options.useDecimals { - return try .decimalValue(tempBuffer.withUnsafeBufferPointer(parseDecimal(from:))) - } - #endif + if options.useDecimals { + return try .decimalValue(tempBuffer.withUnsafeBufferPointer(parseDecimal(from:))) + } tempBuffer.append(0) return .doubleValue(tempBuffer.withUnsafeBufferPointer({strtod($0.baseAddress!, nil)})) } @@ -426,35 +420,33 @@ public struct JSONParserIterator: JSONEventIterator wher switch c { case "0"..."9": bump() - tempBuffer.append(Int8(truncatingBitPattern: c.value)) + tempBuffer.append(Int8(truncatingIfNeeded: c.value)) case ".": bump() - tempBuffer.append(Int8(truncatingBitPattern: c.value)) + tempBuffer.append(Int8(truncatingIfNeeded: c.value)) guard let c = bump(), case "0"..."9" = c else { throw error(.invalidNumber) } - tempBuffer.append(Int8(truncatingBitPattern: c.value)) + tempBuffer.append(Int8(truncatingIfNeeded: c.value)) loop: while let c = base.peek() { switch c { case "0"..."9": bump() - tempBuffer.append(Int8(truncatingBitPattern: c.value)) + tempBuffer.append(Int8(truncatingIfNeeded: c.value)) case "e", "E": bump() - tempBuffer.append(Int8(truncatingBitPattern: c.value)) + tempBuffer.append(Int8(truncatingIfNeeded: c.value)) return try parseExponent() default: break loop } } - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - if options.useDecimals { - return try .decimalValue(tempBuffer.withUnsafeBufferPointer(parseDecimal(from:))) - } - #endif + if options.useDecimals { + return try .decimalValue(tempBuffer.withUnsafeBufferPointer(parseDecimal(from:))) + } tempBuffer.append(0) return .doubleValue(tempBuffer.withUnsafeBufferPointer({strtod($0.baseAddress!, nil)})) case "e", "E": bump() - tempBuffer.append(Int8(truncatingBitPattern: c.value)) + tempBuffer.append(Int8(truncatingIfNeeded: c.value)) return try parseExponent() default: break outerLoop @@ -477,12 +469,10 @@ public struct JSONParserIterator: JSONEventIterator wher return .int64Value(num) } // out of range, fall back to double/decimal - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - if options.useDecimals { - tempBuffer.removeLast() // drop the NUL - return try .decimalValue(tempBuffer.withUnsafeBufferPointer(parseDecimal(from:))) - } - #endif + if options.useDecimals { + tempBuffer.removeLast() // drop the NUL + return try .decimalValue(tempBuffer.withUnsafeBufferPointer(parseDecimal(from:))) + } return .doubleValue(tempBuffer.withUnsafeBufferPointer({strtod($0.baseAddress!, nil)})) case "t": let line = self.line, column = self.column @@ -533,7 +523,7 @@ public struct JSONParserIterator: JSONEventIterator wher throw error(.invalidEscape) } } - return UInt16(truncatingBitPattern: codepoint) + return UInt16(truncatingIfNeeded: codepoint) } @inline(__always) @discardableResult private mutating func bump() -> UnicodeScalar? { @@ -613,11 +603,7 @@ public enum JSONEvent: Hashable { case int64Value(Int64) /// A double value. case doubleValue(Double) - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) case decimalValue(Decimal) - #else - case decimalValue(DecimalPlaceholder) - #endif /// A string value. case stringValue(String) /// The null value. @@ -634,12 +620,7 @@ public enum JSONEvent: Hashable { case .booleanValue(let b): return b.hashValue << 4 + 5 case .int64Value(let i): return i.hashValue << 4 + 6 case .doubleValue(let d): return d.hashValue << 4 + 7 - case .decimalValue(let d): - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - return d.hashValue << 4 + 8 - #else - return 8 - #endif + case .decimalValue(let d): return d.hashValue << 4 + 8 case .stringValue(let s): return s.hashValue << 4 + 9 case .nullValue: return 10 case .error(let error): return error.hashValue << 4 + 11 diff --git a/Tests/PMJSONTests/Info.plist b/Tests/PMJSONTests/Info.plist index f437a4c..b75bb84 100644 --- a/Tests/PMJSONTests/Info.plist +++ b/Tests/PMJSONTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 2.0.3 + 3.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/Tests/PMJSONTests/JSONAccessorTests.swift b/Tests/PMJSONTests/JSONAccessorTests.swift index fbd6c1d..765c7a8 100644 --- a/Tests/PMJSONTests/JSONAccessorTests.swift +++ b/Tests/PMJSONTests/JSONAccessorTests.swift @@ -134,7 +134,7 @@ public final class JSONAccessorTests: XCTestCase { XCTAssertEqual(elts, [5,4,3]) XCTAssertEqual(indices, [0,1,2]) - XCTAssertFalse(try dict.forEachArrayOrNil("invalid", { _ in + XCTAssertFalse(try dict.forEachArrayOrNil("invalid", { _,_ in XCTFail("this shouldn't be invoked") })) @@ -146,7 +146,7 @@ public final class JSONAccessorTests: XCTestCase { XCTAssertEqual(elts, [5,4,3]) XCTAssertEqual(indices, [0,1,2]) - XCTAssertFalse(try dict.object!.forEachArrayOrNil("invalid", { _ in + XCTAssertFalse(try dict.object!.forEachArrayOrNil("invalid", { _,_ in XCTFail("this shouldn't be invoked") })) } catch { @@ -159,8 +159,8 @@ public final class JSONAccessorTests: XCTestCase { XCTAssertThrowsError(try dict["array"]!.flatMapArray("xs", { _ in [1] })) XCTAssertThrowsError(try dict["array"]!.flatMapArrayOrNil("xs", { _ -> Int? in 1 })) XCTAssertThrowsError(try dict["array"]!.flatMapArrayOrNil("xs", { _ in [1] })) - XCTAssertThrowsError(try dict["array"]!.forEachArray("xs", { _ in () })) - XCTAssertThrowsError(try dict["array"]!.forEachArrayOrNil("xs", { _ in () })) + XCTAssertThrowsError(try dict["array"]!.forEachArray("xs", { _,_ in () })) + XCTAssertThrowsError(try dict["array"]!.forEachArrayOrNil("xs", { _,_ in () })) // array-style accessors let array = dict["array"]! @@ -216,13 +216,13 @@ public final class JSONAccessorTests: XCTestCase { XCTAssertEqual(elts, [2, 4, 6]) XCTAssertEqual(indices, [0,1,2]) - XCTAssertThrowsError(try array.forEachArray(3, { _ in // null + XCTAssertThrowsError(try array.forEachArray(3, { _,_ in // null XCTFail("this shouldn't be invoked") })) - XCTAssertThrowsError(try array.forEachArray(4, { _ in // string + XCTAssertThrowsError(try array.forEachArray(4, { _,_ in // string XCTFail("this shouldn't be invoked") })) - XCTAssertThrowsError(try array.forEachArray(100, { _ in // out of bounds + XCTAssertThrowsError(try array.forEachArray(100, { _,_ in // out of bounds XCTFail("this shouldn't be invoked") })) @@ -234,13 +234,13 @@ public final class JSONAccessorTests: XCTestCase { XCTAssertEqual(elts, [2, 4, 6]) XCTAssertEqual(indices, [0,1,2]) - XCTAssertFalse(try array.forEachArrayOrNil(3, { _ in // null + XCTAssertFalse(try array.forEachArrayOrNil(3, { _,_ in // null XCTFail("this shouldn't be invoked") })) - XCTAssertThrowsError(try array.forEachArrayOrNil(4, { _ in // string + XCTAssertThrowsError(try array.forEachArrayOrNil(4, { _,_ in // string XCTFail("this shouldn't be invoked") })) - XCTAssertFalse(try array.forEachArrayOrNil(100, { _ in // out of bounds + XCTAssertFalse(try array.forEachArrayOrNil(100, { _,_ in // out of bounds XCTFail("this shouldn't be invoked") })) } catch { @@ -281,13 +281,11 @@ public final class JSONAccessorTests: XCTestCase { json.double = nil XCTAssertEqual(json, JSON.null) - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - XCTAssertNil(json.decimal) - json.decimal = 42 - XCTAssertEqual(json, 42) - json.decimal = nil - XCTAssertEqual(json, JSON.null) - #endif + XCTAssertNil(json.decimal) + json.decimal = 42 + XCTAssertEqual(json, 42) + json.decimal = nil + XCTAssertEqual(json, JSON.null) XCTAssertNil(json.object) json.object = ["foo": "bar"] @@ -313,14 +311,12 @@ public final class JSONAccessorTests: XCTestCase { func testMixedTypeEquality() { XCTAssertEqual(JSON.int64(42), JSON.double(42)) XCTAssertNotEqual(JSON.int64(42), JSON.double(42.1)) - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - XCTAssertEqual(JSON.int64(42), JSON.decimal(42)) - XCTAssertEqual(JSON.int64(Int64.max), JSON.decimal(Decimal(string: "9223372036854775807")!)) // Decimal(Int64.max) produces the wrong value - XCTAssertEqual(JSON.int64(7393662029337442), JSON.decimal(Decimal(string: "7393662029337442")!)) - XCTAssertNotEqual(JSON.int64(42), JSON.decimal(42.1)) - XCTAssertEqual(JSON.double(42), JSON.decimal(42)) - XCTAssertEqual(JSON.double(42.1), JSON.decimal(42.1)) - XCTAssertEqual(JSON.double(1e100), JSON.decimal(Decimal(string: "1e100")!)) // Decimal(_: Double) can produce incorrect values - #endif + XCTAssertEqual(JSON.int64(42), JSON.decimal(42)) + XCTAssertEqual(JSON.int64(Int64.max), JSON.decimal(Decimal(string: "9223372036854775807")!)) // Decimal(Int64.max) produces the wrong value + XCTAssertEqual(JSON.int64(7393662029337442), JSON.decimal(Decimal(string: "7393662029337442")!)) + XCTAssertNotEqual(JSON.int64(42), JSON.decimal(42.1)) + XCTAssertEqual(JSON.double(42), JSON.decimal(42)) + XCTAssertEqual(JSON.double(42.1), JSON.decimal(42.1)) + XCTAssertEqual(JSON.double(1e100), JSON.decimal(Decimal(string: "1e100")!)) // Decimal(_: Double) can produce incorrect values } } diff --git a/Tests/PMJSONTests/JSONDecoderTests.swift b/Tests/PMJSONTests/JSONDecoderTests.swift index 2a75d26..d17607f 100644 --- a/Tests/PMJSONTests/JSONDecoderTests.swift +++ b/Tests/PMJSONTests/JSONDecoderTests.swift @@ -51,14 +51,10 @@ public final class JSONDecoderTests: XCTestCase { ("testReencode", testReencode), ("testConverions", testConversions), ("testDepthLimit", testBOMDetection), - ("testUnicodeHeuristicDetection", testUnicodeHeuristicDetection) + ("testUnicodeHeuristicDetection", testUnicodeHeuristicDetection), + ("testDecimalParsing", testDecimalParsing), + ("testJSONErrorNSErrorDescription", testJSONErrorNSErrorDescription) ] - #if swift(>=3.1) - tests.append(contentsOf: [ - ("testDecimalParsing", testDecimalParsing), - ("testJSONErrorNSErrorDescription", testJSONErrorNSErrorDescription), - ]) - #endif return tests }() @@ -70,17 +66,13 @@ public final class JSONDecoderTests: XCTestCase { assertMatchesJSON(try JSON.decode("[1, 2, 3]"), [1, 2, 3]) assertMatchesJSON(try JSON.decode("{\"one\": 1, \"two\": 2, \"three\": 3}"), ["one": 1, "two": 2, "three": 3]) assertMatchesJSON(try JSON.decode("[1.23, 4e7]"), [1.23, 4e7]) - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - assertMatchesJSON(try JSON.decode("[1.23, 4e7]", options: [.useDecimals]), [JSON(1.23 as Decimal), JSON(4e7 as Decimal)]) - #endif + assertMatchesJSON(try JSON.decode("[1.23, 4e7]", options: [.useDecimals]), [JSON(1.23 as Decimal), JSON(4e7 as Decimal)]) } func testDouble() { XCTAssertEqual(try JSON.decode("-5.4272823085455e-05"), -5.4272823085455e-05) XCTAssertEqual(try JSON.decode("-5.4272823085455e+05"), -5.4272823085455e+05) - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - XCTAssertEqual(try JSON.decode("-5.4272823085455e+05", options: [.useDecimals]), JSON(Decimal(string: "-5.4272823085455e+05")!)) - #endif + XCTAssertEqual(try JSON.decode("-5.4272823085455e+05", options: [.useDecimals]), JSON(Decimal(string: "-5.4272823085455e+05")!)) } func testStringEscapes() { @@ -92,7 +84,6 @@ public final class JSONDecoderTests: XCTestCase { assertMatchesJSON(try JSON.decode("\"emoji fun: 💩\\uD83D\\uDCA9\""), "emoji fun: 💩💩") } - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) func testDecimalParsing() throws { let data = try readFixture("sample", withExtension: "json") // Decode the data and make sure it contains no .double values @@ -102,7 +93,6 @@ public final class JSONDecoderTests: XCTestCase { } XCTAssertNil(value) } - #endif func testReencode() throws { // sample.json contains a lot of edge cases, so we'll make sure we can re-encode it and re-decode it and get the same thing @@ -116,19 +106,17 @@ public final class JSONDecoderTests: XCTestCase { XCTFail("Re-encoded JSON doesn't match original") } } - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - do { - // test again with decimals - let json = try JSON.decode(data, options: [.useDecimals]) - let encoded = JSON.encodeAsString(json) - let json2 = try JSON.decode(encoded, options: [.useDecimals]) - if json != json2 { // This preserves all precision, but may still convert between int64 and decimal so we can't use matchesJSON - // NB: Don't use XCTAssertEquals because this JSON is too large to be printed to the console - try json.debugMatches(json2, ==) - XCTFail("Re-encoded JSON doesn't match original") - } + do { + // test again with decimals + let json = try JSON.decode(data, options: [.useDecimals]) + let encoded = JSON.encodeAsString(json) + let json2 = try JSON.decode(encoded, options: [.useDecimals]) + if json != json2 { // This preserves all precision, but may still convert between int64 and decimal so we can't use matchesJSON + // NB: Don't use XCTAssertEquals because this JSON is too large to be printed to the console + try json.debugMatches(json2, ==) + XCTFail("Re-encoded JSON doesn't match original") } - #endif + } } func testConversions() { @@ -136,9 +124,7 @@ public final class JSONDecoderTests: XCTestCase { XCTAssertEqual(JSON(42 as Int64), JSON.int64(42)) XCTAssertEqual(JSON(42 as Double), JSON.double(42)) XCTAssertEqual(JSON(42 as Int), JSON.int64(42)) - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - XCTAssertEqual(JSON(42 as Decimal), JSON.decimal(42)) - #endif + XCTAssertEqual(JSON(42 as Decimal), JSON.decimal(42)) XCTAssertEqual(JSON("foo"), JSON.string("foo")) XCTAssertEqual(JSON(["foo": true]), ["foo": true]) XCTAssertEqual(JSON([JSON.bool(true)] as JSONArray), [true]) // JSONArray @@ -147,7 +133,6 @@ public final class JSONDecoderTests: XCTestCase { XCTAssertEqual(JSON([[1,2,3],[4,5,6]].lazy.map(JSONArray.init)), [[1,2,3],[4,5,6]]) // Sequence of JSONArray } - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) func testJSONErrorNSErrorDescription() throws { let jserror: JSONError? do { @@ -163,7 +148,6 @@ public final class JSONDecoderTests: XCTestCase { } XCTAssertEqual(String(describing: error), error.localizedDescription) } - #endif func testDepthLimit() { func assertThrowsDepthError(_ string: String, limit: Int, file: StaticString = #file, line: UInt = #line) { diff --git a/Tests/PMJSONTests/JSONEncoderTests.swift b/Tests/PMJSONTests/JSONEncoderTests.swift index ccaa2bf..0c84f74 100644 --- a/Tests/PMJSONTests/JSONEncoderTests.swift +++ b/Tests/PMJSONTests/JSONEncoderTests.swift @@ -15,24 +15,16 @@ import PMJSON import XCTest -#if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - import struct Foundation.Decimal -#endif +import struct Foundation.Decimal /// - Note: The encoder is primarily tested with round-trip tests in `JSONDecoderTests`. public final class JSONEncoderTests: XCTestCase { - #if swift(>=3.1) public static let allLinuxTests = [ ("testDecimalEncoding", testDecimalEncoding) ] - #else - public static let allLinuxTests: [(String, (JSONEncoderTests) -> () -> Void)] = [] - #endif - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) func testDecimalEncoding() { XCTAssertEqual(JSON.encodeAsString(.decimal(42.714)), "42.714") XCTAssertEqual(JSON.encodeAsString([1, JSON(Decimal(string: "1.234567890123456789")!)]), "[1,1.234567890123456789]") } - #endif } diff --git a/Tests/PMJSONTests/JSONParserTests.swift b/Tests/PMJSONTests/JSONParserTests.swift index aa8d16e..46c22a5 100644 --- a/Tests/PMJSONTests/JSONParserTests.swift +++ b/Tests/PMJSONTests/JSONParserTests.swift @@ -116,7 +116,7 @@ private func assertParserEvents(_ input: String, streaming: Bool = false, _ even assertParserEvents(parser, events, file: file, line: line) } -private func assertParserEvents(_ parser: JSONParser, _ events: [JSONEvent], file: StaticString = #file, line: UInt = #line) where Seq.Iterator.Element == UnicodeScalar { +private func assertParserEvents(_ parser: JSONParser, _ events: [JSONEvent], file: StaticString = #file, line: UInt = #line) { var iter = parser.makeIterator() for (i, expected) in events.enumerated() { guard let event = iter.next() else { diff --git a/Tests/PMJSONTests/JSONTestSuite.swift b/Tests/PMJSONTests/JSONTestSuite.swift index ee9f7a8..1b12cce 100644 --- a/Tests/PMJSONTests/JSONTestSuite.swift +++ b/Tests/PMJSONTests/JSONTestSuite.swift @@ -15,179 +15,175 @@ import XCTest import PMJSON -#if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) - - /// Tests for [JSONTestSuite](https://github.com/nst/JSONTestSuite). - public final class JSONTestSuite: XCTestCase { - /// How we should expect to parse various test cases. - /// This is mainly intended for defining indeterminate cases, but may also - /// be used to override other cases when the case is believed to be incorrect. - private static let expectedParsing: [String: ShouldParse] = [ - "i_object_key_lone_2nd_surrogate": .no, - "i_string_1st_surrogate_but_2nd_missing": .no, - "i_string_1st_valid_surrogate_2nd_invalid": .no, - "i_string_incomplete_surrogate_and_escape_valid": .no, - "i_string_incomplete_surrogate_pair": .no, - "i_string_incomplete_surrogates_escape_valid": .no, - "i_string_inverted_surrogates_U+1D11E": .no, - "i_string_lone_second_surrogate": .no, - "i_string_not_in_unicode_range": .yes, - "i_string_truncated-utf-8": .yes, - - // The following tests are for noncharacters that are still valid codepoints - "i_string_unicode_U+10FFFE_nonchar": .yes, - "i_string_unicode_U+1FFFE_nonchar": .yes, - "i_string_unicode_U+FDD0_nonchar": .yes, - "i_string_unicode_U+FFFE_nonchar": .yes, - - "i_string_UTF-16_invalid_lonely_surrogate": .no, - "i_string_UTF-16_invalid_surrogate": .no, - "i_string_UTF-8_invalid_sequence": .yes, - "i_structure_500_nested_arrays": .yes, - "i_structure_UTF-8_BOM_empty_object": .yes, - "i_string_UTF-16LE_with_BOM": .yes, - - "n_number_then_00": .yes, // Indistinguishable from UTF-161LE - // The following test handling of invalid UTF-8 byte sequences, which we support - "n_string_invalid_utf-8": .yes, - "n_string_iso_latin_1": .yes, - "n_string_lone_utf8_continuation_byte": .yes, - "n_string_overlong_sequence_2_bytes": .yes, - "n_string_overlong_sequence_6_bytes": .yes, - "n_string_overlong_sequence_6_bytes_null": .yes, - "n_string_UTF8_surrogate_U+D800": .yes] +/// Tests for [JSONTestSuite](https://github.com/nst/JSONTestSuite). +public final class JSONTestSuite: XCTestCase { + /// How we should expect to parse various test cases. + /// This is mainly intended for defining indeterminate cases, but may also + /// be used to override other cases when the case is believed to be incorrect. + private static let expectedParsing: [String: ShouldParse] = [ + "i_object_key_lone_2nd_surrogate": .no, + "i_string_1st_surrogate_but_2nd_missing": .no, + "i_string_1st_valid_surrogate_2nd_invalid": .no, + "i_string_incomplete_surrogate_and_escape_valid": .no, + "i_string_incomplete_surrogate_pair": .no, + "i_string_incomplete_surrogates_escape_valid": .no, + "i_string_inverted_surrogates_U+1D11E": .no, + "i_string_lone_second_surrogate": .no, + "i_string_not_in_unicode_range": .yes, + "i_string_truncated-utf-8": .yes, - private static let testCases: [String: (url: URL, shouldParse: ShouldParse)] = { - var testCases: [String: (url: URL, shouldParse: ShouldParse)] = [:] - #if SWIFT_PACKAGE - // We don't have a resource bundle, so let's just look relative to our source file. - let fixtures = URL(fileURLWithPath: #file, isDirectory: false).deletingLastPathComponent().appendingPathComponent("JSONTestSuite") - guard FileManager.default.fileExists(atPath: fixtures.path) else { return [:] } - #else - guard let fixtures = Bundle(for: JSONTestSuite.self).resourcePath.map({ URL(fileURLWithPath: $0, isDirectory: true).appendingPathComponent("JSONTestSuite") }) - else { return [:] } - #endif - if let parsingEnum = FileManager.default.enumerator(at: fixtures, includingPropertiesForKeys: [.isDirectoryKey], options: .skipsHiddenFiles, errorHandler: nil) { - for case let url as URL in parsingEnum where url.pathExtension == "json" { - let name = url.deletingPathExtension().lastPathComponent - guard let identifier = name.sanitized else { - print("*** Skipping test \(url.lastPathComponent) due to invalid name") - continue - } - let shouldParse: ShouldParse - if let expected = expectedParsing[name] { - shouldParse = expected - } else if name.hasPrefix("y_") { - shouldParse = .yes - } else if name.hasPrefix("i_") { - shouldParse = .maybe - } else if name.hasPrefix("n_") { - shouldParse = .no - } else { - print("*** Skipping test \(url.lastPathComponent) due to unknown parse expectation") - continue - } - var selName = "test_\(identifier)" - var attempt = 1 - while testCases[selName] != nil { - attempt += 1 - selName = "test_\(identifier)_\(attempt)" - } - testCases[selName] = (url, shouldParse) - } - } - return testCases - }() + // The following tests are for noncharacters that are still valid codepoints + "i_string_unicode_U+10FFFE_nonchar": .yes, + "i_string_unicode_U+1FFFE_nonchar": .yes, + "i_string_unicode_U+FDD0_nonchar": .yes, + "i_string_unicode_U+FFFE_nonchar": .yes, - #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) - static private var initializedDynamicTests = false - public override class func defaultTestSuite() -> XCTestSuite { - if !initializedDynamicTests { - initializedDynamicTests = true - let imp = unsafeBitCast(executeIMP, to: IMP.self) - for name in testCases.keys { - class_addMethod(self, Selector(name), imp, "c@:^@") - } - } - return super.defaultTestSuite() - } + "i_string_UTF-16_invalid_lonely_surrogate": .no, + "i_string_UTF-16_invalid_surrogate": .no, + "i_string_UTF-8_invalid_sequence": .yes, + "i_structure_500_nested_arrays": .yes, + "i_structure_UTF-8_BOM_empty_object": .yes, + "i_string_UTF-16LE_with_BOM": .yes, - static let executeIMP: @convention(c) (JSONTestSuite, Selector, NSErrorPointer) -> Bool = { (this, cmd, outError) in - do { - try this.execute(name: String(describing: cmd)) - return true - } catch { - outError?.pointee = error as NSError - return false - } - } + "n_number_then_00": .yes, // Indistinguishable from UTF-161LE + // The following test handling of invalid UTF-8 byte sequences, which we support + "n_string_invalid_utf-8": .yes, + "n_string_iso_latin_1": .yes, + "n_string_lone_utf8_continuation_byte": .yes, + "n_string_overlong_sequence_2_bytes": .yes, + "n_string_overlong_sequence_6_bytes": .yes, + "n_string_overlong_sequence_6_bytes_null": .yes, + "n_string_UTF8_surrogate_U+D800": .yes] + + private static let testCases: [String: (url: URL, shouldParse: ShouldParse)] = { + var testCases: [String: (url: URL, shouldParse: ShouldParse)] = [:] + #if SWIFT_PACKAGE + // We don't have a resource bundle, so let's just look relative to our source file. + let fixtures = URL(fileURLWithPath: #file, isDirectory: false).deletingLastPathComponent().appendingPathComponent("JSONTestSuite") + guard FileManager.default.fileExists(atPath: fixtures.path) else { return [:] } + #else + guard let fixtures = Bundle(for: JSONTestSuite.self).resourcePath.map({ URL(fileURLWithPath: $0, isDirectory: true).appendingPathComponent("JSONTestSuite") }) + else { return [:] } #endif - - public static let allLinuxTests: [(String, (JSONTestSuite) -> () throws -> Void)] = Array(testCases.keys.map({ name in - return (name, { suite in { try suite.execute(name: name) } }) - })) - - enum ShouldParse { - case yes - case no - /// Whether it parses is implementation-defined. - case maybe - } - - func execute(name: String) throws { - guard let (url, shouldParse) = JSONTestSuite.testCases[name] - else { return XCTFail("No fixture URL found.") } - - let data = try Data(contentsOf: url) - do { - _ = try JSON.decode(data, options: [.strict]) - switch shouldParse { - case .yes: - break - case .maybe: - XCTFail("\(url.lastPathComponent) - indeterminate parsing unspecified - parse succeeded") - case .no: - XCTFail("\(url.lastPathComponent) - unexpected parse success") + if let parsingEnum = FileManager.default.enumerator(at: fixtures, includingPropertiesForKeys: [.isDirectoryKey], options: .skipsHiddenFiles, errorHandler: nil) { + for case let url as URL in parsingEnum where url.pathExtension == "json" { + let name = url.deletingPathExtension().lastPathComponent + guard let identifier = name.sanitized else { + print("*** Skipping test \(url.lastPathComponent) due to invalid name") + continue } - } catch { - switch shouldParse { - case .yes: - XCTFail("\(url.lastPathComponent) - could not parse data - \(error)") - case .maybe: - XCTFail("\(url.lastPathComponent) - indeterminate parsing unspecified - parse failed") - case .no: - break + let shouldParse: ShouldParse + if let expected = expectedParsing[name] { + shouldParse = expected + } else if name.hasPrefix("y_") { + shouldParse = .yes + } else if name.hasPrefix("i_") { + shouldParse = .maybe + } else if name.hasPrefix("n_") { + shouldParse = .no + } else { + print("*** Skipping test \(url.lastPathComponent) due to unknown parse expectation") + continue } + var selName = "test_\(identifier)" + var attempt = 1 + while testCases[selName] != nil { + attempt += 1 + selName = "test_\(identifier)_\(attempt)" + } + testCases[selName] = (url, shouldParse) + } + } + return testCases + }() + + #if os(iOS) || os(OSX) || os(watchOS) || os(tvOS) + static private var initializedDynamicTests = false + public override class var defaultTestSuite: XCTestSuite { + if !initializedDynamicTests { + initializedDynamicTests = true + let imp = unsafeBitCast(executeIMP, to: IMP.self) + for name in testCases.keys { + class_addMethod(self, Selector(name), imp, "c@:^@") } } + return super.defaultTestSuite } - private extension String { - /// Returns the string, sanitized to be a valid identifier. - /// If the string does not contain any valid identifier characters, returns `nil`. - var sanitized: String? { - guard let start = unicodeScalars.index(where: CharacterSet.identifierStart.contains) else { return nil } - let scalars = unicodeScalars.suffix(from: start) - var result = String.UnicodeScalarView() - result.append(contentsOf: scalars.lazy.map({ CharacterSet.identifierContinue.contains($0) ? $0 : "_" })) - return String(result) + static let executeIMP: @convention(c) (JSONTestSuite, Selector, NSErrorPointer) -> Bool = { (this, cmd, outError) in + do { + try this.execute(name: String(describing: cmd)) + return true + } catch { + outError?.pointee = error as NSError + return false } } + #endif + + public static let allLinuxTests: [(String, (JSONTestSuite) -> () throws -> Void)] = Array(testCases.keys.map({ name in + return (name, { suite in { try suite.execute(name: name) } }) + })) + + enum ShouldParse { + case yes + case no + /// Whether it parses is implementation-defined. + case maybe + } - private extension CharacterSet { - // Letters and _ - static let identifierStart: CharacterSet = { - var cs = CharacterSet.letters - cs.update(with: "_") - return cs - }() + func execute(name: String) throws { + guard let (url, shouldParse) = JSONTestSuite.testCases[name] + else { return XCTFail("No fixture URL found.") } - // Alphanumerics and _ - static let identifierContinue: CharacterSet = { - var cs = CharacterSet.alphanumerics - cs.update(with: "_") - return cs - }() + let data = try Data(contentsOf: url) + do { + _ = try JSON.decode(data, options: [.strict]) + switch shouldParse { + case .yes: + break + case .maybe: + XCTFail("\(url.lastPathComponent) - indeterminate parsing unspecified - parse succeeded") + case .no: + XCTFail("\(url.lastPathComponent) - unexpected parse success") + } + } catch { + switch shouldParse { + case .yes: + XCTFail("\(url.lastPathComponent) - could not parse data - \(error)") + case .maybe: + XCTFail("\(url.lastPathComponent) - indeterminate parsing unspecified - parse failed") + case .no: + break + } + } } +} + +private extension String { + /// Returns the string, sanitized to be a valid identifier. + /// If the string does not contain any valid identifier characters, returns `nil`. + var sanitized: String? { + guard let start = unicodeScalars.index(where: CharacterSet.identifierStart.contains) else { return nil } + let scalars = unicodeScalars.suffix(from: start) + var result = String.UnicodeScalarView() + result.append(contentsOf: scalars.lazy.map({ CharacterSet.identifierContinue.contains($0) ? $0 : "_" })) + return String(result) + } +} + +private extension CharacterSet { + // Letters and _ + static let identifierStart: CharacterSet = { + var cs = CharacterSet.letters + cs.update(with: "_") + return cs + }() -#endif // os(iOS) || os(OSX) || os(watchOS) || os(tvOS) || swift(>=3.1) + // Alphanumerics and _ + static let identifierContinue: CharacterSet = { + var cs = CharacterSet.alphanumerics + cs.update(with: "_") + return cs + }() +} diff --git a/Tests/PMJSONTests/SwiftCodableTests.swift b/Tests/PMJSONTests/SwiftCodableTests.swift new file mode 100644 index 0000000..0ae09bc --- /dev/null +++ b/Tests/PMJSONTests/SwiftCodableTests.swift @@ -0,0 +1,132 @@ +// +// SwiftCodableTests.swift +// PMJSONTests +// +// Created by Kevin Ballard on 2/15/18. +// Copyright © 2018 Kevin Ballard. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// + +import XCTest +import PMJSON +import Foundation + +final class SwiftEncodableTests: XCTestCase { + let encoder = JSONEncoder() + + func testNull() throws { + let data = try encoder.encode(ValueWrapper(nil)) + let json = try JSON.decode(data) + XCTAssertEqual(json, ["value": nil]) + } + + func testBool() throws { + let data = try encoder.encode(ValueWrapper(true)) + let json = try JSON.decode(data) + XCTAssertEqual(json, ["value": true]) + } + + func testString() throws { + let data = try encoder.encode(ValueWrapper("foo")) + let json = try JSON.decode(data) + XCTAssertEqual(json, ["value": "foo"]) + } + + func testInt64() throws { + let data = try encoder.encode(ValueWrapper(.int64(1234))) + let json = try JSON.decode(data) + XCTAssertEqual(json, ["value": 1234]) + } + + func testDouble() throws { + let data = try encoder.encode(ValueWrapper(.double(12.5))) + let json = try JSON.decode(data) + XCTAssertEqual(json, ["value": 12.5]) + } + + func testDecimal() throws { + let data = try encoder.encode(ValueWrapper(.decimal(10))) + let json = try JSON.decode(data) + XCTAssertEqual(json, ["value": 10]) + } + + func testObject() throws { + let data = try encoder.encode(["foo": "bar"] as JSON) + let json = try JSON.decode(data) + XCTAssertEqual(json, ["foo": "bar"]) + } + + func testArray() throws { + let data = try encoder.encode(["foo", "bar"] as JSON) + let json = try JSON.decode(data) + XCTAssertEqual(json, ["foo", "bar"]) + } +} + +final class SwiftDecodableTests: XCTestCase { + let decoder = JSONDecoder() + + func testNull() throws { + let data = JSON.encodeAsData(["value": nil]) + let wrapper = try decoder.decode(ValueWrapper.self, from: data) + XCTAssertEqual(wrapper.value, nil) + } + + func testBool() throws { + let data = JSON.encodeAsData(["value": true]) + let wrapper = try decoder.decode(ValueWrapper.self, from: data) + XCTAssertEqual(wrapper.value, true) + } + + func testString() throws { + let data = JSON.encodeAsData(["value": "foo"]) + let wrapper = try decoder.decode(ValueWrapper.self, from: data) + XCTAssertEqual(wrapper.value, "foo") + } + + func testInt64() throws { + let data = JSON.encodeAsData(["value": .int64(1234)]) + let wrapper = try decoder.decode(ValueWrapper.self, from: data) + XCTAssertEqual(wrapper.value, .int64(1234)) + } + + func testDouble() throws { + let data = JSON.encodeAsData(["value": .double(12.5)]) + let wrapper = try decoder.decode(ValueWrapper.self, from: data) + XCTAssertEqual(wrapper.value, .double(12.5)) + } + + func testHugeDouble() throws { + let data = JSON.encodeAsData(["value": .double(12e50)]) + let wrapper = try decoder.decode(ValueWrapper.self, from: data) + XCTAssertEqual(wrapper.value, .double(12e50)) + } + + // Can't force JSONDecoder to decode as decimal so we won't try + + func testObject() throws { + let data = JSON.encodeAsData(["value": ["foo": "bar"]]) + let wrapper = try decoder.decode(ValueWrapper.self, from: data) + XCTAssertEqual(wrapper.value, ["foo": "bar"]) + } + + func testArray() throws { + let data = JSON.encodeAsData(["value": ["foo", "bar"]]) + let wrapper = try decoder.decode(ValueWrapper.self, from: data) + XCTAssertEqual(wrapper.value, ["foo", "bar"]) + } +} + +/// Wrapper to make JSONEncoder/JSONDecoder happy about encoding values. +private struct ValueWrapper: Codable { + let value: JSON + + init(_ value: JSON) { + self.value = value + } +} diff --git a/Tests/PMJSONTests/SwiftDecoderTests.swift b/Tests/PMJSONTests/SwiftDecoderTests.swift new file mode 100644 index 0000000..3fe22a3 --- /dev/null +++ b/Tests/PMJSONTests/SwiftDecoderTests.swift @@ -0,0 +1,141 @@ +// +// SwiftDecoderTests.swift +// PMJSONTests +// +// Created by Kevin Ballard on 2/15/18. +// Copyright © 2018 Kevin Ballard. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// + +import XCTest +import PMJSON + +private struct Person: Decodable, Equatable { + enum Color: String, Decodable { + case red + case green + case blue + } + + var name: String + var age: Int + var isAlive: Bool + var favoriteColors: [Color] + var fruitRatings: [String: Int] + var birthstone: String? + + static func ==(lhs: Person, rhs: Person) -> Bool { + return (lhs.name, lhs.age) == (rhs.name, rhs.age) + && lhs.favoriteColors == rhs.favoriteColors + && lhs.fruitRatings == rhs.fruitRatings + && lhs.birthstone == rhs.birthstone + } +} + +final class SwiftDecoderTests: XCTestCase { + func testBasicDecodeFromJSON() throws { + let json: JSON = [ + "name": "Anne", + "age": 23, + "isAlive": true, + "favoriteColors": ["red", "green", "blue"], + "fruitRatings": [ + "apple": 3, + "pear": 4, + "orange": 2 + ], + "birthstone": "Opal" + ] + + let person = try JSON.Decoder().decode(Person.self, from: json) + XCTAssertEqual(person, Person(name: "Anne", age: 23, isAlive: true, favoriteColors: [.red, .green, .blue], fruitRatings: ["apple": 3, "pear": 4, "orange": 2], birthstone: "Opal")) + } + + func testDecodeFromData() throws { + let json: JSON = [ + "name": "Anne", + "age": 23, + "isAlive": true, + "favoriteColors": ["red", "green", "blue"], + "fruitRatings": [ + "apple": 3, + "pear": 4, + "orange": 2 + ], + "birthstone": "Opal" + ] + let data = JSON.encodeAsData(json) + + let person = try JSON.Decoder().decode(Person.self, from: data) + XCTAssertEqual(person, Person(name: "Anne", age: 23, isAlive: true, favoriteColors: [.red, .green, .blue], fruitRatings: ["apple": 3, "pear": 4, "orange": 2], birthstone: "Opal")) + } + + func testDecodeFromString() throws { + let json: JSON = [ + "name": "Anne", + "age": 23, + "isAlive": true, + "favoriteColors": ["red", "green", "blue"], + "fruitRatings": [ + "apple": 3, + "pear": 4, + "orange": 2 + ], + "birthstone": "Opal" + ] + let string = JSON.encodeAsString(json) + + let person = try JSON.Decoder().decode(Person.self, from: string) + XCTAssertEqual(person, Person(name: "Anne", age: 23, isAlive: true, favoriteColors: [.red, .green, .blue], fruitRatings: ["apple": 3, "pear": 4, "orange": 2], birthstone: "Opal")) + } + + func testDecodeFromJSONWithNilOptionalValue() throws { + let json: JSON = [ + "name": "Anne", + "age": 23, + "isAlive": true, + "favoriteColors": ["red", "green", "blue"], + "fruitRatings": [ + "apple": 3, + "pear": 4, + "orange": 2 + ] + ] + + let person = try JSON.Decoder().decode(Person.self, from: json) + XCTAssertEqual(person, Person(name: "Anne", age: 23, isAlive: true, favoriteColors: [.red, .green, .blue], fruitRatings: ["apple": 3, "pear": 4, "orange": 2], birthstone: nil)) + } + + func testDecodeThrowsMissingKeyError() { + let json: JSON = [ + "name": "Anne" + ] + + XCTAssertThrowsError(try JSON.Decoder().decode(Person.self, from: json)) { (error) in + switch error { + case let DecodingError.keyNotFound(key, context): + XCTAssertEqual(key.stringValue, "age") + XCTAssertEqual(context.codingPath.map({ $0.stringValue }), []) + switch context.underlyingError { + case JSONError.missingOrInvalidType?: + break + case let error: + XCTFail("Expected underlying error to be JSONError.missingOrInvalidType, found \(error as Any)") + } + default: + XCTFail("Expected DecodingError.keyNotFound, found \(error)") + } + } + } + + func testDecodeSingleValue() throws { + let json: JSON = "red" + let color = try JSON.Decoder().decode(Person.Color.self, from: json) + XCTAssertEqual(color, .red) + } +} diff --git a/Tests/PMJSONTests/SwiftEncoderTests.swift b/Tests/PMJSONTests/SwiftEncoderTests.swift new file mode 100644 index 0000000..368b856 --- /dev/null +++ b/Tests/PMJSONTests/SwiftEncoderTests.swift @@ -0,0 +1,512 @@ +// +// SwiftEncoderTests.swift +// PMJSONTests +// +// Created by Kevin Ballard on 2/18/18. +// Copyright © 2018 Kevin Ballard. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// + +import XCTest +import PMJSON + +private struct Person: Encodable, Equatable { + enum Color: String, Encodable { + case red + case green + case blue + } + + var name: String + var age: Int + var isAlive: Bool + var favoriteColors: [Color] + var fruitRatings: [String: Int] + var birthstone: String? + + static func ==(lhs: Person, rhs: Person) -> Bool { + return (lhs.name, lhs.age) == (rhs.name, rhs.age) + && lhs.favoriteColors == rhs.favoriteColors + && lhs.fruitRatings == rhs.fruitRatings + && lhs.birthstone == rhs.birthstone + } +} + +private struct Wrapper: Encodable { + let value: Value +} + +final class SwiftEncoderTests: XCTestCase { + let encoder = JSON.Encoder() + + func testBasicEncode() throws { + let person = Person(name: "Anne", age: 24, isAlive: true, favoriteColors: [.red, .green, .blue], fruitRatings: ["apple": 3, "pear": 4, "banana": 5], birthstone: "opal") + let json = try encoder.encodeAsJSON(person) + XCTAssertEqual(json, [ + "name": "Anne", + "age": 24, + "isAlive": true, + "favoriteColors": ["red", "green", "blue"], + "fruitRatings": ["apple": 3, "pear": 4, "banana": 5], + "birthstone": "opal" + ]) + } + + func testEncodeOptional() throws { + let person = Person(name: "Anne", age: 24, isAlive: true, favoriteColors: [.red, .green, .blue], fruitRatings: ["apple": 3, "pear": 4, "banana": 5], birthstone: nil) + let json = try encoder.encodeAsJSON(person) + XCTAssertEqual(json, [ + "name": "Anne", + "age": 24, + "isAlive": true, + "favoriteColors": ["red", "green", "blue"], + "fruitRatings": ["apple": 3, "pear": 4, "banana": 5] + ]) + } + + func testEncodePrimitve() throws { + let json = try encoder.encodeAsJSON(42) + XCTAssertEqual(json, 42) + } + + func testEncodeNull() throws { + struct Object: Encodable { + enum CodingKeys: String, CodingKey { + case value + } + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeNil(forKey: .value) + } + } + + let json = try encoder.encodeAsJSON(Object()) + XCTAssertEqual(json, ["value": nil]) + } + + func testEncodeJSONValue() throws { + let wrapper = Wrapper(value: [ + "foo": "bar", + "xs": [1,2,3,4,5] + ]) + let json = try encoder.encodeAsJSON(wrapper) + XCTAssertEqual(json, ["value": wrapper.value]) + } + + func testEncodeDecimal() throws { + let wrapper = Wrapper(value: 12.34) + let json = try encoder.encodeAsJSON(wrapper) + XCTAssertEqual(json, ["value": .decimal(12.34)]) + } + + // MARK: - + + func testEncodeAsString() throws { + let wrapper = Wrapper(value: "foo") + let json = try encoder.encodeAsString(wrapper) + XCTAssertEqual(json, "{\"value\":\"foo\"}") + } + + func testEncodeAsStringWithOptions() throws { + let wrapper = Wrapper(value: "foo") + let json = try encoder.encodeAsString(wrapper, options: [.pretty]) + XCTAssertEqual(json, "{\n \"value\": \"foo\"\n}") + } + + func testEncodePrimitiveAsString() throws { + let json = try encoder.encodeAsString(42) + XCTAssertEqual(json, "42") + } + + func testEncodeAsData() throws { + let wrapper = Wrapper(value: "foo") + let json = try encoder.encodeAsData(wrapper) + XCTAssertEqual(json, "{\"value\":\"foo\"}".data(using: .utf8)!) + } + + func testEncodeAsDataWithOptions() throws { + let wrapper = Wrapper(value: "foo") + let json = try encoder.encodeAsData(wrapper, options: [.pretty]) + XCTAssertEqual(json, "{\n \"value\": \"foo\"\n}".data(using: .utf8)!) + } + + // MARK: - + + func testObjectMultipleContainersSameKeyEncoder() throws { + struct Object: Encodable { + enum CodingKeys: String, CodingKey { + case name + case age + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("Anne", forKey: .name) + try container.encode(24, forKey: .age) + try Child().encode(to: encoder) + } + + struct Child: Encodable { + enum CodingKeys: String, CodingKey { + case color + case fruit + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("red", forKey: .color) + try container.encode("banana", forKey: .fruit) + } + } + } + + let json = try encoder.encodeAsJSON(Object()) + XCTAssertEqual(json, [ + "name": "Anne", + "age": 24, + "color": "red", + "fruit": "banana" + ]) + } + + func testObjectMultipleContainersSameKeyConcurrentlyEncoder() throws { + struct Object: Encodable { + enum CodingKeys1: String, CodingKey { + case name + case age + } + enum CodingKeys2: String, CodingKey { + case color + case fruit + } + + func encode(to encoder: Encoder) throws { + var container1 = encoder.container(keyedBy: CodingKeys1.self) + try container1.encode("Anne", forKey: .name) + var container2 = encoder.container(keyedBy: CodingKeys2.self) + try container2.encode("red", forKey: .color) + try container1.encode(24, forKey: .age) + try container2.encode("banana", forKey: .fruit) + } + } + + let json = try encoder.encodeAsJSON(Object()) + XCTAssertEqual(json, [ + "name": "Anne", + "age": 24, + "color": "red", + "fruit": "banana" + ]) + } + + // MARK: - Nested encoders + + func testObjectNestedKeyedEncoder() throws { + struct Object: Encodable { + enum CodingKeys: String, CodingKey { + case name + case nested + } + enum NestedCodingKeys: String, CodingKey { + case color + case age + } + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("Anne", forKey: .name) + var nested = container.nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested) + try nested.encode("red", forKey: .color) + try nested.encode(24, forKey: .age) + } + } + let json = try encoder.encodeAsJSON(Object()) + XCTAssertEqual(json, [ + "name": "Anne", + "nested": [ + "color": "red", + "age": 24 + ] + ]) + } + + func testObjectNestedUnkeyedEncoder() throws { + struct Object: Encodable { + enum CodingKeys: String, CodingKey { + case name + case nested + } + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("Anne", forKey: .name) + var nested = container.nestedUnkeyedContainer(forKey: .nested) + try nested.encode("foo") + try nested.encode(42) + } + } + let json = try encoder.encodeAsJSON(Object()) + XCTAssertEqual(json, [ + "name": "Anne", + "nested": ["foo", 42] + ]) + } + + func testObjectNestedKeyedEncoderOverwritten() throws { + struct Object: Encodable { + enum CodingKeys: String, CodingKey { + case name + case nested + } + enum NestedCodingKeys: String, CodingKey { + case color + case age + } + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("Anne", forKey: .name) + var nested = container.nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested) + try container.encode("foo", forKey: .nested) // this overwrites the nested container + try nested.encode("red", forKey: .color) // these two lines should have no effect + try nested.encode(24, forKey: .age) + } + } + let json = try encoder.encodeAsJSON(Object()) + XCTAssertEqual(json, [ + "name": "Anne", + "nested": "foo" + ]) + } + + func testArrayNestedKeyedEncoder() throws { + struct Object: Encodable { + enum NestedCodingKeys: String, CodingKey { + case color + case age + } + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode("Anne") + var nested = container.nestedContainer(keyedBy: NestedCodingKeys.self) + try nested.encode("red", forKey: .color) + try nested.encode(24, forKey: .age) + } + } + let json = try encoder.encodeAsJSON(Object()) + XCTAssertEqual(json, ["Anne", ["color": "red", "age": 24]]) + } + + func testArrayNestedUnkeyedEncoder() throws { + struct Object: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode("Anne") + var nested = container.nestedUnkeyedContainer() + try nested.encode("red") + try nested.encode(24) + } + } + let json = try encoder.encodeAsJSON(Object()) + XCTAssertEqual(json, ["Anne", ["red", 24]]) + } + + // MARK: - Super encoders + + func testObjectSuperSingleValueEncoder() throws { + class Parent: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("foo") + } + } + class Child: Parent { + enum CodingKeys: String, CodingKey { + case name + } + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("Anne", forKey: .name) + try super.encode(to: container.superEncoder()) + } + } + let json = try encoder.encodeAsJSON(Child()) + XCTAssertEqual(json, [ + "name": "Anne", + "super": "foo", + ]) + } + + func testObjectKeyedSuperSingleValueEncoder() throws { + class Parent: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("foo") + } + } + class Child: Parent { + enum CodingKeys: String, CodingKey { + case name + case otherSuper + } + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("Anne", forKey: .name) + try super.encode(to: container.superEncoder(forKey: .otherSuper)) + } + } + let json = try encoder.encodeAsJSON(Child()) + XCTAssertEqual(json, [ + "name": "Anne", + "otherSuper": "foo" + ]) + } + + func testObjectSuperKeyedEncoder() throws { + class Parent: Encodable { + enum CodingKeys: String, CodingKey { + case name + } + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("Anne", forKey: .name) + } + } + class Child: Parent { + enum ChildCodingKeys: String, CodingKey { + case age + } + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ChildCodingKeys.self) + try container.encode(24, forKey: .age) + try super.encode(to: container.superEncoder()) + } + } + let json = try encoder.encodeAsJSON(Child()) + XCTAssertEqual(json, [ + "age": 24, + "super": [ + "name": "Anne" + ] + ]) + } + + func testObjectSuperUnkeyedEncoder() throws { + class Parent: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode("foo") + try container.encode(42) + } + } + class Child: Parent { + enum CodingKeys: String, CodingKey { + case age + } + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(24, forKey: .age) + try super.encode(to: container.superEncoder()) + } + } + let json = try encoder.encodeAsJSON(Child()) + XCTAssertEqual(json, [ + "age": 24, + "super": ["foo", 42] + ]) + } + + func testObjectDeferredSuperEncoder() throws { + class Parent: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("foo") + } + } + class Child: Parent { + enum CodingKeys: String, CodingKey { + case name + } + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let superEncoder = container.superEncoder() + try container.encode("Anne", forKey: .name) + try super.encode(to: superEncoder) + } + } + let json = try encoder.encodeAsJSON(Child()) + XCTAssertEqual(json, [ + "name": "Anne", + "super": "foo", + ]) + } + + func testObjectDeferredSuperOverwrittenEncoder() throws { + class Parent: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("foo") + } + } + class Child: Parent { + enum CodingKeys: String, CodingKey { + case name + case `super` + } + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let superEncoder = container.superEncoder() // this line reserves "super" + try container.encode("Anne", forKey: .name) + try container.encode("bar", forKey: .super) // this line overwrites "super" + try super.encode(to: superEncoder) // this line should have no effect on the result + } + } + let json = try encoder.encodeAsJSON(Child()) + XCTAssertEqual(json, [ + "name": "Anne", + "super": "bar", + ]) + } + + func testArraySuperSingleValueEncoder() throws { + class Parent: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("foo") + } + } + class Child: Parent { + override func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode("Anne") + try super.encode(to: container.superEncoder()) + } + } + let json = try encoder.encodeAsJSON(Child()) + XCTAssertEqual(json, ["Anne", "foo"]) + } + + func testArraySuperObjectEncoder() throws { + class Parent: Encodable { + enum CodingKeys: String, CodingKey { + case name + } + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("Anne", forKey: .name) + } + } + class Child: Parent { + override func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(24) + try super.encode(to: container.superEncoder()) + try container.encode(42) + } + } + let json = try encoder.encodeAsJSON(Child()) + XCTAssertEqual(json, [24, ["name": "Anne"], 42]) + } +}