Replies: 3 comments 3 replies
-
Ignore this, I was tired. |
Beta Was this translation helpful? Give feedback.
-
The first restriction why you currently need to mock values is because all To change this we have to pull apart the way we currently init Selection. I think the best way to go would be to use swifts function builders. But even if we remove mocking from the library side, then we still need an initialized object to set values on. So the second restriction is that we have no other way of initializing an object currently. This is a working syntax using both a property wrapper and function builder that doesn't need mock values. struct Launch: SGWrapped {
@SGValue var id: String
@SGValue var mission: String
@SGValue var launchpad: Launchpad?
}
struct Launchpad: SGWrapped {
@SGValue var name: String
}
let launch = Selection<Launch, Objects.Launch> {
try $0.$id << $1.id()
try $0.$mission << $1.mission()
try $0.$launchpad << $1.launchpad(Selection {
try $0.$name << $1.name()
}.nullable)
}
let upcomingLaunches = Selection<[Launch], Operations.Query> {
try $0.upcomingLaunches(launch.list)
}
SG.send(upcomingLaunches, to: url) { result in
// ...
} This works by pulling apart each line in the Selection with a function builder that returns Errors that are thrown: infix operator <<
public func << <V>(lhs: SGValue<V>, from: @autoclosure @escaping () throws -> V) -> Error? {
do {
lhs.box.value = try from()
return nil
} catch {
return error
}
}
enum SelectionFunctionBuilderError: Error {
case errorsInFunctionBuilderBlock([Error])
}
extension Selection where Type: SGWrapped {
@_functionBuilder
public struct SelectionFunctionBuilder {
public static func buildBlock(_ errors: Error?...) -> [Error?] {
Array(errors)
}
}
public init(@SelectionFunctionBuilder _ makeBlocks: @escaping (inout Type, SelectionSet<Type, TypeLock>) throws -> [Error?]) {
self.init { (selection: SelectionSet<Type, TypeLock>) -> Type in
var wrapped = Type()
let errors = try makeBlocks(&wrapped, selection).compactMap { $0 }
if case .fetching = selection.response {
// We are only mocking so we don't need to throw errors here
// But since there is still mocked values we can throw right away
// instead of after the result comes back:
try wrapped.throwIfWrappedValueIsMissing()
// When we no longer mock I advise checking if the amount of
// mock errors thrown is equal to the amount of wrapped properties on Type
} else {
if !errors.isEmpty {
// Have to force a crash right now to show the error because Result.swift#25 hides all errors as badpayload
try! { throw SelectionFunctionBuilderError.errorsInFunctionBuilderBlock(errors) }()
}
// Have to force a crash right now to show the error because Result.swift#25 hides all errors as badpayload
try! wrapped.throwIfWrappedValueIsMissing()
}
return wrapped
}
}
} And wrapping the properties in a wrapper that doesn't need to be set on init but will crash when it's not set. We then protect against the crash by throwing an Error in the Selection block (shown above). public protocol SGWrapped {
/// Initialise the properties that aren't wrapped by @SGValue
init()
}
extension SGWrapped {
internal func throwIfWrappedValueIsMissing() throws {
var optionalMirror: Mirror? = Mirror(reflecting: self)
while let mirror = optionalMirror {
for child in mirror.children {
if let sgValue = child.value as? SGValueProtocol {
guard sgValue.didChangeValue else {
throw SGWrappedError.didNotSetValueOf(child.label!)
}
}
}
optionalMirror = mirror.superclassMirror
}
}
}
public enum SGWrappedError: Error {
case didNotSetValueOf(String)
}
protocol SGValueProtocol {
var didChangeValue: Bool { get }
}
@propertyWrapper public struct SGValue<Value>: SGValueProtocol {
public var wrappedValue: Value {
get { return box.value ?? { fatalError("Value was not initialised, don't try to init this object manually") }() }
set { box = StrongBox(newValue) } // Create new holder so struct mutates
}
internal var didChangeValue: Bool { box.didChange }
internal var box = StrongBox<Value?>(nil)
public init() {}
public init(wrappedValue: Value) {
box.value = wrappedValue
}
public var projectedValue: Self { self }
}
internal class StrongBox<Value> {
var value: Value { didSet { didChange = true } }
var didChange = false
init(_ value: Value) {
self.value = value
}
} |
Beta Was this translation helpful? Give feedback.
-
@Amzd thank you for taking the time to investigate this! I am curious about the limits of function builders in selector functions. We want decoders/selectors to be functions to allow arbitrary manipulation of data. For example, we have an object type in the backend that is better represented by an enum structure on the frontend. Having a decoder-function allows us to represent our view-model better. In a way, our application-model becomes decoupled from our server-model/schema, and that proves invaluable to us. I've considered having property wrappers but decided to go with functions for the reasons above. I think there exist some other GraphQL libraries for Swift that promise very similar functionality. I don't think having mock values is a pressing issue. It's just one up for discussion. Do you think we could somehow fit your suggestions into this context? My first instinct was to return thunks from selections (instead of structure instances), but that makes the selection syntax very cumbersome. What do you think? |
Beta Was this translation helpful? Give feedback.
-
It feels like there should be a way to make a selection without the need for mocking return values. The major issue right now is that we have to run the code inside the functions to get the selection. I want to explore the possibility of turning it around so that we may omit the mocking step.
Beta Was this translation helpful? Give feedback.
All reactions