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

WIP: Code Generators #15

Merged
merged 43 commits into from Jan 18, 2020
Merged

WIP: Code Generators #15

merged 43 commits into from Jan 18, 2020

Conversation

klavs
Copy link
Contributor

@klavs klavs commented Oct 16, 2019

Extends the set of code builders available.

ast_builder

Generates an AST representation of a GraphQL Document

data_builder

Generates a typed view on top of data JSON. The generated classes are not supposed to be used directly as that would introduce tight coupling between your GraphQL documents and your application code.

class $ListPokemon {
  const $ListPokemon(this.data);

  final Map<String, dynamic> data;

  List<$ListPokemon$pokemons> get pokemons => (data["pokemons"] as List)
      .map((dynamic e) => $ListPokemon$pokemons(e as Map<String, dynamic>))
      .toList();
}

class $ListPokemon$pokemons {
  const $ListPokemon$pokemons(this.data);

  final Map<String, dynamic> data;

  String get id => data["id"] as String;
  String get name => data["name"] as String;
}

op_builder

Wraps AST and operation name into an Operation.

import 'package:gql_exec/gql_exec.dart';
import 'find_pokemon.ast.gql.dart' as find_pokemon;

const FindPokemon = Operation(
  document: find_pokemon.document,
  operationName: 'FindPokemon',
);

req_builder

Extend Request class to use specific Operation and provide ability to build variables. Builder pattern let's you handle nullable variables correctly (GraphQL spec pretty much requires both undefined and null values).

import 'package:gql_exec/gql_exec.dart' as _i1;
import 'find_pokemon.op.gql.dart' as _i2;

class FindPokemon extends _i1.Request {
  FindPokemon()
      : super(operation: _i2.FindPokemon, variables: <String, dynamic>{});

  set name(String value) => variables["name"] = value;
}

enum_builder

Generates an enum-like class per GraphQL enum type. Defines known enum values to be used in your code, and allows unknown enum values to be used without causing runtime error when handling response data.

class ReleaseType {
  const ReleaseType(this.value);

  final String value;

  static const ReleaseType ALPHA = ReleaseType('ALPHA');

  static const ReleaseType BETA = ReleaseType('BETA');

  static const ReleaseType GAMMA = ReleaseType('GAMMA');

  @override
  int get hashCode => value.hashCode;
  @override
  bool operator ==(Object o) => o is ReleaseType && o.value == value;
}

scalar_builder

Generates a container for a scalar value to be used when viewing the response data and building request variables.

class ID {
  const ID(this.value);

  final String value;

  @override
  int get hashCode => value.hashCode;
  @override
  bool operator ==(Object o) => o is ID && o.value == value;
}

input_builder

Generates an input builder to be used to build request variables.

class MutationInput {
  final Map<String, dynamic> input = <String, dynamic>{};

  set a(String value) => input['a'] = value;
}

schema_builder

Combines enum_builder, input_builder and scalar_builder.

Usage example

final result = await link.request(
  FindPokemon()..name = "Charizard",
);
final data = $FindPokemon(result.data);

final pokemon = data.pokemon;

print("Found ${pokemon.name}");
print("ID: ${pokemon.id}");

final weight = pokemon.weight;
final height = pokemon.height;

print(
  "Weight: ${weight.minimum}${weight.maximum}",
);
print(
  "Height: ${height.minimum}${height.maximum}",
);

@klavs
Copy link
Contributor Author

klavs commented Jan 12, 2020

This is now published as gql_code_gen: v0.2.0-alpha.1 (no support for fragments).
Feel free to try it out and file issues about unexpected behavior.

@klavs
Copy link
Contributor Author

klavs commented Jan 12, 2020

Usage example can be found in this branch. See gql_example_cli

@smkhalsa
Copy link
Member

smkhalsa commented Jan 13, 2020

For fragments, may I suggest that we generate them as a concrete class and then have any queries that use the fragment simply implement that class? That way the fragment class can be used independent of any particular query.

For example, I have a music player app, and I want my player to interact with Song objects that may be fetched using a number of different queries. The above approach would allow the player to accept any Song subclass.

@smkhalsa
Copy link
Member

smkhalsa commented Jan 15, 2020

@klavs I just created a fragment builder that creates a single fragment.gql.dart file with fragment classes for all fragments defined in any .graphql file within the lib folder.

Let me know what you think. Next steps would be to:

@smkhalsa
Copy link
Member

I just realized that my implementation above only works for the top level data class. However, consider a situation like this:

fragment pokemonWeights on Pokemon {
  weight {
    id
    minimum
  }
}

