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.
- data/{CHANGELOG.markdown → CHANGELOG.md} +21 -1
- data/Gemfile +1 -0
- data/{README.markdown → README.md} +178 -125
- data/grape.gemspec +1 -1
- data/lib/grape.rb +25 -3
- data/lib/grape/api.rb +43 -20
- data/lib/grape/endpoint.rb +32 -13
- data/lib/grape/exceptions/base.rb +50 -1
- data/lib/grape/exceptions/invalid_formatter.rb +13 -0
- data/lib/grape/exceptions/invalid_versioner_option.rb +14 -0
- data/lib/grape/exceptions/invalid_with_option_for_represent.rb +15 -0
- data/lib/grape/exceptions/missing_mime_type.rb +14 -0
- data/lib/grape/exceptions/missing_option.rb +13 -0
- data/lib/grape/exceptions/missing_vendor_option.rb +13 -0
- data/lib/grape/exceptions/unknown_options.rb +14 -0
- data/lib/grape/exceptions/unknown_validator.rb +12 -0
- data/lib/grape/exceptions/{validation_error.rb → validation.rb} +3 -1
- data/lib/grape/formatter/xml.rb +2 -1
- data/lib/grape/locale/en.yml +20 -0
- data/lib/grape/middleware/base.rb +0 -5
- data/lib/grape/middleware/error.rb +1 -2
- data/lib/grape/middleware/formatter.rb +9 -5
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/middleware/versioner/header.rb +16 -6
- data/lib/grape/middleware/versioner/param.rb +1 -1
- data/lib/grape/middleware/versioner/path.rb +1 -1
- data/lib/grape/util/content_types.rb +0 -2
- data/lib/grape/validations.rb +7 -14
- data/lib/grape/validations/coerce.rb +2 -1
- data/lib/grape/validations/presence.rb +2 -1
- data/lib/grape/validations/regexp.rb +2 -1
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +150 -5
- data/spec/grape/endpoint_spec.rb +51 -157
- data/spec/grape/entity_spec.rb +142 -520
- data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
- data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
- data/spec/grape/exceptions/missing_mime_type_spec.rb +24 -0
- data/spec/grape/exceptions/missing_option_spec.rb +18 -0
- data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
- data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
- data/spec/grape/middleware/formatter_spec.rb +40 -34
- data/spec/grape/middleware/versioner/header_spec.rb +78 -20
- data/spec/grape/middleware/versioner/path_spec.rb +12 -8
- data/spec/grape/validations/coerce_spec.rb +1 -0
- data/spec/grape/validations/presence_spec.rb +8 -8
- data/spec/grape/validations_spec.rb +26 -3
- data/spec/spec_helper.rb +3 -6
- metadata +44 -9
- data/lib/grape/entity.rb +0 -386
data/spec/grape/endpoint_spec.rb
CHANGED
@@ -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").
|
108
|
-
|
109
|
-
|
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").
|
125
|
-
|
126
|
-
|
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
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
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
|
data/spec/grape/entity_spec.rb
CHANGED
@@ -1,579 +1,201 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'grape_entity'
|
2
3
|
|
3
4
|
describe Grape::Entity do
|
4
|
-
|
5
|
+
subject { Class.new(Grape::API) }
|
6
|
+
def app; subject end
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
76
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
114
|
-
subject.represent(Hash.new).should be_kind_of(subject)
|
115
|
-
end
|
42
|
+
class TestObject; end
|
116
43
|
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
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
|
-
|
189
|
-
|
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
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
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
|
-
|
210
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
-
|
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
|
-
|
104
|
+
it 'presents with #{format}' do
|
105
|
+
entity = Class.new(Grape::Entity)
|
106
|
+
entity.root "examples", "example"
|
107
|
+
entity.expose :id
|
307
108
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
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
|
-
|
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
|
-
|
372
|
-
|
373
|
-
|
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
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
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
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
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
|
-
|
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
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
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
|
-
|
495
|
-
|
496
|
-
|
178
|
+
it 'presents with json' do
|
179
|
+
entity = Class.new(Grape::Entity)
|
180
|
+
entity.root "examples", "example"
|
181
|
+
entity.expose :name
|
497
182
|
|
498
|
-
|
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
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
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
|
-
|
200
|
+
|
201
|
+
end
|