epiphy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3c60ab2f9d0a39156bab381f3217ad2c8fd210d8
4
+ data.tar.gz: 24fde7963aa8d74479e2a90030928b7fc8fc7d89
5
+ SHA512:
6
+ metadata.gz: 1e18a93aef681e795efd164d3467f7136fe6f938bea9d08448cfe854b4a2a4a50dd4b5ea4bc46c54a18f0bd6996c902c747449dd9448a0f3fd83cf1694ab39e5
7
+ data.tar.gz: f3549f8a25a32cc5bc2f5a6c565e2a081dd029653d499be1aa28743e293fa975de93c7f8b1a12b0915bb3e9d1153d5e25cbc156ada19fa2fc8c656a9c70f5b8c
@@ -0,0 +1,109 @@
1
+ # Epiphy [![wercker status](https://app.wercker.com/status/63dd458158948712a03a00d69a96f67b/m "wercker status")](https://app.wercker.com/project/bykey/63dd458158948712a03a00d69a96f67b)
2
+
3
+ ## [Simply RethinkDB](http://leanpub.com/simplyrethink)
4
+
5
+ I also write this book to practice RethinkDB. Please consider buying a
6
+ copy if you want to support the author.
7
+
8
+ # Introduction
9
+
10
+ A persistence framework for [RethinkDB](http://rethinkdb.com). The library is used on [phim365.today](http://phim365.today). Its API is based on Lotus::Model.
11
+
12
+ I love Lotus::Model so much because it's lightweight, does the job, no
13
+ magic. I also should fork Lotus and add RethinkDB adapter. However, I want
14
+ to learn more Ruby and reinvent the wheel because I didn't know how the
15
+ wheel was created. More than that, my bad code will not be able to make
16
+ it to Lotus::Model.
17
+
18
+ It delivers a convenient public API to execute queries and commands against a database.
19
+ The architecture eases keeping the business logic (entities) separated from details such as persistence or validations.
20
+
21
+ It implements the following concepts:
22
+
23
+ * [Entity](#entities) - An object defined by its identity.
24
+ * [Repository](#repositories) - An object that mediates between the entities and the persistence layer.
25
+ * [Adapter](#adapter) – A database adapter.
26
+ * [Query](#query) - An object that represents a database query.
27
+
28
+ `Epiphy` is name after `Epiphyllum`, my spouse's name.
29
+
30
+ # Install
31
+
32
+ ```
33
+ gem install epiphy
34
+ ```
35
+
36
+ or add
37
+
38
+ ```
39
+ gem 'epiphy'
40
+ ```
41
+
42
+ to your `Gemfile` if you use Bundle. Run `bundle install`
43
+
44
+
45
+ # Testing
46
+
47
+ `Minitest` is used for testing.
48
+
49
+ Make sure you have a working RethinkDB with default connection
50
+ information that is localhost, port 28015, without authentication key
51
+ and run
52
+
53
+ ```
54
+ $ bundle install
55
+ $ rake test
56
+ ```
57
+
58
+ A testing database will be created during the testing. The testing data
59
+ will hit your RethinkDB. Depend on your storge system, test can fast or
60
+ slow.
61
+
62
+ # Example
63
+
64
+ ```ruby
65
+ connection = Epiphy::Connection.create
66
+ adapter = Epiphy::Adapter::RethinkDB.new connection
67
+ RethinkDB::Repository.configure do |r|
68
+ r.adapter = adapter
69
+ end
70
+
71
+ class Movie
72
+ include Epiphy::Entity
73
+ include Epiphy::Entity::Timestamp
74
+
75
+ attributes :title, :url
76
+ end
77
+
78
+ class MovieRepository
79
+ include Epiphy::Repository
80
+ end
81
+
82
+ movie = MovieRepository.find id # Find by id
83
+
84
+ movie = MovieRepository.first
85
+ movie = MovieRepository.last
86
+
87
+ movie = Movie.new
88
+ movie.title = "A movie"
89
+ MovieRepository.create movie
90
+ MovieRepository.update movie
91
+
92
+
93
+
94
+ ```
95
+
96
+ # Contributing to epiphy
97
+
98
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
99
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
100
+ * Fork the project.
101
+ * Start a feature/bugfix branch.
102
+ * Commit and push until you are happy with your contribution.
103
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
104
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
105
+
106
+ # Copyright
107
+
108
+ Copyright (c) 2014 kureikain. See LICENSE.txt for
109
+ further details.
@@ -0,0 +1,2 @@
1
+ require 'rethinkdb'
2
+ require 'epiphy/model'
@@ -0,0 +1,7 @@
1
+ module Epiphy
2
+ module Adapter
3
+ class MissingDatabaseError < ::StandardError
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,114 @@
1
+ module Epiphy
2
+ module Adapter
3
+ class Rethinkdb
4
+ # RethinkDB method related. Should be in its helper
5
+ def insert_object
6
+ get_table.insert(
7
+ safe_params.merge(
8
+ id: params[:id],
9
+ created_at: Time.now,
10
+ updated_at: Time.now
11
+ ).merge(parents)
12
+ ).run(@connection)
13
+ :created
14
+ end
15
+
16
+ def update_object
17
+ get_table.update(
18
+ safe_params.merge(
19
+ id: params[:id],
20
+ updated_at: Time.now
21
+ )
22
+ ).run(@connection)
23
+ :ok
24
+ end
25
+
26
+ def replace_object
27
+ get_table.replace(
28
+ safe_params.merge(
29
+ id: params[:id],
30
+ created_at: Time.now,
31
+ updated_at: Time.now
32
+ ).merge(parents)
33
+ ).run(@connection)
34
+ :ok
35
+ end
36
+
37
+ def delete_object
38
+ get_table.get(params[:id]).delete(
39
+ :durability => "hard", :return_vals => false
40
+ ).run(@connection)
41
+ :no_content
42
+ end
43
+
44
+ def sort(qry)
45
+ ordering = params[:sort].split(",").map do |attr|
46
+ if attr[0] == "-"
47
+ r.desc(attr[1..-1].to_sym)
48
+ else
49
+ r.asc(attr.to_sym)
50
+ end
51
+ end
52
+
53
+ qry.order_by(*ordering)
54
+ end
55
+
56
+ def select(qry)
57
+ qry = qry.get_all(*params[:ids].split(",")) if params[:ids]
58
+ qry
59
+ end
60
+
61
+ def parents
62
+ params.select {|k,v| k.match(/\A[a-z0-9_]+_id\z/i) }.compact
63
+ end
64
+
65
+ def filter(qry)
66
+ parents.empty? ? qry : qry.filter(parents)
67
+ end
68
+
69
+ def attrs
70
+ [ :id ]
71
+ end
72
+
73
+ def get_range(qry)
74
+ begin
75
+ rhdr = request.headers[:HTTP_RANGE].split("=")
76
+
77
+ if rhdr[0] == collection
78
+ qry = qry[Range.new(*rhdr[1].split("-").map(&:to_i))]
79
+ end
80
+ rescue Exception => e
81
+ puts e.message
82
+ raise Exception.new(:bad_request)
83
+ end
84
+ qry
85
+ end
86
+
87
+ def get_records
88
+ qry = get_table
89
+ qry = sort(qry) if params[:sort]
90
+
91
+ fields = if params[:fields]
92
+ params[:fields].split(",").map {|f| f.to_sym }.select do |field|
93
+ attrs.include? field
94
+ end
95
+ else
96
+ attrs
97
+ end
98
+
99
+ qry = filter(select(qry)).pluck(fields)
100
+ qry = get_range(qry) if request.headers[:HTTP_RANGE]
101
+
102
+ qry.run(@connection).map do |record|
103
+ record.merge(href: some_url(record["id"]))
104
+ end
105
+ end
106
+
107
+ def get_object
108
+ get_table.filter(parents.merge({id: params[:id]})).pluck(attrs).run(@connection).first
109
+ end
110
+
111
+ end
112
+ end
113
+ end
114
+
@@ -0,0 +1,99 @@
1
+ require 'errors'
2
+
3
+ class Rethink
4
+ include RethinkDB::Shortcuts
5
+ include RethinkdbHelper
6
+ include Errors
7
+
8
+ def initialize
9
+ create_connect_if_need
10
+ @r = @@r.table(self.class.name.downcase)
11
+ end
12
+
13
+ def all
14
+ self
15
+ end
16
+
17
+ # Get a single element by primary key
18
+ def single(id)
19
+ @r = @r.get(id)
20
+ self
21
+ end
22
+
23
+ # Return raw RethinkDB object
24
+ def raw_query
25
+ @r
26
+ end
27
+
28
+ def reset
29
+ @r = @@r.table self.class.name.downcase
30
+ end
31
+
32
+ # Run a RQL
33
+ def get(r=nil)
34
+ if r.nil?
35
+ r = @r
36
+ end
37
+ r.run(@@rdb_connection)
38
+ end
39
+
40
+ [:update, "get_all", :limit, :order_by, :slice, :count, :filter].each do |name|
41
+ define_method(name) do |*arg, &block|
42
+ @r = @r.send(name, *arg, &block)
43
+ self
44
+ end
45
+ end
46
+
47
+ # Command work ona single object
48
+ #{:destroy => :delete}.each do |public_name, name|
49
+ #define_method(public_name) do |*arg, &block|
50
+ #r = @@r.table self.class.name.downcase
51
+ #r.get(*arg)
52
+ #r.send(name, *arg, &block)
53
+ #r.run @@rdb_connection
54
+ #end
55
+ #end
56
+
57
+ def destroy(id)
58
+ r = @@r.table self.class.name.downcase
59
+ r.get(id)
60
+ .delete()
61
+ .run(@@rdb_connection)
62
+ end
63
+
64
+ # Validation
65
+ def self.validate(args)
66
+ if args.has_key? :errors
67
+ raise ValidationError.new('Bad request', args[:errors])
68
+ end
69
+
70
+ args
71
+ end
72
+
73
+ def self.validate_present(args, *keys)
74
+ for k in keys
75
+ unless args.has_key?(k) and args[k].present?
76
+ args[:errors] ||= {}
77
+ args[:errors][k] ||= []
78
+ args[:errors][k] << "can't be blank."
79
+ end
80
+ end
81
+ args
82
+ end
83
+
84
+ def self.validate_email(args)
85
+ if args.has_key? :email
86
+ args[:email].downcase!
87
+
88
+ unless args[:email].present? and
89
+ args[:email].match EMAIL_REGEX
90
+
91
+ args[:errors] ||= {}
92
+ args[:errors][:email] ||= []
93
+ args[:errors][:email] << "is invalid."
94
+ end
95
+ end
96
+ args
97
+ end
98
+ end
99
+
@@ -0,0 +1,326 @@
1
+ require 'epiphy/adapter/error'
2
+ #require 'epiphy/adapter/helper'
3
+ module Epiphy
4
+ module Adapter
5
+ class Rethinkdb
6
+ include RethinkDB::Shortcuts
7
+ # Create an adapter object, with the option to pass a connection
8
+ # object as dependency.
9
+ #
10
+ # Without the dattabase string, Epiphy will assumes a `test`
11
+ # database just as RethinkDB do by default.
12
+ #
13
+ # @example
14
+ # connection = Epiphy::Connection.create
15
+ # adapter = Epiphy::Adapter::Rethinkdb.new(connection,
16
+ # default_database)
17
+ #
18
+ # @param connection [RethinkDB::Connection]
19
+ # @param database [String] database name
20
+ #
21
+ # @api public
22
+ # @since 0.0.1
23
+ #
24
+ #
25
+ def initialize(conn, database: 'test')
26
+ self.connection=(conn)
27
+ self.database=(database) unless database.nil?
28
+ end
29
+
30
+ # Assign a RethinkDB connection for this adapter.
31
+ #
32
+ # @param connection [RethinkDB::Connection]
33
+ # @return [Object] the current adapter. enable us to continue
34
+ # chain it
35
+ #
36
+ # @api public
37
+ # @since 0.1.0
38
+ def connection= (connection)
39
+ @connection = connection
40
+ end
41
+
42
+ # Set the current, default database for this connection.
43
+ #
44
+ # At, any time, you can re-set this to change the database.
45
+ # Doing so will trigger the adapter to switch to the new
46
+ # database.
47
+ #
48
+ # At RethinkDB level, this is similar with what we do with
49
+ # r.db('foo')
50
+ #
51
+ # @example
52
+ # adapter.database("test")
53
+ # # Subsequent query will be run on `test` database
54
+ # adapter.database("foo")
55
+ # # Subsequent query will be run on `foo` database
56
+ #
57
+ #
58
+ def database= (db)
59
+ @database = db
60
+ end
61
+
62
+ # Execute a ReQL query. The database and table is passed as
63
+ # parameter and the query is build by a block.
64
+ #
65
+ # The table param can be omitted so we can run the drop, create
66
+ # table
67
+ #
68
+ # With a valid database param, only this query will be run on it. To set
69
+ # a persitent different database for subsequent queries, consider
70
+ # set a different database.
71
+ #
72
+ # The block is passed 2 object. The first is the ReQL at the
73
+ # table() level. The second is the ReQL at top name space level
74
+ #
75
+ # @see Epiphy::Adapter::Rethinkdb#database=
76
+ #
77
+ # @example
78
+ # # To create a table
79
+ # adapter.query do |r, rt|
80
+ # r.table_create('table_name')
81
+ # end
82
+ #
83
+ # # To filter
84
+ # adapter.query table: 'movie' do |r|
85
+ # r.filter({category: 'romantic'})
86
+ # end
87
+ #
88
+ # # To Drop a database
89
+ # adapter.query do |t, r|
90
+ # r.db_drop 'dbname'
91
+ # end
92
+ #
93
+ # @param collection [Epiphy::Repository]
94
+ # @param table [String]
95
+ # @param database [String]
96
+ #
97
+ # @return query result of RethinkDB::run
98
+ #
99
+ # @since 0.0.1
100
+ # @api private
101
+ def query(table: nil, database: nil)
102
+ raise ArgumentError, 'Missing query block' unless block_given?
103
+ if block_given?
104
+ rql = get_table(table, database)
105
+ rql = yield(rql, r)
106
+ end
107
+ rql.run(@connection)
108
+ end
109
+
110
+ # Creates or updates a record in the database for the given entity.
111
+ #
112
+ # @param collection [Symbol] the target collection (it must be mapped).
113
+ # @param entity [#id, #id=] the entity to persist
114
+ #
115
+ # @return [Object] the entity
116
+ #
117
+ # @api private
118
+ # @since 0.1.0
119
+ def persist(collection, entity)
120
+ if entity["id"]
121
+ update(collection, entity)
122
+ else
123
+ create(collection, entity)
124
+ end
125
+ end
126
+
127
+ # Insert a document.
128
+ # @param collection [Symbol the target collection
129
+ # @param entity [#id, #id=] the entity to create
130
+ # @return [Object] the entity
131
+ #
132
+ # @api private
133
+ # @since 0.0.1
134
+ def create(collection, entity)
135
+ begin
136
+ result = query table: collection do |r|
137
+ r.insert(entity)
138
+ end
139
+ rescue
140
+ return false
141
+ end
142
+
143
+ if result["inserted"]==1
144
+ result["generated_keys"].first
145
+ end
146
+ end
147
+
148
+ # Insert a document.
149
+ # @param collection [Symbol the target collection
150
+ # @param entity [#id, #id=] the entity to create
151
+ # @return [Object] the entity
152
+ #
153
+ # @api private
154
+ # @since 0.0.1
155
+ def update(collection, entity)
156
+ begin
157
+ result = query table: collection do |r|
158
+ r.get(entity["id"]).update(entity)
159
+ end
160
+ rescue
161
+ return false
162
+ end
163
+ return result["replaced"]
164
+ end
165
+
166
+
167
+ # Returns all the records for the given collection
168
+ #
169
+ # @param collection [Symbol] the target collection (it must be mapped).
170
+ #
171
+ # @return [Array] all the records
172
+ #
173
+ # @api private
174
+ # @since 0.1.0
175
+ def all(collection)
176
+ # TODO consider to make this lazy (aka remove #all)
177
+ #query(collection).all
178
+ begin
179
+ query table: collection do |r|
180
+ r
181
+ end
182
+ rescue
183
+ return false
184
+ end
185
+ end
186
+
187
+ # Returns an unique record from the given collection, with the given
188
+ # id.
189
+ #
190
+ # @param collection [Symbol] the target collection (it must be mapped).
191
+ # @param id [Object] the identity of the object.
192
+ #
193
+ # @return [Object] the entity
194
+ #
195
+ # @api private
196
+ # @since 0.1.0
197
+ def find(collection, id)
198
+ #begin
199
+ result = query table: collection do |r|
200
+ r.get(id)
201
+ end
202
+ #rescue
203
+ #end
204
+ result
205
+ end
206
+
207
+ # Remove the record from the given collection, with the given id
208
+ #
209
+ # @param collection [Symbol] the target collection
210
+ # @param id [Object] the identity of the object
211
+ #
212
+ # @return [Object] the entity
213
+ #
214
+ # @api private
215
+ # @since 0.1.0
216
+ def delete(collection, id)
217
+ begin
218
+ result = query table: collection do |r|
219
+ r.get(id).delete()
220
+ end
221
+ if result["errors"] == 0
222
+ return result["deleted"]
223
+ end
224
+ return false
225
+ rescue
226
+ return false
227
+ end
228
+ end
229
+
230
+ # Delete all records from the table
231
+ #
232
+ # @param collection [Symbol] the target collection
233
+ # @return how many entry we removed
234
+ #
235
+ # @since 0.0.1
236
+ # @api private
237
+ def clear(collection)
238
+ begin
239
+ result = query table: collection do |r|
240
+ r.delete()
241
+ end
242
+ if result["errors"] == 0
243
+ return result['deleted']
244
+ end
245
+ return false
246
+ rescue
247
+ return false
248
+ end
249
+ end
250
+
251
+ # Returns the first record in the given collection.
252
+ #
253
+ # @param collection [Symbol] the target collection (it must be mapped).
254
+ #
255
+ # @return [Object] the first entity
256
+ #
257
+ # @api private
258
+ # @since 0.1.0
259
+ def first(collection)
260
+ _first(
261
+ query(collection).asc(_identity(collection))
262
+ )
263
+ end
264
+
265
+ # Returns the last record in the given collection.
266
+ #
267
+ # @param collection [Symbol] the target collection (it must be mapped).
268
+ #
269
+ # @return [Object] the last entity
270
+ #
271
+ # @api private
272
+ # @since 0.1.0
273
+ def last(collection)
274
+ _first(
275
+ query(collection).desc(_identity(collection))
276
+ )
277
+ end
278
+
279
+ private
280
+ def _collection(name)
281
+ raise NotImplementedError
282
+ end
283
+
284
+ def _mapped_collection(name)
285
+ @mapper.collection(name)
286
+ end
287
+
288
+ def _find(collection, id)
289
+ identity = _identity(collection)
290
+ query(collection).where(identity => _id(collection, identity, id))
291
+ end
292
+
293
+ def _first(query)
294
+ query.limit(1).first
295
+ end
296
+
297
+ def _identity(collection)
298
+ _mapped_collection(collection).identity
299
+ end
300
+
301
+ def _id(collection, column, value)
302
+ _mapped_collection(collection).deserialize_attribute(column, value)
303
+ end
304
+
305
+ protected
306
+
307
+ # Return a ReQL wrapper of a table to start query chaining.
308
+ #
309
+ # This is just a lighweight wrapper of `r.db().table()`
310
+ # @see
311
+ # @param database [string]. default to current database
312
+ # @param table [string]
313
+ # @api private
314
+ # @since 0.0.1
315
+ def get_table(table, database=nil)
316
+ database = @database if database.nil?
317
+ raise Epiphy::Adapter::MissingDatabaseError "Missing a default database name" if database.nil?
318
+ rql = r.db(database)
319
+ rql = rql.table(table) unless table.nil?
320
+ rql
321
+ end
322
+
323
+
324
+ end
325
+ end
326
+ end