defmodule Statifier.FeatureDetector do
@moduledoc """
Detects SCXML features used in documents to enable proper test validation.
This module analyzes SCXML documents (either raw XML strings or parsed Statifier.Document
structures) to identify which SCXML features are being used. This enables the test
framework to fail appropriately when tests depend on unsupported features.
"""
alias Statifier.{Document, State, Transition}
@doc """
Detects features used in an SCXML document.
Takes either a raw XML string or a parsed Statifier.Document and returns a MapSet
of feature atoms representing the SCXML features detected in the document.
## Examples
iex> xml = ""
iex> Statifier.FeatureDetector.detect_features(xml)
#MapSet<[:basic_states, :event_transitions]>
iex> {:ok, document} = Statifier.Parser.SCXML.parse(xml)
iex> Statifier.FeatureDetector.detect_features(document)
#MapSet<[:basic_states, :event_transitions]>
"""
@spec detect_features(String.t() | Document.t()) :: MapSet.t(atom())
def detect_features(xml) when is_binary(xml) do
detect_features_from_xml(xml)
end
def detect_features(%Document{} = document) do
detect_features_from_document(document)
end
@doc """
Returns a registry of all known SCXML features with their support status.
Features are categorized as:
- `:supported` - Fully implemented and working
- `:unsupported` - Not yet implemented
- `:partial` - Partially implemented (may work in simple cases)
"""
@spec feature_registry() :: %{atom() => :supported | :unsupported | :partial}
def feature_registry do
%{
# Basic features (supported)
basic_states: :supported,
event_transitions: :supported,
compound_states: :supported,
parallel_states: :supported,
final_states: :supported,
initial_attributes: :supported,
initial_elements: :supported,
# Conditional features (supported)
conditional_transitions: :supported,
eventless_transitions: :supported,
# Data model features (supported)
datamodel: :supported,
data_elements: :supported,
script_elements: :unsupported,
assign_elements: :supported,
# Executable content (supported)
onentry_actions: :supported,
onexit_actions: :supported,
if_elements: :supported,
send_elements: :unsupported,
log_elements: :supported,
raise_elements: :supported,
# Advanced transitions (unsupported)
targetless_transitions: :unsupported,
internal_transitions: :unsupported,
# History (supported)
history_states: :supported,
# Advanced attributes (unsupported)
send_idlocation: :unsupported,
event_expressions: :unsupported,
target_expressions: :unsupported
}
end
@doc """
Checks if all detected features are supported.
Returns `{:ok, features}` if all features are supported,
or `{:error, unsupported_features}` if any unsupported features are detected.
"""
@spec validate_features(MapSet.t(atom())) ::
{:ok, MapSet.t(atom())} | {:error, MapSet.t(atom())}
def validate_features(detected_features) do
registry = feature_registry()
unsupported =
detected_features
|> Enum.filter(fn feature ->
case Map.get(registry, feature, :unsupported) do
:supported -> false
_unsupported -> true
end
end)
|> MapSet.new()
if MapSet.size(unsupported) == 0 do
{:ok, detected_features}
else
{:error, unsupported}
end
end
# Private functions for XML-based detection
defp detect_features_from_xml(xml) do
features = MapSet.new()
features
|> detect_xml_elements(xml)
|> detect_xml_attributes(xml)
end
defp detect_xml_elements(features, xml) do
features
|> add_if_present(xml, ~r/)/, :basic_states)
|> add_if_present(xml, ~r/)/, :parallel_states)
|> add_if_present(xml, ~r/)/, :final_states)
|> add_if_present(xml, ~r/)/, :initial_elements)
|> add_if_present(xml, ~r/)/, :history_states)
|> add_if_present(xml, ~r/)/, :event_transitions)
|> add_if_present(xml, ~r/)/, :datamodel)
|> add_if_present(xml, ~r/)/, :data_elements)
|> add_if_present(xml, ~r/