Module: Inquirex::SafeSource::Vocabulary
- Defined in:
- lib/inquirex/safe_source/vocabulary.rb
Overview
The registry of DSL words Validator accepts, and of the words it deliberately refuses.
Every entry is bound to the builder that implements it: the six flow
verbs are derived from Node::VERBS, step types from Node::TYPES, the
rule vocabulary from DSL::RuleHelpers, and each scope declares the
method list it governs (vocabulary:) so that Vocabulary.undeclared_names can
name any builder method nobody has made an allowlist decision about.
spec/inquirex/safe_source/vocabulary_spec.rb fails the build when that
list is non-empty — which is what stops this table drifting away from the
DSL it is supposed to describe.
Downstream gems extend the vocabulary at boot rather than forking it.
Constant Summary collapse
- ENTRY_CONSTANT =
The only constant a flow definition may name.
:Inquirex- ENTRY_METHOD =
The only method that constant may receive.
:define- ANY_OTHER =
Keyword-descriptor key standing for "every keyword not named explicitly". Legitimate only where the real method takes
**rest. :*- TRANSITION_KEYWORDS =
Value kinds a
transitionaccepts. Shared by every step-like scope. { to: :symbol, if_rule: :rule, requires_server: :literal }.freeze
- EMAIL_FIELD_KEYWORDS =
The
send_emailfields, and the shape each accepts. One definition serves both DSL forms — the flow-level keywords (send_email to: ...) and the block setters (send_email do to "..." end) — so the two can never drift apart.headersis:literalbecause it is a Hash; every other field is one static string. { to: :string, from: :string, cc: :string, bcc: :string, reply_to: :string, subject: :string, text: :string, markdown_text: :string, html: :string, headers: :literal }.freeze
- PLUMBING =
Builder methods that exist for the builder's own sake and are not DSL words, so they need no allowlist decision.
paramsis SendEmailBuilder's accumulator, read by FlowBuilder once the block has run — nothing a flow author writes. %i[build params method_missing respond_to_missing?].freeze
Class Method Summary collapse
-
.allow(scope, name, positional: [], keywords: nil, block: :forbidden) ⇒ CallSpec
Allowlists one call inside one scope.
-
.allowed_names(scope) ⇒ Array<Symbol>
Every allowlisted call in the scope.
-
.entry_spec ⇒ CallSpec
The spec for
Inquirex.defineitself. -
.exclude(scope, name, reason) ⇒ String
Records a DSL word that is knowingly not allowlisted, with the reason — which Validator quotes back to the author, so a rejection reads as a decision rather than an oversight.
-
.excluded_names(scope) ⇒ Array<Symbol>
Every knowingly excluded call in the scope.
-
.exclusion_for(scope, name) ⇒ String?
Why the call is excluded, nil when it was never part of the scope's vocabulary at all.
-
.governed_names(scope) ⇒ Array<Symbol>
The DSL words the scope's builder implements, per the
vocabulary:binding given to Vocabulary.register_scope, minus builder plumbing. -
.label_for(scope) ⇒ String
The scope's human-readable label, article included.
-
.register_scope(name, label:, vocabulary: nil) ⇒ void
Declares a nested scope (idempotent): a block whose statements are validated against their own table of calls.
-
.reset! ⇒ void
Discards every registration and reinstalls the core vocabulary.
-
.scope?(name) ⇒ Boolean
Whether the scope has been registered.
-
.scopes ⇒ Array<Symbol>
Every registered scope.
-
.spec_for(scope, name) ⇒ CallSpec?
Nil when the call is not allowlisted.
-
.stale_names(scope) ⇒ Array<Symbol>
Allowlist entries that no longer correspond to a real builder method — a typo, or a DSL word that has since been removed or renamed.
-
.undeclared_names(scope) ⇒ Array<Symbol>
Builder methods with no allowlist decision: neither allowed nor knowingly excluded.
Class Method Details
.allow(scope, name, positional: [], keywords: nil, block: :forbidden) ⇒ CallSpec
Allowlists one call inside one scope.
92 93 94 95 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 92 def allow(scope, name, positional: [], keywords: nil, block: :forbidden) table = @calls.fetch(scope.to_sym) { raise ArgumentError, "Unknown SafeSource scope: #{scope.inspect}" } table[name.to_sym] = CallSpec.new(positional:, keywords:, block:) end |
.allowed_names(scope) ⇒ Array<Symbol>
Returns every allowlisted call in the scope.
145 146 147 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 145 def allowed_names(scope) @calls.fetch(scope.to_sym, {}).keys end |
.entry_spec ⇒ CallSpec
The spec for Inquirex.define itself.
192 193 194 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 192 def entry_spec CallSpec.new(positional: [], keywords: { id: :string, version: :string }, block: :flow) end |
.exclude(scope, name, reason) ⇒ String
Records a DSL word that is knowingly not allowlisted, with the reason — which Inquirex::SafeSource::Validator quotes back to the author, so a rejection reads as a decision rather than an oversight.
106 107 108 109 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 106 def exclude(scope, name, reason) table = @exclusions.fetch(scope.to_sym) { raise ArgumentError, "Unknown SafeSource scope: #{scope.inspect}" } table[name.to_sym] = reason end |
.excluded_names(scope) ⇒ Array<Symbol>
Returns every knowingly excluded call in the scope.
151 152 153 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 151 def excluded_names(scope) @exclusions.fetch(scope.to_sym, {}).keys end |
.exclusion_for(scope, name) ⇒ String?
Returns why the call is excluded, nil when it was never part of the scope's vocabulary at all.
122 123 124 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 122 def exclusion_for(scope, name) @exclusions.fetch(scope.to_sym, {})[name.to_sym] end |
.governed_names(scope) ⇒ Array<Symbol>
The DSL words the scope's builder implements, per the vocabulary:
binding given to register_scope, minus builder plumbing.
160 161 162 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 160 def governed_names(scope) @vocabularies.fetch(scope.to_sym, nil)&.call.to_a.map(&:to_sym) - PLUMBING end |
.label_for(scope) ⇒ String
Returns the scope's human-readable label, article included.
128 129 130 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 128 def label_for(scope) @labels.fetch(scope.to_sym, "an unknown") end |
.register_scope(name, label:, vocabulary: nil) ⇒ void
This method returns an undefined value.
Declares a nested scope (idempotent): a block whose statements are validated against their own table of calls.
75 76 77 78 79 80 81 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 75 def register_scope(name, label:, vocabulary: nil) sym = name.to_sym @labels[sym] = label @vocabularies[sym] = vocabulary @calls[sym] ||= {} @exclusions[sym] ||= {} end |
.reset! ⇒ void
This method returns an undefined value.
Discards every registration and reinstalls the core vocabulary. Hosts call this to undo an extension; the specs call it to isolate.
200 201 202 203 204 205 206 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 200 def reset! @labels = {} @calls = {} @exclusions = {} @vocabularies = {} install_core! end |
.scope?(name) ⇒ Boolean
Returns whether the scope has been registered.
134 135 136 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 134 def scope?(name) @labels.key?(name.to_sym) end |
.scopes ⇒ Array<Symbol>
Returns every registered scope.
139 140 141 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 139 def scopes @labels.keys end |
.spec_for(scope, name) ⇒ CallSpec?
Returns nil when the call is not allowlisted.
114 115 116 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 114 def spec_for(scope, name) @calls.fetch(scope.to_sym, {})[name.to_sym] end |
.stale_names(scope) ⇒ Array<Symbol>
Allowlist entries that no longer correspond to a real builder method — a typo, or a DSL word that has since been removed or renamed.
183 184 185 186 187 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 183 def stale_names(scope) return [] if @vocabularies.fetch(scope.to_sym, nil).nil? (allowed_names(scope) + excluded_names(scope)) - governed_names(scope) end |
.undeclared_names(scope) ⇒ Array<Symbol>
Builder methods with no allowlist decision: neither allowed nor knowingly excluded. Anything here is a new DSL word that silently became unusable in safe mode (or, worse, an old one that quietly gained a new meaning).
174 175 176 |
# File 'lib/inquirex/safe_source/vocabulary.rb', line 174 def undeclared_names(scope) governed_names(scope) - allowed_names(scope) - excluded_names(scope) end |