diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index e17c4a01ad..b133ed221a 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -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) ================= diff --git a/README.markdown b/README.markdown index c29af465fd..d82034f78b 100644 --- a/README.markdown +++ b/README.markdown @@ -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 diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index 46a38a9b8a..890cf83849 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -3,7 +3,8 @@ 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 # @@ -11,6 +12,7 @@ module Grape # 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'} @@ -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 @@ -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 : {} @@ -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? @@ -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. # @@ -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 @@ -217,6 +240,10 @@ def exposures self.class.exposures end + def documentation + self.class.documentation + end + def formatters self.class.formatters end diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index e80bc41677..305806724d 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -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