oas_rails 0.12.0 → 0.14.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.
- checksums.yaml +4 -4
- data/README.md +13 -1
- data/lib/generators/oas_rails/config/templates/oas_rails_initializer.rb +5 -2
- data/lib/oas_rails/active_record_example_finder.rb +65 -0
- data/lib/oas_rails/builders/content_builder.rb +2 -2
- data/lib/oas_rails/builders/oas_route_builder.rb +90 -0
- data/lib/oas_rails/builders/parameters_builder.rb +1 -1
- data/lib/oas_rails/builders/request_body_builder.rb +3 -3
- data/lib/oas_rails/builders/responses_builder.rb +34 -14
- data/lib/oas_rails/configuration.rb +26 -4
- data/lib/oas_rails/extractors/oas_route_extractor.rb +4 -4
- data/lib/oas_rails/extractors/render_response_extractor.rb +3 -3
- data/lib/oas_rails/extractors/route_extractor.rb +3 -3
- data/lib/oas_rails/json_schema_generator.rb +4 -0
- data/lib/oas_rails/oas_route.rb +9 -35
- data/lib/oas_rails/spec/media_type.rb +0 -65
- data/lib/oas_rails/utils.rb +12 -13
- data/lib/oas_rails/version.rb +1 -1
- data/lib/oas_rails/yard/oas_rails_factory.rb +1 -11
- data/lib/oas_rails.rb +2 -0
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 041c5783e2bbbedca71601a21756f2d90398bed275866c6214935965a47766f8
|
4
|
+
data.tar.gz: 4ea6110ed149c0d90b2c47e980f06abb8ef9c6efad7aa35699e23a8e95017fb6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd3d0eb79205462ca026002e5bbb64898704d1c2576320568a5332cb50dccf5e7044a7cc12aab06344d583b4d6a7aa0cdf6901f2a9ddbd0509ca305ef3f9dd96
|
7
|
+
data.tar.gz: fcaf3d373fed62a411de505665b6f1c288834902d009fc0d5c1d399693a0a7011b00e3df77a2fce6c77d10113044430e9ffe03ee87400976e8d9bdaca23fa631
|
data/README.md
CHANGED
@@ -9,7 +9,15 @@
|
|
9
9
|
|
10
10
|
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)**.
|
11
11
|
|
12
|
-
|
12
|
+
### 🚀 Demo App
|
13
|
+
|
14
|
+
Explore the interactive documentation live:
|
15
|
+
|
16
|
+
🔗 **[Open Demo App](https://paso.fly.dev/api/docs)**
|
17
|
+
👤 **Username**: `oasrails`
|
18
|
+
🔑 **Password**: `oasrails`
|
19
|
+
|
20
|
+
🎬 A Demo Installation/Usage Video:
|
13
21
|
<https://vimeo.com/1013687332>
|
14
22
|
🎬
|
15
23
|
|
@@ -56,3 +64,7 @@ If you plan a big feature, first open an issue to discuss it before any developm
|
|
56
64
|
## License
|
57
65
|
|
58
66
|
The gem is available as open source under the terms of the [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html#license-text).
|
67
|
+
|
68
|
+
## Star History
|
69
|
+
|
70
|
+
[](https://www.star-history.com/#a-chacon/oas_rails&Date)
|
@@ -26,6 +26,8 @@ OasRails.configure do |config|
|
|
26
26
|
|
27
27
|
3. Use Yard tags in your controller methods to provide detailed API endpoint descriptions.
|
28
28
|
|
29
|
+
Docs: <https://a-chacon.com/oas_rails/>
|
30
|
+
|
29
31
|
## Features
|
30
32
|
|
31
33
|
- Automatic OAS 3.1 document generation
|
@@ -105,6 +107,7 @@ OasRails.configure do |config|
|
|
105
107
|
# Example, if you add forbidden then it will be added only if the endpoint requires authentication.
|
106
108
|
# Example: not_found will be setted to the endpoint only if the operation is a show/update/destroy action.
|
107
109
|
# config.set_default_responses = true
|
108
|
-
# config.possible_default_responses = [:not_found, :unauthorized, :forbidden]
|
109
|
-
# config.response_body_of_default = { message: String }
|
110
|
+
# config.possible_default_responses = [:not_found, :unauthorized, :forbidden, :internal_server_error, :unprocessable_entity]
|
111
|
+
# config.response_body_of_default = "Hash{ message: String }"
|
112
|
+
# config.response_body_of_unprocessable_entity= "Hash{ errors: Array<String> }"
|
110
113
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module OasRails
|
2
|
+
class ActiveRecordExampleFinder
|
3
|
+
def initialize(context: :incoming, utils: Utils, factory_bot: FactoryBot, erb: ERB, yaml: YAML, file: File)
|
4
|
+
@context = context
|
5
|
+
@utils = utils
|
6
|
+
@factory_bot = factory_bot
|
7
|
+
@erb = erb
|
8
|
+
@yaml = yaml
|
9
|
+
@file = file
|
10
|
+
@factory_examples = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def search(klass)
|
14
|
+
case @utils.detect_test_framework
|
15
|
+
when :factory_bot
|
16
|
+
fetch_factory_bot_examples(klass: klass)
|
17
|
+
when :fixtures
|
18
|
+
fetch_fixture_examples(klass: klass)
|
19
|
+
else
|
20
|
+
{}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Fetches examples from FactoryBot for the provided class.
|
25
|
+
#
|
26
|
+
# @param klass [Class] the class to fetch examples for.
|
27
|
+
# @return [Hash] a hash containing examples data or an empty hash if no examples are found.
|
28
|
+
def fetch_factory_bot_examples(klass:)
|
29
|
+
klass_sym = @utils.class_to_symbol(klass)
|
30
|
+
|
31
|
+
begin
|
32
|
+
@factory_examples[klass_sym] = @factory_bot.build_stubbed_list(klass_sym, 1) if @factory_examples[klass_sym].nil?
|
33
|
+
|
34
|
+
@factory_examples[klass_sym].each_with_index.to_h do |obj, index|
|
35
|
+
["#{klass_sym}#{index + 1}", { value: { klass_sym => clean_example_object(obj: obj.as_json) } }]
|
36
|
+
end.deep_symbolize_keys
|
37
|
+
rescue KeyError
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Fetches examples from fixtures for the provided class.
|
43
|
+
#
|
44
|
+
# @param klass [Class] the class to fetch examples for.
|
45
|
+
# @return [Hash] a hash containing examples data or an empty hash if no examples are found.
|
46
|
+
def fetch_fixture_examples(klass:)
|
47
|
+
fixture_file = Rails.root.join('test', 'fixtures', "#{klass.to_s.pluralize.downcase}.yml")
|
48
|
+
begin
|
49
|
+
erb_result = @erb.new(@file.read(fixture_file)).result
|
50
|
+
fixture_data = @yaml.safe_load(
|
51
|
+
erb_result,
|
52
|
+
aliases: true,
|
53
|
+
permitted_classes: [Symbol, ActiveSupport::HashWithIndifferentAccess, Time]
|
54
|
+
).with_indifferent_access
|
55
|
+
rescue Errno::ENOENT
|
56
|
+
return {}
|
57
|
+
end
|
58
|
+
fixture_data.transform_values { |attributes| { value: { klass.to_s.downcase => clean_example_object(obj: attributes) } } }.deep_symbolize_keys
|
59
|
+
end
|
60
|
+
|
61
|
+
def clean_example_object(obj:)
|
62
|
+
obj.reject { |key, _| OasRails.config.send("excluded_columns_#{@context}").include?(key.to_sym) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -33,12 +33,12 @@ module OasRails
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def from_model_class(klass)
|
36
|
-
return self unless
|
36
|
+
return self unless Utils.active_record_class?(klass)
|
37
37
|
|
38
38
|
model_schema = Builders::EsquemaBuilder.send("build_#{@context}_schema", klass:)
|
39
39
|
model_schema["required"] = []
|
40
40
|
schema = { type: "object", properties: { klass.to_s.downcase => model_schema } }
|
41
|
-
examples =
|
41
|
+
examples = ActiveRecordExampleFinder.new(context: @context).search(klass)
|
42
42
|
@media_type.schema = @specification.components.add_schema(schema)
|
43
43
|
@media_type.examples = @media_type.examples.merge(examples)
|
44
44
|
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module OasRails
|
2
|
+
module Builders
|
3
|
+
class OasRouteBuilder
|
4
|
+
def self.build_from_rails_route(rails_route)
|
5
|
+
new(rails_route).build
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(rails_route)
|
9
|
+
@rails_route = rails_route
|
10
|
+
end
|
11
|
+
|
12
|
+
def build
|
13
|
+
OasRoute.new(
|
14
|
+
controller_class: controller_class,
|
15
|
+
controller_action: controller_action,
|
16
|
+
controller: controller,
|
17
|
+
controller_path: controller_path,
|
18
|
+
method: method,
|
19
|
+
verb: verb,
|
20
|
+
path: path,
|
21
|
+
rails_route: @rails_route,
|
22
|
+
docstring: docstring,
|
23
|
+
source_string: source_string,
|
24
|
+
tags: tags
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def controller_class
|
31
|
+
"#{@rails_route.defaults[:controller].camelize}Controller"
|
32
|
+
end
|
33
|
+
|
34
|
+
def controller_action
|
35
|
+
"#{controller_class}##{@rails_route.defaults[:action]}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def controller
|
39
|
+
@rails_route.defaults[:controller]
|
40
|
+
end
|
41
|
+
|
42
|
+
def controller_path
|
43
|
+
Rails.root.join("app/controllers/#{controller}_controller.rb").to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def method
|
47
|
+
@rails_route.defaults[:action]
|
48
|
+
end
|
49
|
+
|
50
|
+
def verb
|
51
|
+
@rails_route.verb
|
52
|
+
end
|
53
|
+
|
54
|
+
def path
|
55
|
+
Extractors::RouteExtractor.clean_route(@rails_route.path.spec.to_s)
|
56
|
+
end
|
57
|
+
|
58
|
+
def source_string
|
59
|
+
controller_class.constantize.instance_method(method).source
|
60
|
+
end
|
61
|
+
|
62
|
+
def docstring
|
63
|
+
comment_lines = controller_class.constantize.instance_method(method).comment.lines
|
64
|
+
processed_lines = comment_lines.map { |line| line.sub(/^# /, '') }
|
65
|
+
|
66
|
+
filtered_lines = processed_lines.reject do |line|
|
67
|
+
line.include?('rubocop') ||
|
68
|
+
line.include?('TODO')
|
69
|
+
end
|
70
|
+
|
71
|
+
::YARD::Docstring.parser.parse(filtered_lines.join).to_docstring
|
72
|
+
end
|
73
|
+
|
74
|
+
def tags
|
75
|
+
method_comment = controller_class.constantize.instance_method(method).comment
|
76
|
+
class_comment = controller_class.constantize.instance_method(method).class_comment
|
77
|
+
|
78
|
+
method_tags = parse_tags(method_comment)
|
79
|
+
class_tags = parse_tags(class_comment)
|
80
|
+
|
81
|
+
method_tags + class_tags
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_tags(comment)
|
85
|
+
lines = comment.lines.map { |line| line.sub(/^# /, '') }
|
86
|
+
::YARD::Docstring.parser.parse(lines.join).tags
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -7,7 +7,7 @@ module OasRails
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def from_oas_route(oas_route)
|
10
|
-
parameters_from_tags(tags: oas_route.
|
10
|
+
parameters_from_tags(tags: oas_route.tags(:parameter))
|
11
11
|
oas_route.path_params.try(:map) do |p|
|
12
12
|
@parameters << ParameterBuilder.new(@specification).from_path(oas_route.path, p).build unless @parameters.any? { |param| param.name.to_s == p.to_s }
|
13
13
|
end
|
@@ -7,18 +7,18 @@ module OasRails
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def from_oas_route(oas_route)
|
10
|
-
tag_request_body = oas_route.
|
10
|
+
tag_request_body = oas_route.tags(:request_body).first
|
11
11
|
if tag_request_body.nil? && OasRails.config.autodiscover_request_body
|
12
12
|
detect_request_body(oas_route) if %w[create update].include? oas_route.method
|
13
13
|
elsif !tag_request_body.nil?
|
14
|
-
from_tags(tag: tag_request_body, examples_tags: oas_route.
|
14
|
+
from_tags(tag: tag_request_body, examples_tags: oas_route.tags(:request_body_example))
|
15
15
|
end
|
16
16
|
|
17
17
|
self
|
18
18
|
end
|
19
19
|
|
20
20
|
def from_tags(tag:, examples_tags: [])
|
21
|
-
if tag.klass
|
21
|
+
if Utils.active_record_class?(tag.klass)
|
22
22
|
from_model_class(klass: tag.klass, description: tag.text, required: tag.required, examples_tags:)
|
23
23
|
else
|
24
24
|
@request_body.description = tag.text
|
@@ -7,8 +7,8 @@ module OasRails
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def from_oas_route(oas_route)
|
10
|
-
oas_route.
|
11
|
-
content = ContentBuilder.new(@specification, :outgoing).with_schema(tag.schema).with_examples_from_tags(oas_route.
|
10
|
+
oas_route.tags(:response).each do |tag|
|
11
|
+
content = ContentBuilder.new(@specification, :outgoing).with_schema(tag.schema).with_examples_from_tags(oas_route.tags(:response_example).filter { |re| re.code == tag.name }).build
|
12
12
|
response = ResponseBuilder.new(@specification).with_code(tag.name.to_i).with_description(tag.text).with_content(content).build
|
13
13
|
|
14
14
|
@responses.add_response(response)
|
@@ -18,7 +18,7 @@ module OasRails
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def add_autodiscovered_responses(oas_route)
|
21
|
-
return self if !OasRails.config.autodiscover_responses || oas_route.
|
21
|
+
return self if !OasRails.config.autodiscover_responses || oas_route.tags(:response).any?
|
22
22
|
|
23
23
|
new_responses = Extractors::RenderResponseExtractor.extract_responses_from_source(@specification, source: oas_route.source_string)
|
24
24
|
|
@@ -32,29 +32,49 @@ module OasRails
|
|
32
32
|
def add_default_responses(oas_route, security)
|
33
33
|
return self unless OasRails.config.set_default_responses
|
34
34
|
|
35
|
-
|
35
|
+
common_errors = determine_common_errors(oas_route, security)
|
36
|
+
add_responses_for_errors(common_errors)
|
37
|
+
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def build
|
42
|
+
@responses
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def determine_common_errors(oas_route, security)
|
36
48
|
common_errors = []
|
37
|
-
common_errors.push(:unauthorized, :forbidden) if security
|
49
|
+
common_errors.push(:unauthorized, :forbidden, :internal_server_error) if security
|
38
50
|
|
39
51
|
case oas_route.method
|
40
|
-
when "show", "
|
52
|
+
when "show", "destroy"
|
41
53
|
common_errors.push(:not_found)
|
42
|
-
when "create"
|
43
|
-
|
54
|
+
when "create"
|
55
|
+
common_errors.push(:unprocessable_entity)
|
56
|
+
when "update"
|
57
|
+
common_errors.push(:not_found, :unprocessable_entity)
|
44
58
|
end
|
45
59
|
|
46
|
-
|
47
|
-
|
60
|
+
OasRails.config.possible_default_responses & common_errors
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_responses_for_errors(errors)
|
64
|
+
errors.each do |error|
|
65
|
+
response_body = resolve_response_body(error)
|
66
|
+
content = ContentBuilder.new(@specification, :outgoing).with_schema(JsonSchemaGenerator.process_string(response_body)[:json_schema]).build
|
67
|
+
code = Utils.status_to_integer(error)
|
48
68
|
response = ResponseBuilder.new(@specification).with_code(code).with_description(Utils.get_definition(code)).with_content(content).build
|
49
69
|
|
50
70
|
@responses.add_response(response) if @responses.responses[response.code].blank?
|
51
71
|
end
|
52
|
-
|
53
|
-
self
|
54
72
|
end
|
55
73
|
|
56
|
-
def
|
57
|
-
|
74
|
+
def resolve_response_body(error)
|
75
|
+
OasRails.config.public_send("response_body_of_#{error}")
|
76
|
+
rescue StandardError
|
77
|
+
OasRails.config.response_body_of_default
|
58
78
|
end
|
59
79
|
end
|
60
80
|
end
|
@@ -11,12 +11,11 @@ module OasRails
|
|
11
11
|
:authenticate_all_routes_by_default,
|
12
12
|
:set_default_responses,
|
13
13
|
:possible_default_responses,
|
14
|
-
:response_body_of_default,
|
15
14
|
:http_verbs,
|
16
15
|
:use_model_names,
|
17
16
|
:rapidoc_theme
|
18
17
|
|
19
|
-
attr_reader :servers, :tags, :security_schema, :include_mode
|
18
|
+
attr_reader :servers, :tags, :security_schema, :include_mode, :response_body_of_default
|
20
19
|
|
21
20
|
def initialize
|
22
21
|
@info = Spec::Info.new
|
@@ -33,12 +32,28 @@ module OasRails
|
|
33
32
|
@security_schema = nil
|
34
33
|
@security_schemas = {}
|
35
34
|
@set_default_responses = true
|
36
|
-
@possible_default_responses = [:not_found, :unauthorized, :forbidden]
|
35
|
+
@possible_default_responses = [:not_found, :unauthorized, :forbidden, :internal_server_error, :unprocessable_entity]
|
37
36
|
@http_verbs = [:get, :post, :put, :patch, :delete]
|
38
|
-
@response_body_of_default = "Hash{
|
37
|
+
@response_body_of_default = "Hash{ status: !Integer, error: String }"
|
39
38
|
@use_model_names = false
|
40
39
|
@rapidoc_theme = :rails
|
41
40
|
@include_mode = :all
|
41
|
+
|
42
|
+
@possible_default_responses.each do |response|
|
43
|
+
method_name = "response_body_of_#{response}="
|
44
|
+
variable_name = "@response_body_of_#{response}"
|
45
|
+
|
46
|
+
define_singleton_method(method_name) do |value|
|
47
|
+
raise ArgumentError, "#{method_name} must be a String With a valid object" unless value.is_a?(String)
|
48
|
+
|
49
|
+
OasRails::JsonSchemaGenerator.parse_type(value)
|
50
|
+
instance_variable_set(variable_name, value)
|
51
|
+
end
|
52
|
+
|
53
|
+
define_singleton_method("response_body_of_#{response}") do
|
54
|
+
instance_variable_get(variable_name) || @response_body_of_default
|
55
|
+
end
|
56
|
+
end
|
42
57
|
end
|
43
58
|
|
44
59
|
def security_schema=(value)
|
@@ -73,6 +88,13 @@ module OasRails
|
|
73
88
|
|
74
89
|
@include_mode = value
|
75
90
|
end
|
91
|
+
|
92
|
+
def response_body_of_default=(value)
|
93
|
+
raise ArgumentError, "response_body_of_default must be a String With a valid object" unless value.is_a?(String)
|
94
|
+
|
95
|
+
OasRails::JsonSchemaGenerator.parse_type(value)
|
96
|
+
@response_body_of_default = value
|
97
|
+
end
|
76
98
|
end
|
77
99
|
|
78
100
|
DEFAULT_SECURITY_SCHEMES = {
|
@@ -2,7 +2,7 @@ module OasRails
|
|
2
2
|
module Extractors
|
3
3
|
module OasRouteExtractor
|
4
4
|
def extract_summary(oas_route:)
|
5
|
-
oas_route.
|
5
|
+
oas_route.tags(:summary).first.try(:text) || generate_crud_name(oas_route.method, oas_route.controller.downcase) || "#{oas_route.verb} #{oas_route.path}"
|
6
6
|
end
|
7
7
|
|
8
8
|
def extract_operation_id(oas_route:)
|
@@ -10,7 +10,7 @@ module OasRails
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def extract_tags(oas_route:)
|
13
|
-
tags = oas_route.
|
13
|
+
tags = oas_route.tags(:tags).first
|
14
14
|
if tags.nil?
|
15
15
|
default_tags(oas_route:)
|
16
16
|
else
|
@@ -30,9 +30,9 @@ module OasRails
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def extract_security(oas_route:)
|
33
|
-
return [] if oas_route.
|
33
|
+
return [] if oas_route.tags(:no_auth).any?
|
34
34
|
|
35
|
-
if (methods = oas_route.
|
35
|
+
if (methods = oas_route.tags(:auth).first)
|
36
36
|
OasRails.config.security_schemas.keys.map { |key| { key => [] } }.select do |schema|
|
37
37
|
methods.types.include?(schema.keys.first.to_s)
|
38
38
|
end
|
@@ -60,7 +60,7 @@ module OasRails
|
|
60
60
|
maybe_a_model, errors = content.gsub('@', "").split(".")
|
61
61
|
klass = maybe_a_model.singularize.camelize(:upper).constantize
|
62
62
|
|
63
|
-
if
|
63
|
+
if Utils.active_record_class?(klass)
|
64
64
|
schema = Builders::EsquemaBuilder.build_outgoing_schema(klass:)
|
65
65
|
if test_singularity(maybe_a_model)
|
66
66
|
build_singular_model_schema_and_examples(maybe_a_model, errors, klass, schema)
|
@@ -81,7 +81,7 @@ module OasRails
|
|
81
81
|
# @return [Array<Hash, Hash>] An array where the first element is the schema and the second is the examples.
|
82
82
|
def build_singular_model_schema_and_examples(_maybe_a_model, errors, klass, schema)
|
83
83
|
if errors.nil?
|
84
|
-
[schema,
|
84
|
+
[schema, ActiveRecordExampleFinder.new(context: :outgoing).search(klass)]
|
85
85
|
else
|
86
86
|
# TODO: this is not building the real schema.
|
87
87
|
[
|
@@ -110,7 +110,7 @@ module OasRails
|
|
110
110
|
# @param schema [Hash] The schema for the model.
|
111
111
|
# @return [Array<Hash, Hash>] An array where the first element is the schema and the second is the examples.
|
112
112
|
def build_array_model_schema_and_examples(maybe_a_model, klass, schema)
|
113
|
-
examples = { maybe_a_model => { value:
|
113
|
+
examples = { maybe_a_model => { value: ActiveRecordExampleFinder.new(context: :outgoing).search(klass).values.map { |p| p.dig(:value, maybe_a_model.singularize.to_sym) } } }
|
114
114
|
[{ type: "array", items: schema }, examples]
|
115
115
|
end
|
116
116
|
|
@@ -48,10 +48,10 @@ module OasRails
|
|
48
48
|
private
|
49
49
|
|
50
50
|
def extract_host_routes
|
51
|
-
routes = valid_routes.map { |r|
|
51
|
+
routes = valid_routes.map { |r| Builders::OasRouteBuilder.build_from_rails_route(r) }
|
52
52
|
|
53
|
-
routes.select! { |route| route.
|
54
|
-
routes.select! { |route| route.
|
53
|
+
routes.select! { |route| route.tags.any? } if OasRails.config.include_mode == :with_tags
|
54
|
+
routes.select! { |route| route.tags.any? { |t| t.tag_name == "oas_include" } } if OasRails.config.include_mode == :explicit
|
55
55
|
routes
|
56
56
|
end
|
57
57
|
|
@@ -29,6 +29,8 @@ module OasRails
|
|
29
29
|
{ type: :object, required:, properties: parse_object_properties(::Regexp.last_match(1)) }
|
30
30
|
when /^Array<(.+)>$/i
|
31
31
|
{ type: :array, required:, items: parse_type(::Regexp.last_match(1)) }
|
32
|
+
when ->(t) { Utils.active_record_class?(t) }
|
33
|
+
Builders::EsquemaBuilder.build_outgoing_schema(klass: type.constantize)
|
32
34
|
else
|
33
35
|
{ type: type.downcase.to_sym, required: }
|
34
36
|
end
|
@@ -100,6 +102,8 @@ module OasRails
|
|
100
102
|
type: 'array',
|
101
103
|
items: to_json_schema(parsed[:items])
|
102
104
|
}
|
105
|
+
when nil
|
106
|
+
parsed
|
103
107
|
else
|
104
108
|
ruby_type_to_json_schema_type(parsed[:type])
|
105
109
|
end
|
data/lib/oas_rails/oas_route.rb
CHANGED
@@ -1,47 +1,21 @@
|
|
1
1
|
module OasRails
|
2
2
|
class OasRoute
|
3
|
-
attr_accessor
|
4
|
-
:rails_route, :docstring, :source_string
|
3
|
+
attr_accessor :controller_class, :controller_action, :controller, :controller_path, :method, :verb, :path,
|
4
|
+
:rails_route, :docstring, :source_string
|
5
|
+
attr_writer :tags
|
5
6
|
|
6
|
-
def initialize
|
7
|
-
|
8
|
-
def self.new_from_rails_route(rails_route: ActionDispatch::Journey::Route)
|
9
|
-
instance = new
|
10
|
-
instance.rails_route = rails_route
|
11
|
-
instance.extract_rails_route_data
|
12
|
-
instance
|
13
|
-
end
|
14
|
-
|
15
|
-
def extract_rails_route_data
|
16
|
-
@controller_action = "#{@rails_route.defaults[:controller].camelize}Controller##{@rails_route.defaults[:action]}"
|
17
|
-
@controller_class = "#{@rails_route.defaults[:controller].camelize}Controller"
|
18
|
-
@controller = @rails_route.defaults[:controller]
|
19
|
-
@controller_path = controller_path_extractor(@rails_route.defaults[:controller])
|
20
|
-
@method = @rails_route.defaults[:action]
|
21
|
-
@verb = @rails_route.verb
|
22
|
-
@path = Extractors::RouteExtractor.clean_route(@rails_route.path.spec.to_s)
|
23
|
-
@docstring = extract_docstring
|
24
|
-
@source_string = extract_source_string
|
25
|
-
end
|
26
|
-
|
27
|
-
def extract_docstring
|
28
|
-
comment_lines = controller_class.constantize.instance_method(method).comment.lines
|
29
|
-
processed_lines = comment_lines.map do |line|
|
30
|
-
line.sub(/^# /, '')
|
31
|
-
end
|
32
|
-
::YARD::Docstring.parser.parse(processed_lines.join).to_docstring
|
33
|
-
end
|
34
|
-
|
35
|
-
def extract_source_string
|
36
|
-
@controller_class.constantize.instance_method(method).source
|
7
|
+
def initialize(attributes = {})
|
8
|
+
attributes.each { |key, value| send("#{key}=", value) }
|
37
9
|
end
|
38
10
|
|
39
11
|
def path_params
|
40
12
|
@rails_route.path.spec.to_s.scan(/:(\w+)/).flatten.reject! { |e| e == 'format' }
|
41
13
|
end
|
42
14
|
|
43
|
-
def
|
44
|
-
|
15
|
+
def tags(name = nil)
|
16
|
+
return @tags if name.nil?
|
17
|
+
|
18
|
+
@tags.select { |tag| tag.tag_name.to_s == name.to_s }
|
45
19
|
end
|
46
20
|
end
|
47
21
|
end
|
@@ -5,9 +5,6 @@ module OasRails
|
|
5
5
|
|
6
6
|
attr_accessor :schema, :example, :examples, :encoding
|
7
7
|
|
8
|
-
@context = :incoming
|
9
|
-
@factory_examples = {}
|
10
|
-
|
11
8
|
# Initializes a new MediaType object.
|
12
9
|
#
|
13
10
|
# @param schema [Hash] the schema of the media type.
|
@@ -22,68 +19,6 @@ module OasRails
|
|
22
19
|
def oas_fields
|
23
20
|
[:schema, :example, :examples, :encoding]
|
24
21
|
end
|
25
|
-
|
26
|
-
class << self
|
27
|
-
# Searches for examples in test files based on the provided class and test framework.
|
28
|
-
#
|
29
|
-
# @param klass [Class] the class to search examples for.
|
30
|
-
# @param utils [Module] a utility module that provides the `detect_test_framework` method. Defaults to `Utils`.
|
31
|
-
# @return [Hash] a hash containing examples data or an empty hash if no examples are found.
|
32
|
-
def search_for_examples_in_tests(klass, context: :incoming, utils: Utils)
|
33
|
-
@context = context
|
34
|
-
case utils.detect_test_framework
|
35
|
-
when :factory_bot
|
36
|
-
fetch_factory_bot_examples(klass:)
|
37
|
-
when :fixtures
|
38
|
-
fetch_fixture_examples(klass:)
|
39
|
-
else
|
40
|
-
{}
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
# Fetches examples from FactoryBot for the provided class.
|
47
|
-
#
|
48
|
-
# @param klass [Class] the class to fetch examples for.
|
49
|
-
# @return [Hash] a hash containing examples data or an empty hash if no examples are found.
|
50
|
-
def fetch_factory_bot_examples(klass:)
|
51
|
-
klass_sym = Utils.class_to_symbol(klass)
|
52
|
-
|
53
|
-
begin
|
54
|
-
@factory_examples[klass_sym] = FactoryBot.build_stubbed_list(klass_sym, 1) if @factory_examples[klass_sym].nil?
|
55
|
-
|
56
|
-
@factory_examples[klass_sym].each_with_index.to_h do |obj, index|
|
57
|
-
["#{klass_sym}#{index + 1}", { value: { klass_sym => clean_example_object(obj: obj.as_json) } }]
|
58
|
-
end
|
59
|
-
rescue KeyError
|
60
|
-
{}
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# Fetches examples from fixtures for the provided class.
|
65
|
-
#
|
66
|
-
# @param klass [Class] the class to fetch examples for.
|
67
|
-
# @return [Hash] a hash containing examples data or an empty hash if no examples are found.
|
68
|
-
def fetch_fixture_examples(klass:)
|
69
|
-
fixture_file = Rails.root.join('test', 'fixtures', "#{klass.to_s.pluralize.downcase}.yml")
|
70
|
-
begin
|
71
|
-
erb_result = ERB.new(File.read(fixture_file)).result
|
72
|
-
fixture_data = YAML.safe_load(
|
73
|
-
erb_result,
|
74
|
-
aliases: true,
|
75
|
-
permitted_classes: [Symbol, ActiveSupport::HashWithIndifferentAccess, Time]
|
76
|
-
).with_indifferent_access
|
77
|
-
rescue Errno::ENOENT
|
78
|
-
return {}
|
79
|
-
end
|
80
|
-
fixture_data.transform_values { |attributes| { value: { klass.to_s.downcase => clean_example_object(obj: attributes) } } }
|
81
|
-
end
|
82
|
-
|
83
|
-
def clean_example_object(obj:)
|
84
|
-
obj.reject { |key, _| OasRails.config.send("excluded_columns_#{@context}").include?(key.to_sym) }
|
85
|
-
end
|
86
|
-
end
|
87
22
|
end
|
88
23
|
end
|
89
24
|
end
|
data/lib/oas_rails/utils.rb
CHANGED
@@ -14,6 +14,8 @@ module OasRails
|
|
14
14
|
}.freeze
|
15
15
|
|
16
16
|
HTTP_STATUS_DEFINITIONS = {
|
17
|
+
200 => "The request has succeeded.",
|
18
|
+
201 => "The request has been fulfilled and resulted in a new resource being created.",
|
17
19
|
404 => "The requested resource could not be found.",
|
18
20
|
401 => "You are not authorized to access this resource. You need to authenticate yourself first.",
|
19
21
|
403 => "You are not allowed to access this resource. You do not have the necessary permissions.",
|
@@ -34,19 +36,6 @@ module OasRails
|
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
37
|
-
# TODO: check if it is in use
|
38
|
-
def type_to_schema(type_string)
|
39
|
-
if type_string.start_with?('Array<')
|
40
|
-
inner_type = type_string[/Array<(.+)>$/, 1]
|
41
|
-
{
|
42
|
-
"type" => "array",
|
43
|
-
"items" => type_to_schema(inner_type)
|
44
|
-
}
|
45
|
-
else
|
46
|
-
{ "type" => TYPE_MAPPING.fetch(type_string, 'string') }
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
39
|
def hash_to_json_schema(hash)
|
51
40
|
{
|
52
41
|
type: 'object',
|
@@ -116,6 +105,16 @@ module OasRails
|
|
116
105
|
|
117
106
|
nil # Return nil if no matching constant is found
|
118
107
|
end
|
108
|
+
|
109
|
+
# Checks if a given text refers to an ActiveRecord class.
|
110
|
+
# @param text [String] The text to check.
|
111
|
+
# @return [Boolean] True if the text refers to an ActiveRecord class, false otherwise.
|
112
|
+
def active_record_class?(klass_or_string)
|
113
|
+
klass = klass_or_string.is_a?(Class) ? klass_or_string : klass_or_string.constantize
|
114
|
+
klass.ancestors.map(&:to_s).include? 'ActiveRecord::Base'
|
115
|
+
rescue StandardError
|
116
|
+
false
|
117
|
+
end
|
119
118
|
end
|
120
119
|
end
|
121
120
|
end
|
data/lib/oas_rails/version.rb
CHANGED
@@ -147,23 +147,13 @@ module OasRails
|
|
147
147
|
match
|
148
148
|
end
|
149
149
|
|
150
|
-
# Checks if a given text refers to an ActiveRecord class.
|
151
|
-
# @param text [String] The text to check.
|
152
|
-
# @return [Boolean] True if the text refers to an ActiveRecord class, false otherwise.
|
153
|
-
def active_record_class?(text)
|
154
|
-
klass = text.constantize
|
155
|
-
klass.ancestors.map(&:to_s).include? 'ActiveRecord::Base'
|
156
|
-
rescue StandardError
|
157
|
-
false
|
158
|
-
end
|
159
|
-
|
160
150
|
# Converts type text to a schema, checking if it's an ActiveRecord class.
|
161
151
|
# @param text [String] The type text to convert.
|
162
152
|
# @return [Array] An array containing the class, schema, and required flag.
|
163
153
|
def type_text_to_schema(text)
|
164
154
|
type_text, required = text_and_required(text)
|
165
155
|
|
166
|
-
if active_record_class?(type_text)
|
156
|
+
if Utils.active_record_class?(type_text)
|
167
157
|
klass = type_text.constantize
|
168
158
|
schema = Builders::EsquemaBuilder.build_outgoing_schema(klass:)
|
169
159
|
else
|
data/lib/oas_rails.rb
CHANGED
@@ -10,6 +10,7 @@ module OasRails
|
|
10
10
|
autoload :OasRoute, "oas_rails/oas_route"
|
11
11
|
autoload :Utils, "oas_rails/utils"
|
12
12
|
autoload :JsonSchemaGenerator, "oas_rails/json_schema_generator"
|
13
|
+
autoload :ActiveRecordExampleFinder, "oas_rails/active_record_example_finder"
|
13
14
|
|
14
15
|
module Builders
|
15
16
|
autoload :OperationBuilder, "oas_rails/builders/operation_builder"
|
@@ -21,6 +22,7 @@ module OasRails
|
|
21
22
|
autoload :ParameterBuilder, "oas_rails/builders/parameter_builder"
|
22
23
|
autoload :RequestBodyBuilder, "oas_rails/builders/request_body_builder"
|
23
24
|
autoload :EsquemaBuilder, "oas_rails/builders/esquema_builder"
|
25
|
+
autoload :OasRouteBuilder, "oas_rails/builders/oas_route_builder"
|
24
26
|
end
|
25
27
|
|
26
28
|
# This module contains all the clases that represent a part of the OAS file.
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: oas_rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.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: 2025-
|
11
|
+
date: 2025-05-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: easy_talk_two
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 1.1.2
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 1.1.2
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: method_source
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,8 +80,10 @@ files:
|
|
80
80
|
- lib/generators/oas_rails/config/config_generator.rb
|
81
81
|
- lib/generators/oas_rails/config/templates/oas_rails_initializer.rb
|
82
82
|
- lib/oas_rails.rb
|
83
|
+
- lib/oas_rails/active_record_example_finder.rb
|
83
84
|
- lib/oas_rails/builders/content_builder.rb
|
84
85
|
- lib/oas_rails/builders/esquema_builder.rb
|
86
|
+
- lib/oas_rails/builders/oas_route_builder.rb
|
85
87
|
- lib/oas_rails/builders/operation_builder.rb
|
86
88
|
- lib/oas_rails/builders/parameter_builder.rb
|
87
89
|
- lib/oas_rails/builders/parameters_builder.rb
|