Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OpenAPI] Support the @param tag #134

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions lib/rage/openapi/converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,23 @@ def run
@spec["paths"] = @nodes.leaves.each_with_object({}) do |node, memo|
next if node.private || node.parents.any?(&:private)

path_parameters = []
path_params = []
path = node.http_path.gsub(/:(\w+)/) do
path_parameters << $1
path_params << $1
"{#{$1}}"
end

unless memo.key?(path)
memo[path] = {}
path_parameters.each do |parameter|
path_params.each do |param|
documented_path_param = node.parameters.delete(param)

(memo[path]["parameters"] ||= []) << {
"in" => "path",
"name" => parameter,
"name" => param,
"required" => true,
"schema" => { "type" => parameter.end_with?("id") ? "integer" : "string" }
"description" => documented_path_param&.dig(:description) || "",
"schema" => get_param_type_spec(param, documented_path_param&.dig(:type))
}
end
end
Expand All @@ -52,6 +55,10 @@ def run
"tags" => build_tags(node)
}

if node.parameters.any?
memo[path][method]["parameters"] = build_parameters(node)
end

responses = node.parents.reverse.map(&:responses).reduce(&:merge).merge(node.responses)

memo[path][method]["responses"] = if responses.any?
Expand Down Expand Up @@ -101,6 +108,22 @@ def build_app_name
basename.capitalize.gsub(/[\s\-_]([a-zA-Z0-9]+)/) { " #{$1.capitalize}" }
end

def build_parameters(node)
node.parameters.map do |param_name, param_info|
if param_info.key?(:ref)
param_info[:ref]
else
{
"name" => param_name,
"in" => "query",
"required" => param_info[:required],
"description" => param_info[:description] || "",
"schema" => get_param_type_spec(param_name, param_info[:type])
}
end
end
end

def build_security(node)
available_before_actions = node.controller.__before_actions_for(node.action.to_sym)

Expand Down Expand Up @@ -138,4 +161,18 @@ def build_tags(node)
@used_tags += node_tags
end
end

def get_param_type_spec(param_name, param_type)
unless param_type
param_type = if param_name == "id" || param_name.end_with?("_id")
"Integer"
elsif param_name.end_with?("_at")
"Time"
else
"String"
end
end

Rage::OpenAPI.__type_to_spec(param_type)
end
end
2 changes: 1 addition & 1 deletion lib/rage/openapi/nodes/method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def initialize(controller, action, parents)
@parents = parents

@responses = {}
@parameters = []
@parameters = {}
end

def root
Expand Down
27 changes: 27 additions & 0 deletions lib/rage/openapi/openapi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,33 @@ def self.__module_parent(klass)
Object
end

# @private
def self.__type_to_spec(type, default: true)
case type
when "Integer"
{ "type" => "integer" }
when "Float"
{ "type" => "number", "format" => "float" }
when "Numeric"
{ "type" => "number" }
when "Boolean"
{ "type" => "boolean" }
when "Hash"
{ "type" => "object" }
when "Date"
{ "type" => "string", "format" => "date" }
when "DateTime", "Time"
{ "type" => "string", "format" => "date-time" }
when "String"
{ "type" => "string" }
else
if default
__log_warn("unrecognized type #{type}")
{ "type" => "string" }
end
end
end

# @private
def self.__log_warn(log)
puts "[OpenAPI] WARNING: #{log}"
Expand Down
39 changes: 39 additions & 0 deletions lib/rage/openapi/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ def parse_method_comments(node, comments)
end
end

elsif expression =~ /@param\s/
parse_param_tag(expression, node, comments[i])

elsif expression =~ /@internal\b/
# no-op
children = find_children(comments[i + 1..], node)
Expand Down Expand Up @@ -208,4 +211,40 @@ def parse_response_tag(expression, node, comment)
end
end
end

def parse_param_tag(expression, node, comment)
param = expression[7..].strip

shared_reference_parser = Rage::OpenAPI::Parsers::SharedReference.new
if shared_reference_parser.known_definition?(param)
if (ref = shared_reference_parser.parse(param))
node.parameters[param] = { ref: }
else
Rage::OpenAPI.__log_warn "invalid shared reference detected at #{location_msg(comment)}"
end
return
end

