Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added documentation to entities #183

Merged
merged 5 commits into from
Jun 26, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Next Release
* [#166](https://github.com/intridea/grape/pull/166): Added support for `redirect`, including permanent and temporary - [@allenwei](https://github.com/allenwei).
* [#159](https://github.com/intridea/grape/pull/159): Added `:requirements` to routes, allowing to use reserved characters in paths - [@gaiottino](https://github.com/gaiottino).
* [#156](https://github.com/intridea/grape/pull/156): Added support for adding formatters to entities - [@bobbytables](https://github.com/bobbytables).
* [#183](https://github.com/intridea/grape/pull/183): Added ability to include documentation in entities - [@flah00](https://github.com/flah00)

0.2.0 (3/28/2012)
=================
Expand Down
113 changes: 112 additions & 1 deletion README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -443,11 +443,122 @@ RSpec.configure do |config|
end
```

## Reusable Responses with Entities

Entities are a reusable means for converting Ruby objects to API responses.
Entities can be used to conditionally include fields, nest other entities, and build
ever larger responses, using inheritance.

### Defining Entities

Entities inherit from Grape::Entity, and define a simple DSL. Exposures can use
runtime options to determine which fields should be visible, these options are
available to :if, :unless, and :proc. The option keys :version and :collection
will always be defined. The :version key is defined as api.version. The
:collection key is boolean, and defined as true if the object presented is an
array.

* `expose SYMBOLS`
* define a list of fields which will always be exposed
* `expose SYMBOLS, HASH`
* HASH keys include :if, :unless, :proc, :as, :using, :format_with, :documentation
* :if and :unless accept hashes (passed during runtime) or procs (arguments are object and options)
* `expose SYMBOL, {:format_with => :formatter}`
* expose a value, formatting it first
* :format_with can only be applied to one exposure at a time
* `expose SYMBOL, {:as => "alias"}`
* Expose a value, changing its hash key from SYMBOL to alias
* :as can only be applied to one exposure at a time
* `expose SYMBOL BLOCK`
* block arguments are object and options
* expose the value returned by the block
* block can only be applied to one exposure at a time

``` ruby
module API
module Entities
class User < Grape::Entity
expose :first_name, :last_name
expose :field, :documentation => {:type => "string", :desc => "words go here"}
expose :email, :if => {:type => :full}
expose :user_type, user_id, :if => lambda{|user,options| user.confirmed?}
expose(:name){|user,options| [user.first_name, user.last_name].join(' ')}
expose :latest_status, :using => API::Status, :as => :status
end
end
end

module API
module Entities
class UserDetailed < API::Entities::User
expose :account_id
end
end
end
```

### Using Entities

Once an entity is defined, it can be used within endpoints, by calling #present. The #present
method accepts two arguments, the object to be presented and the options associated with it. The
options hash must always include :with, which defines the entity to expose.

If the entity includes documentation it can be included in an endpoint's description.

``` ruby
module API
class Users < Grape::API
version 'v1'

desc 'User index', {
:object_fields => API::Entities::User.documentation
}
get '/users' do
@users = User.all
type = current_user.admin? ? :full : :default
present @users, with: API::Entities::User, :type => type
end
end
end
```

### Caveats

Entities with duplicate exposure names and conditions will silently overwrite one another.
In the following example, when object#check equals "foo", only afield will be exposed.
However, when object#check equals "bar" both bfield and foo will be exposed.

```ruby
module API
module Entities
class User < Grape::Entity
expose :afield, :foo, :if => lambda{|object,options| object.check=="foo"}
expose :bfield, :foo, :if => lambda{|object,options| object.check=="bar"}
end
end
end
```

This can be problematic, when you have mixed collections. Using #respond_to? is safer.

```ruby
module API
module Entities
class User < Grape::Entity
expose :afield, :if => lambda{|object,options| object.check=="foo"}
expose :bfield, :if => lambda{|object,options| object.check=="bar"}
expose :foo, :if => lambda{object,options| object.respond_to?(:foo)}
end
end
end
```

## Describing and Inspecting an API

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

``` ruby
class TwitterAPI < Grape::API
Expand Down
33 changes: 30 additions & 3 deletions lib/grape/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
module Grape
# An Entity is a lightweight structure that allows you to easily
# represent data from your application in a consistent and abstracted
# way in your API.
# way in your API. Entities can also provide documentation for the
# fields exposed.
#
# @example Entity Definition
#
# module API
# module Entities
# class User < Grape::Entity
# expose :first_name, :last_name, :screen_name, :location
# expose :field, :documentation => {:type => "string", :desc => "describe the field"}
# expose :latest_status, :using => API::Status, :as => :status, :unless => {:collection => true}
# expose :email, :if => {:type => :full}
# expose :new_attribute, :if => {:version => 'v2'}
Expand All @@ -30,6 +32,7 @@ module Grape
# class Users < Grape::API
# version 'v2'
#
# desc 'User index', { :object_fields => API::Entities::User.documentation }
# get '/users' do
# @users = User.all
# type = current_user.admin? ? :full : :default
Expand Down Expand Up @@ -63,6 +66,8 @@ class Entity
# will be called with the represented object as well as the
# runtime options that were passed in. You can also just supply a
# block to the expose call to achieve the same effect.
# @option options :documentation Define documenation for an exposed
# field, typically the value is a hash with two fields, type and desc.
def self.expose(*args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}

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

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

options[:proc] = block if block_given?

Expand All @@ -93,6 +98,24 @@ def self.exposures
@exposures
end

# Returns a hash, the keys are symbolized references to fields in the entity,
# the values are document keys in the entity's documentation key. When calling
# #docmentation, any exposure without a documentation key will be ignored.
def self.documentation
@documentation ||= exposures.inject({}) do |memo, value|
unless value[1][:documentation].nil? || value[1][:documentation].empty?
memo[value[0]] = value[1][:documentation]
end
memo
end

if superclass.respond_to? :documentation
@documentation = superclass.documentation.merge(@documentation)
end

@documentation
end

# This allows you to declare a Proc in which exposures can be formatted with.
# It take a block with an arity of 1 which is passed as the value of the exposed attribute.
#
Expand Down Expand Up @@ -122,7 +145,7 @@ def self.exposures
# end
#
def self.format_with(name, &block)
raise ArgumentError, "You must has a block for formatters" unless block_given?
raise ArgumentError, "You must pass a block for formatters" unless block_given?
formatters[name.to_sym] = block
end

Expand Down Expand Up @@ -217,6 +240,10 @@ def exposures
self.class.exposures
end

def documentation
self.class.documentation
end

def formatters
self.class.formatters
end
Expand Down
17 changes: 17 additions & 0 deletions spec/grape/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,23 @@ class FriendEntity < Grape::Entity
end
end

describe '#documentation' do
it 'should return an empty hash is no documentation is provided' do
fresh_class.expose :name

subject.documentation.should == {}
end

it 'should return each defined documentation hash' do
doc = {:type => "foo", :desc => "bar"}
fresh_class.expose :name, :documentation => doc
fresh_class.expose :email, :documentation => doc
fresh_class.expose :birthday

subject.documentation.should == {:name => doc, :email => doc}
end
end

describe '#key_for' do
it 'should return the attribute if no :as is set' do
fresh_class.expose :name
Expand Down