ZoiDefstruct - an extension for Zoi to generate Elixir struct

I just did a dirty hack after seeing Zoi on x.com a few hours ago.

Quick Introduction

The zoi_defstruct is a library to help you generate a struct from the Zoi object schema (over Zoi.object). Let’s see a small example below:

defmodule K8S.Deployment do
  use ZoiDefstruct

  @schema Zoi.object(%{
            apiVersion: Zoi.string(),
            metadata:
              Zoi.object(%{
                name: Zoi.string()
              }),
            spec:
              Zoi.object(%{
                replicas: Zoi.integer() |> Zoi.optional()
              })
          })

  defstructure(@schema)
end

When you call a defstructure The macro generates a struct with enforced keys and typespec for you automatically. So in the IEx, you can see:

A struct:

iex(4)> K8S.Deployment.__struct__()
%K8S.Deployment{spec: nil, metadata: nil, apiVersion: nil}

A typespec declaration:

iex(1)> t K8S.Deployment
@type t() :: %{
        spec: %{optional(:replicas) => integer()},
        metadata: %{name: binary()},
        apiVersion: binary()
      } 

ArgumentError if it has no required keys:

iex(5)> %K8S.Deployment{}
** (ArgumentError) the following keys must also be given when building struct K8S.Depl
oyment: [:spec, :metadata, :apiVersion]
    (examples 0.1.0) expanding struct: K8S.Deployment.__struct__/1
    iex:5: (file)

Since this is a quick hack, it has numerous edge cases that need to be addressed. Please feel free to provide feedback or suggestions. Opening an issue or pull request is very welcome.

I need to give special thanks to Zoi for making this awesome library!

2 Likes

Great. will check it out.

How does it differ from zoi/lib/zoi/struct.ex at main · phcurado/zoi · GitHub ?

awesome @wingyplus

You can leverage some of the helpers on the Zoi Struct module as @byu mentioned. I personally decided to not add any macros into the library (unless strictly necessary like infering type specs) but I added these helpers for anyone who wishes to extend it.
Thanks for using Zoi, awesome to see this

2 Likes

Thanks for the comment :folded_hands:

Actually, it’s a wrapper of those three functions to avoid repetition works.

Thanks for the comment. :folded_hands:

I just see that Zoi has a struct helper already (from @byu mentioned). Will refactoring to use those functions instead.

I’m so happy libraries like Zoi seem to catch on and inspire extension (infant adoption steps, but still).

They complement my way of thinking really well!

2 Likes

Update from the latest main. The defstructure is now changed to defstruct and you can embed the schema into defstruct directly.

The API also use Zoi.Struct under the hood. So the enforce keys and struct declaration is now align with the Zoi upstream.

2 Likes

Type composition is now working! You can use t() function to reference another struct.

1 Like

The library is now published on Hex zoi_defstruct | Hex . Feel free to provide feedback, open issues, or pull requests.

2 Likes

Great work @wingyplus, this is some inspiring stuff.

One concern I had though with this approach is that having a defstruct-like DSL that looks like Kernel.defstruct but behaves differently can be confusing.

Your work inspired me to experiment with Sigil, a proof-of-concept DSL for defining nested structs and validating external data using Zoi:

defmodule Post do
  use Sigil.Schema

  schema do
    field :title, Zoi.string()

    field :image, Image do
      field :url, Zoi.string()
      field :blurhash, Zoi.string()

      field :metadata, Metadata do
        field :size, Zoi.integer() |> Zoi.optional()
        field :format, Zoi.string() |> Zoi.optional()
      end
    end
  end
end

I’m still iterating and may try more compact forms for types like:

field :age, :integer, coerce: true

This has been a great way to learn macros and DSL design, and aims to complement(and obviously heavily inspired by) Ecto. Whereas Ecto handles data mapping and persistence at the external data/application(and database) layer, validation libraries like zoi and the likes and thus Sigil aim to provide a layer that works great for things like wrapping external api’s into client libraries and doing validation and egornomic casting into elixir constructs.

Thanks again—I hope others find it interesting.

2 Likes