param_name, param_type, param_description = param.split(" ", 3)
is_required = true
param_type_regexp = /^[{\[]\w+[}\]]$/

if param_type && param_description && !param_type.match?(param_type_regexp)
param_description = "#{param_type} #{param_description}"
param_type = nil
end

if param_name.end_with?("?")
param_name = param_name[0...-1]
is_required = false
end

param_type = param_type[1...-1] if param_type&.match?(param_type_regexp)

if node.parameters[param_name]
Rage::OpenAPI.__log_warn "duplicate `@param` tag detected at #{location_msg(comment)}"
else
node.parameters[param_name] = { type: param_type, description: param_description, required: is_required }
end
end
end
17 changes: 1 addition & 16 deletions lib/rage/openapi/parsers/ext/alba.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,22 +264,7 @@ def get_key_transformer(transformer_id)
end

def get_type_definition(type_id)
case type_id
when "Integer"
{ "type" => "integer" }
when "Boolean", ":Boolean"
{ "type" => "boolean" }
when "Numeric"
{ "type" => "number" }
when "Float"
{ "type" => "number", "format" => "float" }
when "Date"
{ "type" => "string", "format" => "date" }
when "DateTime", "Time"
{ "type" => "string", "format" => "date-time" }
else
{ "type" => "string" }
end
Rage::OpenAPI.__type_to_spec(type_id.delete_prefix(":"))
end
end
end
21 changes: 1 addition & 20 deletions lib/rage/openapi/parsers/yaml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,6 @@ def __parse(object)
private

def type_to_spec(type)
case type
when "Integer"
{ "type" => "integer" }
when "Float"
{ "type" => "number", "format" => "float" }
when "Numeric"
{ "type" => "number" }
when "Boolean"
{ "type" => "boolean" }
when "Hash"
{ "type" => "object" }
when "Date"
{ "type" => "string", "format" => "date" }
when "DateTime", "Time"
{ "type" => "string", "format" => "date-time" }
when "String"
{ "type" => "string" }
else
{ "type" => "string", "enum" => [type] }
end
Rage::OpenAPI.__type_to_spec(type, default: false) || { "type" => "string", "enum" => [type] }
end
end
6 changes: 3 additions & 3 deletions spec/integration/integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -348,10 +348,10 @@
expect(spec["tags"]).to include({ "name" => "v1/Users" }, { "name" => "v2/Users" }, { "name" => "v3/Users" })

