GithubHelp home page GithubHelp logo

ash-project / ash_state_machine Goto Github PK

View Code? Open in Web Editor NEW
15.0 4.0 6.0 184 KB

The extension for building state machines with Ash resources.

Home Page: https://hexdocs.pm/ash_state_machine

License: MIT License

Elixir 100.00%
ash elixir state-machine

ash_state_machine's Introduction

ash_state_machine's People

Contributors

bcksl avatar dependabot[bot] avatar janajri avatar jimsynz avatar joshprice avatar zachdaniel avatar zimt28 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

ash_state_machine's Issues

Ash State Machine does not allow adding state as a valid input attribute for actions.

actions -> update -> accept:
  Cannot accept [:state], because they are not attributes."

It requires changing the action to something like

      argument :state, :atom, constraints: [one_of: [:active, :frozen, :opening]]

      change fn changeset, _context ->
        state = Ash.Changeset.get_argument(changeset, :state)
        previous_state = Ash.Changeset.get_data(changeset, :state)

        if is_nil(state) || state == previous_state do
          changeset
        else
          AshStateMachine.transition_state(changeset, state)
        end
      end

see https://discord.com/channels/711271361523351632/1248715866879561770/1248719109986979930

Proposal: `next_state` change function

Is your feature request related to a problem? Please describe.

If a state machine has two transition definitions, eg:

transition(:initialise, from: :enqueued, to: :initialising)
transition(:initialise, from: :paused, to: :resuming)

Then the initialise action can't neatly change states with transition_state because the target state isn't static, it depends on the resource being initialised.

Describe the solution you'd like

A custom change action could be written to pick the state to transition to, eg.

update :foobar do
  change fn changeset, _ -> 
    case something(changeset) do
      :foo -> AshStateMachine.transition_state(changeset, :foo)
      :bar -> AshStateMachine.transition_state(changeset, :bar)
    end
  end
end

Based on this, I think it should be possible to add another change function like transition_state but called something like next_state.

