Skip to content

Navigation Menu

Sign in
Sign up

Repository files navigation

Logo

License: MIT Hex version badge Hexdocs 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.

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

Without Ash

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
  • consume makes Gleam functions callable from Elixir MyApp.MathBridge.add(2, 3)
  • expose generates Gleam-callable functions backed by Elixir. math_bridge.add_in_elixir(2, 3)
    • Run mix ash_gleam.codegen to generate the gleam bindings.

With Ash

Generate Gleam types from your Ash resources

  1. Add AshGleam.Resource to your resource and declare a gleam 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
 attribute :completed, :boolean, default: false
 end
end
  1. Run mix ash_gleam.codegen
// generated at src/generated/src/todo_item
pub type Todo {
 Todo(
 title: String,
 completed: Boolean,
 )
}

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 AshGleam.Actions to your resource and declare a gleam.actions 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
 })
}

Custom types

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)
}

Using custom types

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

Embedded resource support

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

Requirements

  • Elixir 1.15+
  • Ash 3.0+
  • Gleam (with mix_gleam configured)

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

Links

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /