Restructure as monorepo and add flake packages/apps

- Move Elixir code to server/ subdirectory for monorepo structure
- Update flake.nix to provide packages and apps outputs for nix run support
- Update nix/package.nix to accept src parameter instead of fetchgit
- Add NixOS module export for easy consumption

Now supports: nix run, nix build, and nix develop from git repo

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-02 19:54:17 -07:00
co-authored by Claude
parent 46e585ec92
commit b6769abbe9
197 changed files with 9555 additions and 7 deletions
Binary file not shown.
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+49
View File
@@ -0,0 +1,49 @@
[![Elixir CI](https://github.com/ericentin/gen_state_machine/workflows/Elixir%20CI/badge.svg)](https://github.com/ericentin/gen_state_machine/actions?query=workflow%3A%22Elixir+CI%22)
# GenStateMachine
An idiomatic Elixir wrapper for `gen_statem` in OTP 19 (and above).
**Full documentation is available [here](https://hexdocs.pm/gen_state_machine).**
**You can find the package on Hex [here](https://hex.pm/packages/gen_state_machine).**
One important difference between `gen_statem` and this wrapper is that you
declare your callback mode as part of `use GenStateMachine` in this wrapper,
rather than returning it from `callback_mode/0` on OTP 19.1 and up, or your
`init/1` and `code_change/4` on versions of OTP prior to 19.1. In versions of
OTP prior to 19.1, you can still, however, switch callback modes in
`code_change/4` by returning a callback mode.
Other than that (and the usual automatically-defined default callbacks as a
result of `use`-ing `GenStateMachine`), this wrapper does not make any
functional alterations.
This wrapper also provides a OTP error translator for `Logger`, which is
automatically added when the `:gen_state_machine` application is started.
Optionally, you may add `:gen_state_machine` to `:included_applications` rather
than `:applications` as indicated below if you do not want the translator to be
added to `Logger`.
## Installation
1. Add `gen_state_machine` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:gen_state_machine, "~> 2.0"}]
end
```
2. Ensure `gen_state_machine` is added to your applications:
```elixir
def application do
[applications: [:gen_state_machine]]
end
```
## Special Thanks
I would like to give special thanks to @fishcakez and @michalmuskala, who both
provided invaluable feedback on this library!
@@ -0,0 +1,15 @@
{<<"app">>,<<"gen_state_machine">>}.
{<<"build_tools">>,[<<"mix">>]}.
{<<"description">>,<<"An Elixir wrapper for gen_statem.">>}.
{<<"elixir">>,<<"~> 1.5">>}.
{<<"files">>,
[<<"lib">>,<<"lib/gen_state_machine">>,
<<"lib/gen_state_machine/application.ex">>,
<<"lib/gen_state_machine/translator.ex">>,<<"lib/gen_state_machine.ex">>,
<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>]}.
{<<"licenses">>,[<<"Apache 2.0">>]}.
{<<"links">>,
[{<<"GitHub">>,<<"https://github.com/ericentin/gen_state_machine">>}]}.
{<<"name">>,<<"gen_state_machine">>}.
{<<"requirements">>,[]}.
{<<"version">>,<<"3.0.0">>}.
@@ -0,0 +1,773 @@
defmodule GenStateMachine do
@moduledoc """
A behaviour module for implementing a state machine.
The advantage of using this module is that it will have a standard set of
interface functions and include functionality for tracing and error reporting.
It will also fit into a supervision tree.
## Example
The `GenStateMachine` behaviour abstracts the state machine. Developers are only
required to implement the callbacks and functionality they are interested in.
Let's start with a code example and then explore the available callbacks.
Imagine we want a `GenStateMachine` that works like a switch, allowing us to
turn it on and off, as well as see how many times the switch has been turned
on:
defmodule Switch do
use GenStateMachine
# Callbacks
def handle_event(:cast, :flip, :off, data) do
{:next_state, :on, data + 1}
end
def handle_event(:cast, :flip, :on, data) do
{:next_state, :off, data}
end
def handle_event({:call, from}, :get_count, state, data) do
{:next_state, state, data, [{:reply, from, data}]}
end
end
# Start the server
{:ok, pid} = GenStateMachine.start_link(Switch, {:off, 0})
# This is the client
GenStateMachine.cast(pid, :flip)
#=> :ok
GenStateMachine.call(pid, :get_count)
#=> 1
We start our `Switch` by calling `start_link/3`, passing the module with the
server implementation and its initial argument, a tuple where the first element
is the initial state and the second is the initial data. We can primarily
interact with the state machine by sending two types of messages. **call**
messages expect a reply from the server (and are therefore synchronous) while
**cast** messages do not.
Every time you do a `call/3` or a `cast/2`, the message will be handled by
`handle_event/4`.
We can also use the `:state_functions` callback mode instead of the default,
which is `:handle_event_function`:
defmodule Switch do
use GenStateMachine, callback_mode: :state_functions
def off(:cast, :flip, data) do
{:next_state, :on, data + 1}
end
def off({:call, from}, :get_count, data) do
{:keep_state_and_data, [{:reply, from, data}]}
end
def on(:cast, :flip, data) do
{:next_state, :off, data}
end
def on({:call, from}, :get_count, data) do
{:keep_state_and_data, [{:reply, from, data}]}
end
end
# Start the server
{:ok, pid} = GenStateMachine.start_link(Switch, {:off, 0})
# This is the client
GenStateMachine.cast(pid, :flip)
#=> :ok
GenStateMachine.call(pid, :get_count)
#=> 1
Again, we start our `Switch` by calling `start_link/3`, passing the module
with the server implementation and its initial argument, and interacting with
it via **call** and **cast**.
However, in `:state_functions` callback mode, every time you do a `call/3` or
a `cast/2`, the message will be handled by the `state_name/3` function which
is named the same as the current state.
## Callbacks
In the default `:handle_event_function` callback mode, there are 4 callbacks
required to be implemented. By adding `use GenStateMachine` to your module,
Elixir will automatically define all 4 callbacks for you, leaving it up to you
to implement the ones you want to customize.
In the `:state_functions` callback mode, there are 3 callbacks required to be
implemented. By adding `use GenStateMachine, callback_mode: :state_functions`
to your module, Elixir will automatically define all 3 callbacks for you,
leaving it up to you to implement the ones you want to customize, as well as
`state_name/3` functions named the same as the states you wish to support.
It is important to note that the default implementation of the `code_change/4`
callback results in an `:undefined` error. This is because `code_change/4` is
related to the quite difficult topic of hot upgrades, and if you need it, you
should really be implementing it yourself. In normal use this callback will
not be invoked.
## State Enter Callbacks
If you wish, you can register to receive callbacks when you enter a state by
adding `:state_enter` to your `callback_mode`:
defmodule Switch do
use GenStateMachine, callback_mode: [:handle_event_function, :state_enter]
def handle_event(:enter, _event, state, data) do
{:next_state, state, %{data | enters: data.enters + 1}}
end
def handle_event(:cast, :flip, :off, data) do
{:next_state, :on, %{data | flips: data.flips + 1}}
end
def handle_event(:cast, :flip, :on, data) do
{:next_state, :off, data}
end
def handle_event({:call, from}, :get_count, _state, data) do
{:keep_state_and_data, [{:reply, from, data}]}
end
end
# Start the server
{:ok, pid} = GenStateMachine.start_link(Switch, {:off, %{enters: 0, flips: 0}})
# This is the client
GenStateMachine.cast(pid, :flip)
GenStateMachine.cast(pid, :flip)
#=> :ok
GenStateMachine.call(pid, :get_count)
#=> %{enters: 3, flips: 1}
See the Erlang [documentation](https://erlang.org/doc/man/gen_statem.html#type-state_enter)
for more details.
## Name Registration
Both `start_link/3` and `start/3` support registering the `GenStateMachine`
under a given name on start via the `:name` option. Registered names are also
automatically cleaned up on termination. The supported values are:
* an atom - the `GenStateMachine` is registered locally with the given name
using `Process.register/2`.
* `{:global, term}`- the `GenStateMachine` is registered globally with the
given term using the functions in the `:global` module.
* `{:via, module, term}` - the `GenStateMachine` is registered with the given
mechanism and name. The `:via` option expects a module that exports
`register_name/2`, `unregister_name/1`, `whereis_name/1` and `send/2`.
One such example is the `:global` module which uses these functions
for keeping the list of names of processes and their associated pid's
that are available globally for a network of Erlang nodes.
For example, we could start and register our Switch server locally as follows:
# Start the server and register it locally with name MySwitch
{:ok, _} = GenStateMachine.start_link(Switch, {:off, 0}, name: MySwitch)
# Now messages can be sent directly to MySwitch
GenStateMachine.call(MySwitch, :get_count) #=> 0
Once the server is started, the remaining functions in this module (`call/3`,
`cast/2`, and friends) will also accept an atom, or any `:global` or `:via`
tuples. In general, the following formats are supported:
* a `pid`
* an `atom` if the server is locally registered
* `{atom, node}` if the server is locally registered at another node
* `{:global, term}` if the server is globally registered
* `{:via, module, name}` if the server is registered through an alternative
registry
## Client / Server APIs
Although in the example above we have used `GenStateMachine.start_link/3` and
friends to directly start and communicate with the server, most of the
time we don't call the `GenStateMachine` functions directly. Instead, we wrap
the calls in new functions representing the public API of the server.
Here is a better implementation of our Switch module:
defmodule Switch do
use GenStateMachine
# Client
def start_link() do
GenStateMachine.start_link(Switch, {:off, 0})
end
def flip(pid) do
GenStateMachine.cast(pid, :flip)
end
def get_count(pid) do
GenStateMachine.call(pid, :get_count)
end
# Server (callbacks)
def handle_event(:cast, :flip, :off, data) do
{:next_state, :on, data + 1}
end
def handle_event(:cast, :flip, :on, data) do
{:next_state, :off, data}
end
def handle_event({:call, from}, :get_count, state, data) do
{:next_state, state, data, [{:reply, from, data}]}
end
def handle_event(event_type, event_content, state, data) do
# Call the default implementation from GenStateMachine
super(event_type, event_content, state, data)
end
end
In practice, it is common to have both server and client functions in
the same module. If the server and/or client implementations are growing
complex, you may want to have them in different modules.
## Receiving custom messages
The goal of a `GenStateMachine` is to abstract the "receive" loop for
developers, automatically handling system messages, support code change,
synchronous calls and more. Therefore, you should never call your own
"receive" inside the `GenStateMachine` callbacks as doing so will cause the
`GenStateMachine` to misbehave. If you want to receive custom messages, they
will be delivered to the usual handler for your callback mode with event_type
`:info`.
## Learn more
If you wish to find out more about gen_statem, the documentation and links
in Erlang can provide extra insight.
* [`:gen_statem` module documentation](http://erlang.org/doc/man/gen_statem.html)
* [gen_statem Behaviour OTP Design Principles](http://erlang.org/doc/design_principles/statem.html)
"""
@typedoc """
The term representing the current state.
In `:handle_event_function` callback mode, any term.
In `:state_functions` callback mode, an atom.
"""
@type state :: state_name | term
@typedoc """
The atom representing the current state in `:state_functions` callback mode.
"""
@type state_name :: atom
@typedoc """
The persistent data (similar to a GenServer's `state`) for the GenStateMachine.
"""
@type data :: term
@typedoc """
The source of the current event.
`{:call, from}` will be received as a result of a call.
`:cast` will be received as a result of a cast.
`:info` will be received as a result of any regular process messages received.
`:timeout` or `{:timeout, name}` will be received as a result of a `:timeout` action.
`:state_timeout` will be received as a result of a `:state_timeout` action.
`:internal` will be received as a result of a `:next_event` action.
See the Erlang
[documentation](https://erlang.org/doc/man/gen_statem.html#type-event_type)
for details.
"""
@type event_type :: :gen_statem.event_type()
@typedoc """
The callback mode for the GenStateMachine.
See the Example section above for more info.
"""
@type callback_mode_result :: :gen_statem.callback_mode_result()
@typedoc """
The message content received as the result of an event.
"""
@type event_content :: term
@typedoc """
State transition actions.
They may be invoked by returning them from a state function or init/1.
If present in a list of actions, they are executed in order, and any that set
transition options (postpone, hibernate, and timeout) override any previously
provided options of the same type.
If the state changes, the queue of incoming events is reset to start with the
oldest postponed.
All events added as a result of a `:next_event` action are inserted in the
queue to be processed before all other events. An event of type `:internal`
should be used when you want to reliably distinguish an event inserted this
way from an external event.
See the Erlang
[documentation](https://erlang.org/doc/man/gen_statem.html#type-action)
for the possible values.
"""
@type action :: :gen_statem.action()
@typedoc """
The return type of an event handler function.
See the Erlang [documentation](https://erlang.org/doc/man/gen_statem.html#type-event_handler_result)
for a complete reference.
"""
@type event_handler_result(state) :: :gen_statem.event_handler_result(state)
@typedoc """
The return type of a state enter function.
See the Erlang [documentation](https://erlang.org/doc/man/gen_statem.html#type-state_enter_result)
for a complete reference.
"""
@type state_enter_result(state) :: :gen_statem.state_enter_result(state)
@typedoc """
The return type when the server is started.
See the Erlang [documentation](https://erlang.org/doc/man/gen_statem.html#type-start_ret)
for a complete reference.
"""
@type on_start :: :gen_statem.start_ret()
@typedoc """
The type of allowed server names.
See the Erlang [documentation](https://erlang.org/doc/man/gen_statem.html#type-server_ref)
for a complete reference.
"""
@type server_ref :: :gen_statem.server_ref()
@doc """
Invoked when the server is started. `start_link/3` (or `start/3`) will
block until it returns.
`args` is the argument term (second argument) passed to `start_link/3`.
Returning `{:ok, state, data}` will cause `start_link/3` to return
`{:ok, pid}` and the process to enter its loop.
Returning `{:ok, state, data, actions}` is similar to `{:ok, state}`
except the provided actions will be executed.
Returning `:ignore` will cause `start_link/3` to return `:ignore` and the
process will exit normally without entering the loop or calling `terminate/2`.
If used when part of a supervision tree the parent supervisor will not fail
to start nor immediately try to restart the `GenStateMachine`. The remainder
of the supervision tree will be (re)started and so the `GenStateMachine`
should not be required by other processes. It can be started later with
`Supervisor.restart_child/2` as the child specification is saved in the parent
supervisor. The main use cases for this are:
* The `GenStateMachine` is disabled by configuration but might be enabled
later.
* An error occurred and it will be handled by a different mechanism than the
`Supervisor`. Likely this approach involves calling
`Supervisor.restart_child/2` after a delay to attempt a restart.
Returning `{:stop, reason}` will cause `start_link/3` to return
`{:error, reason}` and the process to exit with reason `reason` without
entering the loop or calling `terminate/2`.
This function can optionally throw a result to return it.
"""
@callback init(args :: term) :: :gen_statem.init_result(state)
@doc """
Whenever a `GenStateMachine` in callback mode `:state_functions` receives a
call, cast, or normal process message, a state function is called.
This spec exists to document the callback, but in actual use the name of the
function is probably not going to be `state_name`. Instead, there will be at
least one state function named after each state you wish to handle. See the
Examples section above for more info.
These functions can optionally throw a result to return it.
See the Erlang [documentation](https://erlang.org/doc/man/gen_statem.html#Module:StateName-3)
for a complete reference.
"""
@callback state_name(:enter, old_state_name :: state_name, data) ::
state_enter_result(state_name)
@callback state_name(event_type, event_content, data) :: event_handler_result(state_name)
@doc """
Whenever a `GenStateMachine` in callback mode `:handle_event_function` (the
default) receives a call, cast, or normal process messsage, this callback will
be invoked.
This function can optionally throw a result to return it.
See the Erlang [documentation](https://erlang.org/doc/man/gen_statem.html#Module:handle_event-4)
for a complete reference.
"""
@callback handle_event(:enter, old_state :: state, state, data) :: state_enter_result(state)
@callback handle_event(event_type, event_content, state, data) :: event_handler_result(state)
@doc """
Invoked when the server is about to exit. It should do any cleanup required.
`reason` is exit reason, `state` is the current state, and `data` is the
current data of the `GenStateMachine`. The return value is ignored.
`terminate/2` is called if a callback (except `init/1`) returns a `:stop`
tuple, raises, calls `Kernel.exit/1` or returns an invalid value. It may also
be called if the `GenStateMachine` traps exits using `Process.flag/2` *and*
the parent process sends an exit signal.
If part of a supervision tree a `GenStateMachine`'s `Supervisor` will send an
exit signal when shutting it down. The exit signal is based on the shutdown
strategy in the child's specification. If it is `:brutal_kill` the
`GenStateMachine` is killed and so `terminate/2` is not called. However if it
is a timeout the `Supervisor` will send the exit signal `:shutdown` and the
`GenStateMachine` will have the duration of the timeout to call `terminate/2`
- if the process is still alive after the timeout it is killed.
If the `GenStateMachine` receives an exit signal (that is not `:normal`) from
any process when it is not trapping exits it will exit abruptly with the same
reason and so not call `terminate/2`. Note that a process does *NOT* trap
exits by default and an exit signal is sent when a linked process exits or its
node is disconnected.
Therefore it is not guaranteed that `terminate/2` is called when a
`GenStateMachine` exits. For such reasons, we usually recommend important
clean-up rules to happen in separated processes either by use of monitoring or
by links themselves. For example if the `GenStateMachine` controls a `port`
(e.g. `:gen_tcp.socket`) or `File.io_device`, they will be closed on receiving
a `GenStateMachine`'s exit signal and do not need to be closed in
`terminate/2`.
If `reason` is not `:normal`, `:shutdown` nor `{:shutdown, term}` an error is
logged.
This function can optionally throw a result, which is ignored.
"""
@callback terminate(reason :: term, state, data) :: any
@doc """
Invoked to change the state of the `GenStateMachine` when a different version
of a module is loaded (hot code swapping) and the state and/or data's term
structure should be changed.
`old_vsn` is the previous version of the module (defined by the `@vsn`
attribute) when upgrading. When downgrading the previous version is wrapped in
a 2-tuple with first element `:down`. `state` is the current state of the
`GenStateMachine`, `data` is the current data, and `extra` is any extra data
required to change the state.
Returning `{:ok, new_state, new_data}` changes the state to `new_state`, the
data to `new_data`, and the code change is successful.
Returning `reason` fails the code change with reason `reason` and the state
and data remains the same.
If `code_change/4` raises the code change fails and the loop will continue
with its previous state. Therefore this callback does not usually contain side
effects.
This function can optionally throw a result to return it.
"""
@callback code_change(old_vsn :: term | {:down, vsn :: term}, state, data, extra :: term) ::
{:ok, state, data}
| (reason :: term)
@doc """
Invoked in some cases to retrieve a formatted version of the `GenStateMachine`
status.
This callback can be useful to control the *appearance* of the status of the
`GenStateMachine`. For example, it can be used to return a compact
representation of the `GenStateMachines`'s state/data to avoid having large
terms printed.
* one of `:sys.get_status/1` or `:sys.get_status/2` is invoked to get the
status of the `GenStateMachine`; in such cases, `reason` is `:normal`
* the `GenStateMachine` terminates abnormally and logs an error; in such cases,
`reason` is `:terminate`
`pdict_state_and_data` is a three-element list `[pdict, state, data]` where
`pdict` is a list of `{key, value}` tuples representing the current process
dictionary of the `GenStateMachine`, `state` is the current state of the
`GenStateMachine`, and `data` is the current data.
This function can optionally throw a result to return it.
"""
@callback format_status(reason :: :normal | :terminate, pdict_state_and_data :: list) :: term
@optional_callbacks state_name: 3, handle_event: 4, format_status: 2
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour GenStateMachine
{callback_mode, opts} = Keyword.pop(opts, :callback_mode, :handle_event_function)
@doc false
def callback_mode do
unquote(Macro.escape(callback_mode))
end
@doc false
def init({state, data}) do
{:ok, state, data}
end
@doc false
def terminate(_reason, _state, _data) do
:ok
end
@doc false
def code_change(_old_vsn, _state, _data, _extra) do
:undefined
end
unless Module.get_attribute(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
See `Supervisor`.
"""
end
def child_spec(init_arg) do
Supervisor.child_spec(
%{id: __MODULE__, start: {__MODULE__, :start_link, [init_arg]}},
unquote(Macro.escape(opts))
)
end
defoverridable init: 1, terminate: 3, code_change: 4, child_spec: 1
if :handle_event_function in List.wrap(callback_mode) do
@doc false
def handle_event(_event_type, _event_content, _state, _data) do
{:stop, :bad_event}
end
defoverridable handle_event: 4
end
end
end
@doc """
Starts a `GenStateMachine` process linked to the current process.
This is often used to start the `GenStateMachine` as part of a supervision
tree.
Once the server is started, the `init/1` function of the given `module` is
called with `args` as its arguments to initialize the server. To ensure a
synchronized start-up procedure, this function does not return until `init/1`
has returned.
Note that a `GenStateMachine` started with `start_link/3` is linked to the
parent process and will exit in case of crashes from the parent. The
`GenStateMachine` will also exit due to the `:normal` reasons in case it is
configured to trap exits in the `init/1` callback.
## Options
* `:name` - used for name registration as described in the "Name
registration" section of the module documentation
* `:timeout` - if present, the server is allowed to spend the given amount of
milliseconds initializing or it will be terminated and the start function
will return `{:error, :timeout}`
* `:debug` - if present, the corresponding function in the [`:sys`
module](http://www.erlang.org/doc/man/sys.html) is invoked
* `:spawn_opt` - if present, its value is passed as options to the
underlying process as in `Process.spawn/4`
## Return values
If the server is successfully created and initialized, this function returns
`{:ok, pid}`, where `pid` is the pid of the server. If a process with the
specified server name already exists, this function returns
`{:error, {:already_started, pid}}` with the pid of that process.
If the `init/1` callback fails with `reason`, this function returns
`{:error, reason}`. Otherwise, if it returns `{:stop, reason}`
or `:ignore`, the process is terminated and this function returns
`{:error, reason}` or `:ignore`, respectively.
"""
@spec start_link(module, any, GenServer.options()) :: on_start()
def start_link(module, args, options \\ []) do
{name, options} = Keyword.pop(options, :name)
if name do
name =
if is_atom(name) do
{:local, name}
else
name
end
:gen_statem.start_link(name, module, args, options)
else
:gen_statem.start_link(module, args, options)
end
end
@doc """
Starts a `GenStateMachine` process without links (outside of a supervision
tree).
See `start_link/3` for more information.
"""
@spec start(module, any, GenServer.options()) :: on_start()
def start(module, args, options \\ []) do
{name, options} = Keyword.pop(options, :name)
if name do
name =
if is_atom(name) do
{:local, name}
else
name
end
:gen_statem.start(name, module, args, options)
else
:gen_statem.start(module, args, options)
end
end
@doc """
Stops the server with the given `reason`.
The `terminate/2` callback of the given `server` will be invoked before
exiting. This function returns `:ok` if the server terminates with the
given reason; if it terminates with another reason, the call exits.
This function keeps OTP semantics regarding error reporting.
If the reason is any other than `:normal`, `:shutdown` or
`{:shutdown, _}`, an error report is logged.
"""
@spec stop(server_ref(), reason :: term, timeout) :: :ok
def stop(server, reason \\ :normal, timeout \\ :infinity) do
:gen_statem.stop(server, reason, timeout)
end
@doc """
Makes a synchronous call to the `server` and waits for its reply.
The client sends the given `request` to the server and waits until a reply
arrives or a timeout occurs. The appropriate state function will be called on
the server to handle the request.
`server` can be any of the values described in the "Name registration"
section of the documentation for this module.
## Timeouts
`timeout` is an integer greater than zero which specifies how many
milliseconds to wait for a reply, or the atom `:infinity` to wait
indefinitely. The default value is `:infinity`. If no reply is received within
the specified time, the function call fails and the caller exits.
If the caller catches an exit, to avoid getting a late reply in the caller's
inbox, this function spawns a proxy process that does the call. A late reply
gets delivered to the dead proxy process, and hence gets discarded. This is
less efficient than using `:infinity` as a timeout.
"""
@spec call(server_ref(), term, timeout) :: term
def call(server, request, timeout \\ :infinity) do
:gen_statem.call(server, request, timeout)
end
@doc """
Sends an asynchronous request to the `server`.
This function always returns `:ok` regardless of whether
the destination `server` (or node) exists. Therefore it
is unknown whether the destination `server` successfully
handled the message.
The appropriate state function will be called on the server to handle
the request.
"""
@spec cast(server_ref(), term) :: :ok
def cast(server, request) do
:gen_statem.cast(server, request)
end
@doc """
Sends replies to clients.
Can be used to explicitly send replies to multiple clients.
This function always returns `:ok`.
See `reply/2` for more information.
"""
@spec reply([:gen_statem.reply_action()] | :gen_statem.reply_action()) :: :ok
def reply(replies) do
:gen_statem.reply(replies)
end
@doc """
Replies to a client.
This function can be used to explicitly send a reply to a client that called
`call/3` when the reply cannot be specified in the return value of a state
function.
`client` must be the `from` element of the event type accepted by state
functions. `reply` is an arbitrary term which will be given
back to the client as the return value of the call.
Note that `reply/2` can be called from any process, not just the one
that originally received the call (as long as that process communicated the
`from` argument somehow).
This function always returns `:ok`.
## Examples
def handle_event({:call, from}, :reply_in_one_second, state, data) do
Process.send_after(self(), {:reply, from}, 1_000)
:keep_state_and_data
end
def handle_event(:info, {:reply, from}, state, data) do
GenStateMachine.reply(from, :one_second_has_passed)
end
"""
@spec reply(:gen_statem.from(), term) :: :ok
def reply(client, reply) do
:gen_statem.reply(client, reply)
end
end
@@ -0,0 +1,11 @@
defmodule GenStateMachine.Application do
@moduledoc false
use Application
@doc false
def start(_type, _args) do
Logger.add_translator({GenStateMachine.Translator, :translate})
Supervisor.start_link([], strategy: :one_for_one)
end
end
@@ -0,0 +1,131 @@
defmodule GenStateMachine.Translator do
@moduledoc false
@doc false
# OTP21 and after
def translate(min_level, :error, :report, {:logger, %{label: label} = report}) do
case label do
{:gen_statem, :terminate} ->
do_translate(min_level, report)
_ ->
:none
end
end
# OTP20 and before
def translate(min_level, :error, :format, message) do
opts = Application.get_env(:logger, :translator_inspect_opts)
case message do
{'** State machine ~tp terminating~n' ++ _ = format, args} ->
do_translate(min_level, format, args, opts)
{'** State machine ~p terminating~n' ++ _ = format, args} ->
do_translate(min_level, format, args, opts)
_ ->
:none
end
end
def translate(_min_level, _level, _kind, _message) do
:none
end
# OTP21 and after
defp do_translate(min_level, report) do
inspect_opts = Application.get_env(:logger, :translator_inspect_opts)
%{name: name, state: state} = report
msg = ["GenStateMachine #{inspect(name)} terminating", statem_exception(report)]
if min_level == :debug do
msg = [msg, "\nState: ", inspect(state, inspect_opts)]
{:ok, statem_debug(report, inspect_opts, msg)}
else
{:ok, msg}
end
end
# OTP20 and before
defp do_translate(min_level, format, args, opts) do
keys =
format
|> to_string()
|> String.split(~r/~tp|~p/, trim: true)
|> Enum.flat_map(&map_key/1)
args =
[keys, args]
|> List.zip()
|> Map.new()
msg = [
"GenStateMachine #{inspect(args.name)} terminating"
| statem_exception(args)
]
if min_level == :debug do
{:ok, statem_debug(args, opts, msg)}
else
{:ok, msg}
end
end
# OTP21 and later
defp statem_exception(%{reason: {class, reason, stack}}) do
do_statem_exception(class, reason, stack)
end
# OTP20 and before
defp statem_exception(%{class: class, reason: reason, stack: stack}) do
do_statem_exception(class, reason, stack)
end
defp do_statem_exception(class, reason, stack) do
formatted = Exception.format(class, reason, stack)
[?\n | :erlang.binary_part(formatted, 0, byte_size(formatted) - 1)]
end
defp map_key(arg) do
String.split(arg, ~r/\*\* |~n/, trim: true)
|> Enum.filter(&(String.contains?(&1, "=") || String.contains?(&1, "State machine")))
|> case do
[] -> []
["State machine" <> _] -> [:name]
["Last event" <> _] -> [:last_event]
["When server state" <> _] -> [:state]
["Reason for termination" <> _] -> [:class, :reason]
["Callback mode" <> _] -> [:callback_mode]
["Stacktrace" <> _] -> [:stack]
["Queued" <> _] -> [:queued]
["Postponed" <> _] -> [:postponed]
end
end
defp statem_debug(args, opts, msg) do
[msg, Enum.map(Enum.sort(args), &translate_arg(&1, opts))]
end
defp translate_arg({:last_event, last_event}, opts),
do: ["\nLast event: " | inspect(last_event, opts)]
defp translate_arg({:state, state}, opts),
do: ["\nState: " | inspect(state, opts)]
defp translate_arg({:callback_mode, callback_mode}, opts),
do: ["\nCallback mode: " | inspect(callback_mode, opts)]
defp translate_arg({:queued, queued}, opts),
do: ["\nQueued events: " | inspect(queued, opts)]
defp translate_arg({:queue, [last | queued]}, opts),
do: ["\nLast event: ", inspect(last, opts), "\nQueued events: " | inspect(queued, opts)]
defp translate_arg({:postponed, postponed}, opts),
do: ["\nPostponed events: " | inspect(postponed, opts)]
defp translate_arg(_arg, _opts), do: []
end
+64
View File
@@ -0,0 +1,64 @@
otp_release =
:erlang.system_info(:otp_release)
|> to_string()
|> String.split(".")
|> List.first()
|> String.to_integer()
if otp_release < 20 do
IO.warn("gen_state_machine requires Erlang/OTP 20 or greater", [])
end
defmodule GenStateMachine.Mixfile do
use Mix.Project
@version "3.0.0"
def project do
[
app: :gen_state_machine,
version: @version,
elixir: "~> 1.5",
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
deps: deps(),
description: description(),
package: package(),
source_url: "https://github.com/ericentin/gen_state_machine",
docs: [
main: "GenStateMachine",
extras: ["README.md"],
source_ref: "v#{@version}"
]
]
end
def application do
[
extra_applications: [:logger],
mod: {GenStateMachine.Application, []}
]
end
defp deps do
[
{:ex_doc, "~> 0.23", only: :dev, runtime: false},
{:dialyxir, "~> 1.0", only: :dev, runtime: false}
]
end
defp description do
"An Elixir wrapper for gen_statem."
end
defp package do
[
files: ["lib", "mix.exs", "README.md", "LICENSE"],
maintainers: ["Eric Entin"],
licenses: ["Apache 2.0"],
links: %{
"GitHub" => "https://github.com/ericentin/gen_state_machine"
}
]
end
end