nerdsupremacist / GraphZahl

A Framework to implement Declarative, Type-Safe GraphQL Server APIs using Runtime Magic 🎩

Home Page:https://quintero.io/GraphZahl/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Feature request: provide Sourcery templates for generating GraphZahl protocol conformances

Evertt opened this issue · comments

When I read the README, it sounded like GraphZahl can only support the simplest of enums, namely enums without any associated values, that conform to RawRepresentable with String as their RawValue type.

However in the very next paragraph you mention that you made special enums for union types, enums that use associated values. So GraphZahl can support enums with associated values. As long as you conform to the right protocols and implement the requirements manually.

But your enums are in fact generated. Which gave me the idea that maybe you could offer templates that generate the right protocol implementations for custom enums as well.

I've tinkered a bit with Sourcery recently and my guess would be that it may even be possible to generate protocol implementations for arbitrarily complex enums.

Do you also think that this is probably possible and adds value?

I'm actually still thinking about how to better support that. So far I've been trying really hard to make sure the users of the library don't rely on code generation.

So the union types are designed as a simple way of using enums with associated types. Because I autogenerated the implementation of those.

I'm thinking how can we better use it

So far I've been trying really hard to make sure the users of the library don't rely on code generation.

Yeah I get that. I was at first also not a fan of that idea. But since I've been working with it myself recently I really see how it can offer me a lot of ease, while not sacrificing much for it.

So the union types are designed as a simple way of using enums with associated types. Because I autogenerated the implementation of those.

Yeah, but those enums do make the developer sacrifice some of their code's readability. For example, I have a custom enum in my code that is definitely nothing more than a union type. It looks like this:

enum Business {
    case auction(Auction)
    case sale(Sale)
}

I could very easily replace this with your Union2<A,B> enum, but when I did that I needed to replace every mention of business.auction with business.a and business.sale with business.b and I immediately wished there would be another way. Because that just makes my code that much harder to read and interpret.

I've tinkered a bit with Sourcery recently and my guess would be that it may even be possible to generate protocol implementations for arbitrarily complex enums.

Okay maybe not arbitrarily complex enums.

I would do it as follows. Create a special protocol, like GraphZahlCustomEnum, and make the template select all enums that conform to that protocol to generate the necessary implementations.

Then it will just go down the tree, for every associated value that it finds that conforms to either GraphZahlScalar or GraphZahlObject or GraphZahlEnum or another GraphZahlCustomEnum, it's easy. If it finds an associated value that does not conform to any of those protocols and also isn't a simple String or Int or whatever, then it generates code that simply throws an error.

That way the developers can see that their types are like 90% correct, and that they just need to fix the last 10%.

Finally I would argue that this can just be an additional option to the enums that you've already generated. So if a developer prefers to not be dependent on code generation then they can use your enums. And if a developer prefers to use code generation, they can use your templates.

@Evertt I decided to offer a completely new API. It's part of the alpha.20 release

As you can see in my test project tmdb you now only use GraphQLUnion. I have an enum that can either be a Movie or a TVShow or a Person:

enum MovieOrTVOrPeople: Decodable, GraphQLUnion {
    private enum CodingKeys: String, CodingKey {
        ...
    }

    case movie(MovieResult)
    case tv(TVShowResult)
    case person(PersonListResult)

    init(from decoder: Decoder) throws {
        ...
    }
}

which results in

union MovieOrTVOrPeople = MovieResult | TVShowResult | PersonListResult

without any additional code. If any of the cases is not compatible with this format, it will throw an error while trying to generate the schema at runtime. But I couldn't get around it.

With this addition we no longer need the Sourcery Template. What do you think?

Hey, thanks for all the works you've put in! It definitely looks like a great improvement in terms of readability / having case names that make sense.

I wonder though, if I remember correctly, the union enums that you had autogenerated, they contained some code that would add useful information to the schema documentation right? Or, I don't know exactly what this code did, but it looked to me like it was partly instructing GraphZahl about what kind of union it was so that GraphZahl could show that in the schema documentation. Is that correct?

So in that case I still see added value in having a Sourcery Template that generates code for that. But I'm not super attached to you having to make that. If at any point I start to use this repository in a serious project then I may just make those templates myself and maybe I'll just offer that on a repo on my own account. And then we can just see if my templates end up being popular. If they do then that may be an incentive for you to integrate them into GraphZahl and if not then I'm happy to just keep maintaining them for myself in my own repo. :-)

So the new code here is doing the exact same thing, but by looking at the type metadata of the enum.

The reason that it looks so different is that the since we don't know the enum at compile time, we can't switch over the value. So I do this weird magic of looking at the most significant bits of the last byte, to understand which enum case it is and load the payload properly. That is actually just reading the data as it is explained in the Swift ABI docs.

So nothing has changed. The same schema is generated in the exact same way. Except that now we are not limiting the user to 20 cases. They can have as many cases as they want.

However a big limitation of GraphZahl at the moment is the lack of documentation in the generated schema. Since the type information is loaded at runtime we have no way of getting a SwiftDoc out of the symbols. This is something that a Sourcery Template could fix. This was the case with the previous implementation as it is right now. We could add some documentation protocols here and there, but they're not going to work for fields, which is where they would be most useful I guess

I however knew from the start that I wanted to load the data at Runtime to make using this framework easier.

So nothing has changed. The same schema is generated in the exact same way. Except that now we are not limiting the user to 20 cases. They can have as many cases as they want.

This is awesome.

However a big limitation of GraphZahl at the moment is the lack of documentation in the generated schema. Since the type information is loaded at runtime we have no way of getting a SwiftDoc out of the symbols.

Ah, so you don't mean the GraphQL documentation, but the Swift documentation that could be generated by a tool called SwiftDoc? I've never used that before, so I'm not familiar with that at all. So for me this limitation probably doesn't matter. 😅

Exactly! I mean that GraphQL supports adding documentation comments to your schema. We currently have no way of creating those

Ah, hmm, yeah I can only imagine doing that with either Sourcery or with PropertyWrappers. But property wrappers wouldn't work for functions and also they "feel a bit too heavy" for this use-case (I don't know how to explain what I mean with "feels too heavy").

So yeah, it should probably be done with Sourcery. But then I don't get what you mean with this:

This is something that a Sourcery Template could fix. We could add some documentation protocols here and there, but they're not going to work for fields, which is where they would be most useful I guess.

Why wouldn't it work for fields? You can literally do ANYTHING with Sourcery!

Sorry I wasn't very clear...

I meant that even without using Sourcery, we could still add documentation for types. Something like this:

class MyObject: GraphQLObject, GraphQLDocumented {
    static let documentation = """
    Bla bla bla
    """

    let foo: String
}

And a similar way for Deprecated:

class MyObject: GraphQLObject, GraphQLDeprecated {
    static let deprecationReason = """
    Use XYZ instead
    """

    let foo: String
}

Aaaah, okay, I see.

And sadly I think that a Property Wrapper while looking fancy and cool, would still not work with a simple string, because we have to resolve the documentation statically.

One way of doing this is to have types for it:

enum FooDocumentation: GraphQLDocumentation {
    static let message = """
    Bla bla
    """ 
}

class MyObject: GraphQLObject {

    @Documented<FooDocumentation>
    let foo: String

    func bar() -> Documented<String, FooDocumentation> { ... }
}