blueprinter 0.26.0 → 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 CHANGED
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rdoc/task'
2
4
  require 'bundler/gem_tasks'
3
5
  require 'rake/testtask'
4
6
  require 'rspec/core/rake_task'
7
+ require 'yard'
8
+ require 'rubocop/rake_task'
5
9
 
6
10
  begin
7
11
  require 'bundler/setup'
@@ -21,10 +25,18 @@ RSpec::Core::RakeTask.new(:spec) do |t|
21
25
  t.rspec_opts = '--pattern spec/**/*_spec.rb --warnings'
22
26
  end
23
27
 
28
+ RuboCop::RakeTask.new
29
+
30
+ YARD::Rake::YardocTask.new do |t|
31
+ t.files = Dir['lib/**/*'].reject do |file|
32
+ file.include?('lib/generators')
33
+ end
34
+ end
35
+
24
36
  Rake::TestTask.new(:benchmarks) do |t|
25
37
  t.libs << 'spec'
26
38
  t.pattern = 'spec/benchmarks/**/*_test.rb'
27
39
  t.verbose = false
28
40
  end
29
41
 
30
- task default: :spec
42
+ task default: %i[spec rubocop]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'blueprinter_error'
2
4
  require_relative 'configuration'
3
5
  require_relative 'deprecation'
@@ -15,10 +17,12 @@ require_relative 'helpers/base_helpers'
15
17
  require_relative 'view'
16
18
  require_relative 'view_collection'
17
19
  require_relative 'transformer'
20
+ require_relative 'reflection'
18
21
 
19
22
  module Blueprinter
20
23
  class Base
21
24
  include BaseHelpers
25
+ extend Reflection
22
26
 
23
27
  # Specify a field or method name used as an identifier. Usually, this is
24
28
  # something like :id
@@ -58,7 +62,7 @@ module Blueprinter
58
62
  name,
59
63
  extractor,
60
64
  self,
61
- block: block,
65
+ block: block
62
66
  )
63
67
  end
64
68
 
@@ -123,7 +127,7 @@ module Blueprinter
123
127
  options.fetch(:name) { method },
124
128
  options.fetch(:extractor) { Blueprinter.configuration.extractor_default.new },
125
129
  self,
126
- options.merge(block: block),
130
+ options.merge(block: block)
127
131
  )
128
132
  end
129
133
 
@@ -163,7 +167,7 @@ module Blueprinter
163
167
  method,
164
168
  options.merge(
165
169
  association: true,
166
- extractor: options.fetch(:extractor) { AssociationExtractor.new },
170
+ extractor: options.fetch(:extractor) { AssociationExtractor.new }
167
171
  ),
168
172
  &block
169
173
  )
@@ -252,10 +256,9 @@ module Blueprinter
252
256
  #
253
257
  # @api private
254
258
  def self.prepare(object, view_name:, local_options:, root: nil, meta: nil)
255
- unless view_collection.has_view? view_name
256
- raise BlueprinterError, "View '#{view_name}' is not defined"
257
- end
259
+ raise BlueprinterError, "View '#{view_name}' is not defined" unless view_collection.view? view_name
258
260
 
261
+ object = Blueprinter.configuration.extensions.pre_render(object, self, view_name, local_options)
259
262
  data = prepare_data(object, view_name, local_options)
260
263
  prepend_root_and_meta(data, root, meta)
261
264
  end
@@ -279,7 +282,6 @@ module Blueprinter
279
282
  end
280
283
  end
281
284
 
282
-
283
285
  # Specify one transformer to be included for serialization.
284
286
  # Takes a class which extends Blueprinter::Transformer
285
287
  #
@@ -315,7 +317,6 @@ module Blueprinter
315
317
  current_view.add_transformer(transformer)
316
318
  end
317
319
 
318
-
319
320
  # Specify another view that should be mixed into the current view.
320
321
  #
321
322
  # @param view_name [Symbol] the view to mix into the current view.
@@ -338,7 +339,6 @@ module Blueprinter
338
339
  current_view.include_view(view_name)
339
340
  end
340
341
 
341
-
342
342
  # Specify additional views that should be mixed into the current view.
343
343
  #
344
344
  # @param view_name [Array<Symbol>] the views to mix into the current view.
@@ -361,12 +361,10 @@ module Blueprinter
361
361
  #
362
362
  # @return [Array<Symbol>] an array of view names.
363
363
 
