License: MIT Hex version badge Hexdocs badge
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.
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
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
If you only want Elixir↔Gleam interop and do not want to hook into Ash, use AshGleam.GleamBridge.
defmodule MyApp.MathBridge do use AshGleam.GleamBridge gleam do consume do function :add_in_gleam, :integer do argument :a, :integer, allow_nil?: false argument :b, :integer, allow_nil?: false run &:my_gleam_module.add/2 end end expose do function :add_in_elixir, :integer do argument :a, :integer, allow_nil?: false argument :b, :integer, allow_nil?: false run fn a, b -> {:ok, a + b} end end end end end
consumemakes Gleam functions callable from ElixirMyApp.MathBridge.add(2, 3)exposegenerates Gleam-callable functions backed by Elixir.math_bridge.add_in_elixir(2, 3)- Run
mix ash_gleam.codegento generate the gleam bindings.
- Run
- Add
AshGleam.Resourceto your resource and declare agleamblock:
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 attribute :completed, :boolean, default: false end end
- Run
mix ash_gleam.codegen
// generated at src/generated/src/todo_item pub type Todo { Todo( title: String, completed: Boolean, ) }
- 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) }
- Add
AshGleam.Actionsto your resource and declare agleam.actionsblock 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
- 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!()
- 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)
- 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
- 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
-
Run
mix ash_gleam.codegen -
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 }) }
You can define the equivalent to Gleam's custom types using AshSumType from ash_sum_type.
| Elixir | Generated Gleam |
|---|---|
defmodule MyApp.Mark do use AshSumType, variants: [:x, :o] end |
pub type Mark { X O } |
defmodule MyApp.Mark do use AshSumType variant :x variant :o end |
pub type Mark { X O } |
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 |
pub type LookupOutcome { Found(Todo) Missing(String) } |
defmodule MyApp.Todo do use Ash.Resource, ..., extensions: [AshGleam.Resource] attributes do ... # Gleam type Tag attribute :primary_tag, MyApp.Tag, allow_nil?: false, default: [] # Gleam type List(Tag) attribute :tags, {:array, MyApp.Tag}, allow_nil?: false, default: [] # Gleam type List(Option(Tag)) attribute :nullable_tags, {:array, MyApp.Tag}, allow_nil?: false, default: [], nil_items?: true end end
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.
If you do not need actions or special validation, sum types are preferrable
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 attribute :color, :string, allow_nil?: false end end
- Elixir 1.15+
- Ash 3.0+
- Gleam (with
mix_gleamconfigured)
- Fork the repository
- Create a feature branch
- Add tests for any new behaviour
- Run
mix testandmix format - Open a pull request