Zaid-Ajaj / Snowflaqe

A dotnet CLI to generate type-safe GraphQL clients for F# and Fable with automatic deserialization, static query verification and type checking
MIT License
157 stars 26 forks source link

Discussion: annotation / auto-conversion / post-processing GraphQL responses into domain types? #24

Closed njlr closed 3 years ago

njlr commented 3 years ago

This is more of a question and discussion rather than a bug.

Let's say I have a custom type in my model like this:

open System

type ProductID = 
  | Apple of Guid
  | Banana of Guid
  | Cherry of Guid
  with 
    member this.ToString () =
      match this with
      | Apple g -> sprintf "apple/%A" g
      | Banana g -> sprintf "banana/%A" g
      | Cherry g -> sprintf "cherry/%A" g

module ProductID = 

  let tryParse (x : string) : ProductID option = 
    // match x.Split([| '/' |]) with
    // etc ...

All good idiomatic F#.

Now, in my GraphQL endpoint, the product ID is encoded as GraphQL ID, which is just a string.

So for a query like this:

query {
  products {
    id
    price
    # ...
  }
}

GraphQL might return this:

{
  "products": [
    {
      "id": "apple/157be5aa-d3c8-4894-befb-a739251a57bb",
      "price": 60
    },
    {
      "id": "banana/11751f22-7908-44c0-bb3b-59d9eddd0457",
      "price": 70
    }
  ]
}

Snowflaqe will (correctly) generate F# code that is something like this:

type Product = 
  {
    id : string
    price : int
    // ...
  }

type Query = 
  {
     products : Product list
  }

I want to consume this response in a Fable app, but now I have a choice to make. The generated Snowflaqe code has product.id as a string, but ideally I would have a ProductID.

Right now, the options I can see are:

asyncResult {
  let client = GraphQL.GraphqlClient "/graphql"
  let! resultUntyped = client.Query.products ()
  let! resultTyped = parseResult resultUntyped

  return resultTyped
}

This give ultimate safety, but it's lots of extra code.

The ProductID is just an example of this problem, but there are many other cases where it appears. I am currently using the "parse on-the-fly" approach in my own code.

Perhaps there should be some way of configuring Snowflaqe to integrate the parsing in the fetch process? Or maybe I am missing a simple pattern for solving this? I'm not sure what this would look like, so I thought it would be best to start a discussion here.

It might also be interesting to leverage Thoth.Json decoders here, since I usually already have those for other purposes.

Zaid-Ajaj commented 3 years ago

Hello @njlr, this is a very good question:

Perhaps there should be some way of configuring Snowflaqe to integrate the parsing in the fetch process?

Currently there isn't any way because we rely on Fable.SimpleJson to automatically and exactly parsing the JSON into the generated types without needing any configuration. In fact, SimpleJson understand GraphQL types and how they should be mapped (for example a GraphQL interface or union maps to a discriminated union of record type for each case).

Now SimpleJson doesn't have a way to override its way of parsing. I believe it is possible but it can be very complicated. Besides changing the automatic parsing behaviour, there needs to be also a way to tell Snowflaqe how to read use the custom converters which complicate the situation even more. Another complication is also making the fsharp target follow these custom generators (Snowflaqe not only generates clients for Fable but also for .NET/F# consumers).

As for Thoth.Json, it is possible to use it and instead generate F# encoders and decoders for each and every generated GraphQL type but this too has downsides:

I think the "on-the-fly" method of your code is the way to go because you can assume the data coming from the GraphQL client is always well-typed and the data always comes through as exact as it can be. If you customize the parsing, you lose this assumption.