yadm 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.
- 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) }
|