364
-
365
364
  def self.include_views(*view_names)
366
365
  current_view.include_views(view_names)
367
366
  end
368
367
 
369
-
370
368
  # Exclude a field that was mixed into the current view.
371
369
  #
372
370
  # @param field_name [Symbol] the field to exclude from the current view.
@@ -444,13 +442,13 @@ module Blueprinter
444
442
  # end
445
443
  # end
446
444
  #
447
- # ExampleBlueprint.has_view?(:custom) => true
448
- # ExampleBlueprint.has_view?(:doesnt_exist) => false
445
+ # ExampleBlueprint.view?(:custom) => true
446
+ # ExampleBlueprint.view?(:doesnt_exist) => false
449
447
  #
450
448
  # @return [Boolean] a boolean value indicating if the view is
451
449
  # supported by this Blueprint.
452
- def self.has_view?(view_name)
453
- view_collection.has_view? view_name
450
+ def self.view?(view_name)
451
+ view_collection.view? view_name
454
452
  end
455
453
  end
456
454
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Blueprinter
2
4
  class BlueprinterError < StandardError; end
3
5
  end
@@ -1,8 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'extensions'
4
+
1
5
  module Blueprinter
2
6
  class Configuration
3
- attr_accessor :association_default, :datetime_format, :deprecations, :field_default, :generator, :if, :method, :sort_fields_by, :unless, :extractor_default, :default_transformers
7
+ attr_accessor :association_default, :datetime_format, :deprecations, :field_default, :generator, :if, :method,
8
+ :sort_fields_by, :unless, :extractor_default, :default_transformers, :custom_array_like_classes
4
9
 
5
- VALID_CALLABLES = %i(if unless).freeze
10
+ VALID_CALLABLES = %i[if unless].freeze
6
11
 
7
12
  def initialize
8
13
  @deprecations = :stderror
@@ -16,6 +21,23 @@ module Blueprinter
16
21
  @unless = nil
17
22
  @extractor_default = AutoExtractor
18
23
  @default_transformers = []
24
+ @custom_array_like_classes = []
25
+ end
26
+
27
+ def extensions
28
+ @extensions ||= Extensions.new
29
+ end
30
+
31
+ def extensions=(list)
32
+ @extensions = Extensions.new(list)
33
+ end
34
+
35
+ def array_like_classes
36
+ @array_like_classes ||= [
37
+ Array,
38
+ defined?(ActiveRecord::Relation) && ActiveRecord::Relation,
39
+ *custom_array_like_classes
40
+ ].compact
19
41
  end
20
42
 
21
43
  def jsonify(blob)
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # @api private
2
4
  module Blueprinter
3
5
  class Deprecation
4
6
  class << self
5
- VALID_BEHAVIORS = %i(silence stderror raise).freeze
6
- MESSAGE_PREFIX = "[DEPRECATION::WARNING] Blueprinter:".freeze
7
+ VALID_BEHAVIORS = %i[silence stderror raise].freeze
8
+ MESSAGE_PREFIX = '[DEPRECATION::WARNING] Blueprinter:'
7
9
 
8
10
  def report(message)
9
11
  full_msg = qualified_message(message)
@@ -26,7 +28,7 @@ module Blueprinter
26
28
 
27
29
  def behavior
28
30
  configured = Blueprinter.configuration.deprecations
29
- return configured unless !VALID_BEHAVIORS.include?(configured)
31
+ return configured if VALID_BEHAVIORS.include?(configured)
30
32
 
31
33
  :stderror
32
34
  end
@@ -1,12 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'helpers/type_helpers'
2
4
 
3
5
  module Blueprinter
4
- EMPTY_COLLECTION = "empty_collection".freeze
5
- EMPTY_HASH = "empty_hash".freeze
6
- EMPTY_STRING = "empty_string".freeze
6
+ EMPTY_COLLECTION = 'empty_collection'
7
+ EMPTY_HASH = 'empty_hash'
8
+ EMPTY_STRING = 'empty_string'
7
9
 
8
10
  module EmptyTypes
9
11
  include TypeHelpers
12
+
10
13
  private
11
14
 
12
15
  def use_default_value?(value, empty_type)
@@ -18,12 +21,7 @@ module Blueprinter
18
21
  when Blueprinter::EMPTY_HASH
19
22
  value.is_a?(Hash) && value.empty?
20
23
  when Blueprinter::EMPTY_STRING
