TypedStruct

repository·main·Indexed 20 days ago

https://github.com/ejpcmac/typed_struct

A library for defining Elixir structs with associated types, reducing boilerplate for defstruct, @enforce_keys, and @type definitions. It provides a typedstruct block and field/3 macro to manage field names, types, and options such as default values and key enforcement. It supports opaque types, submodule definitions, and a plugin system via TypedStruct.Plugin to extend the definition process.

Tokens
3.1K
Snippets
16
Records
16
Agent score
70%

What's inside typed_struct

  1. Extend TypedStruct with plugins

    main

    You can extend the functionality of typedstruct by using the plugin macro within the typedstruct block. This allows third-party libraries to hook into the definition process (e.g., generating lenses or other metadata).

    defmodule MyStruct do
      use TypedStruct
    
      typedstruct do
        plugin TypedStructLens
    
        field :a_field, String.t()
        field :other_field, atom()
      end
    end
  2. Install TypedStruct

    main

    Add typed_struct to your Mix dependencies. Since TypedStruct is only used at compile time, you can set runtime: false to optimize your build.

    {:typed_struct, "~> 0.3.0"}
    # Or for build-time only:
    {:typed_struct, "~> 0.3.0", runtime: false}
  3. Define a basic typed struct

    main

    To define a struct, use use TypedStruct and wrap your field definitions in a typedstruct block. Each field is defined using the field/2 macro (or field/3 with options).

    By default:

    • Fields have a nil default value.
    • Types are automatically made nullable (e.g., String.t() | nil).
    defmodule MyStruct do
      use TypedStruct
    
      typedstruct do
        field :a_string, String.t()
        field :string_with_default, String.t(), default: "default"
        field :enforced_field, integer(), enforce: true
      end
    end
  4. Configure mix format for TypedStruct

    main

    To prevent mix format from automatically adding parentheses to your field definitions, add :typed_struct to the import_deps list in your .formatter.exs file.

    # .formatter.exs
    [
      ...,
      import_deps: [:typed_struct]
    ]
  5. Implement a TypedStruct plugin

    main

    A TypedStruct plugin allows you to extend the typedstruct block by injecting custom logic, helpers, or metadata for the struct and its fields. To create a plugin, define a module that uses TypedStruct.Plugin.

    There are three primary lifecycle callbacks you can implement:

    1. init(opts): Injects code at the point where plugin/2 is called inside the typedstruct block. This is useful for importing custom macros or setting up module-level state (like module attributes) that the plugin will use during the definition process.
    2. field(name, type, opts, env): Injects code for every field defined via the field/3 macro. The opts argument contains a concatenation of the options passed directly to the field macro and any options provided during the plugin's init phase. The env argument provides the macro environment at the time of definition.
    3. after_definition(opts): Injects code after the entire struct and its type have been defined. This is ideal for cleanup tasks, such as deleting temporary module attributes used during the build process.

    Using TypedStruct.Plugin provides default implementations for these callbacks, so you only need to implement the ones you require.

    defmodule MyCustomPlugin do
      use TypedStruct.Plugin
    
      @impl true
      defmacro init(opts) do
        quote do
          # Code to inject when 'plugin MyCustomPlugin' is called
        end
      end
    
      @impl true
      def field(name, type, opts, env) do
        quote do
          # Code to inject for each field
        end
      end
    
      @impl true
      def after_definition(opts) do
        quote do
          # Code to inject after the struct is defined
        end
      end
    end
  6. Generate an opaque type for a struct

    main

    To hide the internal structure of your struct from consumers, pass opaque: true to the typedstruct macro. This replaces the @type t() definition with @opaque t().

    defmodule MyOpaqueStruct do
      use TypedStruct
    
      typedstruct opaque: true do
        field :name, String.t()
      end
    end
  7. Define a struct within a submodule

    main

    If you want to avoid defining a top-level module just for a struct, use the module: option in the typedstruct macro. This wraps the struct definition inside a nested module.

    defmodule MyModule do
      use TypedStruct
    
      # This creates %MyModule.Struct{}
      typedstruct module: Struct do
        field :field, term()
      end
    end
  8. Document a typed struct

    main

    You can add documentation to the struct type by placing the @typedoc attribute inside the typedstruct block. You can also use @moduledoc inside the block when using the module: option to document the submodule.

    # Documenting the type
    typedstruct do
      @typedoc "A typed struct"
      field :a_string, String.t()
    end
    
    # Documenting a submodule
    typedstruct module: MyStruct do
      @moduledoc "A submodule with a typed struct."
      @typedoc "A typed struct in a submodule"
    
      field :a_string, String.t()
    end
  9. Enforce keys by default in a struct

    main

    You can pass enforce: true to the typedstruct macro to make all defined fields required. You can still opt-out of enforcement for specific fields using enforce: false or by providing a default value.

    defmodule MyStruct do
      use TypedStruct
    
      typedstruct enforce: true do
        # This key is enforced.
        field :enforced_by_default, term()
    
        # You can override the default behaviour.
        field :not_enforced, term(), enforce: false
    
        # A key with a default value is not enforced.
        field :not_enforced_either, integer(), default: 1
      end
    end
  10. Use a plugin in a TypedStruct definition

    main

    Once a plugin is defined, you can integrate it into your struct by calling plugin PluginModuleName, options inside the typedstruct block. Any options passed to plugin/2 will be merged into the opts parameter of the plugin's field/4 callback.

    Example of using a DescribedStruct plugin to add descriptions to a struct and its fields:

    defmodule MyStruct do
      use TypedStruct
    
      typedstruct do
        # Import the plugin with specific options
        plugin DescribedStruct, upcase: true
    
        # Use custom macros provided by the plugin
        description "My struct"
    
        field :a_field, String.t(), description: "A field"
        field :second_field, boolean()
      end
    end
    
    # Resulting API:
    # MyStruct.struct_description() => "MY STRUCT"
    # MyStruct.field_description(:a_field) => "A FIELD"
  11. Enforce keys by default in `typedstruct`

    main

    If you want all fields in your struct to be required (enforced) unless explicitly marked otherwise, pass enforce: true to the typedstruct macro. This simplifies definitions where most fields are non-nullable.

    defmodule MyStruct do
      use TypedStruct
    
      typedstruct enforce: true do
        field :field_one, String.t(), enforce: false
        field :field_two, integer()
        field :field_three, boolean()
        field :field_four, atom(), default: :hey
      end
    end
  12. Define a struct in a submodule using `module` option

    main

    By default, typedstruct defines the struct in the current module. If you provide the module: option, the struct will be defined in the specified submodule.

    defmodule MyModule do
      use TypedStruct
    
      typedstruct module: Struct do
        field :field_one, String.t(), enforce: true
        field :field_two, integer(), enforce: true
        field :field_three, boolean(), enforce: true
        field :field_four, atom(), default: :hey
      end
    end