yadm 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +273 -0
- data/Rakefile +10 -0
- data/examples/basic.rb +43 -0
- data/examples/migration.rb +65 -0
- data/lib/yadm.rb +39 -0
- data/lib/yadm/adapters.rb +32 -0
- data/lib/yadm/adapters/common_sql.rb +120 -0
- data/lib/yadm/adapters/memory.rb +175 -0
- data/lib/yadm/adapters/mysql.rb +17 -0
- data/lib/yadm/adapters/postgresql.rb +17 -0
- data/lib/yadm/adapters/sqlite.rb +17 -0
- data/lib/yadm/criteria.rb +32 -0
- data/lib/yadm/criteria/argument.rb +22 -0
- data/lib/yadm/criteria/attribute.rb +15 -0
- data/lib/yadm/criteria/condition.rb +31 -0
- data/lib/yadm/criteria/expression.rb +19 -0
- data/lib/yadm/criteria/limit.rb +25 -0
- data/lib/yadm/criteria/order.rb +48 -0
- data/lib/yadm/criteria_parser.rb +62 -0
- data/lib/yadm/criteria_parser/expression_parser.rb +77 -0
- data/lib/yadm/entity.rb +53 -0
- data/lib/yadm/identity_map.rb +51 -0
- data/lib/yadm/mapper.rb +16 -0
- data/lib/yadm/mapping.rb +71 -0
- data/lib/yadm/mapping/attribute.rb +31 -0
- data/lib/yadm/query.rb +28 -0
- data/lib/yadm/repository.rb +103 -0
- data/lib/yadm/version.rb +3 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/criteria_helpers.rb +33 -0
- data/spec/support/sequel_helpers.rb +25 -0
- data/spec/support/shared_examples_for_a_sequel_adapter.rb +173 -0
- data/spec/yadm/adapters/common_sql_spec.rb +89 -0
- data/spec/yadm/adapters/memory_spec.rb +230 -0
- data/spec/yadm/adapters/mysql_spec.rb +9 -0
- data/spec/yadm/adapters/postgresql_spec.rb +9 -0
- data/spec/yadm/adapters/sqlite_spec.rb +5 -0
- data/spec/yadm/adapters_spec.rb +32 -0
- data/spec/yadm/criteria/condition_spec.rb +50 -0
- data/spec/yadm/criteria/limit_spec.rb +45 -0
- data/spec/yadm/criteria/order_spec.rb +50 -0
- data/spec/yadm/criteria_parser/expression_parser_spec.rb +47 -0
- data/spec/yadm/criteria_parser_spec.rb +55 -0
- data/spec/yadm/criteria_spec.rb +40 -0
- data/spec/yadm/entity_spec.rb +76 -0
- data/spec/yadm/identity_map_spec.rb +128 -0
- data/spec/yadm/mapper_spec.rb +23 -0
- data/spec/yadm/mapping/attribute_spec.rb +35 -0
- data/spec/yadm/mapping_spec.rb +122 -0
- data/spec/yadm/query_spec.rb +45 -0
- data/spec/yadm/repository_spec.rb +175 -0
- data/spec/yadm_spec.rb +45 -0
- data/yadm.gemspec +33 -0
- metadata +254 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 100c78414b9dd599615ad79f69f2deabb996aecc
|
4
|
+
data.tar.gz: fa7071ad5525b2666c38f1b592ea8462ca1cd7aa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 467fd5032131f79119bc4f68a66834079696f910ad517d5eeeaf7fcc7247268508ecd053ad6f79de712dbe0f8afd757b6f03aadaa7078d8da41b6e9894ee2c1a
|
7
|
+
data.tar.gz: c754c914bd0fcf31c4459879cef43a4c21676e3e827ad9f3945a4fb7f586aaf9f3b88733423989467ea7ee45fcf0ad662c101a3ef1f89cee17a4fd2801ba6bf6
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
guard :rspec, all_on_start: true, cmd: 'bundle exec rspec' do
|
2
|
+
watch(%r{^spec/.+_spec\.rb$})
|
3
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
4
|
+
watch(%r{^spec/support/.+\.rb$}) { 'spec' }
|
5
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
6
|
+
|
7
|
+
watch('lib/yadm/adapters/common_sql.rb') { 'spec/yadm/adapters' }
|
8
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Vsevolod Romashov
|
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,273 @@
|
|
1
|
+
# YADM - Yet Another Data Mapper
|
2
|
+
|
3
|
+
Another attempt to implement Data Mapper in ruby.
|
4
|
+
|
5
|
+
Built with 2 goals in mind:
|
6
|
+
|
7
|
+
* to get familiar with common pitfalls in implementing Data Mapper
|
8
|
+
* to make a tool that can be useful now and has the potential to be able
|
9
|
+
to serve as a replacement for ActiveRecord eventually
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
# Gemfile
|
15
|
+
gem 'yadm'
|
16
|
+
```
|
17
|
+
|
18
|
+
``` sh
|
19
|
+
$ bundle
|
20
|
+
```
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
YADM consists of several components:
|
25
|
+
|
26
|
+
* entities
|
27
|
+
* repositories
|
28
|
+
* identity map
|
29
|
+
* data sources
|
30
|
+
* mapper
|
31
|
+
|
32
|
+
### Entities
|
33
|
+
|
34
|
+
Entity is a basic object with some attributes.
|
35
|
+
|
36
|
+
You can create an entity by defining a class that includes `YADM::Entity`:
|
37
|
+
|
38
|
+
``` ruby
|
39
|
+
class Person
|
40
|
+
include YADM::Entity
|
41
|
+
|
42
|
+
attributes :first_name, :last_name, :email, :password, :age
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
_You don't need to specify the `id` attribute, it comes by default._
|
47
|
+
|
48
|
+
### Repositories
|
49
|
+
|
50
|
+
A repository is a module representing a collection of entities. It can fetch
|
51
|
+
the objects from the data store and persist the changes back. Here you can
|
52
|
+
define complex criteria for querying the data source.
|
53
|
+
|
54
|
+
A repository is created as a module that includes `YADM::Repository`
|
55
|
+
and specifies it's entity:
|
56
|
+
|
57
|
+
``` ruby
|
58
|
+
module People
|
59
|
+
include YADM::Repository
|
60
|
+
entity Person
|
61
|
+
|
62
|
+
criteria :kids do
|
63
|
+
with { age < 12 }
|
64
|
+
end
|
65
|
+
|
66
|
+
criteria :older_than do |min_age|
|
67
|
+
with { age > min_age }
|
68
|
+
end
|
69
|
+
|
70
|
+
criteria :in_alphabetical_order do
|
71
|
+
ascending_by { last_name }.ascending_by { first_name }
|
72
|
+
end
|
73
|
+
|
74
|
+
criteria :oldest do |count|
|
75
|
+
descending_by { age }.first(count)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
### Identity map
|
81
|
+
|
82
|
+
The identity map is a cache for data.
|
83
|
+
|
84
|
+
Most data requests first look it up in the identity map. If it's there
|
85
|
+
it is returned without accessing the data source; otherwise it is pulled from
|
86
|
+
the data source, put into the map for subsequent queries and then returned.
|
87
|
+
|
88
|
+
_Currently the identity map doesn't handle any complex queries - only `.find`
|
89
|
+
calls are cached._
|
90
|
+
|
91
|
+
### Data sources
|
92
|
+
|
93
|
+
Data sources encapsulate the ability to read the data and write it back.
|
94
|
+
They are defined by adapters for different data storage solutions;
|
95
|
+
YADM ships with the following adapters:
|
96
|
+
|
97
|
+
* `memory` (useful for testing)
|
98
|
+
* `sqlite` (requires `sequel` and `sqlite3` gems)
|
99
|
+
* `mysql` (requires `sequel` and `mysql2` gems)
|
100
|
+
* `postgresql` (requires `sequel` and `pg` gems)
|
101
|
+
|
102
|
+
Adapters are not required by default (because of their dependencies)
|
103
|
+
so you should manually require each adapter you need manually.
|
104
|
+
|
105
|
+
You can register a data source with some unique identifier to use it later on:
|
106
|
+
|
107
|
+
``` ruby
|
108
|
+
require 'yadm/adapters/memory'
|
109
|
+
require 'yadm/adapters/postgresql'
|
110
|
+
|
111
|
+
YADM.setup do
|
112
|
+
data_source :memory_store, adapter: :memory
|
113
|
+
data_source :pg_store, adapter: :postgresql, database: 'yadm', user: 'yadm', password: 'yadm'
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
### Mapper
|
118
|
+
|
119
|
+
Mapper is the central part glueing everything together - it connects
|
120
|
+
repositories to data sources.
|
121
|
+
|
122
|
+
Assuming the `memory_store` data source created earlier
|
123
|
+
we can link the repository to it and define some attributes:
|
124
|
+
|
125
|
+
``` ruby
|
126
|
+
YADM.setup do
|
127
|
+
map do
|
128
|
+
repository People do
|
129
|
+
data_source :memory_store
|
130
|
+
collection :people
|
131
|
+
|
132
|
+
attribute :id, Integer
|
133
|
+
attribute :first_name, String
|
134
|
+
attribute :last_name, String
|
135
|
+
attribute :email, String
|
136
|
+
attribute :password, String
|
137
|
+
attribute :age, Integer
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
_The data source is divided into separate collections represented by tables
|
144
|
+
in a database (and by plain ruby hashes in the memory adapter)._
|
145
|
+
|
146
|
+
### Creating a new record
|
147
|
+
|
148
|
+
A new record can be created by building a new entity object and passing it
|
149
|
+
to it's repository `.persist` method. Entity gets an `id` after being saved.
|
150
|
+
|
151
|
+
``` ruby
|
152
|
+
john = Person.new(
|
153
|
+
first_name: 'John',
|
154
|
+
last_name: 'Smith',
|
155
|
+
email: 'john@smiths.com',
|
156
|
+
password: 'secret',
|
157
|
+
age: 28
|
158
|
+
)
|
159
|
+
john.id # => nil
|
160
|
+
|
161
|
+
People.persist(john)
|
162
|
+
jonh.id # => 1
|
163
|
+
```
|
164
|
+
|
165
|
+
### Getting a record by id
|
166
|
+
|
167
|
+
Dead simple:
|
168
|
+
|
169
|
+
``` ruby
|
170
|
+
People.find(1) # => #<Person:0x007ffdeab7f8c8 ...>
|
171
|
+
```
|
172
|
+
|
173
|
+
### Updating a record
|
174
|
+
|
175
|
+
The `.persist` method is able to distinguish between a new entity and
|
176
|
+
an already saved one; in the latter case it updates the respective record
|
177
|
+
in the data source.
|
178
|
+
|
179
|
+
``` ruby
|
180
|
+
john.password = 'f1E2m0CdP'
|
181
|
+
People.persist(john)
|
182
|
+
```
|
183
|
+
|
184
|
+
### Deleting a record
|
185
|
+
|
186
|
+
Deleting a record is as simple as passing the respective entity to
|
187
|
+
`.delete` method.
|
188
|
+
|
189
|
+
``` ruby
|
190
|
+
People.delete(john)
|
191
|
+
```
|
192
|
+
|
193
|
+
### Using complex queries
|
194
|
+
|
195
|
+
The `criteria` method in the repository DSL (mentioned earlier)
|
196
|
+
allows to create query criteria such as query conditions, order and limit.
|
197
|
+
Criteria's name serves as a name for the repository method
|
198
|
+
that applies the criteria.
|
199
|
+
|
200
|
+
``` ruby
|
201
|
+
People.kids # => #<People::Query:0x007f940b104db0 ...>
|
202
|
+
```
|
203
|
+
|
204
|
+
The query object is enumerable - you can call any `Enumerable` methods such as
|
205
|
+
`each` or `map` on it. Data is fetched lazily: the data source will be asked
|
206
|
+
for data only when it is needed:
|
207
|
+
|
208
|
+
``` ruby
|
209
|
+
People.kids.map(&:first_name) # => ['John']
|
210
|
+
```
|
211
|
+
|
212
|
+
This laziness allows to chain criteria methods together effectively merging
|
213
|
+
them in one big criteria:
|
214
|
+
|
215
|
+
``` ruby
|
216
|
+
People.older_than(30).in_alphabetical_order # => #<People::Query:0x007f940a9abed8 ...>
|
217
|
+
```
|
218
|
+
|
219
|
+
When you just want to get all the records without filtering/ordering them
|
220
|
+
you can call `.to_a` on the repository:
|
221
|
+
|
222
|
+
``` ruby
|
223
|
+
People.to_a # => [#<Person:0x007f940ae39360 ...>, #<Person:0x007f940acfa580 ...>]
|
224
|
+
```
|
225
|
+
|
226
|
+
_You can call enumerable methods on the repository as well - this allows
|
227
|
+
to traverse all records in the collection._
|
228
|
+
|
229
|
+
### Migrations
|
230
|
+
|
231
|
+
Working with a relational database requires changing it's schema often;
|
232
|
+
this is what migrations are for. YADM provides a very simple interface for
|
233
|
+
defining [sequel migrations](http://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html):
|
234
|
+
|
235
|
+
``` ruby
|
236
|
+
YADM.migrate :store do |db|
|
237
|
+
db.create_table :posts do
|
238
|
+
primary_key :id
|
239
|
+
|
240
|
+
String :title
|
241
|
+
String :author
|
242
|
+
Integer :comments
|
243
|
+
Time :created_at
|
244
|
+
end
|
245
|
+
end
|
246
|
+
```
|
247
|
+
|
248
|
+
_You must define the respective data source before trying to migrate it._
|
249
|
+
|
250
|
+
## Roadmap
|
251
|
+
|
252
|
+
* SQL joins
|
253
|
+
* associations
|
254
|
+
* more adapters
|
255
|
+
|
256
|
+
## Acknowledgements
|
257
|
+
|
258
|
+
This project is heavily inspired by [lotus/model](https://github.com/lotus/model)
|
259
|
+
and [ROM](http://rom-rb.org) projects, the [famous Uncle Bob's
|
260
|
+
"Architecture the Lost Years"](http://www.youtube.com/watch?v=WpkDN78P884)
|
261
|
+
and [POODR](http://www.poodr.com/) of course.
|
262
|
+
|
263
|
+
## Examples
|
264
|
+
|
265
|
+
There are a couple examples in the `examples/` directory.
|
266
|
+
|
267
|
+
## Contributing
|
268
|
+
|
269
|
+
1. Fork it (https://github.com/7even/yadm/fork)
|
270
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
271
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
272
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
273
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/examples/basic.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# use with `pry -r ./examples/basic.rb`
|
2
|
+
$LOAD_PATH << File.expand_path('../../lib')
|
3
|
+
require 'yadm'
|
4
|
+
require 'yadm/adapters/memory'
|
5
|
+
|
6
|
+
class Person
|
7
|
+
include YADM::Entity
|
8
|
+
attributes :first_name, :last_name, :age
|
9
|
+
end
|
10
|
+
|
11
|
+
module People
|
12
|
+
include YADM::Repository
|
13
|
+
entity Person
|
14
|
+
|
15
|
+
criteria :older_than do |min_age|
|
16
|
+
with { age > min_age }
|
17
|
+
end
|
18
|
+
|
19
|
+
criteria :in_alphabetical_order do
|
20
|
+
ascending_by { last_name }.ascending_by { first_name }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
YADM.setup do
|
25
|
+
data_source :store, adapter: :memory
|
26
|
+
|
27
|
+
map do
|
28
|
+
repository People do
|
29
|
+
data_source :store
|
30
|
+
collection :people
|
31
|
+
|
32
|
+
attribute :id, Integer
|
33
|
+
attribute :first_name, String
|
34
|
+
attribute :last_name, String
|
35
|
+
attribute :age, Integer
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
[
|
41
|
+
Person.new(first_name: 'Vsevolod', last_name: 'Romashov', age: 30),
|
42
|
+
Person.new(first_name: 'Alexey', last_name: 'Kurepin', age: 29)
|
43
|
+
].each { |person| People.persist(person) }
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# use with `pry -r ./examples/migration.rb`
|
2
|
+
$LOAD_PATH << File.expand_path('../../lib')
|
3
|
+
require 'yadm'
|
4
|
+
require 'yadm/adapters/sqlite'
|
5
|
+
|
6
|
+
class Post
|
7
|
+
include YADM::Entity
|
8
|
+
attributes :title, :author, :comments, :created_at
|
9
|
+
end
|
10
|
+
|
11
|
+
module Posts
|
12
|
+
include YADM::Repository
|
13
|
+
entity Post
|
14
|
+
|
15
|
+
criteria :recent do
|
16
|
+
descending_by { created_at }.first(20)
|
17
|
+
end
|
18
|
+
|
19
|
+
criteria :created_by do |given_author|
|
20
|
+
with { author == given_author }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
YADM.setup do
|
25
|
+
data_source :store, adapter: :sqlite
|
26
|
+
|
27
|
+
map do
|
28
|
+
repository Posts do
|
29
|
+
data_source :store
|
30
|
+
collection :posts
|
31
|
+
|
32
|
+
attribute :id, Integer
|
33
|
+
attribute :title, String
|
34
|
+
attribute :author, String
|
35
|
+
attribute :comments, Integer
|
36
|
+
attribute :created_at, Time
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
YADM.migrate :store do |db|
|
42
|
+
db.create_table :posts do
|
43
|
+
primary_key :id
|
44
|
+
|
45
|
+
String :title
|
46
|
+
String :author
|
47
|
+
Integer :comments
|
48
|
+
Time :created_at
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
[
|
53
|
+
Post.new(
|
54
|
+
title: 'Hello World!',
|
55
|
+
author: '7even',
|
56
|
+
comments: 7,
|
57
|
+
created_at: Time.now - 3600
|
58
|
+
),
|
59
|
+
Post.new(
|
60
|
+
title: 'Goodbye cruel world.',
|
61
|
+
author: 'foxweb',
|
62
|
+
comments: 5,
|
63
|
+
created_at: Time.now - 1800
|
64
|
+
)
|
65
|
+
].each { |post| Posts.persist(post) }
|