Yaoc Code Climate Build Status Coverage Status Gem Version

Converting one ruby object into another with some rules.

Installation

Add this line to your application's Gemfile:

gem 'yaoc'

And then execute:

$ bundle

Or install it yourself as:

$ gem install yaoc

Usage

What does it do in a high level?

You say transform an object of class A into an object of class B:

mapper =Yaoc::ObjectMapper.new(B, A) # yes, first what you want, then where to get it

Then you define some rules how it should be done:

mapper.add_mapping do
    fetcher :public_send
    strategy :to_hash_mapping # default or :to_array_mapping

    rule to: :role, from: :r_role

    rule to: :firstname,
         from: :fullname,
         converter: ->(source, result){ Yaoc::TransformationCommand.fill_result_with_value(result, :firstname, source.fullname.split().first) },
         reverse_converter: ->(source, result){ Yaoc::TransformationCommand.fill_result_with_value(result, :fullname,  "#{source.firstname} #{source.lastname}") }

    rule to: :lastname,
         from: :fullname,
         converter: ->(source, result){ Yaoc::TransformationCommand.fill_result_with_value(result, :lastname, source.fullname.split().last ) },
         reverse_converter: ->(source, result){ result }

    rule to: :id
end

After this you can transform an object of class A into an object of class B:

mapper.load(A.new)

Or reverse:

mapper.dump(B.new)

Depending on strategy or reverse_strategy the input object is first transformed into a Hash or Array and after this passed to the class constructor.

You can also pass an existing object to load and dump:

b = B.new
mapper.load(A.new, b)

a = A.new
mapper.dump(B.new, a)

In this case setters with matching name to the hash key are invoked with the hash value as value.

The resulting classes have hash enabled constructors?


require 'yaoc'

include Yaoc::Helper

User = StructHE(:id, :firstname, :lastname, :role)

OldUser = StructHE(:id, :fullname, :r_role)

mapper = Yaoc::ObjectMapper.new(User, OldUser).tap do |mapper|
  mapper.add_mapping do
    fetcher :public_send
    rule to: :role, from: :r_role

    rule to: :firstname,
         from: :fullname,
         converter: ->(source, result){ Yaoc::TransformationCommand.fill_result_with_value(result, :firstname, source.fullname.split().first) },
         reverse_converter: ->(source, result){ Yaoc::TransformationCommand.fill_result_with_value(result, :fullname,  "#{source.firstname} #{source.lastname}") }

    rule to: :lastname,
         from: :fullname,
         converter: ->(source, result){ Yaoc::TransformationCommand.fill_result_with_value(result, :lastname, source.fullname.split().last ) },
         reverse_converter: ->(source, result){ result }

    rule to: :id
  end
end

old_user = OldUser.new({id: 1, fullname: "myfirst mysecond", r_role: "admin" })
new_user = mapper.load(old_user)

puts old_user
puts new_user

new_user.firstname = "no"
new_user.lastname = "name"

puts mapper.dump(new_user)

#<struct OldUser id=1, fullname="myfirst mysecond", r_role="admin">
#<struct User id=1, firstname="myfirst", lastname="mysecond", role="admin">
#<struct OldUser id=1, fullname="no name", r_role="admin">


The resulting classes have no hash enabled constructor?


require 'yaoc'

include Yaoc::Helper

OldUser2 = Struct.new(:id, :fullname, :r_role)

User2 = Struct.new(:id, :firstname, :lastname, :role)

reverse_source = ->(attrs){
  OldUser2.new.tap do |old_user|
    attrs.each_pair do |key, value|
      old_user.public_send "#{key}=", value
    end
  end
}

source = ->(attrs){
  User2.new.tap do |old_user|
    attrs.each_pair do |key, value|
      old_user.public_send "#{key}=", value
    end
  end
}

mapper = Yaoc::ObjectMapper.new(source, reverse_source).tap do |mapper|
  mapper.add_mapping do
    fetcher :public_send
    rule to: :role, from: :r_role

    rule to: :firstname,
         from: :fullname,
         converter: ->(source, result){ Yaoc::TransformationCommand.fill_result_with_value(result, :firstname,  source.fullname.split().first ) },
         reverse_converter: ->(source, result){ Yaoc::TransformationCommand.fill_result_with_value(result, :fullname, "#{source.firstname} #{source.lastname}") }

    rule to: :lastname,
         from: :fullname,
         converter: ->(source, result){ Yaoc::TransformationCommand.fill_result_with_value(result, :lastname,  source.fullname.split().last) },
         reverse_converter: ->(source, result){ result }

    rule to: :id
  end
end

old_user2 = OldUser2.new(1, "myfirst mysecond",  "admin" )
new_user2 = mapper.load(old_user2)

puts old_user2
puts new_user2

new_user2.firstname = "no"
new_user2.lastname = "name"

puts mapper.dump(new_user2)


#<struct OldUser2 id=1, fullname="myfirst mysecond", r_role="admin">
#<struct User2 id=1, firstname="myfirst", lastname="mysecond", role="admin">
#<struct OldUser2 id=1, fullname="no name", r_role="admin">

But my classes have positional constructor, what now?


require 'yaoc'

include Yaoc::Helper

puts "\n" * 5

OldUser3 = Struct.new(:id, :fullname, :r_role)
User3 = Struct.new(:id, :firstname, :lastname, :role)

# alternative to proc for converter
converter = Yaoc::TransformationCommand.create(to: 1,
                                               from: :fullname,
                                               deferred: false,
                                               fetcher_proc: ->(source, fetcher, from){source.fullname.split().first} )

reverse_converter = Yaoc::TransformationCommand.create(to: 1,
                                                       from: :first_and_lastname,
                                                       deferred: false,
                                                       fetcher_proc: ->(source, fetcher, from){ "#{source.firstname} #{source.lastname}"} )