21
- value.to_s == ""
22
- else
23
- Blueprinter::Deprecation.report(
24
- "Invalid empty type '#{empty_type}' received. Blueprinter will raise an error in the next major version."\
25
- "Must be one of [nil, Blueprinter::EMPTY_COLLECTION, Blueprinter::EMPTY_HASH, Blueprinter::EMPTY_STRING]"
26
- )
24
+ value.to_s == ''
27
25
  end
28
26
  end
29
27
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blueprinter
4
+ #
5
+ # Base class for all extensions. All extension methods are implemented as no-ops.
6
+ #
7
+ class Extension
8
+ #
9
+ # Called eary during "render", this method receives the object to be rendered and
10
+ # may return a modified (or new) object to be rendered.
11
+ #
12
+ # @param object [Object] The object to be rendered
13
+ # @param _blueprint [Class] The Blueprinter class
14
+ # @param _view [Symbol] The blueprint view
15
+ # @param _options [Hash] Options passed to "render"
16
+ # @return [Object] The object to continue rendering
17
+ #
18
+ def pre_render(object, _blueprint, _view, _options)
19
+ object
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Blueprinter
4
+ #
5
+ # Stores and runs Blueprinter extensions. An extension is any object that implements one or more of the
6
+ # extension methods:
7
+ #
8
+ # The Render Extension intercepts an object before rendering begins. The return value from this
9
+ # method is what is ultimately rendered.
10
+ #
11
+ # def pre_render(object, blueprint, view, options)
12
+ # # returns original, modified, or new object
13
+ # end
14
+ #
15
+ class Extensions
16
+ def initialize(extensions = [])
17
+ @extensions = extensions
18
+ end
19
+
20
+ def to_a
21
+ @extensions.dup
22
+ end
23
+
24
+ # Appends an extension
25
+ def <<(ext)
26
+ @extensions << ext
27
+ self
28
+ end
29
+
30
+ # Runs the object through all Render Extensions and returns the final result
31
+ def pre_render(object, blueprint, view, options = {})
32
+ @extensions.reduce(object) do |acc, ext|
33
+ ext.pre_render(acc, blueprint, view, options)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Blueprinter
2
4
  class Extractor
3
- def extract(_field_name, _object, _local_options, _options={})
4
- fail NotImplementedError, "An Extractor must implement #extract"
5
+ def extract(_field_name, _object, _local_options, _options = {})
6
+ raise NotImplementedError, 'An Extractor must implement #extract'
5
7
  end
6
8
 
7
- def self.extract(field_name, object, local_options, options={})
8
- self.new.extract(field_name, object, local_options, options)
9
+ def self.extract(field_name, object, local_options, options = {})
10
+ new.extract(field_name, object, local_options, options)
9
11
  end
10
12
  end
11
13
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Blueprinter
2
4
  # @api private
3
5
  class AssociationExtractor < Extractor
@@ -7,12 +9,13 @@ module Blueprinter
7
9
  @extractor = Blueprinter.configuration.extractor_default.new
8
10
  end
9
11
 
10
- def extract(association_name, object, local_options, options={})
11
- options_without_default = options.reject { |k,_| k == :default || k == :default_if }
12
+ def extract(association_name, object, local_options, options = {})
13
+ options_without_default = options.reject { |k, _| %i[default default_if].include?(k) }
12
14
  # Merge in assocation options hash
13
15
  local_options = local_options.merge(options[:options]) if options[:options].is_a?(Hash)
14
16
  value = @extractor.extract(association_name, object, local_options, options_without_default)
15
17
  return default_value(options) if use_default_value?(value, options[:default_if])
18
+
16
19
  view = options[:view] || :default
17
20
  blueprint = association_blueprint(options[:blueprint], value)
18
21
  blueprint.prepare(value, view_name: view, local_options: local_options)
@@ -21,7 +24,9 @@ module Blueprinter
21
24
  private
22
25
 
23
26
  def default_value(association_options)
24
- association_options.key?(:default) ? association_options.fetch(:default) : Blueprinter.configuration.association_default
27
+ return association_options.fetch(:default) if association_options.key?(:default)
28
+
29
+ Blueprinter.configuration.association_default
25
30
  end
26
31
 
27
32
  def association_blueprint(blueprint, value)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Blueprinter
2
4
  # @api private
3
5
  class AutoExtractor < Extractor
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Blueprinter
2
4
  # @api private
3
5
  class BlockExtractor < Extractor
