minimapper 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +41 -84
- data/lib/minimapper/mapper.rb +150 -0
- data/lib/minimapper/version.rb +1 -1
- data/minimapper.gemspec +2 -2
- data/spec/{mapper/ar_spec.rb → mapper_spec.rb} +14 -22
- data/unit/entity/convert_spec.rb +27 -20
- data/unit/repository_spec.rb +2 -2
- metadata +11 -14
- data/lib/minimapper/mapper/ar.rb +0 -155
- data/lib/minimapper/mapper/common.rb +0 -9
- data/lib/minimapper/mapper/memory.rb +0 -101
- data/unit/mapper/memory_spec.rb +0 -13
data/README.md
CHANGED
@@ -7,28 +7,30 @@
|
|
7
7
|
|
8
8
|
### Introduction
|
9
9
|
|
10
|
-
Minimapper is a minimalistic way of separating models from
|
10
|
+
Minimapper is a minimalistic way of [separating models](http://martinfowler.com/eaaCatalog/dataMapper.html) from ActiveRecord. It enables you to test your models (and code using your models) within a [sub-second unit test suite](https://github.com/joakimk/fast_unit_tests_example) and makes it simpler to have a modular design as described in [Matt Wynne's Hexagonal Rails posts](http://blog.mattwynne.net/2012/04/09/hexagonal-rails-introduction/).
|
11
11
|
|
12
|
-
Minimapper
|
13
|
-
|
14
|
-
Minimapper is not an ORM, instead it's a tool to make it simpler to handle persistence in existing applications using ORMs like ActiveRecord. It may also be an attractive alternative to using DataMapper 2 (when it's done) for new apps if you already know ActiveRecord well (most of the rails developers I know have many years of experience with ActiveRecord).
|
12
|
+
Minimapper follows many Rails conventions but it does not require Rails.
|
15
13
|
|
16
14
|
### Early days
|
17
15
|
|
18
16
|
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.
|
19
17
|
|
18
|
+
### Important resources
|
19
|
+
|
20
|
+
- [minimapper-extras](https://github.com/barsoom/minimapper-extras) (useful tools for projects using minimapper)
|
21
|
+
- [Gist of yet to be extracted mapper code](https://gist.github.com/joakimk/5656945) from a project using minimapper.
|
22
|
+
|
20
23
|
### Compatibility
|
21
24
|
|
22
|
-
This gem is tested against all major rubies in 1.8, 1.9 and 2.0, see [.travis.yml](https://github.com/joakimk/minimapper/blob/master/.travis.yml). For each ruby version, the
|
25
|
+
This gem is tested against all major rubies in 1.8, 1.9 and 2.0, see [.travis.yml](https://github.com/joakimk/minimapper/blob/master/.travis.yml). For each ruby version, the mapper is tested against SQLite3, PostgreSQL and MySQL.
|
23
26
|
|
24
27
|
### Only the most basic API
|
25
28
|
|
26
29
|
This library only implements the most basic persistence API (mostly just CRUD). Any significant additions will be made into separate gems (like [minimapper-extras](https://github.com/barsoom/minimapper-extras)). The reasons for this are:
|
27
30
|
|
28
|
-
* It should have a stable API
|
29
|
-
* It should be possible to learn all it does in a short time
|
30
|
-
* It should be simple to add an adapter for a new database
|
31
31
|
* It should be simple to maintain minimapper
|
32
|
+
* It should be possible to learn all it does in a short time
|
33
|
+
* It should have a stable API
|
32
34
|
|
33
35
|
## Installation
|
34
36
|
|
@@ -52,15 +54,15 @@ Please avoid installing directly from the github repository. Code will be pushed
|
|
52
54
|
|
53
55
|
### Basics
|
54
56
|
|
55
|
-
|
57
|
+
Basics and how we use minimapper in practice.
|
56
58
|
|
57
59
|
``` ruby
|
58
|
-
# minimapper_example.rb
|
59
60
|
require "rubygems"
|
60
61
|
require "minimapper"
|
61
62
|
require "minimapper/entity"
|
62
|
-
require "minimapper/mapper
|
63
|
+
require "minimapper/mapper"
|
63
64
|
|
65
|
+
# app/models/user.rb
|
64
66
|
class User
|
65
67
|
include Minimapper::Entity
|
66
68
|
|
@@ -68,7 +70,11 @@ class User
|
|
68
70
|
validates :name, :presence => true
|
69
71
|
end
|
70
72
|
|
71
|
-
|
73
|
+
# app/mappers/user_mapper.rb
|
74
|
+
class UserMapper < Minimapper::Mapper
|
75
|
+
class Record < ActiveRecord::Base
|
76
|
+
self.table_name = "users"
|
77
|
+
end
|
72
78
|
end
|
73
79
|
|
74
80
|
## Creating
|
@@ -83,6 +89,7 @@ p user_mapper.first.name # => Joe
|
|
83
89
|
|
84
90
|
## Updating
|
85
91
|
user.name = "Joey"
|
92
|
+
# user.attributes = params[:user]
|
86
93
|
user_mapper.update(user)
|
87
94
|
p user_mapper.first.name # => Joey
|
88
95
|
|
@@ -98,51 +105,33 @@ p user_mapper.find_by_id(old_id) # => nil
|
|
98
105
|
## Using a repository
|
99
106
|
require "minimapper/repository"
|
100
107
|
|
101
|
-
repository
|
108
|
+
# config/initializers/repository.rb
|
109
|
+
Repository = Minimapper::Repository.build({
|
102
110
|
:users => UserMapper.new
|
103
111
|
# :projects => ProjectMapper.new
|
104
112
|
})
|
105
113
|
|
106
114
|
user = User.new(:name => "Joe")
|
107
|
-
|
115
|
+
Repository.users.create(user)
|
108
116
|
p repository.users.find(user.id).name # => Joe
|
109
|
-
|
117
|
+
Repository.users.delete_all
|
110
118
|
|
111
119
|
## Using ActiveModel validations
|
112
120
|
user = User.new
|
113
|
-
|
114
|
-
p
|
121
|
+
Repository.users.create(user)
|
122
|
+
p Repository.users.count # => 0
|
115
123
|
p user.errors.full_messages # Name can't be blank
|
116
124
|
```
|
117
125
|
|
118
|
-
###
|
126
|
+
### Loading all the data you need before acting on it
|
119
127
|
|
120
|
-
|
121
|
-
|
122
|
-
When you do need to use an ORM like ActiveRecord however, it now has the same API as your in-memory persistence (thanks to the [shared tests](https://github.com/joakimk/minimapper/blob/master/spec/support/shared_examples/mapper.rb) which define how a mapper is supposed to behave).
|
123
|
-
|
124
|
-
``` ruby
|
125
|
-
require "minimapper/mapper/ar"
|
126
|
-
|
127
|
-
module AR
|
128
|
-
class UserMapper < Minimapper::AR
|
129
|
-
end
|
130
|
-
|
131
|
-
class User < ActiveRecord::Base
|
132
|
-
attr_accessible :name, :email
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
user = User.new(name: "Joe")
|
137
|
-
mapper = AR::UserMapper.new
|
138
|
-
mapper.create(user)
|
139
|
-
```
|
128
|
+
When using minimapper you generally want to load all data you need upfront whenever possible as you don't have lazy loading. We haven't gotten around to adding the inclusion syntax yet, but [it's quite simple to implement](https://gist.github.com/joakimk/5656945).
|
140
129
|
|
141
130
|
### Uniqueness validations and other DB validations
|
142
131
|
|
143
132
|
Validations on uniqueness can't be implemented on the entity, because they need to access the database.
|
144
133
|
|
145
|
-
Therefore, the
|
134
|
+
Therefore, the mapper will copy over any record errors to the entity when attempting to create or update.
|
146
135
|
|
147
136
|
Add these validations to the record itself, like:
|
148
137
|
|
@@ -161,21 +150,9 @@ So an entity that wouldn't be unique in the database will be `valid?` before you
|
|
161
150
|
You can write custom queries like this:
|
162
151
|
|
163
152
|
``` ruby
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
def waiting_for_review
|
168
|
-
all.find_all { |p| p.waiting_for_review }.sort_by(&:id).reverse
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
# ActiveRecord implementation
|
174
|
-
module AR
|
175
|
-
class ProjectMapper < Minimapper::AR
|
176
|
-
def waiting_for_review
|
177
|
-
entities_for record_class.where(waiting_for_review: true).order("id DESC")
|
178
|
-
end
|
153
|
+
class ProjectMapper < Minimapper::AR
|
154
|
+
def waiting_for_review
|
155
|
+
entities_for record_class.where(waiting_for_review: true).order("id DESC")
|
179
156
|
end
|
180
157
|
end
|
181
158
|
```
|
@@ -183,14 +160,12 @@ end
|
|
183
160
|
And then use it like this:
|
184
161
|
|
185
162
|
``` ruby
|
186
|
-
#
|
187
|
-
|
163
|
+
# Repository = Minimapper::Repository.build(...)
|
164
|
+
Repository.projects.waiting_for_review.each do |project|
|
188
165
|
p project.name
|
189
166
|
end
|
190
167
|
```
|
191
168
|
|
192
|
-
It gets simpler to maintain if you use shared tests to test both implementations. For inspiration, see the [shared tests](https://github.com/joakimk/minimapper/blob/master/spec/support/shared_examples/mapper.rb) used to test minimapper.
|
193
|
-
|
194
169
|
`entity_for` returns nil for nil.
|
195
170
|
|
196
171
|
It takes an optional second argument if you want a different entity class than the mapper's:
|
@@ -204,9 +179,14 @@ class ProjectMapper < Minimapper::AR
|
|
204
179
|
end
|
205
180
|
```
|
206
181
|
|
207
|
-
### Typed attributes
|
182
|
+
### Typed attributes and type coercion
|
183
|
+
|
184
|
+
If you specify type, Minimapper will only allow values of that type, or strings that can be coerced into that type.
|
208
185
|
|
209
|
-
|
186
|
+
The latter means that it can accept e.g. string integers directly from a form.
|
187
|
+
Minimapper aims to be much less of a form value parser than ActiveRecord, but we'll allow ourselves conveniences like this.
|
188
|
+
|
189
|
+
Supported types: Integer and DateTime (`:integer` and `:date_time`).
|
210
190
|
|
211
191
|
``` ruby
|
212
192
|
class User
|
@@ -303,17 +283,6 @@ end
|
|
303
283
|
|
304
284
|
[Minimapper::Entity](https://github.com/joakimk/minimapper/blob/master/lib/minimapper/entity.rb) 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](https://github.com/joakimk/minimapper/blob/master/lib/minimapper/entity/core.rb) module (or implement your own version that behaves like [Minimapper::Entity::Core](https://github.com/joakimk/minimapper/blob/master/lib/minimapper/entity/core.rb)).
|
305
285
|
|
306
|
-
### Adding a new mapper
|
307
|
-
|
308
|
-
If you were to add a [Mongoid](http://mongoid.org/en/mongoid/index.html) mapper:
|
309
|
-
|
310
|
-
1. Start by copying *spec/ar_spec.rb* to *spec/mongoid_spec.rb* and adapt it for Mongoid.
|
311
|
-
2. Add any setup code needed in *spec/support/database_setup.rb*.
|
312
|
-
3. Get the [shared tests](https://github.com/joakimk/minimapper/blob/master/spec/support/shared_examples/mapper.rb) to pass for *spec/mongoid_spec.rb*.
|
313
|
-
4. Ensure all other tests pass.
|
314
|
-
5. Send a pull request.
|
315
|
-
6. As soon as it can be made to work in travis in all ruby versions that apply (in Mongoid's case that is only the 1.9 rubies), I'll merge it in.
|
316
|
-
|
317
286
|
## Inspiration
|
318
287
|
|
319
288
|
### People
|
@@ -336,7 +305,7 @@ Robert "Uncle Bob" Martin:
|
|
336
305
|
|
337
306
|
### Running the tests
|
338
307
|
|
339
|
-
You need mysql and postgres installed (but they do not have to be running) to be able to run bundle. The
|
308
|
+
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.
|
340
309
|
|
341
310
|
bundle
|
342
311
|
rake
|
@@ -355,19 +324,7 @@ You need mysql and postgres installed (but they do not have to be running) to be
|
|
355
324
|
|
356
325
|
### Next
|
357
326
|
|
358
|
-
* Make it possible to override minimapper attributes with super (like it's done in https://github.com/barsoom/traco)
|
359
327
|
* Support default values for attributes (probably only using lambdas to avoid bugs).
|
360
|
-
* Built in way to set induvidual attributes in a way that bypasses protected attributes like you can do with an AR model.
|
361
|
-
- user.is_admin = true; user_mapper.update(user) should probably set is_admin to true, mass-assignment should not.
|
362
|
-
* Extract entity and model class lookup code from the ar-mapper and reuse it in the memory mapper.
|
363
|
-
* Change the memory mapper to store entity attributes, not entity instances.
|
364
|
-
- Unless this makes it difficult to handle associated data.
|
365
|
-
|
366
|
-
### Ideas
|
367
|
-
|
368
|
-
I won't implement anything that isn't actually used. But here are some ideas for things that might make it into minimapper someday if there is a need for it.
|
369
|
-
|
370
|
-
* Provide a hook to convert attributes between entities and the backing models (when your entity attributes and db-schema isn't a one-to-one match).
|
371
328
|
|
372
329
|
## Credits and license
|
373
330
|
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Minimapper
|
2
|
+
EntityNotFound = Class.new(StandardError)
|
3
|
+
|
4
|
+
class Mapper
|
5
|
+
attr_accessor :repository
|
6
|
+
|
7
|
+
# Create
|
8
|
+
|
9
|
+
def create(entity)
|
10
|
+
record = record_class.new
|
11
|
+
|
12
|
+
copy_attributes_to_record(record, entity)
|
13
|
+
validate_record_and_copy_errors_to_entity(record, entity)
|
14
|
+
|
15
|
+
if entity.valid?
|
16
|
+
record.save!
|
17
|
+
entity.id = record.id
|
18
|
+
entity.id
|
19
|
+
else
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Read
|
25
|
+
|
26
|
+
def find(id)
|
27
|
+
entity_for(find_record_safely(id))
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_by_id(id)
|
31
|
+
entity_for(find_record(id))
|
32
|
+
end
|
33
|
+
|
34
|
+
def all
|
35
|
+
entities_for record_class.all
|
36
|
+
end
|
37
|
+
|
38
|
+
def first
|
39
|
+
entity_for(record_class.order("id ASC").first)
|
40
|
+
end
|
41
|
+
|
42
|
+
def last
|
43
|
+
entity_for(record_class.order("id ASC").last)
|
44
|
+
end
|
45
|
+
|
46
|
+
def count
|
47
|
+
record_class.count
|
48
|
+
end
|
49
|
+
|
50
|
+
# Update
|
51
|
+
|
52
|
+
def update(entity)
|
53
|
+
record = record_for(entity)
|
54
|
+
|
55
|
+
copy_attributes_to_record(record, entity)
|
56
|
+
validate_record_and_copy_errors_to_entity(record, entity)
|
57
|
+
|
58
|
+
if entity.valid?
|
59
|
+
record.save!
|
60
|
+
true
|
61
|
+
else
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Delete
|
67
|
+
|
68
|
+
def delete(entity)
|
69
|
+
delete_by_id(entity.id)
|
70
|
+
entity.id = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def delete_by_id(id)
|
74
|
+
find_record_safely(id).delete
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete_all
|
78
|
+
record_class.delete_all
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# NOTE: Don't memoize the record_class or code reloading will break in rails apps.
|
84
|
+
def record_class
|
85
|
+
"#{self.class.name}::Record".constantize
|
86
|
+
end
|
87
|
+
|
88
|
+
# Will attempt to use Project as the entity class when
|
89
|
+
# the mapper class name is ProjectMapper.
|
90
|
+
def entity_class
|
91
|
+
self.class.name.sub(/Mapper$/, '').constantize
|
92
|
+
end
|
93
|
+
|
94
|
+
def accessible_attributes(entity)
|
95
|
+
entity.attributes.reject { |k, v| protected_attributes.include?(k.to_s) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def protected_attributes
|
99
|
+
record_class.protected_attributes
|
100
|
+
end
|
101
|
+
|
102
|
+
def copy_attributes_to_record(record, entity)
|
103
|
+
record.attributes = accessible_attributes(entity)
|
104
|
+
end
|
105
|
+
|
106
|
+
def validate_record_and_copy_errors_to_entity(record, entity)
|
107
|
+
record.valid?
|
108
|
+
entity.mapper_errors = record.errors.map { |k, v| [k, v] }
|
109
|
+
end
|
110
|
+
|
111
|
+
def find_record_safely(id)
|
112
|
+
find_record(id) ||
|
113
|
+
raise(EntityNotFound, :id => id)
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_record(id)
|
117
|
+
id && record_class.find_by_id(id)
|
118
|
+
end
|
119
|
+
|
120
|
+
def record_for(entity)
|
121
|
+
(entity.id && record_class.find_by_id(entity.id)) ||
|
122
|
+
raise(EntityNotFound, entity.inspect)
|
123
|
+
end
|
124
|
+
|
125
|
+
def entities_for(records)
|
126
|
+
records.map { |record| entity_for(record) }
|
127
|
+
end
|
128
|
+
|
129
|
+
def entity_for(record, klass = entity_class)
|
130
|
+
if record
|
131
|
+
entity = klass.new
|
132
|
+
entity.id = record.id
|
133
|
+
entity.attributes = record.attributes.symbolize_keys
|
134
|
+
|
135
|
+
if klass == entity_class
|
136
|
+
after_find(entity, record)
|
137
|
+
end
|
138
|
+
|
139
|
+
entity
|
140
|
+
else
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Hooks
|
146
|
+
|
147
|
+
def after_find(entity, record)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/minimapper/version.rb
CHANGED
data/minimapper.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |gem|
|
|
8
8
|
gem.version = Minimapper::VERSION
|
9
9
|
gem.authors = ["Joakim Kolsjo"]
|
10
10
|
gem.email = ["joakim.kolsjo@gmail.com"]
|
11
|
-
gem.description = %q{A minimalistic way of separating your models from
|
12
|
-
gem.summary = %q{A minimalistic way of separating your models from
|
11
|
+
gem.description = %q{A minimalistic way of separating your models from ActiveRecord.}
|
12
|
+
gem.summary = %q{A minimalistic way of separating your models from ActiveRecord.}
|
13
13
|
gem.homepage = ""
|
14
14
|
|
15
15
|
gem.files = `git ls-files`.split($/)
|
@@ -1,22 +1,14 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
require "minimapper/entity/core"
|
3
|
-
require "minimapper/mapper
|
3
|
+
require "minimapper/mapper"
|
4
4
|
|
5
|
-
class
|
5
|
+
class Project
|
6
6
|
include Minimapper::Entity::Core
|
7
7
|
end
|
8
8
|
|
9
|
-
class
|
9
|
+
class ProjectMapper < Minimapper::Mapper
|
10
10
|
private
|
11
11
|
|
12
|
-
def entity_class
|
13
|
-
TestEntity
|
14
|
-
end
|
15
|
-
|
16
|
-
def record_class
|
17
|
-
Record
|
18
|
-
end
|
19
|
-
|
20
12
|
class Record < ActiveRecord::Base
|
21
13
|
attr_protected :visible
|
22
14
|
|
@@ -29,9 +21,9 @@ class TestMapper < Minimapper::Mapper::AR
|
|
29
21
|
end
|
30
22
|
end
|
31
23
|
|
32
|
-
describe Minimapper::Mapper
|
33
|
-
let(:mapper) {
|
34
|
-
let(:entity_class) {
|
24
|
+
describe Minimapper::Mapper do
|
25
|
+
let(:mapper) { ProjectMapper.new }
|
26
|
+
let(:entity_class) { Project }
|
35
27
|
|
36
28
|
include_examples :mapper
|
37
29
|
|
@@ -45,9 +37,9 @@ describe Minimapper::Mapper::AR do
|
|
45
37
|
stored_entity.attributes[:visible].should be_nil
|
46
38
|
stored_entity.attributes[:name].should == "Joe"
|
47
39
|
|
48
|
-
entity =
|
40
|
+
entity = Project.new
|
49
41
|
entity.attributes = { :visible => true, :name => "Joe" }
|
50
|
-
|
42
|
+
ProjectMapper::Record.stub(:protected_attributes => [])
|
51
43
|
lambda { mapper.create(entity) }.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
|
52
44
|
end
|
53
45
|
|
@@ -75,7 +67,7 @@ describe Minimapper::Mapper::AR do
|
|
75
67
|
end
|
76
68
|
|
77
69
|
def build_entity(attributes)
|
78
|
-
entity =
|
70
|
+
entity = Project.new
|
79
71
|
entity.attributes = attributes
|
80
72
|
entity
|
81
73
|
end
|
@@ -83,7 +75,7 @@ describe Minimapper::Mapper::AR do
|
|
83
75
|
|
84
76
|
describe "#update" do
|
85
77
|
it "does not include protected attributes" do
|
86
|
-
entity =
|
78
|
+
entity = Project.new
|
87
79
|
mapper.create(entity)
|
88
80
|
|
89
81
|
entity.attributes = { :visible => true, :name => "Joe" }
|
@@ -92,7 +84,7 @@ describe Minimapper::Mapper::AR do
|
|
92
84
|
stored_entity.attributes[:visible].should be_nil
|
93
85
|
stored_entity.attributes[:name].should == "Joe"
|
94
86
|
|
95
|
-
|
87
|
+
ProjectMapper::Record.stub(:protected_attributes => [])
|
96
88
|
lambda { mapper.update(entity) }.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
|
97
89
|
end
|
98
90
|
|
@@ -100,7 +92,7 @@ describe Minimapper::Mapper::AR do
|
|
100
92
|
old_entity = build_entity(:email => "joe@example.com")
|
101
93
|
mapper.create(old_entity)
|
102
94
|
|
103
|
-
new_entity =
|
95
|
+
new_entity = Project.new
|
104
96
|
mapper.create(new_entity)
|
105
97
|
new_entity.mapper_errors.should == []
|
106
98
|
|
@@ -113,7 +105,7 @@ describe Minimapper::Mapper::AR do
|
|
113
105
|
old_entity = build_entity(:email => "joe@example.com")
|
114
106
|
mapper.create(old_entity)
|
115
107
|
|
116
|
-
new_entity =
|
108
|
+
new_entity = Project.new
|
117
109
|
mapper.create(new_entity)
|
118
110
|
new_entity.mapper_errors.should == []
|
119
111
|
|
@@ -127,7 +119,7 @@ describe Minimapper::Mapper::AR do
|
|
127
119
|
end
|
128
120
|
|
129
121
|
def build_entity(attributes)
|
130
|
-
entity =
|
122
|
+
entity = Project.new
|
131
123
|
entity.attributes = attributes
|
132
124
|
entity
|
133
125
|
end
|
data/unit/entity/convert_spec.rb
CHANGED
@@ -2,39 +2,46 @@ require 'minimapper/entity/convert'
|
|
2
2
|
require 'active_support/core_ext'
|
3
3
|
|
4
4
|
describe Minimapper::Entity::Convert do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
describe ":integer" do
|
6
|
+
it "allows integers" do
|
7
|
+
described_class.new(10).to(:integer).should == 10
|
8
|
+
end
|
9
|
+
|
10
|
+
it "converts strings into integers" do
|
11
|
+
described_class.new('10').to(:integer).should == 10
|
12
|
+
described_class.new(' 10 ').to(:integer).should == 10
|
13
|
+
end
|
9
14
|
|
10
|
-
|
11
|
-
|
15
|
+
it "makes it nil when it can't convert" do
|
16
|
+
described_class.new(' ').to(:integer).should be_nil
|
17
|
+
described_class.new('garbage').to(:integer).should be_nil
|
18
|
+
end
|
12
19
|
end
|
13
20
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
21
|
+
describe ":date_time" do
|
22
|
+
it "allows DateTimes" do
|
23
|
+
described_class.new(DateTime.new(2013, 6, 1)).to(:date_time).should == DateTime.new(2013, 6, 1)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "converts datetime strings into datetimes" do
|
27
|
+
described_class.new('2012-01-01 20:57').to(:date_time).should == DateTime.new(2012, 01, 01, 20, 57)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "makes it nil when it can't convert" do
|
31
|
+
described_class.new(' ').to(:date_time).should be_nil
|
32
|
+
described_class.new('garbage').to(:date_time).should be_nil
|
33
|
+
end
|
19
34
|
end
|
20
35
|
|
21
36
|
it "returns the value as-is when the type isn't specified" do
|
22
37
|
described_class.new('foobar').to(nil).should == 'foobar'
|
23
38
|
end
|
24
39
|
|
25
|
-
it "does not make false nil" do
|
26
|
-
described_class.new(false).to(nil).should eq(false)
|
27
|
-
end
|
28
|
-
|
29
40
|
it "raises when the type isn't known" do
|
30
41
|
lambda { described_class.new('foobar').to(:unknown) }.should raise_error(/Unknown attribute type/)
|
31
42
|
end
|
32
43
|
|
33
44
|
it "does not make false nil" do
|
34
|
-
described_class.new(false).to(:
|
35
|
-
end
|
36
|
-
|
37
|
-
it "does not make false nil" do
|
38
|
-
described_class.new(false).to(:unknown).should eq(false)
|
45
|
+
described_class.new(false).to(:whatever).should eq(false)
|
39
46
|
end
|
40
47
|
end
|
data/unit/repository_spec.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: minimapper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-06-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
16
|
-
requirement: &
|
16
|
+
requirement: &70148053526900 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,8 +21,8 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
25
|
-
description: A minimalistic way of separating your models from
|
24
|
+
version_requirements: *70148053526900
|
25
|
+
description: A minimalistic way of separating your models from ActiveRecord.
|
26
26
|
email:
|
27
27
|
- joakim.kolsjo@gmail.com
|
28
28
|
executables: []
|
@@ -44,15 +44,13 @@ files:
|
|
44
44
|
- lib/minimapper/entity/convert/to_integer.rb
|
45
45
|
- lib/minimapper/entity/core.rb
|
46
46
|
- lib/minimapper/entity/rails.rb
|
47
|
-
- lib/minimapper/mapper
|
48
|
-
- lib/minimapper/mapper/common.rb
|
49
|
-
- lib/minimapper/mapper/memory.rb
|
47
|
+
- lib/minimapper/mapper.rb
|
50
48
|
- lib/minimapper/repository.rb
|
51
49
|
- lib/minimapper/version.rb
|
52
50
|
- minimapper.gemspec
|
53
51
|
- script/ci
|
54
52
|
- script/turbux_rspec
|
55
|
-
- spec/
|
53
|
+
- spec/mapper_spec.rb
|
56
54
|
- spec/spec_helper.rb
|
57
55
|
- spec/support/database_setup.rb
|
58
56
|
- spec/support/shared_examples/mapper.rb
|
@@ -60,7 +58,6 @@ files:
|
|
60
58
|
- unit/entity/core_spec.rb
|
61
59
|
- unit/entity/rails_spec.rb
|
62
60
|
- unit/entity_spec.rb
|
63
|
-
- unit/mapper/memory_spec.rb
|
64
61
|
- unit/repository_spec.rb
|
65
62
|
- unit/spec_helper.rb
|
66
63
|
homepage: ''
|
@@ -77,7 +74,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
74
|
version: '0'
|
78
75
|
segments:
|
79
76
|
- 0
|
80
|
-
hash: -
|
77
|
+
hash: -4244542897336928819
|
81
78
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
79
|
none: false
|
83
80
|
requirements:
|
@@ -86,15 +83,15 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
83
|
version: '0'
|
87
84
|
segments:
|
88
85
|
- 0
|
89
|
-
hash: -
|
86
|
+
hash: -4244542897336928819
|
90
87
|
requirements: []
|
91
88
|
rubyforge_project:
|
92
89
|
rubygems_version: 1.8.5
|
93
90
|
signing_key:
|
94
91
|
specification_version: 3
|
95
|
-
summary: A minimalistic way of separating your models from
|
92
|
+
summary: A minimalistic way of separating your models from ActiveRecord.
|
96
93
|
test_files:
|
97
|
-
- spec/
|
94
|
+
- spec/mapper_spec.rb
|
98
95
|
- spec/spec_helper.rb
|
99
96
|
- spec/support/database_setup.rb
|
100
97
|
- spec/support/shared_examples/mapper.rb
|
data/lib/minimapper/mapper/ar.rb
DELETED
@@ -1,155 +0,0 @@
|
|
1
|
-
require "minimapper/mapper/common"
|
2
|
-
|
3
|
-
module Minimapper
|
4
|
-
module Mapper
|
5
|
-
class AR
|
6
|
-
include Common
|
7
|
-
|
8
|
-
# Create
|
9
|
-
|
10
|
-
def create(entity)
|
11
|
-
record = record_class.new
|
12
|
-
|
13
|
-
copy_attributes_to_record(record, entity)
|
14
|
-
validate_record_and_copy_errors_to_entity(record, entity)
|
15
|
-
|
16
|
-
if entity.valid?
|
17
|
-
record.save!
|
18
|
-
entity.id = record.id
|
19
|
-
entity.id
|
20
|
-
else
|
21
|
-
false
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
# Read
|
26
|
-
|
27
|
-
def find(id)
|
28
|
-
entity_for(find_record_safely(id))
|
29
|
-
end
|
30
|
-
|
31
|
-
def find_by_id(id)
|
32
|
-
entity_for(find_record(id))
|
33
|
-
end
|
34
|
-
|
35
|
-
def all
|
36
|
-
entities_for record_class.all
|
37
|
-
end
|
38
|
-
|
39
|
-
def first
|
40
|
-
entity_for(record_class.order("id ASC").first)
|
41
|
-
end
|
42
|
-
|
43
|
-
def last
|
44
|
-
entity_for(record_class.order("id ASC").last)
|
45
|
-
end
|
46
|
-
|
47
|
-
def count
|
48
|
-
record_class.count
|
49
|
-
end
|
50
|
-
|
51
|
-
# Update
|
52
|
-
|
53
|
-
def update(entity)
|
54
|
-
record = record_for(entity)
|
55
|
-
|
56
|
-
copy_attributes_to_record(record, entity)
|
57
|
-
validate_record_and_copy_errors_to_entity(record, entity)
|
58
|
-
|
59
|
-
if entity.valid?
|
60
|
-
record.save!
|
61
|
-
true
|
62
|
-
else
|
63
|
-
false
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Delete
|
68
|
-
|
69
|
-
def delete(entity)
|
70
|
-
delete_by_id(entity.id)
|
71
|
-
entity.id = nil
|
72
|
-
end
|
73
|
-
|
74
|
-
def delete_by_id(id)
|
75
|
-
find_record_safely(id).delete
|
76
|
-
end
|
77
|
-
|
78
|
-
def delete_all
|
79
|
-
record_class.delete_all
|
80
|
-
end
|
81
|
-
|
82
|
-
private
|
83
|
-
|
84
|
-
# NOTE: Don't memoize the classes or code reloading will break in rails apps.
|
85
|
-
|
86
|
-
# Will attempt to use AR:Project as the record class
|
87
|
-
# when the mapper class name is AR::ProjectMapper.
|
88
|
-
def record_class
|
89
|
-
self.class.name.sub(/Mapper$/, '').constantize
|
90
|
-
end
|
91
|
-
|
92
|
-
# Will attempt to use Project as the entity class when
|
93
|
-
# the mapper class name is AR::ProjectMapper.
|
94
|
-
def entity_class
|
95
|
-
("::" + self.class.name.split('::').last.sub(/Mapper$/, '')).constantize
|
96
|
-
end
|
97
|
-
|
98
|
-
def accessible_attributes(entity)
|
99
|
-
entity.attributes.reject { |k, v| protected_attributes.include?(k.to_s) }
|
100
|
-
end
|
101
|
-
|
102
|
-
def protected_attributes
|
103
|
-
record_class.protected_attributes
|
104
|
-
end
|
105
|
-
|
106
|
-
def copy_attributes_to_record(record, entity)
|
107
|
-
record.attributes = accessible_attributes(entity)
|
108
|
-
end
|
109
|
-
|
110
|
-
def validate_record_and_copy_errors_to_entity(record, entity)
|
111
|
-
record.valid?
|
112
|
-
entity.mapper_errors = record.errors.map { |k, v| [k, v] }
|
113
|
-
end
|
114
|
-
|
115
|
-
def find_record_safely(id)
|
116
|
-
find_record(id) ||
|
117
|
-
raise(EntityNotFound, :id => id)
|
118
|
-
end
|
119
|
-
|
120
|
-
def find_record(id)
|
121
|
-
id && record_class.find_by_id(id)
|
122
|
-
end
|
123
|
-
|
124
|
-
def record_for(entity)
|
125
|
-
(entity.id && record_class.find_by_id(entity.id)) ||
|
126
|
-
raise(EntityNotFound, entity.inspect)
|
127
|
-
end
|
128
|
-
|
129
|
-
def entities_for(records)
|
130
|
-
records.map { |record| entity_for(record) }
|
131
|
-
end
|
132
|
-
|
133
|
-
def entity_for(record, klass = entity_class)
|
134
|
-
if record
|
135
|
-
entity = klass.new
|
136
|
-
entity.id = record.id
|
137
|
-
entity.attributes = record.attributes.symbolize_keys
|
138
|
-
|
139
|
-
if klass == entity_class
|
140
|
-
after_find(entity, record)
|
141
|
-
end
|
142
|
-
|
143
|
-
entity
|
144
|
-
else
|
145
|
-
nil
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
# Hooks
|
150
|
-
|
151
|
-
def after_find(entity, record)
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
@@ -1,101 +0,0 @@
|
|
1
|
-
require 'minimapper/mapper/common'
|
2
|
-
|
3
|
-
module Minimapper
|
4
|
-
module Mapper
|
5
|
-
class Memory
|
6
|
-
include Common
|
7
|
-
|
8
|
-
def initialize
|
9
|
-
@store = []
|
10
|
-
@last_id = 0
|
11
|
-
end
|
12
|
-
|
13
|
-
# Create
|
14
|
-
def create(entity)
|
15
|
-
if entity.valid?
|
16
|
-
entity.id = next_id
|
17
|
-
store.push(entity.dup)
|
18
|
-
last_id
|
19
|
-
else
|
20
|
-
false
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# Read
|
25
|
-
def find(id)
|
26
|
-
find_internal_safely(id).dup
|
27
|
-
end
|
28
|
-
|
29
|
-
def find_by_id(id)
|
30
|
-
entity = find_internal(id)
|
31
|
-
entity && entity.dup
|
32
|
-
end
|
33
|
-
|
34
|
-
def all
|
35
|
-
store.map { |entity| entity.dup }
|
36
|
-
end
|
37
|
-
|
38
|
-
def first
|
39
|
-
store.first && store.first.dup
|
40
|
-
end
|
41
|
-
|
42
|
-
def last
|
43
|
-
store.last && store.last.dup
|
44
|
-
end
|
45
|
-
|
46
|
-
def count
|
47
|
-
all.size
|
48
|
-
end
|
49
|
-
|
50
|
-
# Update
|
51
|
-
def update(entity)
|
52
|
-
if entity.valid?
|
53
|
-
known_entity = find_internal_safely(entity.id)
|
54
|
-
known_entity.attributes = entity.attributes
|
55
|
-
true
|
56
|
-
else
|
57
|
-
false
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# Delete
|
62
|
-
def delete(entity)
|
63
|
-
delete_by_id(entity.id)
|
64
|
-
entity.id = nil
|
65
|
-
end
|
66
|
-
|
67
|
-
def delete_by_id(id)
|
68
|
-
entity = find_internal_safely(id)
|
69
|
-
store.delete(entity)
|
70
|
-
end
|
71
|
-
|
72
|
-
def delete_all
|
73
|
-
store.clear
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
|
78
|
-
def find_internal_safely(id)
|
79
|
-
find_internal(id) ||
|
80
|
-
raise(EntityNotFound, :id => id)
|
81
|
-
end
|
82
|
-
|
83
|
-
def find_internal(id)
|
84
|
-
entity = id && store.find { |e| e.id == id.to_i }
|
85
|
-
after_find(entity)
|
86
|
-
entity
|
87
|
-
end
|
88
|
-
|
89
|
-
def next_id
|
90
|
-
@last_id += 1
|
91
|
-
end
|
92
|
-
|
93
|
-
attr_reader :store, :last_id
|
94
|
-
|
95
|
-
# Hooks
|
96
|
-
|
97
|
-
def after_find(entity)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
data/unit/mapper/memory_spec.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
require 'minimapper/mapper/memory'
|
2
|
-
require 'minimapper/entity/core'
|
3
|
-
|
4
|
-
class BasicEntity
|
5
|
-
include Minimapper::Entity::Core
|
6
|
-
end
|
7
|
-
|
8
|
-
describe Minimapper::Mapper::Memory do
|
9
|
-
let(:mapper) { described_class.new }
|
10
|
-
let(:entity_class) { BasicEntity }
|
11
|
-
|
12
|
-
include_examples :mapper
|
13
|
-
end
|