Skip to content

Commit

Permalink
Merge pull request #61 from XYOracleNetwork/feature/meta-fields-refactor
Browse files Browse the repository at this point in the history
Meta fields refactor
  • Loading branch information
JoelBCarter authored Dec 10, 2024
2 parents 3e99082 + 22cf1bd commit fdb1450
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 82 deletions.
2 changes: 0 additions & 2 deletions Sources/XyoClient/BoundWitness/BoundWitness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ public class BoundWitnessInstance: PayloadInstance {

enum CodingKeys: String, CodingKey {
case addresses
case _hash = "$hash"
case _meta = "$meta"
case payload_hashes
case payload_schemas
case previous_hashes
Expand Down
13 changes: 10 additions & 3 deletions Sources/XyoClient/BoundWitness/Meta/BoundWitnessMeta.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ public protocol BoundWitnessMetaProtocol: Decodable {
var signatures: [String]? { get set }
}

public class BoundWitnessMeta: BoundWitnessMetaProtocol, Decodable, Encodable {
public class BoundWitnessMeta: EncodableEmptyMeta, BoundWitnessMetaProtocol, Decodable {
public var client: String?
public var signatures: [String]?

enum CodingKeys: String, CodingKey {
case client
case signatures
case client = "$client"
case signatures = "$signatures"
}

public init(_ signatures: [String] = []) {
Expand All @@ -24,4 +24,11 @@ public class BoundWitnessMeta: BoundWitnessMetaProtocol, Decodable, Encodable {
client = try values.decode(String.self, forKey: .client)
signatures = try values.decode([String].self, forKey: .signatures)
}

override public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(self.client, forKey: .client)
try container.encode(self.signatures, forKey: .signatures)
}
}
3 changes: 2 additions & 1 deletion Sources/XyoClient/Module/ModuleQueryResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ public class ModuleQueryResult: Codable {
public let payloads: [EncodablePayloadInstance]
public let errors: [EncodablePayloadInstance]
init(
bw: EncodableBoundWitnessWithMeta, payloads: [EncodablePayloadInstance] = [],
bw: EncodableBoundWitnessWithMeta,
payloads: [EncodablePayloadInstance] = [],
errors: [EncodablePayloadInstance] = []
) {
self.bw = bw
Expand Down
64 changes: 42 additions & 22 deletions Sources/XyoClient/Payload/PayloadBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@ public enum PayloadBuilderError: Error {
case encodingError
}

/// Merges multiple `Encodable` objects into a single JSON object.
///
/// This method accepts a variable number of `Encodable` objects, encodes each into JSON, converts them into dictionaries, and merges the dictionaries into one. In case of key conflicts, the value from the latter object will overwrite the earlier value. The final merged dictionary is then serialized back into JSON data.
///
/// - Parameters:
/// - encodables: A variadic list of objects conforming to the `Encodable` protocol.
///
/// - Returns: A `Data` object representing the merged JSON of all provided `Encodable` objects.
///
/// - Throws:
/// - An error if any of the `Encodable` objects fail to encode.
/// - An error if the JSON data cannot be converted to a dictionary or serialized.
func mergeToJsonObject(_ encodables: (any Encodable)...) throws -> Data {
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys

// Initialize an empty dictionary to store merged data
var mergedDict: [String: Any] = [:]

for encodable in encodables {
// Encode the current object into JSON data
let data = try encoder.encode(encodable)

// Decode the JSON into a dictionary
if let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
// Merge the current dictionary with the existing merged dictionary
mergedDict.merge(dict) { (_, new) in new }
}
}

// Serialize the merged dictionary back into JSON
return try JSONSerialization.data(withJSONObject: mergedDict, options: .sortedKeys)
}

public protocol EncodableWithMeta: Encodable {
var payload: EncodablePayload { get }
var meta: Encodable? { get }
Expand Down Expand Up @@ -34,12 +68,6 @@ public struct AnyEncodableWithMeta<T: EncodableWithMeta>: EncodableWithMeta {
}
}

public struct EncodableInstance: Encodable {
public func encode(to encoder: Encoder) throws {
fatalError("This method must be overridden in a subclass")
}
}

public class EncodableEmptyMeta: Encodable {}

public class EmptyMeta: Codable {}
Expand Down Expand Up @@ -73,11 +101,6 @@ public class EncodableWithCustomMetaInstance<T: EncodablePayload, M: Encodable>:
return self._meta
}

enum CodingKeys: String, CodingKey {
case _hash = "$hash"
case _meta = "$meta"
}

public var schema: String {
return _payload.schema
}
Expand All @@ -88,11 +111,8 @@ public class EncodableWithCustomMetaInstance<T: EncodablePayload, M: Encodable>:
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let hash = try PayloadBuilder.dataHash(from: _payload).toHex()
try container.encode(hash, forKey: ._hash)
if _meta != nil {
try container.encode(_meta, forKey: ._meta)
if let meta = _meta {
try meta.encode(to: encoder)
}
try self._payload.encode(to: encoder)
}
Expand Down Expand Up @@ -187,7 +207,7 @@ public class PayloadBuilder {
return try jsonString.sha256()
}

static public func hash<T: EncodablePayloadInstance, M: EncodableEmptyMeta>(from: T, meta: M)
static public func hash<T: EncodablePayloadInstance, M: EncodableEmptyMeta>(from: T, meta: M?)
throws -> Hash
{
let withMeta = EncodableWithCustomMetaInstance(from: from, meta: meta)
Expand All @@ -211,13 +231,13 @@ public class PayloadBuilder {
return result
}

static public func toJsonWithMeta<T: EncodablePayloadInstance, M: Encodable>(from: T, meta: M?)
static public func toJsonWithMeta<T: EncodablePayloadInstance>(from: T, meta: (any Encodable)?)
throws -> String
{
let target = EncodableWithCustomMetaInstance(from: from, meta: meta)
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
let data = try encoder.encode(target)
guard let m = meta else {
return try PayloadBuilder.toJson(from: from)
}
let data = try mergeToJsonObject(from, m)
guard let result = String(data: data, encoding: .utf8) else {
throw PayloadBuilderError.encodingError
}
Expand Down
31 changes: 21 additions & 10 deletions Tests/XyoClientTests/BoundWitness/BoundWitnessBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,29 +46,40 @@ final class BoundWitnessBuilderTests: XCTestCase {
}

// Build the BW
let bw = try BoundWitnessBuilder().signers(signers).payloads(testCase.payloads)
let (bwJson, _) = try bw.build()
let hash = try PayloadBuilder.dataHash(from: bwJson.typedPayload)
let rootHash = try PayloadBuilder.hash(from: bwJson.typedPayload)
let builder = try BoundWitnessBuilder().signers(signers).payloads(testCase.payloads)
let (bw, payloads) = try builder.build()
let dataHash = try PayloadBuilder.dataHash(from: bw.typedPayload)
// print(
// try PayloadBuilder.toJsonWithMeta(
// from: bw.typedPayload, meta: bw.meta))
let rootHash = try PayloadBuilder.hash(from: bw.typedPayload, meta: bw.typedMeta)

// Ensure the BW is correct
XCTAssertEqual(hash.toHex(), testCase.dataHash, "Incorrect data hash in BW")
XCTAssertEqual(dataHash.toHex(), testCase.dataHash, "Incorrect data hash in BW")
XCTAssertEqual(rootHash.toHex(), testCase.rootHash, "Incorrect root hash in BW")
for (i, expectedPayloadHash) in testCase.payloadHashes.enumerated() {
let actualPayloadHash = bwJson.typedPayload.payload_hashes[i]
// Ensure payload hash is correct
let actualPayloadHash = bw.typedPayload.payload_hashes[i]
// Ensure payload hash is correct from test data
XCTAssertEqual(
expectedPayloadHash, actualPayloadHash,
"Incorrect payload hash in BW as compared to test data")
// Ensure payload hash is correct as calculated from returned payloads
let dataHash = try PayloadBuilder.dataHash(from: payloads[i])
XCTAssertEqual(
expectedPayloadHash, actualPayloadHash, "Incorrect payload hash in BW")
expectedPayloadHash, dataHash.toHex(),
"Incorrect payload hash in BW as compared to BoundWitnessBuilder returned payloads data hash"
)
}
for (i, payload) in testCase.payloads.enumerated() {
let actualSchema = bwJson.typedPayload.payload_schemas[i]
let actualSchema = bw.typedPayload.payload_schemas[i]
// Ensure payload hash is correct
XCTAssertEqual(payload.schema, actualSchema, "Incorrect payload schema in BW")
}

// Ensure correct ending account state
for signer in signers {
// Ensure previous hash is correct
XCTAssertEqual(signer.previousHash, hash, "Incorrect previous hash for account")
XCTAssertEqual(signer.previousHash, dataHash, "Incorrect previous hash for account")
}
}
}
Expand Down
13 changes: 9 additions & 4 deletions Tests/XyoClientTests/TestData/TestBoundWitnessSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public struct BoundWitnessSequenceTestCase {
public var payloadHashes: [String]
public var previousHashes: [String?]
public var dataHash: String
public var rootHash: String
}

public struct PayloadsWithHashes {
Expand Down Expand Up @@ -61,7 +62,8 @@ let boundWitnessSequenceTestCase1: BoundWitnessSequenceTestCase = .init(
payloads: payloadSequences[0].payloads,
payloadHashes: payloadSequences[0].payloadHashes,
previousHashes: [nil],
dataHash: "750113b9826ba94b622667b06cd8467f1330837581c28907c16160fec20d0a4b"
dataHash: "750113b9826ba94b622667b06cd8467f1330837581c28907c16160fec20d0a4b",
rootHash: "d8c29f77505e5da7479de1aa6474b247b348004a90bf7048e60581592deac1e7"
)

let boundWitnessSequenceTestCase2: BoundWitnessSequenceTestCase = .init(
Expand All @@ -71,7 +73,8 @@ let boundWitnessSequenceTestCase2: BoundWitnessSequenceTestCase = .init(
payloads: payloadSequences[1].payloads,
payloadHashes: payloadSequences[1].payloadHashes,
previousHashes: [nil],
dataHash: "bacd010d79126a154339e59c11c5b46be032c3bef65626f83bcafe968dc6dd1b"
dataHash: "bacd010d79126a154339e59c11c5b46be032c3bef65626f83bcafe968dc6dd1b",
rootHash: "ea1d3dd28daea3df2c7d50ffcecec3be95c8011636a6590598a4aab0ce2b6971"
)

let boundWitnessSequenceTestCase3: BoundWitnessSequenceTestCase = .init(
Expand All @@ -84,7 +87,8 @@ let boundWitnessSequenceTestCase3: BoundWitnessSequenceTestCase = .init(
"750113b9826ba94b622667b06cd8467f1330837581c28907c16160fec20d0a4b",
"bacd010d79126a154339e59c11c5b46be032c3bef65626f83bcafe968dc6dd1b",
],
dataHash: "73245ef73517913f4b57c12d56d81199968ecd8fbefea9ddc474f43dd6cfa8c8"
dataHash: "73245ef73517913f4b57c12d56d81199968ecd8fbefea9ddc474f43dd6cfa8c8",
rootHash: "02caf1f81905ec9311b3b4793309f462567b35516d7dee7ce62d1e4759b7022a"
)

let boundWitnessSequenceTestCase4: BoundWitnessSequenceTestCase = .init(
Expand All @@ -97,7 +101,8 @@ let boundWitnessSequenceTestCase4: BoundWitnessSequenceTestCase = .init(
"73245ef73517913f4b57c12d56d81199968ecd8fbefea9ddc474f43dd6cfa8c8",
"73245ef73517913f4b57c12d56d81199968ecd8fbefea9ddc474f43dd6cfa8c8",
],
dataHash: "210d86ea43d82b85a49b77959a8ee4e6016ff7036254cfa39953befc66073010"
dataHash: "210d86ea43d82b85a49b77959a8ee4e6016ff7036254cfa39953befc66073010",
rootHash: "a99467084abb2d7812f4d529a2e84d566716aca9443c4b4800e016572cf91416"
)

let boundWitnessSequenceTestCases = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,9 @@ class LocationPayloadTests: XCTestCase {
let payload = LocationPayload(location)

// Act: Encode the LocationPayload instance into JSON
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .prettyPrinted] // Consistent output for tests
let jsonData = try encoder.encode(payload)
let jsonString = try PayloadBuilder.toJson(from: payload)

// Assert: Verify the serialized JSON matches expectations
let jsonString = String(data: jsonData, encoding: .utf8)!
let jsonString2 = try payload.toJson()
print("\n jsonString")
print(jsonString)
print("\n jsonString2")
print(jsonString2)
print("\n")
let expectedJSON = """
{
"currentLocation" : {
Expand All @@ -61,35 +52,14 @@ class LocationPayloadTests: XCTestCase {
},
"schema" : "network.xyo.location.current"
}
"""
""".filter { !$0.isWhitespace }
XCTAssertEqual(jsonString, expectedJSON)
let dataHash = try PayloadBuilder.dataHash(from: payload)
print("\n dataHash")
print(dataHash.toHex())
print("\n")
XCTAssertEqual(
dataHash, Data("0c1f0c80481b0f391a677eab542a594a192081325b6416acc3dc99db23355ee2"))

let payloadWithMeta = EncodableWithMetaInstance(from: payload)

let encoderMeta = JSONEncoder()
encoderMeta.outputFormatting = [.sortedKeys, .prettyPrinted] // Consistent output for tests
let jsonWithMetaData = try encoder.encode(payloadWithMeta)

let jsonWithMetaString = String(data: jsonWithMetaData, encoding: .utf8)!
let jsonWithMetaString2 = try payloadWithMeta.toJson()
print("\n jsonWithMetaString")
print(jsonWithMetaString)
print("\n jsonWithMetaString2")
print(jsonWithMetaString2)
print("\n")

let hash = try PayloadBuilder.hash(fromWithMeta: payloadWithMeta)
print("\n hash")
print(hash.toHex())
print("\n")
let hash = try PayloadBuilder.hash(fromWithMeta: EncodableWithMetaInstance(from: payload))
XCTAssertEqual(
hash, Data("5a4bb96eb1af7840321cb8a3503ab944957c06111869cc0746e985f49061e746"))
hash, Data("0c1f0c80481b0f391a677eab542a594a192081325b6416acc3dc99db23355ee2"))
}

func testLocationPayloadEncodingHandlesNilValues() throws {
Expand All @@ -106,12 +76,9 @@ class LocationPayloadTests: XCTestCase {
let payload = LocationPayload(location)

// Act: Encode the LocationPayload instance into JSON
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
let jsonData = try encoder.encode(payload)
let jsonString = try PayloadBuilder.toJson(from: payload)

// Assert: Verify the serialized JSON handles NaN values gracefully (e.g., omitted or replaced)
let jsonString = String(data: jsonData, encoding: .utf8)!
let expectedJSON = """
{
"currentLocation" : {
Expand All @@ -123,9 +90,12 @@ class LocationPayloadTests: XCTestCase {
},
"schema" : "network.xyo.location.current"
}
"""
""".filter { !$0.isWhitespace }
XCTAssertEqual(jsonString, expectedJSON)
let hash = try PayloadBuilder.dataHash(from: payload)
let dataHash = try PayloadBuilder.dataHash(from: payload)
XCTAssertEqual(
dataHash, Data("c1bd7396f998a50d20401efd4b5da0cf6670f9418c6f60b42f4c54f3663305c3"))
let hash = try PayloadBuilder.hash(fromWithMeta: EncodableWithMetaInstance(from: payload))
XCTAssertEqual(
hash, Data("c1bd7396f998a50d20401efd4b5da0cf6670f9418c6f60b42f4c54f3663305c3"))
}
Expand Down

0 comments on commit fdb1450

Please sign in to comment.