SwiftyLab / MetaCodable

Supercharge Swift's Codable implementations with macros meta-programming.
https://swiftpackageindex.com/SwiftyLab/MetaCodable/main/documentation/metacodable
MIT License
604 stars 22 forks source link

`MetaProtocolCodable` cannot handle Optional value correctly #76

Closed mo5tone closed 5 months ago

mo5tone commented 5 months ago

Describe the bug I want to handle below two JSONs with DynamicCodable.

JSON to handle ```json { "data": { "id": "some UUID", "type": "Foo.Bar", "attributes": { "id": "another UUID", "expiresIn": 3600, "xxx-token": "xxxxxx", "yyy-token": "yyyyyyyyy" } } } ``` and ```json { "data": { "id": "some UUID", "type": "Foo.Bar", "attributes": { "id": "another UUID", "mac": "message authentication code", "challenge": "some challenge", "operation": "REGISTRATION", "status-code": "200" } } } ```

However the code which followed Tests/MetaCodableTests/DynamicCodable/PostPage.swift generate incorrect HelperCoder

My code ```swift @Codable @CodedAs @CodedAt("data", "attributes", "operation") protocol ResponseAttributes { var id: String { get } var operation: String? { get } } @Codable struct Response { @CodedIn("data") let id: String @CodedIn("data") let type: String @CodedIn("data") @CodedBy(ResponseAttributesCoder()) let attributes: ResponseAttributes } @Codable struct RegistrationAttributes: ResponseAttributes, DynamicCodable { static var identifier: DynamicCodableIdentifier { .one("REGISTRATION") } @CodedIn("data", "attributes") let id: String @CodedAt("data", "attributes", "status-code") let statusCode: String @CodedIn("data", "attributes") let operation: String? } @Codable struct VerificationAttributes: ResponseAttributes, DynamicCodable { static var identifier: DynamicCodableIdentifier { .one(nil) } @CodedIn("data", "attributes") let id: String @CodedIn("data", "attributes") let operation: String? @CodedIn("data", "attributes") let expiresIn: UInt @CodedAt("data", "attributes", "xxx-token") let xxxToken: String @CodedAt("data", "attributes", "yyy-token") let yyyToken: String } ```
Generated `HelperCoder` ```swift import MetaCodable struct ResponseAttributesCoder: HelperCoder { func decode(from decoder: any Decoder) throws -> ResponseAttributes { let container = try decoder.container(keyedBy: CodingKeys.self) if (try? container.decodeNil(forKey: CodingKeys.data)) == false { let data_container = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.data) if (try? data_container.decodeNil(forKey: CodingKeys.attributes)) == false { let attributes_data_container = try data_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.attributes) let type = try attributes_data_container.decodeIfPresent(String.self, forKey: CodingKeys.type) // ❌ } else { let type = nil // ❌ } } else { let type = nil // ❌ } switch type { // ❌ case RegistrationAttributes.identifier: let _0 = try RegistrationAttributes(from: decoder) return _0 case VerificationAttributes.identifier: let _0 = try VerificationAttributes(from: decoder) return _0 default: let context = DecodingError.Context( codingPath: decoder.codingPath, debugDescription: "Couldn't match any cases." ) throw DecodingError.typeMismatch(ResponseAttributesCoder.self, context) } } func encode(_ value: ResponseAttributes, to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) var data_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.data) var attributes_data_container = data_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.attributes) var typeContainer = attributes_data_container switch value { case let _0 as RegistrationAttributes: try typeContainer.encodeIfPresent(RegistrationAttributes.identifier, forKey: CodingKeys.type) try _0.encode(to: encoder) case let _0 as VerificationAttributes: try typeContainer.encodeIfPresent(VerificationAttributes.identifier, forKey: CodingKeys.type) try _0.encode(to: encoder) default: break } } enum CodingKeys: String, CodingKey { case type = "operation" case data = "data" case attributes = "attributes" } } ```

To Reproduce Steps to reproduce the behavior:

  1. Create an empty swift package
  2. Add MetaCodable to dependencies
  3. Copy and paste the code above
  4. commnad + B

Expected behavior A clear and concise description of what you expected to happen. MetaProtocolCodable plugin should handle optional type correctly.

Environment (please complete the following information, remove ones not applicable):

soumyamahunt commented 5 months ago

@mo5tone while there might be bug regarding optional types support in CodedAs macro, the code snippet you have provided for decoding the sample JSON mentioned seems to be wrong. Especially the @CodedAt("data", "attributes", "operation") attached to protocol ResponseAttributes, in your Response you have already provided path to the ResponseAttributes container as data.attributes, specifying the path data.attributes again for the protocol type identifier makes the path of the identifier key data.attributes.data.attributes.operation which is wrong according to your JSON.

mo5tone commented 5 months ago

@soumyamahunt thanks for your correction.

My code with fix ```swift @Codable @CodedAs @CodedAt("operation") protocol ResponseAttributes {} @Codable struct Response { @CodedIn("data") let id: String @CodedIn("data") let type: String @CodedIn("data") @CodedBy(ResponseAttributesCoder()) let attributes: ResponseAttributes } @Codable struct RegistrationAttributes: ResponseAttributes, DynamicCodable { static var identifier: DynamicCodableIdentifier { .one("REGISTRATION") } let id: String @CodedAt("status-code") let statusCode: String let operation: String } @Codable struct VerificationAttributes: ResponseAttributes, DynamicCodable { static var identifier: DynamicCodableIdentifier { .one(nil) } let id: String let operation: String? let expiresIn: UInt @CodedAt("xxx-token") let xxxToken: String @CodedAt("yyy-token") let yyyToken: String } ```

Now the generated ResponseAttributesCoder works like a charm. Will close this issue.

Thanks again for your great library.