a2a-ruby 1.0.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/.rspec +3 -0
- data/.rubocop.yml +137 -0
- data/.simplecov +46 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/CONTRIBUTING.md +165 -0
- data/Gemfile +43 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING_CHECKLIST.md +214 -0
- data/README.md +171 -0
- data/Rakefile +165 -0
- data/docs/agent_execution.md +309 -0
- data/docs/api_reference.md +792 -0
- data/docs/configuration.md +780 -0
- data/docs/events.md +475 -0
- data/docs/getting_started.md +668 -0
- data/docs/integration.md +262 -0
- data/docs/server_apps.md +621 -0
- data/docs/troubleshooting.md +765 -0
- data/lib/a2a/client/api_methods.rb +263 -0
- data/lib/a2a/client/auth/api_key.rb +161 -0
- data/lib/a2a/client/auth/interceptor.rb +288 -0
- data/lib/a2a/client/auth/jwt.rb +189 -0
- data/lib/a2a/client/auth/oauth2.rb +146 -0
- data/lib/a2a/client/auth.rb +137 -0
- data/lib/a2a/client/base.rb +316 -0
- data/lib/a2a/client/config.rb +210 -0
- data/lib/a2a/client/connection_pool.rb +233 -0
- data/lib/a2a/client/http_client.rb +524 -0
- data/lib/a2a/client/json_rpc_handler.rb +136 -0
- data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
- data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
- data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
- data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
- data/lib/a2a/client/middleware.rb +116 -0
- data/lib/a2a/client/performance_tracker.rb +60 -0
- data/lib/a2a/configuration/defaults.rb +34 -0
- data/lib/a2a/configuration/environment_loader.rb +76 -0
- data/lib/a2a/configuration/file_loader.rb +115 -0
- data/lib/a2a/configuration/inheritance.rb +101 -0
- data/lib/a2a/configuration/validator.rb +180 -0
- data/lib/a2a/configuration.rb +201 -0
- data/lib/a2a/errors.rb +291 -0
- data/lib/a2a/modules.rb +50 -0
- data/lib/a2a/monitoring/alerting.rb +490 -0
- data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
- data/lib/a2a/monitoring/health_endpoints.rb +204 -0
- data/lib/a2a/monitoring/metrics_collector.rb +438 -0
- data/lib/a2a/monitoring.rb +463 -0
- data/lib/a2a/plugin.rb +358 -0
- data/lib/a2a/plugin_manager.rb +159 -0
- data/lib/a2a/plugins/example_auth.rb +81 -0
- data/lib/a2a/plugins/example_middleware.rb +118 -0
- data/lib/a2a/plugins/example_transport.rb +76 -0
- data/lib/a2a/protocol/agent_card.rb +8 -0
- data/lib/a2a/protocol/agent_card_server.rb +584 -0
- data/lib/a2a/protocol/capability.rb +496 -0
- data/lib/a2a/protocol/json_rpc.rb +254 -0
- data/lib/a2a/protocol/message.rb +8 -0
- data/lib/a2a/protocol/task.rb +8 -0
- data/lib/a2a/rails/a2a_controller.rb +258 -0
- data/lib/a2a/rails/controller_helpers.rb +499 -0
- data/lib/a2a/rails/engine.rb +167 -0
- data/lib/a2a/rails/generators/agent_generator.rb +311 -0
- data/lib/a2a/rails/generators/install_generator.rb +209 -0
- data/lib/a2a/rails/generators/migration_generator.rb +232 -0
- data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
- data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
- data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
- data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
- data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
- data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
- data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
- data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
- data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
- data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
- data/lib/a2a/rails/tasks/a2a.rake +228 -0
- data/lib/a2a/server/a2a_methods.rb +520 -0
- data/lib/a2a/server/agent.rb +537 -0
- data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
- data/lib/a2a/server/agent_execution/request_context.rb +219 -0
- data/lib/a2a/server/apps/rack_app.rb +311 -0
- data/lib/a2a/server/apps/sinatra_app.rb +261 -0
- data/lib/a2a/server/default_request_handler.rb +350 -0
- data/lib/a2a/server/events/event_consumer.rb +116 -0
- data/lib/a2a/server/events/event_queue.rb +226 -0
- data/lib/a2a/server/example_agent.rb +248 -0
- data/lib/a2a/server/handler.rb +281 -0
- data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
- data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
- data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
- data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
- data/lib/a2a/server/middleware.rb +213 -0
- data/lib/a2a/server/push_notification_manager.rb +327 -0
- data/lib/a2a/server/request_handler.rb +136 -0
- data/lib/a2a/server/storage/base.rb +141 -0
- data/lib/a2a/server/storage/database.rb +266 -0
- data/lib/a2a/server/storage/memory.rb +274 -0
- data/lib/a2a/server/storage/redis.rb +320 -0
- data/lib/a2a/server/storage.rb +38 -0
- data/lib/a2a/server/task_manager.rb +534 -0
- data/lib/a2a/transport/grpc.rb +481 -0
- data/lib/a2a/transport/http.rb +415 -0
- data/lib/a2a/transport/sse.rb +499 -0
- data/lib/a2a/types/agent_card.rb +540 -0
- data/lib/a2a/types/artifact.rb +99 -0
- data/lib/a2a/types/base_model.rb +223 -0
- data/lib/a2a/types/events.rb +117 -0
- data/lib/a2a/types/message.rb +106 -0
- data/lib/a2a/types/part.rb +288 -0
- data/lib/a2a/types/push_notification.rb +139 -0
- data/lib/a2a/types/security.rb +167 -0
- data/lib/a2a/types/task.rb +154 -0
- data/lib/a2a/types.rb +88 -0
- data/lib/a2a/utils/helpers.rb +245 -0
- data/lib/a2a/utils/message_buffer.rb +278 -0
- data/lib/a2a/utils/performance.rb +247 -0
- data/lib/a2a/utils/rails_detection.rb +97 -0
- data/lib/a2a/utils/structured_logger.rb +306 -0
- data/lib/a2a/utils/time_helpers.rb +167 -0
- data/lib/a2a/utils/validation.rb +8 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a-rails.rb +58 -0
- data/lib/a2a.rb +198 -0
- metadata +437 -0
@@ -0,0 +1,496 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module A2A
|
4
|
+
module Protocol
|
5
|
+
##
|
6
|
+
# Represents a capability definition for A2A methods
|
7
|
+
#
|
8
|
+
# Capabilities define the methods that an agent can perform,
|
9
|
+
# including input/output schemas, validation rules, and metadata.
|
10
|
+
#
|
11
|
+
# @example Defining a capability
|
12
|
+
# capability = A2A::Protocol::Capability.new(
|
13
|
+
# name: "analyze_text",
|
14
|
+
# description: "Analyze text content for sentiment and topics",
|
15
|
+
# method: "text/analyze",
|
16
|
+
# input_schema: {
|
17
|
+
# type: "object",
|
18
|
+
# properties: {
|
19
|
+
# text: { type: "string" },
|
20
|
+
# options: { type: "object" }
|
21
|
+
# },
|
22
|
+
# required: ["text"]
|
23
|
+
# },
|
24
|
+
# output_schema: {
|
25
|
+
# type: "object",
|
26
|
+
# properties: {
|
27
|
+
# sentiment: { type: "string" },
|
28
|
+
# topics: { type: "array", items: { type: "string" } }
|
29
|
+
# }
|
30
|
+
# }
|
31
|
+
# )
|
32
|
+
#
|
33
|
+
class Capability
|
34
|
+
attr_reader :name, :description, :method, :input_schema, :output_schema,
|
35
|
+
:examples, :tags, :security_requirements, :metadata,
|
36
|
+
:streaming_supported, :async_supported
|
37
|
+
|
38
|
+
##
|
39
|
+
# Initialize a new capability
|
40
|
+
#
|
41
|
+
# @param name [String] Capability name (required)
|
42
|
+
# @param description [String] Capability description (required)
|
43
|
+
# @param method [String] A2A method name (required)
|
44
|
+
# @param input_schema [Hash, nil] JSON Schema for input validation
|
45
|
+
# @param output_schema [Hash, nil] JSON Schema for output validation
|
46
|
+
# @param examples [Array<Hash>, nil] Usage examples
|
47
|
+
# @param tags [Array<String>, nil] Capability tags
|
48
|
+
# @param security_requirements [Array<String>, nil] Required security schemes
|
49
|
+
# @param metadata [Hash, nil] Additional metadata
|
50
|
+
# @param streaming_supported [Boolean] Whether streaming is supported
|
51
|
+
# @param async_supported [Boolean] Whether async execution is supported
|
52
|
+
def initialize(name:, description:, method:, input_schema: nil, output_schema: nil,
|
53
|
+
examples: nil, tags: nil, security_requirements: nil, metadata: nil,
|
54
|
+
streaming_supported: false, async_supported: false)
|
55
|
+
@name = name
|
56
|
+
@description = description
|
57
|
+
@method = method
|
58
|
+
@input_schema = input_schema
|
59
|
+
@output_schema = output_schema
|
60
|
+
@examples = examples
|
61
|
+
@tags = tags
|
62
|
+
@security_requirements = security_requirements
|
63
|
+
@metadata = metadata
|
64
|
+
@streaming_supported = streaming_supported
|
65
|
+
@async_supported = async_supported
|
66
|
+
|
67
|
+
validate!
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Validate input data against the input schema
|
72
|
+
#
|
73
|
+
# @param input [Hash] The input data to validate
|
74
|
+
# @return [Boolean] True if valid
|
75
|
+
# @raise [ArgumentError] If validation fails
|
76
|
+
def validate_input(input)
|
77
|
+
return true if @input_schema.nil?
|
78
|
+
|
79
|
+
validate_against_schema(input, @input_schema, "input")
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Validate output data against the output schema
|
84
|
+
#
|
85
|
+
# @param output [Hash] The output data to validate
|
86
|
+
# @return [Boolean] True if valid
|
87
|
+
# @raise [ArgumentError] If validation fails
|
88
|
+
def validate_output(output)
|
89
|
+
return true if @output_schema.nil?
|
90
|
+
|
91
|
+
validate_against_schema(output, @output_schema, "output")
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Check if the capability supports streaming
|
96
|
+
#
|
97
|
+
# @return [Boolean] True if streaming is supported
|
98
|
+
def streaming?
|
99
|
+
@streaming_supported == true
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Check if the capability supports async execution
|
104
|
+
#
|
105
|
+
# @return [Boolean] True if async is supported
|
106
|
+
def async?
|
107
|
+
@async_supported == true
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Check if the capability has a specific tag
|
112
|
+
#
|
113
|
+
# @param tag [String] The tag to check
|
114
|
+
# @return [Boolean] True if the tag is present
|
115
|
+
def has_tag?(tag)
|
116
|
+
return false if @tags.nil?
|
117
|
+
|
118
|
+
@tags.include?(tag)
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Check if the capability requires a specific security scheme
|
123
|
+
#
|
124
|
+
# @param scheme [String] The security scheme to check
|
125
|
+
# @return [Boolean] True if the scheme is required
|
126
|
+
def requires_security?(scheme)
|
127
|
+
return false if @security_requirements.nil?
|
128
|
+
|
129
|
+
@security_requirements.include?(scheme)
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# Convert to hash representation
|
134
|
+
#
|
135
|
+
# @return [Hash] The capability as a hash
|
136
|
+
def to_h
|
137
|
+
{
|
138
|
+
name: @name,
|
139
|
+
description: @description,
|
140
|
+
method: @method,
|
141
|
+
input_schema: @input_schema,
|
142
|
+
output_schema: @output_schema,
|
143
|
+
examples: @examples,
|
144
|
+
tags: @tags,
|
145
|
+
security_requirements: @security_requirements,
|
146
|
+
metadata: @metadata,
|
147
|
+
streaming_supported: @streaming_supported,
|
148
|
+
async_supported: @async_supported
|
149
|
+
}.compact
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Create from hash representation
|
154
|
+
#
|
155
|
+
# @param hash [Hash] The hash to create from
|
156
|
+
# @return [Capability] The new capability instance
|
157
|
+
def self.from_h(hash)
|
158
|
+
return nil if hash.nil?
|
159
|
+
|
160
|
+
# Convert string keys to symbols
|
161
|
+
normalized_hash = {}
|
162
|
+
hash.each do |key, value|
|
163
|
+
snake_key = key.to_s.gsub(/([A-Z])/, '_\1').downcase.to_sym
|
164
|
+
normalized_hash[snake_key] = value
|
165
|
+
end
|
166
|
+
|
167
|
+
new(**normalized_hash)
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def validate!
|
173
|
+
if @name.nil? || (respond_to?(:empty?) && empty?) || (is_a?(String) && strip.empty?)
|
174
|
+
raise ArgumentError,
|
175
|
+
"name is required"
|
176
|
+
end
|
177
|
+
if @description.nil? || (respond_to?(:empty?) && empty?) || (is_a?(String) && strip.empty?)
|
178
|
+
raise ArgumentError,
|
179
|
+
"description is required"
|
180
|
+
end
|
181
|
+
if @method.nil? || (respond_to?(:empty?) && empty?) || (is_a?(String) && strip.empty?)
|
182
|
+
raise ArgumentError,
|
183
|
+
"method is required"
|
184
|
+
end
|
185
|
+
|
186
|
+
raise ArgumentError, "name must be a String" unless @name.is_a?(String)
|
187
|
+
raise ArgumentError, "description must be a String" unless @description.is_a?(String)
|
188
|
+
raise ArgumentError, "method must be a String" unless @method.is_a?(String)
|
189
|
+
|
190
|
+
validate_schema(@input_schema, "input_schema") if @input_schema
|
191
|
+
validate_schema(@output_schema, "output_schema") if @output_schema
|
192
|
+
validate_examples if @examples
|
193
|
+
validate_array_of_strings(@tags, "tags") if @tags
|
194
|
+
validate_array_of_strings(@security_requirements, "security_requirements") if @security_requirements
|
195
|
+
validate_hash(@metadata, "metadata") if @metadata
|
196
|
+
validate_boolean(@streaming_supported, "streaming_supported")
|
197
|
+
validate_boolean(@async_supported, "async_supported")
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# Validate that a value is a boolean
|
202
|
+
def validate_boolean(value, field_name)
|
203
|
+
return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
204
|
+
|
205
|
+
raise ArgumentError, "#{field_name} must be a Boolean"
|
206
|
+
end
|
207
|
+
|
208
|
+
##
|
209
|
+
# Validate that a value is a hash
|
210
|
+
def validate_hash(value, field_name)
|
211
|
+
return if value.is_a?(Hash)
|
212
|
+
|
213
|
+
raise ArgumentError, "#{field_name} must be a Hash"
|
214
|
+
end
|
215
|
+
|
216
|
+
##
|
217
|
+
# Validate that a value is an array of strings
|
218
|
+
def validate_array_of_strings(value, field_name)
|
219
|
+
raise ArgumentError, "#{field_name} must be an Array" unless value.is_a?(Array)
|
220
|
+
|
221
|
+
value.each_with_index do |item, index|
|
222
|
+
raise ArgumentError, "#{field_name}[#{index}] must be a String" unless item.is_a?(String)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
##
|
227
|
+
# Validate that a schema is a valid JSON Schema
|
228
|
+
def validate_schema(schema, field_name)
|
229
|
+
raise ArgumentError, "#{field_name} must be a Hash" unless schema.is_a?(Hash)
|
230
|
+
|
231
|
+
# Basic JSON Schema validation - check for type field
|
232
|
+
return if schema.key?(:type) || schema.key?("type")
|
233
|
+
|
234
|
+
raise ArgumentError, "#{field_name} must have a 'type' field"
|
235
|
+
end
|
236
|
+
|
237
|
+
##
|
238
|
+
# Validate examples structure
|
239
|
+
def validate_examples
|
240
|
+
raise ArgumentError, "examples must be an Array" unless @examples.is_a?(Array)
|
241
|
+
|
242
|
+
@examples.each_with_index do |example, index|
|
243
|
+
raise ArgumentError, "examples[#{index}] must be a Hash" unless example.is_a?(Hash)
|
244
|
+
|
245
|
+
# Examples should have at least input or description
|
246
|
+
unless example.key?(:input) || example.key?("input") ||
|
247
|
+
example.key?(:description) || example.key?("description")
|
248
|
+
raise ArgumentError, "examples[#{index}] must have 'input' or 'description'"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
##
|
254
|
+
# Validate data against a JSON Schema (basic implementation)
|
255
|
+
#
|
256
|
+
# @param data [Object] The data to validate
|
257
|
+
# @param schema [Hash] The JSON Schema
|
258
|
+
# @param context [String] Context for error messages
|
259
|
+
def validate_against_schema(data, schema, context)
|
260
|
+
type = schema[:type] || schema["type"]
|
261
|
+
|
262
|
+
case type
|
263
|
+
when "object"
|
264
|
+
validate_object(data, schema, context)
|
265
|
+
when "array"
|
266
|
+
validate_array(data, schema, context)
|
267
|
+
when "string"
|
268
|
+
validate_string(data, context)
|
269
|
+
when "number", "integer"
|
270
|
+
validate_number(data, context)
|
271
|
+
when "boolean"
|
272
|
+
validate_boolean_value(data, context)
|
273
|
+
else
|
274
|
+
# Allow unknown types for extensibility
|
275
|
+
true
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
##
|
280
|
+
# Validate object type
|
281
|
+
def validate_object(data, schema, context)
|
282
|
+
raise ArgumentError, "#{context} must be an object" unless data.is_a?(Hash)
|
283
|
+
|
284
|
+
# Check required properties
|
285
|
+
required = schema[:required] || schema["required"] || []
|
286
|
+
required.each do |prop|
|
287
|
+
raise ArgumentError, "#{context} missing required property: #{prop}" unless data.key?(prop) || data.key?(prop.to_sym)
|
288
|
+
end
|
289
|
+
|
290
|
+
# Validate properties if schema defines them
|
291
|
+
properties = schema[:properties] || schema["properties"]
|
292
|
+
properties&.each do |prop_name, prop_schema|
|
293
|
+
prop_value = data[prop_name] || data[prop_name.to_sym]
|
294
|
+
validate_against_schema(prop_value, prop_schema, "#{context}.#{prop_name}") if prop_value
|
295
|
+
end
|
296
|
+
|
297
|
+
true
|
298
|
+
end
|
299
|
+
|
300
|
+
##
|
301
|
+
# Validate array type
|
302
|
+
def validate_array(data, schema, context)
|
303
|
+
raise ArgumentError, "#{context} must be an array" unless data.is_a?(Array)
|
304
|
+
|
305
|
+
# Validate items if schema defines them
|
306
|
+
items_schema = schema[:items] || schema["items"]
|
307
|
+
if items_schema
|
308
|
+
data.each_with_index do |item, index|
|
309
|
+
validate_against_schema(item, items_schema, "#{context}[#{index}]")
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
true
|
314
|
+
end
|
315
|
+
|
316
|
+
##
|
317
|
+
# Validate string type
|
318
|
+
def validate_string(data, context)
|
319
|
+
raise ArgumentError, "#{context} must be a string" unless data.is_a?(String)
|
320
|
+
|
321
|
+
true
|
322
|
+
end
|
323
|
+
|
324
|
+
##
|
325
|
+
# Validate number type
|
326
|
+
def validate_number(data, context)
|
327
|
+
raise ArgumentError, "#{context} must be a number" unless data.is_a?(Numeric)
|
328
|
+
|
329
|
+
true
|
330
|
+
end
|
331
|
+
|
332
|
+
##
|
333
|
+
# Validate boolean type
|
334
|
+
def validate_boolean_value(data, context)
|
335
|
+
raise ArgumentError, "#{context} must be a boolean" unless data.is_a?(TrueClass) || data.is_a?(FalseClass)
|
336
|
+
|
337
|
+
true
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
##
|
342
|
+
# Manages a registry of capabilities
|
343
|
+
#
|
344
|
+
# The capability registry allows for registration, discovery, and
|
345
|
+
# dynamic updates of agent capabilities.
|
346
|
+
#
|
347
|
+
class CapabilityRegistry
|
348
|
+
def initialize
|
349
|
+
@capabilities = {}
|
350
|
+
@listeners = []
|
351
|
+
end
|
352
|
+
|
353
|
+
##
|
354
|
+
# Register a capability
|
355
|
+
#
|
356
|
+
# @param capability [Capability] The capability to register
|
357
|
+
# @return [Capability] The registered capability
|
358
|
+
def register(capability)
|
359
|
+
raise ArgumentError, "capability must be a Capability instance" unless capability.is_a?(Capability)
|
360
|
+
|
361
|
+
@capabilities[capability.name] = capability
|
362
|
+
notify_listeners(:registered, capability)
|
363
|
+
capability
|
364
|
+
end
|
365
|
+
|
366
|
+
##
|
367
|
+
# Unregister a capability
|
368
|
+
#
|
369
|
+
# @param name [String] The capability name to unregister
|
370
|
+
# @return [Capability, nil] The unregistered capability or nil if not found
|
371
|
+
def unregister(name)
|
372
|
+
capability = @capabilities.delete(name)
|
373
|
+
notify_listeners(:unregistered, capability) if capability
|
374
|
+
capability
|
375
|
+
end
|
376
|
+
|
377
|
+
##
|
378
|
+
# Get a capability by name
|
379
|
+
#
|
380
|
+
# @param name [String] The capability name
|
381
|
+
# @return [Capability, nil] The capability or nil if not found
|
382
|
+
def get(name)
|
383
|
+
@capabilities[name]
|
384
|
+
end
|
385
|
+
|
386
|
+
##
|
387
|
+
# Get all registered capabilities
|
388
|
+
#
|
389
|
+
# @return [Array<Capability>] All capabilities
|
390
|
+
def all
|
391
|
+
@capabilities.values
|
392
|
+
end
|
393
|
+
|
394
|
+
##
|
395
|
+
# Find capabilities by tag
|
396
|
+
#
|
397
|
+
# @param tag [String] The tag to search for
|
398
|
+
# @return [Array<Capability>] Capabilities with the tag
|
399
|
+
def find_by_tag(tag)
|
400
|
+
@capabilities.values.select { |cap| cap.has_tag?(tag) }
|
401
|
+
end
|
402
|
+
|
403
|
+
##
|
404
|
+
# Find capabilities by method pattern
|
405
|
+
#
|
406
|
+
# @param pattern [String, Regexp] The method pattern to match
|
407
|
+
# @return [Array<Capability>] Matching capabilities
|
408
|
+
def find_by_method(pattern)
|
409
|
+
if pattern.is_a?(String)
|
410
|
+
@capabilities.values.select { |cap| cap.method == pattern }
|
411
|
+
elsif pattern.is_a?(Regexp)
|
412
|
+
@capabilities.values.select { |cap| cap.method.match?(pattern) }
|
413
|
+
else
|
414
|
+
raise ArgumentError, "pattern must be a String or Regexp"
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
##
|
419
|
+
# Find capabilities requiring specific security
|
420
|
+
#
|
421
|
+
# @param scheme [String] The security scheme
|
422
|
+
# @return [Array<Capability>] Capabilities requiring the scheme
|
423
|
+
def find_by_security(scheme)
|
424
|
+
@capabilities.values.select { |cap| cap.requires_security?(scheme) }
|
425
|
+
end
|
426
|
+
|
427
|
+
##
|
428
|
+
# Check if a capability is registered
|
429
|
+
#
|
430
|
+
# @param name [String] The capability name
|
431
|
+
# @return [Boolean] True if registered
|
432
|
+
def registered?(name)
|
433
|
+
@capabilities.key?(name)
|
434
|
+
end
|
435
|
+
|
436
|
+
##
|
437
|
+
# Get the number of registered capabilities
|
438
|
+
#
|
439
|
+
# @return [Integer] The count
|
440
|
+
def count
|
441
|
+
@capabilities.size
|
442
|
+
end
|
443
|
+
|
444
|
+
##
|
445
|
+
# Clear all capabilities
|
446
|
+
def clear
|
447
|
+
old_capabilities = @capabilities.values
|
448
|
+
@capabilities.clear
|
449
|
+
old_capabilities.each { |cap| notify_listeners(:unregistered, cap) }
|
450
|
+
end
|
451
|
+
|
452
|
+
##
|
453
|
+
# Add a listener for capability changes
|
454
|
+
#
|
455
|
+
# @param listener [Proc] The listener proc that receives (event, capability)
|
456
|
+
def add_listener(&listener)
|
457
|
+
@listeners << listener
|
458
|
+
end
|
459
|
+
|
460
|
+
##
|
461
|
+
# Remove a listener
|
462
|
+
#
|
463
|
+
# @param listener [Proc] The listener to remove
|
464
|
+
def remove_listener(listener)
|
465
|
+
@listeners.delete(listener)
|
466
|
+
end
|
467
|
+
|
468
|
+
##
|
469
|
+
# Convert registry to hash representation
|
470
|
+
#
|
471
|
+
# @return [Hash] The registry as a hash
|
472
|
+
def to_h
|
473
|
+
{
|
474
|
+
capabilities: @capabilities.transform_values(&:to_h),
|
475
|
+
count: count
|
476
|
+
}
|
477
|
+
end
|
478
|
+
|
479
|
+
private
|
480
|
+
|
481
|
+
##
|
482
|
+
# Notify all listeners of a capability event
|
483
|
+
#
|
484
|
+
# @param event [Symbol] The event type (:registered, :unregistered)
|
485
|
+
# @param capability [Capability] The capability involved
|
486
|
+
def notify_listeners(event, capability)
|
487
|
+
@listeners.each do |listener|
|
488
|
+
listener.call(event, capability)
|
489
|
+
rescue StandardError => e
|
490
|
+
# Log error but don't let listener errors break the registry
|
491
|
+
warn "Capability registry listener error: #{e.message}"
|
492
|
+
end
|
493
|
+
end
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|