blueprinter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +230 -0
- data/Rakefile +30 -0
- data/lib/blueprinter.rb +5 -0
- data/lib/blueprinter/base.rb +294 -0
- data/lib/blueprinter/blueprinter_error.rb +3 -0
- data/lib/blueprinter/configuration.rb +17 -0
- data/lib/blueprinter/field.rb +14 -0
- data/lib/blueprinter/helpers/active_record_helpers.rb +18 -0
- data/lib/blueprinter/optimizer.rb +30 -0
- data/lib/blueprinter/serializer.rb +13 -0
- data/lib/blueprinter/serializers/association_serializer.rb +10 -0
- data/lib/blueprinter/serializers/auto_serializer.rb +8 -0
- data/lib/blueprinter/serializers/block_serializer.rb +5 -0
- data/lib/blueprinter/serializers/hash_serializer.rb +5 -0
- data/lib/blueprinter/serializers/public_send_serializer.rb +5 -0
- data/lib/blueprinter/version.rb +3 -0
- data/lib/blueprinter/view.rb +29 -0
- data/lib/blueprinter/view_collection.rb +46 -0
- data/lib/rabbit_painter.rb +14 -0
- data/lib/tasks/blueprinter_tasks.rake +4 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f69da973430bcbb6be951377ccc7707e2d53228a
|
4
|
+
data.tar.gz: 818f307324662a8871d6abc6353cfbf52367cb33
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a8ee5758ecba43bb06423d4fbe01ebfcabb918c2980775b75e494f75350e90b8348fd20685b6c0c083d47196b0ec75f99757020d6a9e82af7699964231b6f0dc
|
7
|
+
data.tar.gz: 63d4da14d7b9d1144068961c163ef2d6b9a04adbf3e21db33a57e68779d7c580f9c5491571c2154a453bd2c41ee76326dacc5f9f13c990f701a423709aaf74af
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 Procore Technologies, 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.md
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
[![CircleCI](https://circleci.com/gh/procore/blueprinter.svg?style=svg)](https://circleci.com/gh/procore/blueprinter)
|
2
|
+
|
3
|
+
# Blueprinter
|
4
|
+
Blueprinter is a JSON Object Presenter for Ruby that takes business objects and breaks them down into simple hashes and serializes them to JSON. It can be used in Rails in place of other serializers (like JBuilder or ActiveModelSerializers). It is designed to be simple, direct, and performant.
|
5
|
+
|
6
|
+
It heavily relies on the idea of `views` which, similar to Rails views, are ways of predefining output for data in different contexts.
|
7
|
+
|
8
|
+
## Documentation
|
9
|
+
!TODO Link to the docs
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
### Basic
|
13
|
+
If you have an object you would like serialized, simply create a blueprint. Say, for example, you have a User record with the following attributes `[:uuid, :email, :first_name, :last_name, :password, :address]`.
|
14
|
+
|
15
|
+
You may define a simple blueprint like so:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
class UserBlueprint < Blueprinter::Base
|
19
|
+
identifier :uuid
|
20
|
+
|
21
|
+
fields :first_name, :last_name, :email
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
and then, in your code:
|
26
|
+
```ruby
|
27
|
+
puts UserBlueprint.render(user) # Output is a JSON string
|
28
|
+
```
|
29
|
+
|
30
|
+
And the output would look like:
|
31
|
+
|
32
|
+
```json
|
33
|
+
{
|
34
|
+
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
|
35
|
+
"email": "john.doe@some.fake.email.domain",
|
36
|
+
"first_name": "John",
|
37
|
+
"last_name": "Doe"
|
38
|
+
}
|
39
|
+
```
|
40
|
+
|
41
|
+
### Views
|
42
|
+
You may define different ouputs by utilizing views:
|
43
|
+
```ruby
|
44
|
+
class UserBlueprint < Blueprinter::Base
|
45
|
+
identifier :uuid
|
46
|
+
field :email, name: :login
|
47
|
+
|
48
|
+
view :normal do
|
49
|
+
fields :first_name, :last_name
|
50
|
+
end
|
51
|
+
|
52
|
+
view :extended do
|
53
|
+
include_view :normal
|
54
|
+
field :address
|
55
|
+
association :projects
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
Usage:
|
61
|
+
```ruby
|
62
|
+
puts UserBlueprint.render(user, view: :extended)
|
63
|
+
```
|
64
|
+
|
65
|
+
Output:
|
66
|
+
```json
|
67
|
+
{
|
68
|
+
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
|
69
|
+
"address": "123 Fake St.",
|
70
|
+
"first_name": "John",
|
71
|
+
"last_name": "Doe",
|
72
|
+
"login": "john.doe@some.fake.email.domain"
|
73
|
+
}
|
74
|
+
```
|
75
|
+
|
76
|
+
### Associations
|
77
|
+
You may include associated objects. Say for example, a user has projects:
|
78
|
+
```ruby
|
79
|
+
class UserBlueprint < Blueprinter::Base
|
80
|
+
identifier :uuid
|
81
|
+
field :email, name: :login
|
82
|
+
|
83
|
+
view :normal do
|
84
|
+
fields :first_name, :last_name
|
85
|
+
association :projects
|
86
|
+
end
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
Usage:
|
91
|
+
```ruby
|
92
|
+
puts UserBlueprint.render(user, view: :extended)
|
93
|
+
```
|
94
|
+
|
95
|
+
Output:
|
96
|
+
```json
|
97
|
+
{
|
98
|
+
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
|
99
|
+
"first_name": "John",
|
100
|
+
"last_name": "Doe",
|
101
|
+
"login": "john.doe@some.fake.email.domain",
|
102
|
+
"projects": [
|
103
|
+
{
|
104
|
+
"uuid": "dca94051-4195-42bc-a9aa-eb99f7723c82",
|
105
|
+
"name": "Beach Cleanup"
|
106
|
+
},
|
107
|
+
{
|
108
|
+
"uuid": "eb881bb5-9a51-4d27-8a29-b264c30e6160",
|
109
|
+
"name": "Storefront Revamp"
|
110
|
+
}
|
111
|
+
]
|
112
|
+
}
|
113
|
+
```
|
114
|
+
|
115
|
+
### Defining a field directly in the Blueprint
|
116
|
+
|
117
|
+
You can define a field directly in the Blueprint by passing it a block. This is especially useful if the object does not already have such an attribute or method defined, and you want to define it specifically for use with the Blueprint. For example:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class UserBlueprint < Blueprinter::Base
|
121
|
+
identifier :uuid
|
122
|
+
field :full_name do |user|
|
123
|
+
"#{user.first_name} #{user.last_name}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
Usage:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
puts UserBlueprint.render(user)
|
132
|
+
```
|
133
|
+
|
134
|
+
Output:
|
135
|
+
|
136
|
+
```json
|
137
|
+
{
|
138
|
+
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
|
139
|
+
"full_name": "John Doe"
|
140
|
+
}
|
141
|
+
```
|
142
|
+
|
143
|
+
### Passing additional properties to `render`
|
144
|
+
|
145
|
+
`render` takes an options hash which you can pass additional properties, allowing you to utilize those additional properties in the `field` block. For example:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
class UserBlueprint < Blueprinter::Base
|
149
|
+
identifier :uuid
|
150
|
+
field(:company_name) do |_user, options|
|
151
|
+
options[:company].name
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
Usage:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
puts UserBlueprint.render(user, company: company)
|
160
|
+
```
|
161
|
+
|
162
|
+
Output:
|
163
|
+
|
164
|
+
```json
|
165
|
+
{
|
166
|
+
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
|
167
|
+
"company_name": "My Company LLC"
|
168
|
+
}
|
169
|
+
```
|
170
|
+
|
171
|
+
## Installation
|
172
|
+
Add this line to your application's Gemfile:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
gem 'blueprinter'
|
176
|
+
```
|
177
|
+
|
178
|
+
And then execute:
|
179
|
+
```bash
|
180
|
+
$ bundle
|
181
|
+
```
|
182
|
+
|
183
|
+
Or install it yourself as:
|
184
|
+
```bash
|
185
|
+
$ gem install blueprinter
|
186
|
+
```
|
187
|
+
|
188
|
+
You should also have `require 'json'` already in your project if you are not using Rails or if you are not using Oj.
|
189
|
+
|
190
|
+
## OJ
|
191
|
+
|
192
|
+
By default, Blueprinter will be calling `JSON.generate(object)` internally and it expects that you have `require 'json'` already in your project's code. You may use `Oj` to generate in place of `JSON` like so:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
require 'oj' # you can skip this if OJ has already been required.
|
196
|
+
|
197
|
+
Blueprinter.configure do |config|
|
198
|
+
config.generator = Oj # default is JSON
|
199
|
+
end
|
200
|
+
```
|
201
|
+
|
202
|
+
Ensure that you have the `Oj` gem installed in your Gemfile if you haven't already:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
# Gemfile
|
206
|
+
gem 'oj'
|
207
|
+
```
|
208
|
+
|
209
|
+
## Documentation
|
210
|
+
|
211
|
+
We use [Yard](https://yardoc.org/) for documentation. Here are the following
|
212
|
+
documentation rules:
|
213
|
+
|
214
|
+
- Document all public methods we expect to be utilized by the end developers.
|
215
|
+
- Methods that are not set to private due to ruby visibility rule limitations should be marked with `@api private`.
|
216
|
+
|
217
|
+
## Contributing
|
218
|
+
Feel free to browse the issues, converse, and make pull requests. If you need help, first please see if there is already an issue for your problem. Otherwise, go ahead and make a new issue.
|
219
|
+
|
220
|
+
### Tests
|
221
|
+
You can run tests with `bundle exec rake`.
|
222
|
+
|
223
|
+
### Maintain The Docs
|
224
|
+
We use Yard for documentation. Here are the following documentation rules:
|
225
|
+
|
226
|
+
- Document all public methods we expect to be utilized by the end developers.
|
227
|
+
- Methods that are not set to private due to ruby visibility rule limitations should be marked with `@api private`.
|
228
|
+
|
229
|
+
## License
|
230
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rdoc/task'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'bundler/setup'
|
8
|
+
rescue LoadError
|
9
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
10
|
+
end
|
11
|
+
|
12
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
13
|
+
rdoc.rdoc_dir = 'rdoc'
|
14
|
+
rdoc.title = 'Blueprinter'
|
15
|
+
rdoc.options << '--line-numbers'
|
16
|
+
rdoc.rdoc_files.include('README.md')
|
17
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
18
|
+
end
|
19
|
+
|
20
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
21
|
+
t.rspec_opts = '--pattern spec/**/*_spec.rb'
|
22
|
+
end
|
23
|
+
|
24
|
+
Rake::TestTask.new(:benchmarks) do |t|
|
25
|
+
t.libs << 'spec'
|
26
|
+
t.pattern = 'spec/benchmarks/**/*_test.rb'
|
27
|
+
t.verbose = false
|
28
|
+
end
|
29
|
+
|
30
|
+
task default: :spec
|
data/lib/blueprinter.rb
ADDED
@@ -0,0 +1,294 @@
|
|
1
|
+
require_relative 'blueprinter_error'
|
2
|
+
require_relative 'helpers/active_record_helpers'
|
3
|
+
require_relative 'serializer'
|
4
|
+
require_relative 'serializers/association_serializer'
|
5
|
+
require_relative 'serializers/auto_serializer'
|
6
|
+
require_relative 'serializers/block_serializer'
|
7
|
+
require_relative 'serializers/hash_serializer'
|
8
|
+
require_relative 'serializers/public_send_serializer'
|
9
|
+
require_relative 'field'
|
10
|
+
require_relative 'view'
|
11
|
+
require_relative 'view_collection'
|
12
|
+
require_relative 'optimizer'
|
13
|
+
|
14
|
+
module Blueprinter
|
15
|
+
class Base
|
16
|
+
include ActiveRecordHelpers
|
17
|
+
|
18
|
+
# Specify a field or method name used as an identifier. Usually, this is
|
19
|
+
# something like :id
|
20
|
+
#
|
21
|
+
# Note: identifiers are always rendered and considerered their own view,
|
22
|
+
# similar to the :default view.
|
23
|
+
#
|
24
|
+
# @param method [Symbol] the method or field used as an identifier that you
|
25
|
+
# want to set for serialization.
|
26
|
+
# @param name [Symbol] to rename the identifier key in the JSON
|
27
|
+
# output. Defaults to method given.
|
28
|
+
# @param serializer [AssociationSerializer,AutoSerializer,BlockSerializer,HashSerializer,PublicSendSerializer]
|
29
|
+
# Kind of serializer to use.
|
30
|
+
# Either define your own or use Blueprinter's premade serializers.
|
31
|
+
# Defaults to AutoSerializer
|
32
|
+
#
|
33
|
+
# @example Specifying a uuid as an identifier.
|
34
|
+
# class UserBlueprint < Blueprinter::Base
|
35
|
+
# identifier :uuid
|
36
|
+
# # other code
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @return [Field] A Field object
|
40
|
+
def self.identifier(method, name: method, serializer: AutoSerializer)
|
41
|
+
view_collection[:identifier] << Field.new(method, name, serializer)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Specify a field or method name to be included for serialization.
|
45
|
+
# Takes a required method and an option.
|
46
|
+
#
|
47
|
+
# @param method [Symbol] the field or method name you want to include for
|
48
|
+
# serialization.
|
49
|
+
# @param options [Hash] options to overide defaults.
|
50
|
+
# @option options [AssociationSerializer,BlockSerializer,HashSerializer,PublicSendSerializer] :serializer
|
51
|
+
# Kind of serializer to use.
|
52
|
+
# Either define your own or use Blueprinter's premade serializers. The
|
53
|
+
# Default serializer is AutoSerializer
|
54
|
+
# @option options [Symbol] :name Use this to rename the method. Useful if
|
55
|
+
# if you want your JSON key named differently in the output than your
|
56
|
+
# object's field or method name.
|
57
|
+
# @yield [Object] The object passed to `render` is also passed to the
|
58
|
+
# block.
|
59
|
+
#
|
60
|
+
# @example Specifying a user's first_name to be serialized.
|
61
|
+
# class UserBlueprint < Blueprinter::Base
|
62
|
+
# field :first_name
|
63
|
+
# # other code
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# @example Passing a block to be evaluated as the value.
|
67
|
+
# class UserBlueprint < Blueprinter::Base
|
68
|
+
# field :full_name {|obj| "#{obj.first_name} #{obj.last_name}"}
|
69
|
+
# # other code
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# @return [Field] A Field object
|
73
|
+
def self.field(method, options = {}, &block)
|
74
|
+
options = if block_given?
|
75
|
+
{name: method, serializer: BlockSerializer, block: {method => block}}
|
76
|
+
else
|
77
|
+
{name: method, serializer: AutoSerializer}
|
78
|
+
end.merge(options)
|
79
|
+
current_view << Field.new(method,
|
80
|
+
options[:name],
|
81
|
+
options[:serializer],
|
82
|
+
options)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Specify an associated object to be included for serialization.
|
86
|
+
# Takes a required method and an option.
|
87
|
+
#
|
88
|
+
# @param method [Symbol] the association name
|
89
|
+
# @param options [Hash] options to overide defaults.
|
90
|
+
# @option options [Symbol] :name Use this to rename the association in the
|
91
|
+
# JSON output.
|
92
|
+
# @option options [Symbol] :view Specify the view to use or fall back to
|
93
|
+
# to the :default view.
|
94
|
+
#
|
95
|
+
# @example Specifying an association
|
96
|
+
# class UserBlueprint < Blueprinter::Base
|
97
|
+
# # code
|
98
|
+
# association :vehicles, view: :extended
|
99
|
+
# # code
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# @return [Field] A Field object
|
103
|
+
def self.association(method, options = {})
|
104
|
+
name = options.delete(:name) || method
|
105
|
+
current_view << Field.new(method,
|
106
|
+
name,
|
107
|
+
AssociationSerializer,
|
108
|
+
options.merge(association: true))
|
109
|
+
end
|
110
|
+
|
111
|
+
# Generates a JSON formatted String.
|
112
|
+
# Takes a required object and an optional view.
|
113
|
+
#
|
114
|
+
# @param object [Object] the Object to serialize upon.
|
115
|
+
# @param options [Hash] the options hash which requires a :view. Any
|
116
|
+
# additional key value pairs will be exposed during serialization.
|
117
|
+
# @option options [Symbol] :view Defaults to :default.
|
118
|
+
# The view name that corresponds to the group of
|
119
|
+
# fields to be serialized.
|
120
|
+
#
|
121
|
+
# @example Generating JSON with an extended view
|
122
|
+
# post = Post.all
|
123
|
+
# Blueprinter::Base.render post, view: :extended
|
124
|
+
# # => "[{\"id\":1,\"title\":\"Hello\"},{\"id\":2,\"title\":\"My Day\"}]"
|
125
|
+
#
|
126
|
+
# @return [String] JSON formatted String
|
127
|
+
def self.render(object, options = {})
|
128
|
+
view_name = options.delete(:view) || :default
|
129
|
+
jsonify(prepare(object, view_name: view_name, local_options: options))
|
130
|
+
end
|
131
|
+
|
132
|
+
# This is the magic method that converts complex objects into a simple hash
|
133
|
+
# ready for JSON conversion.
|
134
|
+
#
|
135
|
+
# Note: we accept view (public interface) that is in reality a view_name,
|
136
|
+
# so we rename it for clarity
|
137
|
+
#
|
138
|
+
# @api private
|
139
|
+
def self.prepare(object, view_name:, local_options:)
|
140
|
+
unless view_collection.has_view? view_name
|
141
|
+
raise BlueprinterError, "View '#{view_name}' is not defined"
|
142
|
+
end
|
143
|
+
fields = view_collection.fields_for(view_name)
|
144
|
+
prepared_object = Optimizer.optimize(object, fields: fields)
|
145
|
+
prepared_object = include_associations(prepared_object, view_name: view_name)
|
146
|
+
if array_like?(object)
|
147
|
+
prepared_object.map do |obj|
|
148
|
+
object_to_hash(obj,
|
149
|
+
view_name: view_name,
|
150
|
+
local_options: local_options)
|
151
|
+
end
|
152
|
+
else
|
153
|
+
object_to_hash(prepared_object,
|
154
|
+
view_name: view_name,
|
155
|
+
local_options: local_options)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Specify one or more field/method names to be included for serialization.
|
160
|
+
# Takes at least one field or method names.
|
161
|
+
#
|
162
|
+
# @param method [Symbol] the field or method name you want to include for
|
163
|
+
# serialization.
|
164
|
+
#
|
165
|
+
# @example Specifying a user's first_name and last_name to be serialized.
|
166
|
+
# class UserBlueprint < Blueprinter::Base
|
167
|
+
# fields :first_name, :last_name
|
168
|
+
# # other code
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
# @return [Array<Symbol>] an array of field names
|
172
|
+
def self.fields(*field_names)
|
173
|
+
field_names.each do |field_name|
|
174
|
+
current_view << Field.new(field_name, field_name, AutoSerializer)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# @api private
|
179
|
+
def self.associations(view_name = :default)
|
180
|
+
view_collection.fields_for(view_name).select { |f| f.options[:association] }
|
181
|
+
end
|
182
|
+
|
183
|
+
# Specify another view that should be mixed into the current view.
|
184
|
+
#
|
185
|
+
# @param view_name [Symbol] the view to mix into the current view.
|
186
|
+
#
|
187
|
+
# @example Including a normal view into an extended view.
|
188
|
+
# class UserBlueprint < Blueprinter::Base
|
189
|
+
# # other code...
|
190
|
+
# view :normal do
|
191
|
+
# fields :first_name, :last_name
|
192
|
+
# end
|
193
|
+
# view :extended do
|
194
|
+
# include_view :normal # include fields specified from above.
|
195
|
+
# field :description
|
196
|
+
# end
|
197
|
+
# #=> [:first_name, :last_name, :description]
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# @return [Array<Symbol>] an array of view names.
|
201
|
+
def self.include_view(view_name)
|
202
|
+
current_view.include_view(view_name)
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
# Exclude a field that was mixed into the current view.
|
207
|
+
#
|
208
|
+
# @param field_name [Symbol] the field to exclude from the current view.
|
209
|
+
#
|
210
|
+
# @example Excluding a field from being included into the current view.
|
211
|
+
# view :normal do
|
212
|
+
# fields :position, :company
|
213
|
+
# end
|
214
|
+
# view :special do
|
215
|
+
# include_view :normal
|
216
|
+
# field :birthday
|
217
|
+
# exclude :position
|
218
|
+
# end
|
219
|
+
# #=> [:company, :birthday]
|
220
|
+
#
|
221
|
+
# @return [Array<Symbol>] an array of field names
|
222
|
+
def self.exclude(field_name)
|
223
|
+
current_view.exclude_field(field_name)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Specify a view and the fields it should have.
|
227
|
+
# It accepts a view name and a block. The block should specify the fields.
|
228
|
+
#
|
229
|
+
# @param view_name [Symbol] the view name
|
230
|
+
# @yieldreturn [#fields,#field,#include_view,#exclude] Use this block to
|
231
|
+
# specify fields, include fields from other views, or exclude fields.
|
232
|
+
#
|
233
|
+
# @example Using views
|
234
|
+
# view :extended do
|
235
|
+
# fields :position, :company
|
236
|
+
# include_view :normal
|
237
|
+
# exclude :first_name
|
238
|
+
# end
|
239
|
+
#
|
240
|
+
# @return [View] a Blueprinter::View object
|
241
|
+
def self.view(view_name)
|
242
|
+
@current_view = view_collection[view_name]
|
243
|
+
yield
|
244
|
+
@current_view = view_collection[:default]
|
245
|
+
end
|
246
|
+
|
247
|
+
private
|
248
|
+
|
249
|
+
def self.object_to_hash(object, view_name:, local_options:)
|
250
|
+
view_collection.fields_for(view_name).each_with_object({}) do |field, hash|
|
251
|
+
hash[field.name] = field.serialize(object, local_options)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
private_class_method :object_to_hash
|
255
|
+
|
256
|
+
def self.include_associations(object, view_name:)
|
257
|
+
unless defined?(ActiveRecord::Base) &&
|
258
|
+
object.is_a?(ActiveRecord::Base) &&
|
259
|
+
object.respond_to?(:klass)
|
260
|
+
return object
|
261
|
+
end
|
262
|
+
# TODO: Do we need to support more than `eager_load` ?
|
263
|
+
fields_to_include = associations(view).select { |a|
|
264
|
+
a.options[:include] != false
|
265
|
+
}.map(&:method)
|
266
|
+
if !fields_to_include.empty?
|
267
|
+
object.eager_load(*fields_to_include)
|
268
|
+
else
|
269
|
+
object
|
270
|
+
end
|
271
|
+
end
|
272
|
+
private_class_method :include_associations
|
273
|
+
|
274
|
+
def self.jsonify(blob)
|
275
|
+
Blueprinter.configuration.generator.generate(blob)
|
276
|
+
end
|
277
|
+
private_class_method :jsonify
|
278
|
+
|
279
|
+
def self.current_view
|
280
|
+
@current_view ||= view_collection[:default]
|
281
|
+
end
|
282
|
+
private_class_method :current_view
|
283
|
+
|
284
|
+
def self.view_collection
|
285
|
+
@view_collection ||= ViewCollection.new
|
286
|
+
end
|
287
|
+
private_class_method :view_collection
|
288
|
+
|
289
|
+
def self.array_like?(object)
|
290
|
+
object.is_a?(Array) || active_record_relation?(object)
|
291
|
+
end
|
292
|
+
private_class_method :array_like?
|
293
|
+
end
|
294
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Blueprinter
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :generator
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@generator = JSON
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.configuration
|
11
|
+
@configuration ||= Configuration.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configure
|
15
|
+
yield configuration if block_given?
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# @api private
|
2
|
+
class Blueprinter::Field
|
3
|
+
attr_reader :method, :name, :serializer, :options
|
4
|
+
def initialize(method, name, serializer, options = {})
|
5
|
+
@method = method
|
6
|
+
@name = name
|
7
|
+
@serializer = serializer
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def serialize(object, local_options)
|
12
|
+
serializer.serialize(method, object, local_options, options)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Blueprinter
|
2
|
+
module ActiveRecordHelpers
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(SingletonMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
def active_record_relation?(object)
|
8
|
+
self.class.active_record_relation?(object)
|
9
|
+
end
|
10
|
+
|
11
|
+
module SingletonMethods
|
12
|
+
def active_record_relation?(object)
|
13
|
+
!!(defined?(ActiveRecord::Relation) &&
|
14
|
+
object.is_a?(ActiveRecord::Relation))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Blueprinter
|
2
|
+
# @api private
|
3
|
+
class Optimizer
|
4
|
+
include ActiveRecordHelpers
|
5
|
+
class << self
|
6
|
+
def optimize(object, fields:)
|
7
|
+
return object unless active_record_relation?(object)
|
8
|
+
select_columns = (active_record_attributes_for(object) &
|
9
|
+
fields.map(&:method)) +
|
10
|
+
required_lookup_attributes_for(object)
|
11
|
+
object.select(*select_columns)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def active_record_attributes_for(object)
|
17
|
+
object.klass.column_names.map(&:to_sym)
|
18
|
+
end
|
19
|
+
|
20
|
+
def required_lookup_attributes_for(object)
|
21
|
+
# TODO: We may not need all four of these
|
22
|
+
lookup_values = (object.includes_values +
|
23
|
+
object.preload_values +
|
24
|
+
object.joins_values +
|
25
|
+
object.eager_load_values).uniq
|
26
|
+
lookup_values.map {|value| object.reflections[value.to_s].foreign_key}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# @api private
|
2
|
+
class Blueprinter::Serializer
|
3
|
+
def initialize
|
4
|
+
end
|
5
|
+
|
6
|
+
def serialize(field_name, object, local_options, options={})
|
7
|
+
fail NotImplementedError, "A serializer must implement #serialize"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.serialize(field_name, object, local_options, options={})
|
11
|
+
self.new.serialize(field_name, object, local_options, options)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class Blueprinter::AssociationSerializer < Blueprinter::Serializer
|
2
|
+
def serialize(association_name, object, local_options, options={})
|
3
|
+
if options[:blueprint]
|
4
|
+
view = options[:view] || :default
|
5
|
+
options[:blueprint].prepare(object.public_send(association_name), view_name: view, local_options: local_options)
|
6
|
+
else
|
7
|
+
object.public_send(association_name)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Blueprinter
|
2
|
+
class AutoSerializer < Blueprinter::Serializer
|
3
|
+
def serialize(field_name, object, local_options, options = {})
|
4
|
+
serializer = object.is_a?(Hash) ? HashSerializer : PublicSendSerializer
|
5
|
+
serializer.serialize(field_name, object, local_options, options = {})
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Blueprinter
|
2
|
+
# @api private
|
3
|
+
class View
|
4
|
+
attr_reader :excluded_field_names, :fields, :included_view_names, :name
|
5
|
+
|
6
|
+
def initialize(name, fields: {}, included_view_names: [], excluded_view_names: [])
|
7
|
+
@name = name
|
8
|
+
@fields = fields
|
9
|
+
@included_view_names = included_view_names
|
10
|
+
@excluded_field_names = excluded_view_names
|
11
|
+
end
|
12
|
+
|
13
|
+
def include_view(view_name)
|
14
|
+
included_view_names << view_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def exclude_field(field_name)
|
18
|
+
excluded_field_names << field_name
|
19
|
+
end
|
20
|
+
|
21
|
+
def <<(field)
|
22
|
+
if fields.has_key?(field.name)
|
23
|
+
raise BlueprinterError,
|
24
|
+
"Field #{field.name} already defined on #{name}"
|
25
|
+
end
|
26
|
+
fields[field.name] = field
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Blueprinter
|
2
|
+
# @api private
|
3
|
+
class ViewCollection
|
4
|
+
attr_reader :views
|
5
|
+
def initialize
|
6
|
+
@views = {
|
7
|
+
identifier: View.new(:identifier),
|
8
|
+
default: View.new(:default)
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def has_view?(view_name)
|
13
|
+
views.has_key? view_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def fields_for(view_name)
|
17
|
+
identifier_fields + sortable_fields(view_name).values.sort_by(&:name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](view_name)
|
21
|
+
@views[view_name] ||= View.new(view_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def identifier_fields
|
27
|
+
views[:identifier].fields.values
|
28
|
+
end
|
29
|
+
|
30
|
+
def sortable_fields(view_name)
|
31
|
+
fields = views[:default].fields
|
32
|
+
fields = fields.merge(views[view_name].fields)
|
33
|
+
views[view_name].included_view_names.each do |included_view_name|
|
34
|
+
if view_name != included_view_name
|
35
|
+
fields = fields.merge(sortable_fields(included_view_name))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
views[view_name].excluded_field_names.each do |name|
|
40
|
+
fields.delete(name)
|
41
|
+
end
|
42
|
+
|
43
|
+
fields
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: blueprinter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Hess
|
8
|
+
- Derek Carter
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-01-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: oj
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '3.0'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '3.0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: pry
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rails
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 5.1.2
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 5.1.2
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rspec
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.7'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.7'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: sqlite3
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: yard
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 0.9.11
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: 0.9.11
|
98
|
+
description: Blueprinter is a JSON Object Presenter for Ruby that takes business objects
|
99
|
+
and breaks them down into simple hashes and serializes them to JSON. It can be used
|
100
|
+
in Rails in place of other serializers (like JBuilder or ActiveModelSerializers).
|
101
|
+
It is designed to be simple, direct, and performant.
|
102
|
+
email:
|
103
|
+
- adamhess1991@gmail.com
|
104
|
+
executables: []
|
105
|
+
extensions: []
|
106
|
+
extra_rdoc_files: []
|
107
|
+
files:
|
108
|
+
- MIT-LICENSE
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- lib/blueprinter.rb
|
112
|
+
- lib/blueprinter/base.rb
|
113
|
+
- lib/blueprinter/blueprinter_error.rb
|
114
|
+
- lib/blueprinter/configuration.rb
|
115
|
+
- lib/blueprinter/field.rb
|
116
|
+
- lib/blueprinter/helpers/active_record_helpers.rb
|
117
|
+
- lib/blueprinter/optimizer.rb
|
118
|
+
- lib/blueprinter/serializer.rb
|
119
|
+
- lib/blueprinter/serializers/association_serializer.rb
|
120
|
+
- lib/blueprinter/serializers/auto_serializer.rb
|
121
|
+
- lib/blueprinter/serializers/block_serializer.rb
|
122
|
+
- lib/blueprinter/serializers/hash_serializer.rb
|
123
|
+
- lib/blueprinter/serializers/public_send_serializer.rb
|
124
|
+
- lib/blueprinter/version.rb
|
125
|
+
- lib/blueprinter/view.rb
|
126
|
+
- lib/blueprinter/view_collection.rb
|
127
|
+
- lib/rabbit_painter.rb
|
128
|
+
- lib/tasks/blueprinter_tasks.rake
|
129
|
+
homepage: https://github.com/procore/blueprinter
|
130
|
+
licenses:
|
131
|
+
- MIT
|
132
|
+
metadata: {}
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options: []
|
135
|
+
require_paths:
|
136
|
+
- lib
|
137
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: 2.2.2
|
142
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
requirements: []
|
148
|
+
rubyforge_project:
|
149
|
+
rubygems_version: 2.5.1
|
150
|
+
signing_key:
|
151
|
+
specification_version: 4
|
152
|
+
summary: Simple Fast Declarative Serialization Library
|
153
|
+
test_files: []
|