Giter Club home page Giter Club logo

swiftylab / metacodable Goto Github PK

View Code? Open in Web Editor NEW
558.0 6.0 17.0 5.62 MB

Supercharge Swift's Codable implementations with macros meta-programming.

Home Page: https://swiftpackageindex.com/SwiftyLab/MetaCodable/main/documentation/metacodable

License: MIT License

Swift 99.56% Ruby 0.41% Shell 0.03%
codable code-generation macro metaprogramming no-boilerplate swift swift-codable swift-macros swift-package-manager swift-package-manager-plugin

metacodable's People

Contributors

dependabot[bot] avatar hstdt avatar jonasbn avatar midbin avatar soumyamahunt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

metacodable's Issues

Support for different coding keys for encoding / decoding the same property

Is there a way to decode a model's property key with a name and encode it using a different name ?
for ex. if have the following model:

@Codable
struct UserInfo {
 
@CodedAt("emailAddress")
  var email: String 
}

when I fetch the model from the api it returns the email field using the key name: "emailAddress" in the incoming json, but when I post the same model to a different api I want to use the name: "email" instead of "emailAddress"

[Feature Request] IgnoreEncoding depends on other properties

Is your feature request related to a problem? Please describe.
Nope

Describe the solution you'd like
CleanShot 2024-06-13 at 17 22 29

Describe alternatives you've considered

@Codable
public final class SomeClass2 {
    public var value1: Bool
    @IgnoreEncoding(if: { ($0 as SomeClass2).value1 })
    public var value2: Int
}

Additional context
PS: It's a little bit strange that pass true to IgnoreEncoding(if:) lead to encoding but not ignore.

Cocoapods installation does not work

Hi,

when I add pod 'MetaCodable' to my project, I get this error:
env: /my_project_path/Pods/MetaCodableMacro/Utils/macro_plugin_build.sh: No such file or directory

If I see 'Pods/MetaCodableMacro' the Utils directory doesn't exist.

`MetaProtocolCodable` cannot handle Optional value correctly

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

JSON to handle
{
    "data": {
        "id": "some UUID",
        "type": "Foo.Bar",
        "attributes": {
            "id": "another UUID",
            "expiresIn": 3600,
            "xxx-token": "xxxxxx",
            "yyy-token": "yyyyyyyyy"
        }
    }
}

and

