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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -50
- data/README.md +41 -52
- data/Rakefile +13 -1
- data/lib/blueprinter/base.rb +13 -15
- data/lib/blueprinter/blueprinter_error.rb +2 -0
- data/lib/blueprinter/configuration.rb +24 -2
- data/lib/blueprinter/deprecation.rb +5 -3
- data/lib/blueprinter/empty_types.rb +7 -9
- data/lib/blueprinter/extension.rb +22 -0
- data/lib/blueprinter/extensions.rb +37 -0
- data/lib/blueprinter/extractor.rb +6 -4
- data/lib/blueprinter/extractors/association_extractor.rb +8 -3
- data/lib/blueprinter/extractors/auto_extractor.rb +2 -0
- data/lib/blueprinter/extractors/block_extractor.rb +3 -1
- data/lib/blueprinter/extractors/hash_extractor.rb +2 -0
- data/lib/blueprinter/extractors/public_send_extractor.rb +3 -1
- data/lib/blueprinter/field.rb +44 -47
- data/lib/blueprinter/formatters/date_time_formatter.rb +3 -1
- data/lib/blueprinter/helpers/base_helpers.rb +17 -13
- data/lib/blueprinter/helpers/type_helpers.rb +5 -5
- data/lib/blueprinter/reflection.rb +71 -0
- data/lib/blueprinter/transformer.rb +4 -2
- data/lib/blueprinter/version.rb +3 -1
- data/lib/blueprinter/view.rb +7 -9
- data/lib/blueprinter/view_collection.rb +27 -11
- data/lib/blueprinter.rb +3 -0
- data/lib/generators/blueprinter/blueprint_generator.rb +37 -48
- data/lib/generators/blueprinter/templates/blueprint.rb +2 -0
- metadata +8 -173
- data/lib/tasks/blueprinter_tasks.rake +0 -4
    
        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:  | 
| 42 | 
            +
            task default: %i[spec rubocop]
         | 
    
        data/lib/blueprinter/base.rb
    CHANGED
    
    | @@ -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. | 
| 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. | 
| 448 | 
            -
                #  ExampleBlueprint. | 
| 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. | 
| 453 | 
            -
                  view_collection. | 
| 450 | 
            +
                def self.view?(view_name)
         | 
| 451 | 
            +
                  view_collection.view? view_name
         | 
| 454 452 | 
             
                end
         | 
| 455 453 | 
             
              end
         | 
| 456 454 | 
             
            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, | 
| 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 | 
| 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 | 
| 6 | 
            -
                  MESSAGE_PREFIX =  | 
| 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  | 
| 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 =  | 
| 5 | 
            -
              EMPTY_HASH =  | 
| 6 | 
            -
              EMPTY_STRING =  | 
| 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 | 
            -
                   | 
| 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 | 
            -
                   | 
| 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,_|  | 
| 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. | 
| 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,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( | 
| 6 | 
            +
                def extract(_field_name, object, local_options, options = {})
         | 
| 5 7 | 
             
                  options[:block].call(object, local_options)
         | 
| 6 8 | 
             
                end
         | 
| 7 9 | 
             
              end
         | 
| @@ -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,  | 
| 6 | 
            +
                def extract(field_name, object, _local_options, _options = {})
         | 
| 5 7 | 
             
                  object.public_send(field_name)
         | 
| 6 8 | 
             
                end
         | 
| 7 9 | 
             
              end
         | 
    
        data/lib/blueprinter/field.rb
    CHANGED
    
    | @@ -1,63 +1,60 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            # @api private
         | 
| 2 | 
            -
             | 
| 3 | 
            -
               | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
                 | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 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 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 16 | 
            +
                def extract(object, local_options)
         | 
| 17 | 
            +
                  extractor.extract(method, object, local_options, options)
         | 
| 18 | 
            +
                end
         | 
| 15 19 |  | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 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 | 
            -
             | 
| 23 | 
            +
                  unless_callable && unless_callable.call(field_name, object, local_options)
         | 
| 24 | 
            +
                end
         | 
| 22 25 |  | 
| 23 | 
            -
             | 
| 24 | 
            -
                return @if_callable if defined?(@if_callable)
         | 
| 25 | 
            -
                @if_callable = callable_from(:if)
         | 
| 26 | 
            -
              end
         | 
| 26 | 
            +
                private
         | 
| 27 27 |  | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
                @unless_callable = callable_from(:unless)
         | 
| 31 | 
            -
              end
         | 
| 28 | 
            +
                def if_callable
         | 
| 29 | 
            +
                  return @if_callable if defined?(@if_callable)
         | 
| 32 30 |  | 
| 33 | 
            -
             | 
| 34 | 
            -
                 | 
| 31 | 
            +
                  @if_callable = callable_from(:if)
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def unless_callable
         | 
| 35 | 
            +
                  return @unless_callable if defined?(@unless_callable)
         | 
| 35 36 |  | 
| 36 | 
            -
             | 
| 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 | 
            -
             | 
| 45 | 
            -
             | 
| 40 | 
            +
                def callable_from(condition)
         | 
| 41 | 
            +
                  config = Blueprinter.configuration
         | 
| 46 42 |  | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 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 | 
            -
             | 
| 50 | 
            +
                  return false unless tmp
         | 
| 55 51 |  | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 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,  | 
| 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 | 
            -
             | 
| 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,  | 
| 64 | 
            +
                      raise BlueprinterError, 'meta requires a root to be passed' if meta
         | 
| 61 65 | 
             
                    else
         | 
| 62 | 
            -
                      raise BlueprinterError,  | 
| 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 | 
            -
                     | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
                     | 
| 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 | 
            -
                     | 
| 88 | 
            -
             | 
| 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 | 
            -
             | 
| 101 | 
            -
             | 
| 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 | 
            -
                   | 
| 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
         |