blueprinter-rb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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