{
    "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
@Codable
@CodedAs<String?>
@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<String?> { .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<String?> { .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`
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):

  • OS: macOS
  • Version 13.6.4
  • Xcode 15.2
  • Swift 5.9.2

[Bug] Ignoring values with didSet/willSet

I think by trying to ignore computed variables, you've inadvertently ignored values with didSet and willSet

Since updating to a more recent version I now get the error
Return from initializer without initializing all stored properties
when my struct contains something like

var example: String {
    didSet {
        // do something
    }
}

[Feature Request] Encode dictionary with non-String keys

Problem

With Codable types Swift, a Dictionary is only encoded to a map (as JSON), e.g. { "foo": "my value" } when it's key type is either a String or an Int. In all other cases, the encoding is an array where the keys and values are interspersed, e.g. ["key1", "value1", "key2", "value2"]

This is somewhat reasonable, but, it would be really handy when the Key is RawRepresentable and RawValue == String or RawValue == Int it would also be encoded to a map object instead of an array.

Ideal Solution

I guess ideally, maybe this is a topic for Swift Evolution? But, failing that, it would be really handy if MetaCodable could provide macro to achieve this. Perhaps something like: @CodedBy(.dictionary) ?

Alternative

I think the alternative is to write a full-blown custom encoder. I did look at trying to make Dictionary conform to HelperCoder but ran into public/package issues. Although I did get this to work...

struct DictionaryCoder<Key: Hashable & RawRepresentable, Value: Codable>: HelperCoder
where Key.RawValue == String {

  func decode(from decoder: any Decoder) throws -> Dictionary<Key, Value> {
    let container = try decoder.singleValueContainer()
    let dictionary = try container.decode([Key.RawValue: Value].self)
    var output = Dictionary<Key, Value>()
    for (rawValue, value) in dictionary {
      if let key = Key(rawValue: rawValue) {
        output[key] = value
      }
    }
    return output
  }

  func encode(_ value: Dictionary<Key, Value>, to encoder: any Encoder) throws {
    let dictionary: [String: Value] = Dictionary(
      uniqueKeysWithValues: value.map { ($0.rawValue, $1) }
    )
    var container = encoder.singleValueContainer()
    try container.encode(dictionary)
  }
}

extension Dictionary where Key: Hashable & RawRepresentable, Key.RawValue == String, Value: Codable {
  static var coder: DictionaryCoder<Key, Value> {
    DictionaryCoder<Key, Value>()
  }
}

which would then be used as...

@CodedBy([MyRawRepresentableKey: MyValue].coder)
let valuesByKey: [MyRawRepresentableKey: MyValue]

but this is far from ideal, not only does it have some edges cases but the ergonomics are poor. It would be better if the helper coder is generated by the CodedBy macro itself, because it has knowledge of the Coded type.

Thoughts?

Only 21 of 75 Tests succeed when running them on main.

When trying to work on some of the issues I filed earlier today, I discovered that when running the tests on main (simply opening the Package.swift in XCode an pressing Command + U) about 75% of the Tests fail.

The failures seem to indicate that @Codable is not expanded:

An example failure:

CodableTests.swift:75: failed - Actual output (+) differed from expected output (-):
 struct SomeCodable: Encodable {
     let value: String
 
     func encode(to encoder: Encoder) throws {
     }
 }
–
–extension SomeCodable: Decodable {
–    init(from decoder: Decoder) throws {
–        let container = try decoder.container(keyedBy: CodingKeys.self)
–        self.value = try container.decode(String.self, forKey: CodingKeys.value)
–    }
–}
–
–extension SomeCodable {
–    enum CodingKeys: String, CodingKey {
–        case value = "value"
–    }
–}

Actual expanded source:

struct SomeCodable: Encodable {
    let value: String

    func encode(to encoder: Encoder) throws {
    }
}

Is this an error on my end?

[Feature Request] Create helper types for specialized data decoding/encoding

  • Create type that only decodes valid data and ignores invalid data for collections, instead of typical way of failing entire decoding when encountering invalid data.
  • Create type that decodes a basic data type (i.e Bool, Int, String) from any basic data type (i.e Bool, Int, String).
  • Create type that provides various approaches to Date decoding/encoding.
  • Create type that provides various approaches to Data decoding/encoding.
  • Create type that provides various approaches to non-confirming floats decoding/encoding.
  • Create type that can accept two different instances of HelperCoder, decodes and encodes with different instances.

Provide default value of nil for empty strings

I have the following model:

@Codable
struct Model {
 var myVar: String?
}

is there a way to convert myVar's value to nil when its value is empty string when encoding/ decoding the model ?

No value associated with key CodingKeys

Describe the bug
Fails to decode a key that is present (but only after the first time).

DecodingError
      ▿ keyNotFound : 2 elements
        - .0 : CodingKeys(stringValue: "time_zone", intValue: nil)
        ▿ .1 : Context
          - codingPath : 0 elements
          - debugDescription : "No value associated with key CodingKeys(stringValue: \"time_zone\", intValue: nil) (\"time_zone\")."
          - underlyingError : nil

But this is the response (always the same of course):

{
  "id" : "12345-6789-0",
  "opening_times" : {
    "twentyfourseven" : true
  },
  "coordinates" : {
    "longitude" : "9.028339",
    "latitude" : "48.926502"
  },
  "postal_code" : "71739",
  "time_zone" : "Europe\/Berlin"
}

Here is my model:

@Codable
@CodingKeys(.snake_case)
public struct LocationDetails {
    public let id: String
    public let timeZone: String
    public let postalCode: String

    @CodedIn("coordinates")
    @CodedBy(ValueCoder<Double>())
    public let latitude: Double

    @CodedIn("coordinates")
    @CodedBy(ValueCoder<Double>())
    public let longitude: Double

    @CodedIn("opening_times")
    @Default([])
    public let regularHours: [RegularHours]

}

@Codable
@CodingKeys(.snake_case)
public struct RegularHours {
    public let weekday: Int
    public let periodBegin: String
    public let periodEnd: String
}

To Reproduce
Steps to reproduce the behavior:

  1. Fetch data from an endpoint first time
  2. Everything parses correctly, everything is populated:
LocationDetails(id: "12345-6789-0", timeZone: "Europe/Berlin", postalCode: "71739", latitude: 48.926502, longitude: 9.028339, regularHours: [])
  1. Fetch data from the same endpoint again and I get the error mentioned.
    "No value associated with key CodingKeys(stringValue: \"time_zone\", intValue: nil) (\"time_zone\")."

Expected behavior
I expect to correctly parse the model everytime.

Screenshots

Expanded macro:
Screenshot 2024-05-12 at 14 50 39

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

  • OS: macOS
  • Version 14.4.1 (23E224)
  • Xcode 15.3 (15E204a)
  • Swift 5.9

Additional context
The macro looks fine, and I thought it might have something to do with time_zone string, but I commented it out and it gave me the same error (only after the first time) for postal_code as well.

[Bug] Macro expansion does not compile when enabling the 'Existential Any' upcoming feature.

When using the .enableUpcomingFeature("ExistentialAny") for building a package the generated Macro code does not compile.

@Codable
struct Test {
  var test: String
}

Is expanded to:

extension Test: Decodable {
    init(from decoder: Decoder) throws {  //<- Error: Use of protocol 'Decoder' as a type must be written 'any Decoder'
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.test = try container.decode(String.self, forKey: CodingKeys.test)
    }
}

extension Test: Encodable {
    func encode(to encoder: Encoder) throws {  // <- Error: Use of protocol 'Decoder' as a type must be written 'any Decoder'
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.test, forKey: CodingKeys.test)
    }
}

extension Test {
    enum CodingKeys: String, CodingKey {
        case test = "test"
    }
}

[Feature Request] Encodable Default Value Handling

Is your feature request related to a problem? Please describe.

In my scenario, I need to assign default values during decoding if certain fields are missing (which is already supported), but I also want to avoid encoding a field if its value matches the same default.

Consider this Node struct:

@Codable
struct Node: Hashable {
    @Default("")
    var label: String = ""
    @Default(CGPoint.zero)
    var center: CGPoint = .zero
}

For example, if center isn't set, it should default to .zero during decoding. Similarly, for my use case, if it's already set to .zero, there's no need to encode it since it's the expected default.

Describe the solution you'd like
Introduce a new tag, perhaps something like @IgnoreEncodingIfDefault when set alongside @Default(CGPoint.zero) or a standalone @IgnoreEncodingDefault(CGPoint.zero) for this purpose. (I'm not certain what the best naming convention would be, I'd imagine you'd have better ideas.)

I feel this feature would be particularly useful for user-editable configuration files where we would want to omit any "optional" fields, or just keeping human-readable configurations minimal for git diffing.

[Feature Request] No HelperCoder generated when check compilation condition

Describe the bug
Below code works well.

working code
@Codable
@CodedAs<String?>
@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<String?> { .one("REGISTRATION") }
    let id: String
    @CodedAt("status-code")
    let statusCode: String
    let operation: String
}
@Codable
struct VerificationAttributes: ResponseAttributes, DynamicCodable {
    static var identifier: DynamicCodableIdentifier<String?> { .one(nil) }
    let id: String
    let operation: String?
    let expiresIn: UInt
    @CodedAt("xxx-token")
    let xxxToken: String
    @CodedAt("yyy-token")
    let yyyToken: String
}

But the ResponseAttributesCoder will be missing if add #if SOME_SWIFT_ACTIVE_COMPILATION_CONDITION.

Xcode say 'Cannot find 'AnonymousAttestationResponseAttributesCoder' in scope'.

failed to work
// Have set the `SOME_SWIFT_ACTIVE_COMPILATION_CONDITION` in `swiftSettings` from `Package.swift`.
#if SOME_SWIFT_ACTIVE_COMPILATION_CONDITION
@Codable
@CodedAs<String?>
@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<String?> { .one("REGISTRATION") }
    let id: String
    @CodedAt("status-code")
    let statusCode: String
    let operation: String
}
@Codable
struct VerificationAttributes: ResponseAttributes, DynamicCodable {
    static var identifier: DynamicCodableIdentifier<String?> { .one(nil) }
    let id: String
    let operation: String?
    let expiresIn: UInt
    @CodedAt("xxx-token")
    let xxxToken: String
    @CodedAt("yyy-token")
    let yyyToken: String
}
#endif

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
Compilation condition check shouldn't fail plugin or macro.

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

  • OS: macOS
  • Version 13.6.6
  • Xcode 15.2
  • Swift 5.9.2

Custom parsing logic inside decoding method

I'm trying to add custom parsing logic to init(from:) method, and my nested model depends on a property "above" it. You can see this in the response:

{
    "id": "12345",
    "time_zone": "Europe/Berlin", // need to use this when parsing `regular_hours` so that I can figure out proper dates/times...
    "opening_times": {
        "twentyfourseven": false,
        "regular_hours": [
            {
                "weekday": 6,
                "period_begin": "08:00",
                "period_end": "02:00"
            },
            {
                "weekday": 5,
                "period_begin": "08:00",
                "period_end": "02:00"
            }
        ]
    }
}

And I want to achieve something like this:

public struct LocationDetails: Decodable {
    public let id: String
    public let timeZone: TimeZone
    private let regularHours: [RegularHours]

    private enum CodingKeys: String, CodingKey {
        case id
        case timeZone = "time_zone"
        case regularHours = "regular_hours"
        case openingTimes = "opening_times"
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.id = try container.decode(String.self, forKey: CodingKeys.id)
        self.timeZone = try ValueCoder<TimeZone>().decode(from: container, forKey: CodingKeys.timeZone)

        if let openingTimes_container = try? container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.openingTimes) {
            do {
                let hours = try openingTimes_container.decode([RegularHoursDto].self, forKey: CodingKeys.regularHours)

                // Parsing logic
                let calendar = Calendar.iso8601
                let today = Date()
                let todayIndex = today.weekday(from: calendar)
                let todayComponents = calendar.dateComponents([.year, .month, .day], from: today)

                let tz = timeZone

                self.regularHours = hours.map {
                    // Parsing logic...
                    let beginDateComponents = DateComponents(timeZone: tz, ...)
                    let endDateComponents = DateComponents(timeZone: tz, ...)

                    return RegularHours(beginDate: calendar.date(from: beginDateComponents)!,
                                            endDate: calendar.date(from: endDateComponents)!,
                                            timezone: tz)
                }
            } catch {
                self.regularHours = []
            }
        } else {
            self.regularHours = []
        }

    }
}

public struct RegularHours {
    public let beginDate: Date
    public let endDate: Date
    public let timezone: TimeZone // Not in this object, but one level above it
}

public struct RegularHoursDto: Decodable {
    public let weekday: Int
    public let periodBegin: String
    public let periodEnd: String

    private enum CodingKeys: String, CodingKey {
        case weekday
        case periodBegin = "period_begin"
        case periodEnd = "period_end"
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.weekday = try container.decode(Int.self, forKey: CodingKeys.weekday)
        self.periodBegin = try container.decode(String.self, forKey: CodingKeys.periodBegin)
        self.periodEnd = try container.decode(String.self, forKey: CodingKeys.periodEnd)
    }
}

Is there a way to do something like this with pre-defined macros?

Thanks!

`@CodedAs` fails with same keys in different casing

I've noticed a bug with @Codedas when providing multiple keys, which are the same exept for their cases (e.g. camel case and snake case). The macro will expand them to be the same in the CodingKeys enum, which will cause an Invalid redeclaration of 'key' error. See this example:

@Codable
struct TestCodable {
    @CodedAs("fooBar", "foo_bar")
    var fooBar: String
}

This will expand into this code:

extension TestCodable: Decodable {
    init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let fooBarKeys = [CodingKeys.fooBar, CodingKeys.fooBar, CodingKeys.fooBar].filter {
            container.allKeys.contains($0)
        }
        guard fooBarKeys.count == 1 else {
            let context = DecodingError.Context(
                codingPath: container.codingPath,
                debugDescription: "Invalid number of keys found, expected one."
            )
            throw DecodingError.typeMismatch(Self.self, context)
        }
        self.fooBar = try container.decode(String.self, forKey: fooBarKeys[0])
    }
}

extension TestCodable: Encodable {
    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.fooBar, forKey: CodingKeys.fooBar)
    }
}

extension TestCodable {
    enum CodingKeys: String, CodingKey {
        case fooBar = "fooBar"
        case fooBar = "foo_bar" // << see duplicate key here
    }
}

Thanks a lot for this package. This feels like the missing piece in Codable!

CodedBy fails with optional dates decoding

I have the following @Codable model and its HelperCoder struct:

@Codable
struct TestModel {
    
    @CodedBy(DateCoder())
    var startDate: Date?
}

struct DateCoder: HelperCoder {
    func decode(from decoder: Decoder) throws -> Date? {
        var container = try decoder.unkeyedContainer()
        let dateStr = try container.decode(String.self)
        let df = ISO8601DateFormatter()
        let date = df.date(from: dateStr)
        return date
    }
}

the input JSON is

{
"startDate": "2023-09-20T00:00:00Z"
}

I get this compiler error:

Cannot assign value of type 'Date??' to type 'Date?'

the expansion screenshot is as follows:

Screenshot 2023-10-04 at 3 14 54 PM

[Bug] @Codable fails in structs with static members.

In my testing @Codable fails for structs which include static members:

@Codable
struct Test {
  static var test = "test"
}

Fails with Failed to receive result from plugin (from macro 'Codable').

It also fails in all other common permutations (with normal members etc.)

A working workaround for now is to declare these static members in an extension.

Error decoding nested JSON with missing container

Hi there!

Big thanks for this package, I've really been enjoying it!

I have an issue when decoding nested JSON when one level of the keys doesn't exist.

I have this struct:

@Codable
struct Avatar {
    @CodedAt("avatar", "url")
    let url: URL?
}

Trying to decode this works fine.

func testConditionalAndNestedOptionalCodingWorking() throws {
    let jsonStr = """
        {
            "avatar": {
                "url": "https://example.com/"
             }
        }
        """
    let json = try XCTUnwrap(jsonStr.data(using: .utf8))
    let model = try JSONDecoder().decode(Avatar.self, from: json)
    XCTAssertEqual(model.url, URL(string: "https://example.com/"))
}

Decoding JSON with a value for "url" also works.

func testConditionalAndNestedOptionalCoding() throws {
    let jsonStr = """
        {
            "avatar": {
                "url": null
             }
        }
        """
    let json = try XCTUnwrap(jsonStr.data(using: .utf8))
    let model = try JSONDecoder().decode(Avatar.self, from: json)
    XCTAssertEqual(model.url, nil)
}

The problem is when the "avatar" value is null.

func testMissingContainer() throws {
    let jsonStr = """
        {
            "avatar": null
        }
        """
    let json = try XCTUnwrap(jsonStr.data(using: .utf8))
    let model = try JSONDecoder().decode(Avatar.self, from: json)
    XCTAssertEqual(model.url, nil)
}

Since my struct has an optional type for the url property, I would expect it to ignore the missing container and fallback to setting the value as nil. Instead, I get back an error:

typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found a null value instead.", underlyingError: nil))

I tried setting a default value using @Default(nil as URL?) but that didn't seem to work.

Anything I'm missing here or is this a bug?

Thanks again for the package!

Distribute a prebuilt binary

It is recommended to release not only the source code, but also the precompiled version to reduce the CI time

[Feature Request] Allow type level customization for `Codable` implementation generated

Following options can be provided in the @Codable macro to allow customization for generated implementation:

  • @CodingKeys: The strategy to use to generate CodingKey value from property name. i.e. by default property name will be used as is, with option to convert to camel-case, snake-case etc.
  • @IgnoreCodingInitialized: Whether to ignore initialized mutable stored properties in decoding/encoding (immutable stored properties have to be ignored).
    • Adding property macros i.e. @CodedIn, @CodedAt etc. will indicate exception for specific stored property and the attached property will be considered in decoding/encoding .
    • Property level control will also be provided with new macro @IgnoreCoding.
    • Property level decoding/encoding specific control will also be provided with new macros @IgnoreDecoding and @IgnoreEncoding.

Xcode build archive fails

Describe the bug
xcodebuild archive fails in the following scenario.

To Reproduce
Steps to reproduce the behavior:

  1. Create a Swift Package, which depends on MetaCodable
  2. Create a Target which depends on MetaCodable, HelperCoders and uses the MetaProtocolCodable plugin.
  3. Include this target as a product of the package
  4. Add types into the target which make use of dynamic data variations, in particular Step 9 from the tutorial: https://swiftpackageindex.com/swiftylab/metacodable/v1.3.0/tutorials/metacodable/dynamic
  5. Create an Xcode project for an iOS app which imports the product from the package.
  6. Add code-signing for your app
  7. Build and Archive from Xcode Product menu (or using xcodebuild archive)

Expected behavior
I should be able to archive the product.

Actual behaviour
Build script fails because build tools cannot find the executable tool ProtocolGen.

Showing All Issues
sandbox-exec: execvp() of '//Users/daniel/Library/Developer/Xcode/DerivedData/Posts-aaxhnzolyyfxrgcsibzfeeijakbj/Build/Intermediates.noindex/ArchiveIntermediates/Posts/BuildProductsPath/Release/ProtocolGen' failed: No such file or directory

Screenshots

Screenshot 2024-05-01 at 11 17 44

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

  • OS: macOS
  • Version 14.4.1 (23E224)
  • Xcode Version 15.3 (15E204a)
  • Swift $ swift -version
    swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
    Target: arm64-apple-macosx14.0
  • CocoaPods N/A

Additional context
I have created a small project to demonstrate this issue: https://github.com/danthorpe/metacodable_demo

A possible solution, would be to export ProtocolGen as a Swift artefact bundle, and reference this as a binary target in your Package.swift. e.g. how SwiftLint is packaged. In this project, on macOS, the dependency is a binary target instead of the source code executable.

SwiftData class: Method does not override any method from its superclass

If a SwiftData model annotation is added to a class, the build will fail with "Method does not override any method from its superclass".

It can be reproduced using a clean SwiftData project, adding the SPM package from 1.2.1, and the following class:

import Foundation
import SwiftData
import MetaCodable
import HelperCoders

protocol MyProtocol {
    
}

@Codable
@Model
final class Item : MyProtocol {
    @CodedAt("timestamp")
    var timestamp: Date? = nil
    
    init() { }
}

Expanding the macro actually shows it doesn't add the super.* calls:

image

I have however managed to produce one expansion with the error:

image

Note of appreciation :) + Some questions

Hello @soumyamahunt
Thank you for your work, sincerely!
When I saw this repo, I was pleasantly surprised🙂

Finally I feel like I have something to switch to instead of using SwiftyJSON, because default Codable was never good for dynamic responses with absent values and dynamic structure.

Question:
Now I wonder if it's possible to add something like this, cause I didn't work with Macros and don't really know what's possible.

@CodedAt("title", or: "title_name")
let title: String

If the title can come in JSON inside "title" key, and if not present, look for or "title_name" key.

Keep up the good work👋👋👋

@CodedBy and ValueCoder not in scope

I'm trying to use the @CodedBy macro with the provided ValueCoder but whatever I try, Xcode gives me the error "Cannot find 'ValueCoder' in scope".

I am able to successfully use @Codable and @CodedAt, so I know the package is properly referenced in Xcode.

Are there any examples of how to use @codedat with the provided coders, or any direction as to what I might be missing here?

Here's the code for the struct I'm trying to decode.

public struct Item {

    @CodedAt("itemid")
    public let id: Int

    @CodedAt("walmart_id")
    public let walmartId: String

    @CodedBy(ValueCoder<Int>)
    public let name: Int

    @CodedAt("images", "original")
    public let imageUrl: String
}

A way to log when a property defaults on the default value

Hello, the Default implementation is very useful when wanting to create a more resilient application in regards to a highly dynamic backend. The problem is that before when a mandatory value was missing an error is thrown in the debugger or tracked for a production app. Defaulting is great but would make things hard to debug when backend does not send the proper data. Is there a way to log/track when a value defaulted because it was missing in the json or had an incorrect format?

Derive access level of generate CodingKeys from @Codable model

Right now @Codable macro will generate CodingKeys with internal access level.

@Codable
@CodingKeys(.PascalCase)
public struct MessageReceiver {
    public let userId: UUID
    public let status: OData.Enum<Int>
}

extension MessageThread.Response.MessageReceiver {
    enum CodingKeys: String, CodingKey {
        case userId = "UserId"
        case status = "Status"
    }
}

It would great to generate CodingKeys with the same access level (public) as MessageReceiver. This would allow access to CodingKeys in multi package application. As of my particular use case I would like to build OData requests using CodingKeys instead of String from the domain layer of application.

Really appreciate effort in open sourcing the project, unfortunately internal macros plugin implementation is quite complicated to be able to make a quick PR with solution.

Support for customizing/removing nested key for enums with associated values?

Hey! Great lib! Does it already support this use case?

struct StateHolder: Codable {
	let state: State
	enum State: Codable {
		case on(On)
		case off(Off)
		struct On: Codable {
			let onOnlyProperty: String
		}
		struct Off: Codable {
			let offOnlyProperty: String
		}
	}
}

when I JSON encode StateHolder(state: .on(.init(onOnlyProperty: "On!"))) Swift produces:

{
	"state" : {
		"on" : {
			"_0" : {
				"onOnlyProperty": "On!"
			}
		}
	}
}

Or using .off respectively:

{
	"state" : {
		"off" : {
			"_0" : {
				"offOnlyProperty": "Off!"
			}
		}
	}
}

Which is... well I think that was strange design decision by SE-0295.

I always manually change this to:

{
	"state" : {
		"discriminator": "on",
		"on" : {
			"onOnlyProperty": "On!"
		}
	}
}

Is this support by the lib? Or something you would consider adding?

[Bug] `CodedAs` causes crash in enum when encoding.

CodedAs attribute is confusing in enumerations.

enum SomeEnum {
    case bool( variable: Bool)
    @CodedAs("altInt")
    case int(val: Int)
    @CodedAs("altDouble1", "altDouble2")
    case double(_: Double)
    case string(String)
    case multi(_ variable: Bool, val: Int, String)
}

try? JSONDecoder().encode(SomeEnum.multi(false, val: 10, "test")) /// will crash
@Codable
@CodedAt("type")
enum SomeEnum {
    case bool(_ variable: Bool)
    @CodedAs("altInt")
    case int(val: Int)
    @CodedAs("altDouble1", "altDouble2")
    case double(_: Double) /// Crash when encode
    case string(String) /// Crash when encode
    case multi(_ variable: Bool, val: Int, String) /// Crash when encode
}

What json to decode with case double(_: Double) case multi(_ variable: Bool, val: Int, String) ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.