- Notifications
You must be signed in to change notification settings - Fork152
An API focused facade that sits on top of an object model.
License
ruby-grape/grape-entity
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
- Grape::Entity
This gem adds Entity support to API frameworks, such asGrape. Grape's Entity is an API focused facade that sits on top of an object model.
moduleAPImoduleEntitiesclassStatus <Grape::Entityformat_with(:iso_timestamp){ |dt|dt.iso8601}expose:user_nameexpose:text,documentation:{type:"String",desc:"Status update text."}expose:ip,if:{type::full}expose:user_type,:user_id,if:lambda{ |status,options|status.user.public?}expose:location,merge:trueexpose:contact_infodoexpose:phoneexpose:address,merge:true,using:API::Entities::Addressendexpose:digestdo |status,options|Digest::MD5.hexdigeststatus.txtendexpose:replies,using:API::Entities::Status,as::responsesexpose:last_reply,using:API::Entities::Statusdo |status,options|status.replies.lastendwith_options(format_with::iso_timestamp)doexpose:created_atexpose:updated_atendendendendmoduleAPImoduleEntitiesclassStatusDetailed <API::Entities::Statusexpose:internal_idendendend
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.
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
.
Define a list of fields that will always be exposed.
expose:user_name,:ip
The field lookup takes several steps
- first try
entity-instance.exposure
- next try
object.exposure
- next try
object.fetch(exposure)
- last raise an Exception
exposure
is a Symbol by default. Ifobject
is a Hash with stringified keys, you can set the hash accessor at the entity-class level to properly expose its members:
classStatus <GrapeEntityself.hash_access=:to_sexpose:codeexpose:messageendStatus.represent({'code'=>418,'message'=>"I'm a teapot"}).as_json#=> { code: 418, message: "I'm a teapot" }
Don't derive your model classes fromGrape::Entity
, expose them using a presenter.
expose:replies,using:API::Entities::Status,as::responses
Presenter classes can also be specified in string format, which helps with circular dependencies.
expose:replies,using:"API::Entities::Status",as::responses
Use:if
or:unless
to expose fields conditionally.
expose:ip,if:{type::full}expose:ip,if:lambda{ |instance,options|options[:type] ==:full}# exposed if the function evaluates to trueexpose:ip,if::type# exposed if :type is available in the options hashexpose:ip,if:{type::full}# exposed if options :type has a value of :fullexpose:ip,unless: ...# the opposite of :if
Don't raise an exception and expose as nil, even if the :x cannot be evaluated.
expose:ip,safe:true
Supply a block to define a hash using nested exposures.
expose:contact_infodoexpose:phoneexpose:address,using:API::Entities::Addressend
You can also conditionally expose attributes in nested exposures:
expose:contact_infodoexpose:phoneexpose:address,using:API::Entities::Addressexpose:email,if:lambda{ |instance,options|options[:type] ==:full}end
Useroot(plural, singular = nil)
to expose an object or a collection of objects with a root key.
root'users','user'expose:id,:name, ...
By default every object of a collection is wrapped into an instance of yourEntity
class.You can override this behavior and wrap the whole collection into one instance of yourEntity
class.
As example:
present_collectiontrue,:collection_name# `collection_name` is optional and defaults to `items`expose:collection_name,using:API::Entities::Items
Use:merge
option to merge fields into the hash or into the root:
expose:contact_infodoexpose:phoneexpose:address,merge:true,using:API::Entities::Addressendexpose:status,merge:true
This will return something like:
{contact_info:{phone:"88002000700",city:'City 17',address_line:'Block C'},text:'HL3',likes:19}
It also works with collections:
expose:profilesdoexpose:users,merge:true,using:API::Entities::Userexpose:admins,merge:true,using:API::Entities::Adminend
Provide lambda to solve collisions:
expose:status,merge:->(key,old_val,new_val){old_val +new_valifold_val &&new_val}
Use a block or aProc
to evaluate exposure at runtime. The supplied block orProc
will be called with two parameters: the represented object and runtime options.
NOTE: A block supplied with no parameters will be evaluated as a nested exposure (see above).
expose:digestdo |status,options|Digest::MD5.hexdigeststatus.txtend
expose:digest,proc: ...# equivalent to a block
You can also define a method on the entity and it will try that before tryingon the object the entity wraps.
classExampleEntity <Grape::Entityexpose:attr_not_on_wrapped_object# ...privatedefattr_not_on_wrapped_object42endend
You always have access to the presented instance (object
) and the top-levelentity options (options
).
classExampleEntity <Grape::Entityexpose:formatted_value# ...privatedefformatted_value"+ X#{object.value}#{options[:y]}"endend
To undefine an exposed field, use the.unexpose
method. Useful for modifying inherited entities.
classUserData <Grape::Entityexpose:nameexpose:address1expose:address2expose:address_stateexpose:address_cityexpose:emailexpose:phoneendclassMailingAddress <UserDataunexpose:emailunexpose:phoneend
If you want to add one more exposure for the field but don't want the first one to be fired (for instance, when using inheritance), you can use theoverride
flag. For instance:
classUser <Grape::Entityexpose:nameendclassEmployee <Userexpose:name,as::employee_name,override:trueend
User
will return something like this{ "name" : "John" }
whileEmployee
will present the same data as{ "employee_name" : "John" }
instead of{ "name" : "John", "employee_name" : "John" }
.
After exposing the desired attributes, you can choose which one you need when representing some object or collection by using the only: and except: options. See the example:
classUserEntityexpose:idexpose:nameexpose:emailendclassEntityexpose:idexpose:titleexpose:user,using:UserEntityenddata=Entity.represent(model,only:[:title,{user:[:name,:email]}])data.as_json
This will return something like this:
{title:'grape-entity is awesome!',user:{name:'John Applet',email:'john@example.com'}}
Instead of returning all the exposed attributes.
The same result can be achieved with the following exposure:
data=Entity.represent(model,except:[:id,{user:[:id]}])data.as_json
Expose under a different name with:as
.
expose:replies,using:API::Entities::Status,as::responses
Apply a formatter before exposing a value.
moduleEntitiesclassMyModel <Grape::Entityformat_with(:iso_timestamp)do |date|date.iso8601endwith_options(format_with::iso_timestamp)doexpose:created_atexpose:updated_atendendend
Defining a reusable formatter between multiples entities:
moduleApiHelpersextendGrape::API::HelpersGrape::Entity.format_with:utcdo |date|date.utcifdateendend
moduleEntitiesclassMyModel <Grape::Entityexpose:updated_at,format_with::utcendclassAnotherModel <Grape::Entityexpose:created_at,format_with::utcendend
By default, exposures that containnil
values will be represented in the resulting JSON asnull
.
As an example, a hash with the following values:
{name:nil,age:100}
will result in a JSON object that looks like:
{"name":null,"age":100}
There are also times when, rather than displaying an attribute with anull
value, it is more desirable to not display the attribute at all. Using the hash from above the desired JSON would look like:
{"age":100}
In order to turn on this behavior for an as-exposure basis, the optionexpose_nil
can be used. By default,expose_nil
is considered to betrue
, meaning thatnil
values will be represented in JSON asnull
. Iffalse
is provided, then attributes withnil
values will be omitted from the resulting JSON completely.
moduleEntitiesclassMyModel <Grape::Entityexpose:name,expose_nil:falseexpose:age,expose_nil:falseendend
expose_nil
is per exposure, so you can suppress exposures from resulting innull
or expressnull
values on a per exposure basis as you need:
moduleEntitiesclassMyModel <Grape::Entityexpose:name,expose_nil:falseexpose:age# since expose_nil is omitted nil values will be rendered as nullendend
It is also possible to useexpose_nil
withwith_options
if you want to add the configuration to multiple exposures at once.
moduleEntitiesclassMyModel <Grape::Entity# None of the exposures in the with_options block will render nil values as nullwith_options(expose_nil:false)doexpose:nameexpose:ageendendend
When usingwith_options
, it is possible to again override which exposures will rendernil
asnull
by adding the option on a specific exposure.
moduleEntitiesclassMyModel <Grape::Entity# None of the exposures in the with_options block will render nil values as nullwith_options(expose_nil:false)doexpose:nameexpose:age,expose_nil:true# nil values would be rendered as null in the JSONendendend
This option can be used to provide a default value in case the return value is nil or empty.
moduleEntitiesclassMyModel <Grape::Entityexpose:name,default:''expose:age,default:60endend
Expose documentation with the field. Gets bubbled up when used with Grape and various API documentation systems.
expose:text,documentation:{type:"String",desc:"Status update text."}
The option keys:version
and:collection
are always defined. The:version
key is defined asapi.version
. The:collection
key is boolean, and defined astrue
if the object presented is an array. The options also contain the runtime environment in:env
, which includes request parameters inoptions[:env]['grape.request.params']
.
Any additional options defined on the entity exposure are included as is. In the following exampleuser
is set to the value ofcurrent_user
.
classStatus <Grape::Entityexpose:user,if:lambda{ |instance,options|options[:user]}do |instance,options|# examine available environment keys with `p options[:env].keys`options[:user]endend
present s, with: Status, user: current_user
Sometimes you want to pass additional options or parameters to nested a exposure. For example, let's say that you need to expose an address for a contact info and it has two different formats:full andsimple. You can pass an additionalfull_format
option to specify which format to render.
# api/contact.rbexpose:contact_infodoexpose:phoneexpose:addressdo |instance,options|# use `#merge` to extend options and then pass the new version of options to the nested entityAPI::Entities::Address.representinstance.address,options.merge(full_format:instance.need_full_format?)endexpose:email,if:lambda{ |instance,options|options[:type] ==:full}end# api/address.rbexpose:state,if:lambda{|instance,options| !!options[:full_format]}# the new option could be retrieved in options hash for conditional exposureexpose:city,if:lambda{|instance,options| !!options[:full_format]}expose:streetdo |instance,options|# the new option could be retrieved in options hash for runtime exposure !!options[:full_format] ?instance.full_street_name :instance.simple_street_nameend
Notice: In the above code, you should pay attention toSafe Exposure yourself. For example,instance.address
might benil
and it is better to expose it as nil directly.
Sometimes, especially when there are nested attributes, you might want to know which attributeis being exposed. For example, some APIs allow users to provide a parameter to control which fieldswill be included in (or excluded from) the response.
GrapeEntity can track the path of each attribute, which you can access during conditions checkingor runtime exposure viaoptions[:attr_path]
.
The attribute path is an array. The last item of this array is the name (alias) of current attribute.If the attribute is nested, the former items are names (aliases) of its ancestor attributes.
Example:
classStatus <Grape::Entityexpose:user# path is [:user]expose:foo,as::bar# path is [:bar]expose:adoexpose:b,as::xxdoexpose:c# path is [:a, :xx, :c]endendend
Grape ships with a DSL to easily define entities within the context of an existing class:
classStatusincludeGrape::Entity::DSLentity:text,:user_iddoexpose:detailed,if::conditionalendend
The above will automatically create aStatus::Entity
class and define properties on it according to the same rules as above. If you only want to define simple exposures you don't have to supply a block and can instead simply supply a list of comma-separated symbols.
With Grape, once an entity is defined, it can be used within endpoints, by callingpresent
. Thepresent
method accepts two arguments, theobject
to be presented and theoptions
associated with it. The options hash must always include:with
, which defines the entity to expose (unless namespaced entity classes are used, seenext section).If the entity includes documentation it can be included in an endpoint's description.
moduleAPIclassStatuses <Grape::APIversion'v1'desc'Statuses.',{params:API::Entities::Status.documentation}get'/statuses'dostatuses=Status.alltype=current_user.admin? ?:full ::defaultpresentstatuses,with:API::Entities::Status,type:typeendendend
In addition to separately organizing entities, it may be useful to put them as namespaced classes underneath the model they represent.
classStatusdefentityEntity.new(self)endclassEntity <Grape::Entityexpose:text,:user_idendend
If you organize your entities this way, Grape will automatically detect theEntity
class and use it to present your models. In this example, if you addedpresent Status.new
to your endpoint, Grape would automatically detect that there is aStatus::Entity
class and use that as the representative entity. This can still be overridden by using the:with
option or an explicitrepresents
call.
Entities with duplicate exposure names and conditions will silently overwrite one another. In the following example, whenobject.check
equals "foo", onlyfield_a
will be exposed. However, whenobject.check
equals "bar" bothfield_b
andfoo
will be exposed.
moduleAPImoduleEntitiesclassStatus <Grape::Entityexpose:field_a,:foo,if:lambda{ |object,options|object.check =="foo"}expose:field_b,:foo,if:lambda{ |object,options|object.check =="bar"}endendend
This can be problematic, when you have mixed collections. Usingrespond_to?
is safer.
moduleAPImoduleEntitiesclassStatus <Grape::Entityexpose:field_a,if:lambda{ |object,options|object.check =="foo"}expose:field_b,if:lambda{ |object,options|object.check =="bar"}expose:foo,if:lambda{ |object,options|object.respond_to?(:foo)}endendend
Also note that anArgumentError
is raised when unknown options are passed to eitherexpose
orwith_options
.
Add this line to your application's Gemfile:
gem 'grape-entity'
And then execute:
$ bundle
Or install it yourself as:
$ gem install grape-entity
Test API request/response as usual.
Also seeGrape Entity Matchers.
- Need help?Grape Google Group
SeeCONTRIBUTING.md.
MIT License. SeeLICENSE for details.
Copyright (c) 2010-2016 Michael Bleigh, Intridea, Inc., ruby-grape and Contributors.
About
An API focused facade that sits on top of an object model.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.