grape-entity 0.3.0 → 0.4.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/.gitignore +1 -0
- data/.rubocop.yml +69 -0
- data/.travis.yml +5 -6
- data/CHANGELOG.md +35 -0
- data/Gemfile +7 -0
- data/Guardfile +0 -1
- data/README.md +320 -0
- data/Rakefile +3 -33
- data/lib/grape-entity.rb +1 -1
- data/lib/grape_entity.rb +2 -5
- data/lib/grape_entity/entity.rb +190 -71
- data/lib/grape_entity/version.rb +1 -1
- data/spec/grape_entity/entity_spec.rb +539 -158
- metadata +6 -5
- data/CHANGELOG.markdown +0 -21
- data/README.markdown +0 -197
    
        data/Rakefile
    CHANGED
    
    | @@ -15,37 +15,7 @@ RSpec::Core::RakeTask.new(:rcov) do |spec| | |
| 15 15 | 
             
            end
         | 
| 16 16 |  | 
| 17 17 | 
             
            task :spec
         | 
| 18 | 
            -
             | 
| 18 | 
            +
            require 'rubocop/rake_task'
         | 
| 19 | 
            +
            Rubocop::RakeTask.new(:rubocop)
         | 
| 19 20 |  | 
| 20 | 
            -
             | 
| 21 | 
            -
            # TODO: setup a place for documentation and then get this going again.
         | 
| 22 | 
            -
            #
         | 
| 23 | 
            -
            # begin
         | 
| 24 | 
            -
            #   require 'yard'
         | 
| 25 | 
            -
            #   DOC_FILES = ['lib/**/*.rb', 'README.markdown']
         | 
| 26 | 
            -
            #   
         | 
| 27 | 
            -
            #   YARD::Rake::YardocTask.new(:doc) do |t|
         | 
| 28 | 
            -
            #     t.files   = DOC_FILES
         | 
| 29 | 
            -
            #   end
         | 
| 30 | 
            -
            #   
         | 
| 31 | 
            -
            #   namespace :doc do
         | 
| 32 | 
            -
            #     YARD::Rake::YardocTask.new(:pages) do |t|
         | 
| 33 | 
            -
            #       t.files   = DOC_FILES
         | 
| 34 | 
            -
            #       t.options = ['-o', '../grape.doc']
         | 
| 35 | 
            -
            #     end
         | 
| 36 | 
            -
            #     
         | 
| 37 | 
            -
            #     namespace :pages do
         | 
| 38 | 
            -
            #       desc 'Generate and publish YARD docs to GitHub pages.'
         | 
| 39 | 
            -
            #       task :publish => ['doc:pages'] do
         | 
| 40 | 
            -
            #         Dir.chdir(File.dirname(__FILE__) + '/../grape.doc') do
         | 
| 41 | 
            -
            #           system("git add .")
         | 
| 42 | 
            -
            #           system("git add -u")
         | 
| 43 | 
            -
            #           system("git commit -m 'Generating docs for version #{version}.'")
         | 
| 44 | 
            -
            #           system("git push origin gh-pages")
         | 
| 45 | 
            -
            #         end
         | 
| 46 | 
            -
            #       end
         | 
| 47 | 
            -
            #     end
         | 
| 48 | 
            -
            #   end
         | 
| 49 | 
            -
            # rescue LoadError
         | 
| 50 | 
            -
            #   puts "You need to install YARD."
         | 
| 51 | 
            -
            # end
         | 
| 21 | 
            +
            task default: [:rubocop, :spec]
         | 
    
        data/lib/grape-entity.rb
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            require 'grape_entity'
         | 
| 1 | 
            +
            require 'grape_entity'
         | 
    
        data/lib/grape_entity.rb
    CHANGED
    
    
    
        data/lib/grape_entity/entity.rb
    CHANGED
    
    | @@ -12,11 +12,11 @@ module Grape | |
| 12 12 | 
             
              #     module Entities
         | 
| 13 13 | 
             
              #       class User < Grape::Entity
         | 
| 14 14 | 
             
              #         expose :first_name, :last_name, :screen_name, :location
         | 
| 15 | 
            -
              #         expose :field, : | 
