grape-entity 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format=progress
@@ -0,0 +1,8 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - jruby
6
+ - rbx
7
+ - ree
8
+
@@ -0,0 +1,2 @@
1
+ --markup-provider=redcarpet
2
+ --markup=markdown
@@ -0,0 +1,8 @@
1
+ Next Release
2
+ ============
3
+ * Your contribution here.
4
+
5
+ 0.1.0 (01/11/2013)
6
+ ==================
7
+
8
+ * Initial Release
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
@@ -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.
@@ -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.
@@ -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
@@ -0,0 +1 @@
1
+ require 'grape_entity'
@@ -0,0 +1,5 @@
1
+ require "grape_entity/version"
2
+
3
+ module GrapeEntity
4
+ autoload :Entity, 'grape_entity/entity'
5
+ end
@@ -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