query FindPokemon($name: String!) {
  pokemon(name: $name) {
    ...pokemonWeights
    weight {
      minimum
      maximum
    }
    height {
      minimum
      maximum
    }
  }
}

In this case, not only should $FindPokemon$pokemon implement the $pokemonWeights interface, $FindPokemon$pokemon$weight should also implement the $pokemonWeights$weight interface.

I'll dig into this a bit deeper.

@klavs
Copy link
Contributor Author

klavs commented Jan 16, 2020

To me, practice of reusing fragments promotes and invites overfetching. But I might think about it that way because I use a certain flavour of apollo-inspired practices. It could be that there are other valid ways to think about it.

@smkhalsa, could you adjust the gql_example_cli to include an example of how your idea would look like from the users perspective?

@smkhalsa
Copy link
Member

smkhalsa commented Jan 17, 2020

@klavs, I've just added a flutter example to show how fragments could be used.

Since fragments are all about code reuse, I felt an example using flutter would be more instructive. As you can see, both the AllPokemonScreen and the PokemonDetailScreen use the PokemonCard widget.

Using a fragment, we can define all of the fields that PokemonCard needs in the PokemonCard fragment then include it in the queries from AllPokemonScreen and PokemonDetailScreen. This has several significant advantages:

  1. We colocate the data needed for PokemonCard next the the widget that uses it.
  2. If the data needed for PokemonCard changes, we only need to update it in the PokemonCard fragment, not in the queries that use the fragment. (NOTE: since we haven't yet updated data_builder to automatically include getters for implemented fragments, I've included all the fragment fields in the queries.)
  3. Since we are building the fragment as a Class, our PokemonCard can simply accept a $PokemonCard class, allowing any parent widget to pass in data from any query that implements the fragment.

I hope this is clear. Please let me know your thoughts.

@klavs
Copy link
Contributor Author

klavs commented Jan 17, 2020

@smkhalsa looks good and I would like to encourage you to continue exploring this.

Besides some smaller things, I'm wondering would do nested and multiple levels deep fragments behave.

@klavs
Copy link
Contributor Author

klavs commented Jan 17, 2020

I think the phase one could be to generate fragment classes separately and use them by passing data via the constructor instead of data classes implementing fragment classes.

The reason for it is that from the standpoint of data classes, we have to consider anonymous fragments as well. Also anonymous classes are usually used with unions and interfaces, which are still not considered by the data classes. I'm still not sure how they impact the API. I'm hoping they could guide us in understanding what the API should be.

@smkhalsa
Copy link
Member

I'm wondering would do nested and multiple levels deep fragments behave

@klavs great timing :) I just pushed an update that now adds fragment getters to data classes and also adds recursive fragment support.

@smkhalsa
Copy link
Member

from the standpoint of data classes, we have to consider anonymous fragments as well.

For inline fragments, since we don't know at build time whether the class is of the given type, I just add all inlined fields to the data class. (I still need to test this)

I thought about creating a subclass for each concrete type used in the inline fragment, but I didn't really see the value in doing so (and it would add complexity).

@smkhalsa
Copy link
Member

@klavs If you have time, I'd love to for you to test out any fragment corner cases.

@smkhalsa
Copy link
Member

In order to take full advantage of the request builder and not have to manually instantiate the data class with the returned data, I'd like to recommend we add a parse function to the request class, similar to artemis.

I've taken a first pass at this here. (see example)

This is necessary for me to be able to incorporate the builders into the client I'm working on.

There may be a better way to accomplish this. Please let me know your thoughts.

@klavs
Copy link
Contributor Author

klavs commented Jan 18, 2020

@smkhalsa you can easily create a builder which gives you that kind of API.

Currently:

    final result = await link
        .request(
          FindPokemon()..name = find,
        )
        .first;

    final data = $FindPokemon(result.data);

    final pokemon = data.pokemon;

Proposed API:

    final result = await FindPokemon(
        (b) => b..name = find,
    ).execute(link).first;

    final pokemon = result.data.pokemon;

Now I'm starting to think that instead of or in addition to a req_builder, I create a var_builder so that it is actually independent of the link or client user is using it with.

@klavs klavs merged commit 592bce0 into master Jan 18, 2020
@klavs klavs deleted the schema-gen branch January 18, 2020 10:51
@klavs
Copy link
Contributor Author

klavs commented Jan 18, 2020

I've published these builders as 0.0.X version packages as part some of them are already useful.

@smkhalsa please create PRs with your work on fragments and built request to discuss them separately.

micimize pushed a commit that referenced this pull request Mar 10, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants