grape 0.2.6 → 0.3.0

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.

Files changed (50) hide show
  1. data/{CHANGELOG.markdown → CHANGELOG.md} +21 -1
  2. data/Gemfile +1 -0
  3. data/{README.markdown → README.md} +178 -125
  4. data/grape.gemspec +1 -1
  5. data/lib/grape.rb +25 -3
  6. data/lib/grape/api.rb +43 -20
  7. data/lib/grape/endpoint.rb +32 -13
  8. data/lib/grape/exceptions/base.rb +50 -1
  9. data/lib/grape/exceptions/invalid_formatter.rb +13 -0
  10. data/lib/grape/exceptions/invalid_versioner_option.rb +14 -0
  11. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +15 -0
  12. data/lib/grape/exceptions/missing_mime_type.rb +14 -0
  13. data/lib/grape/exceptions/missing_option.rb +13 -0
  14. data/lib/grape/exceptions/missing_vendor_option.rb +13 -0
  15. data/lib/grape/exceptions/unknown_options.rb +14 -0
  16. data/lib/grape/exceptions/unknown_validator.rb +12 -0
  17. data/lib/grape/exceptions/{validation_error.rb → validation.rb} +3 -1
  18. data/lib/grape/formatter/xml.rb +2 -1
  19. data/lib/grape/locale/en.yml +20 -0
  20. data/lib/grape/middleware/base.rb +0 -5
  21. data/lib/grape/middleware/error.rb +1 -2
  22. data/lib/grape/middleware/formatter.rb +9 -5
  23. data/lib/grape/middleware/versioner.rb +1 -1
  24. data/lib/grape/middleware/versioner/header.rb +16 -6
  25. data/lib/grape/middleware/versioner/param.rb +1 -1
  26. data/lib/grape/middleware/versioner/path.rb +1 -1
  27. data/lib/grape/util/content_types.rb +0 -2
  28. data/lib/grape/validations.rb +7 -14
  29. data/lib/grape/validations/coerce.rb +2 -1
  30. data/lib/grape/validations/presence.rb +2 -1
  31. data/lib/grape/validations/regexp.rb +2 -1
  32. data/lib/grape/version.rb +1 -1
  33. data/spec/grape/api_spec.rb +150 -5
  34. data/spec/grape/endpoint_spec.rb +51 -157
  35. data/spec/grape/entity_spec.rb +142 -520
  36. data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
  37. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
  38. data/spec/grape/exceptions/missing_mime_type_spec.rb +24 -0
  39. data/spec/grape/exceptions/missing_option_spec.rb +18 -0
  40. data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
  41. data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
  42. data/spec/grape/middleware/formatter_spec.rb +40 -34
  43. data/spec/grape/middleware/versioner/header_spec.rb +78 -20
  44. data/spec/grape/middleware/versioner/path_spec.rb +12 -8
  45. data/spec/grape/validations/coerce_spec.rb +1 -0
  46. data/spec/grape/validations/presence_spec.rb +8 -8
  47. data/spec/grape/validations_spec.rb +26 -3
  48. data/spec/spec_helper.rb +3 -6
  49. metadata +44 -9
  50. data/lib/grape/entity.rb +0 -386
@@ -47,6 +47,25 @@ describe Grape::Endpoint do
47
47
  end
48
48
  end
49
49
 
50
+ describe '#headers' do
51
+ before do
52
+ subject.get('/headers') do
53
+ headers.to_json
54
+ end
55
+ end
56
+ it 'includes request headers' do
57
+ get '/headers'
58
+ JSON.parse(last_response.body).should == {
59
+ "Host" => "example.org",
60
+ "Cookie" => ""
61
+ }
62
+ end
63
+ it 'includes additional request headers' do
64
+ get '/headers', nil, { "HTTP_X_GRAPE_CLIENT" => "1" }
65
+ JSON.parse(last_response.body)["X-Grape-Client"].should == "1"
66
+ end
67
+ end
68
+
50
69
  describe '#cookies' do
51
70
  it 'is callable from within a block' do
52
71
  subject.get('/get/cookies') do
@@ -104,10 +123,17 @@ describe Grape::Endpoint do
104
123
  end
105
124
  get('/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2')
106
125
  last_response.body.should == '3'
107
- last_response.headers['Set-Cookie'].split("\n").sort.should == [
108
- "and_this=deleted; expires=Thu, 01-Jan-1970 00:00:00 GMT",
109
- "delete_this_cookie=deleted; expires=Thu, 01-Jan-1970 00:00:00 GMT"
110
- ]
126
+ cookies = Hash[last_response.headers['Set-Cookie'].split("\n").map do |set_cookie|
127
+ cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie
128
+ [ cookie.name, cookie ]
129
+ end]
130
+ cookies.size.should == 2
131
+ [ "and_this", "delete_this_cookie" ].each do |cookie_name|
132
+ cookie = cookies[cookie_name]
133
+ cookie.should_not be_nil
134
+ cookie.value.should == "deleted"
135
+ cookie.expired?.should be_true
136
+ end
111
137
  end
112
138
 
113
139
  it 'deletes cookies with path' do
@@ -121,10 +147,18 @@ describe Grape::Endpoint do
121
147
  end
122
148
  get('/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2')
123
149
  last_response.body.should == '3'
124
- last_response.headers['Set-Cookie'].split("\n").sort.should == [
125
- "and_this=deleted; path=/test; expires=Thu, 01-Jan-1970 00:00:00 GMT",
126
- "delete_this_cookie=deleted; path=/test; expires=Thu, 01-Jan-1970 00:00:00 GMT"
127
- ]
150
+ cookies = Hash[last_response.headers['Set-Cookie'].split("\n").map do |set_cookie|
151
+ cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie
152
+ [ cookie.name, cookie ]
153
+ end]
154
+ cookies.size.should == 2
155
+ [ "and_this", "delete_this_cookie" ].each do |cookie_name|
156
+ cookie = cookies[cookie_name]
157
+ cookie.should_not be_nil
158
+ cookie.value.should == "deleted"
159
+ cookie.path.should == "/test"
160
+ cookie.expired?.should be_true
161
+ end
128
162
  end
129
163
  end
130
164
 
@@ -288,7 +322,7 @@ describe Grape::Endpoint do
288
322
  end
289
323
  put '/request_body', '<user>Bobby T.</user>', {'CONTENT_TYPE' => 'application/xml'}
290
324
  last_response.status.should == 406
291
- last_response.body.should == '{"error":"The requested content-type is not supported."}'
325
+ last_response.body.should == '{"error":"The requested content-type \'application/xml\' is not supported."}'
292
326
  end
293
327
 
294
328
  end
@@ -412,148 +446,6 @@ describe Grape::Endpoint do
412
446
  end
413
447
  end
414
448
 
415
- describe '#present' do
416
- it 'sets the object as the body if no options are provided' do
417
- subject.get '/example' do
418
- present({:abc => 'def'})
419
- body.should == {:abc => 'def'}
420
- end
421
- get '/example'
422
- end
423
-
424
- it 'calls through to the provided entity class if one is given' do
425
- subject.get '/example' do
426
- entity_mock = Object.new
427
- entity_mock.should_receive(:represent)
428
- present Object.new, :with => entity_mock
429
- end
430
- get '/example'
431
- end
432
-
433
- it 'pulls a representation from the class options if it exists' do
434
- entity = Class.new(Grape::Entity)
435
- entity.stub!(:represent).and_return("Hiya")
436
-
437
- subject.represent Object, :with => entity
438
- subject.get '/example' do
439
- present Object.new
440
- end
441
- get '/example'
442
- last_response.body.should == 'Hiya'
443
- end
444
-
445
- it 'pulls a representation from the class options if the presented object is a collection of objects' do
446
- entity = Class.new(Grape::Entity)
447
- entity.stub!(:represent).and_return("Hiya")
448
-
449
- class TestObject; end
450
-
451
- subject.represent TestObject, :with => entity
452
- subject.get '/example' do
453
- present [TestObject.new]
454
- end
455
- get '/example'
456
- last_response.body.should == "Hiya"
457
- end
458
-
459
- it 'pulls a representation from the class ancestor if it exists' do
460
- entity = Class.new(Grape::Entity)
461
- entity.stub!(:represent).and_return("Hiya")
462
-
463
- subclass = Class.new(Object)
464
-
465
- subject.represent Object, :with => entity
466
- subject.get '/example' do
467
- present subclass.new
468
- end
469
- get '/example'
470
- last_response.body.should == 'Hiya'
471
- end
472
-
473
- it 'automatically uses Klass::Entity if that exists' do
474
- some_model = Class.new
475
- entity = Class.new(Grape::Entity)
476
- entity.stub!(:represent).and_return("Auto-detect!")
477
-
478
- some_model.const_set :Entity, entity
479
-
480
- subject.get '/example' do
481
- present some_model.new
482
- end
483
- get '/example'
484
- last_response.body.should == 'Auto-detect!'
485
- end
486
-
487
- it 'automatically uses Klass::Entity based on the first object in the collection being presented' do
488
- some_model = Class.new
489
- entity = Class.new(Grape::Entity)
490
- entity.stub!(:represent).and_return("Auto-detect!")
491
-
492
- some_model.const_set :Entity, entity
493
-
494
- subject.get '/example' do
495
- present [some_model.new]
496
- end
497
- get '/example'
498
- last_response.body.should == 'Auto-detect!'
499
- end
500
-
501
- it 'adds a root key to the output if one is given' do
502
- subject.get '/example' do
503
- present({:abc => 'def'}, :root => :root)
504
- body.should == {:root => {:abc => 'def'}}
505
- end
506
- get '/example'
507
- end
508
-
509
- [ :json, :serializable_hash ].each do |format|
510
-
511
- it 'presents with #{format}' do
512
- entity = Class.new(Grape::Entity)
513
- entity.root "examples", "example"
514
- entity.expose :id
515
-
516
- subject.format format
517
- subject.get '/example' do
518
- c = Class.new do
519
- attr_reader :id
520
- def initialize(id)
521
- @id = id
522
- end
523
- end
524
- present c.new(1), :with => entity
525
- end
526
-
527
- get '/example'
528
- last_response.status.should == 200
529
- last_response.body.should == '{"example":{"id":1}}'
530
- end
531
-
532
- it 'presents with #{format} collection' do
533
- entity = Class.new(Grape::Entity)
534
- entity.root "examples", "example"
535
- entity.expose :id
536
-
537
- subject.format format
538
- subject.get '/examples' do
539
- c = Class.new do
540
- attr_reader :id
541
- def initialize(id)
542
- @id = id
543
- end
544
- end
545
- examples = [ c.new(1), c.new(2) ]
546
- present examples, :with => entity
547
- end
548
-
549
- get '/examples'
550
- last_response.status.should == 200
551
- last_response.body.should == '{"examples":[{"id":1},{"id":2}]}'
552
- end
553
-
554
- end
555
- end
556
-
557
449
  context 'filters' do
558
450
  describe 'before filters' do
559
451
  it 'runs the before filter if set' do
@@ -621,13 +513,15 @@ describe Grape::Endpoint do
621
513
  get '/url'
622
514
  last_response.body.should == "http://example.org/url"
623
515
  end
624
- it 'should include version' do
625
- subject.version 'v1', :using => :path
626
- subject.get('/url') do
627
- request.url
516
+ [ 'v1', :v1 ].each do |version|
517
+ it 'should include version #{version}' do
518
+ subject.version version, :using => :path
519
+ subject.get('/url') do
520
+ request.url
521
+ end
522
+ get "/#{version}/url"
523
+ last_response.body.should == "http://example.org/#{version}/url"
628
524
  end
629
- get '/v1/url'
630
- last_response.body.should == "http://example.org/v1/url"
631
525
  end
632
526
  it 'should include prefix' do
633
527
  subject.version 'v1', :using => :path
@@ -1,579 +1,201 @@
1
1
  require 'spec_helper'
2
+ require 'grape_entity'
2
3
 
3
4
  describe Grape::Entity do
4
- let(:fresh_class) { Class.new(Grape::Entity) }
5
+ subject { Class.new(Grape::API) }
6
+ def app; subject end
5
7
 
6
- context 'class methods' do
7
- subject { fresh_class }
8
-
9
- describe '.expose' do
10
- context 'multiple attributes' do
11
- it 'is able to add multiple exposed attributes with a single call' do
12
- subject.expose :name, :email, :location
13
- subject.exposures.size.should == 3
14
- end
15
-
16
- it 'sets the same options for all exposures passed' do
17
- subject.expose :name, :email, :location, :foo => :bar
18
- subject.exposures.values.each{|v| v.should == {:foo => :bar}}
19
- end
20
- end
21
-
22
- context 'option validation' do
23
- it 'makes sure that :as only works on single attribute calls' do
24
- expect{ subject.expose :name, :email, :as => :foo }.to raise_error(ArgumentError)
25
- expect{ subject.expose :name, :as => :foo }.not_to raise_error
26
- end
27
-
28
- it 'makes sure that :format_with as a proc can not be used with a block' do
29
- expect { subject.expose :name, :format_with => Proc.new {} do |_| end }.to raise_error(ArgumentError)
30
- end
31
- end
32
-
33
- context 'with a block' do
34
- it 'errors out if called with multiple attributes' do
35
- expect{ subject.expose(:name, :email) do
36
- true
37
- end }.to raise_error(ArgumentError)
38
- end
39
-
40
- it 'sets the :proc option in the exposure options' do
41
- block = lambda{|_| true }
42
- subject.expose :name, &block
43
- subject.exposures[:name][:proc].should == block
44
- end
8
+ describe '#present' do
9
+ it 'sets the object as the body if no options are provided' do
10
+ subject.get '/example' do
11
+ present({:abc => 'def'})
12
+ body.should == {:abc => 'def'}
45
13
  end
14
+ get '/example'
15
+ end
46
16
 
47
- context 'inherited exposures' do
48
- it 'returns exposures from an ancestor' do
49
- subject.expose :name, :email
50
- child_class = Class.new(subject)
51
-
52
- child_class.exposures.should eq(subject.exposures)
53
- end
54
-
55
- it 'returns exposures from multiple ancestor' do
56
- subject.expose :name, :email
57
- parent_class = Class.new(subject)
58
- child_class = Class.new(parent_class)
59
-
60
- child_class.exposures.should eq(subject.exposures)
61
- end
62
-
63
- it 'returns descendant exposures as a priority' do
64
- subject.expose :name, :email
65
- child_class = Class.new(subject)
66
- child_class.expose :name do |_|
67
- 'foo'
68
- end
69
-
70
- subject.exposures[:name].should_not have_key :proc
71
- child_class.exposures[:name].should have_key :proc
72
- end
17
+ it 'calls through to the provided entity class if one is given' do
18
+ subject.get '/example' do
19
+ entity_mock = Object.new
20
+ entity_mock.should_receive(:represent)
21
+ present Object.new, :with => entity_mock
73
22
  end
23
+ get '/example'
24
+ end
74
25
 
75
- context 'register formatters' do
76
- let(:date_formatter) { lambda {|date| date.strftime('%m/%d/%Y') }}
77
-
78
- it 'registers a formatter' do
79
- subject.format_with :timestamp, &date_formatter
80
-
81
- subject.formatters[:timestamp].should_not be_nil
82
- end
83
-
84
- it 'inherits formatters from ancestors' do
85
- subject.format_with :timestamp, &date_formatter
86
- child_class = Class.new(subject)
87
-
88
- child_class.formatters.should == subject.formatters
89
- end
90
-
91
- it 'does not allow registering a formatter without a block' do
92
- expect{ subject.format_with :foo }.to raise_error(ArgumentError)
93
- end
94
-
95
- it 'formats an exposure with a registered formatter' do
96
- subject.format_with :timestamp do |date|
97
- date.strftime('%m/%d/%Y')
98
- end
99
-
100
- subject.expose :birthday, :format_with => :timestamp
26
+ it 'pulls a representation from the class options if it exists' do
27
+ entity = Class.new(Grape::Entity)
28
+ entity.stub!(:represent).and_return("Hiya")
101
29
 
102
- model = { :birthday => Time.gm(2012, 2, 27) }
103
- subject.new(mock(model)).as_json[:birthday].should == '02/27/2012'
104
- end
30
+ subject.represent Object, :with => entity
31
+ subject.get '/example' do
32
+ present Object.new
105
33
  end
34
+ get '/example'
35
+ last_response.body.should == 'Hiya'
106
36
  end
107
37
 
108
- describe '.represent' do
109
- it 'returns a single entity if called with one object' do
110
- subject.represent(Object.new).should be_kind_of(subject)
111
- end
38
+ it 'pulls a representation from the class options if the presented object is a collection of objects' do
39
+ entity = Class.new(Grape::Entity)
40
+ entity.stub!(:represent).and_return("Hiya")
112
41
 
113
- it 'returns a single entity if called with a hash' do
114
- subject.represent(Hash.new).should be_kind_of(subject)
115
- end
42
+ class TestObject; end
116
43
 
117
- it 'returns multiple entities if called with a collection' do
118
- representation = subject.represent(4.times.map{Object.new})
119
- representation.should be_kind_of Array
120
- representation.size.should == 4
121
- representation.reject{|r| r.kind_of?(subject)}.should be_empty
122
- end
123
-
124
- it 'adds the :collection => true option if called with a collection' do
125
- representation = subject.represent(4.times.map{Object.new})
126
- representation.each{|r| r.options[:collection].should be_true}
44
+ subject.represent TestObject, :with => entity
45
+ subject.get '/example' do
46
+ present [TestObject.new]
127
47
  end
48
+ get '/example'
49
+ last_response.body.should == "Hiya"
128
50
  end
129
51
 
130
- describe '.root' do
131
- context 'with singular and plural root keys' do
132
- before(:each) do
133
- subject.root 'things', 'thing'
134
- end
135
-
136
- context 'with a single object' do
137
- it 'allows a root element name to be specified' do
138
- representation = subject.represent(Object.new)
139
- representation.should be_kind_of Hash
140
- representation.should have_key 'thing'
141
- representation['thing'].should be_kind_of(subject)
142
- end
143
- end
52
+ it 'pulls a representation from the class ancestor if it exists' do
53
+ entity = Class.new(Grape::Entity)
54
+ entity.stub!(:represent).and_return("Hiya")
144
55
 
145
- context 'with an array of objects' do
146
- it 'allows a root element name to be specified' do
147
- representation = subject.represent(4.times.map{Object.new})
148
- representation.should be_kind_of Hash
149
- representation.should have_key 'things'
150
- representation['things'].should be_kind_of Array
151
- representation['things'].size.should == 4
152
- representation['things'].reject{|r| r.kind_of?(subject)}.should be_empty
153
- end
154
- end
56
+ subclass = Class.new(Object)
155
57
 
156
- context 'it can be overridden' do
157
- it 'can be disabled' do
158
- representation = subject.represent(4.times.map{Object.new}, :root=>false)
159
- representation.should be_kind_of Array
160
- representation.size.should == 4
161
- representation.reject{|r| r.kind_of?(subject)}.should be_empty
162
- end
163
- it 'can use a different name' do
164
- representation = subject.represent(4.times.map{Object.new}, :root=>'others')
165
- representation.should be_kind_of Hash
166
- representation.should have_key 'others'
167
- representation['others'].should be_kind_of Array
168
- representation['others'].size.should == 4
169
- representation['others'].reject{|r| r.kind_of?(subject)}.should be_empty
170
- end
171
- end
58
+ subject.represent Object, :with => entity
59
+ subject.get '/example' do
60
+ present subclass.new
172
61
  end
62
+ get '/example'
63
+ last_response.body.should == 'Hiya'
64
+ end
173
65
 
174
- context 'with singular root key' do
175
- before(:each) do
176
- subject.root nil, 'thing'
177
- end
66
+ it 'automatically uses Klass::Entity if that exists' do
67
+ some_model = Class.new
68
+ entity = Class.new(Grape::Entity)
69
+ entity.stub!(:represent).and_return("Auto-detect!")
178
70
 
179
- context 'with a single object' do
180
- it 'allows a root element name to be specified' do
181
- representation = subject.represent(Object.new)
182
- representation.should be_kind_of Hash
183
- representation.should have_key 'thing'
184
- representation['thing'].should be_kind_of(subject)
185
- end
186
- end
71
+ some_model.const_set :Entity, entity
187
72
 
188
- context 'with an array of objects' do
189
- it 'allows a root element name to be specified' do
190
- representation = subject.represent(4.times.map{Object.new})
191
- representation.should be_kind_of Array
192
- representation.size.should == 4
193
- representation.reject{|r| r.kind_of?(subject)}.should be_empty
194
- end
195
- end
73
+ subject.get '/example' do
74
+ present some_model.new
196
75
  end
76
+ get '/example'
77
+ last_response.body.should == 'Auto-detect!'
78
+ end
197
79
 
198
- context 'with plural root key' do
199
- before(:each) do
200
- subject.root 'things'
201
- end
80
+ it 'automatically uses Klass::Entity based on the first object in the collection being presented' do
81
+ some_model = Class.new
82
+ entity = Class.new(Grape::Entity)
83
+ entity.stub!(:represent).and_return("Auto-detect!")
202
84
 
203
- context 'with a single object' do
204
- it 'allows a root element name to be specified' do
205
- subject.represent(Object.new).should be_kind_of(subject)
206
- end
207
- end
85
+ some_model.const_set :Entity, entity
208
86
 
209
- context 'with an array of objects' do
210
- it 'allows a root element name to be specified' do
211
- representation = subject.represent(4.times.map{Object.new})
212
- representation.should be_kind_of Hash
213
- representation.should have_key('things')
214
- representation['things'].should be_kind_of Array
215
- representation['things'].size.should == 4
216
- representation['things'].reject{|r| r.kind_of?(subject)}.should be_empty
217
- end
218
- end
87
+ subject.get '/example' do
88
+ present [some_model.new]
219
89
  end
90
+ get '/example'
91
+ last_response.body.should == 'Auto-detect!'
220
92
  end
221
93
 
222
- describe '#initialize' do
223
- it 'takes an object and an optional options hash' do
224
- expect{ subject.new(Object.new) }.not_to raise_error
225
- expect{ subject.new }.to raise_error(ArgumentError)
226
- expect{ subject.new(Object.new, {}) }.not_to raise_error
227
- end
228
-
229
- it 'has attribute readers for the object and options' do
230
- entity = subject.new('abc', {})
231
- entity.object.should == 'abc'
232
- entity.options.should == {}
94
+ it 'adds a root key to the output if one is given' do
95
+ subject.get '/example' do
96
+ present({:abc => 'def'}, :root => :root)
97
+ body.should == {:root => {:abc => 'def'}}
233
98
  end
99
+ get '/example'
234
100
  end
235
- end
236
-
237
- context 'instance methods' do
238
-
239
- let(:model){ mock(attributes) }
240
-
241
- let(:attributes) { {
242
- :name => 'Bob Bobson',
243
- :email => 'bob@example.com',
244
- :birthday => Time.gm(2012, 2, 27),
245
- :fantasies => ['Unicorns', 'Double Rainbows', 'Nessy'],
246
- :friends => [
247
- mock(:name => "Friend 1", :email => 'friend1@example.com', :fantasies => [], :birthday => Time.gm(2012, 2, 27), :friends => []),
248
- mock(:name => "Friend 2", :email => 'friend2@example.com', :fantasies => [], :birthday => Time.gm(2012, 2, 27), :friends => [])
249
- ]
250
- } }
251
-
252
- subject{ fresh_class.new(model) }
253
-
254
- describe '#serializable_hash' do
255
-
256
- it 'does not throw an exception if a nil options object is passed' do
257
- expect{ fresh_class.new(model).serializable_hash(nil) }.not_to raise_error
258
- end
259
-
260
- it 'does not blow up when the model is nil' do
261
- fresh_class.expose :name
262
- expect{ fresh_class.new(nil).serializable_hash }.not_to raise_error
263
- end
264
-
265
- it 'does not throw an exception when an attribute is not found on the object' do
266
- fresh_class.expose :name, :nonexistent_attribute
267
- expect{ fresh_class.new(model).serializable_hash }.not_to raise_error
268
- end
269
-
270
- it "does not expose attributes that don't exist on the object" do
271
- fresh_class.expose :email, :nonexistent_attribute, :name
272
-
273
- res = fresh_class.new(model).serializable_hash
274
- res.should have_key :email
275
- res.should_not have_key :nonexistent_attribute
276
- res.should have_key :name
277
- end
278
-
279
- it "does not expose attributes that don't exist on the object, even with criteria" do
280
- fresh_class.expose :email
281
- fresh_class.expose :nonexistent_attribute, :if => lambda { false }
282
- fresh_class.expose :nonexistent_attribute2, :if => lambda { true }
283
-
284
- res = fresh_class.new(model).serializable_hash
285
- res.should have_key :email
286
- res.should_not have_key :nonexistent_attribute
287
- res.should_not have_key :nonexistent_attribute2
288
- end
289
-
290
- it "exposes attributes that don't exist on the object only when they are generated by a block" do
291
- fresh_class.expose :nonexistent_attribute do |model, _|
292
- "well, I do exist after all"
293
- end
294
- res = fresh_class.new(model).serializable_hash
295
- res.should have_key :nonexistent_attribute
296
- end
297
101
 
298
- it "does not expose attributes that are generated by a block but have not passed criteria" do
299
- fresh_class.expose :nonexistent_attribute, :proc => lambda {|model, _|
300
- "I exist, but it is not yet my time to shine"
301
- }, :if => lambda { |model, _| false }
302
- res = fresh_class.new(model).serializable_hash
303
- res.should_not have_key :nonexistent_attribute
304
- end
102
+ [ :json, :serializable_hash ].each do |format|
305
103
 
306
- context '#serializable_hash' do
104
+ it 'presents with #{format}' do
105
+ entity = Class.new(Grape::Entity)
106
+ entity.root "examples", "example"
107
+ entity.expose :id
307
108
 
308
- module EntitySpec
309
- class EmbeddedExample
310
- def serializable_hash(opts = {})
311
- { :abc => 'def' }
312
- end
313
- end
314
- class EmbeddedExampleWithMany
315
- def name
316
- "abc"
317
- end
318
- def embedded
319
- [ EmbeddedExample.new, EmbeddedExample.new ]
109
+ subject.format format
110
+ subject.get '/example' do
111
+ c = Class.new do
112
+ attr_reader :id
113
+ def initialize(id)
114
+ @id = id
320
115
  end
321
116
  end
322
- class EmbeddedExampleWithOne
323
- def name
324
- "abc"
325
- end
326
- def embedded
327
- EmbeddedExample.new
328
- end
329
- end
330
- end
331
-
332
- it 'serializes embedded objects which respond to #serializable_hash' do
333
- fresh_class.expose :name, :embedded
334
- presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithOne.new)
335
- presenter.serializable_hash.should == {:name => "abc", :embedded => {:abc => "def"}}
336
- end
337
-
338
- it 'serializes embedded arrays of objects which respond to #serializable_hash' do
339
- fresh_class.expose :name, :embedded
340
- presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithMany.new)
341
- presenter.serializable_hash.should == {:name => "abc", :embedded => [{:abc => "def"}, {:abc => "def"}]}
342
- end
343
-
344
- end
345
-
346
- end
347
-
348
- describe '#value_for' do
349
- before do
350
- fresh_class.class_eval do
351
- expose :name, :email
352
- expose :friends, :using => self
353
- expose :computed do |_, options|
354
- options[:awesome]
355
- end
356
-
357
- expose :birthday, :format_with => :timestamp
358
-
359
- def timestamp(date)
360
- date.strftime('%m/%d/%Y')
361
- end
362
-
363
- expose :fantasies, :format_with => lambda {|f| f.reverse }
117
+ present c.new(1), :with => entity
364
118
  end
365
- end
366
-
367
- it 'passes through bare expose attributes' do
368
- subject.send(:value_for, :name).should == attributes[:name]
369
- end
370
119
 
371
- it 'instantiates a representation if that is called for' do
372
- rep = subject.send(:value_for, :friends)
373
- rep.reject{|r| r.is_a?(fresh_class)}.should be_empty
374
- rep.first.serializable_hash[:name].should == 'Friend 1'
375
- rep.last.serializable_hash[:name].should == 'Friend 2'
120
+ get '/example'
121
+ last_response.status.should == 200
122
+ last_response.body.should == '{"example":{"id":1}}'
376
123
  end
