Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Statifier - SCXML State Machines for Elixir

[![CI](https://github.com/riddler/statifier/workflows/CI/badge.svg?branch=main)](https://github.com/riddler/statifier/actions)
[![Coverage](https://codecov.io/gh/riddler/statifier/branch/main/graph/badge.svg)](https://codecov.io/gh/riddler/statifier)
[![CI](https://github.com/riddler/statifier/actions/workflows/ci.yml/badge.svg)](https://github.com/riddler/statifier/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/riddler/statifier/branch/main/graph/badge.svg)](https://codecov.io/gh/riddler/statifier)
[![Hex.pm Version](https://img.shields.io/hexpm/v/statifier.svg)](https://hex.pm/packages/statifier)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/statifier/)

> ⚠️ **Active Development Notice**
> This project is still under active development and things may change even though it is passed version 1. APIs, features, and behaviors may evolve as we continue improving SCXML compliance and functionality.

An Elixir implementation of SCXML (State Chart XML) state charts with a focus on W3C compliance.

Expand Down
12 changes: 1 addition & 11 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,7 @@ if config_env() == :dev do

config :logger, :console,
format: "[$level] $message $metadata\n",
metadata: [
:current_state,
:event,
:action_type,
:state_id,
:phase,
:transition_type,
:source_state,
:condition,
:targets
]
metadata: :all

# Default Statifier logging configuration for dev
config :statifier,
Expand Down
14 changes: 13 additions & 1 deletion lib/statifier/parser/scxml/state_stack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ defmodule Statifier.Parser.SCXML.StateStack do
and updating parent elements when child elements are completed.
"""

alias Statifier.Actions.IfAction

@doc """
Handle the end of a state element by adding it to its parent.
"""
Expand Down Expand Up @@ -584,6 +586,17 @@ defmodule Statifier.Parser.SCXML.StateStack do
{:ok, %{state | stack: [{"transition", updated_transition} | rest]}}
end

# Handle nested if action within parent if container
def handle_if_end(
%{stack: [{_element_name, nested_if_container} | [{"if", parent_if_container} | rest]]} =
state
) do
# Create IfAction from nested if container and add to parent if container
nested_if_action = create_if_action_from_container(nested_if_container)
updated_parent_container = add_action_to_current_block(parent_if_container, nested_if_action)
{:ok, %{state | stack: [{"if", updated_parent_container} | rest]}}
end

def handle_if_end(state) do
# If element not in an onentry/onexit context, just pop it
{:ok, pop_element(state)}
Expand Down Expand Up @@ -842,7 +855,6 @@ defmodule Statifier.Parser.SCXML.StateStack do
end)

# Create IfAction with collected blocks
alias Statifier.Actions.IfAction
IfAction.new(conditional_blocks, if_container[:location])
end

Expand Down
3 changes: 2 additions & 1 deletion test/passing_tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"test/statifier/**/*_test.exs",
"test/mix/**/*_test.exs"
],
"last_updated": "2025-08-31",
"last_updated": "2025-09-02",
"scion_tests": [
"test/scion_tests/actionSend/send1_test.exs",
"test/scion_tests/actionSend/send2_test.exs",
Expand Down Expand Up @@ -46,6 +46,7 @@
"test/scion_tests/history/history4b_test.exs",
"test/scion_tests/history/history5_test.exs",
"test/scion_tests/history/history6_test.exs",
"test/scion_tests/if_else/test0_test.exs",
"test/scion_tests/misc/deep_initial_test.exs",
"test/scion_tests/more_parallel/test0_test.exs",
"test/scion_tests/more_parallel/test10_test.exs",
Expand Down
88 changes: 88 additions & 0 deletions test/statifier/parser/scxml/state_stack_if_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -289,5 +289,93 @@ defmodule Statifier.Parser.SCXML.StateStackIfTest do
assert length(current_block.actions) == 1
assert hd(current_block.actions) == assign_action
end

test "nested if within parent if container" do
# Test nested if action added to current conditional block within parent if container
nested_if_container = %{
conditional_blocks: [
%{
type: :if,
cond: "x === 40",
actions: [
%AssignAction{location: "x", expr: "x + 10"},
%LogAction{label: "nested", expr: "x"}
]
}
],
current_block_index: 0,
location: %{line: 2, column: 5}
}

parent_if_container = %{
conditional_blocks: [
%{
type: :if,
cond: "x === 28",
actions: [
%AssignAction{location: "x", expr: "x + 12"},
%LogAction{label: "after_assign", expr: "x"}
]
}
],
current_block_index: 0,
location: %{line: 1, column: 1}
}

state = %{
stack: [
{"if", nested_if_container},
{"if", parent_if_container},
{"onentry", :onentry_block},
{"state", %Statifier.State{id: "test_state"}}
]
}

{:ok, result} = StateStack.handle_if_end(state)

# Should have parent if container with nested if added as an action
[{"if", updated_parent_container} | _rest] = result.stack
current_block = Enum.at(updated_parent_container.conditional_blocks, 0)

# Should have 3 actions: assign, log, nested_if_action
assert length(current_block.actions) == 3

# The last action should be the nested IfAction
nested_if_action = List.last(current_block.actions)
assert match?(%Statifier.Actions.IfAction{}, nested_if_action)

# Verify the nested IfAction has correct structure
assert length(nested_if_action.conditional_blocks) == 1
nested_block = hd(nested_if_action.conditional_blocks)
assert nested_block[:type] == :if
assert nested_block[:cond] == "x === 40"
assert length(nested_block[:actions]) == 2
end

test "nested if not in parent if context" do
# Test nested if element not within parent if container (fallback case)
nested_if_container = %{
conditional_blocks: [
%{type: :if, cond: "true", actions: [%LogAction{label: "test", expr: "value"}]}
],
current_block_index: 0,
location: %{line: 1, column: 1}
}

state = %{
stack: [
{"if", nested_if_container},
{"onentry", :onentry_block},
{"state", %Statifier.State{id: "test_state"}}
]
}

{:ok, result} = StateStack.handle_if_end(state)

# Should create IfAction and add to onentry (normal if handling)
[{"onentry", actions} | _rest] = result.stack
assert length(actions) == 1
assert match?(%Statifier.Actions.IfAction{}, hd(actions))
end
end
end