Skip to content

Commit

Permalink
Fix: Ensure ContextKey values are always encoded as base64 string ind…
Browse files Browse the repository at this point in the history
…ependent 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 223a0f9.

* Ordered encoding

* Move to FineJSON for tests to preserve ordering

* Adjust pretty printing
  • Loading branch information
Supereg authored Feb 1, 2022
1 parent 7f0df17 commit 30b8886
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 19 deletions.
27 changes: 27 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 9 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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(
Expand Down
19 changes: 12 additions & 7 deletions Sources/ApodiniContext/CodableContextKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)
}
}
18 changes: 11 additions & 7 deletions Sources/ApodiniContext/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import OrderedCollections

private class ContextBox {
var entries: [ObjectIdentifier: StoredContextValue]
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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<String, String> = [:]

for storedValue in self.entries.values {
guard let contextKey = storedValue.key as? AnyCodableContextKey.Type else {
Expand All @@ -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))
}
}
}
14 changes: 12 additions & 2 deletions Tests/ApodiniContextTests/ContextKeyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import XCTest
import XCTMetadataSystem
@testable import ApodiniContext
import FineJSON

class ContextKeyTests: XCTestCase {
func testContextKeys() {
Expand Down Expand Up @@ -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"])
}
}

0 comments on commit 30b8886

Please sign in to comment.