lotus-model 0.0.0 → 0.1.0

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 (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