epiphy 0.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.
@@ -0,0 +1,17 @@
1
+ module Epiphy
2
+ module Connection
3
+
4
+ include RethinkDB::Shortcuts
5
+ # Create a RethinkDB connection.
6
+ #
7
+ # @param Hash [host, port, db, auth]
8
+ # @return RethinkDB::Connection a connection to RethinkDB
9
+ #
10
+ # @api public
11
+ # @since 0.0.1
12
+ def self.create(opts = {})
13
+ r.connect opts
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,152 @@
1
+ require 'lotus/utils/kernel'
2
+
3
+ module Epiphy
4
+ # An object that is defined by its identity.
5
+ # See Domain Driven Design by Eric Evans.
6
+ #
7
+ # An entity is the core of an application, where the part of the domain
8
+ # logic is implemented. It's a small, cohesive object that express coherent
9
+ # and meaningful behaviors.
10
+ #
11
+ # It deals with one and only one responsibility that is pertinent to the
12
+ # domain of the application, without caring about details such as persistence
13
+ # or validations.
14
+ #
15
+ # This simplicity of design allows developers to focus on behaviors, or
16
+ # message passing if you will, which is the quintessence of Object Oriented
17
+ # Programming.
18
+ #
19
+ # @example With Epiphy::Entity
20
+ # require 'epiphy/model'
21
+ #
22
+ # class Person
23
+ # include Epiphy::Entity
24
+ # self.attributes = :name, :age
25
+ # end
26
+ #
27
+ # When a class includes `Epiphy::Entity` the `.attributes=` method is exposed.
28
+ # By then calling the `.attributes=` class method, the following methods are
29
+ # added:
30
+ #
31
+ # * #id
32
+ # * #id=
33
+ # * #initialize(attributes = {})
34
+ #
35
+ # If we expand the code above in pure Ruby, it would be:
36
+ #
37
+ # @example Pure Ruby
38
+ # class Person
39
+ # attr_accessor :id, :name, :age
40
+ #
41
+ # def initialize(attributes = {})
42
+ # @id, @name, @age = attributes.values_at(:id, :name, :age)
43
+ # end
44
+ # end
45
+ #
46
+ # Indeed, **Epiphy::Model** ships `Entity` only for developers's convenience, but the
47
+ # rest of the framework is able to accept any object that implements the interface above.
48
+ #
49
+ # However, we suggest to implement this interface by including `Epiphy::Entity`,
50
+ # in case that future versions of the framework will expand it.
51
+ #
52
+ # @since 0.1.0
53
+ #
54
+ # @see Epiphy::Repository
55
+ module Entity
56
+ # Inject the public API into the hosting class.
57
+ #
58
+ # @since 0.1.0
59
+ #
60
+ # @example With Object
61
+ # require 'epiphy/model'
62
+ #
63
+ # class User
64
+ # include Epiphy::Entity
65
+ # end
66
+ #
67
+ # @example With Struct
68
+ # require 'epiphy/model'
69
+ #
70
+ # User = Struct.new(:id, :name) do
71
+ # include Epiphy::Entity
72
+ # end
73
+ def self.included(base)
74
+ base.extend ClassMethods
75
+ base.send :attr_accessor, :id
76
+ end
77
+
78
+ module ClassMethods
79
+ # (Re)defines getters, setters and initialization for the given attributes.
80
+ #
81
+ # These attributes can match the database columns, but this isn't a
82
+ # requirement. The mapper used by the relative repository will translate
83
+ # these names automatically.
84
+ #
85
+ # An entity can work with attributes not configured in the mapper, but
86
+ # of course they will be ignored when the entity will be persisted.
87
+ #
88
+ # Please notice that the required `id` attribute is automatically defined
89
+ # and can be omitted in the arguments.
90
+ #
91
+ # @param attributes [Array<Symbol>] a set of arbitrary attribute names
92
+ #
93
+ # @since 0.1.0
94
+ #
95
+ # @see Epiphy::Repository
96
+ # @see Epiphy::Model::Mapper
97
+ #
98
+ # @example
99
+ # require 'epiphy/model'
100
+ #
101
+ # class User
102
+ # include Epiphy::Entity
103
+ # self.attributes = :name
104
+ # end
105
+ def attributes=(*attributes)
106
+ @attributes = Lotus::Utils::Kernel.Array(attributes.unshift(:id))
107
+
108
+ class_eval %{
109
+ def initialize(attributes = {})
110
+ #{ @attributes.map {|a| "@#{a}" }.join(', ') }, = *attributes.values_at(#{ @attributes.map {|a| ":#{a}"}.join(', ') })
111
+ end
112
+ }
113
+
114
+ attr_accessor *@attributes
115
+ end
116
+
117
+ def attributes
118
+ @attributes
119
+ end
120
+ end
121
+
122
+ # Defines a generic, inefficient initializer, in case that the attributes
123
+ # weren't explicitly defined with `.attributes=`.
124
+ #
125
+ # @param attributes [Hash] a set of attribute names and values
126
+ #
127
+ # @raise NoMethodError in case the given attributes are trying to set unknown
128
+ # or private methods.
129
+ #
130
+ # @since 0.1.0
131
+ #
132
+ # @see .attributes
133
+ def initialize(attributes = {})
134
+ attributes.each do |k, v|
135
+ public_send("#{ k }=", v)
136
+ end
137
+ end
138
+
139
+ # Overrides the equality Ruby operator
140
+ #
141
+ # Two entities are considered equal if they are instances of the same class
142
+ # and if they have the same #id.
143
+ #
144
+ # @since 0.1.0
145
+ def ==(other)
146
+ self.class == other.class &&
147
+ self.id == other.id
148
+ end
149
+
150
+ end
151
+ end
152
+
@@ -0,0 +1,37 @@
1
+ require 'epiphy/version'
2
+ require 'epiphy/entity'
3
+ require 'epiphy/connection'
4
+ require 'epiphy/adapter/rethinkdb'
5
+ require 'epiphy/repository'
6
+
7
+ module Epiphy
8
+ # Model
9
+ #
10
+ # @since 0.1.0
11
+ module Model
12
+ # Error for not found entity
13
+ #
14
+ # @since 0.1.0
15
+ #
16
+ # @see epiphy::Repository.find
17
+ class EntityNotFound < ::StandardError
18
+ end
19
+
20
+ class EntityClassNotFound < ::StandardError
21
+
22
+ end
23
+
24
+ class EntityIdNotFound < ::ArgumentError
25
+
26
+ end
27
+
28
+ # Error for non persisted entity
29
+ # It's raised when we try to update or delete a non persisted entity.
30
+ #
31
+ # @since 0.1.0
32
+ #
33
+ # @see epiphy::Repository.update
34
+ class NonPersistedEntityError < ::StandardError
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,759 @@
1
+ require 'lotus/utils/class_attribute'
2
+ require 'epiphy/repository/configuration'
3
+ require 'epiphy/repository/cursor'
4
+
5
+ module Epiphy
6
+ # Mediates between the entities and the persistence layer, by offering an API
7
+ # to query and execute commands on a database.
8
+ #
9
+ #
10
+ #
11
+ # By default, a repository is named after an entity, by appending the
12
+ # `Repository` suffix to the entity class name.
13
+ #
14
+ # @example
15
+ #
16
+ # # Configuration and initalize the necessary config. Can be put in Rails
17
+ # # config file.
18
+ # connection = Epiphy::Connection.create
19
+ # adapter = Epiphy::Adapter::Rethinkdb.new(connection)
20
+ # Epiphy::Repository.configure do |c|
21
+ # c.adapter = adapter
22
+ # end
23
+ #
24
+ #
25
+ # require 'epiphy/model'
26
+ #
27
+ # class Article
28
+ # include Epiphy::Entity
29
+ # end
30
+ #
31
+ # # valid
32
+ # class ArticleRepository
33
+ # include Epiphy::Repository
34
+ # end
35
+ #
36
+ # # not valid for Article
37
+ # class PostRepository
38
+ # include Epiphy::Repository
39
+ # end
40
+ #
41
+ # Repository for an entity can be configured by setting # the `#repository`
42
+ # on the mapper.
43
+ #
44
+ # @example
45
+ # # PostRepository is repository for Article
46
+ # mapper = Epiphy::Model::Mapper.new do
47
+ # collection :articles do
48
+ # entity Article
49
+ # repository PostRepository
50
+ # end
51
+ # end
52
+ #
53
+ # A repository is storage independent.
54
+ # All the queries and commands are delegated to the current adapter.
55
+ #
56
+ # This architecture has several advantages:
57
+ #
58
+ # * Applications depend on an abstract API, instead of low level details
59
+ # (Dependency Inversion principle)
60
+ #
61
+ # * Applications depend on a stable API, that doesn't change if the
62
+ # storage changes
63
+ #
64
+ # * Developers can postpone storage decisions
65
+ #
66
+ # * Isolates the persistence logic at a low level
67
+ #
68
+ # Epiphy::Model is shipped with two adapters:
69
+ #
70
+ # * SqlAdapter
71
+ # * MemoryAdapter
72
+ #
73
+ #
74
+ #
75
+ # All the queries and commands are private.
76
+ # This decision forces developers to define intention revealing API, instead
77
+ # leak storage API details outside of a repository.
78
+ #
79
+ # @example
80
+ # require 'epiphy/model'
81
+ #
82
+ # # This is bad for several reasons:
83
+ # #
84
+ # # * The caller has an intimate knowledge of the internal mechanisms
85
+ # # of the Repository.
86
+ # #
87
+ # # * The caller works on several levels of abstraction.
88
+ # #
89
+ # # * It doesn't express a clear intent, it's just a chain of methods.
90
+ # #
91
+ # # * The caller can't be easily tested in isolation.
92
+ # #
93
+ # # * If we change the storage, we are forced to change the code of the
94
+ # # caller(s).
95
+ #
96
+ # ArticleRepository.where(author_id: 23).order(:published_at).limit(8)
97
+ #
98
+ #
99
+ #
100
+ # # This is a huge improvement:
101
+ # #
102
+ # # * The caller doesn't know how the repository fetches the entities.
103
+ # #
104
+ # # * The caller works on a single level of abstraction.
105
+ # # It doesn't even know about records, only works with entities.
106
+ # #
107
+ # # * It expresses a clear intent.
108
+ # #
109
+ # # * The caller can be easily tested in isolation.
110
+ # # It's just a matter of stub this method.
111
+ # #
112
+ # # * If we change the storage, the callers aren't affected.
113
+ #
114
+ # ArticleRepository.most_recent_by_author(author)
115
+ #
116
+ # class ArticleRepository
117
+ # include Epiphy::Repository
118
+ #
119
+ # def self.most_recent_by_author(author, limit = 8)
120
+ # query do
121
+ # where(author_id: author.id).
122
+ # order(:published_at)
123
+ # end.limit(limit)
124
+ # end
125
+ # end
126
+ #
127
+ # @since 0.1.0
128
+ #
129
+ # @see Epiphy::Entity
130
+ # @see http://martinfowler.com/eaaCatalog/repository.html
131
+ # @see http://en.wikipedia.org/wiki/Dependency_inversion_principle
132
+ module Repository
133
+
134
+ # Configure repository class by using a block. By default, all Repisitory
135
+ # will be initlized with same configuration in an instance of the
136
+ # `Configuration` # class.
137
+ #
138
+ # Each Repository holds a reference to an `Epiphy::Adapter::Rethinkdb`
139
+ # object. This adapter is set when a new Repository is defined.
140
+ #
141
+ # @see Epiphy::Repository::Configuration class for configuration option
142
+ #
143
+ # The adapter can be chaged later if needed with
144
+ # `Epiphy::Repository#adapter=` method
145
+ #
146
+ # @see Epiphy::Repository#adapter=
147
+ #
148
+ # @example
149
+ # adapter = Epiphy::Adapter::Rethinkdb.new connection
150
+ # Epiphy::Repository.configure do |config|
151
+ # config.adapter = adapter
152
+ # end
153
+ # @since 0.0.1
154
+ #
155
+ class <<self
156
+ def configure
157
+ raise(ArgumentError, 'Missing config block') unless block_given?
158
+ @config ||= Configuration.new
159
+ yield(@config)
160
+ end
161
+
162
+ def get_config
163
+ @config
164
+ end
165
+ end
166
+
167
+ # Inject the public API into the hosting class.
168
+ #
169
+ # Also setup the repository. Collection name, Adapter will be set
170
+ # automatically at this step. By changing adapter, you can force the
171
+ # Repository to be read/written from somewhere else.
172
+ #
173
+ # In a master/slave environment, the adapter can be change depend on the
174
+ # repository.
175
+ #
176
+ # The name of table to hold this collection in database can be change with
177
+ # self.collection= method
178
+ #
179
+ # @since 0.1.0
180
+ # @see self#collection
181
+ #
182
+ # @example
183
+ # require 'epiphy/model'
184
+ #
185
+ # class UserRepository
186
+ # include Epiphy::Repository
187
+ # end
188
+ #
189
+ # UserRepository.collection #=> User
190
+ #
191
+ # class MouseRepository
192
+ # include Epiphy::Repository
193
+ #
194
+ # end
195
+ # MouseRepository.collection = 'Mice'
196
+ # MouseRepository.collection #=> Mice
197
+ #
198
+ # class FilmRepository
199
+ # include Epiphy::Repository
200
+ # collection = 'Movie'
201
+ # end
202
+ # FilmRepository.collection = 'Movie'
203
+ #
204
+ def self.included(base)
205
+ #config = self.get_config
206
+ config = Epiphy::Repository.get_config
207
+ base.class_eval do
208
+ extend ClassMethods
209
+ include Lotus::Utils::ClassAttribute
210
+
211
+ class_attribute :collection
212
+ self.adapter=(config.adapter)
213
+ self.collection=(get_name) if self.collection.nil?
214
+ end
215
+ end
216
+
217
+ module ClassMethods
218
+ # Assigns an adapter.
219
+ #
220
+ # Epiphy::Repository is shipped with an adapters:
221
+ #
222
+ # * Rethinkdb
223
+ #
224
+ # @param adapter [Object] an object that implements
225
+ # `Epiphy::Model::Adapters::Abstract` interface
226
+ #
227
+ # @since 0.1.0
228
+ #
229
+ # @see Epiphy::Adapter::Rethinkdb
230
+ #
231
+ # @example
232
+ #
233
+ # class UserRepository
234
+ # include Epiphy::Repository
235
+ # end
236
+ #
237
+ # # Adapter is set by a shared adapter by default. Unless you want
238
+ # to change, you shoul not need this
239
+ # adapter = Epiphy::Adapter::Rethinkdb.new aconnection, adb
240
+ # UserRepository.adapter = adapter
241
+ #
242
+ def adapter=(adapter)
243
+ @adapter = adapter
244
+ end
245
+
246
+ # Creates or updates a record in the database for the given entity.
247
+ #
248
+ # @param entity [#id, #id=] the entity to persist
249
+ #
250
+ # @return [Object] the entity
251
+ #
252
+ # @since 0.1.0
253
+ #
254
+ # @see Epiphy::Repository#create
255
+ # @see Epiphy::Repository#update
256
+ #
257
+ # @example With a non persisted entity
258
+ # require 'epiphy'
259
+ #
260
+ # class ArticleRepository
261
+ # include Epiphy::Repository
262
+ # end
263
+ #
264
+ # article = Article.new(title: 'Introducing Epiphy::Model')
265
+ # article.id # => nil
266
+ #
267
+ # ArticleRepository.persist(article) # creates a record
268
+ # article.id # => 23
269
+ #
270
+ # @example With a persisted entity
271
+ # require 'epiphy'
272
+ #
273
+ # class ArticleRepository
274
+ # include Epiphy::Repository
275
+ # end
276
+ #
277
+ # article = ArticleRepository.find(23)
278
+ # article.id # => 23
279
+ #
280
+ # article.title = 'Launching Epiphy::Model'
281
+ # ArticleRepository.persist(article) # updates the record
282
+ #
283
+ # article = ArticleRepository.find(23)
284
+ # article.title # => "Launching Epiphy::Model"
285
+ def persist(entity)
286
+ @adapter.persist(collection, to_document(entity))
287
+ end
288
+
289
+ # Creates a record in the database for the given entity.
290
+ # It assigns the `id` attribute, in case of success.
291
+ #
292
+ # If already persisted (`id` present) it does nothing.
293
+ #
294
+ # @param entity [#id,#id=] the entity to create
295
+ #
296
+ # @return [Object] the entity
297
+ #
298
+ # @since 0.1.0
299
+ #
300
+ # @see Epiphy::Repository#persist
301
+ #
302
+ # @example
303
+ # require 'epiphy/model'
304
+ #
305
+ # class ArticleRepository
306
+ # include Epiphy::Repository
307
+ # end
308
+ #
309
+ # article = Article.new(title: 'Introducing Epiphy::Model')
310
+ # article.id # => nil
311
+ #
312
+ # ArticleRepository.create(article) # creates a record
313
+ # article.id # => 23
314
+ #
315
+ # ArticleRepository.create(article) # no-op
316
+ def create(entity)
317
+ unless entity.id
318
+ result = @adapter.create(collection, to_document(entity))
319
+ entity.id = result
320
+ end
321
+ end
322
+
323
+ # Updates a record in the database corresponding to the given entity.
324
+ #
325
+ # If not already persisted (`id` present) it raises an exception.
326
+ #
327
+ # @param entity [#id] the entity to update
328
+ #
329
+ # @return [Object] the entity
330
+ #
331
+ # @raise [Epiphy::Model::NonPersistedEntityError] if the given entity
332
+ # wasn't already persisted.
333
+ #
334
+ # @since 0.1.0
335
+ #
336
+ # @see Epiphy::Repository#persist
337
+ # @see Epiphy::Model::NonPersistedEntityError
338
+ #
339
+ # @example With a persisted entity
340
+ # require 'epiphy/model'
341
+ #
342
+ # class ArticleRepository
343
+ # include Epiphy::Repository
344
+ # end
345
+ #
346
+ # article = ArticleRepository.find(23)
347
+ # article.id # => 23
348
+ # article.title = 'Launching Epiphy::Model'
349
+ #
350
+ # ArticleRepository.update(article) # updates the record
351
+ #
352
+ #
353
+ #
354
+ # @example With a non persisted entity
355
+ # require 'epiphy/model'
356
+ #
357
+ # class ArticleRepository
358
+ # include Epiphy::Repository
359
+ # end
360
+ #
361
+ # article = Article.new(title: 'Introducing Epiphy::Model')
362
+ # article.id # => nil
363
+ #
364
+ # ArticleRepository.update(article) # raises Epiphy::Model::NonPersistedEntityError
365
+ def update(entity)
366
+ if entity.id
367
+ @adapter.update(collection, to_document(entity))
368
+ else
369
+ raise Epiphy::Model::NonPersistedEntityError
370
+ end
371
+ end
372
+
373
+ # Deletes a record in the database corresponding to the given entity.
374
+ #
375
+ # If not already persisted (`id` present) it raises an exception.
376
+ #
377
+ # @param entity [#id] the entity to delete
378
+ #
379
+ # @return [Object] the entity
380
+ #
381
+ # @raise [Epiphy::Model::NonPersistedEntityError] if the given entity
382
+ # wasn't already persisted.
383
+ #
384
+ # @since 0.1.0
385
+ #
386
+ # @see Epiphy::Model::NonPersistedEntityError
387
+ #
388
+ # @example With a persisted entity
389
+ # require 'epiphy/model'
390
+ #
391
+ # class ArticleRepository
392
+ # include Epiphy::Repository
393
+ # end
394
+ #
395
+ # article = ArticleRepository.find(23)
396
+ # article.id # => 23
397
+ #
398
+ # ArticleRepository.delete(article) # deletes the record
399
+ #
400
+ #
401
+ #
402
+ # @example With a non persisted entity
403
+ # require 'epiphy/model'
404
+ #
405
+ # class ArticleRepository
406
+ # include Epiphy::Repository
407
+ # end
408
+ #
409
+ # article = Article.new(title: 'Introducing Epiphy::Model')
410
+ # article.id # => nil
411
+ #
412
+ # ArticleRepository.delete(article) # raises Epiphy::Model::NonPersistedEntityError
413
+ def delete(entity)
414
+ if entity.id
415
+ @adapter.delete(collection, entity.id)
416
+ else
417
+ raise Epiphy::Model::NonPersistedEntityError
418
+ end
419
+
420
+ entity
421
+ end
422
+
423
+ # Returns all the persisted entities.
424
+ #
425
+ # @return [Array<Object>] the result of the query
426
+ #
427
+ # @since 0.1.0
428
+ #
429
+ # @example
430
+ # require 'epiphy/model'
431
+ #
432
+ # class ArticleRepository
433
+ # include Epiphy::Repository
434
+ # end
435
+ #
436
+ # ArticleRepository.all # => [ #<Article:0x007f9b19a60098> ]
437
+ def all
438
+ all_row = @adapter.all(collection)
439
+ cursor = Epiphy::Repository::Cursor.new all_row do |item|
440
+ to_entity(item)
441
+ end
442
+ cursor.to_a
443
+ end
444
+
445
+ # Finds an entity by its identity.
446
+ #
447
+ # If used with a SQL database, it corresponds to the primary key.
448
+ #
449
+ # @param id [Object] the identity of the entity
450
+ #
451
+ # @return [Object] the result of the query
452
+ #
453
+ # @raise [Epiphy::Model::EntityNotFound] if the entity cannot be found.
454
+ #
455
+ # @since 0.1.0
456
+ #
457
+ # @see Epiphy::Model::EntityNotFound
458
+ #
459
+ # @example With a persisted entity
460
+ # require 'epiphy/model'
461
+ #
462
+ # class ArticleRepository
463
+ # include Epiphy::Repository
464
+ # end
465
+ #
466
+ # ArticleRepository.find(9) # => raises Epiphy::Model::EntityNotFound
467
+ def find(id)
468
+ entity_id = id
469
+ if !id.is_a? String
470
+ raise Epiphy::Model::EntityIdNotFound, "Missing entity id" if !id.respond_to?(:to_s)
471
+ entity_id = id.to_s
472
+ end
473
+ #if !id.is_a? String
474
+ #entity_id = id.to_i
475
+ #end
476
+ result = @adapter.find(collection, entity_id).tap do |record|
477
+ raise Epiphy::Model::EntityNotFound.new unless record
478
+ end
479
+ to_entity(result)
480
+ end
481
+
482
+ # Returns the first entity in the database.
483
+ #
484
+ # @return [Object,nil] the result of the query
485
+ #
486
+ # @since 0.1.0
487
+ #
488
+ # @see Epiphy::Repository#last
489
+ #
490
+ # @example With at least one persisted entity
491
+ # require 'epiphy/model'
492
+ #
493
+ # class ArticleRepository
494
+ # include Epiphy::Repository
495
+ # end
496
+ #
497
+ # ArticleRepository.first # => #<Article:0x007f8c71d98a28>
498
+ #
499
+ # @example With an empty collection
500
+ # require 'epiphy/model'
501
+ #
502
+ # class ArticleRepository
503
+ # include Epiphy::Repository
504
+ # end
505
+ #
506
+ # ArticleRepository.first # => nil
507
+ def first
508
+ @adapter.first(collection)
509
+ end
510
+
511
+ # Returns the last entity in the database.
512
+ #
513
+ # @return [Object,nil] the result of the query
514
+ #
515
+ # @since 0.1.0
516
+ #
517
+ # @see Epiphy::Repository#last
518
+ #
519
+ # @example With at least one persisted entity
520
+ # require 'epiphy/model'
521
+ #
522
+ # class ArticleRepository
523
+ # include Epiphy::Repository
524
+ # end
525
+ #
526
+ # ArticleRepository.last # => #<Article:0x007f8c71d98a28>
527
+ #
528
+ # @example With an empty collection
529
+ # require 'epiphy/model'
530
+ #
531
+ # class ArticleRepository
532
+ # include Epiphy::Repository
533
+ # end
534
+ #
535
+ # ArticleRepository.last # => nil
536
+ def last
537
+ @adapter.last(collection)
538
+ end
539
+
540
+ # Deletes all the records from the current collection.
541
+ #
542
+ # Execute a `r.table().delete()` on RethinkDB level.
543
+ #
544
+ # @since 0.1.0
545
+ #
546
+ # @example
547
+ # require 'epiphy/model'
548
+ #
549
+ # class ArticleRepository
550
+ # include Epiphy::Repository
551
+ # end
552
+ #
553
+ # ArticleRepository.clear # deletes all the records
554
+ def clear
555
+ @adapter.clear(collection)
556
+ end
557
+
558
+ # Create a collection storage in database.
559
+ #
560
+ def create_collection
561
+ query do |r|
562
+ r.table_create(self.collection)
563
+ end
564
+ end
565
+
566
+ # Drop a collection storage in database
567
+ #
568
+ def drop_collection
569
+ query do |r|
570
+ r.table_drop(self.collection)
571
+ end
572
+ end
573
+
574
+ private
575
+ # Fabricates a query and yields the given block to access the low level
576
+ # APIs exposed by the query itself.
577
+ #
578
+ # This is a Ruby private method, because we wanted to prevent outside
579
+ # objects to query directly the database. However, this is a public API
580
+ # method, and this is the only way to filter entities.
581
+ #
582
+ # The returned query SHOULD be lazy: the entities should be fetched by
583
+ # the database only when needed.
584
+ #
585
+ # The returned query SHOULD refer to the entire collection by default.
586
+ #
587
+ # Queries can be reused and combined together. See the example below.
588
+ # IMPORTANT: This feature works only with the Sql adapter.
589
+ #
590
+ # A repository is storage independent.
591
+ # All the queries are delegated to the current adapter, which is
592
+ # responsible to implement a querying API.
593
+ #
594
+ # Epiphy::Model is shipped with two adapters:
595
+ #
596
+ # * SqlAdapter, which yields a Epiphy::Model::Adapters::Sql::Query
597
+ # * MemoryAdapter, which yields a Epiphy::Model::Adapters::Memory::Query
598
+ #
599
+ # @param blk [Proc] a block of code that is executed in the context of a
600
+ # query
601
+ #
602
+ # @return a query, the type depends on the current adapter
603
+ #
604
+ # @api public
605
+ # @since 0.1.0
606
+ #
607
+ # @see Epiphy::Model::Adapters::Sql::Query
608
+ # @see Epiphy::Model::Adapters::Memory::Query
609
+ #
610
+ # @example
611
+ # require 'epiphy/model'
612
+ #
613
+ # class ArticleRepository
614
+ # include Epiphy::Repository
615
+ #
616
+ # def self.most_recent_by_author(author, limit = 8)
617
+ # query do |r|
618
+ # where(author_id: author.id).
619
+ # desc(:published_at).
620
+ # limit(limit)
621
+ # end
622
+ # end
623
+ #
624
+ # def self.most_recent_published_by_author(author, limit = 8)
625
+ # # combine .most_recent_published_by_author and .published queries
626
+ # most_recent_by_author(author, limit).published
627
+ # end
628
+ #
629
+ # def self.published
630
+ # query do
631
+ # where(published: true)
632
+ # end
633
+ # end
634
+ #
635
+ # def self.rank
636
+ # # reuse .published, which returns a query that respond to #desc
637
+ # published.desc(:comments_count)
638
+ # end
639
+ #
640
+ # def self.best_article_ever
641
+ # # reuse .published, which returns a query that respond to #limit
642
+ # rank.limit(1)
643
+ # end
644
+ #
645
+ # def self.comments_average
646
+ # query.average(:comments_count)
647
+ # end
648
+ # end
649
+ def query(&blk)
650
+ @adapter.query(collection, self, &blk)
651
+ end
652
+
653
+ # Negates the filtering conditions of a given query with the logical
654
+ # opposite operator.
655
+ #
656
+ # This is only supported by the SqlAdapter.
657
+ #
658
+ # @param query [Object] a query
659
+ #
660
+ # @return a negated query, the type depends on the current adapter
661
+ #
662
+ # @api public
663
+ # @since 0.1.0
664
+ #
665
+ # @see Epiphy::Model::Adapters::Sql::Query#negate!
666
+ #
667
+ # @example
668
+ # require 'epiphy/model'
669
+ #
670
+ # class ProjectRepository
671
+ # include Epiphy::Repository
672
+ #
673
+ # def self.cool
674
+ # query do
675
+ # where(language: 'ruby')
676
+ # end
677
+ # end
678
+ #
679
+ # def self.not_cool
680
+ # exclude cool
681
+ # end
682
+ # end
683
+ def exclude(query)
684
+ query.negate!
685
+ query
686
+ end
687
+
688
+ # Determine colleciton/table name of this repository. Note that the
689
+ # repository name has to be the model name, appending Repository
690
+ #
691
+ # @return [Symbol] collection name
692
+ #
693
+ # @api public
694
+ # @since 0.1.0
695
+ #
696
+ # @see Epiphy::Adapter::Rethinkdb#get_table
697
+ def get_name
698
+ name = self.to_s.split('::').last
699
+ #end = Repository.length + 1
700
+ if name.nil?
701
+ return nil
702
+ end
703
+ name = name[0..-11].downcase.to_sym
704
+ end
705
+
706
+ # Determine entity name for this repository
707
+ # @return [String] entity name
708
+ #
709
+ # @api public
710
+ # @since 0.1.0
711
+ #
712
+ # @see self#get_name
713
+ def entity_name
714
+ name = self.to_s.split('::').last
715
+ if name.nil?
716
+ return nil
717
+ end
718
+ name[0..-11]
719
+ end
720
+
721
+ # Convert a hash into the entity object.
722
+ #
723
+ # Note that we require a Entity class same name with Repository class,
724
+ # only different is the suffix Repository.
725
+ #
726
+ # @param [Hash] value object
727
+ # @return [Epiphy::Entity] Entity
728
+ # @api public
729
+ # @since 0.1.0
730
+ def to_entity ahash
731
+ begin
732
+ name = entity_name
733
+ e = Object.const_get(name).new
734
+ ahash.each do |k,v|
735
+ e.send("#{k}=", v)
736
+ end
737
+ rescue
738
+ raise Epiphy::Model::EntityClassNotFound
739
+ end
740
+ e
741
+ end
742
+
743
+ # Convert all value of the entity into a document
744
+ #
745
+ # @param [Epiphy::Entity] Entity
746
+ # @return [Hash] hash object of entity value, except the nil value
747
+ #
748
+ # @api public
749
+ # @since 0.1.0
750
+ #
751
+ def to_document entity
752
+ document = {}
753
+ entity.instance_variables.each {|var| document[var.to_s.delete("@")] = entity.instance_variable_get(var) unless entity.instance_variable_get(var).nil? }
754
+ document
755
+ end
756
+
757
+ end
758
+ end
759
+ end