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.
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blueprinter
4
+ class BlueprinterError < StandardError; end
5
+ 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