aglushkov/serega

View on GitHub
lib/serega/plugins/string_modifiers/parse_string_modifiers.rb

Summary

Maintainability
A
35 mins
Test Coverage
A
100%
# frozen_string_literal: true

require "stringio"

class Serega
  module SeregaPlugins
    #
    # Plugin :string_modifiers
    #
    # Allows to specify modifiers as strings.
    #
    # Serialized attributes must be split with `,` and nested attributes can be defined inside brackets `(`, `)`.
    #
    # @example
    #   PostSerializer.plugin :string_modifiers
    #   PostSerializer.new(only: "id,user(id,username)").to_h(post)
    #   PostSerializer.new(except: "user(username,email)").to_h(post)
    #   PostSerializer.new(with: "user(email)").to_h(post)
    #
    #   # Modifiers can still be provided old way with nested hashes or arrays.
    #   PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
    #
    module StringModifiers
      #
      # Modifiers parser
      #
      class ParseStringModifiers
        class << self
          #
          # Parses string modifiers
          #
          # @param fields [String]
          #
          # @return [Hash] parsed modifiers in form of nested hash
          #
          # @example
          #   parse("user") => { user: {} }
          #   parse("user(id)") => { user: { id: {} } }
          #   parse("user(id,name)") => { user: { id: {}, name: {} } }
          #   parse("user,comments") => { user: {}, comments: {} }
          #   parse("user(comments(text))") => { user: { comments: { text: {} } } }
          def parse(fields)
            res = {}
            attribute = +""
            char = +""
            path_stack = nil
            fields = StringIO.new(fields)

            while fields.read(1, char)
              case char
              when ","
                add_attribute(res, path_stack, attribute, FROZEN_EMPTY_HASH)
              when ")"
                add_attribute(res, path_stack, attribute, FROZEN_EMPTY_HASH)
                path_stack&.pop
              when "("
                name = add_attribute(res, path_stack, attribute, {})
                (path_stack ||= []).push(name) if name
              else
                attribute.insert(-1, char)
              end
            end

            add_attribute(res, path_stack, attribute, FROZEN_EMPTY_HASH)

            res
          end

          private

          def add_attribute(res, path_stack, attribute, nested_attributes = FROZEN_EMPTY_HASH)
            attribute.strip!
            return if attribute.empty?

            name = attribute.to_sym
            attribute.clear

            current_attrs = dig?(res, path_stack)
            current_attrs[name] = nested_attributes

            name
          end

          def dig?(hash, path)
            return hash if !path || path.empty?

            path.each do |point|
              hash = hash[point]
            end

            hash
          end
        end
      end
    end
  end
end