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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.gitlab-ci.yml +34 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +63 -0
  7. data/Rakefile +6 -0
  8. data/bin/.keep +0 -0
  9. data/docs/_config.yml +3 -0
  10. data/docs/annotations.md +62 -0
  11. data/docs/getting-started.md +32 -0
  12. data/docs/image/error-screenshot.png +0 -0
  13. data/docs/index.md +61 -0
  14. data/docs/main-idea.md +72 -0
  15. data/docs/openapi.md +39 -0
  16. data/docs/type-definition.md +178 -0
  17. data/lib/rails-param-validation/errors/missing_parameter_annotation.rb +9 -0
  18. data/lib/rails-param-validation/errors/no_matching_factory.rb +9 -0
  19. data/lib/rails-param-validation/errors/param_validation_failed_error.rb +12 -0
  20. data/lib/rails-param-validation/errors/type_not_found.rb +9 -0
  21. data/lib/rails-param-validation/rails/action_definition.rb +66 -0
  22. data/lib/rails-param-validation/rails/annotation_manager.rb +40 -0
  23. data/lib/rails-param-validation/rails/config.rb +48 -0
  24. data/lib/rails-param-validation/rails/extensions/annotation_extension.rb +95 -0
  25. data/lib/rails-param-validation/rails/extensions/custom_type_extension.rb +13 -0
  26. data/lib/rails-param-validation/rails/extensions/error.template.html.erb +86 -0
  27. data/lib/rails-param-validation/rails/extensions/validation_extension.rb +105 -0
  28. data/lib/rails-param-validation/rails/helper.rb +9 -0
  29. data/lib/rails-param-validation/rails/openapi/openapi.rb +128 -0
  30. data/lib/rails-param-validation/rails/openapi/routing_helper.rb +40 -0
  31. data/lib/rails-param-validation/rails/rails.rb +31 -0
  32. data/lib/rails-param-validation/rails/tasks/openapi.rake +32 -0
  33. data/lib/rails-param-validation/types/types.rb +100 -0
  34. data/lib/rails-param-validation/validator.rb +51 -0
  35. data/lib/rails-param-validation/validator_factory.rb +37 -0
  36. data/lib/rails-param-validation/validators/alternatives.rb +42 -0
  37. data/lib/rails-param-validation/validators/array.rb +49 -0
  38. data/lib/rails-param-validation/validators/boolean.rb +38 -0
  39. data/lib/rails-param-validation/validators/constant.rb +38 -0
  40. data/lib/rails-param-validation/validators/custom_type.rb +28 -0
  41. data/lib/rails-param-validation/validators/date.rb +39 -0
  42. data/lib/rails-param-validation/validators/datetime.rb +39 -0
  43. data/lib/rails-param-validation/validators/float.rb +39 -0
  44. data/lib/rails-param-validation/validators/hash.rb +52 -0
  45. data/lib/rails-param-validation/validators/integer.rb +39 -0
  46. data/lib/rails-param-validation/validators/object.rb +63 -0
  47. data/lib/rails-param-validation/validators/optional.rb +44 -0
  48. data/lib/rails-param-validation/validators/regex.rb +37 -0
  49. data/lib/rails-param-validation/validators/string.rb +31 -0
  50. data/lib/rails-param-validation/validators/uuid.rb +39 -0
  51. data/lib/rails-param-validation/version.rb +3 -0
  52. data/lib/rails-param-validation.rb +42 -0
  53. data/rails-param-validation.gemspec +33 -0
  54. 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,9 @@
1
+ module RailsParamValidation
2
+
3
+ class RailsHelper
4
+ def self.controller_to_tag(klass)
5
+ (klass.is_a?(String) ? klass : klass.name).gsub(/Controller$/, '').to_sym
6
+ end
7
+ end
8
+
9
+ end
@@ -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