mapper = Yaoc::ObjectMapper.new(User3, OldUser3).tap do |mapper|
  mapper.add_mapping do
    fetcher :public_send

    strategy :to_array_mapping
    reverse_strategy :to_array_mapping

    rule to: 0, from: :id,
         reverse_to: 0, reverse_from: :id

    rule to: 1,
         from: :fullname,

         converter: converter,
         reverse_converter: reverse_converter

    rule to: 2,
         from: :fullname,

         converter: ->(source, result){ result[2]  = source.fullname.split().last },
         reverse_converter: ->(source, result){ result }

    rule to: 3, from: :r_role,
         reverse_to: 2, reverse_from: :role

  end
end

old_user3 = OldUser3.new(1, "myfirst mysecond",  "admin" )
new_user3 = mapper.load(old_user3)

puts old_user3
puts new_user3

new_user3.firstname = "no"
new_user3.lastname = "name"

puts mapper.dump(new_user3)


#<struct OldUser3 id=1, fullname="myfirst mysecond", r_role="admin">
#<struct User3 id=1, firstname="myfirst", lastname="mysecond", role="admin">
#<struct OldUser3 id=1, fullname="no name", r_role="admin">

And how to use it with compositions?


require 'yaoc'

include Yaoc::Helper


puts "\n" * 5


User4 = StructHE(:id, :firstname, :lastname, :roles)

OldUser4 = StructHE(:o_id, :o_firstname, :o_lastname, :o_roles)


Role = StructHE(:id, :name)

OldRole = StructHE(:o_id, :o_name)


Yaoc::ObjectMapper.new(Role, OldRole).tap do |mapper|
  mapper.add_mapping do
    register_as :role_mapper
    fetcher :public_send

    rule to: :id, from: :o_id
    rule to: :name, from: :o_name

  end
end

user_mapper = Yaoc::ObjectMapper.new(User4, OldUser4).tap do |mapper|
  mapper.add_mapping do
    fetcher :public_send

    rule to: [:id, :firstname, :lastname],
         from: [:o_id, :o_firstname, :o_lastname]

    rule to: :roles,
         from: :o_roles,
         object_converter: :role_mapper,
         is_collection: true

  end
end


old_user4 = OldUser4.new(o_id: 1,
                         o_firstname: "firstname",
                         o_lastname:"lastname",
                         o_roles: [OldRole.new(o_id: 1, o_name: "admin"), OldRole.new(o_id: 2, o_name: "guest")] )
new_user4 = user_mapper.load(old_user4)

puts old_user4
puts new_user4

puts user_mapper.dump(new_user4)

#<struct OldUser4 o_id=1, o_firstname="firstname", o_lastname="lastname",
# o_roles=[#<struct OldRole o_id=1, o_name="admin">, #<struct OldRole o_id=2, o_name="guest">]>
#<struct User4 id=1, firstname="firstname", lastname="lastname",
# roles=[#<struct Role id=1, name="admin">, #<struct Role id=2, name="guest">]>
#<struct OldUser4 o_id=1, o_firstname="firstname", o_lastname="lastname",
# o_roles=[#<struct OldRole o_id=1, o_name="admin">, #<struct OldRole o_id=2, o_name="guest">]>

And how can I add values to existing objects?

require 'yaoc'

include Yaoc::Helper

puts "\n" * 5

OldUser5 = StructHE(:id, :name)

RoleThing = StructHE(:id, :role)

User5 = StructHE(:id, :name,  :role)


user_mapper = Yaoc::ObjectMapper.new(User5, OldUser5).tap do |mapper|
  mapper.add_mapping do
    fetcher :public_send
    rule to: [:id, :name]
  end
end

role_mapper = Yaoc::ObjectMapper.new(User5, RoleThing).tap do |mapper|
  mapper.add_mapping do
    fetcher :public_send
    rule to: [:role]
  end
end

old_role = RoleThing.new(id: 1, role: "my_role")
old_user5 = OldUser5.new(id: 1, name: "my fullname")

new_user5 = user_mapper.load(old_user5)

role_mapper.load(old_role, new_user5)

# OR
#
# mapper_chain = Yaoc::ManyToOneMapperChain.new(user_mapper, role_mapper)
# new_user5 = mapper_chain.load_all([old_user5, old_role])


puts old_user5
puts old_role
puts new_user5

#<struct OldUser5 id=1, name="my fullname">
#<struct RoleThing id=1, role="my_role">
#<struct User5 id=1, name="my fullname", role="my_role">

How can I lazy load some expensive to convert attributes?

require 'yaoc'

include Yaoc::Helper

puts "\n" * 5


OldUser6 = StructHE(:id) do

  def names=(new_names)
    @names = new_names
  end

  def names
    puts 'some expensive operation in progress ...'
    sleep 10
    @names
  end

end
User6 = StructHE(:id, :names)


user_mapper = Yaoc::ObjectMapper.new(User6, OldUser6).tap do |mapper|
  mapper.add_mapping do
    fetcher :public_send
    rule to: [:id, :names],
         lazy_loading: [false, true]
  end
end

old_user6 = OldUser6.new(id: 'my_id_1', names: ['one', 'two', 'three', 'four'])
new_user6 = user_mapper.load(old_user6)

puts new_user6.id.inspect
puts new_user6.names.inspect
puts new_user6


puts "\n" * 5

# "my_id_1"
# some expensive operation in progress ...
# ["one", "two", "three", "four"]
#<struct User6 id="my_id_1", names=["one", "two", "three", "four"]>

Contributing

  1. Fork it ( http://github.com/slowjack2k/yaoc/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request