grape-entity 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +37 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/.yardopts +2 -0
- data/CHANGELOG.markdown +8 -0
- data/Gemfile +16 -0
- data/Guardfile +15 -0
- data/LICENSE +20 -0
- data/README.markdown +210 -0
- data/Rakefile +51 -0
- data/grape-entity.gemspec +35 -0
- data/lib/grape-entity.rb +1 -0
- data/lib/grape_entity.rb +5 -0
- data/lib/grape_entity/entity.rb +389 -0
- data/lib/grape_entity/version.rb +3 -0
- data/spec/grape_entity/entity_spec.rb +579 -0
- data/spec/spec_helper.rb +12 -0
- data/tmp/rspec_guard_result +1 -0
- metadata +154 -0
data/.gitignore
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
.com.apple.timemachine.supported
|
4
|
+
|
5
|
+
## TEXTMATE
|
6
|
+
*.tmproj
|
7
|
+
tmtags
|
8
|
+
|
9
|
+
## EMACS
|
10
|
+
*~
|
11
|
+
\#*
|
12
|
+
.\#*
|
13
|
+
|
14
|
+
## REDCAR
|
15
|
+
.redcar
|
16
|
+
|
17
|
+
## VIM
|
18
|
+
*.swp
|
19
|
+
*.swo
|
20
|
+
|
21
|
+
## RUBYMINE
|
22
|
+
.idea
|
23
|
+
|
24
|
+
## PROJECT::GENERAL
|
25
|
+
coverage
|
26
|
+
doc
|
27
|
+
pkg
|
28
|
+
.rvmrc
|
29
|
+
.bundle
|
30
|
+
.yardoc/*
|
31
|
+
dist
|
32
|
+
Gemfile.lock
|
33
|
+
|
34
|
+
## Rubinius
|
35
|
+
.rbx
|
36
|
+
|
37
|
+
## PROJECT::SPECIFIC
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/CHANGELOG.markdown
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
group :development, :test do
|
6
|
+
gem 'pry'
|
7
|
+
gem 'guard'
|
8
|
+
gem 'guard-rspec'
|
9
|
+
gem 'guard-bundler'
|
10
|
+
gem 'rb-fsevent'
|
11
|
+
gem 'growl'
|
12
|
+
gem 'json'
|
13
|
+
gem 'rspec'
|
14
|
+
gem 'rack-test', "~> 0.6.2", :require => "rack/test"
|
15
|
+
gem 'github-markup'
|
16
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', :version => 2 do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
7
|
+
watch(%r{^spec/support/shared_versioning_examples.rb$}) { |m| "spec/" }
|
8
|
+
watch('spec/spec_helper.rb') { "spec/" }
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
guard 'bundler' do
|
13
|
+
watch('Gemfile')
|
14
|
+
watch(/^.+\.gemspec/)
|
15
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Michael Bleigh and Intridea, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
# GrapeEntity
|
2
|
+
|
3
|
+
## CI
|
4
|
+
[![Build Status](https://travis-ci.org/agileanimal/grape-entity.png?branch=master)](https://travis-ci.org/agileanimal/grape-entity)
|
5
|
+
|
6
|
+
## Introduction
|
7
|
+
|
8
|
+
This gem is the Entity extracted out of [Grape](https://github.com/intridea/grape).
|
9
|
+
|
10
|
+
Grape's Entity is a great idea: a API focussed Facade that sits on top of a model object.
|
11
|
+
|
12
|
+
The intend is to allow the Entity to be used outside of Grape and provide additional exposure to parts of the entity needed to simplify testing parts of an API.
|
13
|
+
|
14
|
+
We intend to use the specs from grape to ensure we maintain compatability with Grape through our changes so that we can use the Entity to replace Grape's internal Entity.
|
15
|
+
|
16
|
+
## Goals
|
17
|
+
|
18
|
+
Through my own use of Entities I have found them really useful but wished they could be reused in other situations. In the context of Grape I wish it was easier to test the Entity so that you could test them seperately from your API and isolate their behavior. Entities also have a deep connection with the model and they should know what parameters are required and (especially in the case of an ActiveRecord Model) they should know how to ask about validation.
|
19
|
+
|
20
|
+
The Entity is a simple Facade on top of the model to transform it into the object you want to expose on your API.
|
21
|
+
|
22
|
+
There is probably something heavier than an Entity that exists as well. A way to get a model object back from the Entity.
|
23
|
+
|
24
|
+
In this spirit I want to give Entities a life of their own.
|
25
|
+
|
26
|
+
## Project Tracking
|
27
|
+
|
28
|
+
* - Need to setup something up for this -
|
29
|
+
|
30
|
+
## Reusable Responses with Entities
|
31
|
+
|
32
|
+
Entities are a reusable means for converting Ruby objects to API responses.
|
33
|
+
Entities can be used to conditionally include fields, nest other entities, and build
|
34
|
+
ever larger responses, using inheritance.
|
35
|
+
|
36
|
+
### Defining Entities
|
37
|
+
|
38
|
+
Entities inherit from GrapeEntity::Entity, and define a simple DSL. Exposures can use
|
39
|
+
runtime options to determine which fields should be visible, these options are
|
40
|
+
available to `:if`, `:unless`, and `:proc`. The option keys `:version` and `:collection`
|
41
|
+
will always be defined. The `:version` key is defined as `api.version`. The
|
42
|
+
`:collection` key is boolean, and defined as `true` if the object presented is an
|
43
|
+
array.
|
44
|
+
|
45
|
+
* `expose SYMBOLS`
|
46
|
+
* define a list of fields which will always be exposed
|
47
|
+
* `expose SYMBOLS, HASH`
|
48
|
+
* HASH keys include `:if`, `:unless`, `:proc`, `:as`, `:using`, `:format_with`, `:documentation`
|
49
|
+
* `:if` and `:unless` accept hashes (passed during runtime) or procs (arguments are object and options)
|
50
|
+
* `expose SYMBOL, { :format_with => :formatter }`
|
51
|
+
* expose a value, formatting it first
|
52
|
+
* `:format_with` can only be applied to one exposure at a time
|
53
|
+
* `expose SYMBOL, { :as => "alias" }`
|
54
|
+
* Expose a value, changing its hash key from SYMBOL to alias
|
55
|
+
* `:as` can only be applied to one exposure at a time
|
56
|
+
* `expose SYMBOL BLOCK`
|
57
|
+
* block arguments are object and options
|
58
|
+
* expose the value returned by the block
|
59
|
+
* block can only be applied to one exposure at a time
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
module API
|
63
|
+
module Entities
|
64
|
+
class Status < GrapeEntity::Entity
|
65
|
+
expose :user_name
|
66
|
+
expose :text, :documentation => { :type => "string", :desc => "Status update text." }
|
67
|
+
expose :ip, :if => { :type => :full }
|
68
|
+
expose :user_type, user_id, :if => lambda{ |status, options| status.user.public? }
|
69
|
+
expose :digest { |status, options| Digest::MD5.hexdigest(satus.txt) }
|
70
|
+
expose :replies, :using => API::Status, :as => :replies
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
module API
|
76
|
+
module Entities
|
77
|
+
class StatusDetailed < API::Entities::Status
|
78
|
+
expose :internal_id
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
#### Using the Exposure DSL
|
85
|
+
|
86
|
+
Grape ships with a DSL to easily define entities within the context
|
87
|
+
of an existing class:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
class Status
|
91
|
+
include GrapeEntity::Entity::DSL
|
92
|
+
|
93
|
+
entity :text, :user_id do
|
94
|
+
expose :detailed, if: :conditional
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
The above will automatically create a `Status::Entity` class and define properties on it according
|
100
|
+
to the same rules as above. If you only want to define simple exposures you don't have to supply
|
101
|
+
a block and can instead simply supply a list of comma-separated symbols.
|
102
|
+
|
103
|
+
### Using Entities
|
104
|
+
|
105
|
+
Once an entity is defined, it can be used within endpoints, by calling `present`. The `present`
|
106
|
+
method accepts two arguments, the object to be presented and the options associated with it. The
|
107
|
+
options hash must always include `:with`, which defines the entity to expose.
|
108
|
+
|
109
|
+
If the entity includes documentation it can be included in an endpoint's description.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
module API
|
113
|
+
class Statuses < GrapeEntity::API
|
114
|
+
version 'v1'
|
115
|
+
|
116
|
+
desc 'Statuses index', {
|
117
|
+
:object_fields => API::Entities::Status.documentation
|
118
|
+
}
|
119
|
+
get '/statuses' do
|
120
|
+
statuses = Status.all
|
121
|
+
type = current_user.admin? ? :full : :default
|
122
|
+
present statuses, with: API::Entities::Status, :type => type
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
### Entity Organization
|
129
|
+
|
130
|
+
In addition to separately organizing entities, it may be useful to put them as namespaced
|
131
|
+
classes underneath the model they represent.
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
class Status
|
135
|
+
def entity
|
136
|
+
Status.new(self)
|
137
|
+
end
|
138
|
+
|
139
|
+
class Entity < GrapeEntity::Entity
|
140
|
+
expose :text, :user_id
|
141
|
+
end
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
If you organize your entities this way, Grape will automatically detect the `Entity` class and
|
146
|
+
use it to present your models. In this example, if you added `present User.new` to your endpoint,
|
147
|
+
Grape would automatically detect that there is a `Status::Entity` class and use that as the
|
148
|
+
representative entity. This can still be overridden by using the `:with` option or an explicit
|
149
|
+
`represents` call.
|
150
|
+
|
151
|
+
### Caveats
|
152
|
+
|
153
|
+
Entities with duplicate exposure names and conditions will silently overwrite one another.
|
154
|
+
In the following example, when `object.check` equals "foo", only `field_a` will be exposed.
|
155
|
+
However, when `object.check` equals "bar" both `field_b` and `foo` will be exposed.
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
module API
|
159
|
+
module Entities
|
160
|
+
class Status < GrapeEntity::Entity
|
161
|
+
expose :field_a, :foo, :if => lambda { |object, options| object.check == "foo" }
|
162
|
+
expose :field_b, :foo, :if => lambda { |object, options| object.check == "bar" }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
This can be problematic, when you have mixed collections. Using `respond_to?` is safer.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
module API
|
172
|
+
module Entities
|
173
|
+
class Status < GrapeEntity::Entity
|
174
|
+
expose :field_a, :if => lambda { |object, options| object.check == "foo" }
|
175
|
+
expose :field_b, :if => lambda { |object, options| object.check == "bar" }
|
176
|
+
expose :foo, :if => lambda { |object, options| object.respond_to?(:foo) }
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
## Installation
|
183
|
+
|
184
|
+
Add this line to your application's Gemfile:
|
185
|
+
|
186
|
+
gem 'grape-entity'
|
187
|
+
|
188
|
+
And then execute:
|
189
|
+
|
190
|
+
$ bundle
|
191
|
+
|
192
|
+
Or install it yourself as:
|
193
|
+
|
194
|
+
$ gem install grape-entity
|
195
|
+
|
196
|
+
## Contributing
|
197
|
+
|
198
|
+
1. Fork it
|
199
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
200
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
201
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
202
|
+
5. Create new Pull Request
|
203
|
+
|
204
|
+
## License
|
205
|
+
|
206
|
+
MIT License. See LICENSE for details.
|
207
|
+
|
208
|
+
## Copyright
|
209
|
+
|
210
|
+
Copyright (c) 2010-2012 Michael Bleigh, and Intridea, Inc.
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.setup :default, :test, :development
|
4
|
+
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
|
7
|
+
require 'rspec/core/rake_task'
|
8
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
9
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
13
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
14
|
+
spec.rcov = true
|
15
|
+
end
|
16
|
+
|
17
|
+
task :spec
|
18
|
+
task :default => :spec
|
19
|
+
|
20
|
+
#
|
21
|
+
# TODO: setup a place for documentation and then get this going again.
|
22
|
+
#
|
23
|
+
# begin
|
24
|
+
# require 'yard'
|
25
|
+
# DOC_FILES = ['lib/**/*.rb', 'README.markdown']
|
26
|
+
#
|
27
|
+
# YARD::Rake::YardocTask.new(:doc) do |t|
|
28
|
+
# t.files = DOC_FILES
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# namespace :doc do
|
32
|
+
# YARD::Rake::YardocTask.new(:pages) do |t|
|
33
|
+
# t.files = DOC_FILES
|
34
|
+
# t.options = ['-o', '../grape.doc']
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# namespace :pages do
|
38
|
+
# desc 'Generate and publish YARD docs to GitHub pages.'
|
39
|
+
# task :publish => ['doc:pages'] do
|
40
|
+
# Dir.chdir(File.dirname(__FILE__) + '/../grape.doc') do
|
41
|
+
# system("git add .")
|
42
|
+
# system("git add -u")
|
43
|
+
# system("git commit -m 'Generating docs for version #{version}.'")
|
44
|
+
# system("git push origin gh-pages")
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
# rescue LoadError
|
50
|
+
# puts "You need to install YARD."
|
51
|
+
# end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "grape_entity/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "grape-entity"
|
6
|
+
s.version = GrapeEntity::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Michael Bleigh"]
|
9
|
+
s.email = ["michael@intridea.com"]
|
10
|
+
s.homepage = "https://github.com/intridea/grape"
|
11
|
+
s.summary = %q{A simple facade for managing the relationship between your model and API.}
|
12
|
+
s.description = %q{Extracted from Grape, A Ruby framework for rapid API development with great conventions.}
|
13
|
+
s.license = "MIT"
|
14
|
+
|
15
|
+
s.rubyforge_project = "grape-entity"
|
16
|
+
|
17
|
+
# s.add_runtime_dependency 'activesupport'
|
18
|
+
# s.add_runtime_dependency 'rack-jsonp'
|
19
|
+
# s.add_runtime_dependency 'multi_json', '>= 1.3.2'
|
20
|
+
# s.add_runtime_dependency 'multi_xml', '>= 0.5.2'
|
21
|
+
# s.add_runtime_dependency 'hashie', '~> 1.2'
|
22
|
+
# s.add_runtime_dependency 'virtus'
|
23
|
+
# s.add_runtime_dependency 'builder'
|
24
|
+
|
25
|
+
s.add_development_dependency 'rake'
|
26
|
+
s.add_development_dependency 'maruku'
|
27
|
+
s.add_development_dependency 'yard'
|
28
|
+
s.add_development_dependency 'rspec', '~> 2.9'
|
29
|
+
s.add_development_dependency 'bundler'
|
30
|
+
|
31
|
+
s.files = `git ls-files`.split("\n")
|
32
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
33
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
34
|
+
s.require_paths = ["lib"]
|
35
|
+
end
|
data/lib/grape-entity.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'grape_entity'
|
data/lib/grape_entity.rb
ADDED
@@ -0,0 +1,389 @@
|
|
1
|
+
module GrapeEntity
|
2
|
+
# An Entity is a lightweight structure that allows you to easily
|
3
|
+
# represent data from your application in a consistent and abstracted
|
4
|
+
# way in your API. Entities can also provide documentation for the
|
5
|
+
# fields exposed.
|
6
|
+
#
|
7
|
+
# @example Entity Definition
|
8
|
+
#
|
9
|
+
# module API
|
10
|
+
# module Entities
|
11
|
+
# class User < GrapeEntity::Entity
|
12
|
+
# expose :first_name, :last_name, :screen_name, :location
|
13
|
+
# expose :field, :documentation => {:type => "string", :desc => "describe the field"}
|
14
|
+
# expose :latest_status, :using => API::Status, :as => :status, :unless => {:collection => true}
|
15
|
+
# expose :email, :if => {:type => :full}
|
16
|
+
# expose :new_attribute, :if => {:version => 'v2'}
|
17
|
+
# expose(:name){|model,options| [model.first_name, model.last_name].join(' ')}
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Entities are not independent structures, rather, they create
|
23
|
+
# **representations** of other Ruby objects using a number of methods
|
24
|
+
# that are convenient for use in an API. Once you've defined an Entity,
|
25
|
+
# you can use it in your API like this:
|
26
|
+
#
|
27
|
+
# @example Usage in the API Layer
|
28
|
+
#
|
29
|
+
# module API
|
30
|
+
# class Users < GrapeEntity::API
|
31
|
+
# version 'v2'
|
32
|
+
#
|
33
|
+
# desc 'User index', { :object_fields => API::Entities::User.documentation }
|
34
|
+
# get '/users' do
|
35
|
+
# @users = User.all
|
36
|
+
# type = current_user.admin? ? :full : :default
|
37
|
+
# present @users, :with => API::Entities::User, :type => type
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
class Entity
|
42
|
+
attr_reader :object, :options
|
43
|
+
|
44
|
+
# The Entity DSL allows you to mix entity functionality into
|
45
|
+
# your existing classes.
|
46
|
+
module DSL
|
47
|
+
def self.included(base)
|
48
|
+
base.extend ClassMethods
|
49
|
+
ancestor_entity_class = base.ancestors.detect{|a| a.entity_class if a.respond_to?(:entity_class)}
|
50
|
+
base.const_set(:Entity, Class.new(ancestor_entity_class || GrapeEntity::Entity)) unless const_defined?(:Entity)
|
51
|
+
end
|
52
|
+
|
53
|
+
module ClassMethods
|
54
|
+
# Returns the automatically-created entity class for this
|
55
|
+
# Class.
|
56
|
+
def entity_class(search_ancestors=true)
|
57
|
+
klass = const_get(:Entity) if const_defined?(:Entity)
|
58
|
+
klass ||= ancestors.detect{|a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors
|
59
|
+
klass
|
60
|
+
end
|
61
|
+
|
62
|
+
# Call this to make exposures to the entity for this Class.
|
63
|
+
# Can be called with symbols for the attributes to expose,
|
64
|
+
# a block that yields the full Entity DSL (See GrapeEntity::Entity),
|
65
|
+
# or both.
|
66
|
+
#
|
67
|
+
# @example Symbols only.
|
68
|
+
#
|
69
|
+
# class User
|
70
|
+
# include GrapeEntity::Entity::DSL
|
71
|
+
#
|
72
|
+
# entity :name, :email
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# @example Mixed.
|
76
|
+
#
|
77
|
+
# class User
|
78
|
+
# include GrapeEntity::Entity::DSL
|
79
|
+
#
|
80
|
+
# entity :name, :email do
|
81
|
+
# expose :latest_status, using: Status::Entity, if: :include_status
|
82
|
+
# expose :new_attribute, :if => {:version => 'v2'}
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
def entity(*exposures, &block)
|
86
|
+
entity_class.expose *exposures if exposures.any?
|
87
|
+
entity_class.class_eval(&block) if block_given?
|
88
|
+
entity_class
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Instantiates an entity version of this object.
|
93
|
+
def entity
|
94
|
+
self.class.entity_class.new(self)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# This method is the primary means by which you will declare what attributes
|
99
|
+
# should be exposed by the entity.
|
100
|
+
#
|
101
|
+
# @option options :as Declare an alias for the representation of this attribute.
|
102
|
+
# @option options :if When passed a Hash, the attribute will only be exposed if the
|
103
|
+
# runtime options match all the conditions passed in. When passed a lambda, the
|
104
|
+
# lambda will execute with two arguments: the object being represented and the
|
105
|
+
# options passed into the representation call. Return true if you want the attribute
|
106
|
+
# to be exposed.
|
107
|
+
# @option options :unless When passed a Hash, the attribute will be exposed if the
|
108
|
+
# runtime options fail to match any of the conditions passed in. If passed a lambda,
|
109
|
+
# it will yield the object being represented and the options passed to the
|
110
|
+
# representation call. Return true to prevent exposure, false to allow it.
|
111
|
+
# @option options :using This option allows you to map an attribute to another Grape
|
112
|
+
# Entity. Pass it a GrapeEntity::Entity class and the attribute in question will
|
113
|
+
# automatically be transformed into a representation that will receive the same
|
114
|
+
# options as the parent entity when called. Note that arrays are fine here and
|
115
|
+
# will automatically be detected and handled appropriately.
|
116
|
+
# @option options :proc If you pass a Proc into this option, it will
|
117
|
+
# be used directly to determine the value for that attribute. It
|
118
|
+
# will be called with the represented object as well as the
|
119
|
+
# runtime options that were passed in. You can also just supply a
|
120
|
+
# block to the expose call to achieve the same effect.
|
121
|
+
# @option options :documentation Define documenation for an exposed
|
122
|
+
# field, typically the value is a hash with two fields, type and desc.
|
123
|
+
def self.expose(*args, &block)
|
124
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
125
|
+
|
126
|
+
if args.size > 1
|
127
|
+
raise ArgumentError, "You may not use the :as option on multi-attribute exposures." if options[:as]
|
128
|
+
raise ArgumentError, "You may not use block-setting on multi-attribute exposures." if block_given?
|
129
|
+
end
|
130
|
+
|
131
|
+
raise ArgumentError, "You may not use block-setting when also using format_with" if block_given? && options[:format_with].respond_to?(:call)
|
132
|
+
|
133
|
+
options[:proc] = block if block_given?
|
134
|
+
|
135
|
+
args.each do |attribute|
|
136
|
+
exposures[attribute.to_sym] = options
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns a hash of exposures that have been declared for this Entity or ancestors. The keys
|
141
|
+
# are symbolized references to methods on the containing object, the values are
|
142
|
+
# the options that were passed into expose.
|
143
|
+
def self.exposures
|
144
|
+
@exposures ||= {}
|
145
|
+
|
146
|
+
if superclass.respond_to? :exposures
|
147
|
+
@exposures = superclass.exposures.merge(@exposures)
|
148
|
+
end
|
149
|
+
|
150
|
+
@exposures
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns a hash, the keys are symbolized references to fields in the entity,
|
154
|
+
# the values are document keys in the entity's documentation key. When calling
|
155
|
+
# #docmentation, any exposure without a documentation key will be ignored.
|
156
|
+
def self.documentation
|
157
|
+
@documentation ||= exposures.inject({}) do |memo, value|
|
158
|
+
unless value[1][:documentation].nil? || value[1][:documentation].empty?
|
159
|
+
memo[value[0]] = value[1][:documentation]
|
160
|
+
end
|
161
|
+
memo
|
162
|
+
end
|
163
|
+
|
164
|
+
if superclass.respond_to? :documentation
|
165
|
+
@documentation = superclass.documentation.merge(@documentation)
|
166
|
+
end
|
167
|
+
|
168
|
+
@documentation
|
169
|
+
end
|
170
|
+
|
171
|
+
# This allows you to declare a Proc in which exposures can be formatted with.
|
172
|
+
# It take a block with an arity of 1 which is passed as the value of the exposed attribute.
|
173
|
+
#
|
174
|
+
# @param name [Symbol] the name of the formatter
|
175
|
+
# @param block [Proc] the block that will interpret the exposed attribute
|
176
|
+
#
|
177
|
+
#
|
178
|
+
#
|
179
|
+
# @example Formatter declaration
|
180
|
+
#
|
181
|
+
# module API
|
182
|
+
# module Entities
|
183
|
+
# class User < GrapeEntity::Entity
|
184
|
+
# format_with :timestamp do |date|
|
185
|
+
# date.strftime('%m/%d/%Y')
|
186
|
+
# end
|
187
|
+
#
|
188
|
+
# expose :birthday, :last_signed_in, :format_with => :timestamp
|
189
|
+
# end
|
190
|
+
# end
|
191
|
+
# end
|
192
|
+
#
|
193
|
+
# @example Formatters are available to all decendants
|
194
|
+
#
|
195
|
+
# GrapeEntity::Entity.format_with :timestamp do |date|
|
196
|
+
# date.strftime('%m/%d/%Y')
|
197
|
+
# end
|
198
|
+
#
|
199
|
+
def self.format_with(name, &block)
|
200
|
+
raise ArgumentError, "You must pass a block for formatters" unless block_given?
|
201
|
+
formatters[name.to_sym] = block
|
202
|
+
end
|
203
|
+
|
204
|
+
# Returns a hash of all formatters that are registered for this and it's ancestors.
|
205
|
+
def self.formatters
|
206
|
+
@formatters ||= {}
|
207
|
+
|
208
|
+
if superclass.respond_to? :formatters
|
209
|
+
@formatters = superclass.formatters.merge(@formatters)
|
210
|
+
end
|
211
|
+
|
212
|
+
@formatters
|
213
|
+
end
|
214
|
+
|
215
|
+
# This allows you to set a root element name for your representation.
|
216
|
+
#
|
217
|
+
# @param plural [String] the root key to use when representing
|
218
|
+
# a collection of objects. If missing or nil, no root key will be used
|
219
|
+
# when representing collections of objects.
|
220
|
+
# @param singular [String] the root key to use when representing
|
221
|
+
# a single object. If missing or nil, no root key will be used when
|
222
|
+
# representing an individual object.
|
223
|
+
#
|
224
|
+
# @example Entity Definition
|
225
|
+
#
|
226
|
+
# module API
|
227
|
+
# module Entities
|
228
|
+
# class User < GrapeEntity::Entity
|
229
|
+
# root 'users', 'user'
|
230
|
+
# expose :id
|
231
|
+
# end
|
232
|
+
# end
|
233
|
+
# end
|
234
|
+
#
|
235
|
+
# @example Usage in the API Layer
|
236
|
+
#
|
237
|
+
# module API
|
238
|
+
# class Users < GrapeEntity::API
|
239
|
+
# version 'v2'
|
240
|
+
#
|
241
|
+
# # this will render { "users": [ {"id":"1"}, {"id":"2"} ] }
|
242
|
+
# get '/users' do
|
243
|
+
# @users = User.all
|
244
|
+
# present @users, :with => API::Entities::User
|
245
|
+
# end
|
246
|
+
#
|
247
|
+
# # this will render { "user": {"id":"1"} }
|
248
|
+
# get '/users/:id' do
|
249
|
+
# @user = User.find(params[:id])
|
250
|
+
# present @user, :with => API::Entities::User
|
251
|
+
# end
|
252
|
+
# end
|
253
|
+
# end
|
254
|
+
def self.root(plural, singular=nil)
|
255
|
+
@collection_root = plural
|
256
|
+
@root = singular
|
257
|
+
end
|
258
|
+
|
259
|
+
# This convenience method allows you to instantiate one or more entities by
|
260
|
+
# passing either a singular or collection of objects. Each object will be
|
261
|
+
# initialized with the same options. If an array of objects is passed in,
|
262
|
+
# an array of entities will be returned. If a single object is passed in,
|
263
|
+
# a single entity will be returned.
|
264
|
+
#
|
265
|
+
# @param objects [Object or Array] One or more objects to be represented.
|
266
|
+
# @param options [Hash] Options that will be passed through to each entity
|
267
|
+
# representation.
|
268
|
+
#
|
269
|
+
# @option options :root [String] override the default root name set for the
|
270
|
+
# entity. Pass nil or false to represent the object or objects with no
|
271
|
+
# root name even if one is defined for the entity.
|
272
|
+
def self.represent(objects, options = {})
|
273
|
+
inner = if objects.respond_to?(:to_ary)
|
274
|
+
objects.to_ary().map{|o| self.new(o, {:collection => true}.merge(options))}
|
275
|
+
else
|
276
|
+
self.new(objects, options)
|
277
|
+
end
|
278
|
+
|
279
|
+
root_element = if options.has_key?(:root)
|
280
|
+
options[:root]
|
281
|
+
else
|
282
|
+
objects.respond_to?(:to_ary) ? @collection_root : @root
|
283
|
+
end
|
284
|
+
root_element ? { root_element => inner } : inner
|
285
|
+
end
|
286
|
+
|
287
|
+
def initialize(object, options = {})
|
288
|
+
@object, @options = object, options
|
289
|
+
end
|
290
|
+
|
291
|
+
def exposures
|
292
|
+
self.class.exposures
|
293
|
+
end
|
294
|
+
|
295
|
+
def documentation
|
296
|
+
self.class.documentation
|
297
|
+
end
|
298
|
+
|
299
|
+
def formatters
|
300
|
+
self.class.formatters
|
301
|
+
end
|
302
|
+
|
303
|
+
# The serializable hash is the Entity's primary output. It is the transformed
|
304
|
+
# hash for the given data model and is used as the basis for serialization to
|
305
|
+
# JSON and other formats.
|
306
|
+
#
|
307
|
+
# @param runtime_options [Hash] Any options you pass in here will be known to the entity
|
308
|
+
# representation, this is where you can trigger things from conditional options
|
309
|
+
# etc.
|
310
|
+
def serializable_hash(runtime_options = {})
|
311
|
+
return nil if object.nil?
|
312
|
+
opts = options.merge(runtime_options || {})
|
313
|
+
exposures.inject({}) do |output, (attribute, exposure_options)|
|
314
|
+
if (exposure_options.has_key?(:proc) || object.respond_to?(attribute)) && conditions_met?(exposure_options, opts)
|
315
|
+
partial_output = value_for(attribute, opts)
|
316
|
+
output[key_for(attribute)] =
|
317
|
+
if partial_output.respond_to? :serializable_hash
|
318
|
+
partial_output.serializable_hash(runtime_options)
|
319
|
+
elsif partial_output.kind_of?(Array) && !partial_output.map {|o| o.respond_to? :serializable_hash}.include?(false)
|
320
|
+
partial_output.map {|o| o.serializable_hash}
|
321
|
+
else
|
322
|
+
partial_output
|
323
|
+
end
|
324
|
+
end
|
325
|
+
output
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
alias :as_json :serializable_hash
|
330
|
+
|
331
|
+
def to_json(options = {})
|
332
|
+
options = options.to_h if options && options.respond_to?(:to_h)
|
333
|
+
MultiJson.dump(serializable_hash(options))
|
334
|
+
end
|
335
|
+
|
336
|
+
def to_xml(options = {})
|
337
|
+
options = options.to_h if options && options.respond_to?(:to_h)
|
338
|
+
serializable_hash(options).to_xml(options)
|
339
|
+
end
|
340
|
+
|
341
|
+
protected
|
342
|
+
|
343
|
+
def key_for(attribute)
|
344
|
+
exposures[attribute.to_sym][:as] || attribute.to_sym
|
345
|
+
end
|
346
|
+
|
347
|
+
def value_for(attribute, options = {})
|
348
|
+
exposure_options = exposures[attribute.to_sym]
|
349
|
+
|
350
|
+
if exposure_options[:proc]
|
351
|
+
exposure_options[:proc].call(object, options)
|
352
|
+
elsif exposure_options[:using]
|
353
|
+
using_options = options.dup
|
354
|
+
using_options.delete(:collection)
|
355
|
+
using_options[:root] = nil
|
356
|
+
exposure_options[:using].represent(object.send(attribute), using_options)
|
357
|
+
elsif exposure_options[:format_with]
|
358
|
+
format_with = exposure_options[:format_with]
|
359
|
+
|
360
|
+
if format_with.is_a?(Symbol) && formatters[format_with]
|
361
|
+
formatters[format_with].call(object.send(attribute))
|
362
|
+
elsif format_with.is_a?(Symbol)
|
363
|
+
self.send(format_with, object.send(attribute))
|
364
|
+
elsif format_with.respond_to? :call
|
365
|
+
format_with.call(object.send(attribute))
|
366
|
+
end
|
367
|
+
else
|
368
|
+
object.send(attribute)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def conditions_met?(exposure_options, options)
|
373
|
+
if_condition = exposure_options[:if]
|
374
|
+
unless_condition = exposure_options[:unless]
|
375
|
+
|
376
|
+
case if_condition
|
377
|
+
when Hash; if_condition.each_pair{|k,v| return false if options[k.to_sym] != v }
|
378
|
+
when Proc; return false unless if_condition.call(object, options)
|
379
|
+
end
|
380
|
+
|
381
|
+
case unless_condition
|
382
|
+
when Hash; unless_condition.each_pair{|k,v| return false if options[k.to_sym] == v}
|
383
|
+
when Proc; return false if unless_condition.call(object, options)
|
384
|
+
end
|
385
|
+
|
386
|
+
true
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|