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_gleammix 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_gleamManual 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}
]
endThen 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.termStandalone Elixir and Gleam bridges
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, :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
endconsumemakes Gleam functions callable from ElixirMyApp.MathBridge.add(2, 3)exposegenerates Gleam-callable functions backed by Elixir.math_bridge.add_in_elixr(2, 3)
Run mix ash_gleam.codegen to generate both sides.
Generate Gleam types from your Ash resources
-
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, public?: true
attribute :completed, :boolean, default: false, public?: true
end
end-
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
- 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)Expose Ash actions to Gleam
- 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
endRun
mix ash_gleam.codegenUse 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.
| 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.