oas_rails 0.2.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -1
  3. data/app/controllers/oas_rails/oas_rails_controller.rb +1 -1
  4. data/lib/generators/oas_rails/config/templates/oas_rails_initializer.rb +11 -0
  5. data/lib/oas_rails/builders/content_builder.rb +55 -0
  6. data/lib/oas_rails/builders/operation_builder.rb +32 -0
  7. data/lib/oas_rails/builders/parameter_builder.rb +28 -0
  8. data/lib/oas_rails/builders/parameters_builder.rb +39 -0
  9. data/lib/oas_rails/builders/path_item_builder.rb +22 -0
  10. data/lib/oas_rails/builders/request_body_builder.rb +60 -0
  11. data/lib/oas_rails/builders/response_builder.rb +40 -0
  12. data/lib/oas_rails/builders/responses_builder.rb +58 -0
  13. data/lib/oas_rails/configuration.rb +25 -5
  14. data/lib/oas_rails/esquema_builder.rb +37 -0
  15. data/lib/oas_rails/extractors/oas_route_extractor.rb +66 -0
  16. data/lib/oas_rails/extractors/render_response_extractor.rb +148 -0
  17. data/lib/oas_rails/extractors/route_extractor.rb +125 -0
  18. data/lib/oas_rails/oas_route.rb +1 -99
  19. data/lib/oas_rails/spec/components.rb +85 -0
  20. data/lib/oas_rails/spec/contact.rb +18 -0
  21. data/lib/oas_rails/spec/hashable.rb +39 -0
  22. data/lib/oas_rails/{info.rb → spec/info.rb} +30 -24
  23. data/lib/oas_rails/spec/license.rb +18 -0
  24. data/lib/oas_rails/spec/media_type.rb +84 -0
  25. data/lib/oas_rails/spec/operation.rb +25 -0
  26. data/lib/oas_rails/spec/parameter.rb +34 -0
  27. data/lib/oas_rails/spec/path_item.rb +33 -0
  28. data/lib/oas_rails/spec/paths.rb +26 -0
  29. data/lib/oas_rails/spec/reference.rb +16 -0
  30. data/lib/oas_rails/spec/request_body.rb +21 -0
  31. data/lib/oas_rails/spec/response.rb +20 -0
  32. data/lib/oas_rails/spec/responses.rb +25 -0
  33. data/lib/oas_rails/spec/server.rb +17 -0
  34. data/lib/oas_rails/spec/specable.rb +51 -0
  35. data/lib/oas_rails/spec/specification.rb +50 -0
  36. data/lib/oas_rails/spec/tag.rb +18 -0
  37. data/lib/oas_rails/utils.rb +39 -0
  38. data/lib/oas_rails/version.rb +1 -1
  39. data/lib/oas_rails.rb +47 -26
  40. metadata +32 -18
  41. data/lib/oas_rails/contact.rb +0 -12
  42. data/lib/oas_rails/license.rb +0 -11
  43. data/lib/oas_rails/media_type.rb +0 -76
  44. data/lib/oas_rails/oas_base.rb +0 -30
  45. data/lib/oas_rails/operation.rb +0 -134
  46. data/lib/oas_rails/parameter.rb +0 -47
  47. data/lib/oas_rails/path_item.rb +0 -25
  48. data/lib/oas_rails/paths.rb +0 -19
  49. data/lib/oas_rails/request_body.rb +0 -29
  50. data/lib/oas_rails/response.rb +0 -12
  51. data/lib/oas_rails/responses.rb +0 -20
  52. data/lib/oas_rails/route_extractor.rb +0 -119
  53. data/lib/oas_rails/server.rb +0 -10
  54. data/lib/oas_rails/specification.rb +0 -72
  55. data/lib/oas_rails/tag.rb +0 -17
