Logo

License: MITHex version badgeHexdocs badge

AshGleam

Type-safe Gleam interop for Ash resources

AshGleam integrates Elixir's Ash framework and Gleam into a single, cohesive system. It enables you to move data and execution across the boundary with compile-time guarantees.

You can:

Installation

With Igniter (recommended)

mix igniter.install ash_gleam
mix igniter.install ash_gleam
# If testing locally:
mix igniter.install ash_gleam@path:..

This automatically configures your mix.exs with all the settings required by MixGleam: compilers, erlc_paths, erlc_include_path, prune_code_paths, the deps.get alias, and the gleam_stdlib / gleeunit dependencies. It also creates the src/ directory and adds build/ to your .gitignore.

You will still need to install the Gleam compiler and the MixGleam archive:

# Install the Gleam compiler — see https://gleam.run/getting-started/installing-gleam.html

# Install the MixGleam Mix archive
mix archive.install hex mix_gleam

Manual setup

Add ash_gleam to your dependencies:

# mix.exs
defp deps do
  [
    {:ash_gleam, "~> 0.1"},
    {:gleam_stdlib, "~> 0.34 or ~> 1.0"},
    {:gleeunit, "~> 1.0", only: [:dev, :test], runtime: false}
  ]
end

Then follow the MixGleam README to configure your project:

# mix.exs
@app :my_app

def project do
  [
    app: @app,
    # ...
    archives: [mix_gleam: "~> 0.6"],
    compilers: [:gleam | Mix.compilers()],
    aliases: [
      "deps.get": ["deps.get", "gleam.deps.get"]
    ],
    erlc_paths: [
      "_build/dev/lib/#{@app}/_gleam_artefacts",
      "_build/dev/lib/#{@app}/build"
    ],
    erlc_include_path: "_build/dev/lib/#{@app}/include",
    prune_code_paths: false
  ]
end

Create a src/ directory for your Gleam source files and add the following to your .gitignore:

# gleam build files
/build/

# intermediate generation file
/src/generated/manifest.term

Generate Gleam types from your Ash resources

  1. Add <code>AshGleam.Resource</code> to your resource and declare a <code>gleam</code> block:
defmodule MyApp.Todo do
  use Ash.Resource,
    ...,
    extensions: [AshGleam.Resource, AshGleam.Actions]

  gleam do
    type_name "Todo"       # required — the Gleam type name
    module_name "todo_item" # optional — overrides the generated file name
  end

  attributes do
    uuid_primary_key :id
    attribute :title, :string, allow_nil?: false, public?: true
    attribute :completed, :boolean, default: false, public?: true
  end
end
  1. Run mix ash_gleam.codegen
// generated at src/generated/src/todo_item

pub type TicTacToe {
  TicTacToe(
    id: String,
    title: String,
    completed: Boolean,
  )
}

Only public?: true attributes are included in the generated Gleam type.

Expose Gleam functions as Ash actions

  1. Create a gleam function (make sure you have run the generator first)
// Import the generated Todo type
import src/generated/src/todo_item.{type Todo, Todo}

pub fn mark_completed(item: Todo) -> Todo {
  Todo(..item, completed: True)
}
  1. Add <code>AshGleam.Actions</code> to your resource and declare a <code>gleam.actions</code> block for that function
defmodule MyApp.Todo do
  use Ash.Resource,
    ...,
    extensions: [AshGleam.Resource, AshGleam.Actions]


  gleam do
    type_name "Todo"
    module_name "todo_item"

    actions do
      action :mark_completed, __MODULE__ do
        update? true
        argument :todo, __MODULE__, allow_nil?: false

        run &:test_gleam.mark_completed/1
      end
    end
  end
end
  1. Use the exposed function
todo = # create a todo

# mark_completed in memory
assert {:ok, updated} = MyApp.Todo.mark_completed(%{todo: todo})

# mark_completed and persist
{:ok, changeset} =
  todo
  |> AshGleam.Changeset.for_update(:mark_completed, %{}, action: :update)
  |> Ash.update!()
  1. If you want a code interface that does the update for you, update your domain
