lotus-model 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +6 -0
  4. data/.yardopts +5 -0
  5. data/EXAMPLE.md +217 -0
  6. data/Gemfile +14 -2
  7. data/README.md +303 -3
  8. data/Rakefile +17 -1
  9. data/lib/lotus-model.rb +1 -0
  10. data/lib/lotus/entity.rb +157 -0
  11. data/lib/lotus/model.rb +23 -2
  12. data/lib/lotus/model/adapters/abstract.rb +167 -0
  13. data/lib/lotus/model/adapters/implementation.rb +111 -0
  14. data/lib/lotus/model/adapters/memory/collection.rb +132 -0
  15. data/lib/lotus/model/adapters/memory/command.rb +90 -0
  16. data/lib/lotus/model/adapters/memory/query.rb +457 -0
  17. data/lib/lotus/model/adapters/memory_adapter.rb +149 -0
  18. data/lib/lotus/model/adapters/sql/collection.rb +209 -0
  19. data/lib/lotus/model/adapters/sql/command.rb +67 -0
  20. data/lib/lotus/model/adapters/sql/query.rb +615 -0
  21. data/lib/lotus/model/adapters/sql_adapter.rb +154 -0
  22. data/lib/lotus/model/mapper.rb +101 -0
  23. data/lib/lotus/model/mapping.rb +23 -0
  24. data/lib/lotus/model/mapping/coercer.rb +80 -0
  25. data/lib/lotus/model/mapping/collection.rb +336 -0
  26. data/lib/lotus/model/version.rb +4 -1
  27. data/lib/lotus/repository.rb +620 -0
  28. data/lotus-model.gemspec +15 -11
  29. data/test/entity_test.rb +126 -0
  30. data/test/fixtures.rb +81 -0
  31. data/test/model/adapters/abstract_test.rb +75 -0
  32. data/test/model/adapters/implementation_test.rb +22 -0
  33. data/test/model/adapters/memory/query_test.rb +91 -0
  34. data/test/model/adapters/memory_adapter_test.rb +1044 -0
  35. data/test/model/adapters/sql/query_test.rb +121 -0
  36. data/test/model/adapters/sql_adapter_test.rb +1078 -0
  37. data/test/model/mapper_test.rb +94 -0
  38. data/test/model/mapping/coercer_test.rb +27 -0
  39. data/test/model/mapping/collection_test.rb +82 -0
  40. data/test/repository_test.rb +283 -0
  41. data/test/test_helper.rb +30 -0
  42. data/test/version_test.rb +7 -0
  43. metadata +109 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 45eafad8cbc67eedd9b93f6c4febf4a70666e698
4
- data.tar.gz: ef28d6829b48fe1e0a6615fbe436e3a2d296d7a9
3
+ metadata.gz: 49e959406b3eb918c7a4bd905a12ba1c10262710
4
+ data.tar.gz: d94acfc3ddd8cf6b68acda3df7b6316532c3da3c
5
5
  SHA512:
