Skip to content

Commit bf4ce32

Browse files
committed
Support for optional expressions
1 parent 8f2a823 commit bf4ce32

18 files changed

Lines changed: 568 additions & 310 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// AnyExpression.swift
3+
// PredicateView
4+
//
5+
// Created by Phil Zakharchenko on 2/25/24.
6+
//
7+
8+
import Foundation
9+
10+
public typealias ExpressionCompatible = Codable & Hashable
11+
12+
public struct AnyExpression<Root>: Identifiable {
13+
public var wrappedValue: any ValueExpression<Root>
14+
public var id: UUID { wrappedValue.id }
15+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// CompoundExpression.swift
3+
//
4+
//
5+
// Created by Phil Zakharchenko on 4/20/24.
6+
//
7+
8+
import Foundation
9+
10+
// MARK: - CompoundExpression
11+
12+
public protocol CompoundExpression<Root>: Expression {
13+
associatedtype Root
14+
15+
var children: [any Expression<Root>] { get set }
16+
var attribute: CompoundAttribute<Self> { get set }
17+
}
18+
19+
extension CompoundExpression {
20+
public var currentValue: AnyHashable { CompoundExpressionValue(self) }
21+
}
22+
23+
// MARK: - CompoundExpressionValue
24+
25+
struct CompoundExpressionValue<Expr>: Hashable where Expr: CompoundExpression {
26+
var rootAttribute: CompoundAttribute<Expr>
27+
var childAttributes: [AnyHashable]
28+
29+
init(_ expression: Expr) {
30+
self.rootAttribute = expression.attribute
31+
self.childAttributes = expression.children.map(\.currentValue)
32+
}
33+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// ContentExpression.swift
3+
//
4+
//
5+
// Created by Phil Zakharchenko on 4/20/24.
6+
//
7+
8+
import SwiftUI
9+
10+
// MARK: - ContentExpression
11+
12+
public protocol ContentExpression<Root>: SimpleExpression {
13+
associatedtype ExprView = ContentExpressionView<Root, Self>
14+
associatedtype Result: View
15+
16+
@ViewBuilder
17+
static func makeContentView(_ value: Binding<Value>) -> Result
18+
}
19+
20+
// MARK: - StandardValueExpressionView
21+
22+
public struct ContentExpressionView<Root, Expr: ContentExpression<Root>>: ExpressionView {
23+
public var expression: Binding<Expr>
24+
public init(expression: Binding<Expr>) {
25+
self.expression = expression
26+
}
27+
28+
@ViewBuilder
29+
public var body: some View {
30+
@Binding(projectedValue: self.expression) var expression: Expr
31+
32+
TokenView(Root.self, header: {
33+
Text("\(expression.title) \(expression.attribute.operator.rawValue)")
34+
}, content: {
35+
Expr.makeContentView($expression.attribute.value)
36+
}, menu: {
37+
Expr.operatorPickerView(using: $expression.attribute.operator)
38+
})
39+
}
40+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// Expression+View.swift
3+
//
4+
//
5+
// Created by Phil Zakharchenko on 4/20/24.
6+
//
7+
8+
import SwiftUI
9+
10+
extension Expression {
11+
@ViewBuilder
12+
public static func operatorPickerView<T: Hashable>(
13+
using operation: Binding<T>
14+
) -> some View {
15+
operatorPickerView(using: operation) { option in
16+
Text(option.rawValue)
17+
.tag(option as! T)
18+
}
19+
}
20+
21+
@ViewBuilder
22+
public static func operatorPickerView<T: Hashable>(
23+
using operation: Binding<T>,
24+
@ViewBuilder itemProvider: @escaping (Operator) -> some View
25+
) -> some View {
26+
Picker("Operator", selection: operation) {
27+
ForEach(Operator.allCases, id: \.self) { option in
28+
itemProvider(option)
29+
.pickerStyle(.menu)
30+
}
31+
}
32+
.pickerStyle(.inline)
33+
}
34+
35+
public static func makeView(for expression: Binding<Self>) -> some ExpressionView {
36+
makeView(for: expression, parent: nil)
37+
}
38+
39+
public static func makeView(for expression: Binding<Self>, parent: Binding<LogicalExpression<Root>>?) -> some ExpressionView {
40+
ExprView(expression: expression, parent: parent)
41+
}
42+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// Expression.swift
3+
// PredicateView
4+
//
5+
// Created by Phil Zakharchenko on 2/25/24.
6+
//
7+
8+
import SwiftUI
9+
10+
// MARK: - Expression
11+
12+
public protocol Expression<Root>: Identifiable {
13+
associatedtype Root
14+
associatedtype ExprView: ExpressionView<Self>
15+
associatedtype Operator: CaseIterable, Hashable, RawRepresentable where Operator.RawValue == String, Operator.AllCases: RandomAccessCollection, Operator.AllCases.Index == Int
16+
17+
var id: UUID { get set }
18+
var currentValue: AnyHashable { get }
19+
20+
func buildPredicate(using input: PredicateExpressions.Variable<Root>) -> (any StandardPredicateExpression<Bool>)?
21+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// ValueExpression.swift
3+
//
4+
//
5+
// Created by Phil Zakharchenko on 4/20/24.
6+
//
7+
8+
import Foundation
9+
10+
// MARK: - ValueExpression
11+
12+
public protocol ValueExpression<Root>: Expression {
13+
associatedtype Root
14+
associatedtype Value: Hashable
15+
16+
var keyPath: KeyPath<Root, Value> { get }
17+
var title: String { get }
18+
}
19+
20+
// MARK: - SimpleExpression
21+
22+
public protocol SimpleExpression<Root>: ValueExpression {
23+
static var defaultAttribute: ExpressionAttribute<Self> { get }
24+
var attribute: ExpressionAttribute<Self> { get set }
25+
26+
static func buildPredicate<V>(
27+
for variable: V,
28+
using value: Value,
29+
operation: Operator
30+
) -> (any StandardPredicateExpression<Bool>)? where V: StandardPredicateExpression<Value>
31+
}
32+
33+
extension SimpleExpression {
34+
public var currentValue: AnyHashable { attribute }
35+
36+
public func buildPredicate(
37+
using input: PredicateExpressions.Variable<Root>
38+
) -> (any StandardPredicateExpression<Bool>)? {
39+
let keyPath = PredicateExpressions.KeyPath(root: input, keyPath: keyPath)
40+
return Self.buildPredicate(for: keyPath, using: attribute.value, operation: attribute.operator)
41+
}
42+
}
43+
44+
// MARK: - ExpressionWrapper
45+
46+
public protocol ExpressionWrapper<Root, WrappedExpression>: ValueExpression {
47+
associatedtype WrappedExpression: SimpleExpression<Root>
48+
49+
var attribute: ExpressionAttribute<WrappedExpression>? { get set }
50+
var `operator`: Operator { get }
51+
52+
static func buildPredicate<V>(
53+
for variable: V,
54+
using value: WrappedExpression.Value?,
55+
wrappedOperation: WrappedExpression.Operator?,
56+
operation: Operator
57+
) -> (any StandardPredicateExpression<Bool>)? where V: StandardPredicateExpression<Value>
58+
}
59+
60+
extension ExpressionWrapper {
61+
public var currentValue: AnyHashable {
62+
ExpressionWrapperValue(op: self.operator, attribute: self.attribute)
63+
}
64+
65+
public func buildPredicate(
66+
using input: PredicateExpressions.Variable<Root>
67+
) -> (any StandardPredicateExpression<Bool>)? {
68+
let keyPath = PredicateExpressions.KeyPath(root: input, keyPath: keyPath)
69+
let wrappedValue = attribute?.value
70+
let op = attribute?.operator
71+
return Self.buildPredicate(for: keyPath, using: wrappedValue, wrappedOperation: op, operation: `operator`)
72+
}
73+
}
74+
75+
private struct ExpressionWrapperValue: Hashable {
76+
var op: AnyHashable
77+
var attribute: AnyHashable?
78+
}

Sources/PredicateView/Expressions/BoolExpression.swift

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,17 @@
77

88
import SwiftUI
99

10-
struct BoolExpression<Root>: SimpleExpression {
11-
typealias ExprView = BoolExpressionView<Root>
10+
extension AnyExpression {
11+
public init(keyPath: KeyPath<Root, Bool>, title: String) {
12+
self.wrappedValue = BoolExpression(keyPath: keyPath, title: title)
13+
}
1214

15+
public init(keyPath: KeyPath<Root, Bool?>, title: String) {
16+
self.wrappedValue = OptionalExpression<Root, BoolExpression>(keyPath: keyPath, title: title)
17+
}
18+
}
19+
20+
struct BoolExpression<Root>: ContentExpression {
1321
enum Operator: String, CaseIterable {
1422
case `is` = "is"
1523
case isNot = "is not"
@@ -29,47 +37,40 @@ struct BoolExpression<Root>: SimpleExpression {
2937
}
3038
}
3139

40+
static var defaultAttribute: ExpressionAttribute<Self> { .init(operator: .is, value: true) }
41+
3242
var id = UUID()
3343
let keyPath: KeyPath<Root, Bool>
3444
let title: String
35-
var attribute: ExpressionAttribute<Self> = .init(operator: .is, value: true)
45+
var attribute: ExpressionAttribute<Self> = Self.defaultAttribute
3646

37-
func buildPredicate(using input: PredicateExpressions.Variable<Root>) -> (any StandardPredicateExpression<Bool>)? {
38-
switch attribute.operator {
47+
static func buildPredicate<V>(
48+
for variable: V,
49+
using value: Value,
50+
operation: Operator
51+
) -> (any StandardPredicateExpression<Bool>)? where V: StandardPredicateExpression<Value> {
52+
switch operation {
3953
case .is:
4054
PredicateExpressions.Equal(
41-
lhs: PredicateExpressions.KeyPath(root: input, keyPath: keyPath),
42-
rhs: PredicateExpressions.Value(attribute.value)
55+
lhs: variable,
56+
rhs: PredicateExpressions.Value(value)
4357
)
4458
case .isNot:
4559
PredicateExpressions.NotEqual(
46-
lhs: PredicateExpressions.KeyPath(root: input, keyPath: keyPath),
47-
rhs: PredicateExpressions.Value(attribute.value)
60+
lhs: variable,
61+
rhs: PredicateExpressions.Value(value)
4862
)
4963
}
5064
}
51-
}
52-
53-
struct BoolExpressionView<Root>: ExpressionView {
54-
typealias Expression = BoolExpression<Root>
55-
56-
@Binding var expression: Expression
5765

58-
var body: some View {
59-
TokenView(Root.self, header: {
60-
Text("\(expression.title) \(expression.attribute.operator.rawValue)")
61-
}, content: {
62-
Picker("Value", selection: $expression.attribute.value) {
63-
ForEach(Expression.Operator.allCases, id: \.self) {
64-
Text($0.label)
65-
.tag($0.associatedValue)
66-
}
66+
static func makeContentView(_ value: Binding<Bool>) -> some View {
67+
Picker("Value", selection: value) {
68+
ForEach(Operator.allCases, id: \.self) { expression in
69+
Text(expression.label)
70+
.tag(expression.associatedValue)
6771
}
68-
.pickerStyle(.segmented)
69-
.labelsHidden()
70-
}, menu: {
71-
expression.operatorPickerView(using: $expression.attribute)
72-
})
72+
}
73+
.pickerStyle(.segmented)
74+
.labelsHidden()
7375
}
7476
}
75-

0 commit comments

Comments
 (0)