Class: AsyncapiCable::Generator::AsyncapiWriter

Inherits:
Object
  • Object
show all
Defined in:
lib/asyncapi_cable/generator/asyncapi_writer.rb

Class Method Summary collapse

Class Method Details

.build_document(schema_name, schema_config) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/asyncapi_cable/generator/asyncapi_writer.rb', line 21

def build_document(schema_name, schema_config)
  scope = (schema_config[:component_scope] || schema_config["component_scope"] || :cable).to_sym
  contexts = Dsl::MetadataStore.contexts_for(schema_name)
  components = load_cable_components(scope, declared: declared_messages(contexts))

  document = Core::Document.new(
    info: schema_config[:info] || schema_config["info"] || {},
    servers: schema_config[:servers] || schema_config["servers"] || {},
    cable_components: components
  )

  contexts.each do |context|
    document.add_channel(context)
  end

  document
end

.declared_messages(contexts) ⇒ Object

Every declared message becomes a components/messages entry pointing at a schema of the same name, so these are referenced by construction — whatever scope they carry. A contract that reuses an existing REST component as its payload declares one that the document's own scope does not select.



44
45
46
# File 'lib/asyncapi_cable/generator/asyncapi_writer.rb', line 44

def declared_messages(contexts)
  contexts.flat_map { |context| context.operations.flat_map(&:messages) }.uniq
end

.generate_all!(output_dir: nil, format: nil) ⇒ Object

Raises:



7
8
9
10
11
12
13
14
15
16
17
18
19
# File 'lib/asyncapi_cable/generator/asyncapi_writer.rb', line 7

def generate_all!(output_dir: nil, format: nil)
  configuration = AsyncapiCable.configuration
  output_dir ||= configuration.schema_output_dir
  format ||= configuration.schema_output_format

  raise Error, "AsyncapiCable.configuration.schemas is empty" if configuration.schemas.empty?

  configuration.schemas.map do |schema_name, schema_config|
    document = build_document(schema_name, schema_config)
    path = write(document, schema_name, output_dir: output_dir, format: format)
    [schema_name, path]
  end.to_h
end

.load_cable_components(scope, declared: []) ⇒ Object

Bypass OpenapiRuby::Components::Loader#to_openapi_hash and read raw schema definitions directly. The host's openapi-ruby is configured with camelize_keys = true which is correct for REST API docs but wrong for cable AsyncAPI docs whose payloads are the snake_case wire format BroadcastingConcern actually emits. The runtime PayloadValidator already does the same bypass for the same reason.

We still need the Loader's eager-load side effect though: component classes reachable only via $ref strings (e.g. an enum a message schema refs) aren't autoloaded by Ruby, so a raw registry scan would miss them. Loader#load! is idempotent.

Scope and the declared messages select the entry points; ReferenceClosure adds what those components reference, whatever scope the referee carries.



63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/asyncapi_cable/generator/asyncapi_writer.rb', line 63

def load_cable_components(scope, declared: [])
  OpenapiRuby::Components::Loader.new.load!

  scoped = OpenapiRuby::Components::Registry.instance.all_registered_classes.select do |klass|
    klass._component_scopes.include?(scope)
  end
  entry_points = (scoped + declared).uniq
  schemas = Components::ReferenceClosure.expand(entry_points, scope: scope)
    .each_with_object({}) do |klass, acc|
      acc[klass.component_name] = klass._schema_definition
    end
  {"schemas" => schemas}
end

.write(document, schema_name, output_dir:, format:) ⇒ Object



77
78
79
80
81
82
83
84
# File 'lib/asyncapi_cable/generator/asyncapi_writer.rb', line 77

def write(document, schema_name, output_dir:, format:)
  ext = (format.to_sym == :json) ? "json" : "yaml"
  path = File.join(output_dir, "#{schema_name}.#{ext}")
  FileUtils.mkdir_p(File.dirname(path))
  contents = (ext == "json") ? document.to_json : document.to_yaml
  File.write(path, contents)
  path
end