grape-security 0.8.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 (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +45 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +70 -0
  5. data/.travis.yml +18 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG.md +314 -0
  8. data/CONTRIBUTING.md +118 -0
  9. data/Gemfile +21 -0
  10. data/Guardfile +14 -0
  11. data/LICENSE +20 -0
  12. data/README.md +1777 -0
  13. data/RELEASING.md +105 -0
  14. data/Rakefile +69 -0
  15. data/UPGRADING.md +124 -0
  16. data/grape-security.gemspec +39 -0
  17. data/grape.png +0 -0
  18. data/lib/grape.rb +99 -0
  19. data/lib/grape/api.rb +646 -0
  20. data/lib/grape/cookies.rb +39 -0
  21. data/lib/grape/endpoint.rb +533 -0
  22. data/lib/grape/error_formatter/base.rb +31 -0
  23. data/lib/grape/error_formatter/json.rb +15 -0
  24. data/lib/grape/error_formatter/txt.rb +16 -0
  25. data/lib/grape/error_formatter/xml.rb +15 -0
  26. data/lib/grape/exceptions/base.rb +66 -0
  27. data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
  28. data/lib/grape/exceptions/invalid_formatter.rb +10 -0
  29. data/lib/grape/exceptions/invalid_versioner_option.rb +10 -0
  30. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +10 -0
  31. data/lib/grape/exceptions/missing_mime_type.rb +10 -0
  32. data/lib/grape/exceptions/missing_option.rb +10 -0
  33. data/lib/grape/exceptions/missing_vendor_option.rb +10 -0
  34. data/lib/grape/exceptions/unknown_options.rb +10 -0
  35. data/lib/grape/exceptions/unknown_validator.rb +10 -0
  36. data/lib/grape/exceptions/validation.rb +26 -0
  37. data/lib/grape/exceptions/validation_errors.rb +43 -0
  38. data/lib/grape/formatter/base.rb +31 -0
  39. data/lib/grape/formatter/json.rb +12 -0
  40. data/lib/grape/formatter/serializable_hash.rb +35 -0
  41. data/lib/grape/formatter/txt.rb +11 -0
  42. data/lib/grape/formatter/xml.rb +12 -0
  43. data/lib/grape/http/request.rb +26 -0
  44. data/lib/grape/locale/en.yml +32 -0
  45. data/lib/grape/middleware/auth/base.rb +30 -0
  46. data/lib/grape/middleware/auth/basic.rb +13 -0
  47. data/lib/grape/middleware/auth/digest.rb +13 -0
  48. data/lib/grape/middleware/auth/oauth2.rb +83 -0
  49. data/lib/grape/middleware/base.rb +62 -0
  50. data/lib/grape/middleware/error.rb +89 -0
  51. data/lib/grape/middleware/filter.rb +17 -0
  52. data/lib/grape/middleware/formatter.rb +150 -0
  53. data/lib/grape/middleware/globals.rb +13 -0
  54. data/lib/grape/middleware/versioner.rb +32 -0
  55. data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
  56. data/lib/grape/middleware/versioner/header.rb +132 -0
  57. data/lib/grape/middleware/versioner/param.rb +42 -0
  58. data/lib/grape/middleware/versioner/path.rb +52 -0
  59. data/lib/grape/namespace.rb +23 -0
  60. data/lib/grape/parser/base.rb +29 -0
  61. data/lib/grape/parser/json.rb +11 -0
  62. data/lib/grape/parser/xml.rb +11 -0
  63. data/lib/grape/path.rb +70 -0
  64. data/lib/grape/route.rb +27 -0
  65. data/lib/grape/util/content_types.rb +18 -0
  66. data/lib/grape/util/deep_merge.rb +23 -0
  67. data/lib/grape/util/hash_stack.rb +120 -0
  68. data/lib/grape/validations.rb +322 -0
  69. data/lib/grape/validations/coerce.rb +63 -0
  70. data/lib/grape/validations/default.rb +25 -0
  71. data/lib/grape/validations/exactly_one_of.rb +26 -0
  72. data/lib/grape/validations/mutual_exclusion.rb +25 -0
  73. data/lib/grape/validations/presence.rb +16 -0
  74. data/lib/grape/validations/regexp.rb +12 -0
  75. data/lib/grape/validations/values.rb +23 -0
  76. data/lib/grape/version.rb +3 -0
  77. data/spec/grape/api_spec.rb +2571 -0
  78. data/spec/grape/endpoint_spec.rb +784 -0
  79. data/spec/grape/entity_spec.rb +324 -0
  80. data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
  81. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
  82. data/spec/grape/exceptions/missing_mime_type_spec.rb +18 -0
  83. data/spec/grape/exceptions/missing_option_spec.rb +18 -0
  84. data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
  85. data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
  86. data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
  87. data/spec/grape/middleware/auth/basic_spec.rb +31 -0
  88. data/spec/grape/middleware/auth/digest_spec.rb +47 -0
  89. data/spec/grape/middleware/auth/oauth2_spec.rb +135 -0
  90. data/spec/grape/middleware/base_spec.rb +58 -0
  91. data/spec/grape/middleware/error_spec.rb +45 -0
  92. data/spec/grape/middleware/exception_spec.rb +184 -0
  93. data/spec/grape/middleware/formatter_spec.rb +258 -0
  94. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
  95. data/spec/grape/middleware/versioner/header_spec.rb +302 -0
  96. data/spec/grape/middleware/versioner/param_spec.rb +58 -0
  97. data/spec/grape/middleware/versioner/path_spec.rb +44 -0
  98. data/spec/grape/middleware/versioner_spec.rb +22 -0
  99. data/spec/grape/path_spec.rb +229 -0
  100. data/spec/grape/util/hash_stack_spec.rb +132 -0
  101. data/spec/grape/validations/coerce_spec.rb +208 -0
  102. data/spec/grape/validations/default_spec.rb +123 -0
  103. data/spec/grape/validations/exactly_one_of_spec.rb +71 -0
  104. data/spec/grape/validations/mutual_exclusion_spec.rb +61 -0
  105. data/spec/grape/validations/presence_spec.rb +142 -0
  106. data/spec/grape/validations/regexp_spec.rb +40 -0
  107. data/spec/grape/validations/values_spec.rb +152 -0
  108. data/spec/grape/validations/zh-CN.yml +10 -0
  109. data/spec/grape/validations_spec.rb +994 -0
  110. data/spec/shared/versioning_examples.rb +121 -0
  111. data/spec/spec_helper.rb +26 -0
  112. data/spec/support/basic_auth_encode_helpers.rb +3 -0
  113. data/spec/support/content_type_helpers.rb +11 -0
  114. data/spec/support/versioned_helpers.rb +50 -0
  115. metadata +421 -0
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Validations::PresenceValidator do
4
+
5
+ module ValidationsSpec
6
+ module PresenceValidatorSpec
7
+ class API < Grape::API
8
+ default_format :json
9
+
10
+ resource :bacons do
11
+ get do
12
+ "All the bacon"
13
+ end
14
+ end
15
+
16
+ params do
17
+ requires :id, regexp: /^[0-9]+$/
18
+ end
19
+ post do
20
+ { ret: params[:id] }
21
+ end
22
+
23
+ params do
24
+ requires :name, :company
25
+ end
26
+ get do
27
+ "Hello"
28
+ end
29
+
30
+ params do
31
+ requires :user, type: Hash do
32
+ requires :first_name
33
+ requires :last_name
34
+ end
35
+ end
36
+ get '/nested' do
37
+ "Nested"
38
+ end
39
+
40
+ params do
41
+ requires :admin, type: Hash do
42
+ requires :admin_name
43
+ requires :super, type: Hash do
44
+ requires :user, type: Hash do
45
+ requires :first_name
46
+ requires :last_name
47
+ end
48
+ end
49
+ end
50
+ end
51
+ get '/nested_triple' do
52
+ "Nested triple"
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def app
59
+ ValidationsSpec::PresenceValidatorSpec::API
60
+ end
61
+
62
+ it 'does not validate for any params' do
63
+ get "/bacons"
64
+ expect(last_response.status).to eq(200)
65
+ expect(last_response.body).to eq("All the bacon".to_json)
66
+ end
67
+
68
+ it 'validates id' do
69
+ post '/'
70
+ expect(last_response.status).to eq(400)
71
+ expect(last_response.body).to eq('{"error":"id is missing"}')
72
+
73
+ io = StringIO.new('{"id" : "a56b"}')
74
+ post '/', {}, 'rack.input' => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length
75
+ expect(last_response.body).to eq('{"error":"id is invalid"}')
76
+ expect(last_response.status).to eq(400)
77
+
78
+ io = StringIO.new('{"id" : 56}')
79
+ post '/', {}, 'rack.input' => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length
80
+ expect(last_response.body).to eq('{"ret":56}')
81
+ expect(last_response.status).to eq(201)
82
+ end
83
+
84
+ it 'validates name, company' do
85
+ get '/'
86
+ expect(last_response.status).to eq(400)
87
+ expect(last_response.body).to eq('{"error":"name is missing"}')
88
+
89
+ get '/', name: "Bob"
90
+ expect(last_response.status).to eq(400)
91
+ expect(last_response.body).to eq('{"error":"company is missing"}')
92
+
93
+ get '/', name: "Bob", company: "TestCorp"
94
+ expect(last_response.status).to eq(200)
95
+ expect(last_response.body).to eq("Hello".to_json)
96
+ end
97
+
98
+ it 'validates nested parameters' do
99
+ get '/nested'
100
+ expect(last_response.status).to eq(400)
101
+ expect(last_response.body).to eq('{"error":"user is missing, user[first_name] is missing, user[last_name] is missing"}')
102
+
103
+ get '/nested', user: { first_name: "Billy" }
104
+ expect(last_response.status).to eq(400)
105
+ expect(last_response.body).to eq('{"error":"user[last_name] is missing"}')
106
+
107
+ get '/nested', user: { first_name: "Billy", last_name: "Bob" }
108
+ expect(last_response.status).to eq(200)
109
+ expect(last_response.body).to eq("Nested".to_json)
110
+ end
111
+
112
+ it 'validates triple nested parameters' do
113
+ get '/nested_triple'
114
+ expect(last_response.status).to eq(400)
115
+ expect(last_response.body).to include '{"error":"admin is missing'
116
+
117
+ get '/nested_triple', user: { first_name: "Billy" }
118
+ expect(last_response.status).to eq(400)
119
+ expect(last_response.body).to include '{"error":"admin is missing'
120
+
121
+ get '/nested_triple', admin: { super: { first_name: "Billy" } }
122
+ expect(last_response.status).to eq(400)
123
+ expect(last_response.body).to eq('{"error":"admin[admin_name] is missing, admin[super][user] is missing, admin[super][user][first_name] is missing, admin[super][user][last_name] is missing"}')
124
+
125
+ get '/nested_triple', super: { user: { first_name: "Billy", last_name: "Bob" } }
126
+ expect(last_response.status).to eq(400)
127
+ expect(last_response.body).to include '{"error":"admin is missing'
128
+
129
+ get '/nested_triple', admin: { super: { user: { first_name: "Billy" } } }
130
+ expect(last_response.status).to eq(400)
131
+ expect(last_response.body).to eq('{"error":"admin[admin_name] is missing, admin[super][user][last_name] is missing"}')
132
+
133
+ get '/nested_triple', admin: { admin_name: 'admin', super: { user: { first_name: "Billy" } } }
134
+ expect(last_response.status).to eq(400)
135
+ expect(last_response.body).to eq('{"error":"admin[super][user][last_name] is missing"}')
136
+
137
+ get '/nested_triple', admin: { admin_name: 'admin', super: { user: { first_name: "Billy", last_name: "Bob" } } }
138
+ expect(last_response.status).to eq(200)
139
+ expect(last_response.body).to eq("Nested triple".to_json)
140
+ end
141
+
142
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Validations::RegexpValidator do
4
+ module ValidationsSpec
5
+ module RegexpValidatorSpec
6
+ class API < Grape::API
7
+ default_format :json
8
+
9
+ params do
10
+ requires :name, regexp: /^[a-z]+$/
11
+ end
12
+ get do
13
+
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def app
20
+ ValidationsSpec::RegexpValidatorSpec::API
21
+ end
22
+
23
+ context 'invalid input' do
24
+ it 'refuses inapppopriate' do
25
+ get '/', name: "invalid name"
26
+ expect(last_response.status).to eq(400)
27
+ end
28
+
29
+ it 'refuses nil' do
30
+ get '/', name: nil
31
+ expect(last_response.status).to eq(400)
32
+ end
33
+ end
34
+
35
+ it 'accepts valid input' do
36
+ get '/', name: "bob"
37
+ expect(last_response.status).to eq(200)
38
+ end
39
+
40
+ end
@@ -0,0 +1,152 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Validations::ValuesValidator do
4
+
5
+ class ValuesModel
6
+ DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3']
7
+ class << self
8
+ def values
9
+ @values ||= []
10
+ [DEFAULT_VALUES + @values].flatten.uniq
11
+ end
12
+
13
+ def add_value(value)
14
+ @values ||= []
15
+ @values << value
16
+ end
17
+ end
18
+ end
19
+
20
+ module ValidationsSpec
21
+ module ValuesValidatorSpec
22
+ class API < Grape::API
23
+ default_format :json
24
+
25
+ params do
26
+ requires :type, values: ValuesModel.values
27
+ end
28
+ get '/' do
29
+ { type: params[:type] }
30
+ end
31
+
32
+ params do
33
+ optional :type, values: ValuesModel.values, default: 'valid-type2'
34
+ end
35
+ get '/default/valid' do
36
+ { type: params[:type] }
37
+ end
38
+
39
+ params do
40
+ optional :type, values: -> { ValuesModel.values }, default: 'valid-type2'
41
+ end
42
+ get '/lambda' do
43
+ { type: params[:type] }
44
+ end
45
+
46
+ params do
47
+ requires :type, type: Integer, desc: "An integer", values: [10, 11], default: 10
48
+ end
49
+ get '/values/coercion' do
50
+ { type: params[:type] }
51
+ end
52
+
53
+ params do
54
+ optional :optional do
55
+ requires :type, values: ["a", "b"]
56
+ end
57
+ end
58
+ get '/optional_with_required_values'
59
+ end
60
+ end
61
+ end
62
+
63
+ def app
64
+ ValidationsSpec::ValuesValidatorSpec::API
65
+ end
66
+
67
+ it 'allows a valid value for a parameter' do
68
+ get("/", type: 'valid-type1')
69
+ expect(last_response.status).to eq 200
70
+ expect(last_response.body).to eq({ type: "valid-type1" }.to_json)
71
+ end
72
+
73
+ it 'does not allow an invalid value for a parameter' do
74
+ get("/", type: 'invalid-type')
75
+ expect(last_response.status).to eq 400
76
+ expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
77
+ end
78
+
79
+ context 'nil value for a parameter' do
80
+ it 'does not allow for root params scope' do
81
+ get("/", type: nil)
82
+ expect(last_response.status).to eq 400
83
+ expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
84
+ end
85
+
86
+ it 'allows for a required param in child scope' do
87
+ get('/optional_with_required_values')
88
+ expect(last_response.status).to eq 200
89
+ end
90
+ end
91
+
92
+ it 'allows a valid default value' do
93
+ get("/default/valid")
94
+ expect(last_response.status).to eq 200
95
+ expect(last_response.body).to eq({ type: "valid-type2" }.to_json)
96
+ end
97
+
98
+ it 'allows a proc for values' do
99
+ get('/lambda', type: 'valid-type1')
100
+ expect(last_response.status).to eq 200
101
+ expect(last_response.body).to eq({ type: "valid-type1" }.to_json)
102
+ end
103
+
104
+ it 'does not validate updated values without proc' do
105
+ ValuesModel.add_value('valid-type4')
106
+
107
+ get('/', type: 'valid-type4')
108
+ expect(last_response.status).to eq 400
109
+ expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
110
+ end
111
+
112
+ it 'validates against values in a proc' do
113
+ ValuesModel.add_value('valid-type4')
114
+
115
+ get('/lambda', type: 'valid-type4')
116
+ expect(last_response.status).to eq 200
117
+ expect(last_response.body).to eq({ type: "valid-type4" }.to_json)
118
+ end
119
+
120
+ it 'does not allow an invalid value for a parameter using lambda' do
121
+ get("/lambda", type: 'invalid-type')
122
+ expect(last_response.status).to eq 400
123
+ expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
124
+ end
125
+
126
+ it 'raises IncompatibleOptionValues on an invalid default value' do
127
+ subject = Class.new(Grape::API)
128
+ expect {
129
+ subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: 'invalid-type' }
130
+ }.to raise_error Grape::Exceptions::IncompatibleOptionValues
131
+ end
132
+
133
+ it 'raises IncompatibleOptionValues when type is incompatible with values array' do
134
+ subject = Class.new(Grape::API)
135
+ expect {
136
+ subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol }
137
+ }.to raise_error Grape::Exceptions::IncompatibleOptionValues
138
+ end
139
+
140
+ it 'allows values to be a kind of the coerced type not just an instance of it' do
141
+ get("/values/coercion", type: 10)
142
+ expect(last_response.status).to eq 200
143
+ expect(last_response.body).to eq({ type: 10 }.to_json)
144
+ end
145
+
146
+ it 'raises IncompatibleOptionValues when values contains a value that is not a kind of the type' do
147
+ subject = Class.new(Grape::API)
148
+ expect {
149
+ subject.params { requires :type, values: [10.5, 11], type: Integer }
150
+ }.to raise_error Grape::Exceptions::IncompatibleOptionValues
151
+ end
152
+ end
@@ -0,0 +1,10 @@
1
+ zh-CN:
2
+ grape:
3
+ errors:
4
+ format: ! '%{attribute}%{message}'
5
+ attributes:
6
+ age: 年龄
7
+ messages:
8
+ coerce: '格式不正确'
9
+ presence: '请填写'
10
+ regexp: '格式不正确'
@@ -0,0 +1,994 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Validations do
4
+
5
+ subject { Class.new(Grape::API) }
6
+
7
+ def app
8
+ subject
9
+ end
10
+
11
+ describe 'params' do
12
+ context 'optional' do
13
+ it 'validates when params is present' do
14
+ subject.params do
15
+ optional :a_number, regexp: /^[0-9]+$/
16
+ end
17
+ subject.get '/optional' do
18
+ 'optional works!'
19
+ end
20
+
21
+ get '/optional', a_number: 'string'
22
+ expect(last_response.status).to eq(400)
23
+ expect(last_response.body).to eq('a_number is invalid')
24
+
25
+ get '/optional', a_number: 45
26
+ expect(last_response.status).to eq(200)
27
+ expect(last_response.body).to eq('optional works!')
28
+ end
29
+
30
+ it "doesn't validate when param not present" do
31
+ subject.params do
32
+ optional :a_number, regexp: /^[0-9]+$/
33
+ end
34
+ subject.get '/optional' do
35
+ 'optional works!'
36
+ end
37
+
38
+ get '/optional'
39
+ expect(last_response.status).to eq(200)
40
+ expect(last_response.body).to eq('optional works!')
41
+ end
42
+
43
+ it 'adds to declared parameters' do
44
+ subject.params do
45
+ optional :some_param
46
+ end
47
+ expect(subject.settings[:declared_params]).to eq([:some_param])
48
+ end
49
+ end
50
+
51
+ context 'required' do
52
+ before do
53
+ subject.params do
54
+ requires :key
55
+ end
56
+ subject.get '/required' do
57
+ 'required works'
58
+ end
59
+ end
60
+
61
+ it 'errors when param not present' do
62
+ get '/required'
63
+ expect(last_response.status).to eq(400)
64
+ expect(last_response.body).to eq('key is missing')
65
+ end
66
+
67
+ it "doesn't throw a missing param when param is present" do
68
+ get '/required', key: 'cool'
69
+ expect(last_response.status).to eq(200)
70
+ expect(last_response.body).to eq('required works')
71
+ end
72
+
73
+ it 'adds to declared parameters' do
74
+ subject.params do
75
+ requires :some_param
76
+ end
77
+ expect(subject.settings[:declared_params]).to eq([:some_param])
78
+ end
79
+ end
80
+
81
+ context 'requires :all using Grape::Entity documentation' do
82
+ def define_requires_all
83
+ documentation = {
84
+ required_field: { type: String },
85
+ optional_field: { type: String }
86
+ }
87
+ subject.params do
88
+ requires :all, except: :optional_field, using: documentation
89
+ end
90
+ end
91
+ before do
92
+ define_requires_all
93
+ subject.get '/required' do
94
+ 'required works'
95
+ end
96
+ end
97
+
98
+ it 'adds entity documentation to declared params' do
99
+ define_requires_all
100
+ expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field])
101
+ end
102
+
103
+ it 'errors when required_field is not present' do
104
+ get '/required'
105
+ expect(last_response.status).to eq(400)
106
+ expect(last_response.body).to eq('required_field is missing')
107
+ end
108
+
109
+ it 'works when required_field is present' do
110
+ get '/required', required_field: 'woof'
111
+ expect(last_response.status).to eq(200)
112
+ expect(last_response.body).to eq('required works')
113
+ end
114
+ end
115
+
116
+ context 'requires :none using Grape::Entity documentation' do
117
+ def define_requires_none
118
+ documentation = {
119
+ required_field: { type: String },
120
+ optional_field: { type: String }
121
+ }
122
+ subject.params do
123
+ requires :none, except: :required_field, using: documentation
124
+ end
125
+ end
126
+ before do
127
+ define_requires_none
128
+ subject.get '/required' do
129
+ 'required works'
130
+ end
131
+ end
132
+
133
+ it 'adds entity documentation to declared params' do
134
+ define_requires_none
135
+ expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field])
136
+ end
137
+
138
+ it 'errors when required_field is not present' do
139
+ get '/required'
140
+ expect(last_response.status).to eq(400)
141
+ expect(last_response.body).to eq('required_field is missing')
142
+ end
143
+
144
+ it 'works when required_field is present' do
145
+ get '/required', required_field: 'woof'
146
+ expect(last_response.status).to eq(200)
147
+ expect(last_response.body).to eq('required works')
148
+ end
149
+ end
150
+
151
+ context 'requires :all or :none but except a non-existent field using Grape::Entity documentation' do
152
+ context 'requires :all' do
153
+ def define_requires_all
154
+ documentation = {
155
+ required_field: { type: String },
156
+ optional_field: { type: String }
157
+ }
158
+ subject.params do
159
+ requires :all, except: :non_existent_field, using: documentation
160
+ end
161
+ end
162
+
163
+ it 'adds only the entity documentation to declared params, nothing more' do
164
+ define_requires_all
165
+ expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field])
166
+ end
167
+ end
168
+
169
+ context 'requires :none' do
170
+ def define_requires_none
171
+ documentation = {
172
+ required_field: { type: String },
173
+ optional_field: { type: String }
174
+ }
175
+ subject.params do
176
+ requires :none, except: :non_existent_field, using: documentation
177
+ end
178
+ end
179
+
180
+ it 'adds only the entity documentation to declared params, nothing more' do
181
+ expect { define_requires_none }.to raise_error(ArgumentError)
182
+ end
183
+ end
184
+ end
185
+
186
+ context 'required with an Array block' do
187
+ before do
188
+ subject.params do
189
+ requires :items, type: Array do
190
+ requires :key
191
+ end
192
+ end
193
+ subject.get '/required' do
194
+ 'required works'
195
+ end
196
+ end
197
+
198
+ it 'errors when param not present' do
199
+ get '/required'
200
+ expect(last_response.status).to eq(400)
201
+ expect(last_response.body).to eq('items is missing')
202
+ end
203
+
204
+ it "errors when param is not an Array" do
205
+ get '/required', items: "hello"
206
+ expect(last_response.status).to eq(400)
207
+ expect(last_response.body).to eq('items is invalid, items[key] is missing')
208
+
209
+ get '/required', items: { key: 'foo' }
210
+ expect(last_response.status).to eq(400)
211
+ expect(last_response.body).to eq('items is invalid')
212
+ end
213
+
214
+ it "doesn't throw a missing param when param is present" do
215
+ get '/required', items: [{ key: 'hello' }, { key: 'world' }]
216
+ expect(last_response.status).to eq(200)
217
+ expect(last_response.body).to eq('required works')
218
+ end
219
+
220
+ it "doesn't allow any key in the options hash other than type" do
221
+ expect {
222
+ subject.params do
223
+ requires(:items, desc: 'Foo') do
224
+ requires :key
225
+ end
226
+ end
227
+ }.to raise_error ArgumentError
228
+ end
229
+
230
+ it 'adds to declared parameters' do
231
+ subject.params do
232
+ requires :items do
233
+ requires :key
234
+ end
235
+ end
236
+ expect(subject.settings[:declared_params]).to eq([items: [:key]])
237
+ end
238
+ end
239
+
240
+ context 'required with a Hash block' do
241
+ before do
242
+ subject.params do
243
+ requires :items, type: Hash do
244
+ requires :key
245
+ end
246
+ end
247
+ subject.get '/required' do
248
+ 'required works'
249
+ end
250
+ end
251
+
252
+ it 'errors when param not present' do
253
+ get '/required'
254
+ expect(last_response.status).to eq(400)
255
+ expect(last_response.body).to eq('items is missing, items[key] is missing')
256
+ end
257
+
258
+ it "errors when param is not a Hash" do
259
+ get '/required', items: "hello"
260
+ expect(last_response.status).to eq(400)
261
+ expect(last_response.body).to eq('items is invalid, items[key] is missing')
262
+
263
+ get '/required', items: [{ key: 'foo' }]
264
+ expect(last_response.status).to eq(400)
265
+ expect(last_response.body).to eq('items is invalid')
266
+ end
267
+
268
+ it "doesn't throw a missing param when param is present" do
269
+ get '/required', items: { key: 'hello' }
270
+ expect(last_response.status).to eq(200)
271
+ expect(last_response.body).to eq('required works')
272
+ end
273
+
274
+ it "doesn't allow any key in the options hash other than type" do
275
+ expect {
276
+ subject.params do
277
+ requires(:items, desc: 'Foo') do
278
+ requires :key
279
+ end
280
+ end
281
+ }.to raise_error ArgumentError
282
+ end
283
+
284
+ it 'adds to declared parameters' do
285
+ subject.params do
286
+ requires :items do
287
+ requires :key
288
+ end
289
+ end
290
+ expect(subject.settings[:declared_params]).to eq([items: [:key]])
291
+ end
292
+ end
293
+
294
+ context 'group' do
295
+ before do
296
+ subject.params do
297
+ group :items do
298
+ requires :key
299
+ end
300
+ end
301
+ subject.get '/required' do
302
+ 'required works'
303
+ end
304
+ end
305
+
306
+ it 'errors when param not present' do
307
+ get '/required'
308
+ expect(last_response.status).to eq(400)
309
+ expect(last_response.body).to eq('items is missing')
310
+ end
311
+
312
+ it "doesn't throw a missing param when param is present" do
313
+ get '/required', items: [key: 'hello', key: 'world']
314
+ expect(last_response.status).to eq(200)
315
+ expect(last_response.body).to eq('required works')
316
+ end
317
+
318
+ it 'adds to declared parameters' do
319
+ subject.params do
320
+ group :items do
321
+ requires :key
322
+ end
323
+ end
324
+ expect(subject.settings[:declared_params]).to eq([items: [:key]])
325
+ end
326
+ end
327
+
328
+ context 'validation within arrays' do
329
+ before do
330
+ subject.params do
331
+ group :children do
332
+ requires :name
333
+ group :parents do
334
+ requires :name
335
+ end
336
+ end
337
+ end
338
+ subject.get '/within_array' do
339
+ 'within array works'
340
+ end
341
+ end
342
+
343
+ it 'can handle new scopes within child elements' do
344
+ get '/within_array', children: [
345
+ { name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] },
346
+ { name: 'Joe', parents: [{ name: 'Josie' }] }
347
+ ]
348
+ expect(last_response.status).to eq(200)
349
+ expect(last_response.body).to eq('within array works')
350
+ end
351
+
352
+ it 'errors when a parameter is not present' do
353
+ get '/within_array', children: [
354
+ { name: 'Jim', parents: [{}] },
355
+ { name: 'Job', parents: [{ name: 'Joy' }] }
356
+ ]
357
+ # NOTE: with body parameters in json or XML or similar this
358
+ # should actually fail with: children[parents][name] is missing.
359
+ expect(last_response.status).to eq(400)
360
+ expect(last_response.body).to eq('children[parents] is missing')
361
+ end
362
+
363
+ it 'safely handles empty arrays and blank parameters' do
364
+ # NOTE: with body parameters in json or XML or similar this
365
+ # should actually return 200, since an empty array is valid.
366
+ get '/within_array', children: []
367
+ expect(last_response.status).to eq(400)
368
+ expect(last_response.body).to eq('children is missing')
369
+ get '/within_array', children: [name: 'Jay']
370
+ expect(last_response.status).to eq(400)
371
+ expect(last_response.body).to eq('children[parents] is missing')
372
+ end
373
+
374
+ it "errors when param is not an Array" do
375
+ # NOTE: would be nicer if these just returned 'children is invalid'
376
+ get '/within_array', children: "hello"
377
+ expect(last_response.status).to eq(400)
378
+ expect(last_response.body).to eq('children is invalid, children[name] is missing, children[parents] is missing, children[parents] is invalid, children[parents][name] is missing')
379
+
380
+ get '/within_array', children: { name: 'foo' }
381
+ expect(last_response.status).to eq(400)
382
+ expect(last_response.body).to eq('children is invalid, children[parents] is missing')
383
+
384
+ get '/within_array', children: [name: 'Jay', parents: { name: 'Fred' }]
385
+ expect(last_response.status).to eq(400)
386
+ expect(last_response.body).to eq('children[parents] is invalid')
387
+ end
388
+ end
389
+
390
+ context 'with block param' do
391
+ before do
392
+ subject.params do
393
+ requires :planets do
394
+ requires :name
395
+ end
396
+ end
397
+ subject.get '/req' do
398
+ 'within array works'
399
+ end
400
+ subject.put '/req' do
401
+ ''
402
+ end
403
+
404
+ subject.params do
405
+ group :stars do
406
+ requires :name
407
+ end
408
+ end
409
+ subject.get '/grp' do
410
+ 'within array works'
411
+ end
412
+ subject.put '/grp' do
413
+ ''
414
+ end
415
+
416
+ subject.params do
417
+ requires :name
418
+ optional :moons do
419
+ requires :name
420
+ end
421
+ end
422
+ subject.get '/opt' do
423
+ 'within array works'
424
+ end
425
+ subject.put '/opt' do
426
+ ''
427
+ end
428
+ end
429
+
430
+ it 'requires defaults to Array type' do
431
+ get '/req', planets: "Jupiter, Saturn"
432
+ expect(last_response.status).to eq(400)
433
+ expect(last_response.body).to eq('planets is invalid, planets[name] is missing')
434
+
435
+ get '/req', planets: { name: 'Jupiter' }
436
+ expect(last_response.status).to eq(400)
437
+ expect(last_response.body).to eq('planets is invalid')
438
+
439
+ get '/req', planets: [{ name: 'Venus' }, { name: 'Mars' }]
440
+ expect(last_response.status).to eq(200)
441
+
442
+ put_with_json '/req', planets: []
443
+ expect(last_response.status).to eq(200)
444
+ end
445
+
446
+ it 'optional defaults to Array type' do
447
+ get '/opt', name: "Jupiter", moons: "Europa, Ganymede"
448
+ expect(last_response.status).to eq(400)
449
+ expect(last_response.body).to eq('moons is invalid, moons[name] is missing')
450
+
451
+ get '/opt', name: "Jupiter", moons: { name: 'Ganymede' }
452
+ expect(last_response.status).to eq(400)
453
+ expect(last_response.body).to eq('moons is invalid')
454
+
455
+ get '/opt', name: "Jupiter", moons: [{ name: 'Io' }, { name: 'Callisto' }]
456
+ expect(last_response.status).to eq(200)
457
+
458
+ put_with_json '/opt', name: "Venus"
459
+ expect(last_response.status).to eq(200)
460
+
461
+ put_with_json '/opt', name: "Mercury", moons: []
462
+ expect(last_response.status).to eq(200)
463
+ end
464
+
465
+ it 'group defaults to Array type' do
466
+ get '/grp', stars: "Sun"
467
+ expect(last_response.status).to eq(400)
468
+ expect(last_response.body).to eq('stars is invalid, stars[name] is missing')
469
+
470
+ get '/grp', stars: { name: 'Sun' }
471
+ expect(last_response.status).to eq(400)
472
+ expect(last_response.body).to eq('stars is invalid')
473
+
474
+ get '/grp', stars: [{ name: 'Sun' }]
475
+ expect(last_response.status).to eq(200)
476
+
477
+ put_with_json '/grp', stars: []
478
+ expect(last_response.status).to eq(200)
479
+ end
480
+ end
481
+
482
+ context 'validation within arrays with JSON' do
483
+ before do
484
+ subject.params do
485
+ group :children do
486
+ requires :name
487
+ group :parents do
488
+ requires :name
489
+ end
490
+ end
491
+ end
492
+ subject.put '/within_array' do
493
+ 'within array works'
494
+ end
495
+ end
496
+
497
+ it 'can handle new scopes within child elements' do
498
+ put_with_json '/within_array', children: [
499
+ { name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] },
500
+ { name: 'Joe', parents: [{ name: 'Josie' }] }
501
+ ]
502
+ expect(last_response.status).to eq(200)
503
+ expect(last_response.body).to eq('within array works')
504
+ end
505
+
506
+ it 'errors when a parameter is not present' do
507
+ put_with_json '/within_array', children: [
508
+ { name: 'Jim', parents: [{}] },
509
+ { name: 'Job', parents: [{ name: 'Joy' }] }
510
+ ]
511
+ expect(last_response.status).to eq(400)
512
+ expect(last_response.body).to eq('children[parents][name] is missing')
513
+ end
514
+
515
+ it 'safely handles empty arrays and blank parameters' do
516
+ put_with_json '/within_array', children: []
517
+ expect(last_response.status).to eq(200)
518
+ put_with_json '/within_array', children: [name: 'Jay']
519
+ expect(last_response.status).to eq(400)
520
+ expect(last_response.body).to eq('children[parents] is missing')
521
+ end
522
+ end
523
+
524
+ context 'optional with an Array block' do
525
+ before do
526
+ subject.params do
527
+ optional :items, type: Array do
528
+ requires :key
529
+ end
530
+ end
531
+ subject.get '/optional_group' do
532
+ 'optional group works'
533
+ end
534
+ end
535
+
536
+ it "doesn't throw a missing param when the group isn't present" do
537
+ get '/optional_group'
538
+ expect(last_response.status).to eq(200)
539
+ expect(last_response.body).to eq('optional group works')
540
+ end
541
+
542
+ it "doesn't throw a missing param when both group and param are given" do
543
+ get '/optional_group', items: [{ key: 'foo' }]
544
+ expect(last_response.status).to eq(200)
545
+ expect(last_response.body).to eq('optional group works')
546
+ end
547
+
548
+ it "errors when group is present, but required param is not" do
549
+ get '/optional_group', items: [{ not_key: 'foo' }]
550
+ expect(last_response.status).to eq(400)
551
+ expect(last_response.body).to eq('items[key] is missing')
552
+ end
553
+
554
+ it "errors when param is present but isn't an Array" do
555
+ get '/optional_group', items: "hello"
556
+ expect(last_response.status).to eq(400)
557
+ expect(last_response.body).to eq('items is invalid, items[key] is missing')
558
+
559
+ get '/optional_group', items: { key: 'foo' }
560
+ expect(last_response.status).to eq(400)
561
+ expect(last_response.body).to eq('items is invalid')
562
+ end
563
+
564
+ it 'adds to declared parameters' do
565
+ subject.params do
566
+ optional :items do
567
+ requires :key
568
+ end
569
+ end
570
+ expect(subject.settings[:declared_params]).to eq([items: [:key]])
571
+ end
572
+ end
573
+
574
+ context 'nested optional Array blocks' do
575
+ before do
576
+ subject.params do
577
+ optional :items, type: Array do
578
+ requires :key
579
+ optional(:optional_subitems, type: Array) { requires :value }
580
+ requires(:required_subitems, type: Array) { requires :value }
581
+ end
582
+ end
583
+ subject.get('/nested_optional_group') { 'nested optional group works' }
584
+ end
585
+
586
+ it 'does no internal validations if the outer group is blank' do
587
+ get '/nested_optional_group'
588
+ expect(last_response.status).to eq(200)
589
+ expect(last_response.body).to eq('nested optional group works')
590
+ end
591
+
592
+ it 'does internal validations if the outer group is present' do
593
+ get '/nested_optional_group', items: [{ key: 'foo' }]
594
+ expect(last_response.status).to eq(400)
595
+ expect(last_response.body).to eq('items[required_subitems] is missing')
596
+
597
+ get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
598
+ expect(last_response.status).to eq(200)
599
+ expect(last_response.body).to eq('nested optional group works')
600
+ end
601
+
602
+ it 'handles deep nesting' do
603
+ get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }]
604
+ expect(last_response.status).to eq(400)
605
+ expect(last_response.body).to eq('items[optional_subitems][value] is missing')
606
+
607
+ get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ value: 'baz' }] }]
608
+ expect(last_response.status).to eq(200)
609
+ expect(last_response.body).to eq('nested optional group works')
610
+ end
611
+
612
+ it 'handles validation within arrays' do
613
+ get '/nested_optional_group', items: [{ key: 'foo' }]
614
+ expect(last_response.status).to eq(400)
615
+ expect(last_response.body).to eq('items[required_subitems] is missing')
616
+
617
+ get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
618
+ expect(last_response.status).to eq(200)
619
+ expect(last_response.body).to eq('nested optional group works')
620
+
621
+ get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }]
622
+ expect(last_response.status).to eq(400)
623
+ expect(last_response.body).to eq('items[optional_subitems][value] is missing')
624
+ end
625
+
626
+ it 'adds to declared parameters' do
627
+ subject.params do
628
+ optional :items do
629
+ requires :key
630
+ optional(:optional_subitems) { requires :value }
631
+ requires(:required_subitems) { requires :value }
632
+ end
633
+ end
634
+ expect(subject.settings[:declared_params]).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]])
635
+ end
636
+ end
637
+
638
+ context 'multiple validation errors' do
639
+ before do
640
+ subject.params do
641
+ requires :yolo
642
+ requires :swag
643
+ end
644
+ subject.get '/two_required' do
645
+ 'two required works'
646
+ end
647
+ end
648
+
649
+ it 'throws the validation errors' do
650
+ get '/two_required'
651
+ expect(last_response.status).to eq(400)
652
+ expect(last_response.body).to match(/yolo is missing/)
653
+ expect(last_response.body).to match(/swag is missing/)
654
+ end
655
+ end
656
+
657
+ context 'custom validation' do
658
+ module CustomValidations
659
+ class Customvalidator < Grape::Validations::Validator
660
+ def validate_param!(attr_name, params)
661
+ unless params[attr_name] == 'im custom'
662
+ raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message: "is not custom!"
663
+ end
664
+ end
665
+ end
666
+ end
667
+
668
+ context 'when using optional with a custom validator' do
669
+ before do
670
+ subject.params do
671
+ optional :custom, customvalidator: true
672
+ end
673
+ subject.get '/optional_custom' do
674
+ 'optional with custom works!'
675
+ end
676
+ end
677
+
678
+ it 'validates when param is present' do
679
+ get '/optional_custom', custom: 'im custom'
680
+ expect(last_response.status).to eq(200)
681
+ expect(last_response.body).to eq('optional with custom works!')
682
+
683
+ get '/optional_custom', custom: 'im wrong'
684
+ expect(last_response.status).to eq(400)
685
+ expect(last_response.body).to eq('custom is not custom!')
686
+ end
687
+
688
+ it "skips validation when parameter isn't present" do
689
+ get '/optional_custom'
690
+ expect(last_response.status).to eq(200)
691
+ expect(last_response.body).to eq('optional with custom works!')
692
+ end
693
+
694
+ it 'validates with custom validator when param present and incorrect type' do
695
+ subject.params do
696
+ optional :custom, type: String, customvalidator: true
697
+ end
698
+
699
+ get '/optional_custom', custom: 123
700
+ expect(last_response.status).to eq(400)
701
+ expect(last_response.body).to eq('custom is not custom!')
702
+ end
703
+ end
704
+
705
+ context 'when using requires with a custom validator' do
706
+ before do
707
+ subject.params do
708
+ requires :custom, customvalidator: true
709
+ end
710
+ subject.get '/required_custom' do
711
+ 'required with custom works!'
712
+ end
713
+ end
714
+
715
+ it 'validates when param is present' do
716
+ get '/required_custom', custom: 'im wrong, validate me'
717
+ expect(last_response.status).to eq(400)
718
+ expect(last_response.body).to eq('custom is not custom!')
719
+
720
+ get '/required_custom', custom: 'im custom'
721
+ expect(last_response.status).to eq(200)
722
+ expect(last_response.body).to eq('required with custom works!')
723
+ end
724
+
725
+ it 'validates when param is not present' do
726
+ get '/required_custom'
727
+ expect(last_response.status).to eq(400)
728
+ expect(last_response.body).to eq('custom is missing, custom is not custom!')
729
+ end
730
+
731
+ context 'nested namespaces' do
732
+ before do
733
+ subject.params do
734
+ requires :custom, customvalidator: true
735
+ end
736
+ subject.namespace 'nested' do
737
+ get 'one' do
738
+ 'validation failed'
739
+ end
740
+ namespace 'nested' do
741
+ get 'two' do
742
+ 'validation failed'
743
+ end
744
+ end
745
+ end
746
+ subject.namespace 'peer' do
747
+ get 'one' do
748
+ 'no validation required'
749
+ end
750
+ namespace 'nested' do
751
+ get 'two' do
752
+ 'no validation required'
753
+ end
754
+ end
755
+ end
756
+
757
+ subject.namespace 'unrelated' do
758
+ params do
759
+ requires :name
760
+ end
761
+ get 'one' do
762
+ 'validation required'
763
+ end
764
+
765
+ namespace 'double' do
766
+ get 'two' do
767
+ 'no validation required'
768
+ end
769
+ end
770
+ end
771
+ end
772
+
773
+ specify 'the parent namespace uses the validator' do
774
+ get '/nested/one', custom: 'im wrong, validate me'
775
+ expect(last_response.status).to eq(400)
776
+ expect(last_response.body).to eq('custom is not custom!')
777
+ end
778
+
779
+ specify 'the nested namesapce inherits the custom validator' do
780
+ get '/nested/nested/two', custom: 'im wrong, validate me'
781
+ expect(last_response.status).to eq(400)
782
+ expect(last_response.body).to eq('custom is not custom!')
783
+ end
784
+
785
+ specify 'peer namesapces does not have the validator' do
786
+ get '/peer/one', custom: 'im not validated'
787
+ expect(last_response.status).to eq(200)
788
+ expect(last_response.body).to eq('no validation required')
789
+ end
790
+
791
+ specify 'namespaces nested in peers should also not have the validator' do
792
+ get '/peer/nested/two', custom: 'im not validated'
793
+ expect(last_response.status).to eq(200)
794
+ expect(last_response.body).to eq('no validation required')
795
+ end
796
+
797
+ specify 'when nested, specifying a route should clear out the validations for deeper nested params' do
798
+ get '/unrelated/one'
799
+ expect(last_response.status).to eq(400)
800
+ get '/unrelated/double/two'
801
+ expect(last_response.status).to eq(200)
802
+ end
803
+ end
804
+ end
805
+ end # end custom validation
806
+
807
+ context 'named' do
808
+ context 'can be defined' do
809
+ it 'in helpers' do
810
+ subject.helpers do
811
+ params :pagination do
812
+ end
813
+ end
814
+ end
815
+
816
+ it 'in helper module which kind of Grape::API::Helpers' do
817
+ module SharedParams
818
+ extend Grape::API::Helpers
819
+ params :pagination do
820
+ end
821
+ end
822
+ subject.helpers SharedParams
823
+ end
824
+ end
825
+
826
+ context 'can be included in usual params' do
827
+ before do
828
+ module SharedParams
829
+ extend Grape::API::Helpers
830
+ params :period do
831
+ optional :start_date
832
+ optional :end_date
833
+ end
834
+ end
835
+ subject.helpers SharedParams
836
+
837
+ subject.helpers do
838
+ params :pagination do
839
+ optional :page, type: Integer
840
+ optional :per_page, type: Integer
841
+ end
842
+ end
843
+ end
844
+
845
+ it 'by #use' do
846
+ subject.params do
847
+ use :pagination
848
+ end
849
+ expect(subject.settings[:declared_params]).to eq [:page, :per_page]
850
+ end
851
+
852
+ it 'by #use with multiple params' do
853
+ subject.params do
854
+ use :pagination, :period
855
+ end
856
+ expect(subject.settings[:declared_params]).to eq [:page, :per_page, :start_date, :end_date]
857
+ end
858
+
859
+ end
860
+
861
+ context 'with block' do
862
+ before do
863
+ subject.helpers do
864
+ params :order do |options|
865
+ optional :order, type: Symbol, values: [:asc, :desc], default: options[:default_order]
866
+ optional :order_by, type: Symbol, values: options[:order_by], default: options[:default_order_by]
867
+ end
868
+ end
869
+ subject.format :json
870
+ subject.params do
871
+ use :order, default_order: :asc, order_by: [:name, :created_at], default_order_by: :created_at
872
+ end
873
+ subject.get '/order' do
874
+ {
875
+ order: params[:order],
876
+ order_by: params[:order_by]
877
+ }
878
+ end
879
+ end
880
+ it 'returns defaults' do
881
+ get '/order'
882
+ expect(last_response.status).to eq(200)
883
+ expect(last_response.body).to eq({ order: :asc, order_by: :created_at }.to_json)
884
+ end
885
+ it 'overrides default value for order' do
886
+ get '/order?order=desc'
887
+ expect(last_response.status).to eq(200)
888
+ expect(last_response.body).to eq({ order: :desc, order_by: :created_at }.to_json)
889
+ end
890
+ it 'overrides default value for order_by' do
891
+ get '/order?order_by=name'
892
+ expect(last_response.status).to eq(200)
893
+ expect(last_response.body).to eq({ order: :asc, order_by: :name }.to_json)
894
+ end
895
+ it 'fails with invalid value' do
896
+ get '/order?order=invalid'
897
+ expect(last_response.status).to eq(400)
898
+ expect(last_response.body).to eq('{"error":"order does not have a valid value"}')
899
+ end
900
+ end
901
+ end
902
+
903
+ context 'documentation' do
904
+ it 'can be included with a hash' do
905
+ documentation = { example: 'Joe' }
906
+
907
+ subject.params do
908
+ requires 'first_name', documentation: documentation
909
+ end
910
+ subject.get '/' do
911
+ end
912
+
913
+ expect(subject.routes.first.route_params['first_name'][:documentation]).to eq(documentation)
914
+ end
915
+ end
916
+
917
+ context 'mutually exclusive' do
918
+ context 'optional params' do
919
+ it 'errors when two or more are present' do
920
+ subject.params do
921
+ optional :beer
922
+ optional :wine
923
+ optional :juice
924
+ mutually_exclusive :beer, :wine, :juice
925
+ end
926
+ subject.get '/mutually_exclusive' do
927
+ 'mutually_exclusive works!'
928
+ end
929
+
930
+ get '/mutually_exclusive', beer: 'string', wine: 'anotherstring'
931
+ expect(last_response.status).to eq(400)
932
+ expect(last_response.body).to eq("[:beer, :wine] are mutually exclusive")
933
+ end
934
+ end
935
+
936
+ context 'more than one set of mutually exclusive params' do
937
+ it 'errors for all sets' do
938
+ subject.params do
939
+ optional :beer
940
+ optional :wine
941
+ mutually_exclusive :beer, :wine
942
+ optional :scotch
943
+ optional :aquavit
944
+ mutually_exclusive :scotch, :aquavit
945
+ end
946
+ subject.get '/mutually_exclusive' do
947
+ 'mutually_exclusive works!'
948
+ end
949
+
950
+ get '/mutually_exclusive', beer: 'true', wine: 'true', scotch: 'true', aquavit: 'true'
951
+ expect(last_response.status).to eq(400)
952
+ expect(last_response.body).to match(/\[:beer, :wine\] are mutually exclusive/)
953
+ expect(last_response.body).to match(/\[:scotch, :aquavit\] are mutually exclusive/)
954
+ end
955
+ end
956
+ end
957
+
958
+ context 'exactly one of' do
959
+ context 'params' do
960
+ it 'errors when two or more are present' do
961
+ subject.params do
962
+ optional :beer
963
+ optional :wine
964
+ optional :juice
965
+ exactly_one_of :beer, :wine, :juice
966
+ end
967
+ subject.get '/exactly_one_of' do
968
+ 'exactly_one_of works!'
969
+ end
970
+
971
+ get '/exactly_one_of', beer: 'string', wine: 'anotherstring'
972
+ expect(last_response.status).to eq(400)
973
+ expect(last_response.body).to eq("[:beer, :wine] are mutually exclusive")
974
+ end
975
+
976
+ it 'errors when none is selected' do
977
+ subject.params do
978
+ optional :beer
979
+ optional :wine
980
+ optional :juice
981
+ exactly_one_of :beer, :wine, :juice
982
+ end
983
+ subject.get '/exactly_one_of' do
984
+ 'exactly_one_of works!'
985
+ end
986
+
987
+ get '/exactly_one_of'
988
+ expect(last_response.status).to eq(400)
989
+ expect(last_response.body).to eq("[:beer, :wine, :juice] - exactly one parameter must be provided")
990
+ end
991
+ end
992
+ end
993
+ end
994
+ end