# frozen_string_literal: true class Serega module SeregaPlugins # # Plugin :camel_case # # By default when we add attribute like `attribute :first_name` this means: # - adding a `:first_name` key to resulted hash # - adding a `#first_name` method call result as value # # But its often desired to response with *camelCased* keys. # Earlier this can be achieved by specifying attribute name and method directly # for each attribute: `attribute :firstName, method: first_name` # # Now this plugin transforms all attribute names automatically. # We use simple regular expression to replace `_x` to `X` for the whole string. # You can provide your own callable transformation when defining plugin, # for example `plugin :camel_case, transform: ->(name) { name.camelize }` # # For any attribute camelCase-behavior can be skipped when # `camel_case: false` attribute option provided. # # @example Define plugin # class AppSerializer < Serega # plugin :camel_case # end # # class UserSerializer < AppSerializer # attribute :first_name # attribute :last_name # attribute :full_name, camel_case: false, value: proc { |user| [user.first_name, user.last_name].compact.join(" ") } # end # # require "ostruct" # user = OpenStruct.new(first_name: "Bruce", last_name: "Wayne") # UserSerializer.to_h(user) # {firstName: "Bruce", lastName: "Wayne", full_name: "Bruce Wayne"} # module CamelCase # Default camel-case transformation TRANSFORM_DEFAULT = proc { |attribute_name| attribute_name.gsub!(/_[a-z]/) { |m| m[-1].upcase! } attribute_name } # @return [Symbol] Plugin name def self.plugin_name :camel_case end # # Applies plugin code to specific serializer # # @param serializer_class [Class] Current serializer class # @param _opts [Hash] Loaded plugins options # # @return [void] # def self.load_plugin(serializer_class, **_opts) serializer_class::SeregaConfig.include(ConfigInstanceMethods) serializer_class::SeregaAttributeNormalizer.include(AttributeNormalizerInstanceMethods) serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods) end # # Adds config options and runs other callbacks after plugin was loaded # # @param serializer_class [Class] Current serializer class # @param opts [Hash] loaded plugins opts # # @return [void] # def self.after_load_plugin(serializer_class, **opts) config = serializer_class.config config.opts[:camel_case] = {} config.camel_case.transform = opts[:transform] || TRANSFORM_DEFAULT config.attribute_keys << :camel_case end # # Config class additional/patched instance methods # # @see Serega::SeregaConfig # module ConfigInstanceMethods # @return [Serega::SeregaPlugins::CamelCase::CamelCaseConfig] `camel_case` plugin config def camel_case @camel_case ||= CamelCaseConfig.new(opts.fetch(:camel_case)) end end # # Serega::SeregaValidations::CheckAttributeParams additional/patched class methods # # @see Serega::SeregaValidations::CheckAttributeParams # module CheckAttributeParamsInstanceMethods private def check_opts super CheckOptCamelCase.call(opts) end end # # Validator for attribute :camel_case option # class CheckOptCamelCase class << self # # Checks attribute :camel_case option must be boolean # # @param opts [Hash] Attribute options # # @raise [SeregaError] Attribute validation error # # @return [void] # def call(opts) camel_case_option_exists = opts.key?(:camel_case) return unless camel_case_option_exists value = opts[:camel_case] return if value.equal?(true) || value.equal?(false) raise SeregaError, "Attribute option :camel_case must have a boolean value, but #{value.class} was provided" end end end # CamelCase config object class CamelCaseConfig attr_reader :opts # # Initializes CamelCaseConfig object # # @param opts [Hash] camel_case plugin options # @option opts [#call] :transform Callable object that transforms original attribute name # # @return [Serega::SeregaPlugins::CamelCase::CamelCaseConfig] CamelCaseConfig object # def initialize(opts) @opts = opts end # @return [#call] defined object that transforms name def transform opts.fetch(:transform) end # Sets transformation callable object # # @param value [#call] transformation # # @return [#call] camel_case plugin transformation callable object def transform=(value) raise SeregaError, "Transform value must respond to #call" unless value.respond_to?(:call) params = value.is_a?(Proc) ? value.parameters : value.method(:call).parameters if params.count != 1 || !params.all? { |param| (param[0] == :req) || (param[0] == :opt) } raise SeregaError, "Transform value must respond to #call and accept 1 regular parameter" end opts[:transform] = value end end # # SeregaAttributeNormalizer additional/patched instance methods # # @see SeregaAttributeNormalizer::AttributeInstanceMethods # module AttributeNormalizerInstanceMethods private # # Patch for original `prepare_name` method # # Makes camelCased name # def prepare_name res = super return res if init_opts[:camel_case] == false self.class.serializer_class.config.camel_case.transform.call(res.to_s).to_sym end end end register_plugin(CamelCase.plugin_name, CamelCase) end end