Recurse through Optional when allowlisting predicate key paths#2013
Recurse through Optional when allowlisting predicate key paths#2013adityasingh2400 wants to merge 1 commit into
Conversation
allowKeyPathsForPropertiesProvided(by:recursive:) skipped recursion for any property whose declared type is Optional. The recursion guard cast the key path's value type directly to PredicateCodableKeyPathProviding, but for a property like Address? the value type is Optional<Address>, which does not conform to that protocol even when Address does. As a result the wrapped type's key paths were never added to the allowlist, so encoding a predicate that traversed the optional property failed with a 'keypath is not in the provided allowlist' error unless the wrapped type was allowlisted manually. Add a helper that looks through a single layer of Optional before checking for conformance, so an optional providing property recurses into its wrapped type the same way a non-optional property does. The same helper is used by the disallow path for symmetry. Fixes swiftlang#1356
|
This is an interesting bug that I've thought about for a bit but haven't come up with a good answer for yet. The problem is somewhat twofold:
Based on those two points, thus far the simplest answer has been that if you want to allow key paths of properties wrapped in any other type, that you must call that out yourself (and provide any operators to traverse that type yourself). Have you thought about either of these aspects and how they may impact various client expectations? |
|
Thanks, both of these are fair, and they pushed me to look more closely at how the allowlists actually compose. Let me take them in turn. On the operators point: I think the key-path allowlist and the operator allowlist are already fully decoupled, and this change keeps them that way. On Optional being arbitrary: this is the part I am genuinely unsure about, and I agree it is the real design question. My reasoning for picking Optional specifically was that it is the one wrapper where the recursion target is unambiguous and the value identity is preserved: Given that, I am happy to go whichever way you prefer. If the team's lean is that any wrapped property should be called out explicitly, then the consistent answer is to not special-case Optional and drop this, and I will close it. If looking through Optional specifically is acceptable because of the single-wrapped-type and value-identity properties above, I will keep it scoped to Optional and document that collections are intentionally not traversed. I do not have a strong stake in expanding to collections, so I would lean toward either "Optional only, documented" or "drop it," not the full collection traversal. Which direction reads as least surprising to you? |
Summary
PredicateCodableConfiguration.allowKeyPathsForPropertiesProvided(by:recursive:)skipped recursion for any property whose declared type is Optional, so an optional sub-object's key paths were never added to the allowlist. Encoding a predicate that traversed such a property then failed with a "keypath is not in the provided allowlist" error unless the wrapped type was allowlisted by hand. This addresses #1356.Root cause
The recursion guard cast the key path's value type directly to
PredicateCodableKeyPathProviding:For a non-optional property like
let address: Address, the value type isAddress, the cast succeeds, and recursion proceeds. For an optional property likelet address: Address?, the value type isOptional<Address>.Optional<Address>does not conform toPredicateCodableKeyPathProvidingeven whenAddressdoes, so the cast fails and the wrapped type's key paths are silently never allowed. That mismatch is exactly why the issue's example works after a manualallowKeyPathsForPropertiesProvided(by: Address.self)call but not withrecursive: truealone.Fix
A small helper looks through a single layer of Optional before checking for conformance. If the value type already conforms it is used directly, preserving existing behavior. If it is an Optional, the wrapped type is checked for conformance and recursed into when it conforms. The same helper is used by the matching disallow path so the two stay symmetric. Looking through the optional means an optional providing property now recurses into its wrapped type the same way a non-optional property already did.
Testing
Added
recursiveProvidedPropertiesThroughOptionaltoPredicateCodableTests, which encodes and decodes a predicate that traverses an optional providing property under a recursive configuration and verifies the round-tripped predicate evaluates identically. The test fails before this change (encoding throws because the wrapped type's key path is not allowlisted) and passes after it.The predicate archiving code is gated behind
FOUNDATION_FRAMEWORK, so I confirmed the FoundationEssentials and FoundationEssentialsTests targets build cleanly, validated the Optional look-through logic in isolation, and ran the SwiftPM predicate test suite, which passes with no regressions.Fixes #1356