Class: Hames::Schema
- Inherits:
-
Object
- Object
- Hames::Schema
- Defined in:
- lib/hames/schema.rb
Overview
The kernel's own tiny config validator (docs/composition.md §9): a plain description of a service's config keys — for each one a type:, whether it is required:, an optional enum: of legal values, a default:, and a doc: string — plus the code that checks a config hash against it and reports what does not fit.
Pure stdlib rather than dry-schema, because the kernel's zero-runtime- dependency rule is a design constraint rather than a coincidence (CLAUDE.md). The plan's §10 dry-schema mention is superseded by that rule.
Defined Under Namespace
Modules: DSL Classes: KeySpec, Result
Instance Attribute Summary collapse
-
#keys ⇒ Object
readonly
Returns the value of attribute keys.
Class Method Summary collapse
-
.article(word) ⇒ Object
"an Integer", "a String" — the article chosen by the leading sound, close enough on class names, so a message never reads "a Integer".
- .build(specs) ⇒ Object
-
.declared ⇒ Object
Every declared schema, class => schema, in declaration order.
-
.describe(value) ⇒ Object
A scalar renders as itself ("got 5", "got nil"); anything larger renders as its class, so a wrong Hash where a String was wanted does not paste a whole tree into a one-line status.
- .register(klass, schema) ⇒ Object
-
.reset_declared! ⇒ Object
test hook.
-
.type_desc(type) ⇒ Object
A boolean is the TrueClass/FalseClass union, so it reads as "a boolean" rather than naming two classes nobody wrote in a config.
Instance Method Summary collapse
-
#initialize(keys) ⇒ Schema
constructor
A new instance of Schema.
-
#validate(config, subject: nil, redact: false) ⇒ Object
Validates a config hash without mutating it.
Constructor Details
#initialize(keys) ⇒ Schema
Returns a new instance of Schema.
74 75 76 |
# File 'lib/hames/schema.rb', line 74 def initialize(keys) @keys = keys end |
Instance Attribute Details
#keys ⇒ Object (readonly)
Returns the value of attribute keys.
72 73 74 |
# File 'lib/hames/schema.rb', line 72 def keys @keys end |
Class Method Details
.article(word) ⇒ Object
"an Integer", "a String" — the article chosen by the leading sound, close enough on class names, so a message never reads "a Integer".
138 139 140 141 |
# File 'lib/hames/schema.rb', line 138 def self.article(word) word = word.to_s /\A[aeiou]/i.match?(word) ? "an #{word}" : "a #{word}" end |
.build(specs) ⇒ Object
62 63 64 65 66 67 68 69 70 |
# File 'lib/hames/schema.rb', line 62 def self.build(specs) keys = (specs || {}).to_h do |name, opts| opts ||= {} key = name.to_sym [key, KeySpec.new(name: key, type: opts[:type], required: opts.fetch(:required, false), enum: opts[:enum], default: opts[:default], doc: opts[:doc])] end new(keys) end |
.declared ⇒ Object
Every declared schema, class => schema, in declaration order. The catalog generator (rake config:catalog) walks this once the code is required, the way events:catalog walks Hames.catalog.
58 |
# File 'lib/hames/schema.rb', line 58 def self.declared = (@declared ||= {}) |
.describe(value) ⇒ Object
A scalar renders as itself ("got 5", "got nil"); anything larger renders as its class, so a wrong Hash where a String was wanted does not paste a whole tree into a one-line status. Only reached when redact is false — doctor never lets a rejected value's content into a message.
147 148 149 150 151 152 |
# File 'lib/hames/schema.rb', line 147 def self.describe(value) case value when String, Numeric, Symbol, true, false then value.inspect else value.class.to_s end end |
.register(klass, schema) ⇒ Object
59 |
# File 'lib/hames/schema.rb', line 59 def self.register(klass, schema) = declared[klass] = schema |
.reset_declared! ⇒ Object
test hook
60 |
# File 'lib/hames/schema.rb', line 60 def self.reset_declared! = declared.clear # test hook |
.type_desc(type) ⇒ Object
A boolean is the TrueClass/FalseClass union, so it reads as "a boolean" rather than naming two classes nobody wrote in a config.
128 129 130 131 132 133 134 |
# File 'lib/hames/schema.rb', line 128 def self.type_desc(type) types = Array(type) return "a boolean" if types.sort_by(&:name) == [FalseClass, TrueClass] return article(types.first.name) if types.length == 1 "one of #{types.map(&:name).join(', ')}" end |
Instance Method Details
#validate(config, subject: nil, redact: false) ⇒ Object
Validates a config hash without mutating it. subject names the row (or
class) so an error reads as "sandbox: image is required but was not set" —
a refusal that cannot say which of thirty rows it is about is one an
operator cannot act on.
A key that is absent OR present-but-nil counts as UNSET: a required unset
key is an error, a non-required one is fine — its default applies, and an
unset !env resolving to nil is an ordinary state (§5) rather than a fault.
A non-nil value is checked against type and enum. Extra keys WARN
rather than fail, because config rows grow.
redact: governs whether a rejected value's CONTENT may appear in the
message. It stays false for programmatic callers (which hold plain config,
and a message naming the value is more useful), and doctor passes true
because by the time it validates, a value is a materialized !env/!setting/
!ruby result and may be a secret — so its message names the type only,
never the content.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/hames/schema.rb', line 95 def validate(config, subject: nil, redact: false) config ||= {} prefix = subject ? "#{subject}: " : "" errors = [] @keys.each_value do |spec| value = config[spec.name] if value.nil? errors << "#{prefix}#{spec.name} is required but was not set" if spec.required next end if spec.type && Array(spec.type).none? { |t| value.is_a?(t) } got = redact ? Schema.article(value.class.name) : Schema.describe(value) errors << "#{prefix}#{spec.name} must be #{Schema.type_desc(spec.type)}, got #{got}" end if spec.enum && !spec.enum.include?(value) allowed = spec.enum.map(&:inspect).join(", ") # Redacted: the allowed set is schema-defined and safe to print; the # configured value is not, so it is named as out-of-set, not echoed. errors << if redact "#{prefix}#{spec.name} must be one of #{allowed} (the configured value is not)" else "#{prefix}#{spec.name} must be one of #{allowed}, got #{value.inspect}" end end end warnings = (config.keys - @keys.keys).map { |extra| "#{prefix}#{extra} is not a known config key" } Result.new(errors: errors, warnings: warnings) end |