api-regulator 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 +16 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +221 -0
- data/LICENSE.txt +21 -0
- data/README.md +154 -0
- data/Rakefile +6 -0
- data/api-regulator.gemspec +48 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/api-regulator.rb +35 -0
- data/lib/api_regulator/api.rb +82 -0
- data/lib/api_regulator/configuration.rb +18 -0
- data/lib/api_regulator/controller_mixin.rb +60 -0
- data/lib/api_regulator/dsl.rb +30 -0
- data/lib/api_regulator/formats.rb +11 -0
- data/lib/api_regulator/open_api_generator.rb +224 -0
- data/lib/api_regulator/param.rb +55 -0
- data/lib/api_regulator/shared_schema.rb +37 -0
- data/lib/api_regulator/validation_error.rb +11 -0
- data/lib/api_regulator/validator.rb +245 -0
- data/lib/api_regulator/version.rb +3 -0
- data/lib/api_regulator.rb +1 -0
- data/lib/tasks/api_regulator_tasks.rake +67 -0
- metadata +186 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
module ApiRegulator
|
2
|
+
module ControllerMixin
|
3
|
+
def validate_params!
|
4
|
+
validator_class = Validator.get(params[:controller], params[:action])
|
5
|
+
|
6
|
+
unless validator_class
|
7
|
+
raise "No validator found for HTTP method #{request.method} and API path #{api_definition.path}"
|
8
|
+
end
|
9
|
+
|
10
|
+
validator = validator_class.new(api_params)
|
11
|
+
unless validator.valid?
|
12
|
+
raise ApiRegulator::ValidationError.new(validator.errors)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def api_definition
|
17
|
+
return @api_definition if defined?(@api_definition)
|
18
|
+
|
19
|
+
@api_definition ||= self.class.api_definitions.find do |d|
|
20
|
+
d.controller_path == params[:controller] && d.action_name == params[:action]
|
21
|
+
end
|
22
|
+
|
23
|
+
raise "API definition not found for #{params[:controller]}##{params[:action]}" unless @api_definition
|
24
|
+
|
25
|
+
@api_definition
|
26
|
+
end
|
27
|
+
|
28
|
+
def api_params
|
29
|
+
path_params = api_definition.params.select(&:path?).each_with_object({}) do |param, hash|
|
30
|
+
hash[param.name.to_sym] = params[param.name] if params.key?(param.name)
|
31
|
+
end.symbolize_keys
|
32
|
+
|
33
|
+
query_params = api_definition.params.select(&:query?).each_with_object({}) do |param, hash|
|
34
|
+
hash[param.name.to_sym] = params[param.name] if params.key?(param.name)
|
35
|
+
end.symbolize_keys
|
36
|
+
|
37
|
+
body_params = api_definition.params.select(&:body?)
|
38
|
+
permitted_body = body_params.each_with_object({}) do |param, hash|
|
39
|
+
if params.key?(param.name)
|
40
|
+
params.require(param.name).permit(*build_permitted_keys(param.children)).to_h.symbolize_keys
|
41
|
+
hash[param.name.to_sym] = params.require(param.name).permit(*build_permitted_keys(param.children)).to_h.symbolize_keys
|
42
|
+
end
|
43
|
+
end.symbolize_keys
|
44
|
+
|
45
|
+
path_params.merge(query_params).merge(permitted_body).symbolize_keys
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def build_permitted_keys(params)
|
51
|
+
params.map do |param|
|
52
|
+
if param.children.any?
|
53
|
+
{ param.name.to_sym => build_permitted_keys(param.children) }
|
54
|
+
else
|
55
|
+
param.name.to_sym
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module ApiRegulator
|
4
|
+
module DSL
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def api(controller_class, action, description, &block)
|
13
|
+
@api_definitions ||= []
|
14
|
+
|
15
|
+
api_definition = Api.new(
|
16
|
+
controller_class,
|
17
|
+
action.to_s,
|
18
|
+
description,
|
19
|
+
&block
|
20
|
+
)
|
21
|
+
|
22
|
+
@api_definitions << api_definition
|
23
|
+
end
|
24
|
+
|
25
|
+
def api_definitions
|
26
|
+
@api_definitions || []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module ApiRegulator
|
4
|
+
class OpenApiGenerator
|
5
|
+
def self.generate(api_definitions)
|
6
|
+
schema = {
|
7
|
+
openapi: '3.1.0', # Explicitly target OpenAPI 3.1.0
|
8
|
+
info: {
|
9
|
+
title: ApiRegulator.configuration.app_name,
|
10
|
+
version: '1.0.0',
|
11
|
+
description: 'Generated by ApiRegulator'
|
12
|
+
},
|
13
|
+
servers: ApiRegulator.configuration.servers,
|
14
|
+
paths: {}
|
15
|
+
}
|
16
|
+
|
17
|
+
add_components(schema)
|
18
|
+
add_security(schema)
|
19
|
+
|
20
|
+
api_definitions.each do |api|
|
21
|
+
add_api_to_schema(schema, api)
|
22
|
+
end
|
23
|
+
|
24
|
+
File.write(ApiRegulator.configuration.docs_path, JSON.pretty_generate(schema))
|
25
|
+
puts "OpenAPI schema generated: #{ApiRegulator.configuration.docs_path}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def self.add_api_to_schema(schema, api)
|
31
|
+
schema[:paths][api.path] ||= {}
|
32
|
+
data = {
|
33
|
+
summary: api.description,
|
34
|
+
description: api.description,
|
35
|
+
operationId: api.operation_id,
|
36
|
+
tags: api.tags,
|
37
|
+
parameters: generate_parameters(api),
|
38
|
+
responses: generate_responses(api)
|
39
|
+
}
|
40
|
+
|
41
|
+
data[:requestBody] = generate_request_body(api) if api.allows_body?
|
42
|
+
data.delete(:requestBody) if data[:requestBody].blank?
|
43
|
+
|
44
|
+
schema[:paths][api.path][api.http_method] = data
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.generate_request_body(api)
|
48
|
+
params = api.params.select(&:body?)
|
49
|
+
return {} if params.empty?
|
50
|
+
|
51
|
+
{
|
52
|
+
required: true,
|
53
|
+
content: {
|
54
|
+
'application/json' => {
|
55
|
+
schema: expand_nested_params(params)
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.expand_nested_params(params)
|
62
|
+
schema = {
|
63
|
+
type: 'object',
|
64
|
+
properties: {},
|
65
|
+
required: []
|
66
|
+
}
|
67
|
+
|
68
|
+
params.each do |param|
|
69
|
+
schema[:properties][param.name] =
|
70
|
+
if param.type == :array
|
71
|
+
generate_array_schema(param)
|
72
|
+
elsif param.children.any?
|
73
|
+
generate_object_schema(param)
|
74
|
+
else
|
75
|
+
generate_param_schema(param)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Add to required array if marked as required
|
79
|
+
schema[:required] << param.name if param.options[:presence]
|
80
|
+
end
|
81
|
+
|
82
|
+
# Remove the required key if it's empty (not needed in OpenAPI spec)
|
83
|
+
schema.delete(:required) if schema[:required].empty?
|
84
|
+
schema
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.generate_array_schema(param)
|
88
|
+
item_schema = param.children.any? ? generate_object_schema(param) : { type: param.item_type }
|
89
|
+
|
90
|
+
{
|
91
|
+
type: 'array',
|
92
|
+
items: item_schema,
|
93
|
+
description: param.desc.presence
|
94
|
+
}.compact
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.generate_object_schema(param)
|
98
|
+
object_schema = expand_nested_params(param.children)
|
99
|
+
object_schema[:description] = param.desc if param.desc.present?
|
100
|
+
object_schema
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.generate_parameters(api)
|
104
|
+
api.params.select(&:parameter?).map do |p|
|
105
|
+
generate_param_schema(p)
|
106
|
+
.merge({
|
107
|
+
name: p.name,
|
108
|
+
required: p.location == :path || p.options[:presence] || false
|
109
|
+
})
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.generate_param_schema(param)
|
114
|
+
schema = {}
|
115
|
+
|
116
|
+
if param.parameter?
|
117
|
+
schema[:in] = param.location
|
118
|
+
schema[:schema] = { type: param.type.to_s.downcase }
|
119
|
+
else
|
120
|
+
schema[:type] = param.type.to_s.downcase
|
121
|
+
end
|
122
|
+
|
123
|
+
schema[:description] = param.desc if param.desc.present?
|
124
|
+
|
125
|
+
|
126
|
+
# Add length constraints
|
127
|
+
if param.options[:length]
|
128
|
+
schema[:minLength] = param.options[:length][:minimum] if param.options[:length][:minimum]
|
129
|
+
schema[:maxLength] = param.options[:length][:maximum] if param.options[:length][:maximum]
|
130
|
+
end
|
131
|
+
|
132
|
+
# Add format constraints
|
133
|
+
if param.options[:format]
|
134
|
+
if param.options[:format][:with] == Formats::EMAIL
|
135
|
+
schema[:format] = 'email'
|
136
|
+
elsif param.options[:format][:with] == Formats::DATE
|
137
|
+
schema[:format] = 'date'
|
138
|
+
elsif param.options[:format][:with] == Formats::URI
|
139
|
+
schema[:format] = 'uri'
|
140
|
+
elsif param.options[:format][:with].is_a?(Regexp)
|
141
|
+
schema[:pattern] = param.options[:format][:with].source
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
if param.options[:numericality]
|
146
|
+
schema[:type] = param.options[:only_integer] ? 'integer' : 'number'
|
147
|
+
schema[:minimum] = param.options[:greater_than_or_equal_to] if param.options[:greater_than_or_equal_to]
|
148
|
+
schema[:maximum] = param.options[:less_than_or_equal_to] if param.options[:less_than_or_equal_to]
|
149
|
+
schema[:exclusiveMinimum] = param.options[:greater_than] if param.options[:greater_than]
|
150
|
+
schema[:exclusiveMaximum] = param.options[:less_than] if param.options[:less_than]
|
151
|
+
end
|
152
|
+
|
153
|
+
if param.options[:inclusion]
|
154
|
+
schema[:enum] = param.options[:inclusion][:in]
|
155
|
+
end
|
156
|
+
|
157
|
+
if param.options[:exclusion]
|
158
|
+
schema[:not] = { enum: param.options[:exclusion][:in] }
|
159
|
+
end
|
160
|
+
|
161
|
+
schema[:nullable] = true if param.options[:allow_nil]
|
162
|
+
|
163
|
+
schema
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.generate_responses(api)
|
167
|
+
api.responses.each_with_object({}) do |(status_code, schema), responses|
|
168
|
+
if schema.options[:ref]
|
169
|
+
shared_schema = ApiRegulator.shared_schemas[schema.options[:ref]]
|
170
|
+
raise "Shared schema not found for ref: #{schema.options[:ref]}" unless shared_schema
|
171
|
+
|
172
|
+
responses[status_code.to_s] = {
|
173
|
+
description: shared_schema.description.presence,
|
174
|
+
content: {
|
175
|
+
"application/json" => {
|
176
|
+
schema: { "$ref" => "#/components/schemas/#{schema.options[:ref]}" }
|
177
|
+
}
|
178
|
+
}
|
179
|
+
}.compact
|
180
|
+
else
|
181
|
+
responses[status_code.to_s] = {
|
182
|
+
description: schema.desc.presence,
|
183
|
+
content: {
|
184
|
+
"application/json" => {
|
185
|
+
schema: expand_nested_params(schema.children)
|
186
|
+
}
|
187
|
+
}
|
188
|
+
}.compact
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.add_components(schema)
|
194
|
+
add_shared_schemas(schema)
|
195
|
+
add_security_schemes(schema)
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.add_shared_schemas(schema)
|
199
|
+
return unless ApiRegulator.shared_schemas.present?
|
200
|
+
|
201
|
+
schema[:components] ||= {}
|
202
|
+
schema[:components][:schemas] ||= {}
|
203
|
+
|
204
|
+
ApiRegulator.shared_schemas.each do |name, shared_schema|
|
205
|
+
schema[:components][:schemas][name] = expand_nested_params(shared_schema.params).merge(
|
206
|
+
description: shared_schema.description
|
207
|
+
).compact
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def self.add_security_schemes(schema)
|
212
|
+
return unless ApiRegulator.security_schemes.present?
|
213
|
+
|
214
|
+
schema[:components] ||= {}
|
215
|
+
schema[:components][:securitySchemes] = ApiRegulator.security_schemes
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.add_security(schema)
|
219
|
+
return unless ApiRegulator.security.present?
|
220
|
+
|
221
|
+
schema[:security] = ApiRegulator.security
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module ApiRegulator
|
2
|
+
class Param
|
3
|
+
attr_reader :name, :type, :options, :item_type, :desc, :location, :children
|
4
|
+
|
5
|
+
def initialize(name, type = nil, item_type: nil, desc: "", location: :body, **options, &block)
|
6
|
+
@name = name
|
7
|
+
@type = type&.to_sym || (block_given? ? :object : :string)
|
8
|
+
@item_type = item_type
|
9
|
+
@location = location.to_sym
|
10
|
+
@desc = desc
|
11
|
+
@options = options
|
12
|
+
@children = []
|
13
|
+
|
14
|
+
instance_eval(&block) if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def param(name, type = nil, item_type: nil, desc: "", location: :body, **options, &block)
|
18
|
+
child = Param.new(name, type, item_type: item_type, desc: desc, location: location, **options, &block)
|
19
|
+
@children << child
|
20
|
+
end
|
21
|
+
|
22
|
+
def ref(ref_name)
|
23
|
+
shared_schema = ApiRegulator.shared_schemas[ref_name]
|
24
|
+
raise "Shared schema #{ref_name} not found" unless shared_schema
|
25
|
+
|
26
|
+
shared_schema.params.each do |shared_param|
|
27
|
+
@children << shared_param
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def body?
|
32
|
+
location == :body
|
33
|
+
end
|
34
|
+
|
35
|
+
def query?
|
36
|
+
location == :query
|
37
|
+
end
|
38
|
+
|
39
|
+
def path?
|
40
|
+
location == :path
|
41
|
+
end
|
42
|
+
|
43
|
+
def parameter?
|
44
|
+
[:path, :query].include?(location)
|
45
|
+
end
|
46
|
+
|
47
|
+
def object?
|
48
|
+
type.to_sym == :object
|
49
|
+
end
|
50
|
+
|
51
|
+
def array?
|
52
|
+
type.to_sym == :array
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ApiRegulator
|
2
|
+
class SharedSchema
|
3
|
+
attr_reader :name, :description, :params
|
4
|
+
|
5
|
+
def initialize(name, description, &block)
|
6
|
+
@name = name
|
7
|
+
@description = description
|
8
|
+
@params = []
|
9
|
+
instance_eval(&block) if block_given?
|
10
|
+
end
|
11
|
+
|
12
|
+
def param(name, type = nil, item_type: nil, desc: "", location: :body, **options, &block)
|
13
|
+
param = Param.new(name, type, item_type: item_type, desc: desc, location: location, **options, &block)
|
14
|
+
@params << param
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :security
|
20
|
+
|
21
|
+
def shared_schemas
|
22
|
+
@shared_schemas ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def security_schemes
|
26
|
+
@security_schemes ||= {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def shared_schema(name, description, &block)
|
30
|
+
shared_schemas[name] = SharedSchema.new(name, description, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def security_schemes=(scheme)
|
34
|
+
@security_schemes = scheme
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
require "active_model"
|
2
|
+
|
3
|
+
module ApiRegulator
|
4
|
+
module AttributeDefinitionMixin
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :defined_attributes, default: []
|
9
|
+
class_attribute :nested_validators, default: {}
|
10
|
+
end
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
def define_attribute_and_validations(param, parent_key = nil)
|
14
|
+
# Construct the full key
|
15
|
+
full_key = parent_key ? "#{parent_key}.#{param.name}".to_sym : param.name.to_sym
|
16
|
+
|
17
|
+
case param.type
|
18
|
+
when :array
|
19
|
+
define_array_validations(param, full_key)
|
20
|
+
when :object
|
21
|
+
define_object_validations(param, full_key)
|
22
|
+
else
|
23
|
+
define_scalar_validations(param, full_key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def define_scalar_validations(param, full_key)
|
28
|
+
# Define scalar attributes
|
29
|
+
attribute full_key, param.type if param.type
|
30
|
+
self.defined_attributes += [full_key]
|
31
|
+
|
32
|
+
# Add validations for scalar attributes
|
33
|
+
param.options.each do |option, value|
|
34
|
+
validates full_key, option => value
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add type-specific validations
|
38
|
+
validate -> { validate_boolean(full_key) } if param.type == :boolean
|
39
|
+
validate -> { validate_integer(full_key) } if param.type == :integer
|
40
|
+
validate -> { validate_string(full_key) } if param.type == :string
|
41
|
+
end
|
42
|
+
|
43
|
+
def define_object_validations(param, full_key)
|
44
|
+
# Build nested validator class
|
45
|
+
nested_validator_class = build_nested_validator_class(param.children, param.name, self)
|
46
|
+
|
47
|
+
# Add a custom validation for the nested object
|
48
|
+
validate -> { validate_nested_object(full_key, nested_validator_class, param) }
|
49
|
+
|
50
|
+
# Store the nested validator
|
51
|
+
nested_validators[full_key] = nested_validator_class
|
52
|
+
end
|
53
|
+
|
54
|
+
def define_array_validations(param, full_key)
|
55
|
+
if param.children.any?
|
56
|
+
# Build a nested validator class for array items
|
57
|
+
item_validator_class = build_nested_validator_class(param.children, param.name, self)
|
58
|
+
validate -> { validate_array_of_objects(full_key, item_validator_class, param) }
|
59
|
+
elsif param.item_type
|
60
|
+
# Scalar array with specific item type
|
61
|
+
validate -> { validate_array_of_scalars(full_key, param) }
|
62
|
+
else
|
63
|
+
raise "Arrays must have children or an item_type"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def build_nested_validator_class(children, parent_key, parent_class)
|
68
|
+
# Create a unique class name based on the parent key
|
69
|
+
class_name = "#{parent_key.to_s.camelize}"
|
70
|
+
|
71
|
+
# Return the existing class if already defined
|
72
|
+
if parent_class.const_defined?(class_name, false)
|
73
|
+
return parent_class.const_get(class_name)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Create the nested validator class
|
77
|
+
nested_validator_class = Class.new do
|
78
|
+
include ActiveModel::Model
|
79
|
+
include ActiveModel::Attributes
|
80
|
+
include AttributeDefinitionMixin
|
81
|
+
|
82
|
+
def initialize(attributes = {})
|
83
|
+
@raw_attributes = attributes
|
84
|
+
allowed_attributes = attributes.slice(*self.class.defined_attributes.map(&:to_sym))
|
85
|
+
super(allowed_attributes)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Add child attributes and validations
|
90
|
+
children.each do |child|
|
91
|
+
nested_validator_class.define_attribute_and_validations(child)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Store the nested class under the parent class namespace
|
95
|
+
parent_class.const_set(class_name, nested_validator_class)
|
96
|
+
nested_validator_class
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Instance methods
|
101
|
+
def validate_array_of_objects(attribute, item_validator_class, param)
|
102
|
+
raw_value = @raw_attributes[attribute]
|
103
|
+
if raw_value.blank?
|
104
|
+
errors.add(attribute, "can't be blank") if param.options[:presence]
|
105
|
+
return
|
106
|
+
end
|
107
|
+
unless raw_value.is_a?(Array)
|
108
|
+
errors.add(attribute, "must be an array")
|
109
|
+
return
|
110
|
+
end
|
111
|
+
|
112
|
+
raw_value.each_with_index do |value, index|
|
113
|
+
unless value.is_a?(Hash)
|
114
|
+
errors.add("#{attribute}[#{index}]", "must be a hash")
|
115
|
+
next
|
116
|
+
end
|
117
|
+
|
118
|
+
validator = item_validator_class.new(value)
|
119
|
+
unless validator.valid?
|
120
|
+
validator.errors.each do |error|
|
121
|
+
nested_attr = "#{attribute}[#{index}].#{error.attribute}"
|
122
|
+
errors.add(nested_attr, error.message)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def validate_array_of_scalars(attribute, param)
|
129
|
+
raw_value = @raw_attributes[attribute]
|
130
|
+
|
131
|
+
if raw_value.nil?
|
132
|
+
errors.add(attribute, "can't be blank") if param.options[:presence]
|
133
|
+
return
|
134
|
+
end
|
135
|
+
unless raw_value.is_a?(Array)
|
136
|
+
errors.add(attribute, "must be an array")
|
137
|
+
return
|
138
|
+
end
|
139
|
+
|
140
|
+
raw_value.each_with_index do |value, index|
|
141
|
+
case param.item_type
|
142
|
+
when :string
|
143
|
+
unless value.is_a?(String)
|
144
|
+
errors.add("#{attribute}[#{index}]", "must be a string")
|
145
|
+
end
|
146
|
+
when :integer
|
147
|
+
unless value.is_a?(Integer)
|
148
|
+
errors.add("#{attribute}[#{index}]", "must be an integer")
|
149
|
+
end
|
150
|
+
when :boolean
|
151
|
+
unless [true, false].include?(value)
|
152
|
+
errors.add("#{attribute}[#{index}]", "must be a boolean")
|
153
|
+
end
|
154
|
+
else
|
155
|
+
errors.add("#{attribute}[#{index}]", "has an unsupported type")
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def validate_nested_object(attribute, nested_validator_class, param)
|
161
|
+
raw_value = @raw_attributes[attribute]
|
162
|
+
|
163
|
+
if raw_value.nil?
|
164
|
+
errors.add(attribute, "can't be blank") if param.options[:presence]
|
165
|
+
return
|
166
|
+
end
|
167
|
+
unless raw_value.is_a?(Hash)
|
168
|
+
errors.add(attribute, "must be a hash")
|
169
|
+
return
|
170
|
+
end
|
171
|
+
|
172
|
+
# Initialize the nested validator with the raw value
|
173
|
+
validator = nested_validator_class.new(raw_value)
|
174
|
+
|
175
|
+
# If validation fails, propagate errors
|
176
|
+
unless validator.valid?
|
177
|
+
validator.errors.each do |error|
|
178
|
+
nested_attr = "#{attribute}.#{error.attribute}"
|
179
|
+
errors.add(nested_attr, error.message)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def validate_boolean(attribute)
|
185
|
+
raw_value = @raw_attributes[attribute]
|
186
|
+
unless [true, false, "true", "false", nil].include?(raw_value)
|
187
|
+
errors.add(attribute, "must be a boolean (true, false, or nil)")
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def validate_integer(attribute)
|
192
|
+
raw_value = @raw_attributes[attribute]
|
193
|
+
unless raw_value.to_s =~ /\A[-+]?\d+\z/ || raw_value.nil?
|
194
|
+
errors.add(attribute, "must be an integer")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def validate_string(attribute)
|
199
|
+
raw_value = @raw_attributes[attribute]
|
200
|
+
unless raw_value.is_a?(String) || raw_value.nil?
|
201
|
+
errors.add(attribute, "must be a string")
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
class Validator
|
207
|
+
include ActiveModel::Model
|
208
|
+
include ActiveModel::Attributes
|
209
|
+
include AttributeDefinitionMixin
|
210
|
+
|
211
|
+
@validators = {}
|
212
|
+
|
213
|
+
def self.build_all(api_definitions)
|
214
|
+
api_definitions.each do |api_definition|
|
215
|
+
class_name = "#{api_definition.controller_path}/#{api_definition.action_name}".gsub("/", "_").camelcase
|
216
|
+
validator_class = build_class(api_definition)
|
217
|
+
@validators[[api_definition.controller_path, api_definition.action_name]] = validator_class
|
218
|
+
Validator.const_set(class_name, validator_class)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.get(controller, action)
|
223
|
+
@validators[[controller, action]]
|
224
|
+
end
|
225
|
+
|
226
|
+
def self.build_class(api_definition)
|
227
|
+
Class.new(self) do
|
228
|
+
api_definition.params.each do |param|
|
229
|
+
define_attribute_and_validations(param)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def initialize(attributes = {})
|
235
|
+
@raw_attributes = attributes.deep_symbolize_keys
|
236
|
+
allowed_attributes = attributes.slice(*self.class.defined_attributes.map(&:to_sym))
|
237
|
+
super(allowed_attributes)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
class FooValidator
|
243
|
+
include ActiveModel::Model
|
244
|
+
include ActiveModel::Attributes
|
245
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'api-regulator'
|