Module: Servus::Schema
- Defined in:
- lib/servus/schema.rb,
lib/servus/schema/ref.rb,
lib/servus/schema/path.rb,
lib/servus/schema/cache.rb,
lib/servus/schema/errors.rb,
lib/servus/schema/compiler.rb,
lib/servus/schema/declaration.rb
Overview
Registry of reusable JSON Schema fragments, and the entry point for compiling a schema that references them.
Servus services and events declare their contracts inline via the schema
DSL. That keeps a service's inputs and outputs visible in the file that
implements it. The cost of inline-only declaration is duplication: the same
amount or timestamp shape gets copy-pasted across every service that
touches it.
Registered fragments close that gap without giving up explicitness. A
fragment is registered under a key, and services reference into it with a
standard JSON Schema $ref. A service that references a shared type is
still explicitly declaring that type — it just names it once.
Lookups never return nil. An unregistered key raises UnknownKeyError at the point of use, because the alternative — silently skipping validation for a service that appears to declare a contract — is the worst failure mode this system has.
Defined Under Namespace
Modules: Declaration, Path Classes: Cache, CircularReferenceError, Compiler, DepthExceededError, Error, InvalidKeyError, InvalidRefError, Ref, RefNotFoundError, UnknownKeyError
Class Attribute Summary collapse
-
.cache ⇒ Servus::Schema::Cache
readonly
private
Memoized ref resolutions and the generation counter derived from them.
Class Method Summary collapse
-
.compile(schema, context: nil) ⇒ Hash?
Compiles a schema, replacing every
$refwith the fragment it names. -
.compile_all ⇒ Hash{String => Hash}
Compiles every registered fragment, resolving all +$ref+s.
-
.fetch(key, *path) ⇒ ActiveSupport::HashWithIndifferentAccess, Object
Returns a registered fragment, or a definition within one.
-
.generation ⇒ Integer
Monotonic counter bumped whenever the registry changes.
-
.keys ⇒ Array<String>
Registered keys, sorted.
-
.ref(key, *path) ⇒ Hash
Builds a
$refpointing at a registered fragment. -
.register(key, fragment) ⇒ ActiveSupport::HashWithIndifferentAccess
Registers a reusable schema fragment under
key. -
.reset! ⇒ void
Clears the registry.
-
.resolve(key, *path) ⇒ Hash, Object
Returns a fragment, or a definition within one, with all +$ref+s resolved.
-
.restore(snapshot) ⇒ void
private
Restores a snapshot taken by Schema.snapshot.
-
.snapshot ⇒ Hash
private
Captures the registry state so a test can restore it afterwards.
Class Attribute Details
.cache ⇒ Servus::Schema::Cache (readonly)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Memoized ref resolutions and the generation counter derived from them.
55 56 57 |
# File 'lib/servus/schema.rb', line 55 def cache @cache end |
Class Method Details
.compile(schema, context: nil) ⇒ Hash?
Compiles a schema, replacing every $ref with the fragment it names.
188 189 190 191 192 |
# File 'lib/servus/schema.rb', line 188 def compile(schema, context: nil) return nil if schema.nil? Compiler.new(context: context).compile(schema) end |
.compile_all ⇒ Hash{String => Hash}
Compiles every registered fragment, resolving all +$ref+s.
Returns a hash of key to compiled fragment, mirroring the registry's own shape so keys stay addressable and the result serializes straight to JSON. Useful for producing a single schema asset for an API description, a docs build, client codegen, or a CI freshness check.
156 157 158 |
# File 'lib/servus/schema.rb', line 156 def compile_all keys.to_h { |key| [key, compile(fetch(key), context: "schema fragment #{key.inspect}")] } end |
.fetch(key, *path) ⇒ ActiveSupport::HashWithIndifferentAccess, Object
Returns a registered fragment, or a definition within one.
Given no path, returns the whole fragment. Given path segments, walks
them as literal keys — the same addressing a $ref uses, so
fetch(key, *path) reads exactly what ref(key, *path) points at.
A missing path raises rather than returning nil. Reaching for the
fragment and calling dig would return nil on a typo, which is the
silent failure this registry exists to prevent.
Fragments are returned as authored, with any +$ref+s intact. Use compile to resolve them.
112 113 114 115 116 117 |
# File 'lib/servus/schema.rb', line 112 def fetch(key, *path) key = key.to_s fragment = @registry.fetch(key) { raise UnknownKeyError.for(key, available: @registry.keys) } Path.walk(fragment, key, path.map(&:to_s)) end |
.generation ⇒ Integer
Monotonic counter bumped whenever the registry changes.
Consumers memoize compiled schemas alongside the generation they were compiled under, and recompile when it moves. That makes registry updates propagate without any explicit dependency tracking.
64 |
# File 'lib/servus/schema.rb', line 64 def generation = cache.generation |
.keys ⇒ Array<String>
Returns registered keys, sorted.
161 162 163 |
# File 'lib/servus/schema.rb', line 161 def keys @registry.keys.sort end |
.ref(key, *path) ⇒ Hash
Builds a $ref pointing at a registered fragment.
Prefer this over hand-writing ref strings — it is typo-proof in the separator and prefix, which are the parts people get wrong.
177 178 179 |
# File 'lib/servus/schema.rb', line 177 def ref(key, *path) { '$ref' => "#/#{[key, *path].map(&:to_s).join('/')}" } end |
.register(key, fragment) ⇒ ActiveSupport::HashWithIndifferentAccess
Registers a reusable schema fragment under key.
Re-registering an equal value is a silent no-op, so calling this from a
Rails to_prepare block is safe. Re-registering a different value
replaces it, logs an override, and bumps generation — which
invalidates every compiled schema that referenced it.
81 82 83 84 85 86 87 88 |
# File 'lib/servus/schema.rb', line 81 def register(key, fragment) key = normalize_key(key) normalized = normalize_fragment(key, fragment) cache.invalidate! if store(key, normalized) normalized end |
.reset! ⇒ void
This method returns an undefined value.
Clears the registry. Intended for test suites.
197 198 199 |
# File 'lib/servus/schema.rb', line 197 def reset! restore({}.freeze) end |
.resolve(key, *path) ⇒ Hash, Object
Returns a fragment, or a definition within one, with all +$ref+s resolved.
The compiled counterpart to fetch: same addressing, but the result is self-contained and ready to validate against. This is usually what application code outside a service wants — a controller validating a request body, a serializer checking a response shape.
Results are memoized, so asking repeatedly for the same address is cheap.
138 139 140 141 142 |
# File 'lib/servus/schema.rb', line 138 def resolve(key, *path) pointer = ref(key, *path) compile(pointer, context: "schema #{pointer['$ref']}") end |
.restore(snapshot) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Restores a snapshot taken by snapshot.
214 215 216 217 |
# File 'lib/servus/schema.rb', line 214 def restore(snapshot) @mutex.synchronize { @registry = snapshot } cache.invalidate! end |
.snapshot ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Captures the registry state so a test can restore it afterwards.
205 206 207 |
# File 'lib/servus/schema.rb', line 205 def snapshot @registry end |