Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defining a CustomScalarType which is [String:Any] #3369

Open
solidcell opened this issue Apr 16, 2024 · 5 comments
Open

Defining a CustomScalarType which is [String:Any] #3369

solidcell opened this issue Apr 16, 2024 · 5 comments
Labels
awaiting response question Issues that have a question which should be addressed

Comments

@solidcell
Copy link

Question

I'm migrating from 0.51.2 to 1.10.0 and I'm having a tough time converting a custom scalar to the new system.

We used to have a simple public typealias JSON = [String: Any] which was all we needed. However, now that doesn't suffice since we get errors that JSON needs to conform to SelectionSetEntityValue and OutputTypeConvertible.

This migration is quite the beast, so even if I get this JSON scalar related code to compile, I'm not so confident with how it should behave with errors and casting and such. Any help would be very much appreciated.

@solidcell solidcell added the question Issues that have a question which should be addressed label Apr 16, 2024
@calvincestari
Copy link
Member

Hi @solidcell - can you share the code you've got so far for your custom scalar?

I agree moving to 1.0 is a large migration; have you read the 1.0 migration guide that lays out everything that you may need to change during the migration?

@solidcell
Copy link
Author

solidcell commented Apr 19, 2024

Yea I've already read the migration guide.

The easiest way I've found to get a first pass which compiles, and sort of works is to not use a typealias directly on [String:Any], but to make it a wrapper struct. However that has its disadvantages. Here's what I've got so far:

public struct JSON: CustomScalarType, SelectionSetEntityValue {

    public init(_fieldData: AnyHashable?) {
        guard let _fieldData else {
            fatalError()
        }
        self.value = _fieldData
    }

    public init(_jsonValue value: JSONValue) throws {
        self.value = value
    }
    
    public init(anyHashable: JSONValue) {
        self.value = anyHashable
    }

    public var _fieldData: AnyHashable {
        value
    }

    public var _jsonValue: JSONValue {
        value
    }

    public var _asAnyHashable: AnyHashable {
        value
    }

    public subscript<T>(_ key: String) -> T? {
        guard let dict = value as? [String : Any],
              let casted = dict[key] as? T
        else { fatalError() }
        return casted
    }

    public func compactMap<T>(
        _ transform: ((key: String, value: Any)) throws -> T?
    ) rethrows -> [T] {
        guard let dict = value as? [String : Any]
        else { fatalError() }
        return try dict.compactMap(transform)
    }

    public func compactMapValues<T>(
        _ transform: (Any) throws -> T?
    ) rethrows -> [String : T] {
        guard let dict = value as? [String : Any]
        else { fatalError() }
        return try dict.compactMapValues(transform)
    }

    public var values: Dictionary<String, Any>.Values {
        guard let dict = value as? [String : Any]
        else { fatalError() }
        return dict.values
    }

    public var description: String {
        guard let dict = value as? [String : Any]
        else { fatalError() }
        return dict.description
    }

    private let value: JSONValue
}

I haven't refactored this up yet since I don't like the direction this solution is going. It's enabled me to get the app compiling and running again, but there are a few issues I've noticed so far:

  1. Needing to redefine and delegate methods (compactMap, subscript, etc.) to the wrapped Dictionary is getting out of hand and isn't elegant.
  2. It's a bit awkward that a JSON field from a result is treated like JSON wrapper type, but if it contains nested JSON itself, that nested JSON accessed with subscript won't technically be JSON (the wrapper type), but just a regular AnyHashable([String : Any]). Not the end of the world, but it's awkward that with this solution the types for top-level and nested JSON hashes are different, so you need to be careful about casting and what to expect. And I don't think wrapping it with the type explicitly upon returning in subscript is great either.

I would much prefer to be able to simply work with the underlying [String : Any] type as I had been doing with Apollo 0.x. I'm hoping I can get back to that. However, trying to do that is giving me a whole host of problems. So I'm not sure if it's the better solution in the end or if it's even possible.

Doing this, for instance:

public typealias JSON = [String : Any]

extension JSON: CustomScalarType {}

Is giving me:

'CustomScalarType' requires the types 'Any' and 'any GraphQLOperationVariableValue' be equivalent
Conditional conformance of type 'Dictionary<Key, Value>' to protocol 'CustomScalarType' does not imply conformance to inherited protocol 'AnyScalarType'
Conditional conformance of type 'Dictionary<Key, Value>' to protocol 'CustomScalarType' does not imply conformance to inherited protocol 'OutputTypeConvertible'
Did you mean to explicitly state the conformance with different bounds?
Type 'Dictionary<Key, Value>' does not conform to protocol 'CustomScalarType'

I'm just starting to dive deeper, but something tells me I'm fighting against the current. The types and protocols in Apollo 1.x are a lot to take in, so I'm just trying to wrap my head around it. For instance, the first error: 'CustomScalarType' requires the types 'Any' and 'any GraphQLOperationVariableValue' be equivalent. The compiler isn't being particularly helpful and it's not clear to me how CustomScalarType is making that requirement in the first place. Maybe some extension being defined somewhere on Dictionary which is influencing this conformance.. It's hard to say.

@calvincestari
Copy link
Member

  1. Needing to redefine and delegate methods (compactMap, subscript, etc.) to the wrapped Dictionary is getting out of hand and isn't elegant.

I'm not sure why you need to be defining conformance to SelectionSetEntityValue, that might be compounding the issues. Any as the element is not helping though, it's too broad. What about using AnyHashable instead?

  struct JSON: CustomScalarType, Hashable {
    private let wrapped: [String: AnyHashable]

    init(_jsonValue value: ApolloAPI.JSONValue) throws {
      guard let value = value as? [String: AnyHashable] else { throw JSONDecodingError.wrongType }

      self.wrapped = value
    }
    
    var _jsonValue: ApolloAPI.JSONValue { wrapped }
  }
  1. It's a bit awkward that a JSON field from a result is treated like JSON wrapper type, but if it contains nested JSON itself, that nested JSON accessed with subscript won't technically be JSON (the wrapper type), but just a regular AnyHashable([String : Any]). Not the end of the world, but it's awkward that with this solution the types for top-level and nested JSON hashes are different, so you need to be careful about casting and what to expect. And I don't think wrapping it with the type explicitly upon returning in subscript is great either.

Can you show me how you ideally want to be able to access the custom scalar value? I'm not sure there is any way around having to cast the type out of the subscript when using the value type of Any, and you will always need to be careful because there is no type safety in Any.

Custom scalars are for your code to define and the type safety you get from that is entirely up to what your custom type provides. If you want that type to be generated for you then it needs to be fully defined in the schema so codegen can work with it.

@solidcell
Copy link
Author

If I don't conform to SelectionSetEntityValue, I get this error:
image

Using AnyHashable instead of Any as the value didn't change anything (either better or worse), so I'll leave it as AnyHashable. That was my plan, but I was trying to be as incremental in my migration as possible. So I was leaving it as Any for now until I got everything working again, since that's what it was pre-migration.

I'm not sure there is any way around having to cast the type out of the subscript when using the value type of Any

Ah, misunderstanding here. It's not my intention to get more type safety from Any than I should expect. I was just now trying to type up exactly what I was after, but I was on a wrong path. So nevermind my #2.

In the end, it would just be nice to just have #1. A typealias would allow me to stop having this need to delegate everything and just simply have the type I'm really after. Something like what's possible for Date:

public typealias Date = Foundation.Date

extension Foundation.Date: CustomScalarType {
    public init (_jsonValue value: JSONValue) throws {
        guard let string = value as? String else {
            throw JSONDecodingError.couldNotConvert(value: value, to: String.self)
        }

        guard let date = Date(jsonString: string) else {
            throw JSONDecodingError.couldNotConvert(value: string, to: Date.self)
        }

        self = date
    }

...

But using a typealias gives me:

'CustomScalarType' requires the types 'AnyHashable' and 'any GraphQLOperationVariableValue' be equivalent
Conditional conformance of type 'Dictionary<Key, Value>' to protocol 'CustomScalarType' does not imply conformance to inherited protocol 'AnyScalarType'
Conditional conformance of type 'Dictionary<Key, Value>' to protocol 'CustomScalarType' does not imply conformance to inherited protocol 'OutputTypeConvertible'
Did you mean to explicitly state the conformance with different bounds?
Type 'Dictionary<Key, Value>' does not conform to protocol 'CustomScalarType'

@calvincestari
Copy link
Member

@solidcell - have you managed to make any progress on this or is it still an issue? I'm wondering if the other issue we've worked on been able to help you with this one too?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting response question Issues that have a question which should be addressed
Projects
None yet
Development

No branches or pull requests

2 participants