oas_rails 0.2.3 → 0.4.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.
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