flowengine 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,15 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Rules
5
+ # Rule: the answer for the given field (coerced to integer) is greater than the threshold.
6
+ #
7
+ # @attr_reader field [Symbol] answer key
8
+ # @attr_reader value [Integer] threshold
5
9
  class GreaterThan < Base
6
10
  attr_reader :field, :value
7
11
 
12
+ # @param field [Symbol] answer key
13
+ # @param value [Integer] threshold
8
14
  def initialize(field, value)
9
15
  super()
10
16
  @field = field
@@ -12,10 +18,13 @@ module FlowEngine
12
18
  freeze
13
19
  end
14
20
 
21
+ # @param answers [Hash] current answers (field value is coerced with to_i)
22
+ # @return [Boolean] true if answers[field].to_i > value
15
23
  def evaluate(answers)
16
24
  answers[field].to_i > value
17
25
  end
18
26
 
27
+ # @return [String] e.g. "business_income > 100000"
19
28
  def to_s
20
29
  "#{field} > #{value}"
21
30
  end
@@ -2,9 +2,15 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Rules
5
+ # Rule: the answer for the given field (coerced to integer) is less than the threshold.
6
+ #
7
+ # @attr_reader field [Symbol] answer key
8
+ # @attr_reader value [Integer] threshold
5
9
  class LessThan < Base
6
10
  attr_reader :field, :value
7
11
 
12
+ # @param field [Symbol] answer key
13
+ # @param value [Integer] threshold
8
14
  def initialize(field, value)
9
15
  super()
10
16
  @field = field
@@ -12,10 +18,13 @@ module FlowEngine
12
18
  freeze
13
19
  end
14
20
 
21
+ # @param answers [Hash] current answers (field value is coerced with to_i)
22
+ # @return [Boolean] true if answers[field].to_i < value
15
23
  def evaluate(answers)
16
24
  answers[field].to_i < value
17
25
  end
18
26
 
27
+ # @return [String] e.g. "age < 18"
19
28
  def to_s
20
29
  "#{field} < #{value}"
21
30
  end
@@ -2,15 +2,21 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Rules
5
+ # Rule: the answer for the given field is present and not empty (nil or empty? => false).
6
+ #
7
+ # @attr_reader field [Symbol] answer key
5
8
  class NotEmpty < Base
6
9
  attr_reader :field
7
10
 
11
+ # @param field [Symbol] answer key
8
12
  def initialize(field)
9
13
  super()
10
14
  @field = field
11
15
  freeze
12
16
  end
13
17
 
18
+ # @param answers [Hash] current answers
19
+ # @return [Boolean] false if nil or empty, true otherwise
14
20
  def evaluate(answers)
15
21
  val = answers[field]
16
22
  return false if val.nil?
@@ -19,6 +25,7 @@ module FlowEngine
19
25
  true
20
26
  end
21
27
 
28
+ # @return [String] e.g. "name is not empty"
22
29
  def to_s
23
30
  "#{field} is not empty"
24
31
  end
@@ -1,19 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlowEngine
4
+ # A single edge from one step to another, optionally guarded by a rule.
5
+ # Transitions are evaluated in order; the first whose rule is true determines the next step.
6
+ #
7
+ # @attr_reader target [Symbol] id of the step to go to when this transition applies
8
+ # @attr_reader rule [Rules::Base, nil] condition; nil means unconditional (always applies)
4
9
  class Transition
5
10
  attr_reader :target, :rule
6
11
 
12
+ # @param target [Symbol] next step id
13
+ # @param rule [Rules::Base, nil] optional condition (nil = always)
7
14
  def initialize(target:, rule: nil)
8
15
  @target = target
9
16
  @rule = rule
10
17
  freeze
11
18
  end
12
19
 
20
+ # Human-readable label for the condition (e.g. for graph export).
21
+ #
22
+ # @return [String] rule#to_s or "always" when rule is nil
13
23
  def condition_label
14
24
  rule ? rule.to_s : "always"
15
25
  end
16
26
 
27
+ # Whether this transition should be taken given current answers.
28
+ #
29
+ # @param answers [Hash] current answer state
30
+ # @return [Boolean] true if rule is nil or rule evaluates to true
17
31
  def applies?(answers)
18
32
  return true if rule.nil?
19
33
 
@@ -2,21 +2,34 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Validation
5
+ # Result of validating a step answer: either valid or a list of error messages.
6
+ #
7
+ # @attr_reader errors [Array<String>] validation error messages (empty when valid)
5
8
  class Result
6
9
  attr_reader :errors
7
10
 