4
- def extract(field_name, object, local_options, options = {})
6
+ def extract(_field_name, object, local_options, options = {})
5
7
  options[:block].call(object, local_options)
6
8
  end
7
9
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Blueprinter
2
4
  # @api private
3
5
  class HashExtractor < Extractor
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Blueprinter
2
4
  # @api private
3
5
  class PublicSendExtractor < Extractor
4
- def extract(field_name, object, local_options, options = {})
6
+ def extract(field_name, object, _local_options, _options = {})
5
7
  object.public_send(field_name)
6
8
  end
7
9
  end
@@ -1,63 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # @api private
2
- class Blueprinter::Field
3
- attr_reader :method, :name, :extractor, :options, :blueprint
4
- def initialize(method, name, extractor, blueprint, options = {})
5
- @method = method
6
- @name = name
7
- @extractor = extractor
8
- @blueprint = blueprint
9
- @options = options
10
- end
4
+ module Blueprinter
5
+ class Field
6
+ attr_reader :method, :name, :extractor, :options, :blueprint
7
+
8
+ def initialize(method, name, extractor, blueprint, options = {})
9
+ @method = method
10
+ @name = name
11
+ @extractor = extractor
12
+ @blueprint = blueprint
13
+ @options = options
14
+ end
11
15
 
12
- def extract(object, local_options)
13
- extractor.extract(method, object, local_options, options)
14
- end
16
+ def extract(object, local_options)
17
+ extractor.extract(method, object, local_options, options)
18
+ end
15
19
 
16
- def skip?(field_name, object, local_options)
17
- return true if if_callable && !if_callable.call(field_name, object, local_options)
18
- unless_callable && unless_callable.call(field_name, object, local_options)
19
- end
20
+ def skip?(field_name, object, local_options)
21
+ return true if if_callable && !if_callable.call(field_name, object, local_options)
20
22
 
21
- private
23
+ unless_callable && unless_callable.call(field_name, object, local_options)
24
+ end
22
25
 
23
- def if_callable
24
- return @if_callable if defined?(@if_callable)
25
- @if_callable = callable_from(:if)
26
- end
26
+ private
27
27
 
28
- def unless_callable
29
- return @unless_callable if defined?(@unless_callable)
30
- @unless_callable = callable_from(:unless)
31
- end
28
+ def if_callable
29
+ return @if_callable if defined?(@if_callable)
32
30
 
33
- def callable_from(condition)
34
- callable = old_callable_from(condition)
31
+ @if_callable = callable_from(:if)
32
+ end
33
+
34
+ def unless_callable
35
+ return @unless_callable if defined?(@unless_callable)
35
36
 
36
- if callable && callable.arity == 2
37
- Blueprinter::Deprecation.report("`:#{condition}` conditions now expects 3 arguments instead of 2.")
38
- ->(_field_name, obj, options) { callable.call(obj, options) }
39
- else
40
- callable
37
+ @unless_callable = callable_from(:unless)
41
38
  end
42
- end
43
39
 
44
- def old_callable_from(condition)
45
- config = Blueprinter.configuration
40
+ def callable_from(condition)
41
+ config = Blueprinter.configuration
46
42
 
47
- # Use field-level callable, or when not defined, try global callable
48
- tmp = if options.key?(condition)
49
- options.fetch(condition)
50
- elsif config.valid_callable?(condition)
51
- config.public_send(condition)
52
- end
43
+ # Use field-level callable, or when not defined, try global callable
44
+ tmp = if options.key?(condition)
45
+ options.fetch(condition)
46
+ elsif config.valid_callable?(condition)
47
+ config.public_send(condition)
48
+ end
53
49
 
54
- return false unless tmp
50
+ return false unless tmp
55
51
 
56
- case tmp
57
- when Proc then tmp
58
- when Symbol then blueprint.method(tmp)
59
- else
60
- raise ArgumentError, "#{tmp.class} is passed to :#{condition}"
52
+ case tmp
53
+ when Proc then tmp
54
+ when Symbol then blueprint.method(tmp)
55
+ else
56
+ raise ArgumentError, "#{tmp.class} is passed to :#{condition}"
57
+ end
61
58
  end
62
59
  end
63
60
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Blueprinter
2
4
  class DateTimeFormatter
3
5
  InvalidDateTimeFormatterError = Class.new(BlueprinterError)
@@ -24,7 +26,7 @@ module Blueprinter
24
26
  when Proc then format.call(value)
