module Schematrix::Generators::Templates::TypeComposer
The bare-type algebra shared by every type-emitting format: computing the type of a schema, its variants, array elements and intersections is one algorithm; only the syntax of the target notation differs. Formats including this module The spelling hooks every notation supplies: untyped, scalar type, array_of, string_keyed_hash, null_only_type, nilable_type, maybe_type, element_union, join_union and join_intersection. Maybe mode itself is read off the template (Templates::Base#maybe_wrapped?).
Public Instance Methods
# File lib/schematrix/generators/templates/type_composer.rb, line 157 def array_element_type(path, schema) terms = schema .prefix_items .each_with_index .filter_map do |item, index| next if item.type.include?(TYPE_BOTTOM) prefix_item_type(path, item, index) end .uniq if schema.items.nil? terms << untyped elsif !schema.items.type.include?(TYPE_BOTTOM) terms << type_of(path, "items", schema.items) end element_union(union(terms), terms) end
Element union: the union of prefix item types, plus the items type when present, plus untyped whenever the tail is open (no items). items: false closes the tail and adds no term, and a false prefix item accepts nothing and likewise contributes no term. Inline object prefix items fall back to untyped (no stable class name); $ref items keep their class; map-shaped items are plain Hashes.
# File lib/schematrix/generators/templates/type_composer.rb, line 88 def bare_type(path, name, schema) return untyped if schema.nil? return map_type(path, name, schema) if schema.map_shaped? members = Schema.qualifying_enum_members(schema.enum) if members && (@enums == :unions || schema.enum_class?) class_name = enum_class_name(path, name, schema) return enum_type(class_name, members, schema) end base = schema.type.flat_map do |type| case type when TYPE_OBJECT [class_name_from_path(path, name)] when TYPE_ARRAY element = array_element_type(nested_locator(path, name), schema) [array_of(element)] when TYPE_NULL [] else # nil elements (malformed type lists) fall through the # scalar map's nil key to untyped, like any unknown type [scalar_type(type, schema.format)] end end # A conjoined schema may combine its own terms with a # $ref; they intersect when the intersection flag is set, # union otherwise base << class_name_from_path(schema.ref) unless schema.ref.nil? base.concat(variant_types(path, name, schema.any_of, "anyOf")) base.concat(variant_types(path, name, schema.one_of, "oneOf")) return intersection(base) if schema.intersection union(base) end
Returns the type for a schema as a union string, without nilability: the declared type set, the $ref target, and both variant lists folded in, each variant a bare type too. A schema whose whole shape is a catch-all renders as a plain String-keyed Hash instead of a class.
Source
# File lib/schematrix/generators/templates/type_composer.rb, line 187 def intersection(types) terms = types.reject { |type| type == untyped } return untyped if terms.empty? return terms.first if terms.size == 1 join_intersection(terms) end
Conjoin: distinct declarations intersect syntactically, even if uninhabited. untyped is the identity; an empty conjunction falls back to untyped.
# File lib/schematrix/generators/templates/type_composer.rb, line 132 def map_type(path, name, schema) slot = nested_locator(path, name) string_keyed_hash(map_value_type(slot, schema.additional_properties)) end
A catch-all-only schema types as a plain Hash keyed by the original String property names; the values carry the catch-all’s own type (and coerce from it in loading).
# File lib/schematrix/generators/templates/type_composer.rb, line 141 def map_value_type(path, schema) return untyped if schema.nil? || schema.untyped? return scalar_type(TYPE_NULL) if schema.type == Set[TYPE_NULL] terms = [bare_type(path, "additionalProperties", schema)] terms << scalar_type(TYPE_NULL) if schema.type.include?(TYPE_NULL) union(terms) end
The value type of a plain-Hash map slot: the catch-all’s bare type with an explicit nil term when the catch-all admits nulls. Unlike optional slots there is no absence nilability here: a missing key is not a nil value.
# File lib/schematrix/generators/templates/type_composer.rb, line 54 def maybe_absence_type(combined, schema, slot:) if slot && maybe_wrapped?(schema) # Nullability stays inside the wrap: the loader is # over-accepting and builds Present(nil) for an explicit # null, so the declared type has to admit it whatever the # schema says about the value. return maybe_type(nilable_type(combined)) end return combined if schema.strictly_required? nilable_type(combined) end
The absence wrap of one slot under the current optionals mode: the Maybe type over the bare type, nilability inside wherever the value itself may be null. Only property slots wrap: array elements, catch-all values, and every other nested position loads and dumps as a plain value, so they follow the nilable mode’s spelling wherever the schema does not demand the keyword.
Source
# File lib/schematrix/generators/templates/type_composer.rb, line 78 def null_only?(schema) schema.type == Set[TYPE_NULL] && schema.ref.nil? && schema.any_of.empty? && schema.one_of.empty? end
Whether the schema says null and nothing else: no ref, no variants.
# File lib/schematrix/generators/templates/type_composer.rb, line 177 def prefix_item_type(path, item, index) return untyped if item.type.include?(TYPE_OBJECT) && !item.map_shaped? slot = item.enum_class? ? "prefixItems/#{index}" : "prefixItems" type_of(path, slot, item) end
# File lib/schematrix/generators/templates/type_composer.rb, line 72 def property_type_of(path, name, schema) type_of(path, "properties/#{name}", schema, slot: true) end
The type of a property schema, addressed the way its class registers: under a properties container segment. Class names encode that segment as the Props module level, so a reference built from the property name alone would miss it and dangle.
# File lib/schematrix/generators/templates/type_composer.rb, line 26 def type_of(path, name, schema, slot: false) return untyped if schema.nil? # A null-only record is just nil, unless it carries more # terms: a ref to a target typed ["object", "null"] keeps # its class and renders nilable instead. A wrapped slot # types Maybe[NilClass], so absent and null stay distinct; # every other position keeps the nilable mode's bare # spelling. if null_only?(schema) return maybe_type(null_only_type) if slot && maybe_wrapped?(schema) return null_only_type end combined = bare_type(path, name, schema) return combined if combined == untyped maybe_absence_type(combined, schema, slot:) end
The full type of a schema: the bare type underneath, wrapped for absence wherever the property is optional. When the maybe option is on, the optional wrap is the Maybe type with the nilability kept inside, so a slot carries three states: Absent for a missing key, Present(nil) for a null, Present(value) for a value. Partial classes wrap every slot, required ones included, since absence is the partial’s whole content.
Source
# File lib/schematrix/generators/templates/type_composer.rb, line 205 def union(types) return untyped if types.empty? return untyped if types.include?(untyped) return types.first if types.size == 1 join_union(types) end
# File lib/schematrix/generators/templates/type_composer.rb, line 198 def variant_types(path, name, variants, keyword) variant_path = nested_locator(path, name) variants.each_with_index.map do |variant, index| bare_type(variant_path, "#{keyword}_#{index}", variant) end end
Computes the bare type of each variant branch under its positional path segment (anyOf_0, oneOf_1, …). Shared by both variant lists: the keyword only names the path segment.