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.
- checksums.yaml +7 -0
- data/.relaxed_rubocop.yml +153 -0
- data/.rub +0 -0
- data/.ruby-version +1 -0
- data/.secrets.baseline +127 -0
- data/Brewfile +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +289 -0
- data/Rakefile +36 -0
- data/exe/inquirex +3 -0
- data/justfile +38 -0
- data/lefthook.yml +35 -0
- data/lib/inquirex/answers.rb +133 -0
- data/lib/inquirex/definition.rb +109 -0
- data/lib/inquirex/dsl/flow_builder.rb +107 -0
- data/lib/inquirex/dsl/rule_helpers.rb +57 -0
- data/lib/inquirex/dsl/step_builder.rb +109 -0
- data/lib/inquirex/dsl.rb +9 -0
- data/lib/inquirex/engine/state_serializer.rb +36 -0
- data/lib/inquirex/engine.rb +128 -0
- data/lib/inquirex/errors.rb +30 -0
- data/lib/inquirex/evaluator.rb +26 -0
- data/lib/inquirex/graph/mermaid_exporter.rb +55 -0
- data/lib/inquirex/node.rb +192 -0
- data/lib/inquirex/rules/all.rb +33 -0
- data/lib/inquirex/rules/any.rb +33 -0
- data/lib/inquirex/rules/base.rb +52 -0
- data/lib/inquirex/rules/contains.rb +36 -0
- data/lib/inquirex/rules/equals.rb +35 -0
- data/lib/inquirex/rules/greater_than.rb +35 -0
- data/lib/inquirex/rules/less_than.rb +35 -0
- data/lib/inquirex/rules/not_empty.rb +37 -0
- data/lib/inquirex/transition.rb +63 -0
- data/lib/inquirex/validation/adapter.rb +36 -0
- data/lib/inquirex/validation/null_adapter.rb +13 -0
- data/lib/inquirex/version.rb +5 -0
- data/lib/inquirex.rb +105 -0
- data/sig/inquirex.rbs +4 -0
- metadata +91 -0
|
@@ -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
|
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
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: []
|