Skip to content

Conversation

@JosephDuffy
Copy link
Owner

@JosephDuffy JosephDuffy commented Jan 27, 2021

TODO

  • Add Decodable support
  • Add an equivalent to unwrapped that's added by PartialConvertible when Wrapped is PartialCodable
  • Support embedded partials, builders
  • Find a more appropriate name for PartialCodable

@codecov
Copy link

codecov bot commented Jan 27, 2021

Codecov Report

Merging #175 (79f8225) into master (8395463) will increase coverage by 1.94%.
The diff coverage is 95.65%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #175      +/-   ##
==========================================
+ Coverage   92.81%   94.75%   +1.94%     
==========================================
  Files           6        8       +2     
  Lines         153      381     +228     
==========================================
+ Hits          142      361     +219     
- Misses         11       20       +9     
Impacted Files Coverage Δ
Sources/Partial/Partial.swift 82.35% <72.72%> (-10.51%) ⬇️
Sources/Partial/PartialBuilder.swift 91.75% <75.00%> (-1.51%) ⬇️
...s/Partial/Codable/KeyPathCodingKeyCollection.swift 87.50% <87.50%> (ø)
...al/Codable/KeyPathCodingKeyCollectionBuilder.swift 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8395463...79f8225. Read the comment docs.

private var decoders: [CodingKey: Decoder] = [:]

func encode(_ value: Any, forKey keyPath: PartialKeyPath<Root>, to container: inout KeyedEncodingContainer<CodingKey>) throws {
try encoders[keyPath]?(value, &container)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this throw an error if no encoder exists for a given keyPath?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hadn't considered that. It may be useful for consumers to be able to opt out certain values from being encoded, which making this throw would prevent.

But having this throw would also make debugging easier if a key path/coding key is missed.

Would it make sense to allow the CodingKey returned be nil, and if it's nil it'll never be encoded, but if it's missing it'll throw here?

}

func decode(_ codingKey: CodingKey, in container: KeyedDecodingContainer<CodingKey>) throws -> (Any, PartialKeyPath<Root>)? {
try decoders[codingKey]?(codingKey, container)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably throw here also?

/// static var keyPathCodingKeyCollection: KeyPathCodingKeyCollection<Self, CodingKeys> {
/// (\Self.stringValue, CodingKeys.stringValue)
/// (\Self.intValue, CodingKeys.intValue)
/// }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am familiar with @resultBuilder, but haven't had the chance to test it myself yet. Would this example code result in a new KeyPathCodingKeyCollection being created every time keyPathCodingKeyCollection is called? I know that View.body in SwiftUI is processed every time it is called, but it is an instance var, not static, so I'm not certain what the behaviour is here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other possible issue with using @resultBuilder here is that it potentially increases the risk of missing a CodingKeys value. An alternative implementation would be similar, except the function being implemented is:

static func keyPath(for codingKey: CodingKeys) -> PartialKeyPath<Wrapped>

The function would then use a switch internally. Eg, for your sample code:

static func keyPath(for codingKey: CodingKeys) -> PartialKeyPath<Wrapped> {
    switch codingKey {
    case .stringValue: return \Self.stringValue
    case .intValue: return \Self.intValue
    }
}

The builder code would then do pretty much the same as it's doing now, but enumerate through the CodingKeys. This would be added as a default implementation of keyPathCodingKeyCollection:

extension PartialCodable {
    static var keyPathCodingKeyCollection: KeyPathCodingKeyCollection<Self, CodingKey> = {
        var result = KeyPathCodingKeyCollection<Self, CodingKey>()
        for codingKey in Self.CodingKeys.allCases {
            result.addPair(keyPath: keyPath(for: codingKey), codingKey: codingKey)
        }
        return result
    }()
}

The advantage from a resilience POV is that switch forces you to handle all cases (unless you use default) whereas the @resultBuilder will be totally fine if you accidentally miss a case.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this example code result in a new KeyPathCodingKeyCollection being created every time keyPathCodingKeyCollection is called?

A new collection is created every call, but it's only created once per encode/decode.

An alternative implementation would be similar...

I initially had it like this but there's an issue knowing what the type to encode/decode is, so the implementer has to do the casting itself.

The best I could come up was https://github.com/JosephDuffy/Partial/blob/8a99ebbc1c6af9cd520da7654bcf327113eaf2c5/Tests/PartialTests/Tests/PartialCodableTests.swift but I wasn't a fan of it.

The other possible issue with using @resultBuilder here is that it potentially increases the risk of missing a CodingKeys value.

This is a concern for sure, especially since Swift will synthesise CodingKeys. Fixing this would require at least 2 functions (from key path to coding key and visa-versa) which for me is an acceptable traidoff, but I couldn't find a way to do this without the implementation also needing to perform a cast to the expected type.

@iSevenDays
Copy link

That would be a really great feature!

Imagine I have a profile and a complete profile with details.
Profile:
id
name

Full profile:
id
name
email
company

With this PR merged, I could merge models and parse them from JSON.

@JosephDuffy
Copy link
Owner Author

That would be a really great feature!

Imagine I have a profile and a complete profile with details.

Profile:

id

name

Full profile:

id

name

email

company

With this PR merged, I could merge models and parse them from JSON.

This is certainly a good use case, I do plan to work on this when I have more spare time. I don't personally need this feature so it's not a top priority for me currently!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants