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

AnyGraphQLQuery? #65

Closed
nuke-dash opened this issue Mar 3, 2017 · 10 comments
Closed

AnyGraphQLQuery? #65

nuke-dash opened this issue Mar 3, 2017 · 10 comments

Comments

@nuke-dash
Copy link

Is it possible to create an AnyGraphQLQuery class?
I'm having some troubles/difficulties that you cannot assign a GraphQLQuery to a variable without knowing its concrete type.

So I tried building a type erasure but that doesn't work because the protocol contains a static var static var operationDefinition
Is it possible to not make it a static var and just a regular var?
Because it seems to be only used in HTTPNetworkTransport
And there is doesn't seem to be really necessary for it to be a static variable.

let body: GraphQLMap = ["query": type(of: operation).queryDocument, "variables": operation.variables]
@fruitcoder
Copy link

I also tried type erasure to create an AnyGraphQLQuery but failed due to the same restriction. You obviously cannot stub the static var

@martijnwalraven
Copy link
Contributor

I'd love to help, but I'd like to better understand your use case to see if there might be a better way to achieve what you want. What would you want use a AnyGraphQLQuery for? It seems its use would be limited because it would still need to be parameterized by a specific Data. (Which isn't a problem for something like AnyIterator because the type is general, but here Data is bound to a specific query anyway.)

I've run into the same issue myself in working on upcoming support for dynamic operations and a more flexible network layer. One solution I've been thinking about is to create a GraphQLRequest protocol with operationDefinition etc. and use that in the network layer. Because GraphQLOperation would adopt the protocol, that means you can then assign operations to a variable of type GraphQLRequest (or collections of that type, etc.).

The default implementation of the protocol on GraphQLOperation could delegate operationDefinition etc. to the static version. I'd like to keep information that isn't specific to an operation use static to avoid reinitializing it for each individual query/mutation. (That is more important with some of the new information we're adding for the revised execution algorithm, see #63).

@nuke-dash
Copy link
Author

Thank for the comment.

I'll try to explain my use case.
I'm using ReactiveReSwift in other words redux-flow with observables.
In it I want to create a MiddleWare that handles all FetchAction and will fire a success or failure actions depending on what happened.
This way the View layer only needs to fire that action to the Store.
The results will then be handled by the reducers.
The problem I'm currently having is that in order to create a MiddleWare you need to add closures to a MiddleWare class which cannot be parameterized.
Thus I would need to add a case for every query I have in my app.
In order to avoid adding a case for every query AnyGraphQLQuery would be very welcome.
My code looks something like this.

Middleware<MainState>()
    .sideEffect { _, dispatch, action in
        switch action {
        case let action as FetchAction<VerticalTvGuideQuery>:
                    GraphQLMiddleWare.fetch(mediaProvider: mediaProvider, dispatch: dispatch, query: action.query, disposeBag: action.disposeBag)
        default:
            break
        }
    }
}

private static func fetch<Q: GraphQLQuery>(mediaProvider: MediaProvider, dispatch: @escaping Middleware<MainState>.DispatchFunction, query: Q, disposeBag: DisposeBag) {
    mediaProvider.fetch(query).subscribe(
        onNext: { data in
            let finishAction = FetchFinishAction<Q>(disposeBag:disposeBag, data: data)
            dispatch(finishAction)
    },
        onError: { error in
            let failureAction = FetchFailureAction(disposeBag:disposeBag, error: error)
            dispatch(failureAction)
    }) // onComplete or onDisposed
        .addDisposableTo(disposeBag)
 }

In the View layer something like this happens.

let fetchAction = FetchAction(disposeBag: self.disposeBag, query: VerticalTvGuideQuery())
store.dispatch(fetchAction)

The success and failure actions will be handles in the Reducer that is responsible for the screen that fired the FetchAction.

@martijnwalraven
Copy link
Contributor

martijnwalraven commented Mar 6, 2017

I don't think AnyGraphQLQuery would help here, because it would still have to be parameterized by the Data type from the particular query it wraps. So in this case, you'd have AnyGraphQLQuery< VerticalTvGuideQuery.Data> instead of VerticalTvGuideQuery, and you would run into the same issues.

This is similar to how other type erased wrappers like AnyIterator work. Although it does hide the specific iterator type, it is still parameterized by the element type. So AnyIterator<String> is different from AnyIterator<Int>, and you can't refer to an AnyIterator without specifying the type.

If you want access to the typed data, the query-specific type information will have to come from somewhere. You may be able to define your own non-generic wrapper, but then you'll have to force cast the result somewhere else in your code in order to use it:

struct FetchQueryAction {
  let fetch: (ApolloClient, @escaping (Any?, Error?) -> Void) -> Void
  
  init<Query: GraphQLQuery>(query: Query) {
    fetch = { client, completionHandler in
      client.fetch(query: query) { result, error in
        completionHandler(result, error)
      }
    }
  }
}

let action = FetchQueryAction(query: HeroNameQuery())
action.fetch(client) { result, error in
  (result as! GraphQLResult<HeroNameQuery.Data>).data?.hero?.name
}

@nuke-dash
Copy link
Author

You are correct as far as I understand it.
But I'm still trying to wrap my head around it so excuse me if I'm asking the obvious.

To come back to the original reason why I wanted AnyGraphQLQuery: how I can reduce the switch I'm currently using which has (will have) a case for every different query?
e.g.

switch action {
        case let action as FetchAction<Query1>:
                    GraphQLMiddleWare.fetch(mediaProvider: mediaProvider, dispatch: dispatch, query: action.query, disposeBag: action.disposeBag)
        case let action as FetchAction<Query2>:
                    GraphQLMiddleWare.fetch(mediaProvider: mediaProvider, dispatch: dispatch, query: action.query, disposeBag: action.disposeBag)
        case let action as FetchAction<Query3>:
                    GraphQLMiddleWare.fetch(mediaProvider: mediaProvider, dispatch: dispatch, query: action.query, disposeBag: action.disposeBag)
        default:
            break
        }

Also that the results needs to be cast somewhere else is what I'm currently doing with Reducers.
As they will process the finish or failure result.
For example:

static func createViewModelReducer(mediaProvider: MediaProvider) -> Reducer<VerticalTVGuideState> {
    return { action, state in
        switch action {
        case let action as FetchFinishAction<VerticalTvGuideQuery>:
            var state = state
            state.queryData = Box(action.data)
            state.error = nil
            return state
        case let action as FetchFailureAction:
            var state = state
            state.queryData = nil
            state.error = Box(action.error)
            return state
        default:
            return state
        }
    }
}

@martijnwalraven
Copy link
Contributor

This ends up feeling like a bit of a hack, but I think I got something working...

  let fetch: (ApolloClient, @escaping (Any?, Error?) -> Void) -> Void
  let resultType: Any.Type
  
  init<Query: GraphQLQuery>(query: Query) {
    fetch = { client, completionHandler in
      client.fetch(query: query) { result, error in
        completionHandler(result, error)
      }
    }
    resultType = GraphQLResult<Query.Data>.self
  }
}

let action = FetchQueryAction(query: HeroNameQuery())

action.fetch(client) { result, error in  
  switch action.resultType {
  case is GraphQLResult<HeroNameQuery.Data>.Type:
    let hero = (result as! GraphQLResult<HeroNameQuery.Data>).data?.hero
    hero?.name
  default:
    fatalError("Unknown result type")
  }
}

@martijnwalraven
Copy link
Contributor

Then again, if you have to hard code your queries anyway, a cleaner solution might be to use an enum:

enum FetchQueryAction {
  case heroName(HeroNameQuery)
  case heroAndFriendsNames(HeroAndFriendsNamesQuery)
}

@designatednerd
Copy link
Contributor

@Nuke- is this still an issue you need help with? I'm trying to clean up old issues and if there's a particular solution you used here, it'd probably be helpful to share with the community before I close this out. Thanks!

@nuke-dash
Copy link
Author

@designatednerd no longer an issue. I settled on using a giant switch for all my queries and mutation.

@designatednerd
Copy link
Contributor

switch ftw! 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants