Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
Expand All @@ -22,15 +22,15 @@ jobs:
- name: Run Prettier
run: prettier --check --no-error-on-unmatched-pattern "**/*.{json,md,yml,yaml}"
check:
name: Format
name: Format/Credo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: "1.19.4"
otp-version: "28.2"
elixir-version: "1.19"
otp-version: "28"
- name: Restore dependencies cache
uses: actions/cache@v3
with:
Expand All @@ -41,22 +41,30 @@ jobs:
run: mix deps.get
- name: Run formatter
run: mix format --check-formatted
- name: Run Credo
run: mix credo
dialyzer:
name: Dialyzer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: "1.19.4"
otp-version: "28.2"
elixir-version: "1.19"
otp-version: "28"
- name: Restore dependencies cache
uses: actions/cache@v3
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Restore dialyzer cache
uses: actions/cache@v3
with:
path: priv/plts
key: ${{ runner.os }}-mix-plts-${{ hashFiles('./priv/plts/') }}
restore-keys: ${{ runner.os }}-mix-plts-
- name: Install dependencies
run: mix deps.get
- name: Run dialyzer
Expand All @@ -65,12 +73,12 @@ jobs:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: "1.19.4"
otp-version: "28.2"
elixir-version: "1.19"
otp-version: "28"
- name: Restore dependencies cache
uses: actions/cache@v3
with:
Expand Down
9 changes: 0 additions & 9 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.2] - 2025-12-21
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020-2025 Codedge LLC (https://www.codedge.io/)
Copyright (c) 2020-2026 Codedge LLC (https://www.codedge.io/)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,6 @@ Git commit subjects use the [Karma style](http://karma-runner.github.io/5.0/dev/

## License

Copyright (c) 2020-2025 Codedge LLC (https://www.codedge.io/)
Copyright (c) 2020-2026 Codedge LLC (https://www.codedge.io/)

This library is MIT licensed. See the [LICENSE](https://github.com/codedge-llc/commandex/blob/main/LICENSE) for details.
67 changes: 39 additions & 28 deletions lib/commandex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ defmodule Commandex do

`&run/1` takes a command struct and runs it through the pipeline functions defined
in the command. **Functions are executed in the order in which they are defined**.
If a command passes through all pipelines without calling `halt/1`, `:success`
will be set to `true`. Otherwise, subsequent pipelines after the `halt/1` will
If a command passes through all pipelines without calling `halt/1`, `:success`
will be set to `true`. Otherwise, subsequent pipelines after the `halt/1` will
be ignored and `:success` will be set to `false`.

%{email: "example@example.com", password: "asdf1234"}
Expand All @@ -85,7 +85,7 @@ defmodule Commandex do
%{success: false, errors: %{password: :not_given}} ->
# Respond with a 400 or something

%{success: false, errors: _error} ->
%{success: false, errors: _errors} ->
# I'm a lazy programmer that writes catch-all error handling
end

Expand All @@ -112,17 +112,17 @@ defmodule Commandex do

- `pipeline :do_work` - Name of a function inside the command's module, arity three.
- `pipeline {YourModule, :do_work}` - Arity three.
- `pipeline {YourModule, :do_work, [:additonal, "args"]}` - Arity three plus the
- `pipeline {YourModule, :do_work, [:additional, "args"]}` - Arity three plus the
number of additional args given.
- `pipeline &YourModule.do_work/1` - Or any anonymous function of arity one.
- `pipeline &YourModule.do_work/3` - Or any anonymous function of arity three.
"""
@type pipeline ::
atom
| {module, atom}
| {module, atom, [any]}
| (command :: struct -> command :: struct)
| (command :: struct, params :: map, data :: map -> command :: struct)
atom()
| {module(), atom()}
| {module(), atom(), [any()]}
| (command :: struct() -> command :: struct())
| (command :: struct(), params :: map(), data :: map() -> command :: struct())

@typedoc """
Command struct.
Expand All @@ -138,19 +138,20 @@ defmodule Commandex do
`true` if the command was not halted after running all of the pipelines.
"""
@type command :: %{
__struct__: atom,
data: map,
errors: map,
halted: boolean,
params: map,
__struct__: atom(),
data: map(),
errors: map(),
halted: boolean(),
params: map(),
pipelines: [pipeline()],
success: boolean
success: boolean()
}

@doc """
Defines a command struct with params, data, and pipelines.
"""
@spec command(do: any) :: no_return
@spec command(do: any()) :: no_return()
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defmacro command(do: block) do
prelude =
quote do
Expand Down Expand Up @@ -206,7 +207,7 @@ defmodule Commandex do
@doc """
Creates a new struct from given parameters.
"""
@spec new(map | Keyword.t()) :: t
@spec new(map() | Keyword.t()) :: t()
def new(opts \\ []) do
Commandex.parse_params(%__MODULE__{}, opts)
end
Expand All @@ -215,7 +216,7 @@ defmodule Commandex do
@doc """
Runs given pipelines in order and returns command struct.
"""
@spec run :: t
@spec run() :: t()
def run do
new() |> run()
end
Expand All @@ -227,7 +228,7 @@ defmodule Commandex do
`run/1` can either take parameters that would be passed to `new/1`
or the command struct itself.
"""
@spec run(map | Keyword.t() | t) :: t
@spec run(map() | Keyword.t() | t()) :: t()
def run(%unquote(__MODULE__){pipelines: pipelines} = command) do
pipelines
|> Enum.reduce_while(command, fn fun, acc ->
Expand Down Expand Up @@ -263,7 +264,7 @@ defmodule Commandex do
# ...pipelines
end
"""
@spec param(atom, Keyword.t()) :: no_return
@spec param(atom(), Keyword.t()) :: no_return()
defmacro param(name, opts \\ []) do
quote do
Commandex.__param__(__MODULE__, unquote(name), unquote(opts))
Expand All @@ -284,7 +285,7 @@ defmodule Commandex do
# ...pipelines
end
"""
@spec data(atom) :: no_return
@spec data(atom()) :: no_return()
defmacro data(name) do
quote do
Commandex.__data__(__MODULE__, unquote(name))
Expand All @@ -297,7 +298,7 @@ defmodule Commandex do
Pipelines are functions executed against the command, *in the order in which they are defined*.

For example, two pipelines could be defined:

pipeline :check_valid_email
pipeline :create_user

Expand All @@ -311,12 +312,12 @@ defmodule Commandex do

- `pipeline :do_work` - Name of a function inside the command's module, arity three.
- `pipeline {YourModule, :do_work}` - Arity three.
- `pipeline {YourModule, :do_work, [:additonal, "args"]}` - Arity three plus the
- `pipeline {YourModule, :do_work, [:additional, "args"]}` - Arity three plus the
number of additional args given.
- `pipeline &YourModule.do_work/1` - Or any anonymous function of arity one.
- `pipeline &YourModule.do_work/3` - Or any anonymous function of arity three.
"""
@spec pipeline(atom) :: no_return
@spec pipeline(atom()) :: no_return()
defmacro pipeline(name) do
quote do
Commandex.__pipeline__(__MODULE__, unquote(name))
Expand All @@ -330,14 +331,14 @@ defmodule Commandex do

data :password_hash

Set the password pash in one of your pipeline functions:
Set the password hash in one of your pipeline functions:

def hash_password(command, %{password: password} = _params, _data) do
# Better than plaintext, I guess
put_data(command, :password_hash, Base.encode64(password))
end
"""
@spec put_data(command, atom, any) :: command
@spec put_data(command(), atom(), any()) :: command()
def put_data(%{data: data} = command, key, val) do
%{command | data: Map.put(data, key, val)}
end
Expand All @@ -353,15 +354,15 @@ defmodule Commandex do
|> halt()
end
"""
@spec put_error(command, any, any) :: command
@spec put_error(command(), any(), any()) :: command()
def put_error(%{errors: error} = command, key, val) do
%{command | errors: Map.put(error, key, val)}
end

@doc """
Halts a command pipeline.

Any pipelines defined after the halt will be ignored. By default, if a command finishes
Any pipelines defined after the halt will be ignored. By default, if a command finishes
running through all pipelines, `:success` will be set to `true`.

def hash_password(command, %{password: nil} = _params, _data) do
Expand Down Expand Up @@ -389,10 +390,12 @@ defmodule Commandex do
end

@doc false
@spec maybe_mark_successful(command()) :: command()
def maybe_mark_successful(%{halted: false} = command), do: %{command | success: true}
def maybe_mark_successful(command), do: command

@doc false
@spec parse_params(command(), map() | Keyword.t()) :: command()
def parse_params(%{params: p} = struct, params) when is_list(params) do
params = for {key, _} <- p, into: %{}, do: {key, Keyword.get(params, key, p[key])}
%{struct | params: params}
Expand All @@ -404,6 +407,7 @@ defmodule Commandex do
end

@doc false
@spec apply_fun(command(), pipeline()) :: command()
def apply_fun(%mod{params: params, data: data} = command, name) when is_atom(name) do
:erlang.apply(mod, name, [command, params, data])
end
Expand All @@ -424,6 +428,8 @@ defmodule Commandex do
:erlang.apply(m, f, [command, params, data] ++ a)
end

@doc false
@spec __param__(module(), atom(), Keyword.t()) :: :ok
def __param__(mod, name, opts) do
params = Module.get_attribute(mod, :params)

Expand All @@ -435,6 +441,8 @@ defmodule Commandex do
Module.put_attribute(mod, :params, {name, default})
end

@doc false
@spec __data__(module(), atom()) :: :ok
def __data__(mod, name) do
data = Module.get_attribute(mod, :data)

Expand All @@ -445,6 +453,8 @@ defmodule Commandex do
Module.put_attribute(mod, :data, {name, nil})
end

@doc false
@spec __pipeline__(module(), pipeline()) :: :ok
def __pipeline__(mod, name) when is_atom(name) do
Module.put_attribute(mod, :pipelines, name)
end
Expand All @@ -469,6 +479,7 @@ defmodule Commandex do
raise ArgumentError, "pipeline #{inspect(name)} is not valid"
end

@spec get_param(map(), atom(), term()) :: term()
defp get_param(params, key, default) do
case Map.get(params, key) do
nil ->
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule Commandex.MixProject do
elixirc_paths: elixirc_paths(Mix.env()),
name: "Commandex",
package: package(),
source_url: "https://github.com/codedge-llc/commandex",
source_url: @source_url,
start_permanent: Mix.env() == :prod,
test_coverage: test_coverage(),
version: @version
Expand Down
1 change: 1 addition & 0 deletions test/support/register_user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule RegisterUser do
pipeline :verify_tos
pipeline :create_user
pipeline :record_auth_attempt
# credo:disable-for-next-line Credo.Check.Warning.IoInspect
pipeline &IO.inspect/1
end

Expand Down