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.
data/Rakefile CHANGED
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
- require 'timeout'
6
- require 'yard'
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "timeout"
6
+ require "yard"
7
7
 
8
8
  def shell(*args)
9
- puts "running: #{args.join(' ')}"
10
- system(args.join(' '))
9
+ puts "running: #{args.join(" ")}"
10
+ system(args.join(" "))
11
11
  end
12
12
 
13
13
  task :clean do
14
- shell('rm -rf pkg/ tmp/ coverage/ doc/ ' )
14
+ shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
15
15
  end
16
16
 
17
17
  task gem: [:build] do
18
- shell('gem install pkg/*')
18
+ shell("gem install pkg/*")
19
19
  end
20
20
 
21
21
  task permissions: [:clean] do
@@ -26,9 +26,9 @@ end
26
26
  task build: :permissions
27
27
 
28
28
  YARD::Rake::YardocTask.new(:doc) do |t|
29
- t.files = %w(lib/**/*.rb exe/*.rb - README.md LICENSE.txt)
30
- t.options.unshift('--title', '"FlowEngine — DSL + AST for buildiong complex flows in Ruby."')
31
- t.after = -> { exec('open doc/index.html') } if RUBY_PLATFORM =~ /darwin/
29
+ t.files = %w[lib/**/*.rb exe/*.rb - README.md LICENSE.txt CHANGELOG.md]
30
+ t.options.unshift("--title", '"FlowEngine — DSL + AST for buildiong complex flows in Ruby."')
31
+ t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
32
32
  end
33
33
 
34
34
  RSpec::Core::RakeTask.new(:spec)
Binary file
data/justfile CHANGED
@@ -2,6 +2,10 @@ set shell := ["bash", "-c"]
2
2
 
3
3
  set dotenv-load
4
4
 
5
+ [no-exit-message]
6
+ recipes:
7
+ @just --choose
8
+
5
9
  test:
6
10
  @bundle exec rspec
7
11
  @bundle exec rubocop
@@ -32,6 +36,11 @@ format:
32
36
  lint:
33
37
  @bundle exec rubocop
34
38
 
39
+ # Generates library documentation into ./doc folder and opens the browser
40
+ doc:
41
+ @bundle exec rake doc
42
+ @open ./doc/index.html
43
+
35
44
 
36
45
  check-all: lint test
37
46
 
@@ -1,9 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlowEngine
4
+ # Immutable, versionable flow graph: maps step ids to {Node} objects and defines the entry point.
5
+ # Built by {DSL::FlowBuilder}; consumed by {Engine} for navigation.
6
+ #
7
+ # @attr_reader start_step_id [Symbol] id of the first step in the flow
8
+ # @attr_reader steps [Hash<Symbol, Node>] frozen map of step id => node (read-only)
4
9
  class Definition
5
10
  attr_reader :start_step_id, :steps
6
11
 
12
+ # @param start_step_id [Symbol] id of the initial step
13
+ # @param nodes [Hash<Symbol, Node>] all steps keyed by id
14
+ # @raise [DefinitionError] if start_step_id is not present in nodes
7
15
  def initialize(start_step_id:, nodes:)
8
16
  @start_step_id = start_step_id
9
17
  @steps = nodes.freeze
@@ -11,14 +19,19 @@ module FlowEngine
11
19
  freeze
12
20
  end
13
21
 
22
+ # @return [Node] the node for the start step
14
23
  def start_step
15
24
  step(start_step_id)
16
25
  end
17
26
 
27
+ # @param id [Symbol] step id
28
+ # @return [Node] the node for that step
29
+ # @raise [UnknownStepError] if id is not in steps
18
30
  def step(id)
19
31
  steps.fetch(id) { raise UnknownStepError, "Unknown step: #{id.inspect}" }
20
32
  end
21
33
 
34
+ # @return [Array<Symbol>] all step ids in the definition
22
35
  def step_ids
23
36
  steps.keys
24
37
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module FlowEngine
4
4
  module DSL
5
+ # Builds a {Definition} from the declarative DSL used in {FlowEngine.define}.
6
+ # Provides {#start} and {#step}; each step block is evaluated by {StepBuilder} with {RuleHelpers}.
5
7
  class FlowBuilder
6
8
  include RuleHelpers
7
9
 
@@ -10,16 +12,27 @@ module FlowEngine
10
12
  @nodes = {}
11
13
  end
12
14
 
15
+ # Sets the entry step id for the flow.
16
+ #
17
+ # @param step_id [Symbol] id of the first step
13
18
  def start(step_id)
14
19
  @start_step_id = step_id
15
20
  end
16
21
 
22
+ # Defines one step by id; the block is evaluated in a {StepBuilder} context.
23
+ #
24
+ # @param id [Symbol] step id
25
+ # @yield block evaluated in {StepBuilder} (type, question, options, transition, etc.)
17
26
  def step(id, &)
18
27
  builder = StepBuilder.new
19
28
  builder.instance_eval(&)
20
29
  @nodes[id] = builder.build(id)
21
30
  end
22
31
 
32
+ # Produces the frozen {Definition} from the accumulated start and steps.
33
+ #
34
+ # @return [Definition]
35
+ # @raise [DefinitionError] if start was not set or no steps were defined
23
36
  def build
24
37
  raise DefinitionError, "No start step defined" if @start_step_id.nil?
25
38
  raise DefinitionError, "No steps defined" if @nodes.empty?
@@ -2,31 +2,53 @@
2
2
 
3
3
  module FlowEngine
4
4
  module DSL
5
+ # Factory methods for rule objects, included in {FlowBuilder} and {StepBuilder}.
6
+ # Use these inside step blocks for transition conditions and visible_if.
5
7
  module RuleHelpers
8
+ # @param field [Symbol] answer key (step id)
9
+ # @param value [Object] value to check for in the array
10
+ # @return [Rules::Contains]
6
11
  def contains(field, value)
7
12
  Rules::Contains.new(field, value)
8
13
  end
9
14
 
15
+ # @param field [Symbol] answer key
16
+ # @param value [Object] expected value
17
+ # @return [Rules::Equals]
10
18
  def equals(field, value)
11
19
  Rules::Equals.new(field, value)
12
20
  end
13
21
 
22
+ # @param field [Symbol] answer key (value coerced to integer for comparison)
23
+ # @param value [Integer] threshold
24
+ # @return [Rules::GreaterThan]
14
25
  def greater_than(field, value)
15
26
  Rules::GreaterThan.new(field, value)
16
27
  end
17
28
 
29
+ # @param field [Symbol] answer key (value coerced to integer for comparison)
30
+ # @param value [Integer] threshold
31
+ # @return [Rules::LessThan]
18
32
  def less_than(field, value)
19
33
  Rules::LessThan.new(field, value)
20
34
  end
21
35
 
36
+ # @param field [Symbol] answer key
37
+ # @return [Rules::NotEmpty]
22
38
  def not_empty(field)
23
39
  Rules::NotEmpty.new(field)
24
40
  end
25
41
 
42
+ # Logical AND of multiple rules.
43
+ # @param rules [Array<Rules::Base>] rules to combine
44
+ # @return [Rules::All]
26
45
  def all(*rules)
27
46
  Rules::All.new(*rules)
28
47
  end
29
48
 
49
+ # Logical OR of multiple rules.
50
+ # @param rules [Array<Rules::Base>] rules to combine
51
+ # @return [Rules::Any]
30
52
  def any(*rules)
31
53
  Rules::Any.new(*rules)
32
54
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module FlowEngine
4
4
  module DSL
5
+ # Builds a single {Node} from step DSL (type, question, options, transitions, visibility).
6
+ # Used by {FlowBuilder#step}; includes {RuleHelpers} for transition/visibility conditions.
5
7
  class StepBuilder
6
8
  include RuleHelpers
7
9
 
@@ -12,32 +14,58 @@ module FlowEngine
12
14
  @fields = nil
13
15
  @transitions = []
14
16
  @visibility_rule = nil
17
+ @decorations = nil
15
18
  end
16
19
 
20
+ # Sets the step/input type (e.g. :multi_select, :number_matrix).
21
+ # @param value [Symbol]
17
22
  def type(value)
18
23
  @type = value
19
24
  end
20
25
 
26
+ # Sets the prompt/label for the step.
27
+ # @param text [String]
21
28
  def question(text)
22
29
  @question = text
23
30
  end
24
31
 
32
+ # Sets the list of options for multi-select or choice steps.
33
+ # @param list [Array]
25
34
  def options(list)
26
35
  @options = list
27
36
  end
28
37
 
38
+ # Sets the list of field names for matrix-style steps (e.g. number_matrix).
39
+ # @param list [Array]
29
40
  def fields(list)
30
41
  @fields = list
31
42
  end
32
43
 
44
+ # Optional UI decorations (opaque to the engine).
45
+ # @param decorations [Object]
46
+ def decorations(decorations)
47
+ @decorations = decorations
48
+ end
49
+
50
+ # Adds a conditional transition to another step. First matching transition wins.
51
+ #
52
+ # @param to [Symbol] target step id
53
+ # @param if_rule [Rules::Base, nil] condition (nil = unconditional)
33
54
  def transition(to:, if_rule: nil)
34
55
  @transitions << Transition.new(target: to, rule: if_rule)
35
56
  end
36
57
 
58
+ # Sets the visibility rule for this step (DAG mode: step shown only when rule is true).
59
+ #
60
+ # @param rule [Rules::Base]
37
61
  def visible_if(rule)
38
62
  @visibility_rule = rule
39
63
  end
40
64
 
65
+ # Builds the {Node} for the given step id from accumulated attributes.
66
+ #
67
+ # @param id [Symbol] step id
68
+ # @return [Node]
41
69
  def build(id)
42
70
  Node.new(
43
71
  id: id,
@@ -46,7 +74,8 @@ module FlowEngine
46
74
  options: @options,
47
75
  fields: @fields,
48
76
  transitions: @transitions,
49
- visibility_rule: @visibility_rule
77
+ visibility_rule: @visibility_rule,
78
+ decorations: @decorations
50
79
  )
51
80
  end
52
81
  end
@@ -5,6 +5,8 @@ require_relative "dsl/step_builder"
5
5
  require_relative "dsl/flow_builder"
6
6
 
7
7
  module FlowEngine
8
+ # Namespace for the declarative flow DSL: {FlowBuilder} builds a {Definition} from blocks,
9
+ # {StepBuilder} builds individual {Node}s, and {RuleHelpers} provide rule factory methods.
8
10
  module DSL
9
11
  end
10
12
  end
@@ -1,9 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlowEngine
4
+ # Runtime session that drives flow navigation: holds definition, answers, and current step.
5
+ # Validates each answer via an optional {Validation::Adapter}, then advances using node transitions.
6
+ #
7
+ # @attr_reader definition [Definition] immutable flow definition
8
+ # @attr_reader answers [Hash] step_id => value (mutable as user answers)
9
+ # @attr_reader history [Array<Symbol>] ordered list of step ids visited (including current)
10
+ # @attr_reader current_step_id [Symbol, nil] current step id, or nil when flow is finished
4
11
  class Engine
5
12
  attr_reader :definition, :answers, :history, :current_step_id
6
13
 
14
+ # @param definition [Definition] the flow to run
15
+ # @param validator [Validation::Adapter] validator for step answers (default: {Validation::NullAdapter})
7
16
  def initialize(definition, validator: Validation::NullAdapter.new)
8
17
  @definition = definition
9
18
  @answers = {}
@@ -13,16 +22,23 @@ module FlowEngine
13
22
  @history << @current_step_id
14
23
  end
15
24
 
25
+ # @return [Node, nil] current step node, or nil if flow is finished
16
26
  def current_step
17
27
  return nil if finished?
18
28
 
19
29
  definition.step(@current_step_id)
20
30
  end
21
31
 
32
+ # @return [Boolean] true when there is no current step (flow ended)
22
33
  def finished?
23
34
  @current_step_id.nil?
24
35
  end
25
36
 
37
+ # Submits an answer for the current step, validates it, stores it, and advances to the next step.
38
+ #
39
+ # @param value [Object] user's answer for the current step
40
+ # @raise [AlreadyFinishedError] if the flow has already finished
41
+ # @raise [ValidationError] if the validator rejects the value
26
42
  def answer(value)
27
43
  raise AlreadyFinishedError, "Flow is already finished" if finished?
28
44
 
@@ -33,8 +49,74 @@ module FlowEngine
33
49
  advance_step
34
50
  end
35
51
 
52
+ # Serializable state for persistence or resumption.
53
+ #
54
+ # @return [Hash] current_step_id, answers, and history (string/symbol keys as stored)
55
+ def to_state
56
+ {
57
+ current_step_id: @current_step_id,
58
+ answers: @answers,
59
+ history: @history
60
+ }
61
+ end
62
+
63
+ # Rebuilds an engine from a previously saved state (e.g. from DB or session).
64
+ #
65
+ # @param definition [Definition] same definition used when state was captured
66
+ # @param state_hash [Hash] hash with :current_step_id, :answers, :history (keys may be strings)
67
+ # @param validator [Validation::Adapter] validator to use (default: NullAdapter)
68
+ # @return [Engine] restored engine instance
69
+ def self.from_state(definition, state_hash, validator: Validation::NullAdapter.new)
70
+ state = symbolize_state(state_hash)
71
+ engine = allocate
72
+ engine.send(:restore_state, definition, state, validator)
73
+ engine
74
+ end
75
+
76
+ # Normalizes a state hash so step ids and history entries are symbols; answers keys are symbols.
77
+ #
78
+ # @param hash [Hash] raw state (e.g. from JSON)
79
+ # @return [Hash] symbolized state
80
+ def self.symbolize_state(hash)
81
+ return hash unless hash.is_a?(Hash)
82
+
83
+ hash.each_with_object({}) do |(key, value), result|
84
+ sym_key = key.to_sym
85
+ result[sym_key] = case sym_key
86
+ when :current_step_id
87
+ value&.to_sym
88
+ when :history
89
+ Array(value).map { |v| v&.to_sym }
90
+ when :answers
91
+ symbolize_answers(value)
92
+ else
93
+ value
94
+ end
95
+ end
96
+ end
97
+
98
+ # @param answers [Hash] answers map (keys may be strings)
99
+ # @return [Hash] same map with symbol keys
100
+ def self.symbolize_answers(answers)
101
+ return {} unless answers.is_a?(Hash)
102
+
103
+ answers.each_with_object({}) do |(key, value), result|
104
+ result[key.to_sym] = value
105
+ end
106
+ end
107
+
108
+ private_class_method :symbolize_state, :symbolize_answers
109
+
36
110
  private
37
111
 
112
+ def restore_state(definition, state, validator)
113
+ @definition = definition
114
+ @validator = validator
115
+ @current_step_id = state[:current_step_id]
116
+ @answers = state[:answers] || {}
117
+ @history = state[:history] || []
118
+ end
119
+
38
120
  def advance_step
39
121
  node = definition.step(@current_step_id)
40
122
  next_id = node.next_step_id(answers)
@@ -1,10 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlowEngine
4
+ # Base exception for all flowengine errors.
4
5
  class Error < StandardError; end
6
+
7
+ # Raised when a flow definition is invalid (e.g. missing start step, unknown step reference).
5
8
  class DefinitionError < Error; end
9
+
10
+ # Raised when navigating to or requesting a step id that does not exist in the definition.
6
11
  class UnknownStepError < Error; end
12
+
13
+ # Base exception for runtime engine errors (e.g. validation, already finished).
7
14
  class EngineError < Error; end
15
+
16
+ # Raised when {Engine#answer} is called after the flow has already finished.
8
17
  class AlreadyFinishedError < EngineError; end
18
+
19
+ # Raised when the validator rejects the user's answer for the current step.
9
20
  class ValidationError < EngineError; end
10
21
  end
@@ -1,13 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlowEngine
4
+ # Evaluates rule AST nodes against a given answer context.
5
+ # Used by transitions and visibility checks; rule objects implement {Rules::Base#evaluate}.
6
+ #
7
+ # @attr_reader answers [Hash] current answer state (step_id => value)
4
8
  class Evaluator
5
9
  attr_reader :answers
6
10
 
11
+ # @param answers [Hash] answer context to evaluate rules against
7
12
  def initialize(answers)
8
13
  @answers = answers
9
14
  end
10
15
 
16
+ # Evaluates a single rule (or returns true if rule is nil).
17
+ #
18
+ # @param rule [Rules::Base, nil] rule to evaluate
19
+ # @return [Boolean] result of rule#evaluate(answers), or true when rule is nil
11
20
  def evaluate(rule)
12
21
  return true if rule.nil?
13
22
 
@@ -2,15 +2,22 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Graph
5
+ # Exports a flow {Definition} to Mermaid flowchart syntax for visualization.
6
+ # Nodes are labeled with truncated question text; edges show condition labels when present.
5
7
  class MermaidExporter
8
+ # Maximum characters for node labels in the diagram (longer text is truncated with "...").
6
9
  MAX_LABEL_LENGTH = 50
7
10
 
8
11
  attr_reader :definition
9
12
 
13
+ # @param definition [Definition] flow to export
10
14
  def initialize(definition)
11
15
  @definition = definition
12
16
  end
13
17
 
18
+ # Generates Mermaid flowchart TD (top-down) source.
19
+ #
20
+ # @return [String] Mermaid diagram source (e.g. for Mermaid.js or docs)
14
21
  def export
15
22
  lines = ["flowchart TD"]
16
23
 
@@ -1,13 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlowEngine
4
+ # A single step in the flow: question metadata, input config, and conditional transitions.
5
+ # Used by {Engine} to determine the next step and by UI/export to render the step.
6
+ #
7
+ # @attr_reader id [Symbol] unique step identifier
8
+ # @attr_reader type [Symbol] input type (e.g. :multi_select, :number_matrix)
9
+ # @attr_reader question [String] prompt text for the step
10
+ # @attr_reader options [Array, nil] choices for multi_select; nil for other types
11
+ # @attr_reader fields [Array, nil] field names for number_matrix etc.; nil otherwise
12
+ # @attr_reader transitions [Array<Transition>] ordered list of conditional next-step rules
13
+ # @attr_reader visibility_rule [Rules::Base, nil] rule controlling whether this node is visible (DAG mode)
4
14
  class Node
5
15
  attr_reader :id, :type, :question, :options, :fields, :transitions, :visibility_rule
6
16
 
7
- def initialize(id:, type:, question:, options: nil, fields: nil, transitions: [], visibility_rule: nil)
17
+ # @param id [Symbol] step id
18
+ # @param type [Symbol] step/input type
19
+ # @param question [String] label/prompt
20
+ # @param decorations [Object, nil] optional UI decorations (not used by engine)
21
+ # @param options [Array, nil] option list for multi_select
22
+ # @param fields [Array, nil] field list for matrix-style steps
23
+ # @param transitions [Array<Transition>] conditional next-step transitions (default: [])
24
+ # @param visibility_rule [Rules::Base, nil] optional rule for visibility (default: always visible)
25
+ def initialize(id:, # rubocop:disable Metrics/ParameterLists
26
+ type:,
27
+ question:,
28
+ decorations: nil,
29
+ options: nil,
30
+ fields: nil,
31
+ transitions: [],
32
+ visibility_rule: nil)
8
33
  @id = id
9
34
  @type = type
10
35
  @question = question
36
+ @decorations = decorations
11
37
  @options = options&.freeze
12
38
  @fields = fields&.freeze
13
39
  @transitions = transitions.freeze
@@ -15,11 +41,19 @@ module FlowEngine
15
41
  freeze
16
42
  end
17
43
 
44
+ # Resolves the next step id from current answers by evaluating transitions in order.
45
+ #
46
+ # @param answers [Hash] current answer state (step_id => value)
47
+ # @return [Symbol, nil] id of the next step, or nil if no transition matches (flow end)
18
48
  def next_step_id(answers)
19
49
  match = transitions.find { |t| t.applies?(answers) }
20
50
  match&.target
21
51
  end
22
52
 
53
+ # Whether this node should be considered visible given current answers (for DAG/visibility).
54
+ #
55
+ # @param answers [Hash] current answer state
56
+ # @return [Boolean] true if no visibility_rule, else result of rule evaluation
23
57
  def visible?(answers)
24
58
  return true if visibility_rule.nil?
25
59
 
@@ -2,19 +2,26 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Rules
5
+ # Composite rule: logical AND of multiple sub-rules. All must be true.
6
+ #
7
+ # @attr_reader rules [Array<Base>] sub-rules to evaluate
5
8
  class All < Base
6
9
  attr_reader :rules
7
10
 
11
+ # @param rules [Array<Base>] one or more rules (arrays are flattened)
8
12
  def initialize(*rules)
9
13
  super()
10
14
  @rules = rules.flatten.freeze
11
15
  freeze
12
16
  end
13
17
 
18
+ # @param answers [Hash] current answers
19
+ # @return [Boolean] true if every sub-rule evaluates to true
14
20
  def evaluate(answers)
15
21
  rules.all? { |rule| rule.evaluate(answers) }
16
22
  end
17
23
 
24
+ # @return [String] e.g. "(rule1 AND rule2 AND rule3)"
18
25
  def to_s
19
26
  "(#{rules.join(" AND ")})"
20
27
  end
@@ -2,19 +2,26 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Rules
5
+ # Composite rule: logical OR of multiple sub-rules. At least one must be true.
6
+ #
7
+ # @attr_reader rules [Array<Base>] sub-rules to evaluate
5
8
  class Any < Base
6
9
  attr_reader :rules
7
10
 
11
+ # @param rules [Array<Base>] one or more rules (arrays are flattened)
8
12
  def initialize(*rules)
9
13
  super()
10
14
  @rules = rules.flatten.freeze
11
15
  freeze
12
16
  end
13
17
 
18
+ # @param answers [Hash] current answers
19
+ # @return [Boolean] true if any sub-rule evaluates to true
14
20
  def evaluate(answers)
15
21
  rules.any? { |rule| rule.evaluate(answers) }
16
22
  end
17
23
 
24
+ # @return [String] e.g. "(rule1 OR rule2 OR rule3)"
18
25
  def to_s
19
26
  "(#{rules.join(" OR ")})"
20
27
  end
@@ -2,11 +2,20 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Rules
5
+ # Abstract base for rule AST nodes. Subclasses implement {#evaluate} and {#to_s}
6
+ # for use in transitions and visibility conditions.
5
7
  class Base
8
+ # Evaluates the rule against the current answer context.
9
+ #
10
+ # @param _answers [Hash] step_id => value
11
+ # @return [Boolean]
6
12
  def evaluate(_answers)
7
13
  raise NotImplementedError, "#{self.class}#evaluate must be implemented"
8
14
  end
9
15
 
16
+ # Human-readable representation (e.g. for graph labels).
17
+ #
18
+ # @return [String]
10
19
  def to_s
11
20
  raise NotImplementedError, "#{self.class}#to_s must be implemented"
12
21
  end
@@ -2,9 +2,16 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Rules
5
+ # Rule: the answer for the given field (as an array) includes the given value.
6
+ # Used for multi-select steps (e.g. "BusinessOwnership in earnings").
7
+ #
8
+ # @attr_reader field [Symbol] answer key (step id)
9
+ # @attr_reader value [Object] value that must be present in the array
5
10
  class Contains < Base
6
11
  attr_reader :field, :value
7
12
 
13
+ # @param field [Symbol] answer key
14
+ # @param value [Object] value to check for
8
15
  def initialize(field, value)
9
16
  super()
10
17
  @field = field
@@ -12,10 +19,13 @@ module FlowEngine
12
19
  freeze
13
20
  end
14
21
 
22
+ # @param answers [Hash] current answers
23
+ # @return [Boolean] true if answers[field] (as array) includes value
15
24
  def evaluate(answers)
16
25
  Array(answers[field]).include?(value)
17
26
  end
18
27
 
28
+ # @return [String] e.g. "BusinessOwnership in earnings"
19
29
  def to_s
20
30
  "#{value} in #{field}"
21
31
  end
@@ -2,9 +2,15 @@
2
2
 
3
3
  module FlowEngine
4
4
  module Rules
5
+ # Rule: the answer for the given field equals the given value.
6
+ #
7
+ # @attr_reader field [Symbol] answer key
8
+ # @attr_reader value [Object] expected value
5
9
  class Equals < Base
6
10
  attr_reader :field, :value
7
11
 
12
+ # @param field [Symbol] answer key
13
+ # @param value [Object] expected value
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
22
+ # @return [Boolean] true if answers[field] == value
15
23
  def evaluate(answers)
16
24
  answers[field] == value
17
25
  end
18
26
 
27
+ # @return [String] e.g. "marital_status == Married"
19
28
  def to_s
20
29
  "#{field} == #{value}"
21
30
  end