rage-rb 1.10.0 → 1.11.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.
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "yaml"
5
+
6
+ if !defined?(Prism)
7
+ fail <<~ERR
8
+
9
+ rage-rb depends on Prism to build OpenAPI specifications. Add the following line to your Gemfile:
10
+ gem "prism"
11
+
12
+ ERR
13
+ end
14
+
15
+ module Rage::OpenAPI
16
+ # Create a new OpenAPI application.
17
+ #
18
+ # @param namespace [String, Module] limit the parser to a specific namespace
19
+ # @example
20
+ # map "/publicapi" do
21
+ # run Rage.openapi.application
22
+ # end
23
+ # @example
24
+ # map "/publicapi/v1" do
25
+ # run Rage.openapi.application(namespace: "Api::V1")
26
+ # end
27
+ #
28
+ # map "/publicapi/v2" do
29
+ # run Rage.openapi.application(namespace: "Api::V2")
30
+ # end
31
+ def self.application(namespace: nil)
32
+ html_app = ->(env) do
33
+ __data_cache[[:page, namespace]] ||= begin
34
+ scheme, host, path = env["rack.url_scheme"], env["HTTP_HOST"], env["SCRIPT_NAME"]
35
+ spec_url = "#{scheme}://#{host}#{path}/json"
36
+ page = ERB.new(File.read("#{__dir__}/index.html.erb")).result(binding)
37
+
38
+ [200, { "Content-Type" => "text/html; charset=UTF-8" }, [page]]
39
+ end
40
+ end
41
+
42
+ json_app = ->(env) do
43
+ spec = (__data_cache[[:spec, namespace]] ||= build(namespace:).to_json)
44
+ [200, { "Content-Type" => "application/json" }, [spec]]
45
+ end
46
+
47
+ app = ->(env) do
48
+ if env["PATH_INFO"] == ""
49
+ html_app.call(env)
50
+ elsif env["PATH_INFO"] == "/json"
51
+ json_app.call(env)
52
+ else
53
+ [404, {}, ["Not Found"]]
54
+ end
55
+ end
56
+
57
+ if Rage.config.middleware.include?(Rage::Reloader)
58
+ Rage.with_middlewares(app, [Rage::Reloader])
59
+ elsif defined?(ActionDispatch::Reloader) && Rage.config.middleware.include?(ActionDispatch::Reloader)
60
+ Rage.with_middlewares(app, [ActionDispatch::Reloader])
61
+ else
62
+ app
63
+ end
64
+ end
65
+
66
+ # Build an OpenAPI specification for the application.
67
+ # @param namespace [String, Module] limit the parser to a specific namespace
68
+ # @return [Hash]
69
+ def self.build(namespace: nil)
70
+ Builder.new(namespace:).run
71
+ end
72
+
73
+ # @private
74
+ def self.__shared_components
75
+ __data_cache[:shared_components] ||= begin
76
+ components_file = Rage.root.join("config").glob("openapi_components.*")[0]
77
+
78
+ if components_file.nil?
79
+ {}
80
+ else
81
+ case components_file.extname
82
+ when ".yml", ".yaml"
83
+ YAML.safe_load(components_file.read)
84
+ when ".json"
85
+ JSON.parse(components_file.read)
86
+ else
87
+ Rage::OpenAPI.__log_warn "unrecognized file extension: #{components_file.relative_path_from(Rage.root)}; expected either .yml or .json"
88
+ {}
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ # @private
95
+ def self.__data_cache
96
+ @__data_cache ||= {}
97
+ end
98
+
99
+ # @private
100
+ def self.__reset_data_cache
101
+ __data_cache.clear
102
+ end
103
+
104
+ # @private
105
+ def self.__try_parse_collection(str)
106
+ if str =~ /^Array<([\w\s:\(\)]+)>$/ || str =~ /^\[([\w\s:\(\)]+)\]$/
107
+ [true, $1]
108
+ else
109
+ [false, str]
110
+ end
111
+ end
112
+
113
+ # @private
114
+ def self.__module_parent(klass)
115
+ klass.name =~ /::[^:]+\z/ ? Object.const_get($`) : Object
116
+ rescue NameError
117
+ Object
118
+ end
119
+
120
+ # @private
121
+ def self.__log_warn(log)
122
+ puts "WARNING: #{log}"
123
+ end
124
+
125
+ module Nodes
126
+ end
127
+
128
+ module Parsers
129
+ module Ext
130
+ end
131
+ end
132
+ end
133
+
134
+ require_relative "builder"
135
+ require_relative "collector"
136
+ require_relative "parser"
137
+ require_relative "converter"
138
+ require_relative "nodes/root"
139
+ require_relative "nodes/parent"
140
+ require_relative "nodes/method"
141
+ require_relative "parsers/ext/alba"
142
+ require_relative "parsers/ext/active_record"
143
+ require_relative "parsers/yaml"
144
+ require_relative "parsers/shared_reference"
145
+ require_relative "parsers/request"
146
+ require_relative "parsers/response"
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::OpenAPI::Parser
4
+ def parse_dangling_comments(node, comments)
5
+ i = 0
6
+
7
+ while i < comments.length
8
+ children = nil
9
+ expression = comments[i].slice.delete_prefix("#").strip
10
+
11
+ if expression =~ /@deprecated\b/
12
+ if node.deprecated
13
+ Rage::OpenAPI.__log_warn "duplicate @deprecated tag detected at #{location_msg(comments[i])}"
14
+ else
15
+ node.deprecated = true
16
+ end
17
+ children = find_children(comments[i + 1..])
18
+
19
+ elsif expression =~ /@private\b/
20
+ if node.private
21
+ Rage::OpenAPI.__log_warn "duplicate @private tag detected at #{location_msg(comments[i])}"
22
+ else
23
+ node.private = true
24
+ end
25
+ children = find_children(comments[i + 1..])
26
+
27
+ elsif expression =~ /@version\s/
28
+ if node.root.version
29
+ Rage::OpenAPI.__log_warn "duplicate @version tag detected at #{location_msg(comments[i])}"
30
+ else
31
+ node.root.version = expression[9..]
32
+ end
33
+
34
+ elsif expression =~ /@title\s/
35
+ if node.root.title
36
+ Rage::OpenAPI.__log_warn "duplicate @title tag detected at #{location_msg(comments[i])}"
37
+ else
38
+ node.root.title = expression[7..]
39
+ end
40
+
41
+ elsif expression =~ /@auth\s/
42
+ method, name, tail_name = expression[6..].split(" ", 3)
43
+ children = find_children(comments[i + 1..])
44
+
45
+ if tail_name
46
+ Rage::OpenAPI.__log_warn "incorrect `@auth` name detected at #{location_msg(comments[i])}; security scheme name cannot contain spaces"
47
+ end
48
+
49
+ auth_entry = {
50
+ method:,
51
+ name: name || method,
52
+ definition: children.any? ? YAML.safe_load(children.join("\n")) : { "type" => "http", "scheme" => "bearer" }
53
+ }
54
+
55
+ if !node.controller.__before_action_exists?(method.to_sym)
56
+ Rage::OpenAPI.__log_warn "referenced before action `#{method}` is not defined in #{node.controller} at #{location_msg(comments[i])}; ensure a corresponding `before_action` call exists"
57
+ elsif node.auth.include?(auth_entry) || node.root.parent_nodes.any? { |parent_node| parent_node.auth.include?(auth_entry) }
58
+ Rage::OpenAPI.__log_warn "duplicate @auth tag detected at #{location_msg(comments[i])}"
59
+ else
60
+ node.auth << auth_entry
61
+ end
62
+ end
63
+
64
+ if children&.any?
65
+ i += children.length + 1
66
+ else
67
+ i += 1
68
+ end
69
+ end
70
+ end
71
+
72
+ def parse_method_comments(node, comments)
73
+ i = 0
74
+
75
+ while i < comments.length
76
+ children = nil
77
+ expression = comments[i].slice.delete_prefix("#").strip
78
+
79
+ if !expression.start_with?("@")
80
+ if node.summary
81
+ Rage::OpenAPI.__log_warn "invalid summary entry detected at #{location_msg(comments[i])}; summary should only be one line"
82
+ else
83
+ node.summary = expression
84
+ end
85
+
86
+ elsif expression =~ /@deprecated\b/
87
+ if node.parents.any?(&:deprecated)
88
+ Rage::OpenAPI.__log_warn "duplicate `@deprecated` tag detected at #{location_msg(comments[i])}; tag already exists in a parent class"
89
+ else
90
+ node.deprecated = true
91
+ end
92
+ children = find_children(comments[i + 1..])
93
+
94
+ elsif expression =~ /@private\b/
95
+ if node.parents.any?(&:private)
96
+ Rage::OpenAPI.__log_warn "duplicate `@private` tag detected at #{location_msg(comments[i])}; tag already exists in a parent class"
97
+ else
98
+ node.private = true
99
+ end
100
+ children = find_children(comments[i + 1..])
101
+
102
+ elsif expression =~ /@description\s/
103
+ children = find_children(comments[i + 1..])
104
+ node.description = [expression[13..]] + children
105
+
106
+ elsif expression =~ /@response\s/
107
+ response = expression[10..].strip
108
+ status, response_data = if response =~ /^\d{3}$/
109
+ [response, nil]
110
+ elsif response =~ /^\d{3}/
111
+ response.split(" ", 2)
112
+ else
113
+ ["200", response]
114
+ end
115
+
116
+ if node.responses.has_key?(status)
117
+ Rage::OpenAPI.__log_warn "duplicate `@response` tag detected at #{location_msg(comments[i])}"
118
+ elsif response_data.nil?
119
+ node.responses[status] = nil
120
+ else
121
+ parsed = Rage::OpenAPI::Parsers::Response.parse(
122
+ response_data,
123
+ namespace: Rage::OpenAPI.__module_parent(node.controller)
124
+ )
125
+
126
+ if parsed
127
+ node.responses[status] = parsed
128
+ else
129
+ Rage::OpenAPI.__log_warn "unrecognized `@response` tag detected at #{location_msg(comments[i])}"
130
+ end
131
+ end
132
+
133
+ elsif expression =~ /@request\s/
134
+ request = expression[9..]
135
+ if node.request
136
+ Rage::OpenAPI.__log_warn "duplicate `@request` tag detected at #{location_msg(comments[i])}"
137
+ else
138
+ parsed = Rage::OpenAPI::Parsers::Request.parse(
139
+ request,
140
+ namespace: Rage::OpenAPI.__module_parent(node.controller)
141
+ )
142
+
143
+ if parsed
144
+ node.request = parsed
145
+ else
146
+ Rage::OpenAPI.__log_warn "unrecognized `@request` tag detected at #{location_msg(comments[i])}"
147
+ end
148
+ end
149
+
150
+ elsif expression =~ /@internal\b/
151
+ # no-op
152
+ children = find_children(comments[i + 1..])
153
+
154
+ else
155
+ Rage::OpenAPI.__log_warn "unrecognized `#{expression.split(" ")[0]}` tag detected at #{location_msg(comments[i])}"
156
+ end
157
+
158
+ if children&.any?
159
+ i += children.length + 1
160
+ else
161
+ i += 1
162
+ end
163
+ end
164
+ end
165
+
166
+ private
167
+
168
+ def find_children(comments)
169
+ children = []
170
+
171
+ comments.each do |comment|
172
+ expression = comment.slice.sub(/^#\s/, "")
173
+
174
+ if expression.start_with?(/\s{2}/)
175
+ children << expression.strip
176
+ elsif expression.start_with?("@")
177
+ break
178
+ else
179
+ Rage::OpenAPI.__log_warn "unrecognized expression detected at #{location_msg(comment)}; use two spaces to mark multi-line expressions"
180
+ break
181
+ end
182
+ end
183
+
184
+ children
185
+ end
186
+
187
+ def location_msg(comment)
188
+ location = comment.location
189
+ relative_path = Pathname.new(location.__source_path).relative_path_from(Rage.root)
190
+
191
+ "#{relative_path}:#{location.start_line}"
192
+ end
193
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::OpenAPI::Parsers::Ext::ActiveRecord
4
+ BLACKLISTED_ATTRIBUTES = %w(id created_at updated_at)
5
+
6
+ def initialize(namespace: Object, **)
7
+ @namespace = namespace
8
+ end
9
+
10
+ def known_definition?(str)
11
+ _, str = Rage::OpenAPI.__try_parse_collection(str)
12
+ defined?(ActiveRecord::Base) && @namespace.const_get(str).ancestors.include?(ActiveRecord::Base)
13
+ rescue NameError
14
+ false
15
+ end
16
+
17
+ def parse(klass_str)
18
+ is_collection, klass_str = Rage::OpenAPI.__try_parse_collection(klass_str)
19
+ klass = @namespace.const_get(klass_str)
20
+
21
+ schema = {}
22
+
23
+ klass.attribute_types.each do |attr_name, attr_type|
24
+ next if BLACKLISTED_ATTRIBUTES.include?(attr_name) ||
25
+ attr_name.end_with?("_id") ||
26
+ attr_name == klass.inheritance_column ||
27
+ klass.defined_enums.include?(attr_name)
28
+
29
+ schema[attr_name] = case attr_type.type
30
+ when :integer
31
+ { "type" => "integer" }
32
+ when :boolean
33
+ { "type" => "boolean" }
34
+ when :binary
35
+ { "type" => "string", "format" => "binary" }
36
+ when :date
37
+ { "type" => "string", "format" => "date" }
38
+ when :datetime, :time
39
+ { "type" => "string", "format" => "date-time" }
40
+ when :float
41
+ { "type" => "number", "format" => "float" }
42
+ when :decimal
43
+ { "type" => "number" }
44
+ when :json
45
+ { "type" => "object" }
46
+ else
47
+ { "type" => "string" }
48
+ end
49
+ end
50
+
51
+ klass.defined_enums.each do |attr_name, mapping|
52
+ schema[attr_name] = { "type" => "string", "enum" => mapping.keys }
53
+ end
54
+
55
+ result = { "type" => "object" }
56
+ result["properties"] = schema if schema.any?
57
+
58
+ result = { "type" => "array", "items" => result } if is_collection
59
+
60
+ result
61
+ end
62
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::OpenAPI::Parsers::Ext::Alba
4
+ attr_reader :namespace
5
+
6
+ def initialize(namespace: Object, **)
7
+ @namespace = namespace
8
+ end
9
+
10
+ def known_definition?(str)
11
+ _, str = Rage::OpenAPI.__try_parse_collection(str)
12
+ defined?(Alba::Resource) && @namespace.const_get(str).ancestors.include?(Alba::Resource)
13
+ rescue NameError
14
+ false
15
+ end
16
+
17
+ def parse(klass_str)
18
+ __parse(klass_str).build_schema
19
+ end
20
+
21
+ def __parse_nested(klass_str)
22
+ __parse(klass_str).tap { |visitor|
23
+ visitor.root_key = visitor.root_key_for_collection = visitor.key_transformer = nil
24
+ }.build_schema
25
+ end
26
+
27
+ def __parse(klass_str)
28
+ is_collection, klass_str = Rage::OpenAPI.__try_parse_collection(klass_str)
29
+
30
+ klass = @namespace.const_get(klass_str)
31
+ source_path, _ = Object.const_source_location(klass.name)
32
+ ast = Prism.parse_file(source_path)
33
+
34
+ visitor = Visitor.new(self, is_collection)
35
+ ast.value.accept(visitor)
36
+
37
+ visitor
38
+ end
39
+
40
+ class VisitorContext
41
+ attr_accessor :symbols, :hashes, :keywords, :consts, :nil
42
+
43
+ def initialize
44
+ @symbols = []
45
+ @hashes = []
46
+ @keywords = {}
47
+ @consts = []
48
+ @nil = false
49
+ end
50
+ end
51
+
52
+ class Visitor < Prism::Visitor
53
+ attr_accessor :schema, :root_key, :root_key_for_collection, :key_transformer, :collection_key, :meta
54
+
55
+ def initialize(parser, is_collection)
56
+ @parser = parser
57
+ @is_collection = is_collection
58
+
59
+ @schema = {}
60
+ @segment = @schema
61
+ @context = nil
62
+ @prev_contexts = []
63
+
64
+ @self_name = nil
65
+ @root_key = nil
66
+ @root_key_for_collection = nil
67
+ @key_transformer = nil
68
+ @collection_key = false
69
+ @meta = {}
70
+ end
71
+
72
+ def visit_class_node(node)
73
+ @self_name ||= node.name.to_s
74
+
75
+ if node.name =~ /Resource$|Serializer$/ && node.superclass
76
+ visitor = @parser.__parse(node.superclass.name)
77
+ @root_key, @root_key_for_collection = visitor.root_key, visitor.root_key_for_collection
78
+ @key_transformer, @collection_key, @meta = visitor.key_transformer, visitor.collection_key, visitor.meta
79
+ @schema.merge!(visitor.schema)
80
+ end
81
+
82
+ super
83
+ end
84
+
85
+ def build_schema
86
+ result = { "type" => "object" }
87
+
88
+ result["properties"] = @schema if @schema.any?
89
+
90
+ if @is_collection
91
+ result = if @collection_key && @root_key_for_collection
92
+ { "type" => "object", "properties" => { @root_key_for_collection => { "type" => "object", "additionalProperties" => result }, **@meta } }
93
+ elsif @collection_key
94
+ { "type" => "object", "additionalProperties" => result }
95
+ elsif @root_key_for_collection
96
+ { "type" => "object", "properties" => { @root_key_for_collection => { "type" => "array", "items" => result }, **@meta } }
97
+ else
98
+ { "type" => "array", "items" => result }
99
+ end
100
+ elsif @root_key
101
+ result = { "type" => "object", "properties" => { @root_key => result, **@meta } }
102
+ end
103
+
104
+ result = deep_transform_keys(result) if @key_transformer
105
+
106
+ result
107
+ end
108
+
109
+ def visit_call_node(node)
110
+ case node.name
111
+ when :root_key
112
+ context = with_context { visit(node.arguments) }
113
+ @root_key, @root_key_for_collection = context.symbols
114
+
115
+ when :attributes, :attribute
116
+ context = with_context { visit(node.arguments) }
117
+ context.symbols.each { |symbol| @segment[symbol] = { "type" => "string" } }
118
+ context.keywords.except("if").each { |key, type| @segment[key] = get_type_definition(type) }
119
+
120
+ when :nested, :nested_attribute
121
+ context = with_context { visit(node.arguments) }
122
+ with_inner_segment(context.symbols[0]) { visit(node.block) }
123
+
124
+ when :meta
125
+ context = with_context do
126
+ visit(node.arguments)
127
+ visit(node.block)
128
+ end
129
+
130
+ key = context.symbols[0] || "meta"
131
+ unless context.nil
132
+ @meta = { key => hash_to_openapi_schema(context.hashes[0]) }
133
+ end
134
+
135
+ when :many, :has_many, :one, :has_one, :association
136
+ is_array = node.name == :many || node.name == :has_many
137
+ context = with_context { visit(node.arguments) }
138
+ key = context.keywords["key"] || context.symbols[0]
139
+
140
+ if node.block
141
+ with_inner_segment(key, is_array:) { visit(node.block) }
142
+ else
143
+ resource = context.keywords["resource"] || (::Alba.inflector && "#{::Alba.inflector.classify(key.to_s)}Resource")
144
+ is_valid_resource = @parser.namespace.const_get(resource) rescue false
145
+
146
+ @segment[key] = if is_array
147
+ @parser.__parse_nested(is_valid_resource ? "[#{resource}]" : "[Rage]") # TODO
148
+ else
149
+ @parser.__parse_nested(is_valid_resource ? resource : "Rage")
150
+ end
151
+ end
152
+
153
+ when :transform_keys
154
+ context = with_context { visit(node.arguments) }
155
+ @key_transformer = get_key_transformer(context.symbols[0])
156
+
157
+ when :collection_key
158
+ @collection_key = true
159
+
160
+ when :root_key!
161
+ if (inflector = ::Alba.inflector)
162
+ suffix = @self_name.end_with?("Resource") ? "Resource" : "Serializer"
163
+ name = inflector.demodulize(@self_name).delete_suffix(suffix)
164
+ @root_key = inflector.underscore(name)
165
+ @root_key_for_collection = inflector.pluralize(@root_key) if @is_collection
166
+ end
167
+ end
168
+ end
169
+
170
+ def visit_hash_node(node)
171
+ parsed_hash = YAML.safe_load(node.slice) rescue nil
172
+ @context.hashes << parsed_hash if parsed_hash
173
+ end
174
+
175
+ def visit_assoc_node(node)
176
+ value = case node.value
177
+ when Prism::StringNode
178
+ node.value.content
179
+ when Prism::ArrayNode
180
+ context = with_context { visit(node.value) }
181
+ context.symbols[0] || context.consts[0]
182
+ else
183
+ node.value.slice
184
+ end
185
+
186
+ @context.keywords[node.key.value] = value
187
+ end
188
+
189
+ def visit_constant_read_node(node)
190
+ return unless @context
191
+ @context.consts << node.name.to_s
192
+ end
193
+
194
+ def visit_symbol_node(node)
195
+ @context.symbols << node.value
196
+ end
197
+
198
+ def visit_nil_node(node)
199
+ @context.nil = true
200
+ end
201
+
202
+ private
203
+
204
+ def with_inner_segment(key, is_array: false)
205
+ prev_segment = @segment
206
+
207
+ properties = {}
208
+ if is_array
209
+ @segment[key] = { "type" => "array", "items" => { "type" => "object", "properties" => properties } }
210
+ else
211
+ @segment[key] = { "type" => "object", "properties" => properties }
212
+ end
213
+ @segment = properties
214
+
215
+ yield
216
+ @segment = prev_segment
217
+ end
218
+
219
+ def with_context
220
+ @prev_contexts << @context if @context
221
+ @context = VisitorContext.new
222
+ yield
223
+ current_context = @context
224
+ @context = @prev_contexts.pop
225
+ current_context
226
+ end
227
+
228
+ def hash_to_openapi_schema(hash)
229
+ return { "type" => "object" } unless hash
230
+
231
+ schema = hash.each_with_object({}) do |(key, value), memo|
232
+ memo[key.to_s] = if value.is_a?(Hash)
233
+ hash_to_openapi_schema(value)
234
+ elsif value.is_a?(Array)
235
+ { "type" => "array", "items" => { "type" => "string" } }
236
+ else
237
+ { "type" => "string" }
238
+ end
239
+ end
240
+
241
+ { "type" => "object", "properties" => schema }
242
+ end
243
+
244
+ def deep_transform_keys(schema)
245
+ schema.each_with_object({}) do |(key, value), memo|
246
+ transformed_key = %w(type properties items additionalProperties).include?(key) ? key : @key_transformer.call(key)
247
+ memo[transformed_key] = value.is_a?(Hash) ? deep_transform_keys(value) : value
248
+ end
249
+ end
250
+
251
+ def get_key_transformer(transformer_id)
252
+ return nil unless ::Alba.inflector
253
+
254
+ case transformer_id
255
+ when "camel"
256
+ ->(key) { ::Alba.inflector.camelize(key) }
257
+ when "lower_camel"
258
+ ->(key) { ::Alba.inflector.camelize_lower(key) }
259
+ when "dash"
260
+ ->(key) { ::Alba.inflector.dasherize(key) }
261
+ when "snake"
262
+ ->(key) { ::Alba.inflector.underscore(key) }
263
+ end
264
+ end
265
+
266
+ def get_type_definition(type_id)
267
+ case type_id
268
+ when "Integer"
269
+ { "type" => "integer" }
270
+ when "Boolean", ":Boolean"
271
+ { "type" => "boolean" }
272
+ when "Numeric"
273
+ { "type" => "number" }
274
+ when "Float"
275
+ { "type" => "number", "format" => "float" }
276
+ else
277
+ { "type" => "string" }
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::OpenAPI::Parsers::Request
4
+ AVAILABLE_PARSERS = [
5
+ Rage::OpenAPI::Parsers::SharedReference,
6
+ Rage::OpenAPI::Parsers::YAML,
7
+ Rage::OpenAPI::Parsers::Ext::ActiveRecord
8
+ ]
9
+
10
+ def self.parse(request_tag, namespace:)
11
+ parser = AVAILABLE_PARSERS.find do |parser_class|
12
+ parser = parser_class.new(namespace:)
13
+ break parser if parser.known_definition?(request_tag)
14
+ end
15
+
16
+ parser.parse(request_tag) if parser
17
+ end
18
+ end