oas_rails 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c6823d6b64ffb6edbeabed87d2985f006433d6c8243d6e42bf21a6c96fab1e1
4
- data.tar.gz: 85e8a04aa51388ce0ba2f0e27da7c81176ecc1e62630e301147ac0d1bd57fbac
3
+ metadata.gz: c9e40d4f8c810e300bbccaf190fd5143e5be804d2d30f1e86393e0478055965f
4
+ data.tar.gz: 0b6de20a5280115b3550d950da6e26d8424f4d52cc15d6f46f5272224831904e
5
5
  SHA512:
6
- metadata.gz: cfd37bb135fcbe966a2ad9972f5339f9c3c196cc10234a65570e023d8ae4386f4d55bac813f51c3da6cb5f4d2f29c49c8294f29eb391d405fa437fe5c5b34375
7
- data.tar.gz: 49e84dddf1baeb860ae753757a72c5a9989e4c1fd49dcc9b9beb79ea52323a330eb054236e3329756192e68fd43c2cc1ec9f0ea6f269082c0191d4ca184b5596
6
+ metadata.gz: 832e56a728da8974e07d2e83970f6cfd69b892b2e3a89694e08783520ae50698982a59119cf8497d0f2827ca0a4a443a04b80ef03838fe27c596c8dce30a0c1b
7
+ data.tar.gz: 63d37307fc03dd0d832586ce9e9ae4bdbc1f5480b7624991b7d1aa44323bb1f94c86ccd94a523b31e67b0f3f91fb08cfed00793529294c071192b7079bd4a641
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  OasRails is a Rails engine for generating **automatic interactive documentation for your Rails APIs**. It generates an **OAS 3.1** document and displays it using **[RapiDoc](https://rapidocweb.com)**.
8
8
 
9
- ![Screenshot](https://raw.githubusercontent.com/a-chacon/oas_rails/0cfc9abb5be85e6bb3fc4669e29372be8f80a276/oas_rails_ui.png)
9
+ ![Screenshot](https://a-chacon.com/assets/images/oas_rails_ui.png)
10
10
 
11
11
  ## Related Projects
12
12
 
@@ -34,6 +34,14 @@ module OasRails
34
34
  def tags=(value)
35
35
  @tags = value.map { |t| Tag.new(name: t[:name], description: t[:description]) }
36
36
  end
37
+
38
+ def excluded_columns_incoming
39
+ %i[id created_at updated_at deleted_at]
40
+ end
41
+
42
+ def excluded_columns_outgoing
43
+ []
44
+ end
37
45
  end
38
46
 
39
47
  DEFAULT_SECURITY_SCHEMES = {
@@ -0,0 +1,37 @@
1
+ module OasRails
2
+ module EsquemaBuilder
3
+ class << self
4
+ # Builds a schema for a class when it is used as incoming API data.
5
+ #
6
+ # @param klass [Class] The class for which the schema is built.
7
+ # @return [Hash] The schema as a JSON-compatible hash.
8
+ def build_incoming_schema(klass:)
9
+ configure_common_settings
10
+ Esquema.configuration.excluded_columns = OasRails.config.excluded_columns_incoming
11
+
12
+ Esquema::Builder.new(klass).build_schema.as_json
13
+ end
14
+
15
+ # Builds a schema for a class when it is used as outgoing API data.
16
+ #
17
+ # @param klass [Class] The class for which the schema is built.
18
+ # @return [Hash] The schema as a JSON-compatible hash.
19
+ def build_outgoing_schema(klass:)
20
+ configure_common_settings
21
+ Esquema.configuration.excluded_columns = OasRails.config.excluded_columns_outgoing
22
+
23
+ Esquema::Builder.new(klass).build_schema.as_json
24
+ end
25
+
26
+ private
27
+
28
+ # Configures common settings for schema building.
29
+ #
30
+ # Excludes associations and foreign keys from the schema.
31
+ def configure_common_settings
32
+ Esquema.configuration.exclude_associations = true
33
+ Esquema.configuration.exclude_foreign_keys = true
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,173 @@
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(source:)
11
+ render_calls = extract_render_calls(source)
12
+
13
+ return [Response.new(code: 204, description: "No Content", content: {})] if render_calls.empty?
14
+
15
+ render_calls.map { |render_content, status| process_render_content(render_content.strip, status) }
16
+ end
17
+
18
+ private
19
+
20
+ # Extracts render calls from the source string.
21
+ #
22
+ # @param source [String] The source string containing render calls.
23
+ # @return [Array<Array<String, String>>] An array of arrays, each containing render content and status.
24
+ def extract_render_calls(source)
25
+ source.scan(/render json: ((?:\{.*?\}|\S+))(?:, status: :(\w+))?(?:,.*?)?$/m)
26
+ end
27
+
28
+ # Processes the render content and status to build a Response object.
29
+ #
30
+ # @param content [String] The content extracted from the render call.
31
+ # @param status [String] The status code associated with the render call.
32
+ # @return [Response] A Response object based on the processed content and status.
33
+ def process_render_content(content, status)
34
+ schema, examples = build_schema_and_examples(content)
35
+ status_int = status_to_integer(status)
36
+ Response.new(
37
+ code: status_int,
38
+ description: status_code_to_text(status_int),
39
+ content: { "application/json": MediaType.new(schema:, examples:) }
40
+ )
41
+ end
42
+
43
+ # Builds schema and examples based on the content type.
44
+ #
45
+ # @param content [String] The content extracted from the render call.
46
+ # @return [Array<Hash, Hash>] An array where the first element is the schema and the second is the examples.
47
+ def build_schema_and_examples(content)
48
+ if content.start_with?('{')
49
+ [Utils.hash_to_json_schema(parse_hash_structure(content)), {}]
50
+ else
51
+ process_non_hash_content(content)
52
+ end
53
+ rescue StandardError => e
54
+ Rails.logger.debug("Error building schema: #{e.message}")
55
+ [{}]
56
+ end
57
+
58
+ # Processes non-hash content (e.g., model or method calls) to build schema and examples.
59
+ #
60
+ # @param content [String] The content extracted from the render call.
61
+ # @return [Array<Hash, Hash>] An array where the first element is the schema and the second is the examples.
62
+ def process_non_hash_content(content)
63
+ maybe_a_model, errors = content.gsub('@', "").split(".")
64
+ klass = maybe_a_model.singularize.camelize(:upper).constantize
65
+
66
+ if klass.ancestors.include?(ActiveRecord::Base)
67
+ schema = EsquemaBuilder.build_outgoing_schema(klass:)
68
+ if test_singularity(maybe_a_model)
69
+ build_singular_model_schema_and_examples(maybe_a_model, errors, klass, schema)
70
+ else
71
+ build_array_model_schema_and_examples(maybe_a_model, klass, schema)
72
+ end
73
+ else
74
+ [{}]
75
+ end
76
+ end
77
+
78
+ # Builds schema and examples for singular models.
79
+ #
80
+ # @param maybe_a_model [String] The model name or variable.
81
+ # @param errors [String, nil] Errors related to the model.
82
+ # @param klass [Class] The class associated with the model.
83
+ # @param schema [Hash] The schema for the model.
84
+ # @return [Array<Hash, Hash>] An array where the first element is the schema and the second is the examples.
85
+ def build_singular_model_schema_and_examples(_maybe_a_model, errors, klass, schema)
86
+ if errors.nil?
87
+ [schema, MediaType.search_for_examples_in_tests(klass:, context: :outgoing)]
88
+ else
89
+ [
90
+ {
91
+ type: "object",
92
+ properties: {
93
+ success: { type: "boolean" },
94
+ errors: {
95
+ type: "object",
96
+ additionalProperties: {
97
+ type: "array",
98
+ items: { type: "string" }
99
+ }
100
+ }
101
+ }
102
+ },
103
+ {}
104
+ ]
105
+ end
106
+ end
107
+
108
+ # Builds schema and examples for array models.
109
+ #
110
+ # @param maybe_a_model [String] The model name or variable.
111
+ # @param klass [Class] The class associated with the model.
112
+ # @param schema [Hash] The schema for the model.
113
+ # @return [Array<Hash, Hash>] An array where the first element is the schema and the second is the examples.
114
+ def build_array_model_schema_and_examples(maybe_a_model, klass, schema)
115
+ examples = { maybe_a_model => { value: MediaType.search_for_examples_in_tests(klass:, context: :outgoing).values.map { |p| p.dig(:value, maybe_a_model.singularize.to_sym) } } }
116
+ [{ type: "array", items: schema }, examples]
117
+ end
118
+
119
+ # Determines if a string represents a singular model.
120
+ #
121
+ # @param str [String] The string to test.
122
+ # @return [Boolean] True if the string is a singular model, false otherwise.
123
+ def test_singularity(str)
124
+ str.pluralize != str && str.singularize == str
125
+ end
126
+
127
+ # Parses a hash literal to determine its structure.
128
+ #
129
+ # @param hash_literal [String] The hash literal string.
130
+ # @return [Hash<Symbol, String>] A hash representing the structure of the input.
131
+ def parse_hash_structure(hash_literal)
132
+ structure = {}
133
+
134
+ hash_literal.scan(/(\w+):\s*(\S+)/) do |key, value|
135
+ structure[key.to_sym] = case value
136
+ when 'true', 'false'
137
+ 'Boolean'
138
+ when /^\d+$/
139
+ 'Number'
140
+ else
141
+ 'Object'
142
+ end
143
+ end
144
+
145
+ structure
146
+ end
147
+
148
+ # Converts a status symbol or string to an integer.
149
+ #
150
+ # @param status [String, Symbol, nil] The status to convert.
151
+ # @return [Integer] The status code as an integer.
152
+ def status_to_integer(status)
153
+ return 200 if status.nil?
154
+
155
+ if status.to_s =~ /^\d+$/
156
+ status.to_i
157
+ else
158
+ status = "unprocessable_content" if status == "unprocessable_entity"
159
+ Rack::Utils::SYMBOL_TO_STATUS_CODE[status.to_sym]
160
+ end
161
+ end
162
+
163
+ # Converts a status code to its corresponding text description.
164
+ #
165
+ # @param status_code [Integer] The status code.
166
+ # @return [String] The text description of the status code.
167
+ def status_code_to_text(status_code)
168
+ Rack::Utils::HTTP_STATUS_CODES[status_code] || "Unknown Status Code"
169
+ end
170
+ end
171
+ end
172
+ end
173
+ 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
@@ -2,18 +2,29 @@ module OasRails
2
2
  class MediaType < OasBase
3
3
  attr_accessor :schema, :example, :examples, :encoding
4
4
 
5
+ # Initializes a new MediaType object.
6
+ #
7
+ # @param schema [Hash] the schema of the media type.
8
+ # @param kwargs [Hash] additional keyword arguments.
5
9
  def initialize(schema:, **kwargs)
6
10
  super()
7
11
  @schema = schema
8
12
  @example = kwargs[:example] || {}
9
- @examples = kwargs[:examples] || []
13
+ @examples = kwargs[:examples] || {}
10
14
  end
11
15
 
12
16
  class << self
13
- def from_model_class(klass:, examples: {})
17
+ @context = :incoming
18
+ # Creates a new MediaType object from a model class.
19
+ #
20
+ # @param klass [Class] the ActiveRecord model class.
21
+ # @param examples [Hash] the examples hash.
22
+ # @return [MediaType, nil] the created MediaType object or nil if the class is not an ActiveRecord model.
23
+ def from_model_class(klass:, context: :incoming, examples: {})
24
+ @context = context
14
25
  return unless klass.ancestors.include? ActiveRecord::Base
15
26
 
16
- model_schema = Esquema::Builder.new(klass).build_schema.as_json
27
+ model_schema = EsquemaBuilder.send("build_#{@context}_schema", klass:)
17
28
  model_schema["required"] = []
18
29
  schema = { type: "object", properties: { klass.to_s.downcase => model_schema } }
19
30
  examples.merge!(search_for_examples_in_tests(klass:))
@@ -22,45 +33,25 @@ module OasRails
22
33
 
23
34
  # Searches for examples in test files based on the provided class and test framework.
24
35
  #
25
- # This method handles different test frameworks to fetch examples for the given class.
26
- # Currently, it supports FactoryBot and fixtures.
27
- #
28
36
  # @param klass [Class] the class to search examples for.
29
37
  # @param utils [Module] a utility module that provides the `detect_test_framework` method. Defaults to `Utils`.
30
38
  # @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)
39
+ def search_for_examples_in_tests(klass:, context: :incoming, utils: Utils)
40
+ @context = context
45
41
  case utils.detect_test_framework
46
42
  when :factory_bot
47
- {}
48
- # TODO: create examples with FactoryBot
43
+ fetch_factory_bot_examples(klass:)
49
44
  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 } } }
45
+ fetch_fixture_examples(klass:)
59
46
  else
60
47
  {}
61
48
  end
62
49
  end
63
50
 
51
+ # Transforms tags into examples.
52
+ #
53
+ # @param tags [Array] the array of tags.
54
+ # @return [Hash] the transformed examples hash.
64
55
  def tags_to_examples(tags:)
65
56
  tags.each_with_object({}).with_index(1) do |(example, result), _index|
66
57
  key = example.text.downcase.gsub(' ', '_')
@@ -71,6 +62,41 @@ module OasRails
71
62
  result[key] = value
72
63
  end
73
64
  end
65
+
66
+ private
67
+
68
+ # Fetches examples from FactoryBot for the provided class.
69
+ #
70
+ # @param klass [Class] the class to fetch examples for.
71
+ # @return [Hash] a hash containing examples data or an empty hash if no examples are found.
72
+ def fetch_factory_bot_examples(klass:)
73
+ klass_sym = klass.to_s.downcase.to_sym
74
+ begin
75
+ FactoryBot.build_stubbed_list(klass_sym, 3).each_with_index.to_h do |obj, index|
76
+ ["#{klass_sym}#{index + 1}", { value: { klass_sym => clean_example_object(obj: obj.as_json) } }]
77
+ end
78
+ rescue KeyError
79
+ {}
80
+ end
81
+ end
82
+
83
+ # Fetches examples from fixtures for the provided class.
84
+ #
85
+ # @param klass [Class] the class to fetch examples for.
86
+ # @return [Hash] a hash containing examples data or an empty hash if no examples are found.
87
+ def fetch_fixture_examples(klass:)
88
+ fixture_file = Rails.root.join('test', 'fixtures', "#{klass.to_s.pluralize.downcase}.yml")
89
+ begin
90
+ fixture_data = YAML.load_file(fixture_file).with_indifferent_access
91
+ rescue Errno::ENOENT
92
+ return {}
93
+ end
94
+ fixture_data.transform_values { |attributes| { value: { klass.to_s.downcase => clean_example_object(obj: attributes) } } }
95
+ end
96
+
97
+ def clean_example_object(obj:)
98
+ obj.reject { |key, _| OasRails.config.send("excluded_columns_#{@context}").include?(key.to_sym) }
99
+ end
74
100
  end
75
101
  end
76
102
  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
@@ -46,98 +46,5 @@ module OasRails
46
46
  klass = @controller.singularize.camelize.constantize
47
47
  RequestBody.from_model_class(klass:, required: true)
48
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
49
  end
143
50
  end
@@ -104,7 +104,7 @@ module OasRails
104
104
  responses = Responses.from_tags(tags: oas_route.docstring.tags(:response))
105
105
 
106
106
  if OasRails.config.autodiscover_responses
107
- new_responses = oas_route.extract_responses_from_source
107
+ new_responses = Extractors::RenderResponseExtractor.extract_responses_from_source(source: oas_route.source_string)
108
108
 
109
109
  new_responses.each do |new_response|
110
110
  responses.responses << new_response unless responses.responses.any? { |r| r.code == new_response.code }
@@ -8,7 +8,7 @@ module OasRails
8
8
 
9
9
  def self.from_string_paths(string_paths:)
10
10
  new(path_items: string_paths.map do |s|
11
- PathItem.from_oas_routes(path: s, oas_routes: RouteExtractor.host_routes_by_path(s))
11
+ PathItem.from_oas_routes(path: s, oas_routes: Extractors::RouteExtractor.host_routes_by_path(s))
12
12
  end)
13
13
  end
14
14
 
@@ -15,7 +15,7 @@ module OasRails
15
15
  # @return [void]
16
16
  def clear_cache
17
17
  MethodSource.clear_cache
18
- RouteExtractor.clear_cache
18
+ Extractors::RouteExtractor.clear_cache
19
19
  end
20
20
 
21
21
  def to_json(*_args)
@@ -51,7 +51,7 @@ module OasRails
51
51
  # Create the Paths Object For the Root of the OAS.
52
52
  # @see https://spec.openapis.org/oas/latest.html#paths-object
53
53
  def paths
54
- Paths.from_string_paths(string_paths: RouteExtractor.host_paths).to_spec
54
+ Paths.from_string_paths(string_paths: Extractors::RouteExtractor.host_paths).to_spec
55
55
  end
56
56
 
57
57
  # Created the Components Object For the Root of the OAS.
@@ -1,3 +1,3 @@
1
1
  module OasRails
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/oas_rails.rb CHANGED
@@ -9,7 +9,6 @@ module OasRails
9
9
  autoload :OasBase, "oas_rails/oas_base"
10
10
  autoload :Configuration, "oas_rails/configuration"
11
11
  autoload :Specification, "oas_rails/specification"
12
- autoload :RouteExtractor, "oas_rails/route_extractor"
13
12
  autoload :OasRoute, "oas_rails/oas_route"
14
13
  autoload :Operation, "oas_rails/operation"
15
14
  autoload :Info, "oas_rails/info"
@@ -26,15 +25,20 @@ module OasRails
26
25
  autoload :Responses, "oas_rails/responses"
27
26
 
28
27
  autoload :Utils, "oas_rails/utils"
28
+ autoload :EsquemaBuilder, "oas_rails/esquema_builder"
29
29
 
30
30
  module YARD
31
31
  autoload :OasYARDFactory, 'oas_rails/yard/oas_yard_factory'
32
32
  end
33
33
 
34
+ module Extractors
35
+ autoload :RenderResponseExtractor, 'oas_rails/extractors/render_response_extractor'
36
+ autoload :RouteExtractor, "oas_rails/extractors/route_extractor"
37
+ end
38
+
34
39
  class << self
35
40
  # Configurations for make the OasRails engine Work.
36
41
  def configure
37
- OasRails.configure_esquema!
38
42
  OasRails.configure_yard!
39
43
  yield config
40
44
  end
@@ -59,13 +63,5 @@ module OasRails
59
63
  ::YARD::Tags::Library.define_tag(tag_name, method_name, handler)
60
64
  end
61
65
  end
62
-
63
- def configure_esquema!
64
- Esquema.configure do |config|
65
- config.exclude_associations = true
66
- config.exclude_foreign_keys = true
67
- config.excluded_columns = %i[id created_at updated_at deleted_at]
68
- end
69
- end
70
66
  end
71
67
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oas_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - a-chacon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-01 00:00:00.000000000 Z
11
+ date: 2024-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: esquema
@@ -103,6 +103,9 @@ files:
103
103
  - lib/oas_rails/configuration.rb
104
104
  - lib/oas_rails/contact.rb
105
105
  - lib/oas_rails/engine.rb
106
+ - lib/oas_rails/esquema_builder.rb
107
+ - lib/oas_rails/extractors/render_response_extractor.rb
108
+ - lib/oas_rails/extractors/route_extractor.rb
106
109
  - lib/oas_rails/info.rb
107
110
  - lib/oas_rails/license.rb
108
111
  - lib/oas_rails/media_type.rb
@@ -115,7 +118,6 @@ files:
115
118
  - lib/oas_rails/request_body.rb
116
119
  - lib/oas_rails/response.rb
117
120
  - lib/oas_rails/responses.rb
118
- - lib/oas_rails/route_extractor.rb
119
121
  - lib/oas_rails/server.rb
120
122
  - lib/oas_rails/specification.rb
121
123
  - lib/oas_rails/tag.rb
@@ -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