inquirex 0.2.0

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.
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module Rules
5
+ # Rule: the answer for the given field is present and not empty.
6
+ class NotEmpty < Base
7
+ attr_reader :field
8
+
9
+ def initialize(field)
10
+ super()
11
+ @field = field.to_sym
12
+ freeze
13
+ end
14
+
15
+ def evaluate(answers)
16
+ val = answers[@field]
17
+ return false if val.nil?
18
+ return false if val.respond_to?(:empty?) && val.empty?
19
+
20
+ true
21
+ end
22
+
23
+ def to_h
24
+ { "op" => "not_empty", "field" => @field.to_s }
25
+ end
26
+
27
+ def to_s
28
+ "#{@field} is not empty"
29
+ end
30
+
31
+ def self.from_h(hash)
32
+ field = hash["field"] || hash[:field]
33
+ new(field)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
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
+ # Can carry a `requires_server` flag for steps that need a server round-trip (e.g. LLM steps).
7
+ #
8
+ # @attr_reader target [Symbol] id of the step to go to when this transition applies
9
+ # @attr_reader rule [Rules::Base, nil] condition; nil means unconditional (always applies)
10
+ # @attr_reader requires_server [Boolean] whether this transition needs a server round-trip
11
+ class Transition
12
+ attr_reader :target, :rule, :requires_server
13
+
14
+ # @param target [Symbol] next step id
15
+ # @param rule [Rules::Base, nil] optional condition (nil = always)
16
+ # @param requires_server [Boolean] whether this edge needs server evaluation
17
+ def initialize(target:, rule: nil, requires_server: false)
18
+ @target = target.to_sym
19
+ @rule = rule
20
+ @requires_server = requires_server
21
+ freeze
22
+ end
23
+
24
+ # Human-readable label for the condition.
25
+ #
26
+ # @return [String] rule#to_s or "always"
27
+ def condition_label
28
+ @rule ? @rule.to_s : "always"
29
+ end
30
+
31
+ # Whether this transition should be taken given current answers.
32
+ #
33
+ # @param answers [Hash] current answer state
34
+ # @return [Boolean] true if rule is nil or rule evaluates to true
35
+ def applies?(answers)
36
+ return true if @rule.nil?
37
+
38
+ @rule.evaluate(answers)
39
+ end
40
+
41
+ # Serializes to a plain Hash for JSON output.
42
+ #
43
+ # @return [Hash]
44
+ def to_h
45
+ hash = { "to" => @target.to_s }
46
+ hash["rule"] = @rule.to_h if @rule
47
+ hash["requires_server"] = true if @requires_server
48
+ hash
49
+ end
50
+
51
+ # Deserializes from a plain Hash.
52
+ #
53
+ # @param hash [Hash] with string or symbol keys
54
+ # @return [Transition]
55
+ def self.from_h(hash)
56
+ target = hash["to"] || hash[:to]
57
+ rule_hash = hash["rule"] || hash[:rule]
58
+ requires_server = hash["requires_server"] || hash[:requires_server] || false
59
+ rule = rule_hash ? Rules::Base.from_h(rule_hash) : nil
60
+ new(target:, rule:, requires_server:)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module Validation
5
+ # Result of validating a step answer.
6
+ #
7
+ # @attr_reader errors [Array<String>] validation error messages (empty when valid)
8
+ class Result
9
+ attr_reader :errors
10
+
11
+ # @param valid [Boolean]
12
+ # @param errors [Array<String>]
13
+ def initialize(valid:, errors: [])
14
+ @valid = valid
15
+ @errors = errors.freeze
16
+ freeze
17
+ end
18
+
19
+ # @return [Boolean]
20
+ def valid?
21
+ @valid
22
+ end
23
+ end
24
+
25
+ # Abstract adapter for step-level validation. Implement #validate to plug in
26
+ # dry-validation, ActiveModel::Validations, JSON Schema, or any other validator.
27
+ class Adapter
28
+ # @param _node [Node] the current step (for schema/constraints)
29
+ # @param _input [Object] value submitted by the user
30
+ # @return [Result]
31
+ def validate(_node, _input)
32
+ raise NotImplementedError, "#{self.class}#validate must be implemented"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module Validation
5
+ # No-op validator: always accepts any input.
6
+ # Used by default when no validation adapter is configured.
7
+ class NullAdapter < Adapter
8
+ def validate(_node, _input)
9
+ Result.new(valid: true)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ VERSION = "0.2.0"
5
+ end
data/lib/inquirex.rb ADDED
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "inquirex/version"
4
+ require_relative "inquirex/errors"
5
+
6
+ # Rules subsystem
7
+ require_relative "inquirex/rules/base"
8
+ require_relative "inquirex/rules/contains"
9
+ require_relative "inquirex/rules/equals"
10
+ require_relative "inquirex/rules/greater_than"
11
+ require_relative "inquirex/rules/less_than"
12
+ require_relative "inquirex/rules/not_empty"
13
+ require_relative "inquirex/rules/all"
14
+ require_relative "inquirex/rules/any"
15
+
16
+ # Core graph objects
17
+ require_relative "inquirex/transition"
18
+ require_relative "inquirex/evaluator"
19
+ require_relative "inquirex/node"
20
+ require_relative "inquirex/definition"
21
+ require_relative "inquirex/answers"
22
+
23
+ # Validation
24
+ require_relative "inquirex/validation/adapter"
25
+ require_relative "inquirex/validation/null_adapter"
26
+
27
+ # DSL
28
+ require_relative "inquirex/dsl"
29
+ require_relative "inquirex/dsl/rule_helpers"
30
+ require_relative "inquirex/dsl/step_builder"
31
+ require_relative "inquirex/dsl/flow_builder"
32
+
33
+ # Engine
34
+ require_relative "inquirex/engine/state_serializer"
35
+ require_relative "inquirex/engine"
36
+
37
+ # Graph utilities
38
+ require_relative "inquirex/graph/mermaid_exporter"
39
+
40
+ # Declarative, rules-driven questionnaire engine for building conditional intake forms,
41
+ # qualification wizards, and branching surveys.
42
+ #
43
+ # @example Define a flow
44
+ # definition = Inquirex.define id: "tax-intake-2025" do
45
+ # meta title: "Tax Preparation Intake", subtitle: "Let's understand your situation"
46
+ # start :filing_status
47
+ #
48
+ # ask :filing_status do
49
+ # type :enum
50
+ # question "What is your filing status?"
51
+ # options single: "Single", married_jointly: "Married Filing Jointly"
52
+ # transition to: :dependents
53
+ # end
54
+ #
55
+ # ask :dependents do
56
+ # type :integer
57
+ # question "How many dependents?"
58
+ # default 0
59
+ # transition to: :done
60
+ # end
61
+ #
62
+ # say :done do
63
+ # text "Thank you!"
64
+ # end
65
+ # end
66
+ #
67
+ # engine = Inquirex::Engine.new(definition)
68
+ # engine.answer("single")
69
+ # engine.answer(2)
70
+ # engine.advance # past the :done say step
71
+ # engine.finished? # => true
72
+ #
73
+ # @example JSON round-trip
74
+ # json = definition.to_json
75
+ # restored = Inquirex::Definition.from_json(json)
76
+ #
77
+ module Inquirex
78
+ # Builds an immutable Definition from the declarative DSL block.
79
+ #
80
+ # @param id [String, nil] optional flow identifier
81
+ # @param version [String] semver (default: "1.0.0")
82
+ # @yield context of DSL::FlowBuilder
83
+ # @return [Definition]
84
+ def self.define(id: nil, version: "1.0.0", &)
85
+ builder = DSL::FlowBuilder.new(id:, version:)
86
+ builder.instance_eval(&)
87
+ builder.build
88
+ end
89
+
90
+ # Evaluates a string of DSL code and returns the resulting definition.
91
+ # Intended for loading flow definitions from files or stored text.
92
+ #
93
+ # @param text [String] Ruby source containing Inquirex.define { ... }
94
+ # @return [Definition]
95
+ # @raise [Errors::DefinitionError] on syntax or evaluation errors
96
+ def self.load_dsl(text)
97
+ # rubocop:disable Security/Eval
98
+ eval(text, TOPLEVEL_BINDING.dup, "(dsl)", 1)
99
+ # rubocop:enable Security/Eval
100
+ rescue SyntaxError => e
101
+ raise Errors::DefinitionError, "DSL syntax error: #{e.message}"
102
+ rescue StandardError => e
103
+ raise Errors::DefinitionError, "DSL evaluation error: #{e.message}"
104
+ end
105
+ end
data/sig/inquirex.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Inquirex
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inquirex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Gredeskoul
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Inquirex lets you define multi-step questionnaires as directed graphs
13
+ with conditional branching, using a conversational DSL (ask, say, mention) and an
14
+ AST-based rule system (contains, equals, greater_than, all, any). The engine walks
15
+ the graph, collects structured answers, and serializes everything to JSON — making
16
+ it the ideal backbone for cross-platform intake forms where the frontend is a chat
17
+ widget, a terminal, or a mobile app. Framework-agnostic, zero dependencies, thread-safe
18
+ immutable definitions.
19
+ email:
20
+ - kigster@gmail.com
21
+ executables:
22
+ - inquirex
23
+ extensions: []
24
+ extra_rdoc_files: []
25
+ files:
26
+ - ".relaxed_rubocop.yml"
27
+ - ".rub"
28
+ - ".ruby-version"
29
+ - ".secrets.baseline"
30
+ - Brewfile
31
+ - CHANGELOG.md
32
+ - LICENSE.txt
33
+ - README.md
34
+ - Rakefile
35
+ - exe/inquirex
36
+ - justfile
37
+ - lefthook.yml
38
+ - lib/inquirex.rb
39
+ - lib/inquirex/answers.rb
40
+ - lib/inquirex/definition.rb
41
+ - lib/inquirex/dsl.rb
42
+ - lib/inquirex/dsl/flow_builder.rb
43
+ - lib/inquirex/dsl/rule_helpers.rb
44
+ - lib/inquirex/dsl/step_builder.rb
45
+ - lib/inquirex/engine.rb
46
+ - lib/inquirex/engine/state_serializer.rb
47
+ - lib/inquirex/errors.rb
48
+ - lib/inquirex/evaluator.rb
49
+ - lib/inquirex/graph/mermaid_exporter.rb
50
+ - lib/inquirex/node.rb
51
+ - lib/inquirex/rules/all.rb
52
+ - lib/inquirex/rules/any.rb
53
+ - lib/inquirex/rules/base.rb
54
+ - lib/inquirex/rules/contains.rb
55
+ - lib/inquirex/rules/equals.rb
56
+ - lib/inquirex/rules/greater_than.rb
57
+ - lib/inquirex/rules/less_than.rb
58
+ - lib/inquirex/rules/not_empty.rb
59
+ - lib/inquirex/transition.rb
60
+ - lib/inquirex/validation/adapter.rb
61
+ - lib/inquirex/validation/null_adapter.rb
62
+ - lib/inquirex/version.rb
63
+ - sig/inquirex.rbs
64
+ homepage: https://github.com/kigster/inquirex
65
+ licenses:
66
+ - MIT
67
+ metadata:
68
+ allowed_push_host: https://rubygems.org
69
+ homepage_uri: https://github.com/kigster/inquirex
70
+ source_code_uri: https://github.com/kigster/inquirex
71
+ changelog_uri: https://github.com/kigster/inquirex/blob/main/CHANGELOG.md
72
+ rubygems_mfa_required: 'true'
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 4.0.0
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubygems_version: 4.0.10
88
+ specification_version: 4
89
+ summary: A declarative, rules-driven questionnaire engine for building conditionally-branching
90
+ intake forms, qualification wizards, and surveys in pure Ruby.
91
+ test_files: []