grape-entity 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
[](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
|