rails-param-validation 0.1.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 +7 -0
- data/.gitignore +2 -0
- data/.gitlab-ci.yml +34 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +63 -0
- data/Rakefile +6 -0
- data/bin/.keep +0 -0
- data/docs/_config.yml +3 -0
- data/docs/annotations.md +62 -0
- data/docs/getting-started.md +32 -0
- data/docs/image/error-screenshot.png +0 -0
- data/docs/index.md +61 -0
- data/docs/main-idea.md +72 -0
- data/docs/openapi.md +39 -0
- data/docs/type-definition.md +178 -0
- data/lib/rails-param-validation/errors/missing_parameter_annotation.rb +9 -0
- data/lib/rails-param-validation/errors/no_matching_factory.rb +9 -0
- data/lib/rails-param-validation/errors/param_validation_failed_error.rb +12 -0
- data/lib/rails-param-validation/errors/type_not_found.rb +9 -0
- data/lib/rails-param-validation/rails/action_definition.rb +66 -0
- data/lib/rails-param-validation/rails/annotation_manager.rb +40 -0
- data/lib/rails-param-validation/rails/config.rb +48 -0
- data/lib/rails-param-validation/rails/extensions/annotation_extension.rb +95 -0
- data/lib/rails-param-validation/rails/extensions/custom_type_extension.rb +13 -0
- data/lib/rails-param-validation/rails/extensions/error.template.html.erb +86 -0
- data/lib/rails-param-validation/rails/extensions/validation_extension.rb +105 -0
- data/lib/rails-param-validation/rails/helper.rb +9 -0
- data/lib/rails-param-validation/rails/openapi/openapi.rb +128 -0
- data/lib/rails-param-validation/rails/openapi/routing_helper.rb +40 -0
- data/lib/rails-param-validation/rails/rails.rb +31 -0
- data/lib/rails-param-validation/rails/tasks/openapi.rake +32 -0
- data/lib/rails-param-validation/types/types.rb +100 -0
- data/lib/rails-param-validation/validator.rb +51 -0
- data/lib/rails-param-validation/validator_factory.rb +37 -0
- data/lib/rails-param-validation/validators/alternatives.rb +42 -0
- data/lib/rails-param-validation/validators/array.rb +49 -0
- data/lib/rails-param-validation/validators/boolean.rb +38 -0
- data/lib/rails-param-validation/validators/constant.rb +38 -0
- data/lib/rails-param-validation/validators/custom_type.rb +28 -0
- data/lib/rails-param-validation/validators/date.rb +39 -0
- data/lib/rails-param-validation/validators/datetime.rb +39 -0
- data/lib/rails-param-validation/validators/float.rb +39 -0
- data/lib/rails-param-validation/validators/hash.rb +52 -0
- data/lib/rails-param-validation/validators/integer.rb +39 -0
- data/lib/rails-param-validation/validators/object.rb +63 -0
- data/lib/rails-param-validation/validators/optional.rb +44 -0
- data/lib/rails-param-validation/validators/regex.rb +37 -0
- data/lib/rails-param-validation/validators/string.rb +31 -0
- data/lib/rails-param-validation/validators/uuid.rb +39 -0
- data/lib/rails-param-validation/version.rb +3 -0
- data/lib/rails-param-validation.rb +42 -0
- data/rails-param-validation.gemspec +33 -0
- metadata +100 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
module RailsParamValidation
|
2
|
+
|
3
|
+
class ActionDefinition
|
4
|
+
attr_reader :params, :request_body_type, :paths, :responses, :controller, :action
|
5
|
+
attr_accessor :description
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@params = {}
|
9
|
+
@paths = []
|
10
|
+
@param_validation_enabled = true
|
11
|
+
@description = ''
|
12
|
+
@request_body_type = RailsParamValidation.config.default_body_content_type if defined?(Rails)
|
13
|
+
@responses = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def store_origin!(controller, action)
|
17
|
+
@controller = controller
|
18
|
+
@action = action
|
19
|
+
end
|
20
|
+
|
21
|
+
def disable_param_validation!
|
22
|
+
@param_validation_enabled = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def param_validation?
|
26
|
+
@param_validation_enabled
|
27
|
+
end
|
28
|
+
|
29
|
+
def request_body_type!(mime)
|
30
|
+
@request_body_type = mime
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_param(name, type, schema, description)
|
34
|
+
@params[name] = {
|
35
|
+
schema: schema,
|
36
|
+
description: description,
|
37
|
+
type: type
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_response(status, schema, description)
|
42
|
+
@responses[status] = {
|
43
|
+
schema: schema,
|
44
|
+
description: description
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_path(method, path)
|
49
|
+
@paths.push(method: method, path: path)
|
50
|
+
end
|
51
|
+
|
52
|
+
def finalize!(class_name, method_name)
|
53
|
+
@responses.each do |code, response|
|
54
|
+
name = "#{class_name.to_s.capitalize}#{method_name.to_s.camelcase}#{Rack::Utils::SYMBOL_TO_STATUS_CODE.key(code).to_s.camelize}Response".to_sym
|
55
|
+
AnnotationTypes::CustomT.register(name, response[:schema])
|
56
|
+
|
57
|
+
response.merge!(schema: AnnotationTypes::CustomT.new(name))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_schema
|
62
|
+
@params.map { |k, v| [k, v[:schema]] }.to_h
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module RailsParamValidation
|
2
|
+
|
3
|
+
class AnnotationManager
|
4
|
+
attr_reader :annotations
|
5
|
+
def initialize
|
6
|
+
@annotations = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.instance
|
10
|
+
@instance ||= AnnotationManager.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def classes
|
14
|
+
@annotations.keys
|
15
|
+
end
|
16
|
+
|
17
|
+
def methods(klass)
|
18
|
+
@annotations.fetch(klass, {}).keys
|
19
|
+
end
|
20
|
+
|
21
|
+
def annotate_method!(klass, method_name, type, value)
|
22
|
+
@annotations[klass.name] ||= {}
|
23
|
+
@annotations[klass.name][method_name] ||= {}
|
24
|
+
@annotations[klass.name][method_name][type] = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_annotation(class_name, method_name, type)
|
28
|
+
@annotations.fetch(class_name, {}).fetch(method_name, {}).fetch(type, nil)
|
29
|
+
end
|
30
|
+
|
31
|
+
def annotate_class!(klass, type, value)
|
32
|
+
annotate_method! klass, '', type, value
|
33
|
+
end
|
34
|
+
|
35
|
+
def class_annotation(class_name, type)
|
36
|
+
method_annotation class_name, '', type
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module RailsParamValidation
|
2
|
+
|
3
|
+
class OpenApiMetaConfig
|
4
|
+
attr_accessor :title, :version, :url, :description
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
app_class = Rails.application.class
|
8
|
+
|
9
|
+
self.url = 'http://localhost:3000'
|
10
|
+
self.title = app_name(app_class)
|
11
|
+
self.version = '1.0'
|
12
|
+
self.description = "#{app_name(app_class)} application"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def app_name(klass)
|
18
|
+
return klass.module_parent_name if klass.respond_to? :module_parent_name
|
19
|
+
klass.parent.name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Configuration
|
24
|
+
attr_accessor :use_default_json_response
|
25
|
+
attr_accessor :use_default_html_response
|
26
|
+
attr_accessor :use_validator_caching
|
27
|
+
attr_accessor :raise_on_missing_annotation
|
28
|
+
attr_accessor :default_body_content_type
|
29
|
+
attr_reader :openapi
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@use_default_json_response = true
|
33
|
+
@use_default_html_response = true
|
34
|
+
@use_validator_caching = Rails.env.production?
|
35
|
+
@raise_on_missing_annotation = true
|
36
|
+
@default_body_content_type = 'application/json'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.config
|
41
|
+
@config ||= Configuration.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.openapi
|
45
|
+
@openapi ||= OpenApiMetaConfig.new
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require_relative './../action_definition'
|
2
|
+
require_relative './../annotation_manager'
|
3
|
+
|
4
|
+
module RailsParamValidation
|
5
|
+
module AnnotationExtension
|
6
|
+
|
7
|
+
def self.included(klass)
|
8
|
+
klass.extend ClassMethods
|
9
|
+
|
10
|
+
# Check if there already is a method_added implementation
|
11
|
+
begin
|
12
|
+
existing_method_added = klass.method(:method_added)
|
13
|
+
rescue NameError => e
|
14
|
+
existing_method_added = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
klass.define_singleton_method(:method_added) do |name|
|
18
|
+
# If there is a parameter definition: annotate this method and reset the carrier variable
|
19
|
+
if @param_definition
|
20
|
+
# Store where this annotation came from
|
21
|
+
class_name = RailsHelper.controller_to_tag self
|
22
|
+
method_name = name.to_sym
|
23
|
+
|
24
|
+
@param_definition.store_origin! class_name, method_name
|
25
|
+
@param_definition.finalize! class_name, method_name
|
26
|
+
|
27
|
+
AnnotationManager.instance.annotate_method! self, name, :param_definition, @param_definition
|
28
|
+
@param_definition = nil
|
29
|
+
|
30
|
+
# Parameter wrapping needs to be disabled
|
31
|
+
wrap_parameters false if respond_to? :wrap_parameters
|
32
|
+
end
|
33
|
+
|
34
|
+
# If there already was an existing method_added implementation, call it
|
35
|
+
existing_method_added&.call(name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module ClassMethods
|
40
|
+
def param_definition
|
41
|
+
@param_definition || raise(StandardError.new "Annotation must be part of an operation-block")
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param [Symbol] name Name of the parameter as it is accessible by params[<name>]
|
45
|
+
# @param schema Definition of the schema (according to the available validators)
|
46
|
+
# @param [String] description Description of the param, just for documentation
|
47
|
+
def param(name, schema, type = :query, description = nil)
|
48
|
+
param_definition.add_param name, type, schema, description
|
49
|
+
end
|
50
|
+
|
51
|
+
def query_param(name, schema, description = nil)
|
52
|
+
param name, schema, :query, description
|
53
|
+
end
|
54
|
+
|
55
|
+
def body_param(name, schema, description = nil)
|
56
|
+
param name, schema, :body, description
|
57
|
+
end
|
58
|
+
|
59
|
+
def path_param(name, schema, description = nil)
|
60
|
+
param name, schema, :path, description
|
61
|
+
end
|
62
|
+
|
63
|
+
def body_type(mime_type)
|
64
|
+
param_definition.request_body_type! mime_type
|
65
|
+
end
|
66
|
+
|
67
|
+
def desc(description)
|
68
|
+
if @param_definition
|
69
|
+
param_definition.description = description
|
70
|
+
else
|
71
|
+
AnnotationManager.instance.annotate_class! self, :description, description
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def no_params
|
76
|
+
param_definition
|
77
|
+
end
|
78
|
+
|
79
|
+
def accept_all_params
|
80
|
+
param_definition.disable_param_validation!
|
81
|
+
end
|
82
|
+
|
83
|
+
def response(status, schema, description)
|
84
|
+
param_definition.add_response status, schema, description
|
85
|
+
end
|
86
|
+
|
87
|
+
def action(description = nil)
|
88
|
+
@param_definition = ActionDefinition.new
|
89
|
+
@param_definition.description = description
|
90
|
+
|
91
|
+
yield
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module RailsParamValidation
|
2
|
+
module CustomTypesExtension
|
3
|
+
def self.included(klass)
|
4
|
+
klass.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def declare(type, schema)
|
9
|
+
RailsParamValidation::AnnotationTypes::CustomT.register type, schema
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
<html lang="en">
|
2
|
+
<head>
|
3
|
+
<style type="text/css">
|
4
|
+
body {
|
5
|
+
font-family: Arial, Helvetica, sans-serif;
|
6
|
+
background-color: #e8e8e8;
|
7
|
+
font-size: 11pt;
|
8
|
+
height: calc(100% - 20px);
|
9
|
+
padding: 10px;
|
10
|
+
margin: 0;
|
11
|
+
}
|
12
|
+
|
13
|
+
.content {
|
14
|
+
display: flex;
|
15
|
+
height: 100%;
|
16
|
+
}
|
17
|
+
|
18
|
+
.box {
|
19
|
+
background-color: white;
|
20
|
+
padding-left: 20px;
|
21
|
+
padding-right: 20px;
|
22
|
+
padding-bottom: 20px;
|
23
|
+
border-radius: 3px;
|
24
|
+
max-width: 400px;
|
25
|
+
margin: 50px auto auto;
|
26
|
+
|
27
|
+
-webkit-box-shadow: 0 0 10px 1px rgba(0,0,0,0.5);
|
28
|
+
-moz-box-shadow: 0 0 10px 1px rgba(0,0,0,0.5);
|
29
|
+
box-shadow: 0 0 10px 1px rgba(0,0,0,0.5);
|
30
|
+
}
|
31
|
+
|
32
|
+
.headline {
|
33
|
+
margin-top: 15px;
|
34
|
+
margin-left: -20px;
|
35
|
+
margin-right: -20px;
|
36
|
+
padding: 10px 20px;
|
37
|
+
background-color: #660000;
|
38
|
+
text-align: center;
|
39
|
+
}
|
40
|
+
|
41
|
+
.description {
|
42
|
+
font-size: small;
|
43
|
+
color: #606060;
|
44
|
+
}
|
45
|
+
|
46
|
+
h1 {
|
47
|
+
color: white;
|
48
|
+
font-size: 12pt;
|
49
|
+
margin: 0;
|
50
|
+
}
|
51
|
+
|
52
|
+
.path {
|
53
|
+
font-weight: bold;
|
54
|
+
font-size: small;
|
55
|
+
padding-right: 20px;
|
56
|
+
}
|
57
|
+
|
58
|
+
.message {
|
59
|
+
font-size: small;
|
60
|
+
}
|
61
|
+
|
62
|
+
td {
|
63
|
+
padding-bottom: 5px;
|
64
|
+
}
|
65
|
+
</style>
|
66
|
+
<title>Bad Request</title>
|
67
|
+
</head>
|
68
|
+
<body>
|
69
|
+
<div class="content">
|
70
|
+
<div class="box">
|
71
|
+
<div class="headline">
|
72
|
+
<h1>Invalid Parameters</h1>
|
73
|
+
</div>
|
74
|
+
<p class="description">The request was invalid because the validation of the parameters has failed for the following reasons:</p>
|
75
|
+
<table>
|
76
|
+
<% result.errors.each do |error| %>
|
77
|
+
<tr>
|
78
|
+
<td class="path"><%= error[:path].join(' / ') %></td>
|
79
|
+
<td class="message"><%= error[:message] %></td>
|
80
|
+
</tr>
|
81
|
+
<% end %>
|
82
|
+
</table>
|
83
|
+
</div>
|
84
|
+
</div>
|
85
|
+
</body>
|
86
|
+
</html>
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module RailsParamValidation
|
2
|
+
module ActionControllerExtension
|
3
|
+
def self.included(klass)
|
4
|
+
klass.send(:before_action, :auto_validate_params!)
|
5
|
+
end
|
6
|
+
|
7
|
+
# The before_action function called which does the actual work
|
8
|
+
def auto_validate_params!
|
9
|
+
# @type [ActionDefinition] definition
|
10
|
+
definition = RailsParamValidation::AnnotationManager.instance.method_annotation self.class.name, action_name.to_sym, :param_definition
|
11
|
+
|
12
|
+
if definition.nil?
|
13
|
+
if RailsParamValidation.config.raise_on_missing_annotation
|
14
|
+
raise RailsParamValidation::MissingParameterAnnotation.new
|
15
|
+
else
|
16
|
+
return
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
return unless definition.param_validation?
|
21
|
+
|
22
|
+
parameters = {}
|
23
|
+
params.each do |param, value|
|
24
|
+
# The params array contains the name and the controller, so we need to remove it
|
25
|
+
if param == 'action' || param == 'controller'
|
26
|
+
next
|
27
|
+
end
|
28
|
+
|
29
|
+
# Convert ActionController::Parameters to a normal hash
|
30
|
+
parameters[param.to_s] = _to_hash_type value
|
31
|
+
end
|
32
|
+
|
33
|
+
action = params['action']
|
34
|
+
controller = params['controller']
|
35
|
+
|
36
|
+
validator = _validator_from_schema controller, action, definition.to_schema
|
37
|
+
result = validator.matches?([], parameters)
|
38
|
+
|
39
|
+
if result.matches?
|
40
|
+
# Copy the parameters if the validation succeeded
|
41
|
+
@validated_parameters = result.value
|
42
|
+
else
|
43
|
+
# Render an appropriate error message
|
44
|
+
_render_invalid_param_response result
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def params
|
49
|
+
@validated_parameters || super
|
50
|
+
end
|
51
|
+
|
52
|
+
def _render_invalid_param_response(result)
|
53
|
+
# Depending on the accept header, choose the way to answer
|
54
|
+
respond_to do |format|
|
55
|
+
format.html do
|
56
|
+
if RailsParamValidation.config.use_default_html_response
|
57
|
+
_create_html_error result
|
58
|
+
else
|
59
|
+
raise ParamValidationFailedError.new(result)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
format.json do
|
63
|
+
if RailsParamValidation.config.use_default_json_response
|
64
|
+
_create_json_error result
|
65
|
+
else
|
66
|
+
raise ParamValidationFailedError.new(result)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Create an empty object as JSON response
|
73
|
+
def _create_json_error(result)
|
74
|
+
render json: { status: :fail, errors: result.error_messages }, status: :bad_request
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create an empty html error page
|
78
|
+
def _create_html_error(result)
|
79
|
+
@@_param_html_error_template ||= File.read(File.dirname(__FILE__) + '/error.template.html.erb')
|
80
|
+
render html: ERB.new(@@_param_html_error_template).result(binding).html_safe, status: :bad_request
|
81
|
+
end
|
82
|
+
|
83
|
+
# Convert params to "normal" types
|
84
|
+
def _to_hash_type(params)
|
85
|
+
return params.map(&method(:_to_hash_type)) if params.is_a?(Array)
|
86
|
+
return params.keys.map { |k| [k, _to_hash_type(params[k])] }.to_h if params.is_a?(ActionController::Parameters)
|
87
|
+
|
88
|
+
params
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Validator]
|
92
|
+
def _validator_from_schema(controller, method, schema)
|
93
|
+
unless RailsParamValidation.config.use_validator_caching
|
94
|
+
return RailsParamValidation::ValidatorFactory.create schema
|
95
|
+
end
|
96
|
+
|
97
|
+
# Setup static key if it doesn't already exist
|
98
|
+
@@cache ||= {}
|
99
|
+
|
100
|
+
# Create and/or return validator
|
101
|
+
@@cache["#{controller}##{method}"] ||= RailsParamValidation::ValidatorFactory.create schema
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
require_relative './routing_helper'
|
4
|
+
|
5
|
+
module RailsParamValidation
|
6
|
+
|
7
|
+
class OpenApi
|
8
|
+
OPENAPI_VERSION = "3.0.0"
|
9
|
+
|
10
|
+
def initialize(title, version, url, description)
|
11
|
+
@info = {
|
12
|
+
title: title, version: version,
|
13
|
+
description: description,
|
14
|
+
url: [url], basePath: URI(url).path
|
15
|
+
}
|
16
|
+
|
17
|
+
@actions = []
|
18
|
+
@tags = {}
|
19
|
+
|
20
|
+
load_from_annotations
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_object
|
24
|
+
object = {
|
25
|
+
openapi: OPENAPI_VERSION,
|
26
|
+
info: { version: @info[:version], title: @info[:title], description: @info[:description] },
|
27
|
+
servers: @info[:url].map { |url| { url: url } },
|
28
|
+
tags: @tags.map { |tag, description| { name: tag, description: description } },
|
29
|
+
paths: {},
|
30
|
+
components: { schemas: {} }
|
31
|
+
}
|
32
|
+
|
33
|
+
@actions.each do |operation|
|
34
|
+
body = operation.params.filter { |_, v| v[:type] == :body }.map { |name, pd| [name, pd[:schema]] }.to_h
|
35
|
+
|
36
|
+
parameters = operation.params.filter { |_, v| v[:type] != :body }.map do |name, pd|
|
37
|
+
param_definition = { name: name, in: pd[:type] }
|
38
|
+
if pd[:description].present?
|
39
|
+
param_definition[:description] = pd[:description]
|
40
|
+
end
|
41
|
+
|
42
|
+
validator = RailsParamValidation::ValidatorFactory.create pd[:schema]
|
43
|
+
param_definition[:required] = true unless validator.is_a? RailsParamValidation::OptionalValidator
|
44
|
+
param_definition[:schema] = validator.to_openapi
|
45
|
+
|
46
|
+
param_definition
|
47
|
+
end
|
48
|
+
|
49
|
+
RoutingHelper.routes_for(operation.controller.to_s.underscore, operation.action.to_s).each do |route|
|
50
|
+
action_definition = {
|
51
|
+
operationId: "#{route[:method].downcase}#{route[:path].split(/[^a-zA-Z0-9]+/).map(&:downcase).map(&:capitalize).join}",
|
52
|
+
tags: [operation.controller],
|
53
|
+
parameters: parameters,
|
54
|
+
responses: operation.responses.map do |status, values|
|
55
|
+
[
|
56
|
+
status.to_s,
|
57
|
+
{
|
58
|
+
description: values[:description],
|
59
|
+
content: {
|
60
|
+
operation.request_body_type => {
|
61
|
+
schema: ValidatorFactory.create(values[:schema]).to_openapi
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
]
|
66
|
+
end.to_h
|
67
|
+
}
|
68
|
+
|
69
|
+
action_definition.merge!(summary: operation.description) if operation.description.present?
|
70
|
+
|
71
|
+
if body.any?
|
72
|
+
body_type_name = "#{operation.controller.capitalize}#{operation.action.capitalize}Body".to_sym
|
73
|
+
AnnotationTypes::CustomT.register(body_type_name, body)
|
74
|
+
|
75
|
+
action_definition[:requestBody] = {
|
76
|
+
content: {
|
77
|
+
operation.request_body_type => {
|
78
|
+
schema: ValidatorFactory.create(AnnotationTypes::CustomT.new(body_type_name)).to_openapi
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
object[:paths][route[:path]] ||= {}
|
85
|
+
object[:paths][route[:path]][route[:method].downcase.to_sym] = action_definition
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
AnnotationTypes::CustomT.types.each do |name|
|
90
|
+
object[:components][:schemas][name] = ValidatorFactory.create(AnnotationTypes::CustomT.registered(name)).to_openapi
|
91
|
+
end
|
92
|
+
|
93
|
+
stringify_values object
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
def load_from_annotations
|
99
|
+
AnnotationManager.instance.classes.each do |klass|
|
100
|
+
description = AnnotationManager.instance.class_annotation klass, :description
|
101
|
+
|
102
|
+
if description
|
103
|
+
@tags[RailsHelper.controller_to_tag klass] = description
|
104
|
+
end
|
105
|
+
|
106
|
+
AnnotationManager.instance.methods(klass).each do |method|
|
107
|
+
params = AnnotationManager.instance.method_annotation klass, method, :param_definition
|
108
|
+
next if params.nil?
|
109
|
+
|
110
|
+
@actions.push params
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def stringify_values(object)
|
116
|
+
if object.is_a? Hash
|
117
|
+
return object.map { |k, v| [stringify_values(k), stringify_values(v)] }.to_h
|
118
|
+
elsif object.is_a? Array
|
119
|
+
return object.map { |k| stringify_values k }
|
120
|
+
elsif object.is_a? Symbol
|
121
|
+
return object.to_s
|
122
|
+
else
|
123
|
+
return object
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module RailsParamValidation
|
2
|
+
class Formatter < ActionDispatch::Journey::Visitors::FunctionalVisitor
|
3
|
+
def binary(node, seed)
|
4
|
+
visit(node.right, visit(node.left, seed))
|
5
|
+
end
|
6
|
+
|
7
|
+
def nary(node, seed)
|
8
|
+
node.children.inject(seed) { |s, c| visit(c, s) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def terminal(node, seed)
|
12
|
+
seed.map { |s| s + node.left }
|
13
|
+
end
|
14
|
+
|
15
|
+
def visit_GROUP(node, seed)
|
16
|
+
visit(node.left, seed.dup) + seed
|
17
|
+
end
|
18
|
+
|
19
|
+
def visit_SYMBOL(node, seed)
|
20
|
+
name = node.left
|
21
|
+
name = name[1..-1] if name[0] == ":"
|
22
|
+
seed.map { |s| s + "{#{name}}" }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class RoutingHelper
|
27
|
+
def self.routes_for(controller, action)
|
28
|
+
routes = []
|
29
|
+
Rails.application.routes.routes.each do |route|
|
30
|
+
if route.defaults[:controller] == controller && route.defaults[:action] == action
|
31
|
+
Formatter.new.accept(route.path.ast, [""]).each do |path|
|
32
|
+
routes.push(path: path, method: route.verb)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
routes
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative './extensions/validation_extension'
|
2
|
+
require_relative './extensions/annotation_extension'
|
3
|
+
require_relative './extensions/custom_type_extension'
|
4
|
+
|
5
|
+
require_relative './openapi/openapi'
|
6
|
+
|
7
|
+
require_relative '../errors/param_validation_failed_error'
|
8
|
+
require_relative '../errors/missing_parameter_annotation'
|
9
|
+
require_relative '../errors/type_not_found'
|
10
|
+
|
11
|
+
require_relative './config'
|
12
|
+
require_relative './helper'
|
13
|
+
|
14
|
+
module RailsParamValidation
|
15
|
+
class Railtie < Rails::Railtie
|
16
|
+
railtie_name :param_validation
|
17
|
+
|
18
|
+
initializer 'rails_param_validation.action_controller_extension' do
|
19
|
+
ActionController::Base.send :include, ActionControllerExtension
|
20
|
+
ActionController::Base.send :include, AnnotationExtension
|
21
|
+
ActionController::Base.send :include, CustomTypesExtension
|
22
|
+
ActionController::Base.send :extend, RailsParamValidation::Types
|
23
|
+
end
|
24
|
+
|
25
|
+
rake_tasks do
|
26
|
+
path = File.expand_path(__dir__)
|
27
|
+
Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
namespace :openapi do
|
2
|
+
|
3
|
+
desc "Export OpenAPI definition to to openapi.yaml"
|
4
|
+
task export: :environment do
|
5
|
+
# Ensure all controllers are loaded
|
6
|
+
if defined? Zeitwerk
|
7
|
+
# Due to a regression using rails 6 (https://github.com/rails/rails/issues/37006),
|
8
|
+
# we need to call zeitwerk explicitly
|
9
|
+
Zeitwerk::Loader.eager_load_all
|
10
|
+
else
|
11
|
+
Rails.application.eager_load!
|
12
|
+
end
|
13
|
+
|
14
|
+
openapi = RailsParamValidation::OpenApi.new(
|
15
|
+
RailsParamValidation.openapi.title,
|
16
|
+
RailsParamValidation.openapi.version,
|
17
|
+
RailsParamValidation.openapi.url,
|
18
|
+
RailsParamValidation.openapi.description
|
19
|
+
)
|
20
|
+
|
21
|
+
filename = Rails.root.join("openapi.yaml").to_s
|
22
|
+
print "Writing #{filename}..."
|
23
|
+
|
24
|
+
begin
|
25
|
+
File.open(filename, "w") { |f| f.write YAML.dump(openapi.to_object) }
|
26
|
+
puts " done."
|
27
|
+
rescue Exception => e
|
28
|
+
puts " failed."
|
29
|
+
raise e
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|