<!--
SPDX-FileCopyrightText: 2023 ash_state_machine contributors <https://github.com/ash-project/ash_state_machine/graphs.contributors>

SPDX-License-Identifier: MIT
-->

# Getting Started with State Machines

In this tutorial, we will use AshStateMachine to extend the ticketing
tutorial from the [Ash Getting Started Guide](https://hexdocs.pm/ash/get-started.html).
If you are new to Ash, please consider reading that guide first to get
familiar with Ash and working with Resources.

## What you will learn

This tutorial, we will explore the following topics:

- What a state machine is in Ash
- How states are declared
- How actions trigger transitions
- How transitions are validated

## Installation

### Add the ash_state_machine dependency to your project's dependencies

Add the following dependency to your `mix.exs` file:

```elixir
{:ash_state_machine, "~> 0.2.13"}
```

Next, update your project dependencies by executing `mix deps.get`

# Making a resource into a state machine

A state machine (in this case a "Finite State Machine"), models a
system that can only exist in a single `state` at one time. Its
power comes from the ability to specify transitions between states.
For example, you might have an order state machine with states
`[:pending, :on_its_way, :delivered]`. However, you probably can't go from
`:pending` to `:delivered`, 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.

## Add the AshStateMachine extension to your resource

Next, add AshStateMachine as an extension to any existing resource. In
our case, we will add it to the Ticket resource:

```elixir
defmodule Helpdesk.Support.Ticket do
  use Ash.Resource,
    domain: Helpdesk.Support
    data_layer: Ash.DataLayer.Ets
    extensions: [AshStateMachine]

end
```

## Add attributes to your resource if required

This is not a tutorial on [Ash resources](https://hexdocs.pm/ash/get-started.html#steps),
so we won't go into detail here, but we will list the attributes that
we will use in this tutorial:

```elixir
  # The attributes that model a Ticket's data
  attributes do
    uuid_primary_key :id

    attribute :subject, :string
    attribute :description, :string
    attribute :additional_information, :string
  end
```

Note that the previous tutorial uses an attribute named `:status` to
track the ticket status. By default, AshStateMachine uses an attribute
named `:state` that serves the same purpose. For now we are going to
ignore this detail, but we will learn how to change the 'state'
attribute name later.

# Planning our future states

In this example we will proceed in a way that helps to illustrate
AshStateMachine concepts. There is no single 'correct' process for
modelling a domain, and you may choose to follow different steps if
that works better for you.

## Consider the possible states for your application

We will start by listing the states that we think we might need
(and which have been chosen to illustrate some different features)
as comments in our code:

```elixir
# Possible states: [
#   :received, :needs_more_info,
#   :with_it, :with_hr,
#   :will_not_fix, :closed
# ]
...
```

Next, we are going to create some empty actions so that we can think
about how we might like to interact with the Ticket resource.

```elixir
actions do
  create :open do
    accept [:subject, :description]
  end

  update :request_more_information do
  end

  update :assign_to_department do
  end

  update :deny_request do
  end

  update :close do
  end
end
```

## Specify the initial state for the resource

In our example, when a ticket is created, it will start in the
`:received` state, awaiting triage. We can add the following block to
specify the initial state:

```elixir
state_machine do
  initial_states [:received]
  default_initial_state :received
end
```

# Transitioning from one state to another

Ash uses `transition_state/1` to requests a state transition. Whether
the transition is allowed is determined later by the
state_machine.transitions configuration.

In our example, we will start with the simple idea that any user can
request more information about a ticket at any time.

## Use `transition_state` in your actions

We need to update our `:request_more_information` action so that it
requests a transition to the `:needs_more_info` state:

```elixir
actions do
  update :request_more_information do
    change transition_state(:needs_more_info)
  end
end
```

## Add allowed transitions

The power of AshStateMachine is that we can model which transitions
are allowed based upon the current state. To start, we are going to
allow users to 'request more information' from any state.

We accomplish this by adding
[transitions](https://hexdocs.pm/ash_state_machine/dsl-ashstatemachine.html#state_machine-transitions)
to our resource:

```elixir
state_machine do
  initial_states [:received]
  default_initial_state :received

  transitions do
    # the :request_more_information action can transition from
    # :received, :with_it or :with_hr to :needs_more_info.
    # we do not allow closed tickets to request more information.
    transition :request_more_information,
      from: [:received, :with_it, :with_hr],
      to: :needs_more_info
  end
end
```

The syntax is: transition (one or more actions), from: (one or more
states), to: (one or more states).

Note: You must define transitions for your actions. If you call
`change transition_state` and there isn't a matching `from` and
`to` state, the action will fail.


# Conditional state transitions

Sometimes you may need to transfer to one of multiple states depending
upon a particular condition, such as a passed-in argument or an
application-specific value.

## State transitions based upon an argument

In our example, we will let a support user transfer a ticket to the IT
department or the HR department by passing an
[argument](https://hexdocs.pm/ash/actions.html#accepting-inputs) to
the `:assign_to_department` action

First we will update our action:

```elixir
actions do
  update :assign_to_department do
    argument :department, :atom,
      allow_nil?: false,
      constraints: [
        one_of: [:IT, :HR]
      ]

    if :department == :IT do
      change transition_state(:with_it)
    else
      change transition_state(:with_hr)
    end
  end
end
```

We do not want to let the departments transfer tickets to each other,
so we will not allow a transition from `:with_it` to `:with_hr` or
vice versa.

Note that the conditional does not bypass any transition rules.
Even when transitions are chosen tynamically, the resulting
state must still be permitted by the `transitions` block.

```elixir
state_machine do
  transitions do
    # assign_to_dept can transition from
    # :received or :needs_more_info
    # and can transition to
    # :with_it or :with_hr
    transition(:assign_to_department,
      from: [:received, :needs_more_info],
      to: [:with_it, :with_hr]
    )
  end
end
```
  * [ ]


### Advanced: State transitions based upon changesets

For more complex scenarios, you can also branch based upon the
contents of a changeset, as the following example illustrates:

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

# Declaring a custom state attribute

As mentioned earlier, AshStateMachine uses the `:state` attribute by default.
When AshStateMachine is imported into a resource, the `:state` attribute is
created on the resource with the following definition:

```elixir
attribute :state, :atom do
  allow_nil? false
  default AshStateMachine.Info.state_machine_initial_default_state(dsl_state)
  public? true
  constraints one_of: [
    AshStateMachine.Info.state_machine_all_states(dsl_state)
  ]
end
```

In our example, if we wanted to change the name of the attribute from
`:state` to `:status` (to match the value from the previous tutorial),
we would do it like this:

```elixir
state_machine do
  initial_states([:pending])
  default_initial_state(:pending)
  state_attribute(:status) # <-- save state in an attribute named :status
end
```

If you need more control, you can declare the attribute yourself on
the resource:

```elixir
attributes do
  attribute :alternative_state, :atom do
    allow_nil? false
    default :issued
    public? true
    constraints one_of: [:issued, :sold, :reserved, :retired]
  end
end
```

Be aware that the type of this attribute needs to be `:atom` or a type
created with `Ash.Type.Enum`. Both the `default` and list of values
need to be correct!

# Next steps

The true power of AshStateMachine is that it integrates seemlessly with the
rest of the Ash ecosystem. You can easily add guards, policies, authorisation
and any other Ash concept to your state machines.
