Skip to content
Open
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
7 changes: 6 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export_locals_without_parens = [defbypass: 2, deftrans: 2]

[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
plugins: [ExFSM.FormatterPlugin],
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
locals_without_parens: export_locals_without_parens,
export: [locals_without_parens: export_locals_without_parens]
]
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: CI

on:
pull_request:
branches:
- master
push:
branches:
- master

jobs:
mix-test:
runs-on: ubuntu-22.04

strategy:
fail-fast: false
matrix:
include:
- elixir: "1.15"
otp: "25"
- elixir: "1.17"
otp: "27"
- elixir: "1.18"
otp: "27"
- elixir: "1.19"
otp: "28"
lint: lint

steps:
- uses: actions/checkout@v5

- uses: erlef/setup-beam@v1
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}

- uses: actions/cache@v4
with:
path: |
deps
_build
key: deps-${{ runner.os }}-${{matrix.otp}}-${{matrix.elixir}}-${{ hashFiles('**/mix.lock') }}
restore-keys: deps-${{ runner.os }}-${{matrix.otp}}-${{matrix.elixir}}

- run: mix deps.get

- run: mix format --check-formatted
if: ${{ matrix.lint }}

- run: mix deps.unlock --check-unused
if: ${{ matrix.lint }}

- run: mix deps.compile

- run: mix compile --warnings-as-errors
if: ${{ matrix.lint }}

- run: mix test
24 changes: 0 additions & 24 deletions .travis.yml

This file was deleted.

29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Changelog

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.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 1.0.0 - 2026/07/27

### Added

- Macros `deftrans` and `defbypass` are now usable without parenthesis when
importing the dependency in your `.formatter.exs` file.
- Macros `deftrans` and `defbypass` now supports `when` clause.
- Sigil `ExFSM.sigil_FSM/2` and its formatter plugin `ExFSM.FormatterPlugin`.
- `use ExFSM` now accepts the options `:with_action_name_macros` and
`:with_action_names_guard`.

### Fixed

- Detect output states from transition with multiple function heads correctly
which fixes the returned output state of the `fsm/0` function.

### Changed

- **BREAKING**: the reserved Elixir `@doc` attribute used to add documentation
transitions and bypasses was removed as it emitted warnings when used on
a transition or bypasss with several heads. Instead use the `@transition_doc`
for transition and the `@bypass_doc` for bypasses.
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
# ExFSM #

[![Build Status](https://travis-ci.org/kbrw/exfsm.svg?branch=master)](https://travis-ci.org/kbrw/exfsm)
[![Build Status](https://github.com/kbrw/exfsm/actions/workflows/.github/workflows/ci.yml/badge.svg)](https://github.com/kbrw/exfsm/actions/workflows/ci.yml)

Simple elixir library to define composable FSM as function
Simple Elixir library to define composable [mealy
FSM](https://en.wikipedia.org/wiki/Mealy_machine) as function
(not related at all with `:gen_fsm`, no state/process management).

- define FSM with handler modules defining each transition as a simple function but using a
```elixir
defmodule Ligh do

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
defmodule Ligh do
defmodule Light do

use ExFSM

deftrans on({:off, _}, state), do: {:next_state, :off, state}
deftrans off({:on, _}, state), do: {:next_state, :on, state}
end

defmodule Light.State do
@enforced_keys [:state]
defstruct @enforced_keys
end

defimpl ExFSM.Machine.State, for: Light.State do
def state_name(state), do: state.state
def set_state_name(state, state_name), do: struct(state, state: state_name)
def handlers(_staet), do: [Light]
end

{:next_state, %_{state: :off} = state} = ExFSM.Machine.event({:on, nil}, %Light{state: :off})
{:next_state, %_{state: :on} = state} = ExFSM.Machine.event({:off, nil}, %Light{state: :on})
{:error, :illegal_action} = ExFSM.Machine.event({:on, nil}, %Light{state: :on})
```

- define an FSM with handler modules defining each transition as a simple function but using a
macro `deftrans` which creates a function `fsm` returning the fsm transition map for this handler module.
- `deftrans` has the same semantic as [erlang in memory FSM gen_fsm](http://www.erlang.org/doc/man/gen_fsm.html)
- combine together multiple fsm handlers to create a "meta" FSM.
- combine together multiple FSM handlers to create a "meta" FSM.
- send event with the function `event` which simply find the right
handler, execute the handler function.


## Usage ##

See in [in code documentation](http://hexdocs.pm/exfsm)
Expand Down
41 changes: 41 additions & 0 deletions lib/dummy_fsm.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule ExFSM.Dummy.FSM.Instance do
@moduledoc false

@type t :: %__MODULE__{
type: atom(),
state: atom()
}

@behaviour Access

defstruct type: nil, state: nil

@impl Access
defdelegate fetch(obj, key), to: Map

@impl Access
defdelegate get_and_update(data, key, function), to: Map

@impl Access
defdelegate pop(data, key), to: Map
end

defimpl ExFSM.Machine.State, for: ExFSM.Dummy.FSM.Instance do
def state_name(instance), do: instance.state
def set_state_name(instance, state_name), do: Map.put(instance, :state, state_name)
def handlers(_state), do: [ExFSM.Dummy.FSM]
end

defmodule ExFSM.Dummy.FSM do
@moduledoc false

use ExFSM

~FSM"""
opened -- close -> closed
closed -- open -> opened
"""

deftrans opened({:close, _}, state), do: {:next_state, :closed, state}
deftrans closed({:open, _}, state), do: {:next_state, :opened, state}
end
Loading
Loading