Skip to content

Commit 9013459

Browse files
committed
Merge pull request ruby-grape#183 from flah00/entities-with-documentation
Added documentation to entities
2 parents f8d280b + 3de8bf5 commit 9013459

File tree

4 files changed

+160
-4
lines changed

4 files changed

+160
-4
lines changed

CHANGELOG.markdown

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Next Release
88
* [#166](https://github.com/intridea/grape/pull/166): Added support for `redirect`, including permanent and temporary - [@allenwei](https://github.com/allenwei).
99
* [#159](https://github.com/intridea/grape/pull/159): Added `:requirements` to routes, allowing to use reserved characters in paths - [@gaiottino](https://github.com/gaiottino).
1010
* [#156](https://github.com/intridea/grape/pull/156): Added support for adding formatters to entities - [@bobbytables](https://github.com/bobbytables).
11+
* [#183](https://github.com/intridea/grape/pull/183): Added ability to include documentation in entities - [@flah00](https://github.com/flah00)
1112

1213
0.2.0 (3/28/2012)
1314
=================

README.markdown

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,11 +443,122 @@ RSpec.configure do |config|
443443
end
444444
```
445445

446+
## Reusable Responses with Entities
447+
448+
Entities are a reusable means for converting Ruby objects to API responses.
449+
Entities can be used to conditionally include fields, nest other entities, and build
450+
ever larger responses, using inheritance.
451+
452+
### Defining Entities
453+
454+
Entities inherit from Grape::Entity, and define a simple DSL. Exposures can use
455+
runtime options to determine which fields should be visible, these options are
456+
available to :if, :unless, and :proc. The option keys :version and :collection
457+
will always be defined. The :version key is defined as api.version. The
458+
:collection key is boolean, and defined as true if the object presented is an
459+
array.
460+
461+
* `expose SYMBOLS`
462+
* define a list of fields which will always be exposed
463+
* `expose SYMBOLS, HASH`
464+
* HASH keys include :if, :unless, :proc, :as, :using, :format_with, :documentation
465+
* :if and :unless accept hashes (passed during runtime) or procs (arguments are object and options)
466+
* `expose SYMBOL, {:format_with => :formatter}`
467+
* expose a value, formatting it first
468+
* :format_with can only be applied to one exposure at a time
469+
* `expose SYMBOL, {:as => "alias"}`
470+
* Expose a value, changing its hash key from SYMBOL to alias
471+
* :as can only be applied to one exposure at a time
472+
* `expose SYMBOL BLOCK`
473+
* block arguments are object and options
474+
* expose the value returned by the block
475+
* block can only be applied to one exposure at a time
476+
477+
``` ruby
478+
module API
479+
module Entities
480+
class User < Grape::Entity
481+
expose :first_name, :last_name
482+
expose :field, :documentation => {:type => "string", :desc => "words go here"}
483+
expose :email, :if => {:type => :full}
484+
expose :user_type, user_id, :if => lambda{|user,options| user.confirmed?}
485+
expose(:name){|user,options| [user.first_name, user.last_name].join(' ')}
486+
expose :latest_status, :using => API::Status, :as => :status
487+
end
488+
end
489+
end
490+
491+
module API
492+
module Entities
493+
class UserDetailed < API::Entities::User
494+
expose :account_id
495+
end
496+
end
497+
end
498+
```
499+
500+
### Using Entities
501+
502+
Once an entity is defined, it can be used within endpoints, by calling #present. The #present
503+
method accepts two arguments, the object to be presented and the options associated with it. The
504+
options hash must always include :with, which defines the entity to expose.
505+
506+
If the entity includes documentation it can be included in an endpoint's description.
507+
508+
``` ruby
509+
module API
510+
class Users < Grape::API
511+
version 'v1'
512+
513+
desc 'User index', {
514+
:object_fields => API::Entities::User.documentation
515+
}
516+
get '/users' do
517+
@users = User.all
518+
type = current_user.admin? ? :full : :default
519+
present @users, with: API::Entities::User, :type => type
520+
end
521+
end
522+
end
523+
```
524+
525+
### Caveats
526+
527+
Entities with duplicate exposure names and conditions will silently overwrite one another.
528+
In the following example, when object#check equals "foo", only afield will be exposed.
529+
However, when object#check equals "bar" both bfield and foo will be exposed.
530+
531+
```ruby
532+
module API
533+
module Entities
534+
class User < Grape::Entity
535+
expose :afield, :foo, :if => lambda{|object,options| object.check=="foo"}
536+
expose :bfield, :foo, :if => lambda{|object,options| object.check=="bar"}
537+
end
538+
end
539+
end
540+
```
541+
542+
This can be problematic, when you have mixed collections. Using #respond_to? is safer.
543+
544+
```ruby
545+
module API
546+
module Entities
547+
class User < Grape::Entity
548+
expose :afield, :if => lambda{|object,options| object.check=="foo"}
549+
expose :bfield, :if => lambda{|object,options| object.check=="bar"}
550+
expose :foo, :if => lambda{object,options| object.respond_to?(:foo)}
551+
end
552+
end
553+
end
554+
```
555+
446556
## Describing and Inspecting an API
447557

448558
Grape lets you add a description to an API along with any other optional
449559
elements that can also be inspected at runtime.
450-
This can be useful for generating documentation.
560+
This can be useful for generating documentation. If the response
561+
requires documentation, consider using an entity.
451562

452563
``` ruby
453564
class TwitterAPI < Grape::API

lib/grape/entity.rb

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
module Grape
44
# An Entity is a lightweight structure that allows you to easily
55
# represent data from your application in a consistent and abstracted
6-
# way in your API.
6+
# way in your API. Entities can also provide documentation for the
7+
# fields exposed.
78
#
89
# @example Entity Definition
910
#
1011
# module API
1112
# module Entities
1213
# class User < Grape::Entity
1314
# expose :first_name, :last_name, :screen_name, :location
15+
# expose :field, :documentation => {:type => "string", :desc => "describe the field"}
1416
# expose :latest_status, :using => API::Status, :as => :status, :unless => {:collection => true}
1517
# expose :email, :if => {:type => :full}
1618
# expose :new_attribute, :if => {:version => 'v2'}
@@ -30,6 +32,7 @@ module Grape
3032
# class Users < Grape::API
3133
# version 'v2'
3234
#
35+
# desc 'User index', { :object_fields => API::Entities::User.documentation }
3336
# get '/users' do
3437
# @users = User.all
3538
# type = current_user.admin? ? :full : :default
@@ -63,6 +66,8 @@ class Entity
6366
# will be called with the represented object as well as the
6467
# runtime options that were passed in. You can also just supply a
6568
# block to the expose call to achieve the same effect.
69+
# @option options :documentation Define documenation for an exposed
70+
# field, typically the value is a hash with two fields, type and desc.
6671
def self.expose(*args, &block)
6772
options = args.last.is_a?(Hash) ? args.pop : {}
6873

@@ -71,7 +76,7 @@ def self.expose(*args, &block)
7176
raise ArgumentError, "You may not use block-setting on multi-attribute exposures." if block_given?
7277
end
7378

74-
raise ArgumentError, "You may not use block-setting when also using " if block_given? && options[:format_with].respond_to?(:call)
79+
raise ArgumentError, "You may not use block-setting when also using format_with" if block_given? && options[:format_with].respond_to?(:call)
7580

7681
options[:proc] = block if block_given?
7782

@@ -93,6 +98,24 @@ def self.exposures
9398
@exposures
9499
end
95100

101+
# Returns a hash, the keys are symbolized references to fields in the entity,
102+
# the values are document keys in the entity's documentation key. When calling
103+
# #docmentation, any exposure without a documentation key will be ignored.
104+
def self.documentation
105+
@documentation ||= exposures.inject({}) do |memo, value|
106+
unless value[1][:documentation].nil? || value[1][:documentation].empty?
107+
memo[value[0]] = value[1][:documentation]
108+
end
109+
memo
110+
end
111+
112+
if superclass.respond_to? :documentation
113+
@documentation = superclass.documentation.merge(@documentation)
114+
end
115+
116+
@documentation
117+
end
118+
96119
# This allows you to declare a Proc in which exposures can be formatted with.
97120
# It take a block with an arity of 1 which is passed as the value of the exposed attribute.
98121
#
@@ -122,7 +145,7 @@ def self.exposures
122145
# end
123146
#
124147
def self.format_with(name, &block)
125-
raise ArgumentError, "You must has a block for formatters" unless block_given?
148+
raise ArgumentError, "You must pass a block for formatters" unless block_given?
126149
formatters[name.to_sym] = block
127150
end
128151

@@ -217,6 +240,10 @@ def exposures
217240
self.class.exposures
218241
end
219242

243+
def documentation
244+
self.class.documentation
245+
end
246+
220247
def formatters
221248
self.class.formatters
222249
end

spec/grape/entity_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,23 @@ class FriendEntity < Grape::Entity
317317
end
318318
end
319319

320+
describe '#documentation' do
321+
it 'should return an empty hash is no documentation is provided' do
322+
fresh_class.expose :name
323+
324+
subject.documentation.should == {}
325+
end
326+
327+
it 'should return each defined documentation hash' do
328+
doc = {:type => "foo", :desc => "bar"}
329+
fresh_class.expose :name, :documentation => doc
330+
fresh_class.expose :email, :documentation => doc
331+
fresh_class.expose :birthday
332+
333+
subject.documentation.should == {:name => doc, :email => doc}
334+
end
335+
end
336+
320337
describe '#key_for' do
321338
it 'should return the attribute if no :as is set' do
322339
fresh_class.expose :name

0 commit comments

Comments
 (0)