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:
- Generate Gleam types from your Ash resources
- Expose Gleam functions as Ash actions
- Expose Ash actions in Gleam
- Represent Gleam custom-types in Elixir
Installation
### With Igniter (recommended) ```bash mix igniter.install ash_gleam ```
```bash 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](https://github.com/gleam-lang/mix_gleam): 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: ```bash # 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: ```elixir # 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](https://github.com/gleam-lang/mix_gleam) to configure your project: ```elixir # 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 AshGleam.Resource to your resource and declare a gleam block:
```elixir
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
```2. Run `mix ash_gleam.codegen`
```gleam // 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)
```gleam // 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) } ```
2. Add AshGleam.Actions to your resource and declare a gleam.actions block for that function
```elixir
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
```3. Use the exposed function
```elixir 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!() ```4. If you want a code interface that does the update for you, update your domain
```elixir 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
```elixir 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 ```2. Create an entry in gleam.ffi for your resource actions
```elixir 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 ```-
Run
mix ash_gleam.codegen
4. Use the generated gleam functions
```gleam 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.
| Elixir | Generated 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
endRequirements
- Elixir 1.15+
- Ash 3.0+
-
Gleam (with
mix_gleamconfigured)
Contributing
- Fork the repository
- Create a feature branch
- Add tests for any new behaviour
-
Run
mix testandmix format - Open a pull request
License
MIT — see LICENSES/MIT.txt.