# frozen_string_literal: true require 'luna_park/mappers/simple' require 'luna_park/mappers/codirectional/copyists/slice' require 'luna_park/mappers/codirectional/copyists/nested' module LunaPark module Mappers ## # DSL for describe Nested Schema translation: entity attributes to database row and vice-versa # # @example # class Mappers::Transaction < LunaPark::Mappers::Codirectional # map attr: :uid, row: :id # map attr: [:charge, :amount], row: :charge_amount # map attr: [:charge, :currency], row: :charge_currency # using aliased args # map :comment # end # # mapper = Mappers::Transaction # # attrs = { charge: { amount: 10, currency: 'USD' }, comment: 'Foobar' } # transaction = Entities::Transaction.new(attrs) # # # Mapper transforms attr attributes to database row and vice-verse # row = mapper.to_row(transaction) # => { charge_amount: 10, charge_currency: 'USD', comment: 'Foobar' } # new_row = sequel_database_table.insert(row) # => { id: 42, charge_amount: 10, charge_currency: 'USD', comment: 'Foobar' } # new_attrs = mapper.from_row(new_row) # => { uid: 42, charge: { amount: 10, currency: 'USD' }, comment: 'Foobar' } # # transaction.set_attributes(new_attrs) # transaction.to_h # => { uid: 42, charge: { amount: 10, currency: 'USD' }, comment: 'Foobar' } class Codirectional < Simple class << self ## # Describe translation between two schemas: attr and table # # @example # class Mappers::Transaction < LunaPark::Mappers::Codirectional # map attr: :id, row: :uid # map attr: [:charge, :amount], row: :charge_amount # map :comment # end # # Mappers::Transaction.from_row({ id: 1, charge_amount: 2 }) # => { uid: 1, charge: { amount: 2 } } # Mappers::Transaction.to_row({ uid: 1, charge: { amount: 2 } }) # => { id: 1, charge_amount: 2 } def map(*common_keys, attr: nil, row: nil) attrs(*common_keys) if common_keys.any? self.attr attr, row: row if attr end # @example # class Mappers::Transaction < LunaPark::Mappers::Codirectional # attr :uid, row: :id # attr %i[charge amount], row: :charge_amount # end def attr(attr, row: nil) return attrs(attr) if row.nil? attr_path = to_path(attr) row_path = to_path(row) if attr_path == row_path attrs(attr_path) else nested_copyists << Copyists::Nested.new(attrs_path: attr_path, row_path: row_path) end end # @example # class Mappers::Transaction < LunaPark::Mappers::Codirectional # attrs :comment, :uid, %i[addresses home], :created_at # end def attrs(*common_keys) common_keys.each do |common_key| path = to_path(common_key) if path.is_a?(Array) nested_copyists << Copyists::Nested.new(attrs_path: path, row_path: path) else slice_copyist.add_key(path) end end end def from_row(input) row = input.to_h {}.tap do |attrs| slice_copyist.from_row(row: row, attrs: attrs) nested_copyists.each { |copyist| copyist.from_row(row: row, attrs: attrs) } end end def to_row(input) attrs = input.to_h {}.tap do |row| slice_copyist.to_row(row: row, attrs: attrs) nested_copyists.each { |copyist| copyist.to_row(row: row, attrs: attrs) } end end private # @example # to_path :email # => :email # to_path ['email'] # => :email # to_path [:charge, 'amount'] # => [:charge, :amount] def to_path(input, full: input) case input when Symbol then input when String then input.to_sym when Array return to_path(input.first, full: full) if input.size <= 1 input.flat_map { |elem| to_path(elem, full: full) } else raise ArgumentError, "Unexpected path part `#{input.inspect}` in `#{full.inspect}`. " \ 'Expected Symbol, String or Array' end end def slice_copyist @slice_copyist ||= Copyists::Slice.new end def nested_copyists @nested_copyists ||= [] end end end end end