minimapper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/.travis.yml +14 -0
- data/Gemfile +21 -0
- data/LICENSE.txt +22 -0
- data/README.md +133 -0
- data/Rakefile +22 -0
- data/lib/minimapper/ar.rb +95 -0
- data/lib/minimapper/common.rb +5 -0
- data/lib/minimapper/entity.rb +44 -0
- data/lib/minimapper/memory.rb +78 -0
- data/lib/minimapper/repository.rb +29 -0
- data/lib/minimapper/version.rb +3 -0
- data/lib/minimapper.rb +7 -0
- data/minimapper.gemspec +22 -0
- data/script/ci +16 -0
- data/script/turbux_rspec +17 -0
- data/spec/ar_spec.rb +32 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/database_setup.rb +51 -0
- data/spec/support/shared_examples/mapper.rb +223 -0
- data/unit/entity_spec.rb +41 -0
- data/unit/memory_spec.rb +15 -0
- data/unit/repository_spec.rb +40 -0
- data/unit/spec_helper.rb +10 -0
- metadata +123 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm ruby-1.9.3-p0-patched@minimapper --create
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# We don't require active_record to use minimapper, only to
|
4
|
+
# use minimapper/ar. We do require it for the tests though :)
|
5
|
+
gem "activerecord"
|
6
|
+
gem "rspec"
|
7
|
+
|
8
|
+
platforms :ruby do
|
9
|
+
gem "sqlite3"
|
10
|
+
gem "pg"
|
11
|
+
gem "mysql2"
|
12
|
+
end
|
13
|
+
|
14
|
+
platforms :jruby do
|
15
|
+
gem "activerecord-jdbcsqlite3-adapter"
|
16
|
+
gem "activerecord-jdbcpostgresql-adapter"
|
17
|
+
gem "activerecord-jdbcmysql-adapter"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Specify your gem's dependencies in minimapper.gemspec
|
21
|
+
gemspec
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Joakim Kolsjö
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
![Build status](https://secure.travis-ci.org/joakimk/minimapper.png) | [builds](https://travis-ci.org/#!/joakimk/minimapper/builds)
|
2
|
+
|
3
|
+
# Minimapper
|
4
|
+
|
5
|
+
A minimalistic way of separating your models from ORMs like ActiveRecord that allows you to swap out your persistance layer for an in-memory implementation in tests or use different persistance for different models.
|
6
|
+
|
7
|
+
If you're following good style you're probably already pushing all knowledge of your ORM down into your models or model-layer classes. This takes it a step further and let's you work with your models without depending on heavy frameworks like rails or needing a database.
|
8
|
+
|
9
|
+
Minimapper is a partial [repository-pattern](http://martinfowler.com/eaaCatalog/repository.html) implementation (it implements repositories and data mappers but not critera builders).
|
10
|
+
|
11
|
+
## Only the most basic API
|
12
|
+
|
13
|
+
This library only implements the most basic persistance API (mostly just CRUD). Any significant additions will be made into separate gems (with names like "minimapper-FOO").
|
14
|
+
|
15
|
+
The reasons for this are:
|
16
|
+
* You should be able to feel secure about depending on the API
|
17
|
+
* It should be possible to learn all it does in a short time
|
18
|
+
* It should be simple to add an adapter for a new database
|
19
|
+
* It should be simple to maintain minimapper
|
20
|
+
|
21
|
+
## Compatibility
|
22
|
+
|
23
|
+
This gem is tested against all major rubies in both 1.8 and 1.9, see [.travis.yml](https://github.com/joakimk/minimapper/blob/master/.travis.yml). For each ruby version, the SQL mappers are tested against SQLite3, PostgreSQL and MySQL.
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
|
27
|
+
Add this line to your application's Gemfile:
|
28
|
+
|
29
|
+
gem 'minimapper'
|
30
|
+
|
31
|
+
And then execute:
|
32
|
+
|
33
|
+
$ bundle
|
34
|
+
|
35
|
+
Or install it yourself as:
|
36
|
+
|
37
|
+
$ gem install minimapper
|
38
|
+
|
39
|
+
Please avoid installing directly from the github repository. Code will be pushed there that might fail in [CI](https://travis-ci.org/#!/joakimk/minimapper/builds) (because testing all permutations of ruby versions and databases locally isn't practical). Gem releases are only done when CI is green.
|
40
|
+
|
41
|
+
## Usage
|
42
|
+
|
43
|
+
You can use the mappers directly like this:
|
44
|
+
|
45
|
+
``` ruby
|
46
|
+
class User < Minimapper::Entity
|
47
|
+
attributes :name, :email
|
48
|
+
validates :name, presence: true
|
49
|
+
end
|
50
|
+
|
51
|
+
class UserMapper < Minimapper::Memory
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creating
|
55
|
+
user = User.new(name: "Joe")
|
56
|
+
mapper = UserMapper.new
|
57
|
+
mapper.create(user)
|
58
|
+
|
59
|
+
# Finding
|
60
|
+
user = mapper.find(1)
|
61
|
+
|
62
|
+
# Updating
|
63
|
+
user.name = "Joey"
|
64
|
+
mapper.update(user)
|
65
|
+
|
66
|
+
# Deleting
|
67
|
+
mapper.delete(user)
|
68
|
+
|
69
|
+
# Deleting all
|
70
|
+
mapper.delete_all
|
71
|
+
```
|
72
|
+
|
73
|
+
Or though a repository:
|
74
|
+
|
75
|
+
``` ruby
|
76
|
+
repository = Minimapper::Repository.build({
|
77
|
+
users: UserMapper.new,
|
78
|
+
projects: ProjectMapper.new
|
79
|
+
})
|
80
|
+
|
81
|
+
repository.users.find(1)
|
82
|
+
```
|
83
|
+
|
84
|
+
## Using the ActiveRecord mapper
|
85
|
+
|
86
|
+
``` ruby
|
87
|
+
module AR
|
88
|
+
class UserMapper < Minimapper::AR
|
89
|
+
end
|
90
|
+
|
91
|
+
class User < ActiveRecord::Base
|
92
|
+
attr_accessible :name, :email
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
user = User.new(name: "Joe")
|
97
|
+
mapper = AR::UserMapper.new
|
98
|
+
mapper.create(user)
|
99
|
+
```
|
100
|
+
|
101
|
+
## Implementing custom queries
|
102
|
+
|
103
|
+
*todo* show how, talk about shared examples.
|
104
|
+
|
105
|
+
## Implementing another mapper
|
106
|
+
|
107
|
+
*todo*: how to use the shared examples
|
108
|
+
|
109
|
+
## Inspiration
|
110
|
+
|
111
|
+
Jason Roelofs:
|
112
|
+
* [Designing a Rails App](http://jasonroelofs.com/2012/05/29/designing-a-rails-app-part-1/) (find the whole series of posts)
|
113
|
+
|
114
|
+
Robert "Uncle Bob" Martin:
|
115
|
+
* [Architecture: The Lost Years](http://www.confreaks.com/videos/759-rubymidwest2011-keynote-architecture-the-lost-years)
|
116
|
+
* [The Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
117
|
+
|
118
|
+
## Running the tests
|
119
|
+
|
120
|
+
You need mysql and postgres installed (but they do not have to be running) to be able to run bundle. The sql-mapper tests use sqlite3 by default.
|
121
|
+
|
122
|
+
bundle
|
123
|
+
rake
|
124
|
+
|
125
|
+
## Contributing
|
126
|
+
|
127
|
+
0. Read "Only the most basic API" above
|
128
|
+
1. Fork it
|
129
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
130
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
131
|
+
4. Don't forget to write test
|
132
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
133
|
+
6. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
namespace :spec do
|
4
|
+
task :unit do
|
5
|
+
puts "Running unit tests."
|
6
|
+
spec_helper_path = File.expand_path("unit/spec_helper.rb")
|
7
|
+
system("rspec", "-r#{spec_helper_path}", *Dir["unit/**/*_spec.rb"]) || exit(1)
|
8
|
+
end
|
9
|
+
|
10
|
+
task :integrated do
|
11
|
+
puts "Running integrated tests."
|
12
|
+
integrated_helper_path = File.expand_path("spec/spec_helper.rb")
|
13
|
+
system("rspec", "-r#{integrated_helper_path}", *Dir["spec/**/*_spec.rb"]) || exit(1)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
task :spacer do
|
18
|
+
puts
|
19
|
+
end
|
20
|
+
|
21
|
+
task :spec => [ :"spec:unit", :spacer, :"spec:integrated" ]
|
22
|
+
task :default => :spec
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "minimapper/common"
|
2
|
+
|
3
|
+
module Minimapper
|
4
|
+
class AR
|
5
|
+
# Create
|
6
|
+
def create(entity)
|
7
|
+
if entity.valid?
|
8
|
+
entity.id = record_klass.create!(entity.attributes).id
|
9
|
+
else
|
10
|
+
false
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Read
|
15
|
+
def find(id)
|
16
|
+
entity_for(find_record(id))
|
17
|
+
end
|
18
|
+
|
19
|
+
def all
|
20
|
+
record_klass.all.map { |record| entity_klass.new(record.attributes) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def first
|
24
|
+
entity_for(record_klass.order("id ASC").first)
|
25
|
+
end
|
26
|
+
|
27
|
+
def last
|
28
|
+
entity_for(record_klass.order("id ASC").last)
|
29
|
+
end
|
30
|
+
|
31
|
+
def count
|
32
|
+
record_klass.count
|
33
|
+
end
|
34
|
+
|
35
|
+
# Update
|
36
|
+
def update(entity)
|
37
|
+
if entity.valid?
|
38
|
+
record_for(entity).update_attributes!(entity.attributes)
|
39
|
+
true
|
40
|
+
else
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Delete
|
46
|
+
def delete(entity)
|
47
|
+
delete_by_id(entity.id)
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete_by_id(id)
|
51
|
+
find_record(id).delete
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete_all
|
55
|
+
record_klass.delete_all
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# TODO: write tests for these, they are indirectly tested by the fact
|
61
|
+
# that the test suite runs, but there could be bugs and I'll extract minimapper soon.
|
62
|
+
|
63
|
+
# Will attempt to use AR:Project as the record class
|
64
|
+
# when the mapper class name is AR::ProjectMapper
|
65
|
+
def record_klass
|
66
|
+
@record_klass ||= self.class.name.gsub(/Mapper/, '').constantize
|
67
|
+
@record_klass
|
68
|
+
end
|
69
|
+
|
70
|
+
# Will attempt to use Project as the enity class when
|
71
|
+
# the mapper class name is AR::ProjectMapper
|
72
|
+
def entity_klass
|
73
|
+
@entity_klass ||= ("::" + self.class.name.split('::').last.gsub(/Mapper/, '')).constantize
|
74
|
+
@entity_klass
|
75
|
+
end
|
76
|
+
|
77
|
+
def find_record(id)
|
78
|
+
(id && record_klass.find_by_id(id)) ||
|
79
|
+
raise(Common::CanNotFindEntity, :id => id)
|
80
|
+
end
|
81
|
+
|
82
|
+
def record_for(entity)
|
83
|
+
(entity.id && record_klass.find_by_id(entity.id)) ||
|
84
|
+
raise(Common::CanNotFindEntity, entity.inspect)
|
85
|
+
end
|
86
|
+
|
87
|
+
def entity_for(record)
|
88
|
+
if record
|
89
|
+
entity_klass.new(record.attributes)
|
90
|
+
else
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Minimapper does not require you to use this entity base class. It requires a
|
2
|
+
# few methods to be present, like valid?, attributes, attributes=.
|
3
|
+
#
|
4
|
+
# I plan to convert this to a module and add shared examples that cover
|
5
|
+
# the API which minimapper depends upon.
|
6
|
+
#
|
7
|
+
# This class also does some things needed for it to work well with rails.
|
8
|
+
require 'informal'
|
9
|
+
|
10
|
+
module Minimapper
|
11
|
+
class Entity
|
12
|
+
include Informal::Model
|
13
|
+
|
14
|
+
def self.attributes(*list)
|
15
|
+
list.each do |attribute|
|
16
|
+
define_method(attribute) do
|
17
|
+
instance_variable_get("@#{attribute}")
|
18
|
+
end
|
19
|
+
|
20
|
+
define_method("#{attribute}=") do |value|
|
21
|
+
instance_variable_set("@#{attribute}", value)
|
22
|
+
@attributes[attribute] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(*opts)
|
28
|
+
@attributes = {}
|
29
|
+
super(*opts)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_param
|
33
|
+
id
|
34
|
+
end
|
35
|
+
|
36
|
+
def persisted?
|
37
|
+
id
|
38
|
+
end
|
39
|
+
|
40
|
+
attributes :id, :created_at, :updated_at
|
41
|
+
|
42
|
+
attr_reader :attributes
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Minimapper
|
2
|
+
class Memory
|
3
|
+
def initialize
|
4
|
+
@store = []
|
5
|
+
@last_id = 0
|
6
|
+
end
|
7
|
+
|
8
|
+
# Create
|
9
|
+
def create(entity)
|
10
|
+
if entity.valid?
|
11
|
+
entity.id = next_id
|
12
|
+
store.push(entity.dup)
|
13
|
+
last_id
|
14
|
+
else
|
15
|
+
false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Read
|
20
|
+
def find(id)
|
21
|
+
find_internal(id).dup
|
22
|
+
end
|
23
|
+
|
24
|
+
def all
|
25
|
+
store.map { |entity| entity.dup }
|
26
|
+
end
|
27
|
+
|
28
|
+
def first
|
29
|
+
store.first && store.first.dup
|
30
|
+
end
|
31
|
+
|
32
|
+
def last
|
33
|
+
store.last && store.last.dup
|
34
|
+
end
|
35
|
+
|
36
|
+
def count
|
37
|
+
all.size
|
38
|
+
end
|
39
|
+
|
40
|
+
# Update
|
41
|
+
def update(entity)
|
42
|
+
if entity.valid?
|
43
|
+
known_entity = find_internal(entity.id)
|
44
|
+
known_entity.attributes = entity.attributes
|
45
|
+
true
|
46
|
+
else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Delete
|
52
|
+
def delete(entity)
|
53
|
+
delete_by_id(entity.id)
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete_by_id(id)
|
57
|
+
entity = find_internal(id)
|
58
|
+
store.delete(entity)
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete_all
|
62
|
+
store.clear
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def find_internal(id)
|
68
|
+
(id && store.find { |e| e.id == id.to_i }) ||
|
69
|
+
raise(Common::CanNotFindEntity, :id => id)
|
70
|
+
end
|
71
|
+
|
72
|
+
def next_id
|
73
|
+
@last_id += 1
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_reader :store, :last_id
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Minimapper
|
2
|
+
class Repository
|
3
|
+
def self.build(mappers)
|
4
|
+
new(mappers)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(mappers)
|
8
|
+
@mappers = mappers
|
9
|
+
define_mapper_methods
|
10
|
+
end
|
11
|
+
|
12
|
+
def delete_all!
|
13
|
+
mappers.each { |name, instance| instance.delete_all }
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def define_mapper_methods
|
19
|
+
mappers.each do |name, instance|
|
20
|
+
singleton = (class << self; self end)
|
21
|
+
singleton.send(:define_method, name) do # def mapper_name
|
22
|
+
instance # instance
|
23
|
+
end # end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :mappers
|
28
|
+
end
|
29
|
+
end
|
data/lib/minimapper.rb
ADDED
data/minimapper.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'minimapper/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "minimapper"
|
8
|
+
gem.version = Minimapper::VERSION
|
9
|
+
gem.authors = ["Joakim Kolsjö"]
|
10
|
+
gem.email = ["joakim.kolsjo@gmail.com"]
|
11
|
+
gem.description = %q{A minimalistic way of separating your models from ORMs like ActiveRecord (by implementing the repository pattern)}
|
12
|
+
gem.summary = %q{A minimalistic way of separating your models from ORMs like ActiveRecord (by implementing the repository pattern)}
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency "informal"
|
21
|
+
gem.add_dependency "rake"
|
22
|
+
end
|
data/script/ci
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
set -e
|
3
|
+
export CI=true
|
4
|
+
bundle exec rake spec:unit
|
5
|
+
|
6
|
+
echo
|
7
|
+
echo "Using sqlite3:"
|
8
|
+
bundle exec rake spec:integrated
|
9
|
+
|
10
|
+
echo
|
11
|
+
echo "Using postgres:"
|
12
|
+
bundle exec rake spec:integrated DB=postgres
|
13
|
+
|
14
|
+
echo
|
15
|
+
echo "Using mysql:"
|
16
|
+
bundle exec rake spec:integrated DB=mysql
|
data/script/turbux_rspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Command to run the specs the correct way when triggered from the turbux vim plugin.
|
4
|
+
|
5
|
+
rails_spec = ARGV.first.start_with?("spec/") || ARGV.first.include?('/spec/')
|
6
|
+
|
7
|
+
if rails_spec
|
8
|
+
command = "rspec"
|
9
|
+
else
|
10
|
+
spec_helper_path = File.expand_path("unit/spec_helper.rb")
|
11
|
+
|
12
|
+
# Don't load bundler for non-rails unit tests
|
13
|
+
command = "RUBYOPT='' rspec -r#{spec_helper_path} --color --tty"
|
14
|
+
end
|
15
|
+
|
16
|
+
command += " #{ARGV.first}"
|
17
|
+
exit(1) unless system(command)
|
data/spec/ar_spec.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "minimapper/entity"
|
3
|
+
require "minimapper/ar"
|
4
|
+
|
5
|
+
class TestEntity < Minimapper::Entity
|
6
|
+
attributes :name, :github_url
|
7
|
+
validates :name, :presence => true
|
8
|
+
end
|
9
|
+
|
10
|
+
class TestMapper < Minimapper::AR
|
11
|
+
private
|
12
|
+
|
13
|
+
def entity_klass
|
14
|
+
TestEntity
|
15
|
+
end
|
16
|
+
|
17
|
+
def record_klass
|
18
|
+
Record
|
19
|
+
end
|
20
|
+
|
21
|
+
class Record < ActiveRecord::Base
|
22
|
+
attr_accessible :name
|
23
|
+
self.table_name = :projects
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe Minimapper::AR do
|
28
|
+
let(:repository) { TestMapper.new }
|
29
|
+
let(:entity_klass) { TestEntity }
|
30
|
+
|
31
|
+
include_examples :mapper
|
32
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require "minimapper"
|
3
|
+
|
4
|
+
ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..")) unless defined?(ROOT)
|
5
|
+
Dir[File.join(ROOT, "spec/support/shared_examples/*.rb")].each { |f| require f }
|
6
|
+
|
7
|
+
require File.join(ROOT, "spec/support/database_setup")
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.before(:each) do
|
11
|
+
ActiveRecord::Base.connection.execute "DELETE FROM projects;"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class DB
|
2
|
+
POSTGRES_USERNAME = ENV['CI'] ? 'postgres' : ENV['USER']
|
3
|
+
|
4
|
+
def use_sqlite3
|
5
|
+
if jruby?
|
6
|
+
connect :adapter => "jdbcsqlite3", :database => ":memory:"
|
7
|
+
else
|
8
|
+
connect :adapter => "sqlite3", :database => ":memory:"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def use_postgres
|
13
|
+
system "psql -c 'create database minimapper_test;' -d postgres -U #{POSTGRES_USERNAME} 2> /dev/null"
|
14
|
+
connect :adapter => "postgresql", :database => "minimapper_test",
|
15
|
+
:username => POSTGRES_USERNAME
|
16
|
+
end
|
17
|
+
|
18
|
+
def use_mysql
|
19
|
+
system "mysql -e 'create database minimapper_test;' 2> /dev/null"
|
20
|
+
adapter = jruby? ? "jdbcmysql" : "mysql2"
|
21
|
+
connect :adapter => adapter, :database => "minimapper_test",
|
22
|
+
:username => "root"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def jruby?
|
28
|
+
RUBY_PLATFORM == "java"
|
29
|
+
end
|
30
|
+
|
31
|
+
def connect(opts)
|
32
|
+
ActiveRecord::Base.establish_connection(opts)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
case ENV['DB']
|
37
|
+
when 'postgres'
|
38
|
+
DB.new.use_postgres
|
39
|
+
when 'mysql'
|
40
|
+
DB.new.use_mysql
|
41
|
+
else
|
42
|
+
DB.new.use_sqlite3
|
43
|
+
end
|
44
|
+
|
45
|
+
silence_stream(STDOUT) do
|
46
|
+
ActiveRecord::Schema.define(:version => 0) do
|
47
|
+
create_table :projects, :force => true do |t|
|
48
|
+
t.string :name
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
shared_examples :mapper do
|
2
|
+
# expects repository and entity_klass to be defined
|
3
|
+
|
4
|
+
describe "create" do
|
5
|
+
it "sets an id on the entity" do
|
6
|
+
entity1 = build_valid_entity
|
7
|
+
entity1.id.should be_nil
|
8
|
+
repository.create(entity1)
|
9
|
+
entity1.id.should > 0
|
10
|
+
|
11
|
+
entity2 = build_valid_entity
|
12
|
+
repository.create(entity2)
|
13
|
+
entity2.id.should == entity1.id + 1
|
14
|
+
end
|
15
|
+
|
16
|
+
it "returns the id" do
|
17
|
+
id = repository.create(build_valid_entity)
|
18
|
+
id.should be_kind_of(Fixnum)
|
19
|
+
id.should > 0
|
20
|
+
end
|
21
|
+
|
22
|
+
it "does not store by reference" do
|
23
|
+
entity = build_valid_entity
|
24
|
+
repository.create(entity)
|
25
|
+
repository.last.object_id.should_not == entity.object_id
|
26
|
+
repository.last.name.should == "test"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "validates the record before saving" do
|
30
|
+
entity = entity_klass.new
|
31
|
+
repository.create(entity).should be_false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "find" do
|
36
|
+
it "returns an entity matching the id" do
|
37
|
+
entity = build_valid_entity
|
38
|
+
repository.create(entity)
|
39
|
+
found_entity = repository.find(entity.id)
|
40
|
+
found_entity.name.should == "test"
|
41
|
+
found_entity.id.should == entity.id
|
42
|
+
found_entity.should be_kind_of(Minimapper::Entity)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "supports string ids" do
|
46
|
+
entity = build_valid_entity
|
47
|
+
repository.create(entity)
|
48
|
+
repository.find(entity.id.to_s)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "does not return the same instance" do
|
52
|
+
entity = build_valid_entity
|
53
|
+
repository.create(entity)
|
54
|
+
repository.find(entity.id).object_id.should_not == entity.object_id
|
55
|
+
repository.find(entity.id).object_id.should_not == repository.find(entity.id).object_id
|
56
|
+
end
|
57
|
+
|
58
|
+
it "fails when the an entity can not be found" do
|
59
|
+
lambda { repository.find(-1) }.should raise_error(Minimapper::Common::CanNotFindEntity)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "all" do
|
64
|
+
it "returns all entities in undefined order" do
|
65
|
+
first_created_entity = build_valid_entity
|
66
|
+
second_created_entity = build_valid_entity
|
67
|
+
repository.create(first_created_entity)
|
68
|
+
repository.create(second_created_entity)
|
69
|
+
all_entities = repository.all
|
70
|
+
all_entities.map(&:id).should include(first_created_entity.id)
|
71
|
+
all_entities.map(&:id).should include(second_created_entity.id)
|
72
|
+
all_entities.first.should be_kind_of(Minimapper::Entity)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "does not return the same instances" do
|
76
|
+
entity = build_valid_entity
|
77
|
+
repository.create(entity)
|
78
|
+
repository.all.first.object_id.should_not == entity.object_id
|
79
|
+
repository.all.first.object_id.should_not == repository.all.first.object_id
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "first" do
|
84
|
+
it "returns the first entity" do
|
85
|
+
first_created_entity = build_valid_entity
|
86
|
+
repository.create(first_created_entity)
|
87
|
+
repository.create(build_valid_entity)
|
88
|
+
repository.first.id.should == first_created_entity.id
|
89
|
+
repository.first.should be_kind_of(entity_klass)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "does not return the same instance" do
|
93
|
+
entity = build_valid_entity
|
94
|
+
repository.create(entity)
|
95
|
+
repository.first.object_id.should_not == entity.object_id
|
96
|
+
repository.first.object_id.should_not == repository.first.object_id
|
97
|
+
end
|
98
|
+
|
99
|
+
it "returns nil when there is no entity" do
|
100
|
+
repository.first.should be_nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "last" do
|
105
|
+
it "returns the last entity" do
|
106
|
+
last_created_entity = build_valid_entity
|
107
|
+
repository.create(build_valid_entity)
|
108
|
+
repository.create(last_created_entity)
|
109
|
+
repository.last.id.should == last_created_entity.id
|
110
|
+
repository.last.should be_kind_of(entity_klass)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "does not return the same instance" do
|
114
|
+
entity = build_valid_entity
|
115
|
+
repository.create(entity)
|
116
|
+
repository.last.object_id.should_not == entity.object_id
|
117
|
+
repository.last.object_id.should_not == repository.last.object_id
|
118
|
+
end
|
119
|
+
|
120
|
+
it "returns nil when there is no entity" do
|
121
|
+
repository.last.should be_nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "count" do
|
126
|
+
it "returns the number of entities" do
|
127
|
+
repository.create(build_valid_entity)
|
128
|
+
repository.create(build_valid_entity)
|
129
|
+
repository.count.should == 2
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "update" do
|
134
|
+
it "updates" do
|
135
|
+
entity = build_valid_entity
|
136
|
+
repository.create(entity)
|
137
|
+
|
138
|
+
entity.name = "Updated"
|
139
|
+
repository.last.name.should == "test"
|
140
|
+
|
141
|
+
repository.update(entity)
|
142
|
+
repository.last.id.should == entity.id
|
143
|
+
repository.last.name.should == "Updated"
|
144
|
+
end
|
145
|
+
|
146
|
+
it "does not update and returns false when the entity isn't valid" do
|
147
|
+
entity = build_valid_entity
|
148
|
+
repository.create(entity)
|
149
|
+
entity.name = nil
|
150
|
+
|
151
|
+
repository.update(entity).should be_false
|
152
|
+
repository.last.name.should == "test"
|
153
|
+
end
|
154
|
+
|
155
|
+
it "returns true" do
|
156
|
+
entity = build_valid_entity
|
157
|
+
repository.create(entity)
|
158
|
+
repository.update(entity).should == true
|
159
|
+
end
|
160
|
+
|
161
|
+
it "fails when the entity does not have an id" do
|
162
|
+
entity = build_valid_entity
|
163
|
+
lambda { repository.update(entity) }.should raise_error(Minimapper::Common::CanNotFindEntity)
|
164
|
+
end
|
165
|
+
|
166
|
+
it "fails when the entity no longer exists" do
|
167
|
+
entity = build_valid_entity
|
168
|
+
repository.create(entity)
|
169
|
+
repository.delete_all
|
170
|
+
lambda { repository.update(entity) }.should raise_error(Minimapper::Common::CanNotFindEntity)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe "delete" do
|
175
|
+
it "removes the entity" do
|
176
|
+
entity = build_valid_entity
|
177
|
+
repository.create(entity)
|
178
|
+
repository.create(build_valid_entity)
|
179
|
+
repository.delete(entity)
|
180
|
+
repository.all.size.should == 1
|
181
|
+
repository.first.id.should_not == entity.id
|
182
|
+
end
|
183
|
+
|
184
|
+
it "fails when the entity does not have an id" do
|
185
|
+
entity = entity_klass.new
|
186
|
+
lambda { repository.delete(entity) }.should raise_error(Minimapper::Common::CanNotFindEntity)
|
187
|
+
end
|
188
|
+
|
189
|
+
it "fails when the entity can not be found" do
|
190
|
+
entity = entity_klass.new(:id => -1)
|
191
|
+
lambda { repository.delete(entity) }.should raise_error(Minimapper::Common::CanNotFindEntity)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe "delete_by_id" do
|
196
|
+
it "removes the entity" do
|
197
|
+
entity = build_valid_entity
|
198
|
+
repository.create(entity)
|
199
|
+
repository.create(build_valid_entity)
|
200
|
+
repository.delete_by_id(entity.id)
|
201
|
+
repository.all.size.should == 1
|
202
|
+
repository.first.id.should_not == entity.id
|
203
|
+
end
|
204
|
+
|
205
|
+
it "fails when the an entity can not be found" do
|
206
|
+
lambda { repository.delete_by_id(-1) }.should raise_error(Minimapper::Common::CanNotFindEntity)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe "delete_all" do
|
211
|
+
it "empties the repository" do
|
212
|
+
repository.create(build_valid_entity)
|
213
|
+
repository.delete_all
|
214
|
+
repository.all.should == []
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def build_valid_entity
|
221
|
+
entity_klass.new(:name => 'test')
|
222
|
+
end
|
223
|
+
end
|
data/unit/entity_spec.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'minimapper/entity'
|
2
|
+
|
3
|
+
describe Minimapper::Entity do
|
4
|
+
it "handles base attributes" do
|
5
|
+
base = described_class.new
|
6
|
+
base.id = 5
|
7
|
+
base.id.should == 5
|
8
|
+
|
9
|
+
time = Time.now
|
10
|
+
base.created_at = time
|
11
|
+
base.created_at.should == time
|
12
|
+
|
13
|
+
base.updated_at = time
|
14
|
+
base.updated_at.should == time
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe Minimapper::Entity, "attributes" do
|
19
|
+
it "returns the attributes" do
|
20
|
+
base = described_class.new(:id => 5)
|
21
|
+
time = Time.now
|
22
|
+
base.created_at = time
|
23
|
+
base.attributes.should == { :id => 5, :created_at => time }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe Minimapper::Entity, "to_param" do
|
28
|
+
it "responds with the id to be compatible with rails link helpers" do
|
29
|
+
base = described_class.new(:id => 5)
|
30
|
+
base.to_param.should == 5
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe Minimapper::Entity, "persisted?" do
|
35
|
+
it "responds true when there is an id (to be compatible with rails form helpers)" do
|
36
|
+
base = described_class.new
|
37
|
+
base.should_not be_persisted
|
38
|
+
base.id = 5
|
39
|
+
base.should be_persisted
|
40
|
+
end
|
41
|
+
end
|
data/unit/memory_spec.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'minimapper/memory'
|
2
|
+
require 'minimapper/common'
|
3
|
+
require 'minimapper/entity'
|
4
|
+
|
5
|
+
class TestEntity < Minimapper::Entity
|
6
|
+
attributes :name
|
7
|
+
validates :name, :presence => true
|
8
|
+
end
|
9
|
+
|
10
|
+
describe Minimapper::Memory do
|
11
|
+
let(:repository) { described_class.new }
|
12
|
+
let(:entity_klass) { TestEntity }
|
13
|
+
|
14
|
+
include_examples :mapper
|
15
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'minimapper/repository'
|
2
|
+
require 'minimapper/memory'
|
3
|
+
|
4
|
+
module Test
|
5
|
+
class ProjectMapper < Minimapper::Memory
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Minimapper::Repository, "self.build" do
|
10
|
+
it "builds a repository" do
|
11
|
+
repository = described_class.build(:projects => Test::ProjectMapper.new)
|
12
|
+
repository.should be_instance_of(Minimapper::Repository)
|
13
|
+
repository.projects.should be_instance_of(Test::ProjectMapper)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "memoizes the mappers" do
|
17
|
+
repository = described_class.build(:projects => Test::ProjectMapper.new)
|
18
|
+
repository.projects.object_id.should == repository.projects.object_id
|
19
|
+
end
|
20
|
+
|
21
|
+
it "does not leak between instances" do
|
22
|
+
repository1 = described_class.build(:projects => :foo)
|
23
|
+
repository2 = described_class.build(:projects => :bar)
|
24
|
+
repository1.projects.should == :foo
|
25
|
+
repository2.projects.should == :bar
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe Minimapper::Repository, "#delete_all!" do
|
30
|
+
it "removes all records by calling delete_all on all mappers" do
|
31
|
+
project_mapper = mock
|
32
|
+
user_mapper = mock
|
33
|
+
|
34
|
+
project_mapper.should_receive(:delete_all)
|
35
|
+
user_mapper.should_receive(:delete_all)
|
36
|
+
|
37
|
+
repository2 = described_class.build(:projects => project_mapper, :users => user_mapper)
|
38
|
+
repository2.delete_all!
|
39
|
+
end
|
40
|
+
end
|
data/unit/spec_helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
ROOT = File.expand_path(File.join(File.dirname(__FILE__), ".."))
|
2
|
+
$: << File.join(ROOT, "lib")
|
3
|
+
$: << File.join(ROOT, "unit")
|
4
|
+
|
5
|
+
require "minimapper"
|
6
|
+
|
7
|
+
Dir[File.join(ROOT, "spec/support/shared_examples/*.rb")].each { |f| require f }
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: minimapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- "Joakim Kolsj\xC3\xB6"
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-10-14 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: informal
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rake
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
description: A minimalistic way of separating your models from ORMs like ActiveRecord (by implementing the repository pattern)
|
50
|
+
email:
|
51
|
+
- joakim.kolsjo@gmail.com
|
52
|
+
executables: []
|
53
|
+
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- .gitignore
|
60
|
+
- .rspec
|
61
|
+
- .rvmrc
|
62
|
+
- .travis.yml
|
63
|
+
- Gemfile
|
64
|
+
- LICENSE.txt
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- lib/minimapper.rb
|
68
|
+
- lib/minimapper/ar.rb
|
69
|
+
- lib/minimapper/common.rb
|
70
|
+
- lib/minimapper/entity.rb
|
71
|
+
- lib/minimapper/memory.rb
|
72
|
+
- lib/minimapper/repository.rb
|
73
|
+
- lib/minimapper/version.rb
|
74
|
+
- minimapper.gemspec
|
75
|
+
- script/ci
|
76
|
+
- script/turbux_rspec
|
77
|
+
- spec/ar_spec.rb
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
- spec/support/database_setup.rb
|
80
|
+
- spec/support/shared_examples/mapper.rb
|
81
|
+
- unit/entity_spec.rb
|
82
|
+
- unit/memory_spec.rb
|
83
|
+
- unit/repository_spec.rb
|
84
|
+
- unit/spec_helper.rb
|
85
|
+
has_rdoc: true
|
86
|
+
homepage: ""
|
87
|
+
licenses: []
|
88
|
+
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
hash: 3
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
hash: 3
|
109
|
+
segments:
|
110
|
+
- 0
|
111
|
+
version: "0"
|
112
|
+
requirements: []
|
113
|
+
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 1.5.3
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: A minimalistic way of separating your models from ORMs like ActiveRecord (by implementing the repository pattern)
|
119
|
+
test_files:
|
120
|
+
- spec/ar_spec.rb
|
121
|
+
- spec/spec_helper.rb
|
122
|
+
- spec/support/database_setup.rb
|
123
|
+
- spec/support/shared_examples/mapper.rb
|