Skip to content

Commit 114e5bb

Browse files
authored
Codable support for PredicateExpressions.EvaluatePredicate
1 parent 80bbb59 commit 114e5bb

File tree

6 files changed

+170
-106
lines changed

6 files changed

+170
-106
lines changed

Sources/FoundationEssentials/Predicate/Archiving/ExpressionArchiving.swift

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,54 @@ import ReflectionInternal
1919
enum PredicateCodableError : Error, CustomStringConvertible {
2020
case disallowedType(typeName: String, path: String)
2121
case disallowedIdentifier(String, path: String)
22-
case reconstructionFailure(PartialType, [Type])
22+
case reconstructionFailure(PartialType, [GenericArgument])
2323
case variadicType(typeName: String, path: String)
2424

2525
var description: String {
2626
switch self {
2727
case .disallowedType(let typeName, let path): return "The '\(typeName)' type is not in the provided allowlist (required by \(path))"
2828
case .disallowedIdentifier(let id, let path): return "The '\(id)' identifier is not in the provided allowlist (required by \(path))"
29-
case .reconstructionFailure(let partial, let args): return "Reconstruction of '\(partial.name)' with the arguments \(args.map(\.swiftType)) failed"
29+
case .reconstructionFailure(let partial, let args):
30+
let types = args.map {
31+
switch $0 {
32+
case .type(let type): _typeName(type.swiftType)
33+
case .pack(let types): "Pack{\(types.map({ _typeName($0.swiftType) }).joined(separator: ", "))}"
34+
}
35+
}
36+
return "Reconstruction of '\(partial.name)' with the arguments [\(types.joined(separator: ", "))] failed"
3037
case .variadicType(let typeName, let path): return "The '\(typeName)' type is not allowed because it contains type pack parameters (required by \(path))"
3138
}
3239
}
3340
}
3441

