From 30b8886934e49d64130868ac0b89219c35fc5447 Mon Sep 17 00:00:00 2001 From: Andi Date: Tue, 1 Feb 2022 23:28:10 +0100 Subject: [PATCH] Fix: Ensure ContextKey values are always encoded as base64 string independent of the external encoder configuration (#4) * Fix: Ensure ContextKey values are always encoded as base64 string independent of the external encoder configuration * Only test one ContextKey to avoid ordering issues in textual comparison * Revert "Only test one ContextKey to avoid ordering issues in textual comparison" This reverts commit 223a0f961e275b04ebbea67f5f73362f0a8ab471. * Ordered encoding * Move to FineJSON for tests to preserve ordering * Adjust pretty printing --- Package.resolved | 27 +++++++++++++++++++ Package.swift | 12 ++++++--- .../ApodiniContext/CodableContextKey.swift | 19 ++++++++----- Sources/ApodiniContext/Context.swift | 18 ++++++++----- .../ApodiniContextTests/ContextKeyTests.swift | 14 ++++++++-- 5 files changed, 71 insertions(+), 19 deletions(-) diff --git a/Package.resolved b/Package.resolved index 9dc2ba2..26693b3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,6 +10,33 @@ "version": "0.3.2" } }, + { + "package": "FineJSON", + "repositoryURL": "https://github.com/omochi/FineJSON.git", + "state": { + "branch": null, + "revision": "05101709243cb66d80c92e645210a3b80cf4e17f", + "version": "1.14.0" + } + }, + { + "package": "RichJSONParser", + "repositoryURL": "https://github.com/omochi/RichJSONParser.git", + "state": { + "branch": null, + "revision": "263e2ecfe88d0500fa99e4cbc8c948529d335534", + "version": "3.0.0" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", + "version": "1.0.2" + } + }, { "package": "XCTAssertCrash", "repositoryURL": "https://github.com/norio-nomura/XCTAssertCrash.git", diff --git a/Package.swift b/Package.swift index c72e3bd..d97e33e 100644 --- a/Package.swift +++ b/Package.swift @@ -22,11 +22,16 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/nerdsupremacist/AssociatedTypeRequirementsKit.git", .upToNextMinor(from: "0.3.2")), - .package(url: "https://github.com/norio-nomura/XCTAssertCrash.git", from: "0.2.0") + .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/norio-nomura/XCTAssertCrash.git", from: "0.2.0"), + .package(url: "https://github.com/omochi/FineJSON.git", from: "1.14.0") ], targets: [ .target( - name: "ApodiniContext" + name: "ApodiniContext", + dependencies: [ + .product(name: "OrderedCollections", package: "swift-collections") + ] ), .target( name: "MetadataSystem", @@ -51,7 +56,8 @@ let package = Package( name: "ApodiniContextTests", dependencies: [ .target(name: "XCTMetadataSystem"), - .target(name: "ApodiniContext") + .target(name: "ApodiniContext"), + .product(name: "FineJSON", package: "FineJSON") ] ), .testTarget( diff --git a/Sources/ApodiniContext/CodableContextKey.swift b/Sources/ApodiniContext/CodableContextKey.swift index d8b398f..d8b0883 100644 --- a/Sources/ApodiniContext/CodableContextKey.swift +++ b/Sources/ApodiniContext/CodableContextKey.swift @@ -12,15 +12,14 @@ import Foundation public protocol AnyCodableContextKey: AnyContextKey { static var identifier: String { get } - static func anyEncode(value: Any) throws -> Data + static func anyEncode(value: Any) throws -> String } /// An ``OptionalContextKey`` which value is able to be encoded and decoded. public protocol CodableContextKey: AnyCodableContextKey, OptionalContextKey where Value: Codable { - static func decode(from data: Data) throws -> Value + static func decode(from base64String: String) throws -> Value } -// Data, by default, is encoded as base64 private let encoder = JSONEncoder() private let decoder = JSONDecoder() @@ -29,15 +28,21 @@ extension CodableContextKey { "\(Self.self)" } - public static func anyEncode(value: Any) throws -> Data { + public static func anyEncode(value: Any) throws -> String { guard let value = value as? Self.Value else { fatalError("CodableContextKey.anyEncode(value:) received illegal value type \(type(of: value)) instead of \(Value.self)") } - return try encoder.encode(value) + return try encoder + .encode(value) + .base64EncodedString() } - public static func decode(from data: Data) throws -> Value { - try decoder.decode(Value.self, from: data) + public static func decode(from base64String: String) throws -> Value { + guard let data = Data(base64Encoded: base64String) else { + fatalError("Failed to unwrap bas64 encoded data string: \(base64String)") + } + + return try decoder.decode(Value.self, from: data) } } diff --git a/Sources/ApodiniContext/Context.swift b/Sources/ApodiniContext/Context.swift index a212a7d..3507e66 100644 --- a/Sources/ApodiniContext/Context.swift +++ b/Sources/ApodiniContext/Context.swift @@ -7,6 +7,7 @@ // import Foundation +import OrderedCollections private class ContextBox { var entries: [ObjectIdentifier: StoredContextValue] @@ -28,9 +29,10 @@ public struct Context: ContextKeyRetrievable { private var entries: [ObjectIdentifier: StoredContextValue] { boxedEntries.entries } - private let decodedEntries: [String: Data] + /// Mapping from ``CodableContextKey/identifier`` to base64 encoded data + private let decodedEntries: [String: String] - init(_ entries: [ObjectIdentifier: StoredContextValue] = [:], _ decodedEntries: [String: Data] = [:]) { + init(_ entries: [ObjectIdentifier: StoredContextValue] = [:], _ decodedEntries: [String: String] = [:]) { self.boxedEntries = ContextBox(entries) self.decodedEntries = decodedEntries } @@ -138,10 +140,10 @@ extension Context: Codable { self.boxedEntries = ContextBox([:]) - var decodedEntries: [String: Data] = [:] + var decodedEntries: [String: String] = [:] for key in container.allKeys { - decodedEntries[key.stringValue] = try container.decode(Data.self, forKey: key) + decodedEntries[key.stringValue] = try container.decode(String.self, forKey: key) } self.decodedEntries = decodedEntries @@ -150,7 +152,7 @@ extension Context: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringContextKey.self) - var entries: [String: Data] = [:] + var entries: OrderedDictionary = [:] for storedValue in self.entries.values { guard let contextKey = storedValue.key as? AnyCodableContextKey.Type else { @@ -164,8 +166,10 @@ extension Context: Codable { fatalError("Encountered context value conflicts of \(current) and \(new)!") } - for (key, data) in entries { - try container.encode(data, forKey: StringContextKey(stringValue: key)) + entries.sort() + + for (key, base64String) in entries { + try container.encode(base64String, forKey: StringContextKey(stringValue: key)) } } } diff --git a/Tests/ApodiniContextTests/ContextKeyTests.swift b/Tests/ApodiniContextTests/ContextKeyTests.swift index f169ce0..24e9a3c 100644 --- a/Tests/ApodiniContextTests/ContextKeyTests.swift +++ b/Tests/ApodiniContextTests/ContextKeyTests.swift @@ -9,6 +9,7 @@ import XCTest import XCTMetadataSystem @testable import ApodiniContext +import FineJSON class ContextKeyTests: XCTestCase { func testContextKeys() { @@ -88,20 +89,29 @@ class ContextKeyTests: XCTestCase { typealias Value = String } + struct CodableArrayStringContextKey: CodableContextKey { + typealias Value = [String] + } + let context = Context() context.unsafeAdd(CodableStringContextKey.self, value: "Hello World") XCTAssertRuntimeFailure(context.unsafeAdd(CodableStringContextKey.self, value: "Hello Mars")) + context.unsafeAdd(CodableArrayStringContextKey.self, value: ["Hello Sun"]) XCTAssertEqual(context.get(valueFor: CodableStringContextKey.self), "Hello World") XCTAssertEqual(context.get(valueFor: RequiredCodableStringContextKey.self), "Default Value!") + XCTAssertEqual(context.get(valueFor: CodableArrayStringContextKey.self), ["Hello Sun"]) - let encoder = JSONEncoder() - let decoder = JSONDecoder() + let encoder = FineJSONEncoder() + encoder.jsonSerializeOptions = .init(isPrettyPrint: false) + let decoder = FineJSONDecoder() let encodedContext = try encoder.encode(context) + XCTAssertEqual(String(data: encodedContext, encoding: .utf8), "{\"CodableArrayStringContextKey\":\"WyJIZWxsbyBTdW4iXQ==\",\"CodableStringContextKey\":\"IkhlbGxvIFdvcmxkIg==\"}") let decodedContext = try decoder.decode(Context.self, from: encodedContext) XCTAssertEqual(decodedContext.get(valueFor: CodableStringContextKey.self), "Hello World") XCTAssertEqual(decodedContext.get(valueFor: RequiredCodableStringContextKey.self), "Default Value!") + XCTAssertEqual(decodedContext.get(valueFor: CodableArrayStringContextKey.self), ["Hello Sun"]) } }