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