rails-param-validation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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