11
+ # @param valid [Boolean] whether the input passed validation
12
+ # @param errors [Array<String>] error messages (default: [])
8
13
  def initialize(valid:, errors: [])
9
14
  @valid = valid
10
15
  @errors = errors.freeze
11
16
  freeze
12
17
  end
13
18
 
19
+ # @return [Boolean] true if validation passed
14
20
  def valid?
15
21
  @valid
16
22
  end
17
23
  end
18
24
 
25
+ # Abstract adapter for step-level validation. Implement {#validate} to plug in
26
+ # dry-validation, JSON Schema, or other validators; the engine does not depend on a specific one.
19
27
  class Adapter
28
+ # Validates the user's input for the given step.
29
+ #
30
+ # @param _node [Node] the current step (for schema/constraints)
31
+ # @param _input [Object] the value submitted by the user
32
+ # @return [Result] valid: true/false and optional errors list
20
33
  def validate(_node, _input)
21
34
  raise NotImplementedError, "#{self.class}#validate must be implemented"
22
35
  end
@@ -2,7 +2,11 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Validation
5
+ # No-op validator: always accepts any input. Used by default when no validation adapter is given.
5
6
  class NullAdapter < Adapter
7
+ # @param _node [Node] ignored
8
+ # @param _input [Object] ignored
9
+ # @return [Result] always valid with no errors
6
10
  def validate(_node, _input)
7
11
  Result.new(valid: true, errors: [])
8
12
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlowEngine
4
- VERSION = "0.1.0"
4
+ # Semantic version of the flowengine gem (major.minor.patch).
5
+ VERSION = "0.1.2"
5
6
  end
data/lib/flowengine.rb CHANGED
@@ -20,10 +20,53 @@ require_relative "flowengine/engine"
20
20
  require_relative "flowengine/dsl"
21
21
  require_relative "flowengine/graph/mermaid_exporter"
22
22
 
23
+ # Declarative flow definition and execution engine for wizards, intake forms, and
24
+ # multi-step decision graphs. Separates flow logic, data schema, and UI rendering.
25
+ #
26
+ # @example Define and run a flow
27
+ # definition = FlowEngine.define do
28
+ # start :earnings
29
+ # step :earnings do
30
+ # type :multi_select
31
+ # question "What are your main earnings?"
32
+ # options %w[W2 1099 BusinessOwnership]
33
+ # transition to: :business_details, if: contains(:earnings, "BusinessOwnership")
34
+ # end
35
+ # step :business_details do
36
+ # type :number_matrix
37
+ # question "How many business types?"
38
+ # fields %w[RealEstate SCorp CCorp]
39
+ # end
40
+ # end
41
+ # engine = FlowEngine::Engine.new(definition)
42
+ # engine.answer(["W2", "BusinessOwnership"])
43
+ # engine.current_step_id # => :business_details
44
+ #
23
45
  module FlowEngine
46
+ # Builds an immutable {Definition} from the declarative DSL block.
47
+ #
48
+ # @yield context of {DSL::FlowBuilder} (start, step, and rule helpers)
49
+ # @return [Definition] frozen flow definition with start step and nodes
50
+ # @raise [DefinitionError] if no start step or no steps are defined
24
51
  def self.define(&)
25
52
  builder = DSL::FlowBuilder.new
26
53
  builder.instance_eval(&)
27
54
  builder.build
28
55
  end
56
+
57
+ # Evaluates a string of DSL code and returns the resulting definition.
58
+ # Intended for loading flow definitions from files or stored text.
59
+ #
60
+ # @param text [String] Ruby source containing FlowEngine.define { ... }
61
+ # @return [Definition] the definition produced by evaluating the DSL
62
+ # @raise [DefinitionError] on syntax or evaluation errors
63
+ def self.load_dsl(text)
64
+ # rubocop:disable Security/Eval
65
+ eval(text, TOPLEVEL_BINDING.dup, "(dsl)", 1)
66
+ # rubocop:enable Security/Eval
67
+ rescue SyntaxError => e
68
+ raise DefinitionError, "DSL syntax error: #{e.message}"
69
+ rescue StandardError => e
70
+ raise DefinitionError, "DSL evaluation error: #{e.message}"
71
+ end
29
72
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flowengine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Gredeskoul
@@ -67,6 +67,7 @@ files:
67
67
  - LICENSE.txt
68
68
  - README.md
69
69
  - Rakefile
70
+ - docs/floweingine-architecture.png
70
71
  - docs/flowengine-example.png
71
72
  - exe/flowengine
72
73
  - justfile