@@ -0,0 +1,148 @@
1
+ module OasRails
2
+ module Extractors
3
+ # Extracts and processes render responses from a given source.
4
+ module RenderResponseExtractor
5
+ class << self
6
+ # Extracts responses from the provided source string.
7
+ #
8
+ # @param source [String] The source string containing render calls.
9
+ # @return [Array<Response>] An array of Response objects extracted from the source.
10
+ def extract_responses_from_source(specification, source:)
11
+ render_calls = extract_render_calls(source)
12
+ return [Builders::ResponseBuilder.new(specification).with_description("No content").with_code(204).build] if render_calls.empty?
13
+
14
+ render_calls.map { |render_content, status| process_render_content(specification, render_content.strip, status) }
15
+ end
16
+
17
+ private
18
+
19
+ # Extracts render calls from the source string.
20
+ #
21
+ # @param source [String] The source string containing render calls.
22
+ # @return [Array<Array<String, String>>] An array of arrays, each containing render content and status.
23
+ def extract_render_calls(source)
24
+ source.scan(/render json: ((?:\{.*?\}|\S+))(?:, status: :(\w+))?(?:,.*?)?$/m)
25
+ end
26
+
27
+ # Processes the render content and status to build a Response object.
28
+ #
29
+ # @param content [String] The content extracted from the render call.
30
+ # @param status [String] The status code associated with the render call.
31
+ # @return [Response] A Response object based on the processed content and status.
32
+ def process_render_content(specification, content, status)
33
+ schema, examples = build_schema_and_examples(content)
34
+ status_int = Utils.status_to_integer(status)
35
+ content = Builders::ContentBuilder.new(specification, :outgoing).with_schema(schema).with_examples(examples).build
36
+
37
+ Builders::ResponseBuilder.new(specification).with_code(status_int).with_description(Utils.status_code_to_text(status_int)).with_content(content).build
38
+ end
39
+
40
+ # Builds schema and examples based on the content type.
41
+ #
42
+ # @param content [String] The content extracted from the render call.
43
+ # @return [Array<Hash, Hash>] An array where the first element is the schema and the second is the examples.
44
+ def build_schema_and_examples(content)
45
+ if content.start_with?('{')
46
+ [Utils.hash_to_json_schema(parse_hash_structure(content)), {}]
47
+ else
48
+ process_non_hash_content(content)
49
+ end
50
+ rescue StandardError => e
51
+ Rails.logger.debug("Error building schema: #{e.message}")
52
+ [{}]
53
+ end
54
+
55
+ # Processes non-hash content (e.g., model or method calls) to build schema and examples.
56
+ #
57
+ # @param content [String] The content extracted from the render call.
58
+ # @return [Array<Hash, Hash>] An array where the first element is the schema and the second is the examples.
59
+ def process_non_hash_content(content)
60
+ maybe_a_model, errors = content.gsub('@', "").split(".")
61
+ klass = maybe_a_model.singularize.camelize(:upper).constantize
62
+
63
+ if klass.ancestors.include?(ActiveRecord::Base)
64
+ schema = EsquemaBuilder.build_outgoing_schema(klass:)
65
+ if test_singularity(maybe_a_model)
66
+ build_singular_model_schema_and_examples(maybe_a_model, errors, klass, schema)
67
+ else
68
+ build_array_model_schema_and_examples(maybe_a_model, klass, schema)
69
+ end
70
+ else
71
+ [{}]
72
+ end
73
+ end
74
+
75
+ # Builds schema and examples for singular models.
76
+ #
77
+ # @param maybe_a_model [String] The model name or variable.
78
+ # @param errors [String, nil] Errors related to the model.
79
+ # @param klass [Class] The class associated with the model.
80
+ # @param schema [Hash] The schema for the model.
81
+ # @return [Array<Hash, Hash>] An array where the first element is the schema and the second is the examples.
82
+ def build_singular_model_schema_and_examples(_maybe_a_model, errors, klass, schema)
83
+ if errors.nil?
84
+ [schema, Spec::MediaType.search_for_examples_in_tests(klass, context: :outgoing)]
85
+ else
86
+ # TODO: this is not building the real schema.
87
+ [
88
+ {
89
+ type: "object",
90
+ properties: {
91
+ success: { type: "boolean" },
92
+ errors: {
93
+ type: "object",
94
+ additionalProperties: {
95
+ type: "array",
96
+ items: { type: "string" }
97
+ }
98
+ }
99
+ }
100
+ },
101
+ {}
102
+ ]
103
+ end
104
+ end
105
+
106
+ # Builds schema and examples for array models.
107
+ #
108
+ # @param maybe_a_model [String] The model name or variable.
109
+ # @param klass [Class] The class associated with the model.
110
+ # @param schema [Hash] The schema for the model.
111
+ # @return [Array<Hash, Hash>] An array where the first element is the schema and the second is the examples.
112
+ def build_array_model_schema_and_examples(maybe_a_model, klass, schema)
113
+ examples = { maybe_a_model => { value: Spec::MediaType.search_for_examples_in_tests(klass, context: :outgoing).values.map { |p| p.dig(:value, maybe_a_model.singularize.to_sym) } } }
114
+ [{ type: "array", items: schema }, examples]
115
+ end
116
+
117
+ # Determines if a string represents a singular model.
118
+ #
119
+ # @param str [String] The string to test.
120
+ # @return [Boolean] True if the string is a singular model, false otherwise.
121
+ def test_singularity(str)
122
+ str.pluralize != str && str.singularize == str
123
+ end
124
+
125
+ # Parses a hash literal to determine its structure.
126
+ #
127
+ # @param hash_literal [String] The hash literal string.
128
+ # @return [Hash<Symbol, String>] A hash representing the structure of the input.
129
+ def parse_hash_structure(hash_literal)
130
+ structure = {}
131
+
132
+ hash_literal.scan(/(\w+):\s*(\S+)/) do |key, value|
133
+ structure[key.to_sym] = case value
134
+ when 'true', 'false'
135
+ 'Boolean'
136
+ when /^\d+$/
137
+ 'Number'
138
+ else
139
+ 'Object'
140
+ end
141
+ end
142
+
143
+ structure
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,125 @@
1
+ module OasRails
2
+ module Extractors
3
+ class RouteExtractor
4
+ RAILS_DEFAULT_CONTROLLERS = %w[
5
+ rails/info
6
+ rails/mailers
7
+ active_storage/blobs
8
+ active_storage/disk
9
+ active_storage/direct_uploads
10
+ active_storage/representations
11
+ rails/conductor/continuous_integration
12
+ rails/conductor/multiple_databases
13
+ rails/conductor/action_mailbox
14
+ rails/conductor/action_text
15
+ action_cable
16
+ ].freeze
17
+
18
+ RAILS_DEFAULT_PATHS = %w[
19
+ /rails/action_mailbox/
20
+ ].freeze
21
+
22
+ class << self
23
+ def host_routes_by_path(path)
24
+ @host_routes ||= extract_host_routes
25
+ @host_routes.select { |r| r.path == path }
26
+ end
27
+
28
+ def host_routes
29
+ @host_routes ||= extract_host_routes
30
+ end
31
+
32
+ # Clear Class Instance Variable @host_routes
33
+ #
34
+ # This method clear the class instance variable @host_routes
35
+ # to force a extraction of the routes again.
36
+ def clear_cache
37
+ @host_routes = nil
38
+ end
39
+
40
+ def host_paths
41
+ @host_paths ||= host_routes.map(&:path).uniq.sort
42
+ end
43
+
44
+ def clean_route(route)
45
+ route.gsub('(.:format)', '').gsub(/:\w+/) { |match| "{#{match[1..]}}" }
46
+ end
47
+
48
+ # THIS CODE IS NOT IN USE BUT CAN BE USEFULL WITH GLOBAL TAGS OR AUTH TAGS
49
+ # def get_controller_comments(controller_path)
50
+ # YARD.parse_string(File.read(controller_path))
51
+ # controller_class = YARD::Registry.all(:class).first
52
+ # if controller_class
53
+ # class_comment = controller_class.docstring.all
54
+ # method_comments = controller_class.meths.map do |method|
55
+ # {
56
+ # name: method.name,
57
+ # comment: method.docstring.all
58
+ # }
59
+ # end
60
+ # YARD::Registry.clear
61
+ # {
62
+ # class_comment: class_comment,
63
+ # method_comments: method_comments
64
+ # }
65
+ # else
66
+ # YARD::Registry.clear
67
+ # nil
68
+ # end
69
+ # rescue StandardError
70
+ # nil
71
+ # end
72
+ #
73
+ # def get_controller_comment(controller_path)
74
+ # get_controller_comments(controller_path)&.dig(:class_comment) || ''
75
+ # rescue StandardError
76
+ # ''
77
+ # end
78
+
79
+ private
80
+
81
+ def extract_host_routes
82
+ valid_routes.map { |r| OasRoute.new_from_rails_route(rails_route: r) }
83
+ end
84
+
85
+ def valid_routes
86
+ Rails.application.routes.routes.select do |route|
87
+ valid_api_route?(route)
88
+ end
89
+ end
90
+
91
+ def valid_api_route?(route)
92
+ return false unless valid_route_implementation?(route)
93
+ return false if RAILS_DEFAULT_CONTROLLERS.any? { |default| route.defaults[:controller].start_with?(default) }
94
+ return false if RAILS_DEFAULT_PATHS.any? { |path| route.path.spec.to_s.include?(path) }
95
+ return false unless route.path.spec.to_s.start_with?(OasRails.config.api_path)
96
+
97
+ true
98
+ end
99
+
100
+ # Checks if a route has a valid implementation.
101
+ #
102
+ # This method verifies that both the controller and the action specified
103
+ # in the route exist. It checks if the controller class is defined and
104
+ # if the action method is implemented within that controller.
105
+ #
106
+ # @param route [ActionDispatch::Journey::Route] The route to check.
107
+ # @return [Boolean] true if both the controller and action exist, false otherwise.
108
+ def valid_route_implementation?(route)
109
+ controller_name = route.defaults[:controller]&.camelize
110
+ action_name = route.defaults[:action]
111
+
112
+ return false if controller_name.blank? || action_name.blank?
113
+
114
+ controller_class = "#{controller_name}Controller".safe_constantize
115
+
116
+ if controller_class.nil?
117
+ false
118
+ else
119
+ controller_class.instance_methods.include?(action_name.to_sym)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -19,7 +19,7 @@ module OasRails
19
19
  @controller_path = controller_path_extractor(@rails_route.defaults[:controller])
