AMA::Entity::Mapper
This library provides entity mapper - API for seamless one-class-to-another object conversion. This comes handy in:
- Deserializing complex structures
- Converting structures to objects that don't have one-to-one relation
- Retrieving entity information from it's path (e.g. inferring parent hash keys as entity attribute)
- Deserializing objects that may have different representation (e.g. name
may came as both
'John Doe'
or{first: 'John', last: 'Doe'}
)
You may find yourself in such a situation when storing data in Chef node attributes, building API clients or storing data in database.
Installation
Add this line to your application's Gemfile:
gem 'ama-entity-mapper'
And then execute:
$ bundle
Or install it yourself as:
$ gem install ama-entity-mapper
Usage
Mapper is created to map data from one type to another. There are no hard bounds in this, conversion may be done with any types as long as rough corners are handled by corresponding handlers (more on that later). Let's start with easy object to hash and vice versa conversion:
require 'ama-entity-mapper'
class User
attr_accessor :first_name
attr_accessor :last_name
attr_accessor :login
end
data = { first_name: 'John', last_name: 'Doe', login: 'john-doe' }
user = AMA::Entity::Mapper.map(data, User)
restored_data = AMA::Entity::Mapper.map(user, Hash)
This is, of course, terribly simple; the real power shows up in nested structure processing. Imagine an API that responds with hash of lists:
winners:
- first_name: Max
last_name: Payne
login: mpayne
losers:
- first_name: Nicole
last_name: Horne
login: nhorne
End developer would like to receive all keys as symbols, and denormalize User objects. To do so, you should specify target type parameters:
# still hash of arrays, but with users now
data = AMA::Entity::Mapper.map(input, [Hash, K: Symbol, V: [Array, T: User]])
Basically it means 'map input data to hash with symbol keys and values as arrays of User instances'. K, V and T are predefined type parameters, and Array and Hash mappings were added automatically.
The next desired action would be to map some input structure into nested custom classes. This is possible with custom mappings, with easiest way to specify them via custom DSL:
class Page
include AMA::Entity::Mapper::DSL
attribute :number, Integer
attribute :last, TrueClass, FalseClass
# Substituting Array's parameter T with page class parameter E,
# so content is not Array<Array.T>, but Array<Page.E>
attribute :content, [Array, T: parameter(:E)]
end
class User
include AMA::Entity::Mapper::DSL
attribute :login, Symbol, aliases: %i[id user_id]
attribute :policy, Symbol, values: %i[read write manage], default: :read
attribute :keys, [Hash, K: Symbol, V: PrivateKey], default: {}
attribute :last_login, DateTime, nullable: true
end
class PrivateKey
include AMA::Entity::Mapper::DSL
attribute :id, Symbol
# Sensitive attributes may be restored from incoming data, but are ignored
# when mapped into another type.
attribute :content, String, sensitive: true
end
data = {
number: 1,
last: false,
content: [
ron: {
id: 'charlie-the-unicorn',
policy: 'read',
keys: {
id_rsa: {
id: 'id_rsa',
content: '----BEGIN PRIVATE KEY....'
}
}
}
]
}
page = AMA::Entity::Mapper.map(data, [Page, E: User])
Last thing to say is that attributes, in fact, may have several possible types,
and #map
method accepts several types as well:
result = AMA::Entity::Mapper.map(data, TrueClass, FalseClass)
This would try to map data to TrueClass, and, if it fails - to FalseClass.
That's the crash course, most probably you've already got what you need. If that's not enough, full documentation is available at GitHub Pages.
Versioning
This project adheres to Semantic Versioning. In a nutshell, this means the following:
- Every patch release (a.b.X -> a.b.Y) fixes bugs and is safe to update to from previous patch release
- Every minor release (a.X.b -> a.Y.0) contains new features
- Every minor release before 1.0.0 may break API
- Every minor release after 1.0.0 does not break API (but may introduce new coexisting replacement API and schedule existing API for, deprecation) updating to next minor release is safe after 1.0.0
- Every major release contains significant API change and it is not safe to jump from one major release to another.
Development
After checking out the repo, run bin/setup
to install dependencies
and git hooks.
Then, run rake validate
to run lint and tests. If you have
Allure report generator on your machine, you may run
rake test:with-report
to generate report after test run.
Tests are separated into three categories: unit (no dependencies, everything is mocked), integration (testing components with their dependencies, optional mocking) and acceptance (no mocks, real data, testing high-level components and library as a whole).
Development is performed using git-flow, meaning that development id done in dev or release/x.y branch, features and stuff is done in branches spawn from branches specified above.
To install this gem onto your local machine, run bundle exec rake install
.
To release a new version, update the version number in version.rb
,
and then run bundle exec rake release
, which will create a git tag for
the version, push git commits and tags, and push the .gem
file
to rubygems.org.
Dev branch state:
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/ama-team/ruby-entity-mapper. Please send your pull requests into dev branch (not into master).
License
The gem is available as open source under the terms of the MIT License.