minimapper 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
 | [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
|