lotus-rethinkdb 0.1.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,364 @@
1
+ require 'forwardable'
2
+ require 'lotus/utils/kernel'
3
+ require 'rethinkdb'
4
+
5
+ module Lotus
6
+ module Model
7
+ module Adapters
8
+ module Rethinkdb
9
+ # Query the database with a powerful API.
10
+ #
11
+ # All the methods are chainable, it allows advanced composition of
12
+ # ReQL conditions.
13
+ #
14
+ # This works as a lazy filtering mechanism: the documents are fetched
15
+ # from the database only when needed.
16
+ #
17
+ # @example
18
+ #
19
+ # query.where(language: 'ruby')
20
+ # .and(framework: 'lotus')
21
+ # .desc(:users_count).all
22
+ #
23
+ # # the documents are fetched only when we invoke #all
24
+ #
25
+ # It implements Ruby's `Enumerable` and borrows some methods from
26
+ # `Array`. Expect a query to act like them.
27
+ #
28
+ # @since 0.1.0
29
+ class Query
30
+ include RethinkDB::Shortcuts
31
+ include Enumerable
32
+ extend Forwardable
33
+
34
+ def_delegators :all, :each, :to_s, :empty?
35
+
36
+ # @attr_reader conditions [Array] an accumulator for the called
37
+ # methods
38
+ #
39
+ # @since 0.1.0
40
+ # @api private
41
+ attr_reader :conditions
42
+
43
+ # Initialize a query
44
+ #
45
+ # @param collection [Lotus::Model::Adapters::Rethinkdb::Collection]
46
+ # the collection to query
47
+ #
48
+ # @param blk [Proc] an optional block that gets yielded in the
49
+ # context of the current query
50
+ #
51
+ # @return [Lotus::Model::Adapters::Rethinkdb::Query]
52
+ def initialize(collection, context = nil, &blk)
53
+ @collection, @context = collection, context
54
+ @conditions = []
55
+
56
+ instance_eval(&blk) if block_given?
57
+ end
58
+
59
+ # Resolves the query by fetching documents from the database and
60
+ # translating them into entities.
61
+ #
62
+ # @return [Array] a collection of entities
63
+ #
64
+ # @since 0.1.0
65
+ def all
66
+ scoped.execute
67
+ end
68
+
69
+ # Adds a condition like SQL `WHERE` using r.filter().
70
+ #
71
+ # It accepts a `Hash` with only one pair.
72
+ # The key must be the name of the field expressed as a `Symbol`.
73
+ # The value is the one used by the ReQL query
74
+ #
75
+ # @param condition [Hash]
76
+ #
77
+ # @return self
78
+ #
79
+ # @since 0.1.0
80
+ #
81
+ # @example Fixed value
82
+ #
83
+ # query.where(language: 'ruby')
84
+ #
85
+ # # => r.filter(language: 'ruby')
86
+ #
87
+ # @example Multiple conditions
88
+ #
89
+ # query.where(language: 'ruby')
90
+ # .where(framework: 'lotus')
91
+ #
92
+ # # => r.filter(language: 'ruby').filter('framework: 'lotus')
93
+ #
94
+ # @example Blocks
95
+ #
96
+ # query.where { |doc| doc['age'] > 10 }
97
+ #
98
+ # # => r.filter { |doc| doc.bracket('age').gt('10') }
99
+ def where(condition = nil, &blk)
100
+ condition = condition || blk ||
101
+ fail(ArgumentError, 'You need to specify a condition.')
102
+ conditions.push([:filter, condition])
103
+ self
104
+ end
105
+
106
+ alias_method :and, :where
107
+
108
+ # Pluck only the specified fields. Documents without the fields are
109
+ # omitted.
110
+ #
111
+ # By default a query includes all the fields of a table.
112
+ #
113
+ # @param fields [Array<Symbol>]
114
+ #
115
+ # @return self
116
+ #
117
+ # @since 0.1.0
118
+ #
119
+ # @example Single field
120
+ #
121
+ # query.pluck(:name)
122
+ #
123
+ # # => r.pluck(:name)
124
+ #
125
+ # @example Multiple fields
126
+ #
127
+ # query.pluck(:name, :year)
128
+ #
129
+ # # => r.pluck(:name, :year)
130
+ def pluck(*fields)
131
+ conditions.push([:pluck, *fields])
132
+ self
133
+ end
134
+
135
+ # Limit the number of documents to return.
136
+ #
137
+ # This operation is performed at the database level with r.limit().
138
+ #
139
+ # @param number [Fixnum]
140
+ #
141
+ # @return self
142
+ #
143
+ # @since 0.1.0
144
+ #
145
+ # @example
146
+ #
147
+ # query.limit(1)
148
+ #
149
+ # # => r.limit(1)
150
+ def limit(number)
151
+ conditions.push([:limit, number])
152
+ self
153
+ end
154
+
155
+ # Specify the ascending order of the documents, sorted by the given
156
+ # fields or index. Identify an index using `{ index: :key }`.
157
+ #
158
+ # The last invokation of this method takes precidence. Previously
159
+ # called sorts will be overwritten by RethinkDB.
160
+ #
161
+ # @param fields [Array<Symbol, Hash>] the field names, optionally with
162
+ # an index identifier
163
+ #
164
+ # @return self
165
+ #
166
+ # @since 0.1.0
167
+ #
168
+ # @see Lotus::Model::Adapters::Rethinkdb::Query#desc
169
+ #
170
+ # @example Single field
171
+ #
172
+ # query.order(:name)
173
+ #
174
+ # # => r.order_by(:name)
175
+ #
176
+ # @example Multiple columns
177
+ #
178
+ # query.order(:name, :year)
179
+ #
180
+ # # => r.order_by(:name, :year)
181
+ #
182
+ # @example Single index
183
+ #
184
+ # query.order(index: :date)
185
+ #
186
+ # # => r.order_by(index: :date)
187
+ #
188
+ # @example Mixed fields and index
189
+ #
190
+ # query.order(:name, :year, index: :date)
191
+ #
192
+ # # => r.order_by(:name, :year, index: :date)
193
+ def order(*fields)
194
+ conditions.push([:order_by, *fields])
195
+ self
196
+ end
197
+
198
+ alias_method :asc, :order
199
+
200
+ # Specify the descending order of the documents, sorted by the given
201
+ # fields or index. Identify an index using `{ index: :key }`.
202
+ #
203
+ # The last invokation of this method takes precidence. Previously
204
+ # called sorts will be overwritten by RethinkDB.
205
+ #
206
+ # @return self
207
+ #
208
+ # @since 0.1.0
209
+ #
210
+ # @see Lotus::Model::Adapters::Rethinkdb::Query#desc
211
+ #
212
+ # @example Single field
213
+ #
214
+ # query.desc(:name)
215
+ #
216
+ # # => r.order_by(r.desc(:name))
217
+ #
218
+ # @example Multiple columns
219
+ #
220
+ # query.desc(:name, :year)
221
+ #
222
+ # # => r.order_by(r.desc(:name), r.desc(:year))
223
+ #
224
+ # @example Single index
225
+ #
226
+ # query.desc(index: :date)
227
+ #
228
+ # # => r.order_by(index: r.desc(:date))
229
+ #
230
+ # @example Mixed fields and index
231
+ #
232
+ # query.desc(r.desc(:name), r.desc(:year), index: r.desc(:date))
233
+ #
234
+ # # => r.order_by(:name, :year, index: :date)
235
+ def desc(*fields)
236
+ conditions.push([:order_by, *_desc_wrapper(*fields)])
237
+ self
238
+ end
239
+
240
+ # Apply all the conditions and returns a filtered collection.
241
+ #
242
+ # This operation is idempotent, and the returned result didn't
243
+ # fetched the documents yet.
244
+ #
245
+ # @return [Lotus::Model::Adapters::Rethinkdb::Collection]
246
+ #
247
+ # @since 0.1.0
248
+ def scoped
249
+ scope = @collection
250
+
251
+ conditions.each do |(method, *args)|
252
+ scope = scope.public_send(method, *args)
253
+ end
254
+
255
+ scope
256
+ end
257
+
258
+ protected
259
+
260
+ # Handles missing methods for query combinations
261
+ #
262
+ # @api private
263
+ # @since 0.1.0
264
+ #
265
+ # @see Lotus::Model::Adapters:Rethinkdb::Query#apply
266
+ def method_missing(m, *args, &blk)
267
+ if @context.respond_to?(m)
268
+ apply @context.public_send(m, *args, &blk)
269
+ else
270
+ super
271
+ end
272
+ end
273
+
274
+ private
275
+
276
+ # Returns a new query that is the result of the merge of the current
277
+ # conditions with the ones of the given query.
278
+ #
279
+ # This is used to combine queries together in a Repository.
280
+ #
281
+ # @param query [Lotus::Model::Adapters::Rethinkdb::Query] the query
282
+ # to apply
283
+ #
284
+ # @return [Lotus::Model::Adapters::Rethinkdb::Query] a new query with
285
+ # the merged conditions
286
+ #
287
+ # @api private
288
+ # @since 0.1.0
289
+ #
290
+ # @example
291
+ # require 'lotus/model'
292
+ #
293
+ # class ArticleRepository
294
+ # include Lotus::Repository
295
+ #
296
+ # def self.by_author(author)
297
+ # query do
298
+ # where(author_id: author.id)
299
+ # end
300
+ # end
301
+ #
302
+ # def self.rank
303
+ # query.desc(:comments_count)
304
+ # end
305
+ #
306
+ # def self.rank_by_author(author)
307
+ # rank.by_author(author)
308
+ # end
309
+ # end
310
+ #
311
+ # # The code above combines two queries: `rank` and `by_author`.
312
+ # #
313
+ # # The first class method `rank` returns a `Rethinkdb::Query`
314
+ # # instance which doesn't respond to `by_author`. How to solve
315
+ # # this problem?
316
+ # #
317
+ # # 1. When we use `query` to fabricate a `Rethinkdb::Query` we
318
+ # # pass the current context (the repository itself) to the query
319
+ # # initializer.
320
+ # #
321
+ # # 2. When that query receives the `by_author` message, it's
322
+ # # captured by `method_missing` and dispatched to the repository.
323
+ # #
324
+ # # 3. The class method `by_author` returns a query too.
325
+ # #
326
+ # # 4. We just return a new query that is the result of the current
327
+ # # query's conditions (`rank`) and of the conditions from
328
+ # # `by_author`.
329
+ # #
330
+ # # You're welcome ;)
331
+ def apply(query)
332
+ dup.tap do |result|
333
+ result.conditions.push(*query.conditions)
334
+ end
335
+ end
336
+
337
+ # Wrap the given fields with a desc operator.
338
+ #
339
+ # @return [Array] the wrapped fields
340
+ #
341
+ # @api private
342
+ # @since 0.1.0
343
+ def _desc_wrapper(*fields)
344
+ Array(fields).map do |field|
345
+ if field.is_a?(Hash)
346
+ field.merge(field) { |_k, v| r.desc(v) }
347
+ else
348
+ r.desc(field)
349
+ end
350
+ end
351
+ end
352
+
353
+ # Run the enclosed block on the database.
354
+ #
355
+ # @api private
356
+ # @since 0.1.0
357
+ def _run
358
+ yield.run(@connection)
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end
@@ -0,0 +1,234 @@
1
+ require 'lotus/model/adapters/abstract'
2
+ require 'lotus/model/adapters/rethinkdb/collection'
3
+ require 'lotus/model/adapters/rethinkdb/command'
4
+ require 'lotus/model/adapters/rethinkdb/query'
5
+ require 'rethinkdb'
6
+
7
+ module Lotus
8
+ module Model
9
+ module Adapters
10
+ # Adapter for RethinkDB databases
11
+ #
12
+ # @see Lotus::Model::Adapters::Implementation
13
+ #
14
+ # @api private
15
+ # @since 0.1.0
16
+ class RethinkdbAdapter < Abstract
17
+ include ::RethinkDB::Shortcuts
18
+
19
+ # Initialize the adapter.
20
+ #
21
+ # Lotus::Model uses RethinkDB.
22
+ #
23
+ # @param mapper [Object] the database mapper
24
+ # @param connection [RethinkDB::Connection] the database connection
25
+ #
26
+ # @return [Lotus::Model::Adapters::RethinkdbAdapter]
27
+ #
28
+ # @see Lotus::Model::Mapper
29
+ # @see http://rethinkdb.com/api/ruby/
30
+ #
31
+ # @api private
32
+ # @since 0.1.0
33
+ def initialize(mapper, connection)
34
+ super(mapper)
35
+ @connection = connection
36
+ end
37
+
38
+ # Creates or updates a document in the database for the given entity.
39
+ #
40
+ # @param collection [Symbol] the target collection (it must be mapped).
41
+ # @param entity [#id, #id=] the entity to persist
42
+ #
43
+ # @return [Object] the entity
44
+ #
45
+ # @api private
46
+ # @since 0.1.0
47
+ def persist(collection, entity)
48
+ if entity.id
49
+ update(collection, entity)
50
+ else
51
+ create(collection, entity)
52
+ end
53
+ end
54
+
55
+ # Creates a document in the database for the given entity.
56
+ # It assigns the `id` attribute, in case of success.
57
+ #
58
+ # @param collection [Symbol] the target collection (it must be mapped).
59
+ # @param entity [#id=] the entity to create
60
+ #
61
+ # @return [Object] the entity
62
+ #
63
+ # @api private
64
+ # @since 0.1.0
65
+ def create(collection, entity)
66
+ entity.id = command(
67
+ query(collection)
68
+ ).create(entity)
69
+ entity
70
+ end
71
+
72
+ # Updates a document in the database corresponding to the given entity.
73
+ #
74
+ # @param collection [Symbol] the target collection (it must be mapped).
75
+ # @param entity [#id] the entity to update
76
+ #
77
+ # @return [Object] the entity
78
+ #
79
+ # @api private
80
+ # @since 0.1.0
81
+ def update(collection, entity)
82
+ command(
83
+ _find(collection, entity.id)
84
+ ).update(entity)
85
+ end
86
+
87
+ # Deletes a document in the database corresponding to the given entity.
88
+ #
89
+ # @param collection [Symbol] the target collection (it must be mapped).
90
+ # @param entity [#id] the entity to delete
91
+ #
92
+ # @api private
93
+ # @since 0.1.0
94
+ def delete(collection, entity)
95
+ command(
96
+ _find(collection, entity.id)
97
+ ).delete
98
+ end
99
+
100
+ # Returns all the documents for the given collection
101
+ #
102
+ # @param collection [Symbol] the target collection (it must be mapped).
103
+ #
104
+ # @return [Array] all the documents
105
+ #
106
+ # @api private
107
+ # @since 0.1.0
108
+ def all(collection)
109
+ query(collection).all
110
+ end
111
+
112
+ # Returns a unique document from the given collection, with the given
113
+ # id.
114
+ #
115
+ # @param collection [Symbol] the target collection (it must be mapped).
116
+ # @param id [Object] the identity of the object.
117
+ #
118
+ # @return [Object] the entity
119
+ #
120
+ # @api private
121
+ # @since 0.1.0
122
+ def find(collection, id)
123
+ _first(
124
+ _find(collection, id)
125
+ )
126
+ end
127
+
128
+ # This method is not implemented. RethinkDB does not have sequential
129
+ # primary keys.
130
+ #
131
+ # @param _collection [Symbol] the target collection (it must be mapped)
132
+ #
133
+ # @raise [NotImplementedError]
134
+ #
135
+ # @since 0.1.0
136
+ def first(_collection)
137
+ fail NotImplementedError
138
+ end
139
+
140
+ # This method is not implemented. RethinkDB does not have sequential
141
+ # primary keys.
142
+ #
143
+ # @param _collection [Symbol] the target collection (it must be mapped)
144
+ #
145
+ # @raise [NotImplementedError]
146
+ #
147
+ # @since 0.1.0
148
+ def last(_collection)
149
+ fail NotImplementedError
150
+ end
151
+
152
+ # Deletes all the documents from the given collection.
153
+ #
154
+ # @param collection [Symbol] the target collection (it must be mapped).
155
+ #
156
+ # @api private
157
+ # @since 0.1.0
158
+ def clear(collection)
159
+ command(query(collection)).clear
160
+ end
161
+
162
+ # Fabricates a command for the given query.
163
+ #
164
+ # @param query [Lotus::Model::Adapters::Rethinkdb::Query] the query
165
+ # object to act on.
166
+ #
167
+ # @return [Lotus::Model::Adapters::Rethinkdb::Command]
168
+ #
169
+ # @see Lotus::Model::Adapters::Rethinkdb::Command
170
+ #
171
+ # @api private
172
+ # @since 0.1.0
173
+ def command(query)
174
+ Rethinkdb::Command.new(query)
175
+ end
176
+
177
+ # Fabricates a query
178
+ #
179
+ # @param collection [Symbol] the target collection (it must be mapped).
180
+ # @param blk [Proc] a block of code to be executed in the context of
181
+ # the query.
182
+ #
183
+ # @return [Lotus::Model::Adapters::Rethinkdb::Query]
184
+ #
185
+ # @see Lotus::Model::Adapters::Rethinkdb::Query
186
+ #
187
+ # @api private
188
+ # @since 0.1.0
189
+ def query(collection, context = nil, &blk)
190
+ Rethinkdb::Query.new(_collection(collection), context, &blk)
191
+ end
192
+
193
+ private
194
+
195
+ # Returns a collection from the given name.
196
+ #
197
+ # @param name [Symbol] a name of the collection (it must be mapped).
198
+ #
199
+ # @return [Lotus::Model::Adapters::Rethinkdb::Collection]
200
+ #
201
+ # @see Lotus::Model::Adapters::Rethinkdb::Collection
202
+ #
203
+ # @api private
204
+ # @since 0.1.0
205
+ def _collection(name)
206
+ Rethinkdb::Collection.new(
207
+ @connection, r.table(name), _mapped_collection(name)
208
+ )
209
+ end
210
+
211
+ def _mapped_collection(name)
212
+ @mapper.collection(name)
213
+ end
214
+
215
+ def _find(collection, id)
216
+ identity = _identity(collection)
217
+ query(collection).where(identity => _id(collection, identity, id))
218
+ end
219
+
220
+ def _first(query)
221
+ query.limit(1).first
222
+ end
223
+
224
+ def _identity(collection)
225
+ _mapped_collection(collection).identity
226
+ end
227
+
228
+ def _id(collection, column, value)
229
+ _mapped_collection(collection).deserialize_attribute(column, value)
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,16 @@
1
+ module Lotus
2
+ module Model
3
+ module Adapters
4
+ # Adapter for RethinkDB databases
5
+ #
6
+ # @api private
7
+ # @since 0.1.0
8
+ module Rethinkdb
9
+ # Defines the version
10
+ #
11
+ # @since 0.1.0
12
+ VERSION = '0.1.1'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,2 @@
1
+ require 'lotus/model' # rubocop:disable Style/FileName
2
+ require 'lotus/model/adapters/rethinkdb_adapter'
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'lotus/rethinkdb/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'lotus-rethinkdb'
8
+ spec.version = Lotus::Model::Adapters::Rethinkdb::VERSION
9
+ spec.authors = ['Angelo Ashmore']
10
+ spec.email = ['angeloashmore@gmail.com']
11
+ spec.summary = 'RethinkDB adapter for Lotus::Model'
12
+ spec.description = 'RethinkDB adapter for Lotus::Model'
13
+ spec.homepage = 'https://github.com/angeloashmore/lotus-rethinkdb'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split("\n")
17
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_runtime_dependency 'lotus-model', '~> 0.2'
22
+ spec.add_runtime_dependency 'rethinkdb', '~> 1.15'
23
+ spec.add_runtime_dependency 'activesupport', '~> 4.2'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.7'
26
+ spec.add_development_dependency 'minitest', '~> 5.5'
27
+ spec.add_development_dependency 'minitest-line', '~> 0.6.2'
28
+ spec.add_development_dependency 'rake', '~> 10.0'
29
+ end
File without changes