-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: redefine the way request body and responses are described. …
…BREAKING. * refactor(yard): separate clases into different files * refactor: BREAKING! support deeps hashes for describe request body, parameters and responses but changes the syntax used. This fix date and date time support too. * fix: cops validation
- Loading branch information
Showing
15 changed files
with
468 additions
and
185 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,7 +29,7 @@ OasRails is a Rails engine for generating **automatic interactive documentation | |
|
||
After experiencing the interactive documentation in Python's fast-api framework, I sought similar functionality in Ruby on Rails. Unable to find a suitable solution, I [asked on Stack Overflow](https://stackoverflow.com/questions/71947018/is-there-a-way-to-generate-an-interactive-documentation-for-rails-apis) years ago. Now, with some free time while freelancing as an API developer, I decided to build my own tool. | ||
|
||
**Note**: This is not yet a production-ready solution. The code may be rough and behave unexpectedly, but I am actively working on improving it. If you like the idea, please consider contributing to its development. | ||
**Note: This is not yet a production-ready solution. The code may be rough and behave unexpectedly, but I am actively working on improving it. If you like the idea, please consider contributing to its development.** | ||
|
||
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. | ||
|
||
|
@@ -120,7 +120,7 @@ Then fill it with your data. Below are the available configuration options: | |
- `config.possible_default_responses`: Array with possible default errors.(Some will be added depending on the endpoint, example: not_found only works with show/update/delete). Default: [:not_found, :unauthorized, :forbidden]. It should be HTTP status code symbols from the list: `[:not_found, :unauthorized, :forbidden, :internal_server_error, :unprocessable_entity]` | ||
- `config.response_body_of_default`: body for use in default responses. It must be a Hash. Default: { message: String } | ||
- `config.response_body_of_default`: body for use in default responses. It must be a String hash like the used in request body tags. Default: "{ message: String }" | ||
## Usage | ||
|
@@ -158,12 +158,19 @@ Represents a parameter for the endpoint. The position can be: `header`, `path`, | |
<details> | ||
<summary style="font-weight: bold; font-size: 1.2em;">@request_body</summary> | ||
**Structure**: `@request_body text [type] structure` | ||
**Structure**: `@request_body text [type<structure>]` | ||
Documents the request body needed by the endpoint. The structure is optional if you provide a valid Active Record class. Use `!` to indicate a required request body. | ||
**Example**: | ||
`# @request_body The user to be created [Hash] {user: {name: String, age: Integer, password: String}}` | ||
`# @request_body The user to be created [!Hash{user: {name: String, age: Integer, password: String}}]` | ||
`# @request_body The user to be created [!User]` | ||
`# @request_body The user to be created [User]` | ||
`# @request_body The user to be created [!Hash{user: {name: String, age: Integer, password: String, surnames: Array<String>, coords: Hash{lat: String, lng: String}}}]` | ||
</details> | ||
|
@@ -182,12 +189,15 @@ Adds examples to the provided request body. | |
<details> | ||
<summary style="font-weight: bold; font-size: 1.2em;">@response</summary> | ||
**Structure**: `@response text(code) [type] structure` | ||
**Structure**: `@response text(code) [type<structure>]` | ||
Documents the responses of the endpoint and overrides the default responses found by the engine. | ||
**Example**: | ||
`# @response User not found by the provided Id(404) [Hash] {success: Boolean, message: String}` | ||
`# @response User not found by the provided Id(404) [Hash{success: Boolean, message: String}]` | ||
`# @response Validation errors(422) [Hash{success: Boolean, erros: Array<Hash{field: String, type: String, detail: Array<String>}>}]` | ||
</details> | ||
|
@@ -250,17 +260,17 @@ class UsersController < ApplicationController | |
# This method show a User by ID. The id must exist of other way it will be returning a **`404`**. | ||
# | ||
# @parameter id(path) [Integer] Used for identify the user. | ||
# @response Requested User(200) [Hash] {user: {name: String, email: String, created_at: DateTime }} | ||
# @response User not found by the provided Id(404) [Hash] {success: Boolean, message: String} | ||
# @response You don't have the right permission for access to this resource(403) [Hash] {success: Boolean, message: String} | ||
# @response Requested User(200) [Hash{user: {name: String, email: String, created_at: DateTime }}] | ||
# @response User not found by the provided Id(404) [Hash{success: Boolean, message: String}] | ||
# @response You don't have the right permission for access to this resource(403) [Hash{success: Boolean, message: String}] | ||
def show | ||
render json: @user | ||
end | ||
# @summary Create a User | ||
# @no_auth | ||
# | ||
# @request_body The user to be created. At least include an `email`. [User!] | ||
# @request_body The user to be created. At least include an `email`. [!User] | ||
# @request_body_example basic user [Hash] {user: {name: "Luis", email: "[email protected]"}} | ||
def create | ||
@user = User.new(user_params) | ||
|
@@ -276,7 +286,7 @@ class UsersController < ApplicationController | |
# - There is no option | ||
# - It must work | ||
# @tags users, update | ||
# @request_body User to be created [Hash] {user: { name: String, email: String, age: Integer}} | ||
# @request_body User to be created [!Hash{user: { name: String, email: !String, age: Integer, available_dates: Array<Date>}}] | ||
# @request_body_example Update user [Hash] {user: {name: "Luis", email: "[email protected]"}} | ||
# @request_body_example Complete User [Hash] {user: {name: "Luis", email: "[email protected]", age: 21}} | ||
def update | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
require 'json' | ||
|
||
module OasRails | ||
# The JsonSchemaGenerator module provides methods to transform string representations | ||
# of data types into JSON schema formats. | ||
module JsonSchemaGenerator | ||
# Processes a string representing a data type and converts it into a JSON schema. | ||
# | ||
# @param str [String] The string representation of a data type. | ||
# @return [Hash] A hash containing the required flag and the JSON schema. | ||
def self.process_string(str) | ||
parsed = parse_type(str) | ||
{ | ||
required: parsed[:required], | ||
json_schema: to_json_schema(parsed) | ||
} | ||
end | ||
|
||
# Parses a string representing a data type and determines its JSON schema type. | ||
# | ||
# @param str [String] The string representation of a data type. | ||
# @return [Hash] A hash containing the type, whether it's required, and any additional properties. | ||
def self.parse_type(str) | ||
required = str.start_with?('!') | ||
type = str.sub(/^!/, '').strip | ||
|
||
case type | ||
when /^Hash\{(.+)\}$/i | ||
{ type: :object, required:, properties: parse_object_properties(::Regexp.last_match(1)) } | ||
when /^Array<(.+)>$/i | ||
{ type: :array, required:, items: parse_type(::Regexp.last_match(1)) } | ||
else | ||
{ type: type.downcase.to_sym, required: } | ||
end | ||
end | ||
|
||
# Parses the properties of an object type from a string. | ||
# | ||
# @param str [String] The string representation of the object's properties. | ||
# @return [Hash] A hash where keys are property names and values are their JSON schema types. | ||
def self.parse_object_properties(str) | ||
properties = {} | ||
stack = [] | ||
current_key = '' | ||
current_value = '' | ||
|
||
str.each_char.with_index do |char, index| | ||
case char | ||
when '{', '<' | ||
stack.push(char) | ||
current_value += char | ||
when '}', '>' | ||
stack.pop | ||
current_value += char | ||
when ',' | ||
if stack.empty? | ||
properties[current_key.strip.to_sym] = parse_type(current_value.strip) | ||
current_key = '' | ||
current_value = '' | ||
else | ||
current_value += char | ||
end | ||
when ':' | ||
if stack.empty? | ||
current_key = current_value | ||
current_value = '' | ||
else | ||
current_value += char | ||
end | ||
else | ||
current_value += char | ||
end | ||
|
||
properties[current_key.strip.to_sym] = parse_type(current_value.strip) if index == str.length - 1 && !current_key.empty? | ||
end | ||
|
||
properties | ||
end | ||
|
||
# Converts a parsed data type into a JSON schema format. | ||
# | ||
# @param parsed [Hash] The parsed data type hash. | ||
# @return [Hash] The JSON schema representation of the parsed data type. | ||
def self.to_json_schema(parsed) | ||
case parsed[:type] | ||
when :object | ||
schema = { | ||
type: 'object', | ||
properties: {} | ||
} | ||
required_props = [] | ||
parsed[:properties].each do |key, value| | ||
schema[:properties][key] = to_json_schema(value) | ||
required_props << key.to_s if value[:required] | ||
end | ||
schema[:required] = required_props unless required_props.empty? | ||
schema | ||
when :array | ||
{ | ||
type: 'array', | ||
items: to_json_schema(parsed[:items]) | ||
} | ||
else | ||
ruby_type_to_json_schema_type(parsed[:type]) | ||
end | ||
end | ||
|
||
# Converts a Ruby data type into its corresponding JSON schema type. | ||
# | ||
# @param type [Symbol, String] The Ruby data type. | ||
# @return [Hash, String] The JSON schema type or a hash with additional format information. | ||
def self.ruby_type_to_json_schema_type(type) | ||
case type.to_s.downcase | ||
when 'string' then { type: "string" } | ||
when 'integer' then { type: "integer" } | ||
when 'float' then { type: "float" } | ||
when 'boolean' then { type: "boolean" } | ||
when 'array' then { type: "array" } | ||
when 'hash' then { type: "hash" } | ||
when 'nil' then { type: "null" } | ||
when 'date' then { type: "string", format: "date" } | ||
when 'datetime' then { type: "string", format: "date-time" } | ||
else type.to_s.downcase | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.