grape 0.2.1.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of grape might be problematic. Click here for more details.
- data/.gitignore +1 -0
- data/CHANGELOG.markdown +23 -2
- data/Gemfile +2 -0
- data/README.markdown +402 -227
- data/grape.gemspec +5 -2
- data/lib/grape.rb +6 -0
- data/lib/grape/api.rb +59 -2
- data/lib/grape/endpoint.rb +49 -9
- data/lib/grape/entity.rb +75 -8
- data/lib/grape/exceptions/base.rb +17 -0
- data/lib/grape/exceptions/validation_error.rb +10 -0
- data/lib/grape/middleware/base.rb +28 -19
- data/lib/grape/middleware/error.rb +11 -3
- data/lib/grape/middleware/formatter.rb +11 -18
- data/lib/grape/middleware/versioner/header.rb +76 -17
- data/lib/grape/util/deep_merge.rb +23 -0
- data/lib/grape/util/hash_stack.rb +12 -3
- data/lib/grape/validations.rb +202 -0
- data/lib/grape/validations/coerce.rb +61 -0
- data/lib/grape/validations/presence.rb +11 -0
- data/lib/grape/validations/regexp.rb +13 -0
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +281 -123
- data/spec/grape/endpoint_spec.rb +69 -4
- data/spec/grape/entity_spec.rb +204 -16
- data/spec/grape/middleware/exception_spec.rb +21 -0
- data/spec/grape/middleware/formatter_spec.rb +19 -0
- data/spec/grape/middleware/versioner/header_spec.rb +159 -88
- data/spec/grape/validations/coerce_spec.rb +129 -0
- data/spec/grape/validations/presence_spec.rb +138 -0
- data/spec/grape/validations/regexp_spec.rb +33 -0
- data/spec/grape/validations_spec.rb +185 -0
- metadata +65 -74
- data/spec/grape_spec.rb +0 -1
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'active_support/ordered_hash'
|
1
2
|
require 'multi_json'
|
2
3
|
require 'multi_xml'
|
3
4
|
|
@@ -44,14 +45,14 @@ module Grape
|
|
44
45
|
|
45
46
|
|
46
47
|
module Formats
|
47
|
-
|
48
|
-
CONTENT_TYPES =
|
49
|
-
:xml
|
50
|
-
:json
|
51
|
-
:atom
|
52
|
-
:rss
|
53
|
-
:txt
|
54
|
-
|
48
|
+
# Content types are listed in order of preference.
|
49
|
+
CONTENT_TYPES = ActiveSupport::OrderedHash[
|
50
|
+
:xml, 'application/xml',
|
51
|
+
:json, 'application/json',
|
52
|
+
:atom, 'application/atom+xml',
|
53
|
+
:rss, 'application/rss+xml',
|
54
|
+
:txt, 'text/plain',
|
55
|
+
]
|
55
56
|
FORMATTERS = {
|
56
57
|
:json => :encode_json,
|
57
58
|
:txt => :encode_txt,
|
@@ -70,10 +71,6 @@ module Grape
|
|
70
71
|
PARSERS.merge(options[:parsers] || {})
|
71
72
|
end
|
72
73
|
|
73
|
-
def content_type_for(format)
|
74
|
-
Hash.new(content_types)[format.to_sym]
|
75
|
-
end
|
76
|
-
|
77
74
|
def content_types
|
78
75
|
CONTENT_TYPES.merge(options[:content_types] || {})
|
79
76
|
end
|
@@ -114,20 +111,32 @@ module Grape
|
|
114
111
|
MultiJson.load(object)
|
115
112
|
end
|
116
113
|
|
117
|
-
def
|
118
|
-
|
114
|
+
def serializable?(object)
|
115
|
+
object.respond_to?(:serializable_hash) ||
|
116
|
+
object.kind_of?(Array) && !object.map {|o| o.respond_to? :serializable_hash }.include?(false) ||
|
117
|
+
object.kind_of?(Hash)
|
118
|
+
end
|
119
119
|
|
120
|
+
def serialize(object)
|
120
121
|
if object.respond_to? :serializable_hash
|
121
|
-
|
122
|
+
object.serializable_hash
|
122
123
|
elsif object.kind_of?(Array) && !object.map {|o| o.respond_to? :serializable_hash }.include?(false)
|
123
|
-
|
124
|
-
elsif object.
|
125
|
-
object.
|
124
|
+
object.map {|o| o.serializable_hash }
|
125
|
+
elsif object.kind_of?(Hash)
|
126
|
+
object.inject({}) { |h,(k,v)| h[k] = serialize(v); h }
|
126
127
|
else
|
127
|
-
|
128
|
+
object
|
128
129
|
end
|
129
130
|
end
|
130
131
|
|
132
|
+
def encode_json(object)
|
133
|
+
return object if object.is_a?(String)
|
134
|
+
return MultiJson.dump(serialize(object)) if serializable?(object)
|
135
|
+
return object.to_json if object.respond_to?(:to_json)
|
136
|
+
|
137
|
+
MultiJson.dump(object)
|
138
|
+
end
|
139
|
+
|
131
140
|
def encode_txt(object)
|
132
141
|
object.respond_to?(:to_txt) ? object.to_txt : object.to_s
|
133
142
|
end
|
@@ -44,11 +44,19 @@ module Grape
|
|
44
44
|
return @app.call(@env)
|
45
45
|
})
|
46
46
|
rescue Exception => e
|
47
|
-
|
48
|
-
|
47
|
+
is_rescuable = rescuable?(e.class)
|
48
|
+
if e.is_a?(Grape::Exceptions::Base) && !is_rescuable
|
49
|
+
handler = lambda {|e| error_response(e) }
|
50
|
+
else
|
51
|
+
raise unless is_rescuable
|
52
|
+
handler = options[:rescue_handlers][e.class] || options[:rescue_handlers][:all]
|
53
|
+
end
|
49
54
|
handler.nil? ? handle_error(e) : self.instance_exec(e, &handler)
|
50
55
|
end
|
51
|
-
|
56
|
+
end
|
57
|
+
|
58
|
+
def rescuable?(klass)
|
59
|
+
options[:rescue_all] || (options[:rescued_errors] || []).include?(klass)
|
52
60
|
end
|
53
61
|
|
54
62
|
def handle_error(e)
|
@@ -19,29 +19,17 @@ module Grape
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def before
|
22
|
-
fmt = format_from_extension || options[:format] || format_from_header || options[:default_format]
|
22
|
+
fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
|
23
23
|
if content_types.key?(fmt)
|
24
24
|
if !env['rack.input'].nil? and (body = env['rack.input'].read).strip.length != 0
|
25
25
|
parser = parser_for fmt
|
26
26
|
unless parser.nil?
|
27
27
|
begin
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
body = parser.call body
|
34
|
-
env['rack.request.form_hash'] = !env['rack.request.form_hash'].nil? ? env['rack.request.form_hash'].merge(body) : body
|
35
|
-
env['rack.request.form_input'] = env['rack.input']
|
36
|
-
rescue
|
37
|
-
# It's possible that it's just regular POST content -- just back off
|
38
|
-
end
|
39
|
-
end
|
40
|
-
else
|
41
|
-
throw :error, :status => 406, :message => 'The requested content-type is not supported.'
|
42
|
-
end
|
43
|
-
ensure
|
44
|
-
env['rack.input'].rewind
|
28
|
+
body = parser.call(body)
|
29
|
+
env['rack.request.form_hash'] = !env['rack.request.form_hash'].nil? ? env['rack.request.form_hash'].merge(body) : body
|
30
|
+
env['rack.request.form_input'] = env['rack.input']
|
31
|
+
rescue
|
32
|
+
# It's possible that it's just regular POST content -- just back off
|
45
33
|
end
|
46
34
|
end
|
47
35
|
env['rack.input'].rewind
|
@@ -62,6 +50,11 @@ module Grape
|
|
62
50
|
nil
|
63
51
|
end
|
64
52
|
|
53
|
+
def format_from_params
|
54
|
+
fmt = Rack::Utils.parse_nested_query(env['QUERY_STRING'])["format"]
|
55
|
+
fmt ? fmt.to_sym : nil
|
56
|
+
end
|
57
|
+
|
65
58
|
def format_from_header
|
66
59
|
mime_array.each do |t|
|
67
60
|
if mime_types.key?(t)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'grape/middleware/base'
|
2
|
+
require 'rack/accept'
|
2
3
|
|
3
4
|
module Grape
|
4
5
|
module Middleware
|
@@ -21,37 +22,95 @@ module Grape
|
|
21
22
|
# If version does not match this route, then a 406 is throw with
|
22
23
|
# X-Cascade header to alert Rack::Mount to attempt the next matched
|
23
24
|
# route.
|
25
|
+
#
|
26
|
+
# @throws [RuntimeError] if Accept header is invalid
|
24
27
|
class Header < Base
|
28
|
+
include Formats
|
29
|
+
|
25
30
|
def before
|
26
|
-
|
31
|
+
header = Rack::Accept::MediaType.new env['HTTP_ACCEPT']
|
27
32
|
|
28
|
-
if
|
29
|
-
|
30
|
-
|
33
|
+
if strict?
|
34
|
+
# If no Accept header:
|
35
|
+
if header.qvalues.empty?
|
36
|
+
throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => 'Accept header must be set'
|
37
|
+
end
|
38
|
+
# Remove any acceptable content types with ranges.
|
39
|
+
header.qvalues.reject! do |media_type,_|
|
40
|
+
Rack::Accept::Header.parse_media_type(media_type).find{|s| s == '*'}
|
41
|
+
end
|
42
|
+
# If all Accept headers included a range:
|
43
|
+
if header.qvalues.empty?
|
44
|
+
throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => 'Accept header must not contain ranges ("*")'
|
31
45
|
end
|
32
46
|
end
|
33
|
-
|
47
|
+
|
48
|
+
media_type = header.best_of available_media_types
|
49
|
+
|
50
|
+
if media_type
|
51
|
+
type, subtype = Rack::Accept::Header.parse_media_type media_type
|
34
52
|
env['api.type'] = type
|
35
53
|
env['api.subtype'] = subtype
|
36
54
|
|
37
|
-
|
38
|
-
|
39
|
-
|
55
|
+
if /\Avnd\.([a-z0-9*.]+)(?:-([a-z0-9*\-.]+))?(?:\+([a-z0-9*\-.+]+))?\z/ =~ subtype
|
56
|
+
env['api.vendor'] = $1
|
57
|
+
env['api.version'] = $2
|
58
|
+
env['api.format'] = $3 # weird that Grape::Middleware::Formatter also does this
|
59
|
+
end
|
60
|
+
# If none of the available content types are acceptable:
|
61
|
+
elsif strict?
|
62
|
+
throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => '406 Not Acceptable'
|
63
|
+
# If all acceptable content types specify a vendor or version that doesn't exist:
|
64
|
+
elsif header.values.all?{|media_type| has_vendor?(media_type) || has_version?(media_type)}
|
65
|
+
throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => 'API vendor or version not found'
|
66
|
+
end
|
67
|
+
end
|
40
68
|
|
41
|
-
|
42
|
-
throw :error, :status => 406, :headers => {'X-Cascade' => 'pass'}, :message => "406 API Version Not Found"
|
43
|
-
end
|
69
|
+
private
|
44
70
|
|
45
|
-
|
46
|
-
|
47
|
-
|
71
|
+
def available_media_types
|
72
|
+
available_media_types = []
|
73
|
+
|
74
|
+
content_types.each do |extension,media_type|
|
75
|
+
versions.reverse.each do |version|
|
76
|
+
available_media_types += ["application/vnd.#{vendor}-#{version}+#{extension}", "application/vnd.#{vendor}-#{version}"]
|
48
77
|
end
|
78
|
+
available_media_types << "application/vnd.#{vendor}+#{extension}"
|
79
|
+
end
|
80
|
+
|
81
|
+
available_media_types << "application/vnd.#{vendor}"
|
82
|
+
|
83
|
+
content_types.each do |_,media_type|
|
84
|
+
available_media_types << media_type
|
49
85
|
end
|
86
|
+
|
87
|
+
available_media_types = available_media_types.flatten
|
88
|
+
end
|
89
|
+
|
90
|
+
def versions
|
91
|
+
options[:versions] || []
|
92
|
+
end
|
93
|
+
|
94
|
+
def vendor
|
95
|
+
options[:version_options] && options[:version_options][:vendor]
|
96
|
+
end
|
97
|
+
|
98
|
+
def strict?
|
99
|
+
options[:version_options] && options[:version_options][:strict]
|
100
|
+
end
|
101
|
+
|
102
|
+
# @param [String] media_type a content type
|
103
|
+
# @return [Boolean] whether the content type sets a vendor
|
104
|
+
def has_vendor?(media_type)
|
105
|
+
type, subtype = Rack::Accept::Header.parse_media_type media_type
|
106
|
+
subtype[/\Avnd\.[a-z0-9*.]+/]
|
50
107
|
end
|
51
108
|
|
52
|
-
|
53
|
-
|
54
|
-
|
109
|
+
# @param [String] media_type a content type
|
110
|
+
# @return [Boolean] whether the content type sets an API version
|
111
|
+
def has_version?(media_type)
|
112
|
+
type, subtype = Rack::Accept::Header.parse_media_type media_type
|
113
|
+
subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
|
55
114
|
end
|
56
115
|
end
|
57
116
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Hash
|
2
|
+
# deep_merge from rails
|
3
|
+
# activesupport/lib/active_support/core_ext/hash/deep_merge.rb
|
4
|
+
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
5
|
+
#
|
6
|
+
# h1 = {:x => {:y => [4,5,6]}, :z => [7,8,9]}
|
7
|
+
# h2 = {:x => {:y => [7,8,9]}, :z => "xyz"}
|
8
|
+
#
|
9
|
+
# h1.deep_merge(h2) #=> { :x => {:y => [7, 8, 9]}, :z => "xyz" }
|
10
|
+
# h2.deep_merge(h1) #=> { :x => {:y => [4, 5, 6]}, :z => [7, 8, 9] }
|
11
|
+
def deep_merge(other_hash)
|
12
|
+
dup.deep_merge!(other_hash)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Same as +deep_merge+, but modifies +self+.
|
16
|
+
def deep_merge!(other_hash)
|
17
|
+
other_hash.each_pair do |k,v|
|
18
|
+
tv = self[k]
|
19
|
+
self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v
|
20
|
+
end
|
21
|
+
self
|
22
|
+
end
|
23
|
+
end
|
@@ -18,7 +18,7 @@ module Grape
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# Add a new hash to the top of the stack.
|
21
|
-
#
|
21
|
+
#
|
22
22
|
# @param hash [Hash] optional hash to be pushed. Defaults to empty hash
|
23
23
|
# @return [HashStack]
|
24
24
|
def push(hash = {})
|
@@ -31,7 +31,7 @@ module Grape
|
|
31
31
|
end
|
32
32
|
|
33
33
|
# Looks through the stack for the first frame that matches :key
|
34
|
-
#
|
34
|
+
#
|
35
35
|
# @param key [Symbol] key to look for in hash frames
|
36
36
|
# @return value of given key after merging the stack
|
37
37
|
def get(key)
|
@@ -52,7 +52,7 @@ module Grape
|
|
52
52
|
alias_method :[]=, :set
|
53
53
|
|
54
54
|
# Replace multiple values on the top hash of the stack.
|
55
|
-
#
|
55
|
+
#
|
56
56
|
# @param hash [Hash] Hash of values to be merged in.
|
57
57
|
def update(hash)
|
58
58
|
peek.merge!(hash)
|
@@ -83,6 +83,15 @@ module Grape
|
|
83
83
|
self
|
84
84
|
end
|
85
85
|
|
86
|
+
# Looks through the stack for all instances of a given key and returns
|
87
|
+
# them as a flat Array.
|
88
|
+
#
|
89
|
+
# @param key [Symbol] The key to gather
|
90
|
+
# @return [Array]
|
91
|
+
def gather(key)
|
92
|
+
stack.map{|s| s[key] }.flatten.compact.uniq
|
93
|
+
end
|
94
|
+
|
86
95
|
def to_s
|
87
96
|
@stack.to_s
|
88
97
|
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
|
5
|
+
module Validations
|
6
|
+
|
7
|
+
##
|
8
|
+
# All validators must inherit from this class.
|
9
|
+
#
|
10
|
+
class Validator
|
11
|
+
attr_reader :attrs
|
12
|
+
|
13
|
+
def initialize(attrs, options, required, scope)
|
14
|
+
@attrs = Array(attrs)
|
15
|
+
@required = required
|
16
|
+
@scope = scope
|
17
|
+
|
18
|
+
if options.is_a?(Hash) && !options.empty?
|
19
|
+
raise "unknown options: #{options.keys}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate!(params)
|
24
|
+
params = @scope.params(params)
|
25
|
+
|
26
|
+
@attrs.each do |attr_name|
|
27
|
+
if @required || params.has_key?(attr_name)
|
28
|
+
validate_param!(attr_name, params)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def self.convert_to_short_name(klass)
|
36
|
+
ret = klass.name.gsub(/::/, '/').
|
37
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
38
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
39
|
+
tr("-", "_").
|
40
|
+
downcase
|
41
|
+
File.basename(ret, '_validator')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Base class for all validators taking only one param.
|
47
|
+
class SingleOptionValidator < Validator
|
48
|
+
def initialize(attrs, options, required, scope)
|
49
|
+
@option = options
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
# We define Validator::inherited here so SingleOptionValidator
|
56
|
+
# will not be considered a validator.
|
57
|
+
class Validator
|
58
|
+
def self.inherited(klass)
|
59
|
+
short_name = convert_to_short_name(klass)
|
60
|
+
Validations::register_validator(short_name, klass)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class << self
|
65
|
+
attr_accessor :validators
|
66
|
+
end
|
67
|
+
|
68
|
+
self.validators = {}
|
69
|
+
|
70
|
+
def self.register_validator(short_name, klass)
|
71
|
+
validators[short_name] = klass
|
72
|
+
end
|
73
|
+
|
74
|
+
class ParamsScope
|
75
|
+
attr_accessor :element, :parent
|
76
|
+
|
77
|
+
def initialize(api, element, parent, &block)
|
78
|
+
@element = element
|
79
|
+
@parent = parent
|
80
|
+
@api = api
|
81
|
+
instance_eval(&block)
|
82
|
+
end
|
83
|
+
|
84
|
+
def requires(*attrs)
|
85
|
+
validations = {:presence => true}
|
86
|
+
if attrs.last.is_a?(Hash)
|
87
|
+
validations.merge!(attrs.pop)
|
88
|
+
end
|
89
|
+
|
90
|
+
push_declared_params(attrs)
|
91
|
+
validates(attrs, validations)
|
92
|
+
end
|
93
|
+
|
94
|
+
def optional(*attrs)
|
95
|
+
validations = {}
|
96
|
+
if attrs.last.is_a?(Hash)
|
97
|
+
validations.merge!(attrs.pop)
|
98
|
+
end
|
99
|
+
|
100
|
+
push_declared_params(attrs)
|
101
|
+
validates(attrs, validations)
|
102
|
+
end
|
103
|
+
|
104
|
+
def group(element, &block)
|
105
|
+
scope = ParamsScope.new(@api, element, self, &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
def params(params)
|
109
|
+
params = @parent.params(params) if @parent
|
110
|
+
params = params[@element] || {} if @element
|
111
|
+
params
|
112
|
+
end
|
113
|
+
|
114
|
+
def full_name(name)
|
115
|
+
return "#{@parent.full_name(@element)}[#{name}]" if @parent
|
116
|
+
name.to_s
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
def validates(attrs, validations)
|
121
|
+
doc_attrs = { :required => validations.keys.include?(:presence) }
|
122
|
+
|
123
|
+
# special case (type = coerce)
|
124
|
+
if validations[:type]
|
125
|
+
validations[:coerce] = validations.delete(:type)
|
126
|
+
end
|
127
|
+
|
128
|
+
if coerce_type = validations[:coerce]
|
129
|
+
doc_attrs[:type] = coerce_type.to_s
|
130
|
+
end
|
131
|
+
|
132
|
+
if desc = validations.delete(:desc)
|
133
|
+
doc_attrs[:desc] = desc
|
134
|
+
end
|
135
|
+
|
136
|
+
full_attrs = attrs.collect{ |name| { :name => name, :full_name => full_name(name)} }
|
137
|
+
@api.document_attribute(full_attrs, doc_attrs)
|
138
|
+
|
139
|
+
# Validate for presence before any other validators
|
140
|
+
if validations.has_key?(:presence) && validations[:presence]
|
141
|
+
validate('presence', validations[:presence], attrs, doc_attrs)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Before we run the rest of the validators, lets handle
|
145
|
+
# whatever coercion so that we are working with correctly
|
146
|
+
# type casted values
|
147
|
+
if validations.has_key? :coerce
|
148
|
+
validate('coerce', validations[:coerce], attrs, doc_attrs)
|
149
|
+
validations.delete(:coerce)
|
150
|
+
end
|
151
|
+
|
152
|
+
validations.each do |type, options|
|
153
|
+
validate(type, options, attrs, doc_attrs)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def validate(type, options, attrs, doc_attrs)
|
158
|
+
validator_class = Validations::validators[type.to_s]
|
159
|
+
|
160
|
+
if validator_class
|
161
|
+
(@api.settings.peek[:validations] ||= []) << validator_class.new(attrs, options, doc_attrs[:required], self)
|
162
|
+
else
|
163
|
+
raise "unknown validator: #{type}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def push_declared_params(attrs)
|
168
|
+
@api.settings.peek[:declared_params] ||= []
|
169
|
+
@api.settings[:declared_params] += attrs
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# This module is mixed into the API Class.
|
174
|
+
module ClassMethods
|
175
|
+
def reset_validations!
|
176
|
+
settings.peek[:declared_params] = []
|
177
|
+
settings.peek[:validations] = []
|
178
|
+
end
|
179
|
+
|
180
|
+
def params(&block)
|
181
|
+
ParamsScope.new(self, nil, nil, &block)
|
182
|
+
end
|
183
|
+
|
184
|
+
def document_attribute(names, opts)
|
185
|
+
@last_description ||= {}
|
186
|
+
@last_description[:params] ||= {}
|
187
|
+
|
188
|
+
Array(names).each do |name|
|
189
|
+
@last_description[:params][name[:name].to_s] ||= {}
|
190
|
+
@last_description[:params][name[:name].to_s].merge!(opts).merge!({:full_name => name[:full_name]})
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Load all defined validations.
|
200
|
+
Dir[File.expand_path('../validations/*.rb', __FILE__)].each do |path|
|
201
|
+
require(path)
|
202
|
+
end
|