blueprinter 0.26.0 → 1.0.0

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