25
27
  when String then value.strftime(format)
26
28
  else
27
- raise InvalidDateTimeFormatterError, 'Cannot format DateTime object with invalid formatter: #{format.class}'
29
+ raise InvalidDateTimeFormatterError, "Cannot format DateTime object with invalid formatter: #{format.class}"
28
30
  end
29
31
  end
30
32
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Blueprinter
2
4
  module BaseHelpers
3
5
  def self.included(base)
@@ -33,7 +35,8 @@ module Blueprinter
33
35
 
34
36
  def prepend_root_and_meta(data, root, meta)
35
37
  return data unless root
36
- ret = {root => data}
38
+
39
+ ret = { root => data }
37
40
  meta ? ret.merge!(meta: meta) : ret
38
41
  end
39
42
 
@@ -44,6 +47,7 @@ module Blueprinter
44
47
  def object_to_hash(object, view_name:, local_options:)
45
48
  result_hash = view_collection.fields_for(view_name).each_with_object({}) do |field, hash|
46
49
  next if field.skip?(field.name, object, local_options)
50
+
47
51
  hash[field.name] = field.extract(object, local_options)
48
52
  end
49
53
  view_collection.transformers(view_name).each do |transformer|
@@ -57,9 +61,9 @@ module Blueprinter
57
61
  when String, Symbol
58
62
  # no-op
59
63
  when NilClass
60
- raise BlueprinterError, "meta requires a root to be passed" if meta
64
+ raise BlueprinterError, 'meta requires a root to be passed' if meta
61
65
  else
62
- raise BlueprinterError, "root should be one of String, Symbol, NilClass"
66
+ raise BlueprinterError, 'root should be one of String, Symbol, NilClass'
63
67
  end
64
68
  end
65
69
 
@@ -69,10 +73,10 @@ module Blueprinter
69
73
 
70
74
  def validate_blueprint!(blueprint, method)
71
75
  validate_presence_of_blueprint!(blueprint)
72
- unless dynamic_blueprint?(blueprint)
73
- validate_blueprint_has_ancestors!(blueprint, method)
74
- validate_blueprint_has_blueprinter_base_ancestor!(blueprint, method)
75
- end
76
+ return if dynamic_blueprint?(blueprint)
77
+
78
+ validate_blueprint_has_ancestors!(blueprint, method)
79
+ validate_blueprint_has_blueprinter_base_ancestor!(blueprint, method)
76
80
  end
77
81
 
78
82
  def validate_presence_of_blueprint!(blueprint)
@@ -84,10 +88,10 @@ module Blueprinter
84
88
  # it means it, at the very least, does not have Blueprinter::Base as
85
89
  # one of its ancestor classes (e.g: Hash) and thus an error should
86
90
  # be raised.
87
- unless blueprint.respond_to?(:ancestors)
88
- raise BlueprinterError, "Blueprint provided for #{association_name} "\
91
+ return if blueprint.respond_to?(:ancestors)
92
+
93
+ raise BlueprinterError, "Blueprint provided for #{association_name} " \
89
94
  'association is not valid.'
90
- end
91
95
  end
92
96
 
93
97
  def validate_blueprint_has_blueprinter_base_ancestor!(blueprint, association_name)
@@ -96,9 +100,9 @@ module Blueprinter
96
100
  return if blueprint.ancestors.include? Blueprinter::Base
97
101
 
98
102
  # Raise error describing what's wrong.
99
- raise BlueprinterError, "Class #{blueprint.name} does not inherit from "\
100
- 'Blueprinter::Base and is not a valid Blueprinter '\
101
- "for #{association_name} association."
103
+ raise BlueprinterError, "Class #{blueprint.name} does not inherit from " \
104
+ 'Blueprinter::Base and is not a valid Blueprinter ' \
105
+ "for #{association_name} association."
102
106
  end
103
107
 
104
108
  def jsonify(blob)
@@ -1,13 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Blueprinter
2
4
  module TypeHelpers
3
5
  private
4
- def active_record_relation?(object)
5
- !!(defined?(ActiveRecord::Relation) &&
6
- object.is_a?(ActiveRecord::Relation))
7
- end
8
6
 
9
7
  def array_like?(object)
10
- object.is_a?(Array) || active_record_relation?(object)
8
+ Blueprinter.configuration.array_like_classes.any? do |klass|
9
+ object.is_a?(klass)
10
+ end
11
11
  end
12
12
  end
13
13
  end