oas_rails 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +91 -61
- data/app/controllers/oas_rails/oas_rails_controller.rb +2 -0
- data/app/views/layouts/oas_rails/application.html.erb +0 -16
- data/app/views/oas_rails/oas_rails/index.html.erb +27 -1
- data/lib/oas_rails/configuration.rb +8 -0
- data/lib/oas_rails/engine.rb +5 -0
- data/lib/oas_rails/esquema_builder.rb +37 -0
- data/lib/oas_rails/extractors/render_response_extractor.rb +173 -0
- data/lib/oas_rails/extractors/route_extractor.rb +125 -0
- data/lib/oas_rails/media_type.rb +57 -31
- data/lib/oas_rails/oas_route.rb +2 -95
- data/lib/oas_rails/operation.rb +1 -1
- data/lib/oas_rails/paths.rb +1 -1
- data/lib/oas_rails/specification.rb +13 -3
- data/lib/oas_rails/version.rb +1 -1
- data/lib/oas_rails/yard/oas_yard_factory.rb +2 -2
- data/lib/oas_rails.rb +11 -12
- metadata +5 -3
- data/lib/oas_rails/route_extractor.rb +0 -111
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9e40d4f8c810e300bbccaf190fd5143e5be804d2d30f1e86393e0478055965f
|
4
|
+
data.tar.gz: 0b6de20a5280115b3550d950da6e26d8424f4d52cc15d6f46f5272224831904e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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://
|
9
|
+
![Screenshot](https://a-chacon.com/assets/images/oas_rails_ui.png)
|
10
10
|
|
11
11
|
## Related Projects
|
12
12
|
|
@@ -30,6 +30,24 @@ After experiencing the interactive documentation in Python's fast-api framework,
|
|
30
30
|
|
31
31
|
The goal is to minimize the effort required to create comprehensive documentation. By following REST principles in Rails, we believe this is achievable. You can enhance the documentation using [Yard](https://yardoc.org/) tags.
|
32
32
|
|
33
|
+
## Table of Contents
|
34
|
+
|
35
|
+
- [Installation](#installation)
|
36
|
+
- [Configuration](#configuration)
|
37
|
+
- [Basic Information about the API](#basic-information-about-the-api)
|
38
|
+
- [Servers Information](#servers-information)
|
39
|
+
- [Tag Information](#tag-information)
|
40
|
+
- [Optional Settings](#optional-settings)
|
41
|
+
- [Authentication Settings](#authentication-settings)
|
42
|
+
- [Usage](#usage)
|
43
|
+
- [Documenting Your Endpoints](#documenting-your-endpoints)
|
44
|
+
- [Example](#example-of-documented-endpoints)
|
45
|
+
- [Securing the OasRails Engine](#securing-the-oasrails-engine)
|
46
|
+
- [Customizing the View](#customizing-the-view)
|
47
|
+
- [Contributing](#contributing)
|
48
|
+
- [Roadmap and Ideas for Improvement](#roadmap-and-ideas-for-improvement)
|
49
|
+
- [License](#license)
|
50
|
+
|
33
51
|
## Installation
|
34
52
|
|
35
53
|
1. Add this line to your Rails application's Gemfile:
|
@@ -52,23 +70,15 @@ The goal is to minimize the effort required to create comprehensive documentatio
|
|
52
70
|
|
53
71
|
You'll now have **basic documentation** based on your routes and automatically gathered information at `localhost:3000/docs`. To enhance it, create an initializer file and add [Yard](https://yardoc.org/) tags to your controller methods.
|
54
72
|
|
55
|
-
##
|
56
|
-
|
57
|
-
### Initializer File
|
73
|
+
## Configuration
|
58
74
|
|
59
|
-
|
75
|
+
To configure OasRails, you MUST create an initializer file including all your settings. The first step is to create your initializer file, which you can easily do with:
|
60
76
|
|
61
77
|
```bash
|
62
78
|
rails generate oas_rails:config
|
63
79
|
```
|
64
80
|
|
65
|
-
Then
|
66
|
-
|
67
|
-
**Almost every description in a OAS file support simple markdown**
|
68
|
-
|
69
|
-
## Configuration
|
70
|
-
|
71
|
-
To configure OasRails, edit the `config/initializers/oas_rails.rb` file. Below are the available configuration options:
|
81
|
+
Then fill it with your data. Below are the available configuration options:
|
72
82
|
|
73
83
|
### Basic Information about the API
|
74
84
|
|
@@ -100,56 +110,11 @@ To configure OasRails, edit the `config/initializers/oas_rails.rb` file. Below a
|
|
100
110
|
- `config.security_schema`: The default security schema used for authentication. Choose a predefined security schema from `[:api_key_cookie, :api_key_header, :api_key_query, :basic, :bearer, :bearer_jwt, :mutual_tls]`.
|
101
111
|
- `config.security_schemas`: Custom security schemas. Follow the [OpenAPI Specification](https://spec.openapis.org/oas/latest.html#security-scheme-object) for defining these schemas.
|
102
112
|
|
103
|
-
##
|
104
|
-
|
105
|
-
To secure the OasRails engine, which exposes an endpoint for showing the OAS definition, you can configure authentication to ensure that only authorized users have access. Here are a few methods to achieve this:
|
106
|
-
|
107
|
-
### 1. Using Basic Authentication
|
108
|
-
|
109
|
-
Use basic authentication to protect the OasRails endpoint. You can set this up in an initializer:
|
110
|
-
|
111
|
-
```ruby
|
112
|
-
# config/initializers/oas_rails.rb
|
113
|
-
OasRails::Engine.middleware.use(Rack::Auth::Basic) do |username, password|
|
114
|
-
ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.oas_rails_username, username) &
|
115
|
-
ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.oas_rails_password, password)
|
116
|
-
end
|
117
|
-
```
|
118
|
-
|
119
|
-
### 2. Using Devise's `authenticate` Helper
|
120
|
-
|
121
|
-
You can use Devise's `authenticate` helper to restrict access to the OasRails endpoint. For example, you can allow only admin users to access the endpoint:
|
122
|
-
|
123
|
-
```ruby
|
124
|
-
# config/routes.rb
|
125
|
-
# ...
|
126
|
-
authenticate :user, ->(user) { user.admin? } do
|
127
|
-
mount OasRails::Engine, at: '/docs'
|
128
|
-
end
|
129
|
-
```
|
130
|
-
|
131
|
-
### 3. Custom Authentication
|
132
|
-
|
133
|
-
To support custom authentication, you can extend the OasRails' ApplicationController using a hook. This allows you to add custom before actions to check for specific user permissions:
|
134
|
-
|
135
|
-
```ruby
|
136
|
-
# config/initializers/oas_rails.rb
|
137
|
-
|
138
|
-
ActiveSupport.on_load(:oas_rails_application_controller) do
|
139
|
-
# context here is OasRails::ApplicationController
|
140
|
-
|
141
|
-
before_action do
|
142
|
-
raise ActionController::RoutingError.new('Not Found') unless current_user&.admin?
|
143
|
-
end
|
113
|
+
## Usage
|
144
114
|
|
145
|
-
|
146
|
-
# Load the current user
|
147
|
-
User.find(session[:user_id]) # Adjust according to your authentication logic
|
148
|
-
end
|
149
|
-
end
|
150
|
-
```
|
115
|
+
In addition to the information provided in the initializer file and the data that can be extracted from the routes and methods automatically, it is essential to document your API in the following way. The documentation is created **with the help of YARD**, so the methods are documented with **comment tags**.
|
151
116
|
|
152
|
-
|
117
|
+
### Documenting Your Endpoints
|
153
118
|
|
154
119
|
Almost every description in an OAS file supports simple markdown. The following tags are available for documenting your endpoints:
|
155
120
|
|
@@ -331,9 +296,74 @@ class UsersController < ApplicationController
|
|
331
296
|
end
|
332
297
|
```
|
333
298
|
|
299
|
+
## Securing the OasRails Engine
|
300
|
+
|
301
|
+
To secure the OasRails engine, which exposes an endpoint for showing the OAS definition, you can configure authentication to ensure that only authorized users have access. Here are a few methods to achieve this:
|
302
|
+
|
303
|
+
### 1. Using Basic Authentication
|
304
|
+
|
305
|
+
Use basic authentication to protect the OasRails endpoint. You can set this up in an initializer:
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
# config/initializers/oas_rails.rb
|
309
|
+
OasRails::Engine.middleware.use(Rack::Auth::Basic) do |username, password|
|
310
|
+
ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.oas_rails_username, username) &
|
311
|
+
ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.oas_rails_password, password)
|
312
|
+
end
|
313
|
+
```
|
314
|
+
|
315
|
+
### 2. Using Devise's `authenticate` Helper
|
316
|
+
|
317
|
+
You can use Devise's `authenticate` helper to restrict access to the OasRails endpoint. For example, you can allow only admin users to access the endpoint:
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
# config/routes.rb
|
321
|
+
# ...
|
322
|
+
authenticate :user, ->(user) { user.admin? } do
|
323
|
+
mount OasRails::Engine, at: '/docs'
|
324
|
+
end
|
325
|
+
```
|
326
|
+
|
327
|
+
### 3. Custom Authentication
|
328
|
+
|
329
|
+
To support custom authentication, you can extend the OasRails' ApplicationController using a hook. This allows you to add custom before actions to check for specific user permissions:
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
# config/initializers/oas_rails.rb
|
333
|
+
|
334
|
+
ActiveSupport.on_load(:oas_rails_application_controller) do
|
335
|
+
# context here is OasRails::ApplicationController
|
336
|
+
|
337
|
+
before_action do
|
338
|
+
raise ActionController::RoutingError.new('Not Found') unless current_user&.admin?
|
339
|
+
end
|
340
|
+
|
341
|
+
def current_user
|
342
|
+
# Load the current user
|
343
|
+
User.find(session[:user_id]) # Adjust according to your authentication logic
|
344
|
+
end
|
345
|
+
end
|
346
|
+
```
|
347
|
+
|
348
|
+
## Customizing the View
|
349
|
+
|
350
|
+
The OasRails engine provides an easy way to display your OpenAPI Specification (OAS) within your Rails application. By default, it includes an `index` view in the `OasRailsController` that displays [RapiDoc](https://rapidocweb.com/) through a CDN with default configurations. You can easily override this view to replace RapiDoc entirely or configure it differently.
|
351
|
+
|
352
|
+
#### Overriding the `index` View
|
353
|
+
|
354
|
+
To override the `index` view provided by the OasRails engine, follow these steps:
|
355
|
+
|
356
|
+
1. **Create the Override View File**: In your host application, create a new file at the path `app/views/oas_rails/oas_rails/index.html.erb`. If the directories do not exist, you will need to create them.
|
357
|
+
|
358
|
+
2. **Customize the View**: Open the newly created `index.html.erb` file and add your custom HTML and ERB code to display the OAS as desired. You can refer to the source code of this project for guidance.
|
359
|
+
|
360
|
+
#### Using the Custom View
|
361
|
+
|
362
|
+
Once the custom view file is in place, Rails will automatically use it instead of the view provided by the OasRails engine. This allows you to fully customize the presentation of the OAS without modifying the engine's code.
|
363
|
+
|
334
364
|
## Contributing
|
335
365
|
|
336
|
-
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star
|
366
|
+
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star⭐! Thanks again!
|
337
367
|
|
338
368
|
1. Fork the Project
|
339
369
|
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
@@ -1,16 +0,0 @@
|
|
1
|
-
<!DOCTYPE html>
|
2
|
-
<html>
|
3
|
-
<head>
|
4
|
-
<title>Oas rails</title>
|
5
|
-
<%= csrf_meta_tags %>
|
6
|
-
<%= csp_meta_tag %>
|
7
|
-
<meta charset="utf-8">
|
8
|
-
<!-- Important: rapi-doc uses utf8 characters -->
|
9
|
-
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
|
10
|
-
</head>
|
11
|
-
<body>
|
12
|
-
|
13
|
-
<%= yield %>
|
14
|
-
|
15
|
-
</body>
|
16
|
-
</html>
|
@@ -1 +1,27 @@
|
|
1
|
-
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Oas rails</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
<meta charset="utf-8">
|
8
|
+
<!-- Important: rapi-doc uses utf8 characters -->
|
9
|
+
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
<rapi-doc
|
13
|
+
spec-url = "<%= main_app.oas_rails_path %>.json"
|
14
|
+
theme = "dark"
|
15
|
+
bg-color="#0F172A"
|
16
|
+
text-color= "#f7f7f7"
|
17
|
+
show-header = 'false'
|
18
|
+
primary-color = "#2de410"
|
19
|
+
font-size="largest"
|
20
|
+
show-method-in-nav-bar="as-colored-text"
|
21
|
+
nav-text-color="#f7f7f7"
|
22
|
+
nav-item-spacing="relaxed"
|
23
|
+
allow-spec-file-download="true"
|
24
|
+
>
|
25
|
+
</rapi-doc>
|
26
|
+
</body>
|
27
|
+
</html>
|
@@ -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 = {
|
data/lib/oas_rails/engine.rb
CHANGED
@@ -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
|
data/lib/oas_rails/media_type.rb
CHANGED
@@ -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
|
-
|
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 =
|
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
|
-
|
32
|
-
|
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
|
-
|
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
|
data/lib/oas_rails/oas_route.rb
CHANGED
@@ -19,13 +19,13 @@ 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
|
26
26
|
|
27
27
|
def extract_docstring
|
28
|
-
YARD::Docstring.parser.parse(
|
28
|
+
::YARD::Docstring.parser.parse(
|
29
29
|
controller_class.constantize.instance_method(method).comment.lines.map { |line| line.sub(/^#\s*/, '') }.join
|
30
30
|
).to_docstring
|
31
31
|
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
|
data/lib/oas_rails/operation.rb
CHANGED
@@ -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.
|
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 }
|
data/lib/oas_rails/paths.rb
CHANGED
@@ -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
|
|
@@ -2,12 +2,22 @@ require 'json'
|
|
2
2
|
|
3
3
|
module OasRails
|
4
4
|
class Specification
|
5
|
+
# Initializes a new Specification object.
|
6
|
+
# Clears the cache if running in the development environment.
|
5
7
|
def initialize
|
6
|
-
|
7
|
-
|
8
|
+
clear_cache unless Rails.env.production?
|
9
|
+
|
8
10
|
@specification = base_spec
|
9
11
|
end
|
10
12
|
|
13
|
+
# Clears the cache for MethodSource and RouteExtractor.
|
14
|
+
#
|
15
|
+
# @return [void]
|
16
|
+
def clear_cache
|
17
|
+
MethodSource.clear_cache
|
18
|
+
Extractors::RouteExtractor.clear_cache
|
19
|
+
end
|
20
|
+
|
11
21
|
def to_json(*_args)
|
12
22
|
@specification.to_json
|
13
23
|
rescue StandardError => e
|
@@ -41,7 +51,7 @@ module OasRails
|
|
41
51
|
# Create the Paths Object For the Root of the OAS.
|
42
52
|
# @see https://spec.openapis.org/oas/latest.html#paths-object
|
43
53
|
def paths
|
44
|
-
Paths.from_string_paths(string_paths: RouteExtractor.host_paths).to_spec
|
54
|
+
Paths.from_string_paths(string_paths: Extractors::RouteExtractor.host_paths).to_spec
|
45
55
|
end
|
46
56
|
|
47
57
|
# Created the Components Object For the Root of the OAS.
|
data/lib/oas_rails/version.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module OasRails
|
2
|
-
module
|
2
|
+
module YARD
|
3
3
|
class RequestBodyTag < ::YARD::Tags::Tag
|
4
4
|
attr_accessor :klass, :schema, :required
|
5
5
|
|
@@ -41,7 +41,7 @@ module OasRails
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
class
|
44
|
+
class OasYARDFactory < ::YARD::Tags::DefaultFactory
|
45
45
|
## parse_tag is a prefix used by YARD
|
46
46
|
|
47
47
|
def parse_tag_with_request_body(tag_name, text)
|
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,13 +25,21 @@ 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
|
-
module
|
31
|
-
autoload :
|
30
|
+
module YARD
|
31
|
+
autoload :OasYARDFactory, 'oas_rails/yard/oas_yard_factory'
|
32
|
+
end
|
33
|
+
|
34
|
+
module Extractors
|
35
|
+
autoload :RenderResponseExtractor, 'oas_rails/extractors/render_response_extractor'
|
36
|
+
autoload :RouteExtractor, "oas_rails/extractors/route_extractor"
|
32
37
|
end
|
33
38
|
|
34
39
|
class << self
|
40
|
+
# Configurations for make the OasRails engine Work.
|
35
41
|
def configure
|
42
|
+
OasRails.configure_yard!
|
36
43
|
yield config
|
37
44
|
end
|
38
45
|
|
@@ -41,7 +48,7 @@ module OasRails
|
|
41
48
|
end
|
42
49
|
|
43
50
|
def configure_yard!
|
44
|
-
::YARD::Tags::Library.default_factory =
|
51
|
+
::YARD::Tags::Library.default_factory = YARD::OasYARDFactory
|
45
52
|
yard_tags = {
|
46
53
|
'Request body' => [:request_body, :with_request_body],
|
47
54
|
'Request body Example' => [:request_body_example, :with_request_body_example],
|
@@ -56,13 +63,5 @@ module OasRails
|
|
56
63
|
::YARD::Tags::Library.define_tag(tag_name, method_name, handler)
|
57
64
|
end
|
58
65
|
end
|
59
|
-
|
60
|
-
def configure_esquema!
|
61
|
-
Esquema.configure do |config|
|
62
|
-
config.exclude_associations = true
|
63
|
-
config.exclude_foreign_keys = true
|
64
|
-
config.excluded_columns = %i[id created_at updated_at deleted_at]
|
65
|
-
end
|
66
|
-
end
|
67
66
|
end
|
68
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.
|
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-
|
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,111 +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
|
-
def host_paths
|
32
|
-
@host_paths ||= host_routes.map(&:path).uniq.sort
|
33
|
-
end
|
34
|
-
|
35
|
-
def clean_route(route)
|
36
|
-
route.gsub('(.:format)', '').gsub(/:\w+/) { |match| "{#{match[1..]}}" }
|
37
|
-
end
|
38
|
-
|
39
|
-
# THIS CODE IS NOT IN USE BUT CAN BE USEFULL WITH GLOBAL TAGS OR AUTH TAGS
|
40
|
-
# def get_controller_comments(controller_path)
|
41
|
-
# YARD.parse_string(File.read(controller_path))
|
42
|
-
# controller_class = YARD::Registry.all(:class).first
|
43
|
-
# if controller_class
|
44
|
-
# class_comment = controller_class.docstring.all
|
45
|
-
# method_comments = controller_class.meths.map do |method|
|
46
|
-
# {
|
47
|
-
# name: method.name,
|
48
|
-
# comment: method.docstring.all
|
49
|
-
# }
|
50
|
-
# end
|
51
|
-
# YARD::Registry.clear
|
52
|
-
# {
|
53
|
-
# class_comment: class_comment,
|
54
|
-
# method_comments: method_comments
|
55
|
-
# }
|
56
|
-
# else
|
57
|
-
# YARD::Registry.clear
|
58
|
-
# nil
|
59
|
-
# end
|
60
|
-
# rescue StandardError
|
61
|
-
# nil
|
62
|
-
# end
|
63
|
-
#
|
64
|
-
# def get_controller_comment(controller_path)
|
65
|
-
# get_controller_comments(controller_path)&.dig(:class_comment) || ''
|
66
|
-
# rescue StandardError
|
67
|
-
# ''
|
68
|
-
# end
|
69
|
-
|
70
|
-
private
|
71
|
-
|
72
|
-
def extract_host_routes
|
73
|
-
Rails.application.routes.routes.select do |route|
|
74
|
-
valid_api_route?(route)
|
75
|
-
end.map { |r| OasRoute.new_from_rails_route(rails_route: r) }
|
76
|
-
end
|
77
|
-
|
78
|
-
def valid_api_route?(route)
|
79
|
-
return false unless valid_route_implementation?(route)
|
80
|
-
return false if RAILS_DEFAULT_CONTROLLERS.any? { |default| route.defaults[:controller].start_with?(default) }
|
81
|
-
return false if RAILS_DEFAULT_PATHS.any? { |path| route.path.spec.to_s.include?(path) }
|
82
|
-
return false unless route.path.spec.to_s.start_with?(OasRails.config.api_path)
|
83
|
-
|
84
|
-
true
|
85
|
-
end
|
86
|
-
|
87
|
-
# Checks if a route has a valid implementation.
|
88
|
-
#
|
89
|
-
# This method verifies that both the controller and the action specified
|
90
|
-
# in the route exist. It checks if the controller class is defined and
|
91
|
-
# if the action method is implemented within that controller.
|
92
|
-
#
|
93
|
-
# @param route [ActionDispatch::Journey::Route] The route to check.
|
94
|
-
# @return [Boolean] true if both the controller and action exist, false otherwise.
|
95
|
-
def valid_route_implementation?(route)
|
96
|
-
controller_name = route.defaults[:controller]&.camelize
|
97
|
-
action_name = route.defaults[:action]
|
98
|
-
|
99
|
-
return false if controller_name.blank? || action_name.blank?
|
100
|
-
|
101
|
-
controller_class = "#{controller_name}Controller".safe_constantize
|
102
|
-
|
103
|
-
if controller_class.nil?
|
104
|
-
false
|
105
|
-
else
|
106
|
-
controller_class.instance_methods.include?(action_name.to_sym)
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|