lib/alba/resource.rb in alba-1.2.0 vs lib/alba/resource.rb in alba-1.3.0

- old
+ new

@@ -1,16 +1,21 @@ require_relative 'one' require_relative 'many' +require_relative 'key_transform_factory' +require_relative 'typed_attribute' module Alba # This module represents what should be serialized module Resource # @!parse include InstanceMethods # @!parse extend ClassMethods - DSLS = {_attributes: {}, _key: nil, _transform_keys: nil, _transforming_root_key: false, _on_error: nil}.freeze + DSLS = {_attributes: {}, _key: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil}.freeze private_constant :DSLS + WITHIN_DEFAULT = Object.new.freeze + private_constant :WITHIN_DEFAULT + # @private def self.included(base) super base.class_eval do # Initialize @@ -27,11 +32,11 @@ attr_reader :object, :params # @param object [Object] the object to be serialized # @param params [Hash] user-given Hash for arbitrary data # @param within [Hash] determines what associations to be serialized. If not set, it serializes all associations. - def initialize(object, params: {}, within: true) + def initialize(object, params: {}, within: WITHIN_DEFAULT) @object = object @params = params.freeze @within = within DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.public_send(name)) } end @@ -58,46 +63,59 @@ # @return [String] def _key return @_key.to_s unless @_key == true && Alba.inferring - resource_name = self.class.name.demodulize.delete_suffix('Resource').underscore - key = collection? ? resource_name.pluralize : resource_name - transforming_root_key = @_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key - transforming_root_key ? transform_key(key) : key + transforming_root_key? ? transform_key(key_from_resource_name) : key_from_resource_name end + def key_from_resource_name + collection? ? resource_name.pluralize : resource_name + end + + def resource_name + self.class.name.demodulize.delete_suffix('Resource').underscore + end + + def transforming_root_key? + @_transforming_root_key.nil? ? Alba.transforming_root_key : @_transforming_root_key + end + def converter 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 + key_and_attribute_body_from(object, key, attribute) rescue ::Alba::Error, FrozenError, TypeError raise rescue StandardError => e handle_error(e, object, key, attribute) end arrays.reject(&:empty?).to_h end end + def key_and_attribute_body_from(object, key, attribute) + key = transform_key(key) + if attribute.is_a?(Array) # Conditional + conditional_attribute(object, key, attribute) + else + [key, fetch_attribute(object, attribute)] + end + end + def conditional_attribute(object, key, attribute) condition = attribute.last arity = condition.arity - return [] if arity <= 1 && !condition.call(object) + return [] if arity <= 1 && !instance_exec(object, &condition) 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) + return [] if arity >= 2 && !instance_exec(object, attr, &condition) [key, fetched_attribute] end def handle_error(error, object, key, attribute) @@ -116,87 +134,44 @@ end end # Override this method to supply custom key transform method def transform_key(key) - return key unless @_transform_keys + return key if @_transform_key_function.nil? - require_relative 'key_transformer' - KeyTransformer.transform(key, @_transform_keys) + @_transform_key_function.call(key.to_s) end def fetch_attribute(object, attribute) case attribute when Symbol object.public_send attribute when Proc instance_exec(object, &attribute) when Alba::One, Alba::Many - within = check_within + within = check_within(attribute.name.to_sym) return unless within attribute.to_hash(object, params: params, within: within) - when Hash # Typed Attribute - typed_attribute(object, attribute) + when TypedAttribute + attribute.value(object) else raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}" end end - def typed_attribute(object, hash) - attr_name = hash[:attr_name] - type = hash[:type] - type_converter = hash[:type_converter] - value, result = type_check(object, attr_name, type) - return value if result - raise TypeError if !result && !type_converter - - type_converter = type_converter_for(type) if type_converter == true - type_converter.call(value) - rescue TypeError - raise TypeError, "Attribute #{attr_name} is expected to be #{type} but actually #{value.nil? ? 'nil' : value.class.name}." - end - - def type_check(object, attr_name, type) - value = object.public_send(attr_name) - type_correct = case type - when :String, ->(klass) { klass == String } - value.is_a?(String) - when :Integer, ->(klass) { klass == Integer } - value.is_a?(Integer) - when :Boolean - [true, false].include?(attr_name) - else - raise Alba::UnsupportedType, "Unknown type: #{type}" - end - [value, type_correct] - end - - def type_converter_for(type) - case type - when :String, ->(klass) { klass == String } - ->(object) { object.to_s } - when :Integer, ->(klass) { klass == Integer } - ->(object) { Integer(object) } - when :Boolean - ->(object) { !!object } - else - raise Alba::UnsupportedType, "Unknown type: #{type}" - end - end - - def check_within + def check_within(association_name) case @within + when WITHIN_DEFAULT # Default value, doesn't check within tree + WITHIN_DEFAULT when Hash # Traverse within tree - @within.fetch(_key.to_sym, nil) + @within.fetch(association_name, nil) when Array # within tree ends with Array - @within.find { |item| item.to_sym == _key.to_sym } # Check if at least one item in the array matches current resource + @within.find { |item| item.to_sym == association_name } when Symbol # within tree could end with Symbol - @within == _key.to_sym # Check if the symbol matches current resource - when true # In this case, Alba serializes all associations. - true - when nil, false # In these cases, Alba stops serialization here. + @within == association_name + when nil, true, false # In these cases, Alba stops serialization here. false else raise Alba::Error, "Unknown type for within option: #{@within.class}" end end @@ -217,25 +192,36 @@ end # Set multiple attributes at once # # @param attrs [Array<String, Symbol>] - # @param options [Hash] option hash including `if` that is a condition to render these attributes + # @param if [Boolean] condition to decide if it should render these attributes + # @param attrs_with_types [Hash] attributes with name in its key and type and optional type converter in its value def attributes(*attrs, if: nil, **attrs_with_types) # rubocop:disable Naming/MethodParameterName if_value = binding.local_variable_get(:if) + assign_attributes(attrs, if_value) + assign_attributes_with_types(attrs_with_types, if_value) + end + + def assign_attributes(attrs, if_value) attrs.each do |attr_name| attr = if_value ? [attr_name.to_sym, if_value] : attr_name.to_sym @_attributes[attr_name.to_sym] = attr end + end + private :assign_attributes + + def assign_attributes_with_types(attrs_with_types, if_value) attrs_with_types.each do |attr_name, type_and_converter| attr_name = attr_name.to_sym type, type_converter = type_and_converter - typed_attr = {attr_name: attr_name, type: type, type_converter: type_converter} + typed_attr = TypedAttribute.new(name: attr_name, type: type, converter: type_converter) attr = if_value ? [typed_attr, if_value] : typed_attr @_attributes[attr_name] = attr end end + private :assign_attributes_with_types # 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 @@ -305,10 +291,10 @@ # Transform keys as specified type # # @param type [String, Symbol] # @param root [Boolean] decides if root key also should be transformed def transform_keys(type, root: nil) - @_transform_keys = type.to_sym + @_transform_key_function = KeyTransformFactory.create(type.to_sym) @_transforming_root_key = root end # Set error handler #