blueprinter-rb 1.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +167 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1050 -0
- data/Rakefile +30 -0
- data/lib/blueprinter/base.rb +458 -0
- data/lib/blueprinter/blueprinter_error.rb +5 -0
- data/lib/blueprinter/configuration.rb +39 -0
- data/lib/blueprinter/deprecation.rb +37 -0
- data/lib/blueprinter/empty_types.rb +32 -0
- data/lib/blueprinter/extractor.rb +13 -0
- data/lib/blueprinter/extractors/association_extractor.rb +33 -0
- data/lib/blueprinter/extractors/auto_extractor.rb +37 -0
- data/lib/blueprinter/extractors/block_extractor.rb +10 -0
- data/lib/blueprinter/extractors/hash_extractor.rb +10 -0
- data/lib/blueprinter/extractors/public_send_extractor.rb +10 -0
- data/lib/blueprinter/field.rb +65 -0
- data/lib/blueprinter/formatters/date_time_formatter.rb +33 -0
- data/lib/blueprinter/helpers/base_helpers.rb +123 -0
- data/lib/blueprinter/helpers/type_helpers.rb +15 -0
- data/lib/blueprinter/transformer.rb +14 -0
- data/lib/blueprinter/version.rb +5 -0
- data/lib/blueprinter/view.rb +78 -0
- data/lib/blueprinter/view_collection.rb +90 -0
- data/lib/blueprinter.rb +6 -0
- data/lib/generators/blueprinter/blueprint_generator.rb +129 -0
- data/lib/generators/blueprinter/templates/blueprint.rb +16 -0
- metadata +214 -0
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 --warnings'
|
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
|
@@ -0,0 +1,458 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'blueprinter_error'
|
4
|
+
require_relative 'configuration'
|
5
|
+
require_relative 'deprecation'
|
6
|
+
require_relative 'empty_types'
|
7
|
+
require_relative 'extractor'
|
8
|
+
require_relative 'extractors/association_extractor'
|
9
|
+
require_relative 'extractors/auto_extractor'
|
10
|
+
require_relative 'extractors/block_extractor'
|
11
|
+
require_relative 'extractors/hash_extractor'
|
12
|
+
require_relative 'extractors/public_send_extractor'
|
13
|
+
require_relative 'formatters/date_time_formatter'
|
14
|
+
require_relative 'field'
|
15
|
+
require_relative 'helpers/type_helpers'
|
16
|
+
require_relative 'helpers/base_helpers'
|
17
|
+
require_relative 'view'
|
18
|
+
require_relative 'view_collection'
|
19
|
+
require_relative 'transformer'
|
20
|
+
|
21
|
+
module Blueprinter
|
22
|
+
class Base
|
23
|
+
include BaseHelpers
|
24
|
+
|
25
|
+
# Specify a field or method name used as an identifier. Usually, this is
|
26
|
+
# something like :id
|
27
|
+
#
|
28
|
+
# Note: identifiers are always rendered and considered their own view,
|
29
|
+
# similar to the :default view.
|
30
|
+
#
|
31
|
+
# @param method [Symbol] the method or field used as an identifier that you
|
32
|
+
# want to set for serialization.
|
33
|
+
# @param name [Symbol] to rename the identifier key in the JSON
|
34
|
+
# output. Defaults to method given.
|
35
|
+
# @param extractor [AssociationExtractor,AutoExtractor,BlockExtractor,HashExtractor,PublicSendExtractor]
|
36
|
+
# @yield [object, options] The object and the options passed to render are
|
37
|
+
# also yielded to the block.
|
38
|
+
#
|
39
|
+
# Kind of extractor to use.
|
40
|
+
# Either define your own or use Blueprinter's premade extractors.
|
41
|
+
# Defaults to AutoExtractor
|
42
|
+
#
|
43
|
+
# @example Specifying a uuid as an identifier.
|
44
|
+
# class UserBlueprint < Blueprinter::Base
|
45
|
+
# identifier :uuid
|
46
|
+
# # other code
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# @example Passing a block to be evaluated as the value.
|
50
|
+
# class UserBlueprint < Blueprinter::Base
|
51
|
+
# identifier :uuid do |user, options|
|
52
|
+
# options[:current_user].anonymize(user.uuid)
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# @return [Field] A Field object
|
57
|
+
def self.identifier(method, name: method, extractor: Blueprinter.configuration.extractor_default.new, &block)
|
58
|
+
view_collection[:identifier] << Field.new(
|
59
|
+
method,
|
60
|
+
name,
|
61
|
+
extractor,
|
62
|
+
self,
|
63
|
+
block: block,
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Specify a field or method name to be included for serialization.
|
68
|
+
# Takes a required method and an option.
|
69
|
+
#
|
70
|
+
# @param method [Symbol] the field or method name you want to include for
|
71
|
+
# serialization.
|
72
|
+
# @param options [Hash] options to overide defaults.
|
73
|
+
# @option options [AssociationExtractor,BlockExtractor,HashExtractor,PublicSendExtractor] :extractor
|
74
|
+
# Kind of extractor to use.
|
75
|
+
# Either define your own or use Blueprinter's premade extractors. The
|
76
|
+
# Default extractor is AutoExtractor
|
77
|
+
# @option options [Symbol] :name Use this to rename the method. Useful if
|
78
|
+
# if you want your JSON key named differently in the output than your
|
79
|
+
# object's field or method name.
|
80
|
+
# @option options [String,Proc] :datetime_format Format Date or DateTime object
|
81
|
+
# If the option provided is a String, the object will be formatted with given strftime
|
82
|
+
# formatting.
|
83
|
+
# If this option is a Proc, the object will be formatted by calling the provided Proc
|
84
|
+
# on the Date/DateTime object.
|
85
|
+
# @option options [Symbol,Proc] :if Specifies a method, proc or string to
|
86
|
+
# call to determine if the field should be included (e.g.
|
87
|
+
# `if: :include_first_name?, or if: Proc.new { |_field_name, user, options| options[:current_user] == user }).
|
88
|
+
# The method, proc or string should return or evaluate to a true or false value.
|
89
|
+
# @option options [Symbol,Proc] :unless Specifies a method, proc or string
|
90
|
+
# to call to determine if the field should be included (e.g.
|
91
|
+
# `unless: :include_first_name?, or unless: Proc.new { |_field_name, user, options| options[:current_user] != user }).
|
92
|
+
# The method, proc or string should return or evaluate to a true or false value.
|
93
|
+
# @yield [object, options] The object and the options passed to render are
|
94
|
+
# also yielded to the block.
|
95
|
+
#
|
96
|
+
# @example Specifying a user's first_name to be serialized.
|
97
|
+
# class UserBlueprint < Blueprinter::Base
|
98
|
+
# field :first_name
|
99
|
+
# # other code
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# @example Passing a block to be evaluated as the value.
|
103
|
+
# class UserBlueprint < Blueprinter::Base
|
104
|
+
# field :full_name do |object, options|
|
105
|
+
# "options[:title_prefix] #{object.first_name} #{object.last_name}"
|
106
|
+
# end
|
107
|
+
# # other code
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# @example Passing an if proc and unless method.
|
111
|
+
# class UserBlueprint < Blueprinter::Base
|
112
|
+
# def skip_first_name?(_field_name, user, options)
|
113
|
+
# user.first_name == options[:first_name]
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# field :first_name, unless: :skip_first_name?
|
117
|
+
# field :last_name, if: ->(_field_name, user, options) { user.first_name != options[:first_name] }
|
118
|
+
# # other code
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# @return [Field] A Field object
|
122
|
+
def self.field(method, options = {}, &block)
|
123
|
+
current_view << Field.new(
|
124
|
+
method,
|
125
|
+
options.fetch(:name) { method },
|
126
|
+
options.fetch(:extractor) { Blueprinter.configuration.extractor_default.new },
|
127
|
+
self,
|
128
|
+
options.merge(block: block),
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Specify an associated object to be included for serialization.
|
133
|
+
# Takes a required method and an option.
|
134
|
+
#
|
135
|
+
# @param method [Symbol] the association name
|
136
|
+
# @param options [Hash] options to overide defaults.
|
137
|
+
# @option options [Symbol] :blueprint Required. Use this to specify the
|
138
|
+
# blueprint to use for the associated object.
|
139
|
+
# @option options [Symbol] :name Use this to rename the association in the
|
140
|
+
# JSON output.
|
141
|
+
# @option options [Symbol] :view Specify the view to use or fall back to
|
142
|
+
# to the :default view.
|
143
|
+
# @yield [object, options] The object and the options passed to render are
|
144
|
+
# also yielded to the block.
|
145
|
+
#
|
146
|
+
# @example Specifying an association
|
147
|
+
# class UserBlueprint < Blueprinter::Base
|
148
|
+
# # code
|
149
|
+
# association :vehicles, view: :extended, blueprint: VehiclesBlueprint
|
150
|
+
# # code
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# @example Passing a block to be evaluated as the value.
|
154
|
+
# class UserBlueprint < Blueprinter::Base
|
155
|
+
# association :vehicles, blueprint: VehiclesBlueprint do |user, opts|
|
156
|
+
# user.vehicles + opts[:additional_vehicles]
|
157
|
+
# end
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# @return [Field] A Field object
|
161
|
+
def self.association(method, options = {}, &block)
|
162
|
+
validate_blueprint!(options[:blueprint], method)
|
163
|
+
|
164
|
+
field(
|
165
|
+
method,
|
166
|
+
options.merge(
|
167
|
+
association: true,
|
168
|
+
extractor: options.fetch(:extractor) { AssociationExtractor.new },
|
169
|
+
),
|
170
|
+
&block
|
171
|
+
)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Generates a JSON formatted String.
|
175
|
+
# Takes a required object and an optional view.
|
176
|
+
#
|
177
|
+
# @param object [Object] the Object to serialize upon.
|
178
|
+
# @param options [Hash] the options hash which requires a :view. Any
|
179
|
+
# additional key value pairs will be exposed during serialization.
|
180
|
+
# @option options [Symbol] :view Defaults to :default.
|
181
|
+
# The view name that corresponds to the group of
|
182
|
+
# fields to be serialized.
|
183
|
+
# @option options [Symbol|String] :root Defaults to nil.
|
184
|
+
# Render the json/hash with a root key if provided.
|
185
|
+
# @option options [Any] :meta Defaults to nil.
|
186
|
+
# Render the json/hash with a meta attribute with provided value
|
187
|
+
# if both root and meta keys are provided in the options hash.
|
188
|
+
#
|
189
|
+
# @example Generating JSON with an extended view
|
190
|
+
# post = Post.all
|
191
|
+
# Blueprinter::Base.render post, view: :extended
|
192
|
+
# # => "[{\"id\":1,\"title\":\"Hello\"},{\"id\":2,\"title\":\"My Day\"}]"
|
193
|
+
#
|
194
|
+
# @return [String] JSON formatted String
|
195
|
+
def self.render(object, options = {})
|
196
|
+
jsonify(prepare_for_render(object, options))
|
197
|
+
end
|
198
|
+
|
199
|
+
# Generates a hash.
|
200
|
+
# Takes a required object and an optional view.
|
201
|
+
#
|
202
|
+
# @param object [Object] the Object to serialize upon.
|
203
|
+
# @param options [Hash] the options hash which requires a :view. Any
|
204
|
+
# additional key value pairs will be exposed during serialization.
|
205
|
+
# @option options [Symbol] :view Defaults to :default.
|
206
|
+
# The view name that corresponds to the group of
|
207
|
+
# fields to be serialized.
|
208
|
+
# @option options [Symbol|String] :root Defaults to nil.
|
209
|
+
# Render the json/hash with a root key if provided.
|
210
|
+
# @option options [Any] :meta Defaults to nil.
|
211
|
+
# Render the json/hash with a meta attribute with provided value
|
212
|
+
# if both root and meta keys are provided in the options hash.
|
213
|
+
#
|
214
|
+
# @example Generating a hash with an extended view
|
215
|
+
# post = Post.all
|
216
|
+
# Blueprinter::Base.render_as_hash post, view: :extended
|
217
|
+
# # => [{id:1, title: Hello},{id:2, title: My Day}]
|
218
|
+
#
|
219
|
+
# @return [Hash]
|
220
|
+
def self.render_as_hash(object, options = {})
|
221
|
+
prepare_for_render(object, options)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Generates a JSONified hash.
|
225
|
+
# Takes a required object and an optional view.
|
226
|
+
#
|
227
|
+
# @param object [Object] the Object to serialize upon.
|
228
|
+
# @param options [Hash] the options hash which requires a :view. Any
|
229
|
+
# additional key value pairs will be exposed during serialization.
|
230
|
+
# @option options [Symbol] :view Defaults to :default.
|
231
|
+
# The view name that corresponds to the group of
|
232
|
+
# fields to be serialized.
|
233
|
+
# @option options [Symbol|String] :root Defaults to nil.
|
234
|
+
# Render the json/hash with a root key if provided.
|
235
|
+
# @option options [Any] :meta Defaults to nil.
|
236
|
+
# Render the json/hash with a meta attribute with provided value
|
237
|
+
# if both root and meta keys are provided in the options hash.
|
238
|
+
#
|
239
|
+
# @example Generating a hash with an extended view
|
240
|
+
# post = Post.all
|
241
|
+
# Blueprinter::Base.render_as_json post, view: :extended
|
242
|
+
# # => [{"id" => "1", "title" => "Hello"},{"id" => "2", "title" => "My Day"}]
|
243
|
+
#
|
244
|
+
# @return [Hash]
|
245
|
+
def self.render_as_json(object, options = {})
|
246
|
+
prepare_for_render(object, options).as_json
|
247
|
+
end
|
248
|
+
|
249
|
+
# This is the magic method that converts complex objects into a simple hash
|
250
|
+
# ready for JSON conversion.
|
251
|
+
#
|
252
|
+
# Note: we accept view (public interface) that is in reality a view_name,
|
253
|
+
# so we rename it for clarity
|
254
|
+
#
|
255
|
+
# @api private
|
256
|
+
def self.prepare(object, view_name:, local_options:, root: nil, meta: nil)
|
257
|
+
unless view_collection.has_view? view_name
|
258
|
+
raise BlueprinterError, "View '#{view_name}' is not defined"
|
259
|
+
end
|
260
|
+
|
261
|
+
data = prepare_data(object, view_name, local_options)
|
262
|
+
prepend_root_and_meta(data, root, meta)
|
263
|
+
end
|
264
|
+
|
265
|
+
# Specify one or more field/method names to be included for serialization.
|
266
|
+
# Takes at least one field or method names.
|
267
|
+
#
|
268
|
+
# @param method [Symbol] the field or method name you want to include for
|
269
|
+
# serialization.
|
270
|
+
#
|
271
|
+
# @example Specifying a user's first_name and last_name to be serialized.
|
272
|
+
# class UserBlueprint < Blueprinter::Base
|
273
|
+
# fields :first_name, :last_name
|
274
|
+
# # other code
|
275
|
+
# end
|
276
|
+
#
|
277
|
+
# @return [Array<Symbol>] an array of field names
|
278
|
+
def self.fields(*field_names)
|
279
|
+
field_names.each do |field_name|
|
280
|
+
field(field_name)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
# Specify one transformer to be included for serialization.
|
286
|
+
# Takes a class which extends Blueprinter::Transformer
|
287
|
+
#
|
288
|
+
# @param class name [Class] which implements the method transform to include for
|
289
|
+
# serialization.
|
290
|
+
#
|
291
|
+
#
|
292
|
+
# @example Specifying a DynamicFieldTransformer transformer for including dynamic fields to be serialized.
|
293
|
+
# class User
|
294
|
+
# def custom_columns
|
295
|
+
# self.dynamic_fields # which is an array of some columns
|
296
|
+
# end
|
297
|
+
#
|
298
|
+
# def custom_fields
|
299
|
+
# custom_columns.each_with_object({}) { |col,result| result[col] = self.send(col) }
|
300
|
+
# end
|
301
|
+
# end
|
302
|
+
#
|
303
|
+
# class UserBlueprint < Blueprinter::Base
|
304
|
+
# fields :first_name, :last_name
|
305
|
+
# transform DynamicFieldTransformer
|
306
|
+
# # other code
|
307
|
+
# end
|
308
|
+
#
|
309
|
+
# class DynamicFieldTransformer < Blueprinter::Transformer
|
310
|
+
# def transform(hash, object, options)
|
311
|
+
# hash.merge!(object.dynamic_fields)
|
312
|
+
# end
|
313
|
+
# end
|
314
|
+
#
|
315
|
+
# @return [Array<Class>] an array of transformers
|
316
|
+
def self.transform(transformer)
|
317
|
+
current_view.add_transformer(transformer)
|
318
|
+
end
|
319
|
+
|
320
|
+
|
321
|
+
# Specify another view that should be mixed into the current view.
|
322
|
+
#
|
323
|
+
# @param view_name [Symbol] the view to mix into the current view.
|
324
|
+
#
|
325
|
+
# @example Including a normal view into an extended view.
|
326
|
+
# class UserBlueprint < Blueprinter::Base
|
327
|
+
# # other code...
|
328
|
+
# view :normal do
|
329
|
+
# fields :first_name, :last_name
|
330
|
+
# end
|
331
|
+
# view :extended do
|
332
|
+
# include_view :normal # include fields specified from above.
|
333
|
+
# field :description
|
334
|
+
# end
|
335
|
+
# #=> [:first_name, :last_name, :description]
|
336
|
+
# end
|
337
|
+
#
|
338
|
+
# @return [Array<Symbol>] an array of view names.
|
339
|
+
def self.include_view(view_name)
|
340
|
+
current_view.include_view(view_name)
|
341
|
+
end
|
342
|
+
|
343
|
+
|
344
|
+
# Specify additional views that should be mixed into the current view.
|
345
|
+
#
|
346
|
+
# @param view_name [Array<Symbol>] the views to mix into the current view.
|
347
|
+
#
|
348
|
+
# @example Including the normal and special views into an extended view.
|
349
|
+
# class UserBlueprint < Blueprinter::Base
|
350
|
+
# # other code...
|
351
|
+
# view :normal do
|
352
|
+
# fields :first_name, :last_name
|
353
|
+
# end
|
354
|
+
# view :special do
|
355
|
+
# fields :birthday, :company
|
356
|
+
# end
|
357
|
+
# view :extended do
|
358
|
+
# include_views :normal, :special # include fields specified from above.
|
359
|
+
# field :description
|
360
|
+
# end
|
361
|
+
# #=> [:first_name, :last_name, :birthday, :company, :description]
|
362
|
+
# end
|
363
|
+
#
|
364
|
+
# @return [Array<Symbol>] an array of view names.
|
365
|
+
|
366
|
+
|
367
|
+
def self.include_views(*view_names)
|
368
|
+
current_view.include_views(view_names)
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
# Exclude a field that was mixed into the current view.
|
373
|
+
#
|
374
|
+
# @param field_name [Symbol] the field to exclude from the current view.
|
375
|
+
#
|
376
|
+
# @example Excluding a field from being included into the current view.
|
377
|
+
# view :normal do
|
378
|
+
# fields :position, :company
|
379
|
+
# end
|
380
|
+
# view :special do
|
381
|
+
# include_view :normal
|
382
|
+
# field :birthday
|
383
|
+
# exclude :position
|
384
|
+
# end
|
385
|
+
# #=> [:company, :birthday]
|
386
|
+
#
|
387
|
+
# @return [Array<Symbol>] an array of field names
|
388
|
+
def self.exclude(field_name)
|
389
|
+
current_view.exclude_field(field_name)
|
390
|
+
end
|
391
|
+
|
392
|
+
# When mixing multiple views under a single view, some fields may required to be excluded from
|
393
|
+
# current view
|
394
|
+
#
|
395
|
+
# @param [Array<Symbol>] the fields to exclude from the current view.
|
396
|
+
#
|
397
|
+
# @example Excluding mutiple fields from being included into the current view.
|
398
|
+
# view :normal do
|
399
|
+
# fields :name,:address,:position,
|
400
|
+
# :company, :contact
|
401
|
+
# end
|
402
|
+
# view :special do
|
403
|
+
# include_view :normal
|
404
|
+
# fields :birthday,:joining_anniversary
|
405
|
+
# excludes :position,:address
|
406
|
+
# end
|
407
|
+
# => [:name, :company, :contact, :birthday, :joining_anniversary]
|
408
|
+
#
|
409
|
+
# @return [Array<Symbol>] an array of field names
|
410
|
+
|
411
|
+
def self.excludes(*field_names)
|
412
|
+
current_view.exclude_fields(field_names)
|
413
|
+
end
|
414
|
+
|
415
|
+
# Specify a view and the fields it should have.
|
416
|
+
# It accepts a view name and a block. The block should specify the fields.
|
417
|
+
#
|
418
|
+
# @param view_name [Symbol] the view name
|
419
|
+
# @yieldreturn [#fields,#field,#include_view,#exclude] Use this block to
|
420
|
+
# specify fields, include fields from other views, or exclude fields.
|
421
|
+
#
|
422
|
+
# @example Using views
|
423
|
+
# view :extended do
|
424
|
+
# fields :position, :company
|
425
|
+
# include_view :normal
|
426
|
+
# exclude :first_name
|
427
|
+
# end
|
428
|
+
#
|
429
|
+
# @return [View] a Blueprinter::View object
|
430
|
+
def self.view(view_name)
|
431
|
+
@current_view = view_collection[view_name]
|
432
|
+
view_collection[:default].track_definition_order(view_name)
|
433
|
+
yield
|
434
|
+
@current_view = view_collection[:default]
|
435
|
+
end
|
436
|
+
|
437
|
+
# Check whether or not a Blueprint supports the supplied view.
|
438
|
+
# It accepts a view name.
|
439
|
+
#
|
440
|
+
# @param view_name [Symbol] the view name
|
441
|
+
#
|
442
|
+
# @example With the following Blueprint
|
443
|
+
#
|
444
|
+
# class ExampleBlueprint < Blueprinter::Base
|
445
|
+
# view :custom do
|
446
|
+
# end
|
447
|
+
# end
|
448
|
+
#
|
449
|
+
# ExampleBlueprint.has_view?(:custom) => true
|
450
|
+
# ExampleBlueprint.has_view?(:doesnt_exist) => false
|
451
|
+
#
|
452
|
+
# @return [Boolean] a boolean value indicating if the view is
|
453
|
+
# supported by this Blueprint.
|
454
|
+
def self.has_view?(view_name)
|
455
|
+
view_collection.has_view? view_name
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Blueprinter
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :association_default, :datetime_format, :deprecations, :field_default, :generator, :if, :method, :sort_fields_by, :unless, :extractor_default, :default_transformers
|
6
|
+
|
7
|
+
VALID_CALLABLES = %i(if unless).freeze
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@deprecations = :stderror
|
11
|
+
@association_default = nil
|
12
|
+
@datetime_format = nil
|
13
|
+
@field_default = nil
|
14
|
+
@generator = JSON
|
15
|
+
@if = nil
|
16
|
+
@method = :generate
|
17
|
+
@sort_fields_by = :name_asc
|
18
|
+
@unless = nil
|
19
|
+
@extractor_default = AutoExtractor
|
20
|
+
@default_transformers = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def jsonify(blob)
|
24
|
+
generator.public_send(method, blob)
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid_callable?(callable_name)
|
28
|
+
VALID_CALLABLES.include?(callable_name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.configuration
|
33
|
+
@configuration ||= Configuration.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.configure
|
37
|
+
yield configuration if block_given?
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
module Blueprinter
|
5
|
+
class Deprecation
|
6
|
+
class << self
|
7
|
+
VALID_BEHAVIORS = %i(silence stderror raise).freeze
|
8
|
+
MESSAGE_PREFIX = "[DEPRECATION::WARNING] Blueprinter:"
|
9
|
+
|
10
|
+
def report(message)
|
11
|
+
full_msg = qualified_message(message)
|
12
|
+
|
13
|
+
case behavior
|
14
|
+
when :silence
|
15
|
+
# Silence deprecation (noop)
|
16
|
+
when :stderror
|
17
|
+
warn full_msg
|
18
|
+
when :raise
|
19
|
+
raise BlueprinterError, full_msg
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def qualified_message(message)
|
26
|
+
"#{MESSAGE_PREFIX} #{message}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def behavior
|
30
|
+
configured = Blueprinter.configuration.deprecations
|
31
|
+
return configured unless !VALID_BEHAVIORS.include?(configured)
|
32
|
+
|
33
|
+
:stderror
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helpers/type_helpers'
|
4
|
+
|
5
|
+
module Blueprinter
|
6
|
+
EMPTY_COLLECTION = "empty_collection"
|
7
|
+
EMPTY_HASH = "empty_hash"
|
8
|
+
EMPTY_STRING = "empty_string"
|
9
|
+
|
10
|
+
module EmptyTypes
|
11
|
+
include TypeHelpers
|
12
|
+
private
|
13
|
+
|
14
|
+
def use_default_value?(value, empty_type)
|
15
|
+
return value.nil? unless empty_type
|
16
|
+
|
17
|
+
case empty_type
|
18
|
+
when Blueprinter::EMPTY_COLLECTION
|
19
|
+
array_like?(value) && value.empty?
|
20
|
+
when Blueprinter::EMPTY_HASH
|
21
|
+
value.is_a?(Hash) && value.empty?
|
22
|
+
when Blueprinter::EMPTY_STRING
|
23
|
+
value.to_s == ""
|
24
|
+
else
|
25
|
+
Blueprinter::Deprecation.report(
|
26
|
+
"Invalid empty type '#{empty_type}' received. Blueprinter will raise an error in the next major version."\
|
27
|
+
"Must be one of [nil, Blueprinter::EMPTY_COLLECTION, Blueprinter::EMPTY_HASH, Blueprinter::EMPTY_STRING]"
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Blueprinter
|
4
|
+
class Extractor
|
5
|
+
def extract(_field_name, _object, _local_options, _options={})
|
6
|
+
fail NotImplementedError, "An Extractor must implement #extract"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.extract(field_name, object, local_options, options={})
|
10
|
+
self.new.extract(field_name, object, local_options, options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Blueprinter
|
4
|
+
# @api private
|
5
|
+
class AssociationExtractor < Extractor
|
6
|
+
include EmptyTypes
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@extractor = Blueprinter.configuration.extractor_default.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def extract(association_name, object, local_options, options={})
|
13
|
+
options_without_default = options.reject { |k,_| k == :default || k == :default_if }
|
14
|
+
# Merge in assocation options hash
|
15
|
+
local_options = local_options.merge(options[:options]) if options[:options].is_a?(Hash)
|
16
|
+
value = @extractor.extract(association_name, object, local_options, options_without_default)
|
17
|
+
return default_value(options) if use_default_value?(value, options[:default_if])
|
18
|
+
view = options[:view] || :default
|
19
|
+
blueprint = association_blueprint(options[:blueprint], value)
|
20
|
+
blueprint.prepare(value, view_name: view, local_options: local_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def default_value(association_options)
|
26
|
+
association_options.key?(:default) ? association_options.fetch(:default) : Blueprinter.configuration.association_default
|
27
|
+
end
|
28
|
+
|
29
|
+
def association_blueprint(blueprint, value)
|
30
|
+
blueprint.is_a?(Proc) ? blueprint.call(value) : blueprint
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Blueprinter
|
4
|
+
# @api private
|
5
|
+
class AutoExtractor < Extractor
|
6
|
+
include EmptyTypes
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@hash_extractor = HashExtractor.new
|
10
|
+
@public_send_extractor = PublicSendExtractor.new
|
11
|
+
@block_extractor = BlockExtractor.new
|
12
|
+
@datetime_formatter = DateTimeFormatter.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def extract(field_name, object, local_options, options = {})
|
16
|
+
extraction = extractor(object, options).extract(field_name, object, local_options, options)
|
17
|
+
value = @datetime_formatter.format(extraction, options)
|
18
|
+
use_default_value?(value, options[:default_if]) ? default_value(options) : value
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def default_value(field_options)
|
24
|
+
field_options.key?(:default) ? field_options.fetch(:default) : Blueprinter.configuration.field_default
|
25
|
+
end
|
26
|
+
|
27
|
+
def extractor(object, options)
|
28
|
+
if options[:block]
|
29
|
+
@block_extractor
|
30
|
+
elsif object.is_a?(Hash)
|
31
|
+
@hash_extractor
|
32
|
+
else
|
33
|
+
@public_send_extractor
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|