defmodule MyApp.Domain do
  use Ash.Domain,
    otp_app: :my_app,
    extensions: [AshGleam.Domain]

  gleam do
    code_interface do
      resource AshGleam.TestTodo do
        define_gleam_update :mark_completed, action: :update
      end
    end
  end
end

# mark_completed and persist
{:ok, updated} = MyApp.Domain.mark_completed(todo)

Expose Ash actions to Gleam

  1. Add the resource actions you want to expose to Gleam
defmodule MyApp.Todo do
  use Ash.Resource,
    ...,
    extensions: [AshGleam.Resource, AshGleam.Actions]

  ...

  actions do
    defaults [:read]

    create :create do
      accept [:title, :completed, :priority]
    end

    update :update do
      accept [:title, :completed, :priority]
      require_atomic? false
    end

    destroy :destroy

    read :get do
      get_by [:id]
    end

    read :first_completed do
      get? true
      filter expr(completed == true)
      prepare build(sort: [title: :asc], limit: 1)
    end
  end
end
  1. Create an entry in gleam.ffi for your resource actions
defmodule MyApp.Domain do
  use Ash.Domain,
    otp_app: :my_app,
    extensions: [AshGleam.Domain]

  gleam do
    ffi do
      resource MyApp.Todo do
        action :list_todos, :read
        action :create_todo, :create
        action :get_todo, :get
        action :destroy_todo, :destroy
        action :first_completed, :first_completed
      end
    end
  end
end
  1. Run mix ash_gleam.codegen

  2. Use the generated gleam functions

import myapp/generated/src/list_todos
import myapp/generated/src/todo_item.{type TodoFilter, type TodoSort}

pub fn fetch_incomplete_todo_titles(): Result(List(String), String) {
  list_todos.new()
  |> list_todos.filter([todo_item.CompletedEq(False)])
  |> list_todos.sort([todo_item.Title(Asc)])
  |> list_todos.limit(option.Some(10))
  |> list_todos.run()
  |> result.map(fn (todo_item) {
    todo_item.title
  })
}

Represent Gleam custom-types in Elixir

You can define the equivalent to Gleam's custom types using AshSumType from ash_sum_type.

ElixirGenerated Gleam
```elixir defmodule MyApp.Mark do use AshSumType, variants: [:x, :o] end ``` ```gleam pub type Mark { X O } ```
```elixir defmodule MyApp.Mark do use AshSumType variant :x variant :o end ``` ```gleam pub type Mark { X O } ```
```elixir defmodule MyApp.LookupOutcome do use AshSumType variant :found do field :value, MyApp.Todo, allow_nil?: false end variant :missing do field :error, :string, allow_nil?: false end end ``` ```gleam pub type LookupOutcome { Found(Todo) Missing(String) } ```

Embedded resources

Resources with the :embedded data layer work as field types in other resources. The embedded resource gets its own Gleam type and is imported automatically in the parent resource's generated file.

defmodule MyApp.Tag do
  use Ash.Resource,
    domain: MyApp.Domain,
    data_layer: :embedded,
    extensions: [AshGleam.Resource]

  gleam do
    type_name "Tag"
  end

  attributes do
    attribute :label, :string, allow_nil?: false, public?: true
    attribute :color, :string, allow_nil?: false, public?: true
  end
end

defmodule MyApp.Todo do
  use Ash.Resource,
    ...,
    extensions: [AshGleam.Resource]

  attributes do
    ...
    # Gleam type List(Tag)
    attribute :tags, {:array, MyApp.Tag}, allow_nil?: false, default: [], public?: true

    
    # Gleam type List(Option(Tag))
    attribute :nullable_tags, {:array, MyApp.Tag}, allow_nil?: false, default: [], public?: true, nil_items?: true
  end
end

Requirements

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for any new behaviour
  4. Run mix test and mix format
  5. Open a pull request

License

MIT — see LICENSES/MIT.txt.

Links