Module: AsyncapiCable::Components::ReferenceClosure
- Defined in:
- lib/asyncapi_cable/components/reference_closure.rb
Overview
Expands a set of component classes to everything they $ref.
Scope alone is not a sufficient filter for a document. A cable message
can legitimately reference a component that is not cable-scoped — the
presence channels embed a rendered REST representation, so the honest
contract says "this is an Item" and points at the REST component. Include
the referrer without the referee and the document carries a pointer that
resolves to nothing: @asyncapi/parser rejects the whole document with
'#/components/schemas/X' does not exist.
So a document includes what its own scope selects, plus the transitive closure of what those components reference. Scope stays the entry point into the graph rather than a fence around it.
Constant Summary collapse
- SCHEMA_REF =
%r{\A#/components/schemas/(?<name>\w+)\z}
Class Method Summary collapse
- .ambiguity_message(name, referrer, candidates) ⇒ Object
-
.expand(classes, scope: nil, registry: OpenapiRuby::Components::Registry.instance.all_registered_classes) ⇒ Object
scopeis the document's own scope (a symbol, or several in priority order), consulted when the referring component cannot settle a name on its own. - .least_scoped(candidates) ⇒ Object
-
.narrow(candidates, scopes) ⇒ Object
Narrowing never empties the set: a scope no candidate carries tells us nothing, so it leaves the decision to the next rule.
- .referenced_names(node, found = []) ⇒ Object
-
.resolve(name, candidates, referrer:, prefer: []) ⇒ Object
A component_name is only unique within a scope: openapi-ruby documents routinely carry a richer admin variant of a public resource under the same name, and its own
to_openapi_hashnever meets the collision because it filters by scope before indexing by name.
Class Method Details
.ambiguity_message(name, referrer, candidates) ⇒ Object
116 117 118 119 120 |
# File 'lib/asyncapi_cable/components/reference_closure.rb', line 116 def (name, referrer, candidates) listed = candidates.map { |klass| "#{klass.name} #{klass._component_scopes.inspect}" }.join(", ") "Ambiguous $ref #/components/schemas/#{name} from #{referrer.name}: #{listed}. " \ "Give the intended component a scope the referrer shares, or name the variants distinctly." end |
.expand(classes, scope: nil, registry: OpenapiRuby::Components::Registry.instance.all_registered_classes) ⇒ Object
scope is the document's own scope (a symbol, or several in priority
order), consulted when the referring component cannot settle a name on
its own. The entry points' own scopes follow it: a document assembled
from :v1 components resolves its pointers the way the :v1 document
would, which is what makes the walk order-independent.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/asyncapi_cable/components/reference_closure.rb', line 28 def (classes, scope: nil, registry: OpenapiRuby::Components::Registry.instance.all_registered_classes) entry_points = classes.to_a candidates = registry.group_by(&:component_name) preferred = (Array(scope) + entry_points.flat_map(&:_component_scopes)).compact.uniq included = {} queue = entry_points.dup until queue.empty? klass = queue.shift name = klass.component_name next if included.key?(name) included[name] = klass referenced_names(klass._schema_definition).each do |referenced| next if included.key?(referenced) referee = resolve(referenced, candidates, referrer: klass, prefer: preferred) queue << referee if referee end end included.values end |
.least_scoped(candidates) ⇒ Object
111 112 113 114 |
# File 'lib/asyncapi_cable/components/reference_closure.rb', line 111 def least_scoped(candidates) fewest = candidates.map { |klass| klass._component_scopes.size }.min candidates.select { |klass| klass._component_scopes.size == fewest } end |
.narrow(candidates, scopes) ⇒ Object
Narrowing never empties the set: a scope no candidate carries tells us nothing, so it leaves the decision to the next rule.
103 104 105 106 107 108 109 |
# File 'lib/asyncapi_cable/components/reference_closure.rb', line 103 def narrow(candidates, scopes) wanted = scopes.compact return candidates if wanted.empty? matching = candidates.select { |klass| (klass._component_scopes & wanted).any? } matching.empty? ? candidates : matching end |
.referenced_names(node, found = []) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/asyncapi_cable/components/reference_closure.rb', line 83 def referenced_names(node, found = []) case node when Hash node.each do |key, value| match = (key.to_s == "$ref") && value.is_a?(String) && SCHEMA_REF.match(value) if match found << match[:name] else referenced_names(value, found) end end when Array node.each { |element| referenced_names(element, found) } end found.uniq end |
.resolve(name, candidates, referrer:, prefer: []) ⇒ Object
A component_name is only unique within a scope: openapi-ruby documents
routinely carry a richer admin variant of a public resource under the
same name, and its own to_openapi_hash never meets the collision
because it filters by scope before indexing by name. A closure walk has
no such filter, so it has to say which variant a pointer meant.
A $ref means what it means in the referring component's own document,
so a candidate sharing a scope with the referrer wins. A shared
component settles nothing on its own though — one carrying
[:v1, :admin] shares a scope with both variants of the name it
references — so the document's scopes are applied next, in priority
order, then openapi-ruby's specificity rule (scope-specific beats
multi-scope). Anything still undecided is a real ambiguity and says so
rather than picking by registration order.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/asyncapi_cable/components/reference_closure.rb', line 66 def resolve(name, candidates, referrer:, prefer: []) found = candidates[name] return nil if found.nil? || found.empty? return found.first if found.size == 1 narrowed = narrow(found, referrer._component_scopes) prefer.each do |scope| break if narrowed.size == 1 narrowed = narrow(narrowed, [scope]) end narrowed = least_scoped(narrowed) if narrowed.size > 1 return narrowed.first if narrowed.size == 1 raise Error, (name, referrer, narrowed) end |