yadm 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +8 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +273 -0
  8. data/Rakefile +10 -0
  9. data/examples/basic.rb +43 -0
  10. data/examples/migration.rb +65 -0
  11. data/lib/yadm.rb +39 -0
  12. data/lib/yadm/adapters.rb +32 -0
  13. data/lib/yadm/adapters/common_sql.rb +120 -0
  14. data/lib/yadm/adapters/memory.rb +175 -0
  15. data/lib/yadm/adapters/mysql.rb +17 -0
  16. data/lib/yadm/adapters/postgresql.rb +17 -0
  17. data/lib/yadm/adapters/sqlite.rb +17 -0
  18. data/lib/yadm/criteria.rb +32 -0
  19. data/lib/yadm/criteria/argument.rb +22 -0
  20. data/lib/yadm/criteria/attribute.rb +15 -0
  21. data/lib/yadm/criteria/condition.rb +31 -0
  22. data/lib/yadm/criteria/expression.rb +19 -0
  23. data/lib/yadm/criteria/limit.rb +25 -0
  24. data/lib/yadm/criteria/order.rb +48 -0
  25. data/lib/yadm/criteria_parser.rb +62 -0
  26. data/lib/yadm/criteria_parser/expression_parser.rb +77 -0
  27. data/lib/yadm/entity.rb +53 -0
  28. data/lib/yadm/identity_map.rb +51 -0
  29. data/lib/yadm/mapper.rb +16 -0
  30. data/lib/yadm/mapping.rb +71 -0
  31. data/lib/yadm/mapping/attribute.rb +31 -0
  32. data/lib/yadm/query.rb +28 -0
  33. data/lib/yadm/repository.rb +103 -0
  34. data/lib/yadm/version.rb +3 -0
  35. data/spec/spec_helper.rb +26 -0
  36. data/spec/support/criteria_helpers.rb +33 -0
  37. data/spec/support/sequel_helpers.rb +25 -0
  38. data/spec/support/shared_examples_for_a_sequel_adapter.rb +173 -0
  39. data/spec/yadm/adapters/common_sql_spec.rb +89 -0
  40. data/spec/yadm/adapters/memory_spec.rb +230 -0
  41. data/spec/yadm/adapters/mysql_spec.rb +9 -0
  42. data/spec/yadm/adapters/postgresql_spec.rb +9 -0
  43. data/spec/yadm/adapters/sqlite_spec.rb +5 -0
  44. data/spec/yadm/adapters_spec.rb +32 -0
  45. data/spec/yadm/criteria/condition_spec.rb +50 -0
  46. data/spec/yadm/criteria/limit_spec.rb +45 -0
  47. data/spec/yadm/criteria/order_spec.rb +50 -0
  48. data/spec/yadm/criteria_parser/expression_parser_spec.rb +47 -0
  49. data/spec/yadm/criteria_parser_spec.rb +55 -0
  50. data/spec/yadm/criteria_spec.rb +40 -0
  51. data/spec/yadm/entity_spec.rb +76 -0
  52. data/spec/yadm/identity_map_spec.rb +128 -0
  53. data/spec/yadm/mapper_spec.rb +23 -0
  54. data/spec/yadm/mapping/attribute_spec.rb +35 -0
  55. data/spec/yadm/mapping_spec.rb +122 -0
  56. data/spec/yadm/query_spec.rb +45 -0
  57. data/spec/yadm/repository_spec.rb +175 -0
  58. data/spec/yadm_spec.rb +45 -0
  59. data/yadm.gemspec +33 -0
  60. metadata +254 -0
@@ -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
@@ -0,0 +1,8 @@
1
+ /.bundle
2
+ /.yardoc
3
+ /_yardoc
4
+ /Gemfile.lock
5
+ /doc
6
+ /pkg
7
+ /tmp
8
+ /*.sqlite
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in yadm.gemspec
4
+ gemspec
@@ -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
@@ -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.
@@ -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
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ desc 'Loads YADM and launches a console'
4
+ task :console do
5
+ require 'yadm'
6
+ require 'pry'
7
+ Pry.start
8
+ end
9
+
10
+ task default: :console
@@ -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) }