20
20
  @method = @rails_route.defaults[:action]
21
21
  @verb = @rails_route.verb
22
- @path = RouteExtractor.clean_route(@rails_route.path.spec.to_s)
22
+ @path = Extractors::RouteExtractor.clean_route(@rails_route.path.spec.to_s)
23
23
  @docstring = extract_docstring
24
24
  @source_string = extract_source_string
25
25
  end
@@ -41,103 +41,5 @@ module OasRails
41
41
  def controller_path_extractor(controller)
42
42
  Rails.root.join("app/controllers/#{controller}_controller.rb").to_s
43
43
  end
44
-
45
- def detect_request_body
46
- klass = @controller.singularize.camelize.constantize
47
- RequestBody.from_model_class(klass:, required: true)
48
- end
49
-
50
- def extract_responses_from_source
51
- render_calls = @source_string.scan(/render json: ((?:\{.*?\}|\S+))(?:, status: :(\w+))?(?:,.*?)?$/m)
52
-
53
- return [Response.new(code: 204, description: "No Content", content: {})] if render_calls.empty?
54
-
55
- render_calls.map do |render_content, status|
56
- content = render_content.strip
57
-
58
- # TODO: manage when is an array of errors
59
- schema = {}
60
- begin
61
- schema = if content.start_with?('{')
62
- Utils.hash_to_json_schema(parse_hash_structure(content))
63
- else
64
- # It's likely a variable or method call
65
- maybe_a_model, errors = content.gsub('@', "").split(".")
66
- klass = maybe_a_model.singularize.camelize(:upper).constantize
67
- return {} unless klass.ancestors.include? ActiveRecord::Base
68
-
69
- e = Esquema::Builder.new(klass).build_schema.as_json
70
- if test_singularity(maybe_a_model)
71
- if errors.nil?
72
- e
73
- else
74
- {
75
- type: "object",
76
- properties: {
77
- success: {
78
- type: "boolean"
79
- },
80
- errors: {
81
- type: "object",
82
- additionalProperties: {
83
- type: "array",
84
- items: {
85
- type: "string"
86
- }
87
- }
88
- }
89
- }
90
- } end
91
- else
92
- { type: "array", items: e }
93
- end
94
- end
95
- rescue StandardError => e
96
- Rails.logger.debug("Error building schema: #{e.message}")
97
- end
98
-
99
- status_int = status_to_integer(status)
100
- Response.new(code: status_int, description: status_code_to_text(status_int), content: { "application/json": MediaType.new(schema:) })
101
- end
102
- end
103
-
104
- def test_singularity(str)
105
- str.pluralize != str && str.singularize == str
106
- end
107
-
108
- def parse_hash_structure(hash_literal)
109
- structure = {}
110
-
111
- hash_literal.scan(/(\w+):\s*(\S+)/) do |key, value|
112
- structure[key.to_sym] = case value
113
- when 'true', 'false'
114
- 'Boolean'
115
- when /^\d+$/
116
- 'Number'
117
- when '@user.errors'
118
- 'Object'
119
- else
120
- 'Object'
121
- end
122
- end
123
-
124
- structure
125
- end
126
-
127
- def status_to_integer(status)
128
- return 200 if status.nil?
129
-
130
- if status.to_s =~ /^\d+$/
131
- status.to_i
132
- else
133
- status = "unprocessable_content" if status == "unprocessable_entity"
134
- Rack::Utils::SYMBOL_TO_STATUS_CODE[status.to_sym]
135
-
136
- end
137
- end
138
-
139
- def status_code_to_text(status_code)
140
- Rack::Utils::HTTP_STATUS_CODES[status_code] || "Unknown Status Code"
141
- end
142
44
  end