expect(spec["paths"]["/api/v1/users"]).to match({ "get" => { "summary" => "Returns the list of all users.", "description" => "Test description for the method.", "deprecated" => false, "security" => [{ "authenticate_user" => [] }], "tags" => ["v1/Users"], "responses" => { "200" => { "description" => "", "content" => { "application/json" => { "schema" => { "type" => "object", "properties" => { "users" => { "type" => "array", "items" => { "type" => "object", "properties" => { "email" => { "type" => "string" }, "id" => { "type" => "string" }, "name" => { "type" => "string" }, "avatar" => { "type" => "object", "properties" => { "url" => { "type" => "string" }, "updated_at" => { "type" => "string" } } }, "address" => { "type" => "object", "properties" => { "city" => { "type" => "string" }, "zip" => { "type" => "string" }, "country" => { "type" => "string" } } } } } } } } } } } } }, "post" => { "summary" => "Creates a user.", "description" => "", "deprecated" => false, "security" => [{ "authenticate_user" => [] }], "tags" => ["v1/Users"], "responses" => { "200" => { "description" => "", "content" => { "application/json" => { "schema" => { "type" => "object", "properties" => { "user" => { "type" => "object", "properties" => { "email" => { "type" => "string" }, "id" => { "type" => "string" }, "name" => { "type" => "string" }, "avatar" => { "type" => "object", "properties" => { "url" => { "type" => "string" }, "updated_at" => { "type" => "string" } } }, "address" => { "type" => "object", "properties" => { "city" => { "type" => "string" }, "zip" => { "type" => "string" }, "country" => { "type" => "string" } } } } } } } } } } }, "requestBody" => { "content" => { "application/json" => { "schema" => { "type" => "object", "properties" => { "user" => { "type" => "object", "properties" => { "name" => { "type" => "string" }, "email" => { "type" => "string" }, "password" => { "type" => "string" } } } } } } } } } })
expect(spec["paths"]["/api/v1/users/{id}"]).to match({ "parameters" => [{ "in" => "path", "name" => "id", "required" => true, "schema" => { "type" => "integer" } }], "get" => { "summary" => "Returns a specific user.", "description" => "", "deprecated" => false, "security" => [{ "authenticate_user" => [] }], "tags" => ["v1/Users"], "responses" => { "200" => { "description" => "", "content" => { "application/json" => { "schema" => { "type" => "object", "properties" => { "full_name" => { "type" => "string" }, "comments" => { "type" => "array", "items" => { "type" => "object", "properties" => { "content" => { "type" => "string" }, "created_at" => { "type" => "string" } } } } } } } } }, "404" => { "description" => "" } } } })
expect(spec["paths"]["/api/v1/users/{id}"]).to match({ "parameters" => [{ "description" => "", "in" => "path", "name" => "id", "required" => true, "schema" => { "type" => "integer" } }], "get" => { "summary" => "Returns a specific user.", "description" => "", "deprecated" => false, "security" => [{ "authenticate_user" => [] }], "tags" => ["v1/Users"], "responses" => { "200" => { "description" => "", "content" => { "application/json" => { "schema" => { "type" => "object", "properties" => { "full_name" => { "type" => "string" }, "comments" => { "type" => "array", "items" => { "type" => "object", "properties" => { "content" => { "type" => "string" }, "created_at" => { "type" => "string" } } } } } } } } }, "404" => { "description" => "" } } } })
expect(spec["paths"]["/api/v2/users"]).to match({ "get" => { "summary" => "Returns the list of all users.", "description" => "Test description.", "deprecated" => false, "security" => [], "tags" => ["v2/Users"], "responses" => { "200" => { "description" => "", "content" => { "application/json" => { "schema" => { "type" => "array", "items" => { "type" => "object", "properties" => { "full_name" => { "type" => "string" }, "comments" => { "type" => "array", "items" => { "type" => "object", "properties" => { "content" => { "type" => "string" }, "created_at" => { "type" => "string" } } } } } } } } } } } } })
expect(spec["paths"]["/api/v2/users/{id}"]).to match({ "parameters" => [{ "in" => "path", "name" => "id", "required" => true, "schema" => { "type" => "integer" } }], "get" => { "summary" => "Returns a specific user.", "description" => "", "deprecated" => true, "security" => [{ "authenticate_user" => [] }], "tags" => ["v2/Users"], "responses" => { "200" => { "description" => "", "content" => { "application/json" => { "schema" => { "type" => "object", "properties" => { "full_name" => { "type" => "string" }, "comments" => { "type" => "array", "items" => { "type" => "object", "properties" => { "content" => { "type" => "string" }, "created_at" => { "type" => "string" } } } } } } } } } } } })
expect(spec["paths"]["/api/v3/users/{id}"]).to match({ "parameters" => [{ "in" => "path", "name" => "id", "required" => true, "schema" => { "type" => "integer" } }], "get" => { "summary" => "Returns a specific user.", "description" => "", "deprecated" => false, "security" => [{ "authenticate_user" => [] }], "tags" => ["v3/Users"], "responses" => { "200" => { "description" => "", "content" => { "application/json" => { "schema" => { "$ref" => "#/components/schemas/V3_User" } } } }, "404" => { "$ref" => "#/components/responses/404NotFound" } } } })
expect(spec["paths"]["/api/v2/users/{id}"]).to match({ "parameters" => [{ "description" => "", "in" => "path", "name" => "id", "required" => true, "schema" => { "type" => "integer" } }], "get" => { "summary" => "Returns a specific user.", "description" => "", "deprecated" => true, "security" => [{ "authenticate_user" => [] }], "tags" => ["v2/Users"], "responses" => { "200" => { "description" => "", "content" => { "application/json" => { "schema" => { "type" => "object", "properties" => { "full_name" => { "type" => "string" }, "comments" => { "type" => "array", "items" => { "type" => "object", "properties" => { "content" => { "type" => "string" }, "created_at" => { "type" => "string" } } } } } } } } } } } })
expect(spec["paths"]["/api/v3/users/{id}"]).to match({ "parameters" => [{ "description" => "", "in" => "path", "name" => "id", "required" => true, "schema" => { "type" => "integer" } }], "get" => { "summary" => "Returns a specific user.", "description" => "", "deprecated" => false, "security" => [{ "authenticate_user" => [] }], "tags" => ["v3/Users"], "responses" => { "200" => { "description" => "", "content" => { "application/json" => { "schema" => { "$ref" => "#/components/schemas/V3_User" } } } }, "404" => { "$ref" => "#/components/responses/404NotFound" } } } })
end
end
end
4 changes: 2 additions & 2 deletions spec/openapi/builder/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def show
end

