Skip to content

Commit

Permalink
docs: rework docs
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel committed May 5, 2024
1 parent a02b6d3 commit 32e2ced
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 105 deletions.
16 changes: 9 additions & 7 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
13 changes: 13 additions & 0 deletions documentation/topics/charts.md
Original file line number Diff line number Diff line change
@@ -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
```
42 changes: 42 additions & 0 deletions documentation/topics/what-is-ash-state-machine.md
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 13 additions & 0 deletions documentation/topics/working-with-ash-can.md
Original file line number Diff line number Diff line change
@@ -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.
149 changes: 51 additions & 98 deletions documentation/tutorials/getting-started-with-ash-state-machine.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
```

## 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`.
## Add the extension to your resource

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.
```elixir
use Ash.Resource,
extensions: [AshStateMachine]
```

## A basic state machine
## Add initial states, and a default initial state

```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
use Ash.Resource,
extensions: [AshStateMachine]

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
state_machine do
inital_states [:pending]
default_inital_state :pending
end
```

update :error do
accept [:error_state, :error]
change transition_state(:error)
end
end
## Add allowed transitions

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
```elixir
state_machine do
inital_states [:pending]
default_inital_state :pending

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.
transitions do
# `:begin` action can move state from `:pending` to `:started`/`:aborted`
transition :begin, from: :pending, to: [:started, :aborted]
end
end
```

## Adding a state machine policy
## Use `transition_state` in your actions

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:
### For simple/static state transitions

```elixir
policies do
policy always() do
authorize_if AshStateMachine.Checks.ValidNextState
actions do
update :begin do
# for a static state transition
change transition_state(:started)
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.
### For dynamic/conditional state transitions

## 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:
```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

```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
actions do
update :begin do
# for a dynamic state transition
change Start
end
end
```

## Learning more
## Making a resource into a state machine

- Check out the [DSL documentation](dsl-ashstatemachine.html)
- Check out the `AshStateMachine` module docs.
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.
3 changes: 3 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down

0 comments on commit 32e2ced

Please sign in to comment.