NOTE: This project is no longer being actively maintained
We didn't end up using it to the extent I thought and it hasn't seen much active use outside of auctionet as far as I can see.
I recommend looking at rom-rb or lotus.
Old readme below
Minimapper
About
Introduction
Minimapper is a minimalistic way of separating models from ActiveRecord. It enables you to test your models (and code using your models) within a sub-second unit test suite and makes it simpler to have a modular design as described in Matt Wynne's Hexagonal Rails posts.
Minimapper follows many Rails conventions but it does not require Rails.
Early days
The API may not be entirely stable yet and there are probably edge cases that aren't covered. However... it's most likely better to use this than to roll your own project specific solution. We need good tools for this kind of thing in the rails community, but to make that possible we need to gather around a few of them and make them good.
Important resources
- minimapper-extras (useful tools for projects using minimapper)
- Gist of yet to be extracted mapper code from a project using minimapper.
- Gist of alternative way of organizing records and mappers
Compatibility
This gem is tested against all major rubies in 1.8, 1.9 and 2.0, see .travis.yml. For each ruby version, the mapper is tested against SQLite3, PostgreSQL and MySQL.
Only the most basic API
This library only implements the most basic persistence API (mostly just CRUD). Any significant additions will be made into separate gems (like minimapper-extras). The reasons for this are:
- It should be simple to maintain minimapper
- It should be possible to learn all it does in a short time
- It should have a stable API
Installation
Add this line to your application's Gemfile:
gem 'minimapper'
And then execute:
$ bundle
Or install it yourself as:
$ gem install minimapper
You also need the activemodel
gem if you use Minimapper::Entity and not only Minimapper::Entity::Core.
Please avoid installing directly from the github repository. Code will be pushed there that might fail in CI (because testing all permutations of ruby versions and databases locally isn't practical). Gem releases are only done when CI is green.
Usage
Basics
Basics and how we use minimapper in practice.
require "rubygems"
require "minimapper"
require "minimapper/entity"
require "minimapper/mapper"
# app/models/user.rb
class User
include Minimapper::Entity
attributes :name, :email
validates :name, :presence => true
end
# app/mappers/user_mapper.rb
class UserMapper < Minimapper::Mapper
class Record < ActiveRecord::Base
self.table_name = "users"
end
end
## Creating
user = User.new(:name => "Joe")
user_mapper = UserMapper.new
user_mapper.create(user)
## Finding
user = user_mapper.find(user.id)
p user.name # => Joe
p user_mapper.first.name # => Joe
## Updating
user.name = "Joey"
# user.attributes = params[:user]
user_mapper.update(user)
p user_mapper.first.name # => Joey
## Deleting
old_id = user.id
user_mapper.delete(user)
p user.id # => nil
p user_mapper.find_by_id(old_id) # => nil
# user_mapper.find(old_id) # raises ActiveRecord::RecordNotFound
# user_mapper.delete_all
# user_mapper.delete_by_id(1)
## Using a repository
require "minimapper/repository"
# config/initializers/repository.rb
Repository = Minimapper::Repository.build({
:users => UserMapper.new
# :projects => ProjectMapper.new
})
user = User.new(:name => "Joe")
Repository.users.create(user)
p Repository.users.find(user.id).name # => Joe
Repository.users.delete_all
## Using ActiveModel validations
user = User.new
Repository.users.create(user)
p Repository.users.count # => 0
p user.errors.full_messages # Name can't be blank
Eager loading
When using minimapper you don't have lazy loading. We haven't gotten around to adding the association-inclusion syntax yet, but it's quite simple to implement.
Uniqueness validations and other DB validations
Validations on uniqueness can't be implemented on the entity, because they need to access the database.
Therefore, the mapper will copy over any record errors to the entity when attempting to create or update.
Add these validations to the record itself, like:
class User < ActiveRecord::Base
validates :email, :uniqueness => true
end
Note that just calling valid?
on the entity will not access the database. Errors copied over from the record will remain until the next attempt to create or update.
So an entity that wouldn't be unique in the database will be valid?
before you attempt to create it. And after you attempt to create it, the entity will not be valid?
even after assigning a new value, until you attempt to create it again.
Custom queries
You can write custom queries like this:
class ProjectMapper < Minimapper::Mapper
def waiting_for_review
entities_for query_scope.where(waiting_for_review: true).order("id DESC")
end
end
And then use it like this:
# Repository = Minimapper::Repository.build(...)
Repository.projects.waiting_for_review.each do |project|
p project.name
end
record_class
is shorthand for ProjectMapper::Record.
query_scope
is a wrapper around record_class
to allow you to add custom scopes to all queries.
entity_for
returns nil for nil.
entity_for
and entities_for
take an optional second argument if you want a different entity class than the mapper's:
class ProjectMapper < Minimapper::AR
def owner_of(project)
owner_record = find(project).owner
entity_for(owner_record, User)
end
end
Typed attributes and type coercion
If you specify type, Minimapper will only allow values of that type, or strings that can be coerced into that type.
The latter means that it can accept e.g. string integers directly from a form. Minimapper aims to be much less of a form value parser than ActiveRecord, but we'll allow ourselves conveniences like this.
Supported types: Integer and DateTime.
class User
include Minimapper::Entity
attributes [ :profile_id, Integer ]
# Or for single attributes:
# attribute :profile_id, Integer
end
User.new(:profile_id => "10").profile_id # => 10
User.new(:profile_id => " 10 ").profile_id # => 10
User.new(:profile_id => " ").profile_id # => nil
User.new(:profile_id => "foobar").profile_id # => nil
You can add your own type conversions like this:
require "date"
class ToDate
def convert(value)
Date.parse(value) rescue nil
end
end
Minimapper::Entity::Convert.register_converter(:date, ToDate.new)
class User
include Minimapper::Entity
attributes [ :reminder_on, :date ]
end
User.new(:reminder_on => "2012-01-01").reminder # => #<Date: 2012-01-01 ...>
Minimapper only calls #convert on non-empty strings. When the value is blank or nil, the attribute is set to nil.
(FIXME? We're considering changing this so Minimapper core can only enforce type, and there's some Minimapper::FormObject
mixin to parse string values.)
Overriding attribute accessors
Attribute readers and writers are implemented so that you can override them with inheritance:
class User
include Minimapper::Entity
attribute :name
def name
super.upcase
end
def name=(value)
super(value.strip)
end
end
Protected attributes
We recommend using strong_parameters for attribute security, without including ActiveModel::ForbiddenAttributesProtection
.
Use of attr_accessible
or attr_protected
may obstruct the mapper.
If you use Minimapper as intended, you only assign attributes on the entity. Once they're on the entity, the mapper will assume they're permitted to be persisted; and once they're in the record, the mapper will assume they are permitted for populating an entity.
(FIXME?: There's a ongoing discussion about whether Minimapper should actively bypass attribute protection, or encourage you not to use it, or what.)
Associations
There is no core support for associations, but we're implementing them in minimapper-extras as we need them.
For some discussion, see this issue.
Lifecycle hooks
after_find
This is called after any kind of find and can be used for things like loading associated data.
class ProjectMapper < Minimapper::AR
private
def after_find(entity, record)
entity.owner = User.new(record.owner.attributes)
end
end
Deletion
When you do mapper.delete(entity)
, it will use ActiveRecord's delete
, which means that no destroy callbacks or :dependent
association options are honored.
(FIXME?: Should we support destroy
instead or as well?)
Custom entity class
Minimapper::Entity adds some convenience methods for when a model is used within a Rails application. If you don't need that you can just include the core API from the Minimapper::Entity::Core module (or implement your own version that behaves like Minimapper::Entity::Core).
Inspiration
People
Jason Roelofs:
- Designing a Rails App (find the whole series of posts)
Robert "Uncle Bob" Martin:
Apps
- Barsoom has built an app which at the time of writing has 16 tables mapped with minimapper. Most additions to minimapper comes from this app.
Contributing
Running the tests
You need mysql and postgres installed (but they do not have to be running) to be able to run bundle. The mapper tests use sqlite3 by default.
bundle
rake
Steps
- Read "Only the most basic API" above
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Don't forget to write tests
- Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
Credits and license
By Joakim Kolsjö under the MIT license:
Copyright (c) 2012 Joakim Kolsjö
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.