it "returns correct schema" do
expect(subject).to eq({ "openapi" => "3.0.0", "info" => { "version" => "1.0.0", "title" => "Rage" }, "components" => {}, "tags" => [{ "name" => "Users" }], "paths" => { "/users" => { "get" => { "summary" => "", "description" => "", "deprecated" => false, "security" => [], "tags" => ["Users"], "responses" => { "200" => { "description" => "" } } } }, "/users/{id}" => { "parameters" => [{ "in" => "path", "name" => "id", "required" => true, "schema" => { "type" => "integer" } }], "get" => { "summary" => "", "description" => "", "deprecated" => false, "security" => [], "tags" => ["Users"], "responses" => { "200" => { "description" => "" } } } } } })
expect(subject).to eq({ "openapi" => "3.0.0", "info" => { "version" => "1.0.0", "title" => "Rage" }, "components" => {}, "tags" => [{ "name" => "Users" }], "paths" => { "/users" => { "get" => { "summary" => "", "description" => "", "deprecated" => false, "security" => [], "tags" => ["Users"], "responses" => { "200" => { "description" => "" } } } }, "/users/{id}" => { "parameters" => [{ "description" => "", "in" => "path", "name" => "id", "required" => true, "schema" => { "type" => "integer" } }], "get" => { "summary" => "", "description" => "", "deprecated" => false, "security" => [], "tags" => ["Users"], "responses" => { "200" => { "description" => "" } } } } } })
end
end

Expand Down Expand Up @@ -183,7 +183,7 @@ def show
end

it "returns correct schema" do
expect(subject).to eq({ "openapi" => "3.0.0", "info" => { "version" => "1.0.0", "title" => "Rage" }, "components" => {}, "tags" => [{ "name" => "Photos" }], "paths" => { "/users/{user_id}/photos/{id}" => { "parameters" => [{ "in" => "path", "name" => "user_id", "required" => true, "schema" => { "type" => "integer" } }, { "in" => "path", "name" => "id", "required" => true, "schema" => { "type" => "integer" } }], "get" => { "summary" => "", "description" => "", "deprecated" => false, "security" => [], "tags" => ["Photos"], "responses" => { "200" => { "description" => "" } } } } } })
expect(subject).to eq({ "openapi" => "3.0.0", "info" => { "version" => "1.0.0", "title" => "Rage" }, "components" => {}, "tags" => [{ "name" => "Photos" }], "paths" => { "/users/{user_id}/photos/{id}" => { "parameters" => [{ "description" => "", "in" => "path", "name" => "user_id", "required" => true, "schema" => { "type" => "integer" } }, { "description" => "", "in" => "path", "name" => "id", "required" => true, "schema" => { "type" => "integer" } }], "get" => { "summary" => "", "description" => "", "deprecated" => false, "security" => [], "tags" => ["Photos"], "responses" => { "200" => { "description" => "" } } } } } })
end
end

Expand Down
Loading