377
124
 
378
- context 'child representations' do
379
- it 'disables root key name for child representations' do
380
-
381
- module EntitySpec
382
- class FriendEntity < Grape::Entity
383
- root 'friends', 'friend'
384
- expose :name, :email
385
- end
386
- end
387
-
388
- fresh_class.class_eval do
389
- expose :friends, :using => EntitySpec::FriendEntity
390
- end
391
-
392
- rep = subject.send(:value_for, :friends)
393
- rep.should be_kind_of Array
394
- rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
395
- rep.first.serializable_hash[:name].should == 'Friend 1'
396
- rep.last.serializable_hash[:name].should == 'Friend 2'
397
- end
398
-
399
- it 'passes through custom options' do
400
- module EntitySpec
401
- class FriendEntity < Grape::Entity
402
- root 'friends', 'friend'
403
- expose :name
404
- expose :email, :if => { :user_type => :admin }
405
- end
406
- end
407
-
408
- fresh_class.class_eval do
409
- expose :friends, :using => EntitySpec::FriendEntity
410
- end
411
-
412
- rep = subject.send(:value_for, :friends)
413
- rep.should be_kind_of Array
414
- rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
415
- rep.first.serializable_hash[:email].should be_nil
416
- rep.last.serializable_hash[:email].should be_nil
417
-
418
- rep = subject.send(:value_for, :friends, { :user_type => :admin })
419
- rep.should be_kind_of Array
420
- rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
421
- rep.first.serializable_hash[:email].should == 'friend1@example.com'
422
- rep.last.serializable_hash[:email].should == 'friend2@example.com'
423
- end
125
+ it 'presents with #{format} collection' do
126
+ entity = Class.new(Grape::Entity)
127
+ entity.root "examples", "example"
128
+ entity.expose :id
424
129
 
425
- it 'ignores the :collection parameter in the source options' do
426
- module EntitySpec
427
- class FriendEntity < Grape::Entity
428
- root 'friends', 'friend'
429
- expose :name
430
- expose :email, :if => { :collection => true }
130
+ subject.format format
131
+ subject.get '/examples' do
132
+ c = Class.new do
133
+ attr_reader :id
134
+ def initialize(id)
135
+ @id = id
431
136
  end
432
137
  end
433
-
434
- fresh_class.class_eval do
435
- expose :friends, :using => EntitySpec::FriendEntity
436
- end
437
-
438
- rep = subject.send(:value_for, :friends, { :collection => false })
439
- rep.should be_kind_of Array
440
- rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
441
- rep.first.serializable_hash[:email].should == 'friend1@example.com'
442
- rep.last.serializable_hash[:email].should == 'friend2@example.com'
138
+ examples = [ c.new(1), c.new(2) ]
139
+ present examples, :with => entity
443
140
  end
444
141
 
142
+ get '/examples'
143
+ last_response.status.should == 200
144
+ last_response.body.should == '{"examples":[{"id":1},{"id":2}]}'
445
145
  end
446
146
 
447
- it 'calls through to the proc if there is one' do
448
- subject.send(:value_for, :computed, :awesome => 123).should == 123
449
- end
450
-
451
- it 'returns a formatted value if format_with is passed' do
452
- subject.send(:value_for, :birthday).should == '02/27/2012'
453
- end
454
-
455
- it 'returns a formatted value if format_with is passed a lambda' do
456
- subject.send(:value_for, :fantasies).should == ['Nessy', 'Double Rainbows', 'Unicorns']
457
- end
458
147
  end
459
148
 
460
- describe '#documentation' do
461
- it 'returns an empty hash is no documentation is provided' do
462
- fresh_class.expose :name
463
-
464
- subject.documentation.should == {}
465
- end
466
-
467
- it 'returns each defined documentation hash' do
468
- doc = {:type => "foo", :desc => "bar"}
469
- fresh_class.expose :name, :documentation => doc
470
- fresh_class.expose :email, :documentation => doc
471
- fresh_class.expose :birthday
472
-
473
- subject.documentation.should == {:name => doc, :email => doc}
474
- end
475
- end
476
-
477
- describe '#key_for' do
478
- it 'returns the attribute if no :as is set' do
479
- fresh_class.expose :name
480
- subject.send(:key_for, :name).should == :name
481
- end
482
-
483
- it 'returns a symbolized version of the attribute' do
484
- fresh_class.expose :name
485
- subject.send(:key_for, 'name').should == :name
486
- end
487
-
488
- it 'returns the :as alias if one exists' do
489
- fresh_class.expose :name, :as => :nombre
490
- subject.send(:key_for, 'name').should == :nombre
491
- end
149
+ it 'presents with xml' do
150
+ entity = Class.new(Grape::Entity)
151
+ entity.root "examples", "example"
152
+ entity.expose :name
153
+
154
+ subject.format :xml
155
+
156
+ subject.get '/example' do
157
+ c = Class.new do
158
+ attr_reader :name
159
+ def initialize(args)
160
+ @name = args[:name] || "no name set"
161
+ end
162
+ end
163
+ present c.new({:name => "johnnyiller"}), :with => entity
164
+ end
165
+ get '/example'
166
+ last_response.status.should == 200
167
+ last_response.headers['Content-type'].should == "application/xml"
168
+ last_response.body.should == <<-XML
169
+ <?xml version="1.0" encoding="UTF-8"?>
170
+ <hash>
171
+ <example>
172
+ <name>johnnyiller</name>
173
+ </example>
174
+ </hash>
175
+ XML
492
176
  end
