lib/alba/resource.rb in alba-0.13.1 vs lib/alba/resource.rb in alba-1.0.0

- old
+ new

@@ -1,15 +1,14 @@ -require_relative 'serializer' require_relative 'one' require_relative 'many' module Alba # This module represents what should be serialized module Resource # @!parse include InstanceMethods # @!parse extend ClassMethods - DSLS = {_attributes: {}, _serializer: nil, _key: nil, _transform_keys: nil}.freeze + DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _on_error: nil}.freeze private_constant :DSLS # @private def self.included(base) super @@ -23,96 +22,122 @@ base.extend ClassMethods end # Instance methods module InstanceMethods - attr_reader :object, :_key, :params + attr_reader :object, :params # @param object [Object] the object to be serialized # @param params [Hash] user-given Hash for arbitrary data def initialize(object, params: {}) @object = object @params = params.freeze DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) } end - # Get serializer with `with` argument and serialize self with it + # Serialize object into JSON string # - # @param with [nil, Proc, Alba::Serializer] selializer + # @param key [Symbol] # @return [String] serialized JSON string - def serialize(with: nil) - serializer = case with - when nil - @_serializer || empty_serializer - when ->(obj) { obj.is_a?(Class) && obj <= Alba::Serializer } - with - when Proc - inline_extended_serializer(with) - else - raise ArgumentError, 'Unexpected type for with, possible types are Class or Proc' - end - serializer.new(self).serialize + def serialize(key: nil) + key = key.nil? ? _key : key + hash = key && key != '' ? {key.to_s => serializable_hash} : serializable_hash + Alba.encoder.call(hash) end # A Hash for serialization # # @return [Hash] def serializable_hash collection? ? @object.map(&converter) : converter.call(@object) end alias to_hash serializable_hash - # @return [Symbol] - def key - @_key || self.class.name.delete_suffix('Resource').downcase.gsub(/:{2}/, '_').to_sym - end - private - # rubocop:disable Style/MethodCalledOnDoEndBlock + # @return [String] + def _key + if @_key == true && Alba.inferring + demodulized = ActiveSupport::Inflector.demodulize(self.class.name) + meth = collection? ? :tableize : :singularize + ActiveSupport::Inflector.public_send(meth, demodulized.delete_suffix('Resource').downcase) + else + @_key.to_s + end + end + def converter - lambda do |resource| - @_attributes.map do |key, attribute| - [transform_key(key), fetch_attribute(resource, attribute)] - end.to_h + lambda do |object| + arrays = @_attributes.map do |key, attribute| + key = transform_key(key) + if attribute.is_a?(Array) # Conditional + conditional_attribute(object, key, attribute) + else + [key, fetch_attribute(object, attribute)] + end + rescue ::Alba::Error, FrozenError + raise + rescue StandardError => e + handle_error(e, object, key, attribute) + end + arrays.reject(&:empty?).to_h end end - # rubocop:enable Style/MethodCalledOnDoEndBlock + def conditional_attribute(object, key, attribute) + condition = attribute.last + arity = condition.arity + return [] if arity <= 1 && !condition.call(object) + + fetched_attribute = fetch_attribute(object, attribute.first) + attr = if attribute.first.is_a?(Alba::Association) + attribute.first.object + else + fetched_attribute + end + return [] if arity >= 2 && !condition.call(object, attr) + + [key, fetched_attribute] + end + + def handle_error(error, object, key, attribute) + on_error = @_on_error || Alba._on_error + case on_error + when :raise, nil + raise + when :nullify + [key, nil] + when :ignore + [] + when Proc + on_error.call(error, object, key, attribute, self.class) + else + raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}" + end + end + # Override this method to supply custom key transform method def transform_key(key) return key unless @_transform_keys require_relative 'key_transformer' KeyTransformer.transform(key, @_transform_keys) end - def fetch_attribute(resource, attribute) + def fetch_attribute(object, attribute) case attribute when Symbol - resource.public_send attribute + object.public_send attribute when Proc - instance_exec(resource, &attribute) + instance_exec(object, &attribute) when Alba::One, Alba::Many - attribute.to_hash(resource, params: params) + attribute.to_hash(object, params: params) else raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}" end end - def empty_serializer - klass = Class.new - klass.include Alba::Serializer - klass - end - - def inline_extended_serializer(with) - klass = empty_serializer - klass.class_eval(&with) - klass - end - def collection? @object.is_a?(Enumerable) end end @@ -127,65 +152,75 @@ end # Set multiple attributes at once # # @param attrs [Array<String, Symbol>] - def attributes(*attrs) - attrs.each { |attr_name| @_attributes[attr_name.to_sym] = attr_name.to_sym } + # @param options [Hash] option hash including `if` that is a condition to render these attributes + def attributes(*attrs, **options) + attrs.each do |attr_name| + attr = options[:if] ? [attr_name.to_sym, options[:if]] : attr_name.to_sym + @_attributes[attr_name.to_sym] = attr + end end # Set an attribute with the given block # # @param name [String, Symbol] key name + # @param options [Hash] option hash including `if` that is a condition to render # @param block [Block] the block called during serialization # @raise [ArgumentError] if block is absent - def attribute(name, &block) + def attribute(name, **options, &block) raise ArgumentError, 'No block given in attribute method' unless block - @_attributes[name.to_sym] = block + @_attributes[name.to_sym] = options[:if] ? [block, options[:if]] : block end # Set One association # # @param name [String, Symbol] # @param condition [Proc] # @param resource [Class<Alba::Resource>] # @param key [String, Symbol] used as key when given + # @param options [Hash] option hash including `if` that is a condition to render # @param block [Block] # @see Alba::One#initialize - def one(name, condition = nil, resource: nil, key: nil, &block) - @_attributes[key&.to_sym || name.to_sym] = One.new(name: name, condition: condition, resource: resource, &block) + def one(name, condition = nil, resource: nil, key: nil, **options, &block) + nesting = self.name&.rpartition('::')&.first + one = One.new(name: name, condition: condition, resource: resource, nesting: nesting, &block) + @_attributes[key&.to_sym || name.to_sym] = options[:if] ? [one, options[:if]] : one end alias has_one one # Set Many association # # @param name [String, Symbol] # @param condition [Proc] # @param resource [Class<Alba::Resource>] # @param key [String, Symbol] used as key when given + # @param options [Hash] option hash including `if` that is a condition to render # @param block [Block] # @see Alba::Many#initialize - def many(name, condition = nil, resource: nil, key: nil, &block) - @_attributes[key&.to_sym || name.to_sym] = Many.new(name: name, condition: condition, resource: resource, &block) + def many(name, condition = nil, resource: nil, key: nil, **options, &block) + nesting = self.name&.rpartition('::')&.first + many = Many.new(name: name, condition: condition, resource: resource, nesting: nesting, &block) + @_attributes[key&.to_sym || name.to_sym] = options[:if] ? [many, options[:if]] : many end alias has_many many - # Set serializer for the resource - # - # @param name [Alba::Serializer] - def serializer(name) - @_serializer = name <= Alba::Serializer ? name : nil - end - # Set key # # @param key [String, Symbol] def key(key) - @_key = key.to_sym + @_key = key.respond_to?(:to_sym) ? key.to_sym : key end + # Set key to true + # + def key! + @_key = true + end + # Delete attributes # Use this DSL in child class to ignore certain attributes # # @param attributes [Array<String, Symbol>] def ignoring(*attributes) @@ -197,9 +232,20 @@ # Transform keys as specified type # # @param type [String, Symbol] def transform_keys(type) @_transform_keys = type.to_sym + end + + # Set error handler + # + # @param [Symbol] handler + # @param [Block] + def on_error(handler = nil, &block) + raise ArgumentError, 'You cannot specify error handler with both Symbol and block' if handler && block + raise ArgumentError, 'You must specify error handler with either Symbol or block' unless handler || block + + @_on_error = handler || block end end end end