Packages
dagger
0.18.9
0.21.7
0.21.6
0.21.5
0.21.4
0.21.3
0.21.2
0.21.1
0.21.0
0.20.8
0.20.7
0.20.6
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0
0.19.11
0.19.10
0.19.9
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.19
0.18.18
0.18.17
0.18.16
0.18.15
0.18.14
0.18.13
0.18.12
0.18.11
0.18.10
0.18.9
0.18.8
0.18.7
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.2
0.17.1
0.17.0
0.16.3
0.16.2
0.16.1
0.16.0
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.0
0.13.7
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.7
0.12.6
0.12.5
0.12.4
0.12.3
0.12.2
0.12.1
0.12.0
0.11.9
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.2
0.0.0
retired
Dagger SDK for Elixir
Current section
Files
Jump to
Current section
Files
lib/dagger/mod/object.ex
defmodule Dagger.Mod.Object do
@moduledoc """
Declare a module as an object type.
## Declare an object
Add `use Dagger.Mod.Object` to the Elixir module that want to be a
Dagger module and give a name through `:name` configuration:
defmodule Potato do
use Dagger.Mod.Object, name: "Potato"
# ...
end
The module also support documentation by using Elixir standard documentation,
`@moduledoc`.
## Declare a function
The module provides a `defn`, a macro for declare a function.
Let's declare a new function named `echo` that accepts a `name` as a string
and return a container that echo a name in the module `Potato` from the previous
section:
defmodule Potato do
use Dagger.Mod.Object, name: "Potato"
defn echo(name: String.t()) :: Dagger.Container.t() do
dag()
|> Dagger.Client.container()
|> Dagger.Container.from("alpine")
|> Dagger.Container.with_exec(["echo", name])
end
end
From the example above, the `defn` allows you to annotate a type to function
arguments and return type by using Elixir Typespec. The type will convert to
a Dagger type when registering a module.
The supported primitive types for now are:
1. `integer()` for a boolean type.
2. `boolean()` for a boolean type.
3. `String.t()` or `binary()` for a string type.
4. `list(type)` or `[type]` for a list type.
5. `type | nil` for optional type.
6. Any type that generated under `Dagger` namespace (`Dagger.Container.t()`,
`Dagger.Directory.t()`, etc.).
The function also support documentation by using Elixir standard documentation,
`@doc`.
"""
@type function_name() :: atom()
@type function_def() :: {function_name(), keyword()}
alias Dagger.Mod.Object.Defn
alias Dagger.Mod.Object.Meta
@doc """
Get module documentation.
Returns module doc string or `nil` if the given module didn't have a documentation.
"""
@spec get_module_doc(module()) :: String.t() | nil
def get_module_doc(module) do
with {module_doc, _} <- fetch_docs(module),
%{"en" => doc} <- module_doc do
String.trim(doc)
else
:none -> nil
:hidden -> nil
{:error, :module_not_found} -> nil
end
end
@doc """
Get function documentation.
Return doc string or `nil` if that function didn't have a documentation.
"""
@spec get_function_doc(module(), function_name()) :: String.t() | nil
def get_function_doc(module, name) do
fun = fn
{{:function, ^name, _}, _, _, _, _} -> true
_ -> false
end
with {_, function_docs} <- fetch_docs(module),
{{:function, ^name, _}, _, _, doc_content, _} <- Enum.find(function_docs, fun),
%{"en" => doc} <- doc_content do
String.trim(doc)
else
nil -> nil
:none -> nil
:hidden -> nil
end
end
defp fetch_docs(module) do
{:docs_v1, _, :elixir, _, module_doc, _, function_docs} = Code.fetch_docs(module)
{module_doc, function_docs}
end
defmacro __using__(opts) do
name = opts[:name]
quote do
import Dagger.Mod.Object, only: [defn: 2, field: 2, field: 3, object: 1]
import Dagger.Global, only: [dag: 0]
Module.register_attribute(__MODULE__, :function, accumulate: true, persist: true)
Module.register_attribute(__MODULE__, :field, accumulate: true, persist: true)
# Get an object name
def __object__(:name), do: unquote(name)
# List available function definitions.
def __object__(:functions) do
__MODULE__.__info__(:attributes)
|> Keyword.get_values(:function)
|> Enum.flat_map(& &1)
end
# Get a function definition.
def __object__(:function, name) do
__object__(:functions)
|> Keyword.fetch!(name)
end
# List available field definitions.
def __object__(:fields) do
__MODULE__.__info__(:attributes)
|> Keyword.get_values(:field)
|> Enum.flat_map(& &1)
end
end
end
@doc """
Declare a function.
"""
defmacro defn(call, do: block) do
{name, args, return} = extract_call(call)
has_self? = is_tuple(args)
arg_defs = compile_args(args)
return_def = compile_typespec!(return)
quote do
@function {unquote(name),
%Dagger.Mod.Object.FunctionDef{
self: unquote(has_self?),
args: unquote(arg_defs),
return: unquote(return_def)
}}
unquote(Defn.define(name, args, return, block))
end
end
@doc """
Declare an object struct.
"""
defmacro object(do: block) do
quote do
Module.register_attribute(__MODULE__, :required_fields, accumulate: true)
Module.register_attribute(__MODULE__, :optional_fields, accumulate: true)
unquote(block)
required_fields = @required_fields || []
optional_fields = @optional_fields || []
fields = @required_fields ++ @optional_fields
# TODO: convert fields into typespec.
@type t() :: %__MODULE__{}
@derive [Jason.Encoder, {Nestru.Decoder, hint: Dagger.Mod.Object.decoder_hint(fields)}]
@enforce_keys Keyword.keys(required_fields)
defstruct fields |> Keyword.keys() |> Enum.sort()
end
end
def decoder_hint(fields) do
fields
|> Enum.filter(&only_module/1)
|> Enum.into(%{}, fn {name, field_def} ->
type =
case field_def.type do
{:list, type} -> type
{:optional, type} -> type
type -> type
end
{name, type}
end)
end
defp only_module({_, field_def}) do
case field_def.type do
{:list, type} -> module?(type)
{:optional, type} -> module?(type)
type -> module?(type)
end
end
defp module?(type) do
{:module, ^type} = Code.ensure_loaded(type)
function_exported?(type, :__struct__, 0)
end
@doc """
Declare a field.
"""
defmacro field(name, type, opts \\ []) do
type = compile_typespec!(type)
optional? = match?({:optional, _}, type)
doc = opts[:doc]
field = Macro.escape({name, %Dagger.Mod.Object.FieldDef{type: type, doc: doc}})
quote do
@field unquote(field)
if unquote(optional?) do
Module.put_attribute(__MODULE__, :optional_fields, unquote(field))
else
Module.put_attribute(__MODULE__, :required_fields, unquote(field))
end
end
end
defguardp is_self(self) when is_atom(elem(self, 0)) and is_nil(elem(self, 2))
defguardp is_args(args) when is_list(args)
defp extract_call({:"::", _, [call_def, return]}) do
{name, args} = extract_call_def(call_def)
{name, args, return}
end
defp extract_call_def({name, _, []}) do
{name, []}
end
defp extract_call_def({name, _, [self]}) when is_self(self) do
{name, {self, []}}
end
defp extract_call_def({name, _, [args]}) when is_args(args) do
{name, args}
end
defp extract_call_def({name, _, [self, args]}) when is_self(self) and is_args(args) do
{name, {self, args}}
end
defp compile_args({_, args}) do
compile_args(args)
end
defp compile_args(args) do
for {name, spec} <- args do
type = compile_typespec!(spec)
meta = spec |> extract_options() |> Keyword.put(:type, type)
{name, Meta.validate!(meta)}
end
end
defp compile_typespec!({:integer, _, []}), do: :integer
defp compile_typespec!({:float, _, []}), do: :float
defp compile_typespec!({:boolean, _, []}), do: :boolean
## String
defp compile_typespec!({:binary, _, []}), do: :string
defp compile_typespec!(
{{:., _,
[
{:__aliases__, _, [:String]},
:t
]}, _, []}
) do
:string
end
defp compile_typespec!({{:., _, [{:__aliases__, _, module}, :t]}, _, []}) do
Module.concat(module)
end
## List
defp compile_typespec!({:list, _, [type]}) do
{:list, compile_typespec!(type)}
end
defp compile_typespec!([type]) do
{:list, compile_typespec!(type)}
end
## Optional
defp compile_typespec!({:|, _, [type, nil]}) do
{:optional, compile_typespec!(type)}
end
## Type with options
defp compile_typespec!({type, _}) do
compile_typespec!(type)
end
defp compile_typespec!(unsupported_type) do
raise ArgumentError, "type `#{Macro.to_string(unsupported_type)}` is not supported"
end
defp extract_options({_, options}), do: options
defp extract_options(_), do: []
end