module Schematrix
Compiles JSON Schema documents and generates Ruby source and type signatures from them. The CLI (Schematrix::CLI) and the library entry point (Schematrix.generate) share this pipeline.
Constants
- BUILTIN_SCALAR_TYPES
-
The rendering of every JSON
Schemascalar type and of the standard formats, keyed by [type, format]; a nil format names the bare type. A format with no entry renders as its bare type, so custom annotation formats change nothing until the project maps them through a generator’sscalar_types:option, which merges over this table. - Body
-
The mergeable shape of an object schema: its own properties and its normalized additionalProperties schema. The fold algebra (allOf conjoins, anyOf/oneOf/then/else unions) composes these.
- GENERATORS
-
The generator registry the
CLIresolves-gnames against:plain_ruby,rbs,sorbet_ruby,rbi, the dsl_ prefixed builders of each, the partial_ prefixed partial classes of each, plusauxiliary_files, the run-once generator for auxiliary files. The generated Maybe module is not a registry entry: the pipeline writes it once per output directory when a generator runs with optionals: :maybe and no supplied maybe module. - ScalarType
-
How a [type, format] pair renders in each notation, and the module that converts its values on load and dump when the Ruby type is not the JSON one. A nil coercer means the value passes through.
- Schema
-
The compiled record for one schema node. This is the internal representation the generators render;
Schematrix.generatereturns these, keyed by locator.-
type: set of JSONSchematype names (the TYPE_ constants). -
ref: locator of the referenced object or enum schema, when the node resolved to a$refthe compiler kept; nil otherwise. -
properties: hash of original property name =>Schema. -
required: whether the node demands a value (a property the constructor demands); nil on records that say nothing about it. -
additional_properties:Schemaof the catch-all, Schema::Empty for the open default, nil when the shape is closed. -
items:Schemaof the array element. -
prefix_items: array of Schemas, one per tuple position. -
any_of/one_of: arrays of variant Schemas (value context only; object context merges its branches instead). -
intersection: true when the record conjoins (anallOfmerge); its terms intersect instead of union. -
default: the schema’sdefaultvalue. -
description,title,enum: captured annotations. The description becomes the generated class comment. -
enum_class: whether this record names a generated enum class. -
format: the schema’sformatannotation. Scalar slots read it against the scalar type table to render a formatted Ruby type and to find the coercer that converts on load and dump.
-
- TYPE_ARRAY
-
The JSON
Schematype names the compiler tracks, shared by the visitor and the notation hooks.TYPE_BOTTOMis not a schema type name: it marks the “false” schema (no valid instance) and maps to each notation’s bottom type. - TYPE_BOOLEAN
- TYPE_BOTTOM
-
Pseudo type marking the “false” schema (no valid instance). Not a JSON
Schematype name: templates map it to their bottom type. - TYPE_INTEGER
- TYPE_NULL
- TYPE_NUMBER
- TYPE_OBJECT
- TYPE_STRING
- VERSION
-
Version of the released gem.
Attributes
The pipeline logs through this object when one is set. nil by default, so library runs are silent. The CLI assigns a TTY::Logger; anything answering debug, info, warn and fatal works.
Public Class Methods
# File lib/schematrix.rb, line 182 def self.assert_distinct_targets(generators, objects) claimed = {} claimed_maybe = {} claim = lambda do |generator, path, locator| if claimed.key?(path) raise OutputCollisionError, "#{generator.class} writes #{locator} to #{path}, " \ "already claimed by #{claimed[path]}" end claimed[path] = locator end generators.each do |generator| if generator.class.run_once? generator.run_once_target_paths.each do |path| claim.call(generator, path, "auxiliary output") end else objects.each do |locator, node| next unless generator.class.enum_generator? == node.enum_class? claim.call(generator, generator.target_path(locator), locator) generator.validate_enum_members(locator, node) if node.enum_class? end next unless generator.maybe_needed? maybe_key = [ generator.output_dir, generator.class.maybe_flavor, generator.module_name, ] next if claimed_maybe.key?(maybe_key) claimed_maybe[maybe_key] = true Generators::Maybe .new( generator.output_dir, generator.module_name, flavor: generator.class.maybe_flavor, format: generator.format, command: generator.command, ) .run_once_target_paths .each { |path| claim.call(generator, path, "generated Maybe") } end end end
Checks that every generator writing every compiled locator lands on a distinct file, and that run-once generators’ auxiliary paths stay clear of everything else, and raises Schematrix::OutputCollisionError when two outputs share a path. Container provenance is part of every class name, so distinct locators almost always map to distinct files; this check catches what name folding cannot express (two input files with the same basename, property names that normalize to one identifier, two same-extension generators in one directory, an auxiliary file a class would fold onto) before any file is written.
Source
# File lib/schematrix.rb, line 130 def self.enum_mode(generators) modes = generators.filter_map(&:enums).uniq if modes.size > 1 raise ArgumentError, "conflicting enums modes: #{modes.inspect}" end modes.first || ( if generators.any? { |generator| generator.class.enum_generator? } :classes else :unions end ) end
# File lib/schematrix.rb, line 75 def self.generate( generators:, input_files:, closed_shapes: false, schema_dirs: [], max_compilation_work: CompilationBudget::DEFAULT_LIMIT ) support = generators .flat_map(&:support_generators) .uniq do |generator| [generator.class, generator.output_dir, generator.module_name] end generators += support mode = enum_mode(generators) enum_writers = generators.any? { |generator| generator.class.enum_generator? } if mode == :classes && !enum_writers logger&.warn "enums: :classes references enum constants; add a " \ "*_enums generator to write their class files" end store = DocumentStore.new(schema_dirs:) objects = Compiler.new( store:, closed_shapes:, enums_classes: mode == :classes || enum_writers, max_compilation_work:, ).compile(input_files) assert_distinct_targets(generators, objects) per_locator, run_once = generators.partition { |g| !g.class.run_once? } objects.each do |locator, node| logger&.info "Writing output for #{locator}" per_locator.each do |generator| next unless generator.class.enum_generator? == node.enum_class? generator.write(locator, node, enums: mode) end end run_once.each do |generator| logger&.info "Writing auxiliary output for #{generator.class}" generator.write_run_once(objects) end maybe_writers(objects, per_locator).each do |generator| logger&.info "Writing generated Maybe for #{generator.module_name} " \ "in #{generator.output_dir}" generator.write_run_once(objects) end objects end
Compiles every schema reachable from input_files and writes one file per compiled object schema per generator, plus each run-once generator’s auxiliary files.
-
generators: generator instances, one per output format. Each per-locator generator receives awrite(locator, node, enums:)call per compiled class; a run-once generator (one answeringrun_once?) receives a singlewrite_run_once(objects)call with the whole compiled record set. -
input_files: paths to JSON or YAML schema documents. Each is a compilation root;$refedges reachable from it are followed. -
closed_shapes: when true, an absentadditionalPropertiesbehaves asfalse(closed classes, no catch-all). The spec default is the open catch-all. -
schema_dirs: directories that remotehttp(s)schema URIs resolve through, in search order. Nothing is ever fetched over the network. -
max_compilation_work: positive cumulative work limit across all roots and references. Exceeding it raisesCompilationLimitErrorbefore any output is written. See docs/limits.md for scope.
Returns the compiled records as a hash keyed by schema locator (a URI string such as file:///path/person.schema.json#/properties/address), with Schematrix::Schema values.
Raises Schematrix::InvalidSchemaError when a document cannot be a schema, Schematrix::UnsupportedDialectError when a $schema header names a dialect other than draft 2020-12, Schematrix::UnresolvableLocatorError when a reference cannot be resolved, Schematrix::OutputCollisionError when two outputs would share one file, and Schematrix::CoercionHelperCollisionError when a property name would override a generated coercion helper. Output collisions are detected before the first write, so such failures leave no partial output.
# File lib/schematrix.rb, line 151 def self.maybe_writers(_objects, generators) writers = {} generators.each do |generator| next unless generator.maybe_needed? key = [ generator.output_dir, generator.class.maybe_flavor, generator.module_name, ] writers[key] ||= Generators::Maybe.new( generator.output_dir, generator.module_name, flavor: generator.class.maybe_flavor, format: generator.format, command: generator.command, ) end writers.values end
One generated Maybe per output directory, flavor, and module: generators sharing all three share one file; anything less (two flavors, or two modules in one directory) collides in assert_distinct_targets, because one maybe.rb declares one namespace and every other module’s Maybe reference would dangle.