Pattern matching / destructuring GraphQL results from foreign APIs

TL;DR: Is there better way how to deal with nested records with optional fields than huge Pattern Matching matrix? Some pattern everyone else knows about and I’m just unaware of? :slight_smile:

I’m in early beginner phase with rescript, I went through documentation couple times and tried to search for something relevant to my question here and probably everywhere else :smiley: I suspect it is hiding somewhere in plain sight and I’m just not realizing it is the thing I’m looking for.

I think it isn’t really relevant, but to provide context: I’m querying some GraphQL endpoint in GatsbyJS site (with help of graphql-ppx).

After many hours spent over stupid mistakes, I have working code which allows me to retrieve record with title and siteUrl always present.

%%raw("import { graphql } from 'gatsby'")

type rec metadata = {
  title: string,
  siteUrl: string,
}

let empty = {
  title: "no title",
  siteUrl: "",
}

%graphql(`
      query SiteMetaData {
        site {
          siteMetadata {
            title
            siteUrl
          }
        }
      }
`)

@module("gatsby") external useStaticQueryUnsafe: string => SiteMetaData.Raw.t = "useStaticQuery"

let useSiteMetadata = () => {
  let {site} = SiteMetaData.query->useStaticQueryUnsafe->SiteMetaData.parse
  switch site {
  | None => empty
  | Some({siteMetadata: None}) => empty
  | Some({siteMetadata: Some(smd)}) =>
    switch smd {
    | {title: None, siteUrl: None} => empty
    | {title: Some(someTitle), siteUrl: None} => {title: someTitle, siteUrl: ""}
    | {title: None, siteUrl: Some(someUrl)} => {title: "no title", siteUrl: someUrl}
    | {title: Some(someTitle), siteUrl: Some(someUrl)} => {title: someTitle, siteUrl: someUrl}
    }
  }
}

As you can see from pattern matching, almost everything in the query can be nullable and graphql-ppx’s parse is converting this to Options (which is great).

But, whole time it seems to me that I have something fundamentally wrong and there has to be more sensible way to deal with options and defaults in similar responses. Just imagine how it would even work if I add 10 more attributes to siteMetadata and all of them optional? What if there would be another level with more options?

Btw. don’t focus on Gatsby part at all. I know that I can make fields non-optional, I just hit this problem and want to know how to deal with it. No matter if it comes from Gatsby, totally different GraphQL server or simply from nested rescript record with a lot of options.

There are helper functions in the Belt module, that you can use to decode the value in a more convenient way, similar to optional chaining Belt.Option | ReScript API.

open Belt

let useSiteMetadata = () => {
  let data = SiteMetaData.query->useStaticQueryUnsafe->SiteMetaData.parse
  data.site
  ->Option.flatMap(site => site.siteMetadata)
  ->Option.flatMap(siteMetadata => {
    switch siteMetadata {
    | {title: None, siteUrl: None} => None
    | {title: maybeTitle, siteUrl: maybeSiteUrl} =>
      Some({
        title: maybeTitle->Option.getWithDefault("no title"),
        siteUrl: maybeSiteUrl->Option.getWithDefault(""),
      })
    }
  })
}
1 Like

You can do some very cool things with switch statements once you realise that patterns can be nested. You can also have multiple patterns fall through to a single case by leaving off the => (if all the variable names line up).

  switch site {
  | Some({
      siteMetadata: Some({title: Some(someTitle), siteUrl: Some(someUrl)}),
    }) => {title: someTitle, siteUrl: someUrl}
  | None
  | Some({siteMetadata: None})
  | Some({siteMetadata: Some({title: None, siteUrl: None})}) => empty
  | Some({siteMetadata: Some({title: Some(someTitle), siteUrl: None})}) => {
      title: someTitle,
      siteUrl: "",
    }
  | Some({siteMetadata: Some({title: None, siteUrl: Some(someUrl)})}) => {
      title: "no title",
      siteUrl: someUrl,
    }
  }

It’s a little more verbose than the flatmap style, but it’s far more powerful and it lets the compiler generate the absolute minimum JS to distinguish cases.

3 Likes

Huge thanks, it not only make sense but is totally obvious… why I didn’t see it? :smiley:

I really appreciate you are including whole example. It helped me not only with the original issue but also with better understanding of “chaining”.

1 Like

Oh my… I tried to have it in single “case” at first attempt, but no matter what I tried, it didn’t compile. I had been searching for missing bracket or anything for 10 minutes and than gave up. Pitty I closed the editor, it would be fun to find out when it differed.

Thanks for your answer. Btw. I didn’t realize before that I can use single return value for multiple patterns. With you naming it case it seems obvious now :slight_smile:

For now, I’ll try to use this when there are only couple optional arguments and flatmap style for other ones.

1 Like