diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a4ff0ac..33bfc63 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,11 +1,12 @@ --- + name: Bug report about: Create a report to help us improve title: '' labels: bug, needs review assignees: '' --https://hexdocs.pm/ash_json_api-- +-https://hexdocs.pm/ash_state_machine-- **Describe the bug** A clear and concise description of what the bug is. @@ -16,12 +17,13 @@ A minimal set of resource definitions and calls that can reproduce the bug. **Expected behavior** A clear and concise description of what you expected to happen. -** Runtime - - Elixir version - - Erlang version - - OS - - Ash version - - any related extension versions +\*\* Runtime + +- Elixir version +- Erlang version +- OS +- Ash version +- any related extension versions **Additional context** Add any other context about the problem here. diff --git a/README.md b/README.md index 3dedfd3..0acefaf 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ Welcome! This is the extension for building state machines with [Ash](https://he - [Getting Started with AshStateMachine](documentation/tutorials/getting-started-with-ash-state-machine.md) +## Topics + +- [What is AshStateMachine?](documentation/topics/what-is-ash-state-machine.md) +- [Charts](documentation/topics/charts.md) +- [Working with `Ash.can?`](documentation/topics/working-with-ash-can.md) + ## Reference - [AshStateMachine DSL](documentation/dsls/DSL:-AshStateMachine.md) diff --git a/documentation/topics/charts.md b/documentation/topics/charts.md new file mode 100644 index 0000000..b7b338e --- /dev/null +++ b/documentation/topics/charts.md @@ -0,0 +1,13 @@ +# Charts + +Run `mix ash_state_machine.generate_flow_charts` to generate flow charts for your resources. See the task documentation for more. Here is an example: + +```mermaid +stateDiagram-v2 +pending --> confirmed: confirm +confirmed --> on_its_way: begin_delivery +on_its_way --> arrived: package_arrived +on_its_way --> error: error +confirmed --> error: error +pending --> error: error +``` diff --git a/documentation/topics/what-is-ash-state-machine.md b/documentation/topics/what-is-ash-state-machine.md new file mode 100644 index 0000000..38f8fb8 --- /dev/null +++ b/documentation/topics/what-is-ash-state-machine.md @@ -0,0 +1,42 @@ +# What is AshStateMachine? + +## What is a State Machine? + +A state machine is a program who's purpose is to manage an internal "state". The simplest example of a state machine could be a program representing a light switch. A light switch might have two states, "on" and "off". You can transition from "on" to "off", and back. + +```mermaid +classDiagram + +class Switch { + state on | off + turnOn() off -> on + turnOff() on -> off +} +``` + +To build state machines with `Ash.Resource`, we use [`AshStateMachine`](https://hexdocs.pm/ash_state_machine). + +When we refer to "state machines" in AshStateMachine, we're referring to a specific type of state machine known as a "Finite State Machine". +It is "finite", because there are a statically known list of states that the machine may be in at any time, just like the `Switch` example above. + +### Why should we use state machines? + +#### Flexible + +State machines are a _simple_ and _powerful_ way to represent complex workflows. They are flexible to modifications over time by adding new states, or new transitions between states. + +#### Migrateable + +State machines typically contain additional data about the state that they are in, or past states that they have been in, and this state must be migrated over time. When representing data as state machines, it becomes simple to do things like "update all `package` records that are in the `pending_shipment` state". + +#### Easy to reason about for humans + +State machines, when compared to things like workflows, are easy for people to reason about. We have an intuition for things like "the package is currently `on_its_way`, with a `current_location` of New York, New York", or "your package is now `out_for_delivery` with an ETA of 6PM". + +#### Compatible with any storage mechanism + +Since state machines are backed by simple state, you can often avoid any fancy workflow runners or complex storage mechanisms. You can store them in a database table, a json blob, a CSV file, at the end of the day its just a `:state` field and accompanying additional fields. + +## What does AshStateMachine do differently than other implementations? + +AshStateMachine is an [`Ash.Resource`](https://hexdocs.pm/ash/Ash.Resource.html) extension, meaning it _enhances a resource_ with state machine capabilities. In `Ash`, all modifications go through [_actions_](actions.html). In accordance with this, `AshStateMachine` offers a DSL for declaring _valid states and transitions_, but does not, itself, _perform_ those transitions. You will use a change called `transition_state/1` in an action to move from one state to the other. For more, check out the [CookBook](https://hexdocs.pm/ash/readme.html#cookbook) diff --git a/documentation/topics/working-with-ash-can.md b/documentation/topics/working-with-ash-can.md new file mode 100644 index 0000000..1c874f6 --- /dev/null +++ b/documentation/topics/working-with-ash-can.md @@ -0,0 +1,13 @@ +# Working with `Ash.can?` + +Using `Ash.can?/3` won't return `false` if a given state machine transition is invalid. This is because `Ash.can?/3` is only concerned with policies, not changes/validations. However, many folks use `Ash.can?/3` in their UI to determine whether a given button/form/etc should be shown. To help with this you can add the following to your resource: + +```elixir +policies do + policy always() do + authorize_if AshStateMachine.Checks.ValidNextState + end +end +``` + +This check is only used in _pre_flight_ authorization checks (i.e calling `Ash.can?/3`), but it will return `true` in all cases when running real authorization checks. This is because the change is validated when you use the `transition_state/1` change and `AshStateMachine.transition_state/2`, and so you would be doing extra work for no reason. diff --git a/documentation/tutorials/getting-started-with-ash-state-machine.md b/documentation/tutorials/getting-started-with-ash-state-machine.md index 5f2fbba..d19155b 100644 --- a/documentation/tutorials/getting-started-with-ash-state-machine.md +++ b/documentation/tutorials/getting-started-with-ash-state-machine.md @@ -10,126 +10,79 @@ If you haven't already, read the [Ash Getting Started Guide](https://hexdocs.pm/ {:ash_state_machine, "~> 0.2.3-rc.1"} ``` +## Add the extension to your resource + +```elixir +use Ash.Resource, + extensions: [AshStateMachine] +``` + +## Add initial states, and a default initial state + +```elixir +use Ash.Resource, + extensions: [AshStateMachine] + +... + +state_machine do + inital_states [:pending] + default_inital_state :pending +end +``` + +## Add allowed transitions + +```elixir +state_machine do + inital_states [:pending] + default_inital_state :pending + + transitions do + # `:begin` action can move state from `:pending` to `:started`/`:aborted` + transition :begin, from: :pending, to: [:started, :aborted] + end +end +``` + +## Use `transition_state` in your actions + +### For simple/static state transitions + +```elixir +actions do + update :begin do + # for a static state transition + change transition_state(:started) + end +end +``` + +### For dynamic/conditional state transitions + +```elixir +defmodule Start do + use Ash.Resource.Change + + def change(changeset, _, _) do + if ready_to_start?(changeset) do + AshStateMachine.transition_state(changeset, :started) + else + AshStateMachine.transition_state(changeset, :aborted) + end + end +end + +actions do + update :begin do + # for a dynamic state transition + change Start + end +end +``` + ## Making a resource into a state machine The concept of a state machine (in this case a "Finite State Machine"), essentially involves a single `state`, with specified transitions between states. For example, you might have an order state machine with states `[:pending, :on_its_way, :delivered]`. However, you can't go from `:pending` to `:delivered` (probably), and so you want to only allow certain transitions in certain circumstances, i.e `:pending -> :on_its_way -> :delivered`. This extension's goal is to help you write clear and clean state machines, with all of the extensibility and power of Ash resources and actions. - -## A basic state machine - -```elixir -defmodule Order do - # leaving out data layer configuration for brevity - use Ash.Resource, - extensions: [AshStateMachine] - - state_machine do - initial_states [:pending] - default_initial_state :pending - - transitions do - transition :confirm, from: :pending, to: :confirmed - transition :begin_delivery, from: :confirmed, to: :on_its_way - transition :package_arrived, from: :on_its_way, to: :arrived - transition :error, from: [:pending, :confirmed, :on_its_way], to: :error - end - end - - actions do - # create sets the state - defaults [:create, :read] - - update :confirm do - # accept [...] - # you can change other attributes - # or do anything else an action can normally do - # this transition will be validated according to - # the state machine rules above - change transition_state(:confirmed) - end - - update :begin_delivery do - # accept [...] - change transition_state(:on_its_way) - end - - update :package_arrived do - # accept [...] - change transition_state(:arrived) - end - - update :error do - accept [:error_state, :error] - change transition_state(:error) - end - end - - changes do - # any failures should be captured and transitioned to the error state - change after_transaction(fn - changeset, {:ok, result}, _ -> - {:ok, result} - - changeset, {:error, error}, _ -> - if changeset.context[:error_handler?] do - {:error, error} - else - changeset.data - |> Ash.Changeset.for_update(:error, %{ - error_state: changeset.data.state - }) - |> Ash.Changeset.set_context(%{error_handler?: true}) - |> Ash.update() - - {:error, error} - end - end), - on: [:update] - end - end - - attributes do - uuid_primary_key :id - # ...attributes like address/delivery options would go here - attribute :error, :string - attribute :error_state, :string - # :state attribute is added for you by `state_machine` - # however, you can add it yourself, and you will be guided by - # compile errors on what states need to be allowed by your type. - end -end -``` - -## Adding a state machine policy - -Using `Ash.can?/3` won't return `false` if a given state machine transition is invalid. This is because `Ash.can?/3` is only concerned with policies, not changes/validations. However, many folks use `Ash.can?/3` in their UI to determine whether a given button/form/etc should be shown. To help with this you can add the following to your resource: - -```elixir -policies do - policy always() do - authorize_if AshStateMachine.Checks.ValidNextState - end -end -``` - -This check is only used in _pre_flight_ authorization checks (i.e calling `Ash.can?/3`), but it will return `true` in all cases when running real authorization checks. This is because the change is validated when you use the `transition_state/1` change and `AshStateMachine.transition_state/2`, and so you would be doing extra work for no reason. - -## Generating Flow Charts - -run `mix ash_state_machine.generate_flow_charts` to generate flow charts for your resources. See the task documentation for more. Here is a chart generated from the example above: - -```mermaid -stateDiagram-v2 -pending --> confirmed: confirm -confirmed --> on_its_way: begin_delivery -on_its_way --> arrived: package_arrived -on_its_way --> error: error -confirmed --> error: error -pending --> error: error -``` - -## Learning more - -- Check out the [DSL documentation](dsl-ashstatemachine.html) -- Check out the `AshStateMachine` module docs. diff --git a/mix.exs b/mix.exs index 91b9913..31f3f91 100644 --- a/mix.exs +++ b/mix.exs @@ -79,6 +79,9 @@ defmodule AshStateMachine.MixProject do extras: [ {"README.md", title: "Home"}, "documentation/tutorials/getting-started-with-ash-state-machine.md", + "documentation/topics/what-is-ash-state-machine.md", + "documentation/topics/charts.md", + "documentation/topics/working-with-ash-can.md", "documentation/dsls/DSL:-AshStateMachine.md" ], groups_for_extras: [