From 1d88ac3f91dc4048725032d9462e9ea0d4ca9f24 Mon Sep 17 00:00:00 2001 From: Philip Champon Date: Mon, 11 Jun 2012 15:23:36 -0400 Subject: [PATCH 1/5] Added documentation to entities --- lib/grape/entity.rb | 22 ++++++++++++++++++++++ spec/grape/entity_spec.rb | 17 +++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index 46a38a9b8a..2af0caefb7 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -93,6 +93,24 @@ def self.exposures @exposures end + # Returns a hash of documentation hashes that have been declared for this Entity or ancestors. The keys + # are symbolized references to methods on the containing object, the values are + # the options that were passed into expose. + 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. # @@ -217,6 +235,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 From 04d7efd0fee9f0d5f4ebb0c9a7df2a9f716ebe8d Mon Sep 17 00:00:00 2001 From: Philip Champon Date: Tue, 12 Jun 2012 13:05:58 -0400 Subject: [PATCH 2/5] updated documentation for entities --- README.markdown | 80 ++++++++++++++++++++++++++++++++++++++++++++- lib/grape/entity.rb | 13 +++++--- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/README.markdown b/README.markdown index c29af465fd..4bbd57b388 100644 --- a/README.markdown +++ b/README.markdown @@ -443,11 +443,89 @@ RSpec.configure do |config| end ``` +## Reusable Responses with Entities + +Entities are a simple and 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. The `#expose` method can be called in +a number of ways. When processing options passed to exposures two keys will always be defined, :version +and :collection. The :version key is define 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 +``` + ## 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 2af0caefb7..5b6b2f821a 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -63,6 +63,11 @@ 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 Provide documenation for an exposed + # field, typically the value is a hash with two fields, type and desc. + # Call the class or instance method #documentation for use with #desc. + # When calling #docmentation, any exposure without a documentation key + # will be ignored. 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? @@ -94,8 +99,8 @@ def self.exposures end # Returns a hash of documentation hashes that have been declared for this Entity or ancestors. The keys - # are symbolized references to methods on the containing object, the values are - # the options that were passed into expose. + # are symbolized references to fields in the response, the values are those defined in the + # Entity's documentation key. def self.documentation @documentation ||= exposures.inject({}) do |memo, value| unless value[1][:documentation].nil? || value[1][:documentation].empty? @@ -140,7 +145,7 @@ def self.documentation # 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 From 44dd623c4e98fe00feeac0c92186524df2f41adb Mon Sep 17 00:00:00 2001 From: Philip Champon Date: Tue, 12 Jun 2012 14:37:27 -0400 Subject: [PATCH 3/5] possibly improved docs --- README.markdown | 28 +++++++++++++++------------- lib/grape/entity.rb | 16 ++++++++-------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/README.markdown b/README.markdown index 4bbd57b388..fcac5e4a90 100644 --- a/README.markdown +++ b/README.markdown @@ -445,32 +445,34 @@ end ## Reusable Responses with Entities -Entities are a simple and reusable means for converting Ruby objects to API responses. +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. The `#expose` method can be called in -a number of ways. When processing options passed to exposures two keys will always be defined, :version -and :collection. The :version key is define as api.version. The :collection key is boolean, and defined -as true if the object presented is an array. +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 + * `expose SYMBOLS` * define a list of fields which will always be exposed - * expose SYMBOLS, HASH + * `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 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"} + * :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 + * :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 + * block can only be applied to one exposure at a time ``` ruby module API diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index 5b6b2f821a..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,11 +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 Provide documenation for an exposed + # @option options :documentation Define documenation for an exposed # field, typically the value is a hash with two fields, type and desc. - # Call the class or instance method #documentation for use with #desc. - # When calling #docmentation, any exposure without a documentation key - # will be ignored. def self.expose(*args, &block) options = args.last.is_a?(Hash) ? args.pop : {} @@ -98,9 +98,9 @@ def self.exposures @exposures end - # Returns a hash of documentation hashes that have been declared for this Entity or ancestors. The keys - # are symbolized references to fields in the response, the values are those defined in the - # Entity's documentation key. + # 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? From abb173890e1e4b585e71649c613f1dcb31c9324c Mon Sep 17 00:00:00 2001 From: Philip Champon Date: Tue, 12 Jun 2012 14:55:56 -0400 Subject: [PATCH 4/5] updated changelog --- CHANGELOG.markdown | 1 + 1 file changed, 1 insertion(+) 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) ================= From 3de8bf5afd1d250123bd6c4f92d289266e2d91fd Mon Sep 17 00:00:00 2001 From: Philip Champon Date: Tue, 12 Jun 2012 19:20:45 -0400 Subject: [PATCH 5/5] entity caveats --- README.markdown | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.markdown b/README.markdown index fcac5e4a90..d82034f78b 100644 --- a/README.markdown +++ b/README.markdown @@ -522,6 +522,37 @@ module API 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