6
- metadata.gz: b931608e74e698873231ec83a8bb52ec8139e806734b2c8bc72a441d3f18afc82c790a46df99715fb2f6e73a2f37247e0240d48ef2be3c38f3aa6e6ef458aae4
7
- data.tar.gz: d45a1c327e775c321d70c214e760a9d67e92f71323e855e8519519d190858d216dc3a2c2c5bb019380f7e35ca6f9dca0aee35257c8d0f7c1c674c222795d8f75
6
+ metadata.gz: 5fbddd4ce436384d0c76cdb23fe97c836bcb86f68bc43ce18a6c128be5effe992454e0e542d08113e8e0f920498257d06efe8633b9c07256a73c5f9cac24f7fd
7
+ data.tar.gz: 61a389ebd9c46d67fb19bd5af588c1c5681253f3470eea10b2975c32d246741c66bf53ed888261c07d33f81d99fd7f641ff7b2932f1ac8afc367f84cb2346917
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .greenbar
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ script: 'bundle exec rake test:coverage'
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.1.1
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --protected
2
+ --private
3
+ -
4
+ LICENSE.txt
5
+ lib/**/*.rb
data/EXAMPLE.md ADDED
@@ -0,0 +1,217 @@
1
+ # Lotus::Model
2
+
3
+ This is a guide that helps you to getting started with [**Lotus::Model**](https://github.com/lotus/model).
4
+ You can find the full code source [here](https://gist.github.com/jodosha/11211048).
5
+
6
+ ## Gems
7
+
8
+ First of all, we need to setup a `Gemfile`.
9
+
10
+ ```ruby
11
+ source 'https://rubygems.org'
12
+
13
+ gem 'sqlite3'
14
+ gem 'lotus-model'
15
+ ```
16
+
17
+ Then we can fetch the dependencies with `bundle install`.
18
+
19
+ ## Setup
20
+
21
+ **Lotus::Model** doesn't have migrations, for this example we're gonna use [Sequel](http://sequel.jeremyevans.net).
22
+ We create the database first, and then two tables: `authors` and `articles`.
23
+
24
+ ```ruby
25
+ require 'bundler/setup'
26
+ require 'sqlite3'
27
+ require 'lotus/model'
28
+ require 'lotus/model/adapters/sql_adapter'
29
+
30
+ connection_uri = "sqlite://#{ __dir__ }/test.db"
31
+
32
+ database = Sequel.connect(connection_uri)
33
+
34
+ database.create_table! :authors do
35
+ primary_key :id
36
+ String :name
37
+ end
38
+
39
+ database.create_table! :articles do
40
+ primary_key :id
41
+ Integer :author_id, null: false
42
+ String :title
43
+ Integer :comments_count, default: 0
44
+ Boolean :published, default: false
45
+ end
46
+ ```
47
+
48
+ ## Entities
49
+
50
+ We have two entities in our application: `Author` and `Article`.
51
+ `Author` is a `Struct`, Lotus::Model can persist it.
52
+ `Article` has a small API concerning its publishing process.
53
+
54
+ ```ruby
55
+ Author = Struct.new(:id, :name) do
56
+ def initialize(attributes = {})
57
+ @id, @name = attributes.values_at(:id, :name)
58
+ end
59
+ end
60
+
61
+ class Article
62
+ include Lotus::Entity
63
+ self.attributes = :author_id, :title, :comments_count, :published # id is implicit
64
+
65
+ def published?
66
+ !!published
67
+ end
68
+
69
+ def publish!
70
+ @published = true
71
+ end
72
+ end
73
+ ```
74
+
75
+ ## Repositories
76
+
77
+ In order to persist and query the entities above, we define two corresponding repositories:
78
+
79
+ ```ruby
80
+ class AuthorRepository
81
+ include Lotus::Repository
82
+ end
83
+
84
+ class ArticleRepository
85
+ include Lotus::Repository
86
+
87
+ def self.most_recent_by_author(author, limit = 8)
88
+ query do
89
+ where(author_id: author.id).
90
+ desc(:id).
91
+ limit(limit)
92
+ end
93
+ end
94
+
95
+ def self.most_recent_published_by_author(author, limit = 8)
96
+ most_recent_by_author(author, limit).published
97
+ end
98
+
99
+ def self.published
100
+ query do
101
+ where(published: true)
102
+ end
103
+ end
104
+
105
+ def self.drafts
106
+ exclude published
107
+ end
108
+
109
+ def self.rank
110
+ published.desc(:comments_count)
111
+ end
112
+
113
+ def self.best_article_ever
114
+ rank.limit(1)
115
+ end
116
+
117
+ def self.comments_average
118
+ query.average(:comments_count)
119
+ end
120
+ end
121
+ ```
122
+
123
+ ## Mapper
124
+
125
+ We create a correspondence between the database columns with the entities' attributes.
126
+
127
+ ```ruby
128
+ mapper = Lotus::Model::Mapper.new do
129
+ collection :authors do
130
+ entity Author
131
+
132
+ attribute :id, Integer
133
+ attribute :name, String
134
+ end
135
+
136
+ collection :articles do
137
+ entity Article
138
+
139
+ attribute :id, Integer
140
+ attribute :author_id, Integer
141
+ attribute :title, String
142
+ attribute :comments_count, Integer
143
+ attribute :published, Boolean
144
+ end
145
+ end
146
+ ```
147
+
148
+ ## Loading
149
+
150
+ We create an adapter instance, passing `mapper` and the connection URI (see above).
151
+ Please remember that the setup code is only required for the standalone usage of **Lotus::Model**.
152
+ A **Lotus** application will handle that configurations for you.
153
+
154
+ ```ruby
155
+ adapter = Lotus::Model::Adapters::SqlAdapter.new(mapper, connection_uri)
156
+ AuthorRepository.adapter = adapter
157
+ ArticleRepository.adapter = adapter
158
+
159
+ mapper.load! # last operation
160
+ ```
161
+
162
+ ## Persist
163
+
164
+ Let's instantiate and persist some objects for our example:
165
+
166
+ ```ruby
167
+ author = Author.new(name: 'Luca')
168
+ AuthorRepository.create(author)
169
+
170
+ articles = [
171
+ Article.new(title: 'Announcing Lotus', author_id: author.id, comments_count: 123, published: true),
172
+ Article.new(title: 'Introducing Lotus::Router', author_id: author.id, comments_count: 63, published: true),
173
+ Article.new(title: 'Introducing Lotus::Controller', author_id: author.id, comments_count: 82, published: true),
174
+ Article.new(title: 'Introducing Lotus::Model', author_id: author.id)
175
+ ]
176
+
177
+ articles.each do |article|
178
+ ArticleRepository.create(article)
179
+ end
180
+ ```
181
+
182
+ ## Query
183
+
184
+ We can use repositories to query the database and return the entities we're looking for:
185
+
186
+ ```ruby
187
+ ArticleRepository.first # => return the first article
188
+ ArticleRepository.last # => return the last article
189
+
190
+ ArticleRepository.published # => return all the published articles
191
+ ArticleRepository.drafts # => return all the drafts
192
+
193
+ ArticleRepository.rank # => all the published articles, sorted by popularity
194
+
195
+ ArticleRepository.best_article_ever # => the most commented article
196
+
197
+ ArticleRepository.comments_average # => calculates the average of comments across all the published articles.
198
+
199
+ ArticleRepository.most_recent_by_author(author) # => most recent articles by an author (drafts and published).
200
+ ArticleRepository.most_recent_published_by_author(author) # => most recent published articles by an author
201
+ ```
202
+
203
+ ## Business logic
204
+
205
+ As we've seen above, `Article` implements an API for publishing.
206
+ We're gonna use that logic to alter the state of an article (from draft to published) and then we use the repository to persist this new state.
207
+
208
+ ```ruby
209
+ article = ArticleRepository.drafts.first
210
+
211
+ article.published? # => false
212
+ article.publish!
213
+
214
+ article.published? # => true
215
+
216
+ ArticleRepository.update(article)
217
+ ```
data/Gemfile CHANGED
@@ -1,4 +1,16 @@
1
1
  source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in lotus-model.gemspec
4
2
  gemspec
3
+
4
+ if !ENV['TRAVIS']
5
+ gem 'byebug', require: false, platforms: :ruby if RUBY_VERSION == '2.1.1'
6
+ gem 'yard', require: false
7
+ # gem 'lotus-utils', require: false, github: 'lotus/utils'
8
+ else
9
+ # gem 'lotus-utils', '~> 0.1', '> 0.1.0'
10
+ end
11
+
12
+ gem 'lotus-utils', require: false, github: 'lotus/utils'
13
+
14
+ gem 'sqlite3', require: false
15
+ gem 'simplecov', require: false
16
+ gem 'coveralls', require: false
data/README.md CHANGED
@@ -1,6 +1,41 @@
1
1
  # Lotus::Model
2
2
 
3
- TODO: Write a gem description
3
+ A persistence framework for [Lotus](http://lotusrb.org).
4
+
5
+ It delivers a convenient public API to execute queries and commands against a database.
6
+ The architecture allows to keep business logic (entities) separated from details such as persistence or validations.
7
+
8
+ It implements the following concepts:
9
+
10
+ * [Entity](#entities) - An object defined by its identity.
11
+ * [Repository](#repositories) - An object that mediates between the entities and the persistence layer.
12
+ * [Data Mapper](#datamapper) - A persistence mapper that keep entities independent from database details.
13
+ * [Adapter](#adapters) – A database adapter.
14
+ * [Query](#queries) - An object that represents a database query.
15
+
16
+ Like all the other Lotus compontents, it can be used as a standalone framework or within a full Lotus application.
17
+
18
+ ## Status
19
+
20
+ [![Gem Version](https://badge.fury.io/rb/lotus-model.png)](http://badge.fury.io/rb/lotus-model)
21
+ [![Build Status](https://secure.travis-ci.org/lotus/model.png?branch=master)](http://travis-ci.org/lotus/model?branch=master)
22
+ [![Coverage](https://coveralls.io/repos/lotus/model/badge.png?branch=master)](https://coveralls.io/r/lotus/model)
23
+ [![Code Climate](https://codeclimate.com/github/lotus/model.png)](https://codeclimate.com/github/lotus/model)
24
+ [![Dependencies](https://gemnasium.com/lotus/model.png)](https://gemnasium.com/lotus/model)
25
+ [![Inline docs](http://inch-pages.github.io/github/lotus/model.png)](http://inch-pages.github.io/github/lotus/model)
26
+
27
+ ## Contact
28
+
29
+ * Home page: http://lotusrb.org
30
+ * Mailing List: http://lotusrb.org/mailing-list
31
+ * API Doc: http://rdoc.info/gems/lotus-model
32
+ * Bugs/Issues: https://github.com/lotus/model/issues
33
+ * Support: http://stackoverflow.com/questions/tagged/lotus-ruby
34
+ * Chat: https://gitter.im/lotus/chat
35
+
36
+ ## Rubies
37
+
38
+ __Lotus::View__ supports Ruby (MRI) 2+
4
39
 
5
40
  ## Installation
6
41
 
@@ -18,12 +53,277 @@ Or install it yourself as:
18
53
 
19
54
  ## Usage
20
55
 
21
- TODO: Write usage instructions here
56
+ ### Entities
57
+
58
+ An object that is defined by its identity.
59
+
60
+ An entity is the core of an application, where the part of the domain logic is implemented.
61
+ It's a small, cohesive object that express coherent and meagniful behaviors.
62
+
63
+ It deals with one and only one responsibility that is pertinent to the
64
+ domain of the application, without caring about details such as persistence
65
+ or validations.
66
+
67
+ This simplicity of design allows developers to focus on behaviors, or
68
+ message passing if you will, which is the quintessence of Object Oriented Programming.
69
+
70
+ ```ruby
71
+ require 'lotus/model'
72
+
73
+ class Person
74
+ include Lotus::Entity
75
+ self.attributes = :name, :age
76
+ end
77
+ ```
78
+
79
+ When a class includes `Lotus::Entity` it will receive the following interface:
80
+
81
+ * `#id`
82
+ * `#id=`
83
+ * `#initialize(attributes = {})`
84
+
85
+ Also, the usage of `.attributes=` defines accessors for the given attribute names.
86
+
87
+ If we expand the code above in **pure Ruby**, it would be:
88
+
89
+ ```ruby
90
+ class Person
91
+ attr_accessor :id, :name, :age
92
+
93
+ def initialize(attributes = {})
94
+ @id, @name, @age = attributes.values_at(:id, :name, :age)
95
+ end
96
+ end
97
+ ```
98
+
99
+ Indeed, **Lotus::Model** ships `Entity` only for developers's convenience, but the
100
+ rest of the framework is able to accept any object that implements the interface above.
101
+
102
+ ### Repositories
103
+
104
+ A object that mediates between entites and the persistence layer.
105
+ It offers a standardized API to query and execute commands on a database.
106
+
107
+ A repository is **storage idenpendent**, all the queries and commands are
108
+ delegated to the current adapter.
109
+
110
+ This architecture has several advantages:
111
+
112
+ * Applications depends on an standard API, instead of low level details
113
+ (Dependency Inversion principle)
114
+
115
+ * Applications depends on a stable API, that doesn't change if the
116
+ storage changes
117
+
118
+ * Developers can postpone storage decisions
119
+
120
+ * Confines persistence logic at a low level
121
+
122
+ * Multiple data sources can easily coexist in an application
123
+
124
+ When a class includes `Lotus::Repository`, it will receive the following interface:
125
+
126
+ * `.persist(entity)` – Create or update an entity
127
+ * `.create(entity)` – Create a record for the given entity
128
+ * `.update(entity)` – Update the record correspoding to the given entity
129
+ * `.delete(entity)` – Delete the record correspoding to the given entity
130
+ * `.all` - Fetch all the entities from the collection
131
+ * `.first` - Fetch the first entity from the collection
132
+ * `.last` - Fetch the last entity from the collection
133
+ * `.clear` - Delete all the records from the collection
134
+ * `.query` - Fabricates a query object
135
+
136
+ **A collection is a homogenous set of records.**
137
+ It corresponds to a table for a SQL database or to a MongoDB collection.
138
+
139
+ **All the queries are private**.
140
+ This decision forces developers to define intention revealing API, instead leak storage API details outside of a repository.
141
+
142
+ Look at the following code:
143
+
144
+ ```ruby
145
+ ArticleRepository.where(author_id: 23).order(:published_at).limit(8)
146
+ ```
147
+
148
+ This is **bad** for a variety of reasons:
149
+
150
+ * The caller has an intimate knowledge of the internal mechanisms of the Repository.
151
+
152
+ * The caller works on several levels of abstraction.
153
+
154
+ * It doesn't express a clear intent, it's just a chain of methods.
155
+
156
+ * The caller can't be easily tested in isolation.
157
+
158
+ * If we change the storage, we are forced to change the code of the caller(s).
159
+
160
+ There is a better way:
161
+
162
+ ```ruby
163
+ require 'lotus/model'
164
+
165
+ class ArticleRepository
166
+ include Lotus::Repository
167
+
168
+ def self.most_recent_by_author(author, limit = 8)
169
+ query do
170
+ where(author_id: author.id).
171
+ order(:published_at)
172
+ end.limit(limit)
173
+ end
174
+ end
175
+ ```
176
+
177
+ This is a **huge improvement**, because:
178
+
179
+ * The caller doesn't know how the repository fetches the entities.
180
+
181
+ * The caller works on a single level of abstraction. It doesn't even know about records, only works with entities.
182
+
183
+ * It expresses a clear intent.
184
+
185
+ * The caller can be easily tested in isolation. It's just a matter of stub this method.
186
+
187
+ * If we change the storage, the callers aren't affected.
188
+
189
+ Here an extended example of a repository that uses the SQL adapter.
190
+
191
+ ```ruby
192
+ class ArticleRepository
193
+ include Lotus::Repository
194
+
195
+ def self.most_recent_by_author(author, limit = 8)
196
+ query do
197
+ where(author_id: author.id).
198
+ desc(:id).
199
+ limit(limit)
200
+ end
201
+ end
202
+
203
+ def self.most_recent_published_by_author(author, limit = 8)
204
+ most_recent_by_author(author, limit).published
205
+ end
206
+
207
+ def self.published
208
+ query do
209
+ where(published: true)
210
+ end
211
+ end
212
+
213
+ def self.drafts
214
+ exclude published
215
+ end
216
+
217
+ def self.rank
218
+ published.desc(:comments_count)
219
+ end
220
+
221
+ def self.best_article_ever
222
+ rank.limit(1)
223
+ end
224
+
225
+ def self.comments_average
226
+ query.average(:comments_count)
227
+ end
228
+ end
229
+ ```
230
+
231
+ ### Data Mapper
232
+
233
+ A persistence mapper that keep entities independent from database details.
234
+ It's database independent, it can work with SQL, document, and even with key/value stores.
235
+
236
+ The role of a data mapper is to translate database columns into the corresponding attribute of an entity.
237
+
238
+ ```ruby
239
+ require 'lotus/model'
240
+
241
+ mapper = Lotus::Model::Mapper.new do
242
+ collection :users do
243
+ entity User
244
+
245
+ attribute :id, Integer
246
+ attribute :name, String
247
+ attribute :age, Integer
248
+ end
249
+ end
250
+ ```
251
+
252
+ For simplicity sake, imagine that the mapper above is used with a SQL database.
253
+ We use `#collection` to indicate the table that we want to map, `#entity` to indicate the class that we want to associate.
254
+ In the end, each `#attribute` call, is to associate the column with a Ruby type.
255
+
256
+ For advanced mapping and legacy databases, please have a look at the API doc.
257
+
258
+ ### Adapter
259
+
260
+ An adapter is a concrete implementation of persistence logic for a specific database.
261
+ **Lotus::Model** is shipped with two adapters:
262
+
263
+ * SqlAdapter
264
+ * MemoryAdapter
265
+
266
+ An adapter can be associated to one or multiple repositories.
267
+
268
+ ```ruby
269
+ require 'pg'
270
+ require 'lotus/model'
271
+ require 'lotus/model/adapters/sql_adapter'
272
+
273
+ mapper = Lotus::Model::Mapper.new do
274
+ # ...
275
+ end
276
+
277
+ adapter = Lotus::Model::Adapters::SqlAdapter.new(mapper, 'postgres://host:port/database')
278
+
279
+ PersonRepository.adapter = adapter
280
+ ArticleRepository.adapter = adapter
281
+ ```
282
+
283
+ In the example above, we reuse the adpter because the target tables (`people` and `articles`) are defined in the same database.
284
+ **As rule of thumb, one adapter instance per database.**
285
+
286
+ ### Query
287
+
288
+ An object that implements an interface for quering the database.
289
+ This interface may vary, according to the adapter's specifications.
290
+ Think of an adapter for Redis, it will probably employ different strategies to filter records from an SQL query object.
291
+
292
+ ### Conventions
293
+
294
+ * A repository must be named after an entity, by appeding `"Repository"` to the entity class name (eg. `Article` => `ArticleRepository`).
295
+
296
+ ### Thread safety
297
+
298
+ **Lotus::Model**'s is thread safe during the runtime, but it isn't during the loading process.
299
+ The mapper compiles some code internally, be sure to safely load it before your application starts.
300
+
301
+ ```ruby
302
+ Mutex.new.synchronize do
303
+ mapper.load!
304
+ end
305
+ ```
306
+
307
+ **This is not necessary, when Lotus::Model is used within a Lotus application.**
308
+
309
+ ## Example
310
+
311
+ For a full working example, have a look at [EXAMPLE.md](https://github.com/lotus/model/blob/master/EXAMPLE.md).
312
+ Please remember that the setup code is only required for the standalone usage of **Lotus::Model**.
313
+ A **Lotus** application will handle that configurations for you.
314
+
315
+ ## Versioning
316
+
317
+ __Lotus::Model__ uses [Semantic Versioning 2.0.0](http://semver.org)
22
318
 
23
319
  ## Contributing
24
320
 
25
- 1. Fork it ( http://github.com/<my-github-username>/lotus-model/fork )
321
+ 1. Fork it ( https://github.com/lotus/model/fork )
26
322
  2. Create your feature branch (`git checkout -b my-new-feature`)
27
323
  3. Commit your changes (`git commit -am 'Add some feature'`)
28
324
  4. Push to the branch (`git push origin my-new-feature`)
29
325
  5. Create new Pull Request
326
+
327
+ ## Copyright
328
+
329
+ Copyright 2014 Luca Guidi – Released under MIT License