next_state could introspect the defined transitions for a resource and find one that:

  • matches the current action
  • has the current state in the from list
  • has a single state in the to list (that isn't :*)

and then transition to that state.

An error could be raised (similar to NoValidTransition, maybe NoValidNextState) if there is no single state that can be determined to transition to.

Express the feature either with a change to resource syntax, or with a change to the resource interface

  update :initialise do 
    change next_state() # new function!
  end

Additional context

My initial attempt at starting to implement this that doesn't even compile and is probably pretty dodgy (based on copypasting and tweaking the code for transition_state) -

defmodule AshStateMachine.BuiltinChanges.NextState do
  use Ash.Resource.Change

  def next_state, do: {__MODULE__, []}

  def change(%{action_type: :update} = changeset) do
    transitions =
      AshStateMachine.Info.state_machine_transitions(changeset.resource, changeset.action.name)

    attribute = AshStateMachine.Info.state_machine_state_attribute!(changeset.resource)
    current_state = Map.get(changeset.data, attribute)

    case Enum.filter(transitions, &(current_state in List.wrap(&1.from))) do
      [%{to: to}] when to != [:*] and to != :* ->
        target = List.unwrap(to)
        Ash.Changeset.force_change_attribute(changeset, attribute, target)

      list ->
        Ash.Changeset.add_error(
          changeset,
          AshStateMachine.Errors.NoValidNextState.exception(
            old_state: current_state,
            action: changeset.action.name,
            next_states: Enum.flat_map(list, & &1.to)
          )
        )
    end
  end
end

State machines with I/O and Ash state machine

This is a general question about how to approach a problem, not a feature request. If the best approach will be to change ash_state_machine I'd be happy to work on this on a fork.

The problem

I want to model a system that has to react to inputs, call some services and based on their output transition to next state.
I'm not sure how to do this in using ash_state_machine.

Describe the solution you'd like

Because I'm a noob when it comes to Ash I'd be interested to hear whether my use case is handled with current library or the best approach would be to work on the DSL to customize some transition code. The biggest problem is I see several places where I can put logic that will handle state transitions based on inputs and I'm not sure which approach is the most natural.

Describe alternatives you've considered

Natural approach - packing everything into transition functions

This means that each state knows how to handle the inputs and the corresponding transition has to decide what would be the next state after getting results from external service.

In this approach I'd have transitions like

transition :handle_some_state, from: some_state, to: [:next_state, :another_next_state]

The problem with this approach is that I would have to pass arguments which seems cumbersome - I tried to write something like

update :handle_some_state do
  argument :input, :string
  change GetServiceResults
  change TransitionSomeState
end

I'm fine with this syntax but I don't know how to actually use this transition - I tried to call State.handle_some_state but I don't know how to provide input.
For example when I try to call state |> State.handle_some_state!(input: "foo") I get

** (NimbleOptions.ValidationError) unknown options [:input], valid options are: [:internal?, :timeout, :tracer, :verbose?, :action, :authorize?, :stacktraces?, :tenant, :actor, :after_action, :return_notifications?, :notification_metadata]
    (nimble_options 1.0.2) lib/nimble_options.ex:343: NimbleOptions.validate!/2
    (ash 2.14.15) lib/ash/api/api.ex:2035: Ash.Api.update!/3
    (stdlib 3.15) erl_eval.erl:685: :erl_eval.do_apply/6
    (elixir 1.14.2) lib/module/parallel_checker.ex:107: Module.ParallelChecker.verify/1

Custom module to dispatch transitions

Let's call this TransitionDispatcher. In this approach the dispatcher know what services to call and based on their outputs it transitions the state.

This approach seems like it's a wrong way to use state machines - basically this dispatcher would have select the transition, which raises the question what is the purpose of using state machine in the first place.

Express the feature either with a change to resource syntax, or with a change to the resource interface**

I'm not really sure whether my use case can be handled with current ash_state_machine or whether it would make sense to provide a way to set some functions to transition ... or change (in action definitions) that will determine what should be the next state based on service results.

Additional context

I am writing a rule-based chatbot, my transitions will call some external NLP services that among others will find the intent, and based on that select the next state.

Ash seems like a good fit for this because Elixir is a great language for writing backend, and out of the box Ash provides features that will handle minutiae of chatbot engine.

Anyway thank you for the library, I'm really excited about Ash as a person coming from Python - it seems like many issues plaguing Python's ecosystem that generate accidental complexity do not exist in Ash.

Create new states instead of updating

I am trying to use the state machine in a setting where I want to persist every transition so that I can use Ash.Query to retrieve the transition history. How to approach this? I wrote some code that tries to add this functionality, but I've run in some problems.

Proposed approach

The default behavior uses changesets that update the states, so I wrote something like this in change after_transaction (to_attributes_map gets the attributes I'm passing to next state)

fn changeset, {:ok, result} ->
    State
    |> Ash.Changeset.for_create(:create, result |> to_attributes_map())
    |> Ash.Changeset.force_change_attribute(:state, changeset.attributes[:state])
    |> State.Api.create()

The problem is this way when I query

  • I don't see the state that comes from State.create!
  • the last state is duplicated

Questions

Is there a more straightforward approach? Is there something obviously wrong with my approach that results in the mentioned problems?

Thanks in advance!

Make `transition_state` atomic

Currently, adding transition_state to an action results in a compile error when the action is not defined as require_atomic?: false.

I think it should be possible to add this change, without forgoing atomicity.

ash: v3.0
ash_state_machine: v0.2.2

state_attribute does not work

I have this:

state_machine do
    initial_states [:initialized]
    default_initial_state :initialized
    state_attribute :status

    transitions do
      # transition :upload, from: :initialized, to: :uploading
      # transition :process, from: :uploading, to: :processing
      # transition :fail, from: :processing, to: :failed
      # transition :complete, from: :processing, to: :completed
    end
  end

And when loading the resource that has the state machine from the parent resource I get:

iex(2)> vom = Safari.Virtual.VirtualOutcropModel.by_id!(303, load: :cesium_asset)
[debug] QUERY OK source="virtual_outcrop_models" db=0.8ms queue=0.9ms idle=1120.9ms
SELECT v0."id" FROM "virtual_outcrop_models" AS v0 WHERE (v0."id"::bigint = $1::bigint) [303]
↳ AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:629
[debug] QUERY ERROR source="cesium_assets" db=0.0ms queue=3.5ms idle=1131.6ms
SELECT c0."status", c0."id", c0."state", c0."cesium_id", c0."tileset_url", c0."cesium_progress", c0."default_camera", c0."cesium_bucket_credentials", c0."virtual_outcrop_model_id" FROM "cesium_assets" AS c0 WHERE (c0."virtual_outcrop_model_id"::bigint = $1::bigint) [303]
↳ AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:629
** (Ash.Error.Unknown) Unknown Error

* ** (Postgrex.Error) ERROR 42703 (undefined_column) column c0.state does not exist

    query: SELECT c0."status", c0."id", c0."state", c0."cesium_id", c0."tileset_url", c0."cesium_progress", c0."default_camera", c0."cesium_bucket_credentials", c0."virtual_outcrop_model_id" FROM "cesium_assets" AS c0 WHERE (c0."virtual_outcrop_model_id"::bigint = $1::bigint)

    hint: Perhaps you meant to reference the column "c0.status".
    (ash 2.15.6) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3
    (safari 0.1.0) deps/ash/lib/ash/code_interface.ex:515: Safari.Virtual.VirtualOutcropModel.by_id!/3
    iex:2: (file)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.