| 16 | 
            -
              #         expose :latest_status, : | 
| 17 | 
            -
              #         expose :email, : | 
| 18 | 
            -
              #         expose :new_attribute, : | 
| 19 | 
            -
              #         expose(:name){|model,options| [model.first_name, model.last_name].join(' ')}
         | 
| 15 | 
            +
              #         expose :field, documentation: { type: "string", desc: "describe the field" }
         | 
| 16 | 
            +
              #         expose :latest_status, using: API::Status, as: :status, unless: { collection: true }
         | 
| 17 | 
            +
              #         expose :email, if: { type: :full }
         | 
| 18 | 
            +
              #         expose :new_attribute, if: { version: 'v2' }
         | 
| 19 | 
            +
              #         expose(:name) { |model, options| [model.first_name, model.last_name].join(' ') }
         | 
| 20 20 | 
             
              #       end
         | 
| 21 21 | 
             
              #     end
         | 
| 22 22 | 
             
              #   end
         | 
| @@ -32,11 +32,11 @@ module Grape | |
| 32 32 | 
             
              #     class Users < Grape::API
         | 
| 33 33 | 
             
              #       version 'v2'
         | 
| 34 34 | 
             
              #
         | 
| 35 | 
            -
              #       desc 'User index', { : | 
| 35 | 
            +
              #       desc 'User index', { params: API::Entities::User.documentation }
         | 
| 36 36 | 
             
              #       get '/users' do
         | 
| 37 37 | 
             
              #         @users = User.all
         | 
| 38 38 | 
             
              #         type = current_user.admin? ? :full : :default
         | 
| 39 | 
            -
              #         present @users, : | 
| 39 | 
            +
              #         present @users, with: API::Entities::User, type: type
         | 
| 40 40 | 
             
              #       end
         | 
| 41 41 | 
             
              #     end
         | 
| 42 42 | 
             
              #   end
         | 
| @@ -48,16 +48,16 @@ module Grape | |
| 48 48 | 
             
                module DSL
         | 
| 49 49 | 
             
                  def self.included(base)
         | 
| 50 50 | 
             
                    base.extend ClassMethods
         | 
| 51 | 
            -
                    ancestor_entity_class = base.ancestors.detect{|a| a.entity_class if a.respond_to?(:entity_class)}
         | 
| 51 | 
            +
                    ancestor_entity_class = base.ancestors.detect { |a| a.entity_class if a.respond_to?(:entity_class) }
         | 
| 52 52 | 
             
                    base.const_set(:Entity, Class.new(ancestor_entity_class || Grape::Entity)) unless const_defined?(:Entity)
         | 
| 53 53 | 
             
                  end
         | 
| 54 54 |  | 
| 55 55 | 
             
                  module ClassMethods
         | 
| 56 56 | 
             
                    # Returns the automatically-created entity class for this
         | 
| 57 57 | 
             
                    # Class.
         | 
| 58 | 
            -
                    def entity_class(search_ancestors=true)
         | 
| 58 | 
            +
                    def entity_class(search_ancestors = true)
         | 
| 59 59 | 
             
                      klass = const_get(:Entity) if const_defined?(:Entity)
         | 