3542
@available(FoundationPredicate 0.1, *)
3643
private struct ExpressionStructure : Codable {
37-
let identifier: String
38-
let args: [ExpressionStructure]
44+
private enum Argument : Codable {
45+
case scalar(ExpressionStructure)
46+
case pack([ExpressionStructure])
47+
48+
init(from decoder: any Decoder) throws {
49+
let container = try decoder.singleValueContainer()
50+
if let scalarArg = try? container.decode(ExpressionStructure.self) {
51+
self = .scalar(scalarArg)
52+
} else {
53+
self = .pack(try container.decode([ExpressionStructure].self))
54+
}
55+
}
56+
57+
func encode(to encoder: any Encoder) throws {
58+
switch self {
59+
case let .scalar(arg):
60+
var container = encoder.singleValueContainer()
61+
try container.encode(arg)
62+
case let .pack(args):
63+
var container = encoder.singleValueContainer()
64+
try container.encode(args)
65+
}
66+
}
67+
}
68+
private let identifier: String
69+
private let args: [Argument]
3970

4071
private enum CodingKeys: CodingKey {
4172
case identifier
@@ -56,7 +87,7 @@ private struct ExpressionStructure : Codable {
5687
init(from decoder: Decoder) throws {
5788
if let keyedContainer = try? decoder.container(keyedBy: CodingKeys.self) {
5889
identifier = try keyedContainer.decode(String.self, forKey: .identifier)
59-
args = try keyedContainer.decode([ExpressionStructure].self, forKey: .args)
90+
args = try keyedContainer.decode([Argument].self, forKey: .args)
6091
return
6192
}
6293

@@ -65,20 +96,20 @@ private struct ExpressionStructure : Codable {
6596
}
6697

6798
init(_ type: Type, with configuration: PredicateCodableConfiguration, path: [String] = []) throws {
68-
#if canImport(ReflectionInternal, _version: "18")
69-
if type.partial?.hasParameterPacks ?? false {
70-
throw PredicateCodableError.variadicType(typeName: _typeName(type.swiftType), path: "/\(path.joined(separator: "/"))")
71-
}
72-
#endif
7399
guard let result = configuration._identifier(for: type) else {
74100
throw PredicateCodableError.disallowedType(typeName: _typeName(type.swiftType), path: "/\(path.joined(separator: "/"))")
75101
}
76102

77103
self.identifier = result.identifier
78104

79105
if !result.isConcrete {
80-
self.args = try type.genericArguments.map {
81-
try .init($0, with: configuration, path: path + [result.identifier])
106+
self.args = try type.genericArguments2.map {
107+
switch $0 {
108+
case .type(let type):
109+
.scalar(try .init(type, with: configuration, path: path + [result.identifier]))
110+
case .pack(let types):
111+
.pack(try types.map { try .init($0, with: configuration, path: path + [result.identifier]) })
112+
}
82113
}
83114
} else {
84115
self.args = []
@@ -98,17 +129,16 @@ private struct ExpressionStructure : Codable {
98129
partial = partialType
99130
}
100131

101-
#if canImport(ReflectionInternal, _version: "18")
102-
if partial.hasParameterPacks {
103-
throw PredicateCodableError.variadicType(typeName: partial.name, path: "/\(path.joined(separator: "/"))")
104-
}
105-
#endif
106-
107-
let argTypes = try args.map {
108-
try $0.reconstruct(with: configuration, path: path + [identifier])
132+
let argTypes: [GenericArgument] = try args.map {
133+
switch $0 {
134+
case let .scalar(arg):
135+
.type(try arg.reconstruct(with: configuration, path: path + [identifier]))
136+
case let .pack(args):
137+
.pack(try args.map { try $0.reconstruct(with: configuration, path: path + [identifier]) })
138+
}
109139
}
110140

111-
guard let created = partial.create(with: argTypes) else {
141+
guard let created = partial.create2(with: argTypes) else {
112142
throw PredicateCodableError.reconstructionFailure(partial, argTypes)
113143
}
114144
return created
@@ -117,7 +147,7 @@ private struct ExpressionStructure : Codable {
117147

118148
@available(FoundationPredicate 0.1, *)
119149
class PredicateArchivingState {
120-
let configuration: PredicateCodableConfiguration
150+
var configuration: PredicateCodableConfiguration
121151

122152
private var variableMap: [UInt : PredicateExpressions.VariableID]
123153

@@ -150,6 +180,7 @@ enum PredicateExpressionCodingKeys : CodingKey {
150180
@available(FoundationPredicate 0.1, *)
151181
fileprivate extension PredicateCodableConfiguration {
152182
mutating func allowInputs<each Input>(_ input: repeat (each Input).Type) {
183+
guard self.shouldAddInputTypes else { return }
153184
var inputTypes = [Any.Type]()
154185
repeat inputTypes.append((each Input).self)
155186
for (index, type) in inputTypes.enumerated() {
@@ -158,16 +189,29 @@ fileprivate extension PredicateCodableConfiguration {
158189
}
159190
}
160191

192+
private func _withPredicateArchivingState<R>(_ configuration: PredicateCodableConfiguration, _ block: () throws -> R) rethrows -> R {
193+
if let currentState = _ThreadLocal[.predicateArchivingState] {
194+
// Store the new configuration and reset it after encoding the subtree
195+
let oldConfiguration = currentState.configuration
196+
defer { currentState.configuration = oldConfiguration }
197+
198+
currentState.configuration = configuration
199+
return try block()
200+
} else {
201+
var state = PredicateArchivingState(configuration: configuration)
202+
return try _ThreadLocal.withValue(&state, for: .predicateArchivingState, block)
203+
}
204+
}
205+
161206
@available(FoundationPredicate 0.1, *)
162207
extension KeyedEncodingContainer where Key == PredicateExpressionCodingKeys {
163208
mutating func _encode<T: PredicateExpression & Encodable, each Input>(_ expression: T, variable: repeat PredicateExpressions.Variable<each Input>, predicateConfiguration: PredicateCodableConfiguration) throws where T.Output == Bool {
164209
var predicateConfiguration = predicateConfiguration
165210
predicateConfiguration.allowInputs(repeat (each Input).self)
166211
let structure = try ExpressionStructure(Type(expression), with: predicateConfiguration)
167-
var state = PredicateArchivingState(configuration: predicateConfiguration)
168212
var variableContainer = self.nestedUnkeyedContainer(forKey: .variable)
169213
repeat try variableContainer.encode(each variable)
170-
try _ThreadLocal.withValue(&state, for: .predicateArchivingState) {
214+
try _withPredicateArchivingState(predicateConfiguration) {
171215
try self.encode(structure, forKey: .structure)
172216
try self.encode(expression, forKey: .expression)
173217
}
@@ -188,9 +232,8 @@ extension KeyedDecodingContainer where Key == PredicateExpressionCodingKeys {
188232
guard let exprType = try structure.reconstruct(with: predicateConfiguration).swiftType as? any (Decodable & PredicateExpression<Bool>).Type else {
189233
throw DecodingError.dataCorruptedError(forKey: .structure, in: self, debugDescription: "This expression is unsupported by this predicate")
190234
}
191-
var state = PredicateArchivingState(configuration: predicateConfiguration)
192235
var container = try self.nestedUnkeyedContainer(forKey: .variable)
193-
return try _ThreadLocal.withValue(&state, for: .predicateArchivingState) {
236+
return try _withPredicateArchivingState(predicateConfiguration) {
194237
let variable = (repeat try container.decode(PredicateExpressions.Variable<each Input>.self))
195238
return (try decode(exprType), variable)
196239
}

Sources/FoundationEssentials/Predicate/Archiving/Predicate+Codable.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,28 @@
1212

1313
#if FOUNDATION_FRAMEWORK
1414

15+
extension PredicateCodableConfiguration {
16+
fileprivate static var `default`: Self {
17+
// If we're encoding this predicate inside of another one, use the parent predicate's configuration since the "default" was specified here
18+
if var parent = _ThreadLocal[.predicateArchivingState]?.configuration {
19+
// When decoding sub-predicates, we don't want to overwrite inputs since they by definition must already be in the parent predicate's configuration
20+
parent.shouldAddInputTypes = false
21+
return parent
22+
} else {
23+
// Otherwise, the default is the standardConfiguration
24+
return .standardConfiguration
25+
}
26+
}
27+
}
28+
1529
@available(FoundationPredicate 0.1, *)
1630
extension Predicate : Codable {
1731
public func encode(to encoder: Encoder) throws {
18-
try self.encode(to: encoder, configuration: .standardConfiguration)
32+
try self.encode(to: encoder, configuration: .default)
1933
}
2034

2135
public init(from decoder: Decoder) throws {
22-
try self.init(from: decoder, configuration: .standardConfiguration)
36+
try self.init(from: decoder, configuration: .default)
2337
}
2438
}
2539

Sources/FoundationEssentials/Predicate/Archiving/PredicateCodableConfiguration.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public struct PredicateCodableConfiguration: Sendable, CustomDebugStringConverti
7777

7878
private var allowedKeyPaths: [String : AllowListKeyPath] = [:]
7979
private var allowedTypes: [String : AllowListType] = [:]
80+
internal var shouldAddInputTypes = true
8081

8182
public init() {}
8283

Sources/FoundationEssentials/Predicate/Expressions/PredicateEvaluation.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,15 @@ extension PredicateExpressions.PredicateEvaluate : StandardPredicateExpression w
5656
@available(FoundationPredicate 0.3, *)
5757
extension PredicateExpressions.PredicateEvaluate : Codable where Condition : Codable, repeat each Input : Codable {
5858
public func encode(to encoder: Encoder) throws {
59-
throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Encoding the PredicateEvaluate operator is not yet supported"))
59+
var container = encoder.unkeyedContainer()
60+
try container.encode(predicate)
61+
repeat try container.encode(each input)
6062
}
6163

6264
public init(from decoder: Decoder) throws {
63-
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Decoding the PredicateEvaluate operator is not yet supported"))
65+
var container = try decoder.unkeyedContainer()
66+
self.predicate = try container.decode(Condition.self)
67+
self.input = (repeat try container.decode((each Input).self))
6468
}
6569
}
6670

Tests/FoundationEssentialsTests/PredicateCodableTests.swift

Lines changed: 74 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -468,64 +468,85 @@ final class PredicateCodableTests: XCTestCase {
468468
let predicate = #Predicate<Int> { _ in
469469
a == a
470470
}
471+
471472

472-
let encoder = JSONEncoder()
473-
var config = PredicateCodableConfiguration.standardConfiguration
474-
config.allowPartialType(A< >.self, identifier: "PredicateCodableTests.A")
475-
XCTAssertThrowsError(try encoder.encode(predicate, configuration: config)) {
476-
XCTAssertTrue(String(describing: $0).contains("type is not allowed because it contains type pack parameters"))
477-
}
478-
479-
let json = """
480-
[
481-
{
482-
"expression" : [
483-
null,
484-
null
485-
],
486-
"structure" : {
487-
"identifier" : "PredicateExpressions.Equal",
488-
"args" : [
489-
{
490-
"identifier" : "PredicateExpressions.Value",
491-
"args" : [
492-
{
493-
"identifier": "PredicateCodableTests.A",
494-
"args": [
495-
"Swift.String",
496-
"Swift.Int"
497-
]
498-
}
499-
]
500-
},
501-
{
502-
"args" : [
503-
{
504-
"identifier": "PredicateCodableTests.A",
505-
"args": [
506-
"Swift.String",
507-
"Swift.Int"
508-
]
509-
}
510-
],
511-
"identifier" : "PredicateExpressions.Value"
512-
}
513-
]
514-
},
515-
"variable" : [
516-
{
517-
"key" : 0
518-
}
519-
]
520-
}
473+
struct CustomConfig : PredicateCodingConfigurationProviding {
474+
static let config = {
475+
var configuration = PredicateCodableConfiguration.standardConfiguration
476+
configuration.allowPartialType(A< >.self, identifier: "PredicateCodableTests.A")
477+
return configuration
478+
}()
479+
}
480+
481+
let decoded = try _encodeDecode(predicate, for: CustomConfig.self)
482+
XCTAssertEqual(try decoded.evaluate(2), try predicate.evaluate(2))
483+
}
484+
485+
func testNestedPredicates() throws {
486+
let predicateA = #Predicate<Object> {
487+
$0.a == 3
488+
}
489+
490+
let predicateB = #Predicate<Object> {
491+
predicateA.evaluate($0) && $0.a > 2
492+
}
493+
494+
let decoded = try _encodeDecode(predicateB, for: StandardConfig.self)
495+
496+
let objects = [
497+
Object(a: 3, b: "abc", c: 0.0, d: 0, e: "c", f: true, g: [1, 3], h: Object2(a: 1, b: "Foo")),
498+
Object(a: 2, b: "abc", c: 0.0, d: 0, e: "c", f: true, g: [1, 3], h: Object2(a: 1, b: "Foo")),
499+
Object(a: 3, b: "abc", c: 0.0, d: 0, e: "c", f: true, g: [1, 3], h: Object2(a: 1, b: "Foo")),
500+
Object(a: 2, b: "abc", c: 0.0, d: 0, e: "c", f: true, g: [1, 3], h: Object2(a: 1, b: "Foo")),
501+
Object(a: 4, b: "abc", c: 0.0, d: 0, e: "c", f: true, g: [1, 3], h: Object2(a: 1, b: "Foo"))
521502
]
522-
"""
523503

524-
let decoder = JSONDecoder()
525-
XCTAssertThrowsError(try decoder.decode(Predicate<Int>.self, from: json.data(using: .utf8)!, configuration: config)) {
526-
XCTAssertTrue(String(describing: $0).contains("type is not allowed because it contains type pack parameters"))
504+
for object in objects {
505+
XCTAssertEqual(try decoded.evaluate(object), try predicateB.evaluate(object), "Evaluation failed to produce equal results for \(object)")
527506
}
528507
}
508+
509+
func testNestedPredicateRestrictedConfiguration() throws {
510+
struct RestrictedBox<each T> : Codable {
511+
let predicate: Predicate<repeat each T>
512+
513+
func encode(to encoder: any Encoder) throws {
514+
var container = encoder.unkeyedContainer()
515+
// Restricted empty configuration
516+
try container.encode(predicate, configuration: PredicateCodableConfiguration())
517+
}
518+
519+
init(_ predicate: Predicate<repeat each T>) {
520+
self.predicate = predicate
521+
}
522+
523+
init(from decoder: any Decoder) throws {
524+
var container = try decoder.unkeyedContainer()
525+
self.predicate = try container.decode(Predicate<repeat each T>.self, configuration: PredicateCodableConfiguration())
526+
}
527+
}
528+
529+
let predicateA = #Predicate<Object> {
530+
$0.a == 3
531+
}
532+
let box = RestrictedBox(predicateA)
533+
534+
let predicateB = #Predicate<Object> {
535+
box.predicate.evaluate($0) && $0.a > 2
536+
}
537+
538+
struct CustomConfig : PredicateCodingConfigurationProviding {
539+
static let config = {
540+
var configuration = PredicateCodableConfiguration.standardConfiguration
541+
configuration.allowKeyPathsForPropertiesProvided(by: PredicateCodableTests.Object.self)
542+
configuration.allowKeyPath(\RestrictedBox<Object>.predicate, identifier: "RestrictedBox.Predicate")
543+
return configuration
544+
}()
545+
}
546+
547+
// Throws an error because the sub-predicate's configuration won't contain anything in the allowlist
548+
XCTAssertThrowsError(try _encodeDecode(predicateB, for: CustomConfig.self))
549+
}
529550
}
530551

531552
#endif // FOUNDATION_FRAMEWORK

0 commit comments

Comments
 (0)