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
@@ -1,11 +0,0 @@
1
- module OasRails
2
- class License < OasBase
3
- attr_accessor :name, :url
4
-
5
- def initialize(**kwargs)
6
- super()
7
- @name = kwargs[:name] || 'GPL 3.0'
8
- @url = kwargs[:url] || 'https://www.gnu.org/licenses/gpl-3.0.html#license-text'
9
- end
10
- end
11
- end
@@ -1,76 +0,0 @@
1
- module OasRails
2
- class MediaType < OasBase
3
- attr_accessor :schema, :example, :examples, :encoding
4
-
5
- def initialize(schema:, **kwargs)
6
- super()
7
- @schema = schema
8
- @example = kwargs[:example] || {}
9
- @examples = kwargs[:examples] || []
10
- end
11
-
12
- class << self
13
- def from_model_class(klass:, examples: {})
14
- return unless klass.ancestors.include? ActiveRecord::Base
15
-
16
- model_schema = Esquema::Builder.new(klass).build_schema.as_json
17
- model_schema["required"] = []
18
- schema = { type: "object", properties: { klass.to_s.downcase => model_schema } }
19
- examples.merge!(search_for_examples_in_tests(klass:))
20
- new(media_type: "", schema:, examples:)
21
- end
22
-
23
- # Searches for examples in test files based on the provided class and test framework.
24
- #
25
- # This method handles different test frameworks to fetch examples for the given class.
26
- # Currently, it supports FactoryBot and fixtures.
27
- #
28
- # @param klass [Class] the class to search examples for.
29
- # @param utils [Module] a utility module that provides the `detect_test_framework` method. Defaults to `Utils`.
30
- # @return [Hash] a hash containing examples data or an empty hash if no examples are found.
31
- # @example Usage with FactoryBot
32
- # search_for_examples_in_tests(klass: User)
33
- #
34
- # @example Usage with fixtures
35
- # search_for_examples_in_tests(klass: Project)
36
- #
37
- # @example Usage with a custom utils module
38
- # custom_utils = Module.new do
39
- # def self.detect_test_framework
40
- # :factory_bot
41
- # end
42
- # end
43
- # search_for_examples_in_tests(klass: User, utils: custom_utils)
44
- def search_for_examples_in_tests(klass:, utils: Utils)
45
- case utils.detect_test_framework
46
- when :factory_bot
47
- {}
48
- # TODO: create examples with FactoryBot
49
- when :fixtures
50
- fixture_file = Rails.root.join('test', 'fixtures', "#{klass.to_s.pluralize.downcase}.yml")
51
-
52
- begin
53
- fixture_data = YAML.load_file(fixture_file).with_indifferent_access
54
- rescue Errno::ENOENT
55
- return {}
56
- end
57
-
58
- fixture_data.transform_values { |attributes| { value: { klass.to_s.downcase => attributes } } }
59
- else
60
- {}
61
- end
62
- end
63
-
64
- def tags_to_examples(tags:)
65
- tags.each_with_object({}).with_index(1) do |(example, result), _index|
66
- key = example.text.downcase.gsub(' ', '_')
67
- value = {
68
- "summary" => example.text,
69
- "value" => example.content
70
- }
71
- result[key] = value
72
- end
73
- end
74
- end
75
- end
76
- end
@@ -1,30 +0,0 @@
1
- module OasRails
2
- class OasBase
3
- def to_spec
4
- hash = {}
5
- instance_variables.each do |var|
6
- key = var.to_s.delete('@')
7
- camel_case_key = key.camelize(:lower).to_sym
8
- value = instance_variable_get(var)
9
-
10
- processed_value = if value.respond_to?(:to_spec)
11
- value.to_spec
12
- else
13
- value
14
- end
15
-
16
- # hash[camel_case_key] = processed_value unless (processed_value.is_a?(Hash) || processed_value.is_a?(Array)) && processed_value.empty?
17
- hash[camel_case_key] = processed_value
18
- end
19
- hash
20
- end
21
-
22
- private
23
-
24
- def snake_to_camel(snake_str)
25
- words = snake_str.to_s.split('_')
26
- words[1..].map!(&:capitalize)
27
- (words[0] + words[1..].join).to_sym
28
- end
29
- end
30
- end
@@ -1,134 +0,0 @@
1
- module OasRails
2
- class Operation < OasBase
3
- attr_accessor :tags, :summary, :description, :operation_id, :parameters, :method, :docstring, :request_body, :responses, :security
4
-
5
- def initialize(method:, summary:, operation_id:, **kwargs)
6
- super()
7
- @method = method
8
- @summary = summary
9
- @operation_id = operation_id
10
- @tags = kwargs[:tags] || []
11
- @description = kwargs[:description] || @summary
12
- @parameters = kwargs[:parameters] || []
13
- @request_body = kwargs[:request_body] || {}
14
- @responses = kwargs[:responses] || {}
15
- @security = kwargs[:security] || []
16
- end
17
-
18
- class << self
19
- def from_oas_route(oas_route:)
20
- summary = extract_summary(oas_route:)
21
- operation_id = extract_operation_id(oas_route:)
22
- tags = extract_tags(oas_route:)
23
- description = oas_route.docstring
24
- parameters = extract_parameters(oas_route:)
25
- request_body = extract_request_body(oas_route:)
26
- responses = extract_responses(oas_route:)
27
- security = extract_security(oas_route:)
28
- new(method: oas_route.verb.downcase, summary:, operation_id:, tags:, description:, parameters:, request_body:, responses:, security:)
29
- end
30
-
31
- def extract_summary(oas_route:)
32
- oas_route.docstring.tags(:summary).first.try(:text) || generate_crud_name(oas_route.method, oas_route.controller.downcase) || oas_route.verb + " " + oas_route.path
33
- end
34
-
35
- def generate_crud_name(method, controller)
36
- controller_name = controller.to_s.underscore.humanize.downcase.pluralize
37
-
38
- case method.to_sym
39
- when :index
40
- "List #{controller_name}"
41
- when :show
42
- "View #{controller_name.singularize}"
43
- when :create
44
- "Create new #{controller_name.singularize}"
45
- when :update
46
- "Update #{controller_name.singularize}"
47
- when :destroy
48
- "Delete #{controller_name.singularize}"
49
- end
50
- end
51
-
52
- def extract_operation_id(oas_route:)
53
- "#{oas_route.method}#{oas_route.path.gsub('/', '_').gsub(/[{}]/, '')}"
54
- end
55
-
56
- # This method should check tags defined by yard, then extract tag from path namespace or controller name depending on configuration
57
- def extract_tags(oas_route:)
58
- tags = oas_route.docstring.tags(:tags).first
59
- if !tags.nil?
60
- tags.text.split(",").map(&:strip).map(&:titleize)
61
- else
62
- default_tags(oas_route:)
63
- end
64
- end
65
-
66
- def default_tags(oas_route:)
67
- tags = []
68
- if OasRails.config.default_tags_from == "namespace"
69
- tag = oas_route.path.split('/').reject(&:empty?).first.try(:titleize)
70
- tags << tag unless tag.nil?
71
- else
72
- tags << oas_route.controller.titleize
73
- end
74
- tags
75
- end
76
-
77
- def extract_parameters(oas_route:)
78
- parameters = []
79
- parameters.concat(parameters_from_tags(tags: oas_route.docstring.tags(:parameter)))
80
- oas_route.path_params.try(:map) do |p|
81
- parameters << Parameter.from_path(path: oas_route.path, param: p) unless parameters.any? { |param| param.name.to_s == p.to_s }
82
- end
83
- parameters
84
- end
85
-
86
- def parameters_from_tags(tags:)
87
- tags.map do |t|
88
- Parameter.new(name: t.name, location: t.location, required: t.required, schema: t.schema, description: t.text)
89
- end
90
- end
91
-
92
- def extract_request_body(oas_route:)
93
- tag_request_body = oas_route.docstring.tags(:request_body).first
94
- if tag_request_body.nil? && OasRails.config.autodiscover_request_body
95
- oas_route.detect_request_body if %w[create update].include? oas_route.method
96
- elsif !tag_request_body.nil?
97
- RequestBody.from_tags(tag: tag_request_body, examples_tags: oas_route.docstring.tags(:request_body_example))
98
- else
99
- {}
100
- end
101
- end
102
-
103
- def extract_responses(oas_route:)
104
- responses = Responses.from_tags(tags: oas_route.docstring.tags(:response))
105
-
106
- if OasRails.config.autodiscover_responses
107
- new_responses = oas_route.extract_responses_from_source
108
-
109
- new_responses.each do |new_response|
110
- responses.responses << new_response unless responses.responses.any? { |r| r.code == new_response.code }
111
- end
112
- end
113
-
114
- responses
115
- end
116
-
117
- def extract_security(oas_route:)
118
- return [] if oas_route.docstring.tags(:no_auth).any?
119
-
120
- if (methods = oas_route.docstring.tags(:auth).first)
121
- OasRails.config.security_schemas.keys.map { |key| { key => [] } }.select do |schema|
122
- methods.types.include?(schema.keys.first.to_s)
123
- end
124
- elsif OasRails.config.authenticate_all_routes_by_default
125
- OasRails.config.security_schemas.keys.map { |key| { key => [] } }
126
- else
127
- []
128
- end
129
- end
130
-
131
- def external_docs; end
132
- end
133
- end
134
- end
@@ -1,47 +0,0 @@
1
- module OasRails
2
- class Parameter
3
- STYLE_DEFAULTS = { query: 'form', path: 'simple', header: 'simple', cookie: 'form' }.freeze
4
-
5
- attr_accessor :name, :in, :style, :description, :required, :schema
6
-
7
- def initialize(name:, location:, description:, **kwargs)
8
- @name = name
9
- @in = location
10
- @description = description
11
-
12
- @required = kwargs[:required] || required?
13
- @style = kwargs[:style] || default_from_in
14
- @schema = kwargs[:schema] || { "type": 'string' }
15
- end
16
-
17
- def self.from_path(path:, param:)
18
- new(name: param, location: 'path',
19
- description: "#{param.split('_')[-1].titleize} of existing #{extract_word_before(path, param).singularize}.")
20
- end
21
-
22
- def self.extract_word_before(string, param)
23
- regex = %r{/(\w+)/\{#{param}\}}
24
- match = string.match(regex)
25
- match ? match[1] : nil
26
- end
27
-
28
- def default_from_in
29
- STYLE_DEFAULTS[@in.to_sym]
30
- end
31
-
32
- def required?
33
- @in == 'path'
34
- end
35
-
36
- def to_spec
37
- {
38
- "name": @name,
39
- "in": @in,
40
- "description": @description,
41
- "required": @required,
42
- "schema": @schema,
43
- "style": @style
44
- }
45
- end
46
- end
47
- end
@@ -1,25 +0,0 @@
1
- module OasRails
2
- class PathItem
3
- attr_reader :path, :operations, :parameters
4
-
5
- def initialize(path:, operations:, parameters:)
6
- @path = path
7
- @operations = operations
8
- @parameters = parameters
9
- end
10
-
11
- def self.from_oas_routes(path:, oas_routes:)
12
- new(path: path, operations: oas_routes.map do |oas_route|
13
- Operation.from_oas_route(oas_route: oas_route)
14
- end, parameters: [])
15
- end
16
-
17
- def to_spec
18
- spec = {}
19
- @operations.each do |o|
20
- spec[o.method] = o.to_spec
21
- end
22
- spec
23
- end
24
- end
25
- end
@@ -1,19 +0,0 @@
1
- module OasRails
2
- class Paths
3
- attr_accessor :path_items
4
-
5
- def initialize(path_items:)
6
- @path_items = path_items
7
- end
8
-
9
- def self.from_string_paths(string_paths:)
10
- new(path_items: string_paths.map do |s|
11
- PathItem.from_oas_routes(path: s, oas_routes: RouteExtractor.host_routes_by_path(s))
12
- end)
13
- end
14
-
15
- def to_spec
16
- @path_items.each_with_object({}) { |p, object| object[p.path] = p.to_spec }
17
- end
18
- end
19
- end
@@ -1,29 +0,0 @@
1
- module OasRails
2
- class RequestBody < OasBase
3
- attr_accessor :description, :content, :required
4
-
5
- def initialize(description:, content:, required: false)
6
- super()
7
- @description = description
8
- @content = content # Should be an array of media type object
9
- @required = required
10
- end
11
-
12
- class << self
13
- def from_tags(tag:, examples_tags: [])
14
- if tag.klass.ancestors.include? ActiveRecord::Base
15
- from_model_class(klass: tag.klass, description: tag.text, required: tag.required, examples_tags:)
16
- else
17
- # hash content to schema
18
- content = { "application/json": MediaType.new(schema: tag.schema, examples: MediaType.tags_to_examples(tags: examples_tags)) }
19
- new(description: tag.text, content:, required: tag.required)
20
- end
21
- end
22
-
23
- def from_model_class(klass:, **kwargs)
24
- content = { "application/json": MediaType.from_model_class(klass:, examples: MediaType.tags_to_examples(tags: kwargs[:examples_tags] || {})) }
25
- new(description: kwargs[:description] || klass.to_s, content:, required: kwargs[:required])
26
- end
27
- end
28
- end
29
- end
@@ -1,12 +0,0 @@
1
- module OasRails
2
- class Response < OasBase
3
- attr_accessor :code, :description, :content
4
-
5
- def initialize(code:, description:, content:)
6
- super()
7
- @code = code
8
- @description = description
9
- @content = content # Should be an array of media type object
10
- end
11
- end
12
- end
@@ -1,20 +0,0 @@
1
- module OasRails
2
- class Responses < OasBase
3
- attr_accessor :responses
4
-
5
- def initialize(responses)
6
- super()
7
- @responses = responses
8
- end
9
-
10
- def to_spec
11
- @responses.each_with_object({}) { |r, object| object[r.code] = r.to_spec }
12
- end
13
-
14
- class << self
15
- def from_tags(tags:)
16
- new(tags.map { |t| Response.new(code: t.name.to_i, description: t.text, content: { "application/json": MediaType.new(schema: t.schema) }) })
17
- end
18
- end
19
- end
20
- end
@@ -1,119 +0,0 @@
1
- module OasRails
2
- class RouteExtractor
3
- RAILS_DEFAULT_CONTROLLERS = %w[
4
- rails/info
5
- rails/mailers
6
- active_storage/blobs
7
- active_storage/disk
8
- active_storage/direct_uploads
9
- active_storage/representations
10
- rails/conductor/continuous_integration
11
- rails/conductor/multiple_databases
12
- rails/conductor/action_mailbox
13
- rails/conductor/action_text
14
- action_cable
15
- ].freeze
16
-
17
- RAILS_DEFAULT_PATHS = %w[
18
- /rails/action_mailbox/
19
- ].freeze
20
-
21
- class << self
22
- def host_routes_by_path(path)
23
- @host_routes ||= extract_host_routes
24
- @host_routes.select { |r| r.path == path }
25
- end
26
-
27
- def host_routes
28
- @host_routes ||= extract_host_routes
29
- end
30
-
31
- # Clear Class Instance Variable @host_routes
32
- #
33
- # This method clear the class instance variable @host_routes
34
- # to force a extraction of the routes again.
35
- def clear_cache
36
- @host_routes = nil
37
- end
38
-
39
- def host_paths
40
- @host_paths ||= host_routes.map(&:path).uniq.sort
41
- end
42
-
43
- def clean_route(route)
44
- route.gsub('(.:format)', '').gsub(/:\w+/) { |match| "{#{match[1..]}}" }
45
- end
46
-
47
- # THIS CODE IS NOT IN USE BUT CAN BE USEFULL WITH GLOBAL TAGS OR AUTH TAGS
48
- # def get_controller_comments(controller_path)
49
- # YARD.parse_string(File.read(controller_path))
50
- # controller_class = YARD::Registry.all(:class).first
51
- # if controller_class
52
- # class_comment = controller_class.docstring.all
53
- # method_comments = controller_class.meths.map do |method|
54
- # {
55
- # name: method.name,
56
- # comment: method.docstring.all
57
- # }
58
- # end
59
- # YARD::Registry.clear
60
- # {
61
- # class_comment: class_comment,
62
- # method_comments: method_comments
63
- # }
64
- # else
65
- # YARD::Registry.clear
66
- # nil
67
- # end
68
- # rescue StandardError
69
- # nil
70
- # end
71
- #
72
- # def get_controller_comment(controller_path)
73
- # get_controller_comments(controller_path)&.dig(:class_comment) || ''
74
- # rescue StandardError
75
- # ''
76
- # end
77
-
78
- private
79
-
80
- def extract_host_routes
81
- Rails.application.routes.routes.select do |route|
82
- valid_api_route?(route)
83
- end.map { |r| OasRoute.new_from_rails_route(rails_route: r) }
84
- end
85
-
86
- def valid_api_route?(route)
87
- return false unless valid_route_implementation?(route)
88
- return false if RAILS_DEFAULT_CONTROLLERS.any? { |default| route.defaults[:controller].start_with?(default) }
89
- return false if RAILS_DEFAULT_PATHS.any? { |path| route.path.spec.to_s.include?(path) }
90
- return false unless route.path.spec.to_s.start_with?(OasRails.config.api_path)
91
-
92
- true
93
- end
94
-
95
- # Checks if a route has a valid implementation.
96
- #
97
- # This method verifies that both the controller and the action specified
98
- # in the route exist. It checks if the controller class is defined and
99
- # if the action method is implemented within that controller.
100
- #
101
- # @param route [ActionDispatch::Journey::Route] The route to check.
102
- # @return [Boolean] true if both the controller and action exist, false otherwise.
103
- def valid_route_implementation?(route)
104
- controller_name = route.defaults[:controller]&.camelize
105
- action_name = route.defaults[:action]
106
-
107
- return false if controller_name.blank? || action_name.blank?
108
-
109
- controller_class = "#{controller_name}Controller".safe_constantize
110
-
111
- if controller_class.nil?
112
- false
113
- else
114
- controller_class.instance_methods.include?(action_name.to_sym)
115
- end
116
- end
117
- end
118
- end
119
- end
@@ -1,10 +0,0 @@
1
- module OasRails
2
- class Server < OasBase
3
- attr_accessor :url, :description
4
-
5
- def initialize(url:, description:)
6
- @url = url
7
- @description = description
8
- end
9
- end
10
- end
@@ -1,72 +0,0 @@
1
- require 'json'
2
-
3
- module OasRails
4
- class Specification
5
- # Initializes a new Specification object.
6
- # Clears the cache if running in the development environment.
7
- def initialize
8
- clear_cache unless Rails.env.production?
9
-
10
- @specification = base_spec
11
- end
12
-
13
- # Clears the cache for MethodSource and RouteExtractor.
14
- #
15
- # @return [void]
16
- def clear_cache
17
- MethodSource.clear_cache
18
- RouteExtractor.clear_cache
19
- end
20
-
21
- def to_json(*_args)
22
- @specification.to_json
23
- rescue StandardError => e
24
- Rails.logger.error("Error Generating OAS: #{e.message}")
25
- {}
26
- end
27
-
28
- # Create the Base of the OAS hash.
29
- # @see https://spec.openapis.org/oas/latest.html#schema
30
- def base_spec
31
- {
32
- openapi: '3.1.0',
33
- info: OasRails.config.info.to_spec,
34
- servers: OasRails.config.servers.map(&:to_spec),
35
- paths:,
36
- components:,
37
- security:,
38
- tags: OasRails.config.tags.map(&:to_spec),
39
- externalDocs: {}
40
- }
41
- end
42
-
43
- # Create the Security Requirement Object.
44
- # @see https://spec.openapis.org/oas/latest.html#security-requirement-object
45
- def security
46
- return [] unless OasRails.config.authenticate_all_routes_by_default
47
-
48
- OasRails.config.security_schemas.map { |key, _| { key => [] } }
49
- end
50
-
51
- # Create the Paths Object For the Root of the OAS.
52
- # @see https://spec.openapis.org/oas/latest.html#paths-object
53
- def paths
54
- Paths.from_string_paths(string_paths: RouteExtractor.host_paths).to_spec
55
- end
56
-
57
- # Created the Components Object For the Root of the OAS.
58
- # @see https://spec.openapis.org/oas/latest.html#components-object
59
- def components
60
- {
61
- schemas: {}, parameters: {}, securitySchemes: security_schemas, requestBodies: {}, responses: {},
62
- headers: {}, examples: {}, links: {}, callbacks: {}
63
- }
64
- end
65
-
66
- # Create the Security Schemas Array inside components field of the OAS.
67
- # @see https://spec.openapis.org/oas/latest.html#security-scheme-object
68
- def security_schemas
69
- OasRails.config.security_schemas
70
- end
71
- end
72
- end
data/lib/oas_rails/tag.rb DELETED
@@ -1,17 +0,0 @@
1
- module OasRails
2
- class Tag
3
- attr_accessor :name, :description
4
-
5
- def initialize(name:, description:)
6
- @name = name.titleize
7
- @description = description
8
- end
9
-
10
- def to_spec
11
- {
12
- name: @name,
13
- description: @description
14
- }
15
- end
16
- end
17
- end