Module: Legion::Extensions::Llm::Canonical::Strict
- Defined in:
- lib/legion/extensions/llm/canonical/strict.rb
Overview
Shared strict-factory guards (04 L1/L3/L5/L6) — one implementation for every type. Nil or wrong-class input raises ArgumentError naming the site, member, and offending class. No factory returns nil; unknown keys fold into the metadata member (no drops, no raises).
H1: every type installs a validated .new (a single strict
constructor). .new, .build, and .from_hash all run the same
member contract — a .new-minted object cannot carry poison past
the class-membership boundaries (enforce_canonical_messages!,
fleet W4 rehydration, the conformance kit).
Class Method Summary collapse
- .enum!(value, allowed, site, member) ⇒ Object
- .expect_type!(value, allowed, site, member) ⇒ Object
-
.fold_unknowns!(type_class, site, hash) ⇒ Object
04 L5: unknown keys fold into the metadata member.
-
.install_strict_new!(type_class, &validate) ⇒ Object
H1: install the strict
.newon a Data type. -
.member_values!(type_class, site, args, kwargs) ⇒ Object
Map raw
.newarguments (positional or keyword form) onto a member => value Hash. - .metadata!(value, site, member: :metadata) ⇒ Object
- .require_hash!(source, site) ⇒ Object
- .symbolize_keys(hash) ⇒ Object
Class Method Details
.enum!(value, allowed, site, member) ⇒ Object
88 89 90 91 92 93 |
# File 'lib/legion/extensions/llm/canonical/strict.rb', line 88 def enum!(value, allowed, site, member) return value if value.nil? || allowed.include?(value) raise ArgumentError, "#{site}: Invalid #{member}: #{value.inspect}. Must be one of: #{allowed.join(', ')}" end |
.expect_type!(value, allowed, site, member) ⇒ Object
95 96 97 98 99 100 |
# File 'lib/legion/extensions/llm/canonical/strict.rb', line 95 def expect_type!(value, allowed, site, member) return value if value.nil? || allowed.any? { |klass| value.is_a?(klass) } raise ArgumentError, "#{site}: #{member} expected #{allowed.map(&:name).join(' | ')}, got #{value.class}" end |
.fold_unknowns!(type_class, site, hash) ⇒ Object
04 L5: unknown keys fold into the metadata member.
73 74 75 76 77 78 |
# File 'lib/legion/extensions/llm/canonical/strict.rb', line 73 def fold_unknowns!(type_class, site, hash) = (hash.delete(:metadata), site) known = type_class.members.map(&:to_sym) (hash.keys - known).each { |key| [key] = hash.delete(key) } end |
.install_strict_new!(type_class, &validate) ⇒ Object
H1: install the strict .new on a Data type. The C-level
constructor is preserved as the PRIVATE data_define_new (used
only by the strict .new itself); the public .new maps the
call shape (member_values!), runs the type's member contract
(validate, a ->(values, site) block returning the normalized
values), and delegates. Every construction path — .new, .build,
.from_hash — funnels through the same contract.
29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/legion/extensions/llm/canonical/strict.rb', line 29 def install_strict_new!(type_class, &validate) singleton = type_class.singleton_class singleton.alias_method(:data_define_new, :new) singleton.send(:private, :data_define_new) singleton.define_method(:new) do |*args, **kwargs| values = Strict.member_values!(self, self::NEW_SITE, args, kwargs) values = validate.call(values, self::NEW_SITE) data_define_new(**values) end end |
.member_values!(type_class, site, args, kwargs) ⇒ Object
Map raw .new arguments (positional or keyword form) onto a
member => value Hash. Wrong call shapes raise a typed
ArgumentError naming the site: mixing both forms, a positional
count mismatch, or an unknown member. Members absent from the
call map to nil and follow each member's own contract.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/legion/extensions/llm/canonical/strict.rb', line 46 def member_values!(type_class, site, args, kwargs) members = type_class.members raise ArgumentError, "#{site}: pass either positional or keyword members, not both" if args.any? && kwargs.any? if args.any? raise ArgumentError, "#{site}: expected #{members.size} positional members, got #{args.size}" unless args.size == members.size return members.zip(args).to_h end unknown = kwargs.keys - members.map(&:to_sym) raise ArgumentError, "#{site}: unknown member(s) #{unknown.sort.join(', ')}" unless unknown.empty? members.to_h { |member| [member, kwargs[member]] } end |
.metadata!(value, site, member: :metadata) ⇒ Object
80 81 82 83 84 85 86 |
# File 'lib/legion/extensions/llm/canonical/strict.rb', line 80 def (value, site, member: :metadata) return {} if value.nil? raise ArgumentError, "#{site}: #{member} expected Hash, got #{value.class}" unless value.is_a?(::Hash) value end |
.require_hash!(source, site) ⇒ Object
62 63 64 65 66 |
# File 'lib/legion/extensions/llm/canonical/strict.rb', line 62 def require_hash!(source, site) return source if source.is_a?(::Hash) raise ArgumentError, "#{site}: expected Hash, got #{source.class}" end |
.symbolize_keys(hash) ⇒ Object
68 69 70 |
# File 'lib/legion/extensions/llm/canonical/strict.rb', line 68 def symbolize_keys(hash) hash.transform_keys { |key| key.respond_to?(:to_sym) ? key.to_sym : key } end |