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

[rust] Better support for abstract types (unions and interfaces) #3280

Open
wants to merge 50 commits into
base: main
Choose a base branch
from

Conversation

MaartenStaa
Copy link
Contributor

This is the Rust equivalent of #3178. What I have added compared to that is I made the future proofness of enums and abstract types optional and configurable.

Since the feedback there was positive but the main objection was that the old Relay compiler is basically in support mode until the Rust version is released, I ported this.

Copy paste from the other PR: Basically I have included some changes to support more accurate type output, especially regarding union types and type names.

This should also fix the case where duplicated __typename property definitions are generated, depending on where in the query or fragment the __typename property is requested.

Please let me know what you think! I'm personally not that fond of the current setup I have with testing different combinations of future proof enums/abstract types and how this is captured in the fixtures, maybe someone else has a better idea?

/cc @kassens @josephsavona @tyao1

…t relay-compiler-language-typescript package output.
…ture proof abstract types and future proof enums.
Copy link
Contributor

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kassens has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

Copy link
Member

@kassens kassens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments on parts. There's a bit work left here to make it possible to merge. We have a huge existing codebase, so this affects a lot of generated files internally that break them. We might have to make the advanced code generation behind config options.

I'm unsure about the combinational explosion in the test outputs. What do you think of generating the vast majority of the tests only in 1 configuration option and then having a few tests that test the differences the options produce? A way I can think of to do that would be either to create additional fixture folders or a thing we did in other places is adding a "header" with options to the file, i.e.

{
  futureProofTypename: false
}
%%%
query Test {
  # ...
}

then in the fixture transform function, if the file contains %%% split on that and use

let typegen_options = serde_json::from_string(header);
``
to parse the header as json.

(if you have questions for any of this, feel free to reach out!)

compiler/crates/relay-typegen/src/flow.rs Outdated Show resolved Hide resolved
compiler/crates/relay-typegen/src/lib.rs Outdated Show resolved Hide resolved
compiler/crates/relay-typegen/src/lib.rs Outdated Show resolved Hide resolved
@MaartenStaa
Copy link
Contributor Author

Thanks for the feedback @kassens! Was a bit busy lately so didn't get around earlier to make changes based on your comments. Please take a look when you have time, and happy holidays :)

Maarten Staa added 3 commits February 14, 2022 14:56
This happened when there was a combination between a concrete type with
a selection that overlapped with selections on an interface, that is
also implemented by the concrete type. In such cases, the base selections
are added to the selections of the concrete type, but that could result
in duplicates. Now the selections are merged instead.
For example, allow excluding `Node`, to avoid super long lists of
concrete types.
@MaartenStaa
Copy link
Contributor Author

MaartenStaa commented Feb 14, 2022

Thanks for getting back to me @alunyov! I've moved to a different apartment the other weekend, so didn't have much time in the past weeks to dig deeper into where the code stands. Looking at the old discussion over at #3178, I think the feedback there can be summarized as follows:

  • __typename: "%other" is intentional, in case the client is outdated in relation to the server. This has been addressed by making the future proofness opt-in/opt-out.
  • Rollout is a breaking change. I believe this depends on the default value of the two future proofness config settings (for enums and for abstract types). The current default is for future proofness to be enabled (same as the old compiler), so I think this resolves the breaking change. Having said that, I have not double-checked this (comparing output between the old and the new compiler).
  • @jacob-indieocean mentioned an issue where a type name may not be resolved correctly. I have not yet looked into this, but will see if I can add a test here based on his example.
    edit: I could not reproduce this one.

Have I missed anything?

Regarding your own points, I have dug into the issue with the duplicated address field in the typename_on_union test (the username field as was actually also being duplicated) and found the cause, so this has been resolved. I have also introduced a new option to exclude certain types from the listing of all implementations, which I believe takes care of your concern regarding the Node interface. I'm not very happy with the name of the option though (exclude_from_typename_unions), so I'm open to suggestions.

If a rollout plan has been discussed, I am not aware of it.

@MaartenStaa MaartenStaa requested a review from alunyov February 14, 2022 20:04
@alunyov
Copy link
Contributor

alunyov commented Feb 16, 2022

Thanks @MaartenStaa I'll try to import this for internal review.

@facebook-github-bot
Copy link
Contributor

@alunyov has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@rbalicki2
Copy link
Contributor

rbalicki2 commented Feb 16, 2022

Hi @MaartenStaa — thanks for the awesome work on this diff! I'm really excited about this. This will improve the ergonomics of some work that I'm doing.

I'm sorry that I didn't review this sooner, and have different feedback than @kassens et al.

First, the diff changes the order of generated fields and unions. This is going to make this very hard to land internally. Can you figure out a way to keep the order the same, or else could you split this into two PRs, one of which alphabetizes the order of fields, unions, etc., and the other which is this PR? Alternatively, I could work on alphabetizing fields internally, since I am planning on making that change myself, anyway.

Second, I think this shouldn't be set by global configuration. Let's look for the presence of a schema directive on the type/enum to determine whether to make the type/enum future safe.

A sketch of how this might work is:

  • Add directive Closed on INTERFACE | UNION | ENUM in relay-extensions.graphql. (Feel free to think of a better name. Maybe NotExtendable?)
  • Then (e.g. for testing and for fixture tests), add that directive to some types in testschema.graphql.
  • Then, that directive should be accessible as union_or_enum_or_interface.directives.named(string_key) or the like

How does that sound?

Once we agree on the contours of the diff, I can review the implementation in more detail.

@rbalicki2
Copy link
Contributor

Okay, having thought about this diff some more, the typegen config is a good place to put a global default for the future-proofed-ness of these enums.