493
177
 
494
- describe '#conditions_met?' do
495
- it 'only passes through hash :if exposure if all attributes match' do
496
- exposure_options = {:if => {:condition1 => true, :condition2 => true}}
178
+ it 'presents with json' do
179
+ entity = Class.new(Grape::Entity)
180
+ entity.root "examples", "example"
181
+ entity.expose :name
497
182
 
498
- subject.send(:conditions_met?, exposure_options, {}).should be_false
499
- subject.send(:conditions_met?, exposure_options, :condition1 => true).should be_false
500
- subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true).should be_true
501
- subject.send(:conditions_met?, exposure_options, :condition1 => false, :condition2 => true).should be_false
502
- subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true, :other => true).should be_true
503
- end
504
-
505
- it 'only passes through proc :if exposure if it returns truthy value' do
506
- exposure_options = {:if => lambda{|_,opts| opts[:true]}}
507
-
508
- subject.send(:conditions_met?, exposure_options, :true => false).should be_false
509
- subject.send(:conditions_met?, exposure_options, :true => true).should be_true
510
- end
511
-
512
- it 'only passes through hash :unless exposure if any attributes do not match' do
513
- exposure_options = {:unless => {:condition1 => true, :condition2 => true}}
183
+ subject.format :json
514
184
 
515
- subject.send(:conditions_met?, exposure_options, {}).should be_true
516
- subject.send(:conditions_met?, exposure_options, :condition1 => true).should be_false
517
- subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true).should be_false
518
- subject.send(:conditions_met?, exposure_options, :condition1 => false, :condition2 => true).should be_false
519
- subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true, :other => true).should be_false
520
- subject.send(:conditions_met?, exposure_options, :condition1 => false, :condition2 => false).should be_true
521
- end
522
-
523
- it 'only passes through proc :unless exposure if it returns falsy value' do
524
- exposure_options = {:unless => lambda{|_,options| options[:true] == true}}
525
-
526
- subject.send(:conditions_met?, exposure_options, :true => false).should be_true
527
- subject.send(:conditions_met?, exposure_options, :true => true).should be_false
528
- end
529
- end
530
-
531
- describe '::DSL' do
532
- subject{ Class.new }
533
-
534
- it 'creates an Entity class when called' do
535
- subject.should_not be_const_defined :Entity
536
- subject.send(:include, Grape::Entity::DSL)
537
- subject.should be_const_defined :Entity
538
- end
539
-
540
- context 'pre-mixed' do
541
- before{ subject.send(:include, Grape::Entity::DSL) }
542
-
543
- it 'is able to define entity traits through DSL' do
544
- subject.entity do
545
- expose :name
546
- end
547
-
548
- subject.entity_class.exposures.should_not be_empty
549
- end
550
-
551
- it 'is able to expose straight from the class' do
552
- subject.entity :name, :email
553
- subject.entity_class.exposures.size.should == 2
554
- end
555
-
556
- it 'is able to mix field and advanced exposures' do
557
- subject.entity :name, :email do
558
- expose :third
559
- end
560
- subject.entity_class.exposures.size.should == 3
561
- end
562
-
563
- context 'instance' do
564
- let(:instance){ subject.new }
565
-
566
- describe '#entity' do
567
- it 'is an instance of the entity class' do
568
- instance.entity.should be_kind_of(subject.entity_class)
569
- end
570
-
571
- it 'has an object of itself' do
572
- instance.entity.object.should == instance
573
- end
185
+ subject.get '/example' do
186
+ c = Class.new do
187
+ attr_reader :name
188
+ def initialize(args)
189
+ @name = args[:name] || "no name set"
574
190
  end
575
191
  end
192
+ present c.new({:name => "johnnyiller"}), :with => entity
576
193
  end
194
+ get '/example'
195
+ last_response.status.should == 200
196
+ last_response.headers['Content-type'].should == "application/json"
197
+ last_response.body.should == '{"example":{"name":"johnnyiller"}}'
577
198
  end
578
199
  end
579
- end
200
+
201
+ end