143
45
  end
@@ -0,0 +1,85 @@
1
+ module OasRails
2
+ module Spec
3
+ class Components
4
+ include Specable
5
+
6
+ attr_accessor :schemas, :parameters, :security_schemes, :request_bodies, :responses, :headers, :examples, :links, :callbacks
7
+
8
+ def initialize(specification)
9
+ @specification = specification
10
+ @schemas = {}
11
+ @parameters = {}
12
+ @security_schemes = OasRails.config.security_schemas
13
+ @request_bodies = {}
14
+ @responses = {}
15
+ @headers = {}
16
+ @examples = {}
17
+ @links = {}
18
+ @callbacks = {}
19
+ end
20
+
21
+ def oas_fields
22
+ [:request_bodies, :examples, :responses, :schemas, :parameters, :security_schemes]
23
+ end
24
+
25
+ def add_response(response)
26
+ key = response.hash_key
27
+ @responses[key] = response unless @responses.key? key
28
+
29
+ response_reference(key)
30
+ end
31
+
32
+ def add_parameter(parameter)
33
+ key = parameter.hash_key
34
+ @parameters[key] = parameter unless @parameters.key? key
35
+
36
+ parameter_reference(key)
37
+ end
38
+
39
+ def add_request_body(request_body)
40
+ key = request_body.hash_key
41
+ @request_bodies[key] = request_body unless @request_bodies.key? key
42
+
43
+ request_body_reference(key)
44
+ end
45
+
46
+ def add_schema(schema)
47
+ key = Hashable.generate_hash(schema)
48
+ @schemas[key] = schema if @schemas[key].nil?
49
+
50
+ schema_reference(key)
51
+ end
52
+
53
+ def add_example(example)
54
+ key = Hashable.generate_hash(example)
55
+ @examples[key] = example if @examples[key].nil?
56
+
57
+ example_reference(key)
58
+ end
59
+
60
+ def create_reference(type, name)
61
+ "#/components/#{type}/#{name}"
62
+ end
63
+
64
+ def schema_reference(name)
65
+ Reference.new(create_reference('schemas', name))
66
+ end
67
+
68
+ def response_reference(name)
69
+ Reference.new(create_reference('responses', name))
70
+ end
71
+
72
+ def parameter_reference(name)
73
+ Reference.new(create_reference('parameters', name))
74
+ end
75
+
76
+ def example_reference(name)
77
+ Reference.new(create_reference('examples', name))
78
+ end
79
+
80
+ def request_body_reference(name)
81
+ Reference.new(create_reference('requestBodies', name))
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,18 @@
1
+ module OasRails
2
+ module Spec
3
+ class Contact
4
+ include Specable
5
+ attr_accessor :name, :url, :email
6
+
7
+ def initialize(**kwargs)
8
+ @name = kwargs[:name] || ''
9
+ @url = kwargs[:url] || ''
10
+ @email = kwargs[:email] || ''
11
+ end
12
+
13
+ def oas_fields
14
+ [:name, :url, :email]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ module OasRails
2
+ module Spec
3
+ require 'digest'
4
+
5
+ module Hashable
6
+ def hash_key
7
+ Hashable.generate_hash(hash_representation)
8
+ end
9
+
10
+ def hash_representation
11
+ public_instance_variables.sort.to_h { |var| [var, instance_variable_get(var)] }
12
+ end
13
+
14
+ def self.generate_hash(obj)
15
+ Digest::MD5.hexdigest(hash_representation_recursive(obj).to_s)
16
+ end
17
+
18
+ def public_instance_variables
19
+ instance_variables.select do |var|
20
+ method_name = var.to_s.delete('@')
21
+ respond_to?(method_name) || respond_to?("#{method_name}=")
22
+ end
23
+ end
24
+
25
+ def self.hash_representation_recursive(obj)
26
+ case obj
27
+ when Hash
28
+ obj.transform_values { |v| hash_representation_recursive(v) }
29
+ when Array
30
+ obj.map { |v| hash_representation_recursive(v) }
31
+ when Hashable
32
+ obj.hash_representation
33
+ else
34
+ obj
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,28 +1,33 @@
1
1
  module OasRails
