asyncapi_cable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +201 -0
- data/Rakefile +3 -0
- data/app/controllers/asyncapi_cable/schemas_controller.rb +29 -0
- data/config/routes.rb +4 -0
- data/lib/asyncapi_cable/adapters/minitest.rb +53 -0
- data/lib/asyncapi_cable/adapters/rspec.rb +58 -0
- data/lib/asyncapi_cable/configuration.rb +22 -0
- data/lib/asyncapi_cable/core/document.rb +137 -0
- data/lib/asyncapi_cable/dsl/channel_context.rb +48 -0
- data/lib/asyncapi_cable/dsl/metadata_store.rb +46 -0
- data/lib/asyncapi_cable/dsl/operation_context.rb +32 -0
- data/lib/asyncapi_cable/engine.rb +18 -0
- data/lib/asyncapi_cable/generator/asyncapi_writer.rb +71 -0
- data/lib/asyncapi_cable/generator/declaration_loader.rb +97 -0
- data/lib/asyncapi_cable/generator/rake_task_support.rb +52 -0
- data/lib/asyncapi_cable/generator/runner.rb +27 -0
- data/lib/asyncapi_cable/minitest.rb +4 -0
- data/lib/asyncapi_cable/rake_tasks.rb +39 -0
- data/lib/asyncapi_cable/rspec.rb +4 -0
- data/lib/asyncapi_cable/runtime/channel_hook.rb +61 -0
- data/lib/asyncapi_cable/runtime/contract_registry.rb +129 -0
- data/lib/asyncapi_cable/runtime/payload_validator.rb +48 -0
- data/lib/asyncapi_cable/runtime/stream_matcher.rb +24 -0
- data/lib/asyncapi_cable/testing/assert_helpers.rb +137 -0
- data/lib/asyncapi_cable/version.rb +3 -0
- data/lib/asyncapi_cable.rb +47 -0
- data/lib/tasks/asyncapi_cable_tasks.rake +1 -0
- metadata +150 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module AsyncapiCable
|
|
4
|
+
module Testing
|
|
5
|
+
# Shared core of assert_asyncapi_broadcast. Adapters mix this in and
|
|
6
|
+
# supply two hooks: #asyncapi_declared_contexts (the channel contexts
|
|
7
|
+
# declared on the test class / example group) and #asyncapi_flunk
|
|
8
|
+
# (framework-native test failure).
|
|
9
|
+
#
|
|
10
|
+
# Usage errors (undeclared broadcast operation, unresolved template
|
|
11
|
+
# params, non-test cable adapter) raise AsyncapiCable::Error; only
|
|
12
|
+
# genuine test failures go through asyncapi_flunk.
|
|
13
|
+
module AssertHelpers
|
|
14
|
+
def assert_asyncapi_broadcast(params: {}, &trigger)
|
|
15
|
+
raise Error, "assert_asyncapi_broadcast requires a block that triggers the broadcast" unless trigger
|
|
16
|
+
|
|
17
|
+
context = asyncapi_find_broadcast_context!(asyncapi_declared_contexts, params)
|
|
18
|
+
stream = asyncapi_expand_stream!(context.stream_template, params)
|
|
19
|
+
payloads = asyncapi_capture_broadcasts(stream, &trigger)
|
|
20
|
+
|
|
21
|
+
if payloads.empty?
|
|
22
|
+
asyncapi_flunk("Expected at least one broadcast on #{stream.inspect}, but none was captured")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
payloads.each do |payload|
|
|
26
|
+
errors = asyncapi_broadcast_errors(context, payload)
|
|
27
|
+
next if errors.empty?
|
|
28
|
+
|
|
29
|
+
asyncapi_flunk(
|
|
30
|
+
"AsyncAPI broadcast validation failed for #{stream.inspect}:\n #{asyncapi_error_summary(errors)}"
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
payloads
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Declared message schemas may `$ref` sibling components (enums etc.),
|
|
38
|
+
# so the validation document carries every registry class sharing the
|
|
39
|
+
# messages' scopes — the same set AsyncapiWriter publishes for those
|
|
40
|
+
# scopes. Loader#load! eager-loads classes reachable only via `$ref`
|
|
41
|
+
# strings and is idempotent. Memoized per scope set: PayloadValidator
|
|
42
|
+
# caches one compiled schema per components object.
|
|
43
|
+
def self.components_for(message_classes)
|
|
44
|
+
scopes = message_classes.flat_map(&:_component_scopes).uniq.sort
|
|
45
|
+
@components ||= {}
|
|
46
|
+
@components[scopes] ||= begin
|
|
47
|
+
OpenapiRuby::Components::Loader.new.load!
|
|
48
|
+
schemas = OpenapiRuby::Components::Registry.instance.all_registered_classes.select { |klass|
|
|
49
|
+
(klass._component_scopes & scopes).any?
|
|
50
|
+
}.to_h { |klass| [klass.component_name, klass._schema_definition] }
|
|
51
|
+
{"schemas" => schemas}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def asyncapi_find_broadcast_context!(contexts, params)
|
|
58
|
+
candidates = contexts.select { |ctx| ctx.operations.any? { |op| op.kind == :broadcast } }
|
|
59
|
+
|
|
60
|
+
if candidates.empty?
|
|
61
|
+
raise Error, "No channel with a broadcast operation is declared in this test class"
|
|
62
|
+
end
|
|
63
|
+
return candidates.first if candidates.size == 1
|
|
64
|
+
|
|
65
|
+
keys = params.keys.map(&:to_s).sort
|
|
66
|
+
scoped = candidates.select { |ctx| asyncapi_template_params(ctx.stream_template).sort == keys }
|
|
67
|
+
return scoped.first if scoped.size == 1
|
|
68
|
+
|
|
69
|
+
raise Error, "Ambiguous channel for params #{params.keys.inspect}: " \
|
|
70
|
+
"#{candidates.size} declared channels have broadcast operations " \
|
|
71
|
+
"(#{candidates.map { |c| c.stream_template.inspect }.join(", ")})"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def asyncapi_expand_stream!(template, params)
|
|
75
|
+
missing = []
|
|
76
|
+
stream = template.gsub(/\{(\w+)\}/) do
|
|
77
|
+
name = ::Regexp.last_match(1)
|
|
78
|
+
value = params[name.to_sym] || params[name]
|
|
79
|
+
missing << name if value.nil?
|
|
80
|
+
value.to_s
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
unless missing.empty?
|
|
84
|
+
raise Error, "Missing params #{missing.inspect} to expand stream template #{template.inspect}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
stream
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def asyncapi_capture_broadcasts(stream)
|
|
91
|
+
pubsub = ::ActionCable.server.pubsub
|
|
92
|
+
unless pubsub.respond_to?(:broadcasts)
|
|
93
|
+
raise Error, "assert_asyncapi_broadcast requires the ActionCable test adapter " \
|
|
94
|
+
"(set `adapter: test` for the test environment in config/cable.yml)"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
seen = pubsub.broadcasts(stream).size
|
|
98
|
+
yield
|
|
99
|
+
pubsub.broadcasts(stream).drop(seen).map { |message| asyncapi_decode(message) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Validates against the *declared* message classes (the authoring
|
|
103
|
+
# side), not the committed YAML the runtime ContractRegistry reads —
|
|
104
|
+
# when a spec documents a new channel, the YAML doesn't exist yet.
|
|
105
|
+
# Messages are alternatives per AsyncAPI 3: pass if any validates
|
|
106
|
+
# clean, otherwise surface the closest match's errors.
|
|
107
|
+
def asyncapi_broadcast_errors(context, payload)
|
|
108
|
+
message_classes = context.operations
|
|
109
|
+
.select { |op| op.kind == :broadcast }
|
|
110
|
+
.flat_map(&:messages)
|
|
111
|
+
return [] if message_classes.empty?
|
|
112
|
+
|
|
113
|
+
components = AssertHelpers.components_for(message_classes)
|
|
114
|
+
results = message_classes.map do |klass|
|
|
115
|
+
Runtime::PayloadValidator.instance.validate(payload, klass.component_name, components)
|
|
116
|
+
end
|
|
117
|
+
return [] if results.any?(&:empty?)
|
|
118
|
+
|
|
119
|
+
results.min_by(&:size) || []
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def asyncapi_error_summary(errors)
|
|
123
|
+
errors.map { |e| e["error"] }.compact.uniq.join("\n ")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def asyncapi_template_params(template)
|
|
127
|
+
template.scan(/\{(\w+)\}/).flatten
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def asyncapi_decode(message)
|
|
131
|
+
message.is_a?(String) ? JSON.parse(message) : message
|
|
132
|
+
rescue JSON::ParserError
|
|
133
|
+
message
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "asyncapi_cable/version"
|
|
2
|
+
require "asyncapi_cable/configuration"
|
|
3
|
+
require "asyncapi_cable/dsl/metadata_store"
|
|
4
|
+
require "asyncapi_cable/dsl/operation_context"
|
|
5
|
+
require "asyncapi_cable/dsl/channel_context"
|
|
6
|
+
require "asyncapi_cable/core/document"
|
|
7
|
+
require "asyncapi_cable/generator/asyncapi_writer"
|
|
8
|
+
require "asyncapi_cable/runtime/stream_matcher"
|
|
9
|
+
require "asyncapi_cable/runtime/payload_validator"
|
|
10
|
+
require "asyncapi_cable/runtime/contract_registry"
|
|
11
|
+
require "asyncapi_cable/runtime/channel_hook"
|
|
12
|
+
require "asyncapi_cable/engine"
|
|
13
|
+
|
|
14
|
+
module AsyncapiCable
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def configuration
|
|
19
|
+
@configuration ||= Configuration.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def configure
|
|
23
|
+
yield configuration
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reset_configuration!
|
|
27
|
+
@configuration = Configuration.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# True when the current process was started by `asyncapi_cable:generate`
|
|
31
|
+
# (the rake task sets ASYNCAPI_CABLE_GENERATING=true in the subprocess).
|
|
32
|
+
#
|
|
33
|
+
# Such a run loads declaration files for their `channel` blocks and never
|
|
34
|
+
# executes them, so a host test helper can skip its test-time setup:
|
|
35
|
+
#
|
|
36
|
+
# unless AsyncapiCable.schema_generating?
|
|
37
|
+
# require "rails/test_help"
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# Cable generation also sets openapi-ruby's OPENAPI_RUBY_GENERATING, so a
|
|
41
|
+
# helper already guarding on `OpenapiRuby.schema_generating?` needs no
|
|
42
|
+
# second guard.
|
|
43
|
+
def schema_generating?
|
|
44
|
+
ENV["ASYNCAPI_CABLE_GENERATING"] == "true"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "asyncapi_cable/rake_tasks"
|
metadata
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: asyncapi_cable
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Fobizz
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-24 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: actioncable
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '7.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '7.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: json_schemer
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.3'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.3'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: openapi-ruby
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 4.0.3
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 4.0.3
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: railties
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '7.1'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '7.1'
|
|
83
|
+
description: |
|
|
84
|
+
AsyncAPI Cable adds an AsyncAPI 3 DSL on top of Rails ActionCable.
|
|
85
|
+
It generates an AsyncAPI document for your channels, sharing JSON
|
|
86
|
+
Schema components with OpenAPI tooling, and validates broadcast and
|
|
87
|
+
transmit payloads against declared message schemas at runtime.
|
|
88
|
+
email:
|
|
89
|
+
- dev@fobizz.com
|
|
90
|
+
executables: []
|
|
91
|
+
extensions: []
|
|
92
|
+
extra_rdoc_files: []
|
|
93
|
+
files:
|
|
94
|
+
- MIT-LICENSE
|
|
95
|
+
- README.md
|
|
96
|
+
- Rakefile
|
|
97
|
+
- app/controllers/asyncapi_cable/schemas_controller.rb
|
|
98
|
+
- config/routes.rb
|
|
99
|
+
- lib/asyncapi_cable.rb
|
|
100
|
+
- lib/asyncapi_cable/adapters/minitest.rb
|
|
101
|
+
- lib/asyncapi_cable/adapters/rspec.rb
|
|
102
|
+
- lib/asyncapi_cable/configuration.rb
|
|
103
|
+
- lib/asyncapi_cable/core/document.rb
|
|
104
|
+
- lib/asyncapi_cable/dsl/channel_context.rb
|
|
105
|
+
- lib/asyncapi_cable/dsl/metadata_store.rb
|
|
106
|
+
- lib/asyncapi_cable/dsl/operation_context.rb
|
|
107
|
+
- lib/asyncapi_cable/engine.rb
|
|
108
|
+
- lib/asyncapi_cable/generator/asyncapi_writer.rb
|
|
109
|
+
- lib/asyncapi_cable/generator/declaration_loader.rb
|
|
110
|
+
- lib/asyncapi_cable/generator/rake_task_support.rb
|
|
111
|
+
- lib/asyncapi_cable/generator/runner.rb
|
|
112
|
+
- lib/asyncapi_cable/minitest.rb
|
|
113
|
+
- lib/asyncapi_cable/rake_tasks.rb
|
|
114
|
+
- lib/asyncapi_cable/rspec.rb
|
|
115
|
+
- lib/asyncapi_cable/runtime/channel_hook.rb
|
|
116
|
+
- lib/asyncapi_cable/runtime/contract_registry.rb
|
|
117
|
+
- lib/asyncapi_cable/runtime/payload_validator.rb
|
|
118
|
+
- lib/asyncapi_cable/runtime/stream_matcher.rb
|
|
119
|
+
- lib/asyncapi_cable/testing/assert_helpers.rb
|
|
120
|
+
- lib/asyncapi_cable/version.rb
|
|
121
|
+
- lib/tasks/asyncapi_cable_tasks.rake
|
|
122
|
+
homepage: https://github.com/openapi-ruby/asyncapi-cable
|
|
123
|
+
licenses:
|
|
124
|
+
- MIT
|
|
125
|
+
metadata:
|
|
126
|
+
allowed_push_host: https://rubygems.org
|
|
127
|
+
homepage_uri: https://github.com/openapi-ruby/asyncapi-cable
|
|
128
|
+
source_code_uri: https://github.com/openapi-ruby/asyncapi-cable/tree/main/ruby
|
|
129
|
+
changelog_uri: https://github.com/openapi-ruby/asyncapi-cable/blob/main/ruby/CHANGELOG.md
|
|
130
|
+
rubygems_mfa_required: 'true'
|
|
131
|
+
post_install_message:
|
|
132
|
+
rdoc_options: []
|
|
133
|
+
require_paths:
|
|
134
|
+
- lib
|
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
136
|
+
requirements:
|
|
137
|
+
- - ">="
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: 3.3.0
|
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
requirements: []
|
|
146
|
+
rubygems_version: 3.5.22
|
|
147
|
+
signing_key:
|
|
148
|
+
specification_version: 4
|
|
149
|
+
summary: AsyncAPI 3 documentation and runtime validation for Rails ActionCable channels.
|
|
150
|
+
test_files: []
|