| 60 | 
            -
                      klass ||= ancestors.detect{|a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors
         | 
| 60 | 
            +
                      klass ||= ancestors.detect { |a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors
         | 
| 61 61 | 
             
                      klass
         | 
| 62 62 | 
             
                    end
         | 
| 63 63 |  | 
| @@ -70,7 +70,7 @@ module Grape | |
| 70 70 | 
             
                    #
         | 
| 71 71 | 
             
                    #   class User
         | 
| 72 72 | 
             
                    #     include Grape::Entity::DSL
         | 
| 73 | 
            -
                    # | 
| 73 | 
            +
                    #
         | 
| 74 74 | 
             
                    #     entity :name, :email
         | 
| 75 75 | 
             
                    #   end
         | 
| 76 76 | 
             
                    #
         | 
| @@ -78,14 +78,14 @@ module Grape | |
| 78 78 | 
             
                    #
         | 
| 79 79 | 
             
                    #   class User
         | 
| 80 80 | 
             
                    #     include Grape::Entity::DSL
         | 
| 81 | 
            -
                    # | 
| 81 | 
            +
                    #
         | 
| 82 82 | 
             
                    #     entity :name, :email do
         | 
| 83 83 | 
             
                    #       expose :latest_status, using: Status::Entity, if: :include_status
         | 
| 84 | 
            -
                    #       expose :new_attribute, : | 
| 84 | 
            +
                    #       expose :new_attribute, if: { version: 'v2' }
         | 
| 85 85 | 
             
                    #     end
         | 
| 86 86 | 
             
                    #   end
         | 
| 87 87 | 
             
                    def entity(*exposures, &block)
         | 
| 88 | 
            -
                      entity_class.expose | 
| 88 | 
            +
                      entity_class.expose(*exposures) if exposures.any?
         | 
| 89 89 | 
             
                      entity_class.class_eval(&block) if block_given?
         | 
| 90 90 | 
             
                      entity_class
         | 
| 91 91 | 
             
                    end
         | 
| @@ -123,8 +123,7 @@ module Grape | |
| 123 123 | 
             
                # @option options :documentation Define documenation for an exposed
         | 
| 124 124 | 
             
                #   field, typically the value is a hash with two fields, type and desc.
         | 
| 125 125 | 
             
                def self.expose(*args, &block)
         | 
| 126 | 
            -
                  options = args.last.is_a?(Hash) ? args.pop : {}
         | 
| 127 | 
            -
                  options = (@block_options ||= []).inject({}){|final, step| final.merge!(step)}.merge(options)
         | 
| 126 | 
            +
                  options = merge_options(args.last.is_a?(Hash) ? args.pop : {})
         | 
| 128 127 |  | 
| 129 128 | 
             
                  if args.size > 1
         | 
| 130 129 | 
             
                    raise ArgumentError, "You may not use the :as option on multi-attribute exposures." if options[:as]
         | 
| @@ -133,24 +132,36 @@ module Grape | |
| 133 132 |  | 
| 134 133 | 
             
                  raise ArgumentError, "You may not use block-setting when also using format_with" if block_given? && options[:format_with].respond_to?(:call)
         | 
| 135 134 |  | 
| 136 | 
            -
                  options[:proc] = block if block_given?
         | 
| 135 | 
            +
                  options[:proc] = block if block_given? && block.parameters.any?
         | 
| 137 136 |  | 
| 137 | 
            +
                  @nested_attributes ||= []
         | 
| 138 138 | 
             
                  args.each do |attribute|
         | 
| 139 | 
            +
                    unless @nested_attributes.empty?
         | 
| 140 | 
            +
                      attribute = "#{@nested_attributes.last}__#{attribute}"
         | 
| 141 | 
            +
                    end
         | 
| 142 | 
            +
             | 
| 139 143 | 
             
                    exposures[attribute.to_sym] = options
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                    # Nested exposures are given in a block with no parameters.
         | 
| 146 | 
            +
                    if block_given? && block.parameters.empty?
         | 
| 147 | 
            +
                      @nested_attributes << attribute
         | 
| 148 | 
            +
                      block.call
         | 
| 149 | 
            +
                      @nested_attributes.pop
         | 
| 150 | 
            +
                    end
         | 
| 140 151 | 
             
                  end
         | 
| 141 152 | 
             
                end
         | 
| 142 153 |  | 
| 143 154 | 
             
                # Set options that will be applied to any exposures declared inside the block.
         | 
| 144 155 | 
             
                #
         | 
| 145 156 | 
             
                # @example Multi-exposure if
         | 
| 146 | 
            -
                # | 
| 157 | 
            +
                #
         | 
| 147 158 | 
             
                #   class MyEntity < Grape::Entity
         | 
| 148 | 
            -
                #     with_options : | 
| 159 | 
            +
                #     with_options if: { awesome: true } do
         | 
| 149 160 | 
             
                #       expose :awesome, :sweet
         | 
| 150 161 | 
             
                #     end
         | 
| 151 162 | 
             
                #   end
         | 
| 152 163 | 
             
                def self.with_options(options)
         | 
| 153 | 
            -
                  (@block_options ||= []).push(options)
         | 
| 164 | 
            +
                  (@block_options ||= []).push(valid_options(options))
         | 
| 154 165 | 
             
                  yield
         | 
| 155 166 | 
             
                  @block_options.pop
         | 
| 156 167 | 
             
                end
         | 
| @@ -172,12 +183,12 @@ module Grape | |
| 172 183 | 
             
                # the values are document keys in the entity's documentation key. When calling
         | 
| 173 184 | 
             
                # #docmentation, any exposure without a documentation key will be ignored.
         | 
| 174 185 | 
             
                def self.documentation
         | 
| 175 | 
            -
                  @documentation ||= exposures.inject({}) do |memo,  | 
| 176 | 
            -
             | 
| 177 | 
            -
             | 
| 178 | 
            -
             | 
| 179 | 
            -
             | 
| 180 | 
            -
             | 
| 186 | 
            +
                  @documentation ||= exposures.inject({}) do |memo, (attribute, exposure_options)|
         | 
| 187 | 
            +
                    unless exposure_options[:documentation].nil? || exposure_options[:documentation].empty?
         | 
| 188 | 
            +
                      memo[key_for(attribute)] = exposure_options[:documentation]
         | 
| 189 | 
            +
                    end
         | 
| 190 | 
            +
                    memo
         | 
| 191 | 
            +
                  end
         | 
| 181 192 |  | 
| 182 193 | 
             
                  if superclass.respond_to? :documentation
         | 
| 183 194 | 
             
                    @documentation = superclass.documentation.merge(@documentation)
         | 
| @@ -192,8 +203,6 @@ module Grape | |
| 192 203 | 
             
                # @param name [Symbol] the name of the formatter
         | 
| 193 204 | 
             
                # @param block [Proc] the block that will interpret the exposed attribute
         | 
| 194 205 | 
             
                #
         | 
| 195 | 
            -
                #
         | 
| 196 | 
            -
                #
         | 
| 197 206 | 
             
                # @example Formatter declaration
         | 
| 198 207 | 
             
                #
         | 
| 199 208 | 
             
                #   module API
         | 
| @@ -203,7 +212,7 @@ module Grape | |
| 203 212 | 
             
                #           date.strftime('%m/%d/%Y')
         | 
| 204 213 | 
             
                #         end
         | 
| 205 214 | 
             
                #
         | 
| 206 | 
            -
                #         expose :birthday, :last_signed_in, : | 
| 215 | 
            +
                #         expose :birthday, :last_signed_in, format_with: :timestamp
         | 
| 207 216 | 
             
                #       end
         | 
| 208 217 | 
             
                #     end
         | 
| 209 218 | 
             
                #   end
         | 
| @@ -256,20 +265,20 @@ module Grape | |
| 256 265 | 
             
                #     class Users < Grape::API
         | 
| 257 266 | 
             
                #       version 'v2'
         | 
| 258 267 | 
             
                #
         | 
| 259 | 
            -
                #       # this will render { "users": [ {"id":"1"}, {"id":"2"} ] }
         | 
| 268 | 
            +
                #       # this will render { "users" : [ { "id" : "1" }, { "id" : "2" } ] }
         | 
| 260 269 | 
             
                #       get '/users' do
         | 
| 261 270 | 
             
                #         @users = User.all
         | 
| 262 | 
            -
                #         present @users, : | 
| 271 | 
            +
                #         present @users, with: API::Entities::User
         | 
| 263 272 | 
             
                #       end
         | 
| 264 273 | 
             
                #
         | 
| 265 | 
            -
                #       # this will render { "user": {"id":"1"} }
         | 
| 274 | 
            +
                #       # this will render { "user" : { "id" : "1" } }
         | 
| 266 275 | 
             
                #       get '/users/:id' do
         | 
| 267 276 | 
             
                #         @user = User.find(params[:id])
         | 
| 268 | 
            -
                #         present @user, : | 
| 277 | 
            +
                #         present @user, with: API::Entities::User
         | 
| 269 278 | 
             
                #       end
         | 
| 270 279 | 
             
                #     end
         | 
| 271 280 | 
             
                #   end
         | 
| 272 | 
            -
                def self.root(plural, singular=nil)
         | 
| 281 | 
            +
                def self.root(plural, singular = nil)
         | 
| 273 282 | 
             
                  @collection_root = plural
         | 
| 274 283 | 
             
                  @root = singular
         | 
| 275 284 | 
             
                end
         | 
| @@ -284,21 +293,24 @@ module Grape | |
| 284 293 | 
             
                # @param options [Hash] Options that will be passed through to each entity
         | 
| 285 294 | 
             
                #   representation.
         | 
| 286 295 | 
             
                #
         | 
| 287 | 
            -
                # @option options :root [String] override the default root name set for the
         | 
| 288 | 
            -
                # | 
| 289 | 
            -
                #    | 
| 296 | 
            +
                # @option options :root [String] override the default root name set for the entity.
         | 
| 297 | 
            +
                #   Pass nil or false to represent the object or objects with no root name
         | 
| 298 | 
            +
                #   even if one is defined for the entity.
         | 
| 290 299 | 
             
                def self.represent(objects, options = {})
         | 
| 291 | 
            -
                   | 
| 292 | 
            -
                    objects.to_ary | 
| 300 | 
            +
                  if objects.respond_to?(:to_ary)
         | 
| 301 | 
            +
                    inner = objects.to_ary.map { |object| new(object, { collection: true }.merge(options)) }
         | 
| 302 | 
            +
                    inner = inner.map(&:serializable_hash) if options[:serializable]
         | 
| 293 303 | 
             
                  else
         | 
| 294 | 
            -
                     | 
| 304 | 
            +
                    inner = new(objects, options)
         | 
| 305 | 
            +
                    inner = inner.serializable_hash if options[:serializable]
         | 
| 295 306 | 
             
                  end
         | 
| 296 307 |  | 
| 297 308 | 
             
                  root_element = if options.has_key?(:root)
         | 
| 298 | 
            -
             | 
| 299 | 
            -
             | 
| 300 | 
            -
             | 
| 301 | 
            -
             | 
| 309 | 
            +
                                   options[:root]
         | 
| 310 | 
            +
                                 else
         | 
| 311 | 
            +
                                   objects.respond_to?(:to_ary) ? @collection_root : @root
         | 
| 312 | 
            +
                                 end
         | 
| 313 | 
            +
             | 
| 302 314 | 
             
                  root_element ? { root_element => inner } : inner
         | 
| 303 315 | 
             
                end
         | 
| 304 316 |  | 
| @@ -310,6 +322,12 @@ module Grape | |
| 310 322 | 
             
                  self.class.exposures
         | 
| 311 323 | 
             
                end
         | 
| 312 324 |  | 
| 325 | 
            +
                def valid_exposures
         | 
| 326 | 
            +
                  exposures.select do |attribute, exposure_options|
         | 
| 327 | 
            +
                    valid_exposure?(attribute, exposure_options)
         | 
| 328 | 
            +
                  end
         | 
| 329 | 
            +
                end
         | 
| 330 | 
            +
             | 
| 313 331 | 
             
                def documentation
         | 
| 314 332 | 
             
                  self.class.documentation
         | 
| 315 333 | 
             
                end
         | 
| @@ -328,14 +346,18 @@ module Grape | |
| 328 346 | 
             
                def serializable_hash(runtime_options = {})
         | 
| 329 347 | 
             
                  return nil if object.nil?
         | 
| 330 348 | 
             
                  opts = options.merge(runtime_options || {})
         | 
| 331 | 
            -
                   | 
| 332 | 
            -
                    if  | 
| 349 | 
            +
                  valid_exposures.inject({}) do |output, (attribute, exposure_options)|
         | 
| 350 | 
            +
                    if conditions_met?(exposure_options, opts)
         | 
| 333 351 | 
             
                      partial_output = value_for(attribute, opts)
         | 
| 334 | 
            -
                      output[key_for(attribute)] =
         | 
| 352 | 
            +
                      output[self.class.key_for(attribute)] =
         | 
| 335 353 | 
             
                        if partial_output.respond_to? :serializable_hash
         | 
| 336 354 | 
             
                          partial_output.serializable_hash(runtime_options)
         | 
| 337 | 
            -
                        elsif partial_output.kind_of?(Array) && !partial_output.map {|o| o.respond_to? :serializable_hash}.include?(false)
         | 
| 338 | 
            -
                          partial_output.map {|o| o.serializable_hash}
         | 
| 355 | 
            +
                        elsif partial_output.kind_of?(Array) && !partial_output.map { |o| o.respond_to? :serializable_hash }.include?(false)
         | 
| 356 | 
            +
                          partial_output.map { |o| o.serializable_hash }
         | 
| 357 | 
            +
                        elsif partial_output.kind_of?(Hash)
         | 
| 358 | 
            +
                          partial_output.each do |key, value|
         | 
| 359 | 
            +
                            partial_output[key] = value.serializable_hash if value.respond_to? :serializable_hash
         | 
| 360 | 
            +
                          end
         | 
| 339 361 | 
             
                        else
         | 
| 340 362 | 
             
                          partial_output
         | 
| 341 363 | 
             
                        end
         | 
| @@ -344,7 +366,7 @@ module Grape | |
| 344 366 | 
             
                  end
         | 
| 345 367 | 
             
                end
         | 
| 346 368 |  | 
| 347 | 
            -
                 | 
| 369 | 
            +
                alias_method :as_json, :serializable_hash
         | 
| 348 370 |  | 
| 349 371 | 
             
                def to_json(options = {})
         | 
| 350 372 | 
             
                  options = options.to_h if options && options.respond_to?(:to_h)
         | 
| @@ -358,52 +380,149 @@ module Grape | |
| 358 380 |  | 
| 359 381 | 
             
                protected
         | 
| 360 382 |  | 
| 361 | 
            -
                def  | 
| 362 | 
            -
                   | 
| 383 | 
            +
                def self.name_for(attribute)
         | 
| 384 | 
            +
                  attribute.to_s.split('__').last.to_sym
         | 
| 385 | 
            +
                end
         | 
| 386 | 
            +
             | 
| 387 | 
            +
                def self.key_for(attribute)
         | 
| 388 | 
            +
                  exposures[attribute.to_sym][:as] || name_for(attribute)
         | 
| 389 | 
            +
                end
         | 
| 390 | 
            +
             | 
| 391 | 
            +
                def self.nested_exposures_for(attribute)
         | 
| 392 | 
            +
                  exposures.select { |a, _| a.to_s =~ /^#{attribute}__/ }
         | 
| 363 393 | 
             
                end
         | 
| 364 394 |  | 
| 365 395 | 
             
                def value_for(attribute, options = {})
         | 
| 366 396 | 
             
                  exposure_options = exposures[attribute.to_sym]
         | 
| 367 397 |  | 
| 368 | 
            -
                   | 
| 369 | 
            -
             | 
| 370 | 
            -
                   | 
| 398 | 
            +
                  nested_exposures = self.class.nested_exposures_for(attribute)
         | 
| 399 | 
            +
             | 
| 400 | 
            +
                  if exposure_options[:using]
         | 
| 401 | 
            +
                    exposure_options[:using] = exposure_options[:using].constantize if exposure_options[:using].respond_to? :constantize
         | 
| 402 | 
            +
             | 
| 371 403 | 
             
                    using_options = options.dup
         | 
| 372 404 | 
             
                    using_options.delete(:collection)
         | 
| 373 405 | 
             
                    using_options[:root] = nil
         | 
| 374 | 
            -
             | 
| 406 | 
            +
             | 
| 407 | 
            +
                    if exposure_options[:proc]
         | 
| 408 | 
            +
                      exposure_options[:using].represent(instance_exec(object, options, &exposure_options[:proc]), using_options)
         | 
| 409 | 
            +
                    else
         | 
| 410 | 
            +
                      exposure_options[:using].represent(delegate_attribute(attribute), using_options)
         | 
| 411 | 
            +
                    end
         | 
| 412 | 
            +
             | 
| 413 | 
            +
                  elsif exposure_options[:proc]
         | 
| 414 | 
            +
                    instance_exec(object, options, &exposure_options[:proc])
         | 
| 415 | 
            +
             | 
| 375 416 | 
             
                  elsif exposure_options[:format_with]
         | 
| 376 417 | 
             
                    format_with = exposure_options[:format_with]
         | 
| 377 418 |  | 
| 378 419 | 
             
                    if format_with.is_a?(Symbol) && formatters[format_with]
         | 
| 379 | 
            -
                      formatters[format_with] | 
| 420 | 
            +
                      instance_exec(delegate_attribute(attribute), &formatters[format_with])
         | 
| 380 421 | 
             
                    elsif format_with.is_a?(Symbol)
         | 
| 381 | 
            -
                       | 
| 422 | 
            +
                      send(format_with, delegate_attribute(attribute))
         | 
| 382 423 | 
             
                    elsif format_with.respond_to? :call
         | 
| 383 | 
            -
                       | 
| 424 | 
            +
                      instance_exec(delegate_attribute(attribute), &format_with)
         | 
| 384 425 | 
             
                    end
         | 
| 426 | 
            +
             | 
| 427 | 
            +
                  elsif nested_exposures.any?
         | 
| 428 | 
            +
                    Hash[nested_exposures.map do |nested_attribute, _|
         | 
| 429 | 
            +
                      [self.class.key_for(nested_attribute), value_for(nested_attribute, options)]
         | 
| 430 | 
            +
                    end]
         | 
| 431 | 
            +
             | 
| 385 432 | 
             
                  else
         | 
| 386 | 
            -
                     | 
| 433 | 
            +
                    delegate_attribute(attribute)
         | 
| 387 434 | 
             
                  end
         | 
| 388 435 | 
             
                end
         | 
| 389 436 |  | 
| 390 | 
            -
                def  | 
| 391 | 
            -
                   | 
| 392 | 
            -
                   | 
| 437 | 
            +
                def delegate_attribute(attribute)
         | 
| 438 | 
            +
                  name = self.class.name_for(attribute)
         | 
| 439 | 
            +
                  if respond_to?(name, true)
         | 
| 440 | 
            +
                    send(name)
         | 
| 441 | 
            +
                  else
         | 
| 442 | 
            +
                    object.send(name)
         | 
| 443 | 
            +
                  end
         | 
| 444 | 
            +
                end
         | 
| 393 445 |  | 
| 394 | 
            -
             | 
| 395 | 
            -
             | 
| 396 | 
            -
             | 
| 397 | 
            -
             | 
| 446 | 
            +
                def valid_exposure?(attribute, exposure_options)
         | 
| 447 | 
            +
                  nested_exposures = self.class.nested_exposures_for(attribute)
         | 
| 448 | 
            +
                  (nested_exposures.any? && nested_exposures.all? { |a, o| valid_exposure?(a, o) }) || \
         | 
| 449 | 
            +
                  exposure_options.has_key?(:proc) || \
         | 
| 450 | 
            +
                  !exposure_options[:safe] || \
         | 
| 451 | 
            +
                  object.respond_to?(self.class.name_for(attribute))
         | 
| 452 | 
            +
                end
         | 
| 453 | 
            +
             | 
| 454 | 
            +
                def conditions_met?(exposure_options, options)
         | 
| 455 | 
            +
                  if_conditions = (exposure_options[:if_extras] || []).dup
         | 
| 456 | 
            +
                  if_conditions << exposure_options[:if] unless exposure_options[:if].nil?
         | 
| 457 | 
            +
             | 
| 458 | 
            +
                  if_conditions.each do |if_condition|
         | 
| 459 | 
            +
                    case if_condition
         | 
| 460 | 
            +
                    when Hash then if_condition.each_pair { |k, v| return false if options[k.to_sym] != v }
         | 
| 461 | 
            +
                    when Proc then return false unless instance_exec(object, options, &if_condition)
         | 
| 462 | 
            +
                    when Symbol then return false unless options[if_condition]
         | 
| 463 | 
            +
                    end
         | 
| 398 464 | 
             
                  end
         | 
| 399 465 |  | 
| 400 | 
            -
                   | 
| 401 | 
            -
             | 
| 402 | 
            -
             | 
| 403 | 
            -
             | 
| 466 | 
            +
                  unless_conditions = (exposure_options[:unless_extras] || []).dup
         | 
| 467 | 
            +
                  unless_conditions << exposure_options[:unless] unless exposure_options[:unless].nil?
         | 
| 468 | 
            +
             | 
| 469 | 
            +
                  unless_conditions.each do |unless_condition|
         | 
| 470 | 
            +
                    case unless_condition
         | 
| 471 | 
            +
                    when Hash then unless_condition.each_pair { |k, v| return false if options[k.to_sym] == v }
         | 
| 472 | 
            +
                    when Proc then return false if instance_exec(object, options, &unless_condition)
         | 
| 473 | 
            +
                    when Symbol then return false if options[unless_condition]
         | 
| 474 | 
            +
                    end
         | 
| 404 475 | 
             
                  end
         | 
| 405 476 |  | 
| 406 477 | 
             
                  true
         | 
| 407 478 | 
             
                end
         | 
| 479 | 
            +
             | 
| 480 | 
            +
                private
         | 
| 481 | 
            +
             | 
| 482 | 
            +
                # All supported options.
         | 
| 483 | 
            +
                OPTIONS = [
         | 
| 484 | 
            +
                  :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :if_extras, :unless_extras
         | 
| 485 | 
            +
                ].to_set.freeze
         | 
| 486 | 
            +
             | 
| 487 | 
            +
                # Merges the given options with current block options.
         | 
| 488 | 
            +
                #
         | 
| 489 | 
            +
                # @param options [Hash] Exposure options.
         | 
| 490 | 
            +
                def self.merge_options(options)
         | 
| 491 | 
            +
                  opts = {}
         | 
| 492 | 
            +
             | 
| 493 | 
            +
                  merge_logic = proc do |key, existing_val, new_val|
         | 
| 494 | 
            +
                    if [:if, :unless].include?(key)
         | 
| 495 | 
            +
                      if existing_val.is_a?(Hash) && new_val.is_a?(Hash)
         | 
| 496 | 
            +
                        existing_val.merge(new_val)
         | 
| 497 | 
            +
                      elsif new_val.is_a?(Hash)
         | 
| 498 | 
            +
                        (opts["#{key}_extras".to_sym] ||= []) << existing_val
         | 
| 499 | 
            +
                        new_val
         | 
| 500 | 
            +
                      else
         | 
| 501 | 
            +
                        (opts["#{key}_extras".to_sym] ||= []) << new_val
         | 
| 502 | 
            +
                        existing_val
         | 
| 503 | 
            +
                      end
         | 
| 504 | 
            +
                    else
         | 
| 505 | 
            +
                      new_val
         | 
| 506 | 
            +
                    end
         | 
| 507 | 
            +
                  end
         | 
| 508 | 
            +
             | 
| 509 | 
            +
                  @block_options ||= []
         | 
| 510 | 
            +
                  opts.merge @block_options.inject({}) { |final, step|
         | 
| 511 | 
            +
                    final.merge(step, &merge_logic)
         | 
| 512 | 
            +
                  }.merge(valid_options(options), &merge_logic)
         | 
| 513 | 
            +
                end
         | 
| 514 | 
            +
             | 
| 515 | 
            +
                # Raises an error if the given options include unknown keys.
         | 
| 516 | 
            +
                # Renames aliased options.
         | 
| 517 | 
            +
                #
         | 
| 518 | 
            +
                # @param options [Hash] Exposure options.
         | 
| 519 | 
            +
                def self.valid_options(options)
         | 
| 520 | 
            +
                  options.keys.each do |key|
         | 
| 521 | 
            +
                    raise ArgumentError, "#{key.inspect} is not a valid option." unless OPTIONS.include?(key)
         | 
| 522 | 
            +
                  end
         | 
| 523 | 
            +
             | 
| 524 | 
            +
                  options[:using] = options.delete(:with) if options.has_key?(:with)
         | 
| 525 | 
            +
                  options
         | 
| 526 | 
            +
                end
         | 
| 408 527 | 
             
              end
         | 
| 409 528 | 
             
            end
         |