From c7b97567a9fb3db03b9af08b68a2fa37bd037727 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 20 Jun 2019 22:35:55 -0700 Subject: [PATCH] update swift tools version in package file, add some property wrappers, add some tests for wrappers. its all broken but worth holding onto for now. --- Package.swift | 2 +- Sources/JSONAPI/Resource/Attribute.swift | 4 +- .../JSONAPI/Resource/PropertyWrappers.swift | 115 ++++++++++++++++++ .../Attribute/AttributeTests.swift | 91 +++++++++++++- Tests/JSONAPITests/Entity/EntityTests.swift | 2 +- 5 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 Sources/JSONAPI/Resource/PropertyWrappers.swift diff --git a/Package.swift b/Package.swift index 0789b25..675e093 100644 --- a/Package.swift +++ b/Package.swift @@ -34,5 +34,5 @@ let package = Package( name: "JSONAPITestingTests", dependencies: ["JSONAPI", "JSONAPITesting"]) ], - swiftLanguageVersions: [.v5] + swiftLanguageVersions: [.version("5.1")] ) diff --git a/Sources/JSONAPI/Resource/Attribute.swift b/Sources/JSONAPI/Resource/Attribute.swift index a43c368..c05983d 100644 --- a/Sources/JSONAPI/Resource/Attribute.swift +++ b/Sources/JSONAPI/Resource/Attribute.swift @@ -17,9 +17,9 @@ public protocol AttributeType: Codable { /// A TransformedAttribute takes a Codable type and attempts to turn it into another type. public struct TransformedAttribute: AttributeType where Transformer.From == RawValue { public let rawValue: RawValue - + public let value: Transformer.To - + public init(rawValue: RawValue) throws { self.rawValue = rawValue value = try Transformer.transform(rawValue) diff --git a/Sources/JSONAPI/Resource/PropertyWrappers.swift b/Sources/JSONAPI/Resource/PropertyWrappers.swift new file mode 100644 index 0000000..6c50f18 --- /dev/null +++ b/Sources/JSONAPI/Resource/PropertyWrappers.swift @@ -0,0 +1,115 @@ +// +// PropertyWrappers.swift +// +// +// Created by Mathew Polzin on 6/20/19. +// + + +// MARK: - Transformed +@propertyWrapper +public struct Transformed { + + public typealias RawValue = Transformer.From + public typealias Value = Transformer.To + + private var _value: Value? + + public var wrappedValue: Value { + get { + guard let ret = _value else { + fatalError("Attribute read from before initialization.") + } + return ret + } + set { + _value = newValue + } + } + + public init(initialValue: Value, _ transformer: Transformer.Type) { + self._value = initialValue + } + + public init(_ transformer: Transformer.Type) { + self._value = nil + } + + public init(rawValue: RawValue, _ transformer: Transformer.Type) throws { + self._value = try Transformer.transform(rawValue) + } +} + +extension Transformed: Decodable where Transformer.From: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + let rawVal = try container.decode(Transformer.From.self) + + _value = try Transformer.transform(rawVal) + } +} + +extension Transformed: Encodable where Transformer: ReversibleTransformer, Transformer.From: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + guard let value = _value else { + fatalError("Attribute encoded before initialization.") + } + + try container.encode(Transformer.reverse(value)) + } +} + +// MARK: - Nullable + +public protocol _Optional { + static var nilValue: Self { get } + var isNilValue: Bool { get } +} + +extension Optional: _Optional { + public static var nilValue: Self { + return .none + } + + public var isNilValue: Bool { return self == nil } +} + +protocol _Nullable {} + +@propertyWrapper +public struct Nullable: Decodable, _Optional, _Nullable { + public var wrappedValue: T? + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if container.decodeNil() { + wrappedValue = nil + return + } + + wrappedValue = try container.decode(T.self) + } + + public init(initialValue: T? = nil) { + wrappedValue = initialValue + } + + public static var nilValue: Self { + return .init() + } + + public var isNilValue: Bool { + return wrappedValue == nil + } +} + +extension Nullable: Encodable where T: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(wrappedValue) + } +} diff --git a/Tests/JSONAPITests/Attribute/AttributeTests.swift b/Tests/JSONAPITests/Attribute/AttributeTests.swift index 22acd71..a516f36 100644 --- a/Tests/JSONAPITests/Attribute/AttributeTests.swift +++ b/Tests/JSONAPITests/Attribute/AttributeTests.swift @@ -62,6 +62,70 @@ class AttributeTests: XCTestCase { } } +// MARK: Property Wrappers +extension AttributeTests { + func test_Transformed() { + + struct Test: Codable { + @Transformed(IntToString.self) + var value: String = "" + } + + let test = Test(value: "hello") + XCTAssertEqual(test.value, "hello") + + let test2 = try! JSONDecoder().decode(Test.self, + from: #"{"value": 12}"#.data(using: .utf8)!) + + XCTAssertEqual(test2.value, "12") + try! print(String(data: JSONEncoder().encode(test2), encoding: .utf8)!) + + let test3 = try? JSONDecoder().decode(Test.self, + from: #"{"value": null}"#.data(using: .utf8)!) + + XCTAssertNil(test3) + } + + func test_Nullable() { + struct Test: Codable { + @Nullable + var value: String? + } + + let test = Test(value: nil) + XCTAssertNil(test.value) + + let test2 = Test(value: "hello") + XCTAssertEqual(test2.value, "hello") + + let test3 = try! JSONDecoder().decode(Test.self, + from: #"{"value": "world"}"#.data(using: .utf8)!) + + XCTAssertEqual(test3.value, "world") + try! print(String(data: JSONEncoder().encode(test2), encoding: .utf8)!) + + let test4 = try? JSONDecoder().decode(Test.self, + from: #"{"value": null}"#.data(using: .utf8)!) + + XCTAssertNotNil(test4) + XCTAssertNil(test4?.value) + } + + func test_NullableTransformed() { + struct Test: Codable { +// Nullable> + let x: Transformed +// @Nullable @Transformed(IdentityTransformer.self) + @Transformed(IntToString.self) @Nullable + var value: String? + } + + let test = Test(x: .init(initialValue: "12", IntToString.self)) + + print(test.x.wrappedValue) + } +} + // MARK: Test types extension AttributeTests { enum TestTransformer: ReversibleTransformer { @@ -77,12 +141,37 @@ extension AttributeTests { } } - enum IntToString: Transformer { + enum IntToString: ReversibleTransformer { public static func transform(_ from: Int) -> String { return String(from) } + + public static func reverse(_ value: String) throws -> Int { + guard let intValue = Int(value) else { + fatalError("Reversed IntToString with invalid String value.") + } + return intValue + } } + enum OptionalIntToOptionalString: ReversibleTransformer { + public static func transform(_ from: Int?) -> String? { + return from.map(String.init) + } + + public static func reverse(_ value: String?) throws -> Int? { + guard let stringValue = value else { + return nil + } + + guard let intValue = Int(stringValue) else { + fatalError("Reversed IntToString with invalid String value.") + } + + return intValue + } + } + enum IntToInt: Transformer { public static func transform(_ from: Int) -> Int { return from + 100 diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift index 18e313e..0110865 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -29,7 +29,7 @@ class EntityTests: XCTestCase { let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity = TestEntity9(attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalNullableOne: nil, optionalMany: .init(resourceObjects: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none) - XCTAssertEqual(entity ~> \.optionalOne, entity1.id) + XCTAssertEqual(entity ~> \.optionalOne, Optional(entity1.id)) } func test_toMany_relationship_operator_access() {