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:
Binary file not shown.
@@ -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.
|
||||
@@ -0,0 +1,49 @@
|
||||
[](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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user