2
- class Info < OasBase
3
- attr_accessor :title, :summary, :description, :terms_of_service, :contact, :license, :version
4
-
5
- def initialize(**kwargs)
6
- super()
7
- @title = kwargs[:title] || default_title
8
- @summary = kwargs[:summary] || default_summary
9
- @description = kwargs[:description] || default_description
10
- @terms_of_service = kwargs[:terms_of_service] || ''
11
- @contact = Contact.new
12
- @license = License.new
13
- @version = kwargs[:version] || '0.0.1'
14
- end
15
-
16
- def default_title
17
- "OasRails #{VERSION}"
18
- end
19
-
20
- def default_summary
21
- "OasRails: Automatic Interactive API Documentation for Rails"
22
- end
23
-
24
- def default_description
25
- "# Welcome to OasRails
2
+ module Spec
3
+ class Info
4
+ include Specable
5
+ attr_accessor :title, :summary, :description, :terms_of_service, :contact, :license, :version
6
+
7
+ def initialize(**kwargs)
8
+ @title = kwargs[:title] || default_title
9
+ @summary = kwargs[:summary] || default_summary
10
+ @description = kwargs[:description] || default_description
11
+ @terms_of_service = kwargs[:terms_of_service] || ''
12
+ @contact = Spec::Contact.new
13
+ @license = Spec::License.new
14
+ @version = kwargs[:version] || '0.0.1'
15
+ end
16
+
17
+ def oas_fields
18
+ [:title, :summary, :description, :terms_of_service, :contact, :license, :version]
19
+ end
20
+
21
+ def default_title
22
+ "OasRails #{VERSION}"
23
+ end
24
+
25
+ def default_summary
26
+ "OasRails: Automatic Interactive API Documentation for Rails"
27
+ end
28
+
29
+ def default_description
30
+ "# Welcome to OasRails
26
31
 
27
32
  OasRails automatically generates interactive documentation for your Rails APIs using the OpenAPI Specification 3.1 (OAS 3.1) and displays it with a nice UI.
28
33
 
@@ -55,6 +60,7 @@ Explore your API documentation and enjoy the power of OasRails!
55
60
  For more information and advanced usage, visit the [OasRails GitHub repository](https://github.com/a-chacon/oas_rails).
56
61
 
57
62
  "
63
+ end
58
64
  end
59
65
  end
60
66
  end
@@ -0,0 +1,18 @@
1
+ module OasRails
2
+ module Spec
3
+ class License
4
+ include Specable
5
+
6
+ attr_accessor :name, :url
7
+
8
+ def initialize(**kwargs)
9
+ @name = kwargs[:name] || 'GPL 3.0'
10
+ @url = kwargs[:url] || 'https://www.gnu.org/licenses/gpl-3.0.html#license-text'
11
+ end
12
+
13
+ def oas_fields
14
+ [:name, :url]
15
+ end
16
+ end
17
+ end
18
+ end