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

- old
+ new

@@ -6,11 +6,11 @@ module Alba # This module represents what should be serialized module Resource # @!parse include InstanceMethods # @!parse extend ClassMethods - DSLS = {_attributes: {}, _key: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil}.freeze + DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_key_function: nil, _transforming_root_key: false, _on_error: nil}.freeze # rubocop:disable Layout/LineLength private_constant :DSLS WITHIN_DEFAULT = Object.new.freeze private_constant :WITHIN_DEFAULT @@ -31,25 +31,33 @@ module InstanceMethods 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. + # @param within [Object, nil, false, true] determines what associations to be serialized. If not set, it serializes all associations. 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 # Serialize object into JSON string # - # @param key [Symbol] + # @param key [Symbol, nil, true] DEPRECATED, use root_key instead + # @param root_key [Symbol, nil, true] + # @param meta [Hash] metadata for this seialization # @return [String] serialized JSON string - def serialize(key: nil) - key = key.nil? ? _key : key - hash = key && key != '' ? {key.to_s => serializable_hash} : serializable_hash + def serialize(key: nil, root_key: nil, meta: {}) + warn '`key` option to `serialize` method is deprecated, use `root_key` instead.' if key + key = key.nil? && root_key.nil? ? fetch_key : root_key || key + hash = if key && key != '' + h = {key.to_s => serializable_hash} + hash_with_metadata(h, meta) + else + serializable_hash + end Alba.encoder.call(hash) end # A Hash for serialization # @@ -59,21 +67,35 @@ end alias to_hash serializable_hash private + def hash_with_metadata(hash, meta) + base = @_meta ? instance_eval(&@_meta) : {} + metadata = base.merge(meta) + hash[:meta] = metadata unless metadata.empty? + hash + end + + def fetch_key + collection? ? _key_for_collection : _key + end + + def _key_for_collection + return @_key_for_collection.to_s unless @_key_for_collection == true && Alba.inferring + + key = resource_name.pluralize + transforming_root_key? ? transform_key(key) : key + end + # @return [String] def _key return @_key.to_s unless @_key == true && Alba.inferring - transforming_root_key? ? transform_key(key_from_resource_name) : key_from_resource_name + transforming_root_key? ? transform_key(resource_name) : 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? @@ -103,34 +125,27 @@ end def conditional_attribute(object, key, attribute) condition = attribute.last arity = condition.arity + # We can return early to skip fetch_attribute 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 + attr = attribute.first.is_a?(Alba::Association) ? attribute.first.object : fetched_attribute return [] if arity >= 2 && !instance_exec(object, attr, &condition) [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) + when :raise, nil then raise + when :nullify then [key, nil] + when :ignore then [] + when Proc then on_error.call(error, object, key, attribute, self.class) else raise ::Alba::Error, "Unknown on_error: #{on_error.inspect}" end end @@ -141,38 +156,31 @@ @_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(attribute.name.to_sym) - return unless within - - attribute.to_hash(object, params: params, within: within) - when TypedAttribute - attribute.value(object) + when Symbol then object.public_send attribute + when Proc then instance_exec(object, &attribute) + when Alba::One, Alba::Many then yield_if_within(attribute.name.to_sym) { |within| attribute.to_hash(object, params: params, within: within) } + when TypedAttribute then attribute.value(object) else raise ::Alba::Error, "Unsupported type of attribute: #{attribute.class}" end end + def yield_if_within(association_name) + within = check_within(association_name) + yield(within) if within + end + 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(association_name, nil) - when Array # within tree ends with Array - @within.find { |item| item.to_sym == association_name } - when Symbol # within tree could end with Symbol - @within == association_name - when nil, true, false # In these cases, Alba stops serialization here. - false + when WITHIN_DEFAULT then WITHIN_DEFAULT # Default value, doesn't check within tree + when Hash then @within.fetch(association_name, nil) # Traverse within tree + when Array then @within.find { |item| item.to_sym == association_name } + when Symbol then @within == association_name + when nil, true, false then false # Stop here else raise Alba::Error, "Unknown type for within option: #{@within.class}" end end @@ -189,15 +197,20 @@ def inherited(subclass) super DSLS.each_key { |name| subclass.instance_variable_set("@#{name}", instance_variable_get("@#{name}").clone) } end + # Defining methods for DSLs and disable parameter number check since for users' benefits increasing params is fine + # rubocop:disable Metrics/ParameterLists + # Set multiple attributes at once # # @param attrs [Array<String, Symbol>] - # @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 + # @param if [Proc] condition to decide if it should serialize these attributes + # @param attrs_with_types [Hash<[Symbol, String], [Array<Symbol, Proc>, Symbol]>] + # attributes with name in its key and type and optional type converter in its value + # @return [void] 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 @@ -222,43 +235,49 @@ 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 + # @param options [Hash<Symbol, Proc>] + # @option options [Proc] if a condition to decide if this attribute should be serialized # @param block [Block] the block called during serialization # @raise [ArgumentError] if block is absent + # @return [void] def attribute(name, **options, &block) raise ArgumentError, 'No block given in attribute method' unless 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 name [String, Symbol] name of the association, used as key when `key` param doesn't exist + # @param condition [Proc, nil] a Proc to modify the association + # @param resource [Class<Alba::Resource>, String, nil] representing resource for this association + # @param key [String, Symbol, nil] used as key when given + # @param options [Hash<Symbol, Proc>] + # @option options [Proc] if a condition to decide if this association should be serialized # @param block [Block] + # @return [void] # @see Alba::One#initialize 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 name [String, Symbol] name of the association, used as key when `key` param doesn't exist + # @param condition [Proc, nil] a Proc to filter the collection + # @param resource [Class<Alba::Resource>, String, nil] representing resource for this association + # @param key [String, Symbol, nil] used as key when given + # @param options [Hash<Symbol, Proc>] + # @option options [Proc] if a condition to decide if this association should be serialized # @param block [Block] + # @return [void] # @see Alba::Many#initialize 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 @@ -266,20 +285,46 @@ alias has_many many # Set key # # @param key [String, Symbol] + # @deprecated Use {#root_key} instead def key(key) + warn '[DEPRECATION] `key` is deprecated, use `root_key` instead.' @_key = key.respond_to?(:to_sym) ? key.to_sym : key end + # Set root key + # + # @param key [String, Symbol] + # @param key_for_collection [String, Symbol] + # @raise [NoMethodError] when key doesn't respond to `to_sym` method + def root_key(key, key_for_collection = nil) + @_key = key.to_sym + @_key_for_collection = key_for_collection&.to_sym + end + # Set key to true # + # @deprecated Use {#root_key!} instead def key! + warn '[DEPRECATION] `key!` is deprecated, use `root_key!` instead.' @_key = true + @_key_for_collection = true end + # Set root key to true + def root_key! + @_key = true + @_key_for_collection = true + end + + # Set metadata + def meta(&block) + @_meta = block + end + # Delete attributes # Use this DSL in child class to ignore certain attributes # # @param attributes [Array<String, Symbol>] def ignoring(*attributes) @@ -296,17 +341,20 @@ @_transform_key_function = KeyTransformFactory.create(type.to_sym) @_transforming_root_key = root end # Set error handler + # If this is set it's used as a error handler overriding global one # - # @param [Symbol] handler - # @param [Block] + # @param handler [Symbol] `:raise`, `:ignore` or `:nullify` + # @param block [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 + + # rubocop:enable Metrics/ParameterLists end end end