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.
- checksums.yaml +7 -0
- data/README.md +109 -0
- data/lib/epiphy.rb +2 -0
- data/lib/epiphy/adapter/error.rb +7 -0
- data/lib/epiphy/adapter/helper.rb +114 -0
- data/lib/epiphy/adapter/model.rb +99 -0
- data/lib/epiphy/adapter/rethinkdb.rb +326 -0
- data/lib/epiphy/connection.rb +17 -0
- data/lib/epiphy/entity.rb +152 -0
- data/lib/epiphy/model.rb +37 -0
- data/lib/epiphy/repository.rb +759 -0
- data/lib/epiphy/repository/configuration.rb +33 -0
- data/lib/epiphy/repository/cursor.rb +47 -0
- data/lib/epiphy/version.rb +6 -0
- metadata +100 -0
@@ -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
|
+
|
data/lib/epiphy/model.rb
ADDED
@@ -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
|