If there is a default, the type-level directives should be local overrides, in which maybe a good directive + parameter would be @futureProof(isFutureProof: bool)? Again, I'm not sure what a good name for this would be.

Anyway, I'll go ahead and review the implementation of this diff!

@MaartenStaa
Copy link
Contributor Author

Thanks @rbalicki2! Curious to hear what you think of the implementation. I'll try to look into the field ordering in the coming days.

So for the @futureProof directive, I just want to make sure I understand the use case correctly. The global would be set to future proof, and then you could add the directive in your Relay extensions to disable future proofness for client only types, correct? Or can you also add directives to server types/unions/interfaces?
Continuing that thought, does the reverse (no future proofness as the global setting, and enabling it for a few client side types) even make sense? Or to take it a step further, is future proofness ever really needed for client only types?

Looking forward to hearing your thoughts. I just want to make sure I understand which direction you'd like to go in before starting on an implementation.

@rbalicki2
Copy link
Contributor

I'm envisioning a global future-proofness setting, set e.g. in the typegen config. Then, developers would override that setting for individual types in their schema file.

Yeah, you're right — client only types should not be future proofed, ideally.

@rbalicki2
Copy link
Contributor

Hi @MaartenStaa , I started reviewing your diff. This is a lot! Thank you so much for taking the plunge. I'm sorry that you're dealing with relay-typegen/src/lib, and in particular, with the to_babel functions. They make no sense to begin with, which makes modifying them hard, and makes reviewing changes to them hard.

Anyway, so, because it's so hard to reason about this file, I think it'd be best if we paired so that I could understand exactly what's going on in your changes.

Can you DM me on Twitter? @statisticsftw or DM me on the GraphQL discord?

That being said, this needs to be broken up into smaller diffs. Here are some ideas for landing this in discrete chunks:

  • Trivial renames, e.g. "to_babel" to "to_ast", renaming types to concrete types, etc. should go in a separate diff
  • We should add the AST intersection variant in a separate diff. In this diff, lets modify the printing for AST::Union and AST::Intersection to be wrapped in parentheses if there is more than one, and remove the parentheses wrapping from write_nullable (and anywhere else).
  • We need to alphabetize everything in a separate diff, and roll that out. It's impossible to reason about why things are ordered how they are right now, and (in OSS) you don't have access to all of the generated files, so you won't find all the weird edge cases :'(
  • It seems (but I can't tell!) that the work to make enums non-future-proof is pretty separate from the work to make unions and intersections non-future-proof. Could those be split into separate diffs?

I think if we divide it up like that, it'll be a lot easier to review the meaty final diffs.

rbalicki2 and others added 4 commits February 24, 2022 21:30
Differential Revision: D34470327

fbshipit-source-id: 561dcf8b8763fb05261a33dbbb5251140f3715ef
Maarten Staa added 5 commits March 2, 2022 15:42
…bstract-types

This removes the intersection type, so we also simplify the output, even
if exact objects aren't supported (i.e. for TypeScript). A generated
type of

intersection(union(type_a, type_b), shared_props)

is now instead generated as

union(type_a_plus_shared_props, type_b_plus_shared_props)

This also means the output between Flow and TypeScript is now following
basically the same structure, with the main difference being that Flow
supports exact objects.
facebook-github-bot pushed a commit that referenced this pull request Mar 11, 2022
Summary:
# This diff stack

* This diff stack will sort the following items in generated graphql types:
  * Sorting of Inexact and Exact object keys
  * Sorting of fragment names in the right hand side of `$fragmentSpreads: Foo$fragmentType`
  * Sorting of union types
  * Addition of parentheses around union types
* Sorting these is controlled by a rollout parameter, so we can change all the files in www in parts.

# This diff

* Sorts the props of Exact and InexactObjects (if the config option is enabled, or omitted)
* Sorts props in the following order:
```
enum PropSortOrder {
    Typename,
    ClientId,
    KeyValuePair,
    GetterSetterPair,
    ObjectSpread,
    FragmentSpread,
}
```
* Sort props at the creation time of Exact and Inexact objects, not at write time
* Why sort things? There are a variety of situations in which the ordering of fields in the emitted output is arbitrary. For example, the order of fields in a linked field with inline fragments that gets merged into a linked field with optional keys is arbitrary. A developer might refactor relay-typegen to be more efficient but in the process change that arbitrary order. This change will make that refactor easier to land.
* Within a Prop, sort by the ASTStringKey sort order of the prop name (either lefthand side or the spread value).
* Also, derive Ord, Eq, PartialOrd and PartialEq for AST and related items.

# Files to review

* flow.rs and typescript.rs -> the only changes are to tests, you can safely ignore these files
* lib.rs -> uses the new APIs. Mostly, this means passing `self.should_sort_typegen_items` whenever an `InexactObject` or `ExactObject` is created
* writer.rs -> impl Ord in a custom way for Prop, sort props when creating InexactObjects and ExactObjects
* other files -> generated changes

cc kassens

----

Trying to start split #3280 into smaller parts. When writing objects (whether exact or inexact) in typegen, sort the object's members by name. The used sorting is a bit subjective, so I'm open to suggestions.

rbalicki2

Pull Request resolved: #3808

Test Plan:
Changes to generated files for the test project and fixtures.
See D34469747 for changes this would do to generated files in xplat with rollout: 100. TLDR works as expected.

Reviewed By: mofeiZ

Differential Revision: D34439437

Pulled By: rbalicki2

fbshipit-source-id: a0c3825dc6259cb047d876e22c4ac8fcbd01eccc
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.

5 participants