From e5549ff3503025dac5e017236a3da2a7f1ce09d5 Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Sun, 22 Feb 2026 13:38:33 -0600 Subject: [PATCH 1/2] chore: various wardening --- .github/workflows/ci.yml | 30 ++++++++++++------- .travis.yml | 9 ------ CHANGELOG.md | 2 +- LICENSE | 2 +- README.md | 2 +- lib/commandex.ex | 62 +++++++++++++++++++++++----------------- mix.exs | 2 +- 7 files changed, 59 insertions(+), 50 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80fe825..ab60356 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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: @@ -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 @@ -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: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cdc457d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: elixir -matrix: - include: - - elixir: 1.9 - otp_release: 22.0 - - elixir: 1.10 - otp_release: 22.0 - - elixir: 1.11 - otp_release: 23.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 81eef46..8c349af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/LICENSE b/LICENSE index 317b910..03a5b2c 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index 0d8020a..8c428c8 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/commandex.ex b/lib/commandex.ex index 842570b..4981aeb 100644 --- a/lib/commandex.ex +++ b/lib/commandex.ex @@ -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"} @@ -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, [:additonal, "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. @@ -138,19 +138,19 @@ 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() defmacro command(do: block) do prelude = quote do @@ -206,7 +206,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 @@ -215,7 +215,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 @@ -227,7 +227,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 -> @@ -263,7 +263,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)) @@ -284,7 +284,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)) @@ -297,7 +297,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 @@ -311,12 +311,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, [:additonal, "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)) @@ -337,7 +337,7 @@ defmodule Commandex do 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 @@ -353,7 +353,7 @@ 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 @@ -361,7 +361,7 @@ defmodule Commandex do @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 @@ -389,10 +389,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} @@ -404,6 +406,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 @@ -424,6 +427,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) @@ -435,6 +440,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) @@ -445,6 +452,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 @@ -469,6 +478,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 -> diff --git a/mix.exs b/mix.exs index 301908e..6707865 100644 --- a/mix.exs +++ b/mix.exs @@ -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 From 544c0945ee20f84109ff33ba7c527cbc99d8c08c Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Sun, 22 Feb 2026 13:49:30 -0600 Subject: [PATCH 2/2] test: add more tests Also: pass credo --- lib/commandex.ex | 9 +++++---- test/support/register_user.ex | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/commandex.ex b/lib/commandex.ex index 4981aeb..5d7965b 100644 --- a/lib/commandex.ex +++ b/lib/commandex.ex @@ -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 @@ -112,7 +112,7 @@ 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. @@ -151,6 +151,7 @@ defmodule Commandex do Defines a command struct with params, data, and pipelines. """ @spec command(do: any()) :: no_return() + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity defmacro command(do: block) do prelude = quote do @@ -311,7 +312,7 @@ 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. @@ -330,7 +331,7 @@ 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 diff --git a/test/support/register_user.ex b/test/support/register_user.ex index ab208a3..48bc3d7 100644 --- a/test/support/register_user.ex +++ b/test/support/register_user.ex @@ -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