reorm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +219 -0
- data/Rakefile +2 -0
- data/config/database.yml +11 -0
- data/lib/reorm/configuration.rb +12 -0
- data/lib/reorm/cursor.rb +162 -0
- data/lib/reorm/exceptions.rb +13 -0
- data/lib/reorm/field_path.rb +53 -0
- data/lib/reorm/model.rb +132 -0
- data/lib/reorm/modules/database_modules.rb +67 -0
- data/lib/reorm/modules/event_modules.rb +82 -0
- data/lib/reorm/modules/validation_modules.rb +29 -0
- data/lib/reorm/modules.rb +7 -0
- data/lib/reorm/property_errors.rb +53 -0
- data/lib/reorm/validators/exclusion_validator.rb +18 -0
- data/lib/reorm/validators/inclusion_validator.rb +18 -0
- data/lib/reorm/validators/maximum_length_validator.rb +19 -0
- data/lib/reorm/validators/minimum_length_validator.rb +19 -0
- data/lib/reorm/validators/presence_validator.rb +17 -0
- data/lib/reorm/validators/validator.rb +13 -0
- data/lib/reorm/validators.rb +10 -0
- data/lib/reorm/version.rb +6 -0
- data/lib/reorm.rb +47 -0
- data/reorm.gemspec +30 -0
- data/spec/catwalk/modules/timestamped_spec.rb +17 -0
- data/spec/reorm/cursor_spec.rb +214 -0
- data/spec/reorm/field_path_spec.rb +65 -0
- data/spec/reorm/model_spec.rb +268 -0
- data/spec/reorm/modules/event_source_spec.rb +49 -0
- data/spec/reorm/modules/table_backed_spec.rb +46 -0
- data/spec/reorm/modules/timestamped_spec.rb +28 -0
- data/spec/reorm/modules/validation_modules_spec.rb +157 -0
- data/spec/reorm/property_errors_spec.rb +120 -0
- data/spec/reorm/validators/exclusion_validator_spec.rb +34 -0
- data/spec/reorm/validators/inclusion_validator_spec.rb +36 -0
- data/spec/reorm/validators/maximum_length_validator_spec.rb +37 -0
- data/spec/reorm/validators/minimum_length_validator_spec.rb +39 -0
- data/spec/reorm/validators/presence_validator_spec.rb +44 -0
- data/spec/reorm/validators/validator_spec.rb +23 -0
- data/spec/spec_helper.rb +118 -0
- metadata +216 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c4d147f8ec8ab156ae124379903853b37a6aec29
|
4
|
+
data.tar.gz: c53da10347253a776ecfea2cee5d56d9f49e1622
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 356d4b82f8285da450df48392baa417df7760f81c7ebf866eb74257ab0e2352bcfd0c7b2f0e5682f925217e528b2a2e29c3b97c89f1a2376b2247e571cd7ddbd
|
7
|
+
data.tar.gz: 0e412ac34981e3f3551538e38263e6b88a5be80333d06e5dfa5f5f6d323d0cc9980fb5b22137a6abce7cd8e15f35e9869c9998ac593451b487c867a0e4fdcf4b
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Peter Wood
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
# Reorm
|
2
|
+
|
3
|
+
A (possibly naive) ORM for use with the RethinkDB driver for Ruby. The library
|
4
|
+
is heavily influenced by the implementations of the active record pattern from
|
5
|
+
the Sequel and ActiveRecord libraries. I'm not 100% sure that this is a good
|
6
|
+
match for a document oriented store such as RethinkDB but here it is anyway.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'reorm'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install reorm
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
First things first, the library needs to be configured to connect to your
|
27
|
+
RethinkDB instance. The simplest way to do this is create a file called
|
28
|
+
database.yml in the current working directory. Place content like the following
|
29
|
+
into this file...
|
30
|
+
|
31
|
+
defaults: &defaults
|
32
|
+
host: localhost
|
33
|
+
port: 28015
|
34
|
+
|
35
|
+
development:
|
36
|
+
<<: *defaults
|
37
|
+
db: reorm_dev
|
38
|
+
|
39
|
+
test:
|
40
|
+
<<: *defaults
|
41
|
+
db: reorm_test
|
42
|
+
|
43
|
+
This will be picked up by the library and used to create a connection based on
|
44
|
+
it current environment (this defaults to development but if you set either the
|
45
|
+
RAILS_ENV or RACK_ENV environment variables that will be used instead). The
|
46
|
+
example above connects to the same RethinkDB instance, on localhost at port
|
47
|
+
28015, and then defaults the database it will use based on the enviornment
|
48
|
+
setting. Note the configuration options are more complicated and flexible than
|
49
|
+
using this approach but more on that later.
|
50
|
+
|
51
|
+
Next, declare a model class like so...
|
52
|
+
|
53
|
+
class MyModel < Reorm::Model
|
54
|
+
end
|
55
|
+
|
56
|
+
This small amount of code declares a class that will expect to save its data
|
57
|
+
elements into a table called my_models (it will create this table if it does not
|
58
|
+
already exist) and with an assumption that it uses a primary key called id. You
|
59
|
+
can change the table name used like so...
|
60
|
+
|
61
|
+
class MyModel < Reorm::Model
|
62
|
+
table_name "other_records"
|
63
|
+
end
|
64
|
+
|
65
|
+
You could now create an instance of your model and save it to the database like
|
66
|
+
so...
|
67
|
+
|
68
|
+
model = MyModel.create(one: 1, two: 2, three: {four: 4})
|
69
|
+
|
70
|
+
Once a model has been created like this it will have all of the top level fields
|
71
|
+
specified as parameters available as properties from the model object generated.
|
72
|
+
So, for example, you could access some of the value from the object created
|
73
|
+
above in code as follows...
|
74
|
+
|
75
|
+
model.one # = 1
|
76
|
+
model.two # = 2
|
77
|
+
model.three # = {four: 4}
|
78
|
+
|
79
|
+
When an object is created it will automatically have its primary key filled out
|
80
|
+
by RethinkDB. By default models use a primary key called id but you can change
|
81
|
+
this by adding code like the following to you class declaration...
|
82
|
+
|
83
|
+
primary_key :my_key
|
84
|
+
|
85
|
+
This will change the primary key used by the class to the field specified to
|
86
|
+
the call to ```primary_key```. When a primary key value has been generated by
|
87
|
+
RethinkDB its also available as a property from the object. Given the create
|
88
|
+
example above the models primary key can be accessed like any other property of
|
89
|
+
the object...
|
90
|
+
|
91
|
+
model.id # RethinkDB generated primary key value.
|
92
|
+
|
93
|
+
To retrieve models back from the database you call either call the ```#all()```
|
94
|
+
method or use the ```#filter()``` method that is available on all model classes.
|
95
|
+
For example, if you had a model called User, you could use this code to iterate
|
96
|
+
across all user records...
|
97
|
+
|
98
|
+
User.all.each do |user|
|
99
|
+
# Do some stuff here.
|
100
|
+
...
|
101
|
+
end
|
102
|
+
|
103
|
+
Or you could search for a user with a particular email address using code like
|
104
|
+
the following...
|
105
|
+
|
106
|
+
user = User.filter({email: "user@email.com"}).first
|
107
|
+
|
108
|
+
If I wanted all users whose email address had a particular domain then I would
|
109
|
+
use code like the following...
|
110
|
+
|
111
|
+
users = User.filter {|record| record["email"].match("@gmail.com$")}
|
112
|
+
users.each do |user|
|
113
|
+
# Do some stuff here.
|
114
|
+
...
|
115
|
+
end
|
116
|
+
|
117
|
+
Note that in the predicate passed to the filter method the value passed to the
|
118
|
+
block is the raw record and not an instance of the model class. This means that
|
119
|
+
you have to dereference field values as you would from a Hash. When using the
|
120
|
+
output of the filter however you will receive model class instances and can
|
121
|
+
use those as normal.
|
122
|
+
|
123
|
+
The filter code is based on the RethinkDB filter functionality so consult the
|
124
|
+
documentation for more information.
|
125
|
+
|
126
|
+
### Validations
|
127
|
+
|
128
|
+
Model classes can provide functionality that allows them to be validated to
|
129
|
+
ensure that their data settings are consistent. To do this implement a method on
|
130
|
+
your class called ```#validate()```. The first thing to do in this method is to
|
131
|
+
make a call to the parent class implementation of this method via a call to
|
132
|
+
```super``` - this is important so don't forget to do it! After that you can
|
133
|
+
perform tests on the objects settings and add errors to the model where you
|
134
|
+
discover discrepancies. For example...
|
135
|
+
|
136
|
+
def validate
|
137
|
+
super
|
138
|
+
if [nil, ""].include?(email)
|
139
|
+
errors.add(:email, "cannot be blank.")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
The library provides a number of helper method to shortcut some common
|
144
|
+
validations such as the following...
|
145
|
+
|
146
|
+
def validate
|
147
|
+
super
|
148
|
+
validate_presence_of :email
|
149
|
+
end
|
150
|
+
|
151
|
+
Would do the same thing as the first version of the ```validate()``` method
|
152
|
+
shown above. Some other examples include...
|
153
|
+
|
154
|
+
def validate
|
155
|
+
super
|
156
|
+
validate_length_of :field1, minimum: 5, maximum: 20
|
157
|
+
validate_inclusion_of :field2, "One", "Of", "These", "Values"
|
158
|
+
validate_exclusion_of :field3 "Not", "One", "Of", "These"
|
159
|
+
end
|
160
|
+
|
161
|
+
If you call te ```valid?()``` method on a model then the model object will be
|
162
|
+
validated and this method will return true if no errors were set and false if at
|
163
|
+
least one error was set. You can view the errors for a model by accessing its
|
164
|
+
```errors``` property which returns an instance of the
|
165
|
+
```Reorm::Reorm::PropertyErrors``` class.
|
166
|
+
|
167
|
+
Note that a object is automatically validated any time it is saved and that the
|
168
|
+
save request will fail if the object fails validation and an exception will be
|
169
|
+
raised. You can turn validation off for a save by passing false as a parameter
|
170
|
+
to the save call.
|
171
|
+
|
172
|
+
### Event Callbacks
|
173
|
+
|
174
|
+
The library supports a number of event related callbacks that will be invoked
|
175
|
+
when a specific event occurs. You can add an event related callback to a model
|
176
|
+
by declaring it within the model class like so...
|
177
|
+
|
178
|
+
before_save :method_name
|
179
|
+
|
180
|
+
In this case the library will attempt to invoked a method called ```method_name```
|
181
|
+
on the model object after validation but before the object is actually written
|
182
|
+
to RethinkDB. Note the method has to actually exist on the model for this to
|
183
|
+
work. The following callbacks, in the order in which they occur, are available -
|
184
|
+
before_validate, after_validate, before_create, before_update, before_save,
|
185
|
+
after_save, after_update and after_create.
|
186
|
+
|
187
|
+
### Unit Tests
|
188
|
+
|
189
|
+
To run the unit tests you'll need a locally running instance of RethinkDB (none
|
190
|
+
of that nonsense mocking out DB writes rubbish here!) and then you can run the
|
191
|
+
available unit tests using the ```rspec``` command.
|
192
|
+
|
193
|
+
### Configuration
|
194
|
+
|
195
|
+
The library, on load up, looks for a file containing the RethinkDB configuration
|
196
|
+
details. It will settle upon the first file that it finds called database.yml,
|
197
|
+
rethinkdb.yml or application.yml (note a .json extension is also acceptable).
|
198
|
+
This file will be expected to contain a Hash in the appropriate format. Within
|
199
|
+
this Hash it will first look for a an environment base entry (i.e. a key of
|
200
|
+
'development', 'production' or 'test'). If it finds no such key it will assume
|
201
|
+
that the entire Hash is the configuration, otherwise it will extract the entry
|
202
|
+
under the given key and use that as configuration.
|
203
|
+
|
204
|
+
Next it takes the output from the previous section and looks for an entry keyed
|
205
|
+
under 'rethinkdb'. Again, if it does not find it, it will assume the entire
|
206
|
+
entry is its configuration, otherwise it will focus down on the keyed entry
|
207
|
+
once more. The reasoning here is to allow you to have a separate standalone
|
208
|
+
database configuration file or to allow your database configuration values to
|
209
|
+
be part of a larger configuration file. The output from this process is expected
|
210
|
+
to be the parmaeters that will allow the library to connect to a RethinkDB
|
211
|
+
server.
|
212
|
+
|
213
|
+
## Contributing
|
214
|
+
|
215
|
+
1. Fork it ( https://github.com/[my-github-username]/reorm/fork )
|
216
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
217
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
218
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
219
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/config/database.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
# Copyright (c), 2015 Peter Wood
|
3
|
+
# See the license.txt for details of the licensing of the code in this file.
|
4
|
+
|
5
|
+
module Reorm
|
6
|
+
class Configuration < Configurative::Settings
|
7
|
+
files *(Dir.glob(File.join(Dir.getwd, "**", "database.{yml,yaml,json}")) +
|
8
|
+
Dir.glob(File.join(Dir.getwd, "**", "rethinkdb.{yml,yaml,json}")) +
|
9
|
+
Dir.glob(File.join(Dir.getwd, "**", "application.{yml,yaml,json}")))
|
10
|
+
#section "rethinkdb"
|
11
|
+
end
|
12
|
+
end
|
data/lib/reorm/cursor.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
# Copyright (c), 2015 Peter Wood
|
3
|
+
# See the license.txt for details of the licensing of the code in this file.
|
4
|
+
|
5
|
+
module Reorm
|
6
|
+
class Cursor
|
7
|
+
def initialize(model_class, query, order_by=nil)
|
8
|
+
@model_class = model_class
|
9
|
+
@query = query
|
10
|
+
@cursor = nil
|
11
|
+
@offset = 0
|
12
|
+
@total = 0
|
13
|
+
@order_by = order_by
|
14
|
+
end
|
15
|
+
attr_reader :model_class
|
16
|
+
|
17
|
+
def close
|
18
|
+
@cursor.close if @cursor && !@cursor.kind_of?(Array)
|
19
|
+
@cursor = nil
|
20
|
+
@offset = @total = 0
|
21
|
+
self
|
22
|
+
end
|
23
|
+
alias :reset :close
|
24
|
+
|
25
|
+
def filter(predicate)
|
26
|
+
Cursor.new(model_class, @query.filter(predicate), @order_by)
|
27
|
+
end
|
28
|
+
|
29
|
+
def count
|
30
|
+
Reorm.connection do |connection|
|
31
|
+
@query.count.run(connection)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def exhausted?
|
36
|
+
open? && @offset == @total
|
37
|
+
end
|
38
|
+
|
39
|
+
def find
|
40
|
+
model = nil
|
41
|
+
each do |record|
|
42
|
+
found = yield(record)
|
43
|
+
if found
|
44
|
+
model = record
|
45
|
+
break
|
46
|
+
end
|
47
|
+
end
|
48
|
+
model
|
49
|
+
end
|
50
|
+
alias :detect :find
|
51
|
+
|
52
|
+
def next
|
53
|
+
open if !open?
|
54
|
+
if exhausted?
|
55
|
+
raise Error, "There are no more matching records."
|
56
|
+
end
|
57
|
+
data = @order_by.nil? ? @cursor.next : @cursor[@offset]
|
58
|
+
@offset += 1
|
59
|
+
model_class.new(data)
|
60
|
+
end
|
61
|
+
|
62
|
+
def each(&block)
|
63
|
+
@order_by.nil? ? each_without_order_by(&block) : each_with_order_by(&block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def inject(token=nil)
|
67
|
+
each do |record|
|
68
|
+
yield token, record
|
69
|
+
end
|
70
|
+
token
|
71
|
+
end
|
72
|
+
|
73
|
+
def nth(offset)
|
74
|
+
model = nil
|
75
|
+
if offset >= 0 && offset < count
|
76
|
+
Reorm.connection do |connection|
|
77
|
+
model = model_class.new(@query.nth(offset).run(connection))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
model
|
81
|
+
end
|
82
|
+
|
83
|
+
def first
|
84
|
+
nth(0)
|
85
|
+
end
|
86
|
+
|
87
|
+
def last
|
88
|
+
nth(count - 1)
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_a
|
92
|
+
inject([]) {|list, record| list << record}
|
93
|
+
end
|
94
|
+
|
95
|
+
def limit(size)
|
96
|
+
Cursor.new(model_class, @query.limit(size), @order_by)
|
97
|
+
end
|
98
|
+
|
99
|
+
def offset(index)
|
100
|
+
Cursor.new(model_class, @query.skip(quantity), @order_by)
|
101
|
+
end
|
102
|
+
alias :skip :offset
|
103
|
+
|
104
|
+
def slice(start_at, end_at=nil, left_bound='closed', right_bound="open")
|
105
|
+
if end_at
|
106
|
+
Cursor.new(model_class, @query.slice(start_at, end_at, left_bound, right_bound), @order_by)
|
107
|
+
else
|
108
|
+
Cursor.new(model_class, @query.slice(start_at), @order_by)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def order_by(*arguments)
|
113
|
+
Cursor.new(model_class, @query, arguments)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def open
|
119
|
+
Reorm.connection do |connection|
|
120
|
+
array_based = false
|
121
|
+
if @order_by && @order_by.size > 0
|
122
|
+
clause = @order_by.find {|entry| entry.kind_of?(Hash)}
|
123
|
+
array_based = clause.nil? || clause.keys != [:index]
|
124
|
+
end
|
125
|
+
|
126
|
+
@offset = 0
|
127
|
+
if !array_based
|
128
|
+
@total = @query.count.run(connection)
|
129
|
+
@cursor = @query.run(connection)
|
130
|
+
else
|
131
|
+
@cursor = @query.order_by(*@order_by).run(connection)
|
132
|
+
@total = @cursor.size
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def open?
|
138
|
+
!@cursor.nil?
|
139
|
+
end
|
140
|
+
|
141
|
+
def each_with_order_by
|
142
|
+
Reorm.connection do |connection|
|
143
|
+
@query.order_by(*@order_by).run(connection).each do |record|
|
144
|
+
yield model_class.new(record)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def each_without_order_by
|
150
|
+
Reorm.connection do |connection|
|
151
|
+
cursor = @query.run(connection)
|
152
|
+
begin
|
153
|
+
cursor.each do |record|
|
154
|
+
yield model_class.new(record)
|
155
|
+
end
|
156
|
+
ensure
|
157
|
+
cursor.close
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
# Copyright (c), 2015 Peter Wood
|
3
|
+
# See the license.txt for details of the licensing of the code in this file.
|
4
|
+
|
5
|
+
module Reorm
|
6
|
+
class Error < StandardError
|
7
|
+
def initialize(message, cause=nil)
|
8
|
+
super(message)
|
9
|
+
@cause = cause
|
10
|
+
end
|
11
|
+
attr_reader :cause
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
# Copyright (c), 2015 Peter Wood
|
3
|
+
# See the license.txt for details of the licensing of the code in this file.
|
4
|
+
|
5
|
+
module Reorm
|
6
|
+
class FieldPath
|
7
|
+
def initialize(*path)
|
8
|
+
@path = [].concat(path)
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
@path.last
|
13
|
+
end
|
14
|
+
|
15
|
+
def value(document)
|
16
|
+
locate(document).first
|
17
|
+
end
|
18
|
+
|
19
|
+
def value!(document)
|
20
|
+
result = locate(document)
|
21
|
+
raise Error, "Unable to locate the #{name} (full path: #{self}) field for an instance of the #{document.class.name} class." if !result[1]
|
22
|
+
result[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
def exists?(document)
|
26
|
+
locate(document)[1]
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
@path.join(" -> ")
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def locate(document)
|
36
|
+
result = [nil, false]
|
37
|
+
value = document
|
38
|
+
@path.each_with_index do |field, index|
|
39
|
+
if !value || !value.respond_to?(:include?) || !value.include?(field)
|
40
|
+
value = nil
|
41
|
+
break
|
42
|
+
else
|
43
|
+
if index == @path.length - 1
|
44
|
+
result = [value[field], true]
|
45
|
+
else
|
46
|
+
value = value[field]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
result
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/reorm/model.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
# Copyright (c), 2015 Peter Wood
|
3
|
+
# See the license.txt for details of the licensing of the code in this file.
|
4
|
+
|
5
|
+
module Reorm
|
6
|
+
class Model
|
7
|
+
extend EventHandler
|
8
|
+
extend EventSource
|
9
|
+
extend TableBacked
|
10
|
+
include EventSource
|
11
|
+
include TableBacked
|
12
|
+
include Validations
|
13
|
+
|
14
|
+
@@class_tables = {}
|
15
|
+
|
16
|
+
def initialize(properties={})
|
17
|
+
@properties = {}
|
18
|
+
properties.each do |key, value|
|
19
|
+
@properties[key.to_sym] = value
|
20
|
+
end
|
21
|
+
@errors = PropertyErrors.new
|
22
|
+
end
|
23
|
+
attr_reader :errors
|
24
|
+
|
25
|
+
def valid?
|
26
|
+
validate
|
27
|
+
@errors.clear?
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate
|
31
|
+
fire_events(events: [:before_validate])
|
32
|
+
@errors.reset
|
33
|
+
fire_events(events: [:after_validate])
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def save(validated=true)
|
38
|
+
if validated && !valid?
|
39
|
+
raise Error, "Validation error encountered saving an instance of the #{self.class.name} class."
|
40
|
+
end
|
41
|
+
|
42
|
+
action_type = (@properties[primary_key] ? :update : :create)
|
43
|
+
if action_type == :create
|
44
|
+
fire_events(events: [:before_create, :before_save])
|
45
|
+
else
|
46
|
+
fire_events(events: [:before_update, :before_save])
|
47
|
+
end
|
48
|
+
|
49
|
+
Reorm.connection do |connection|
|
50
|
+
ensure_table_exists(connection)
|
51
|
+
if !@properties.include?(primary_key)
|
52
|
+
result = r.table(table_name).insert(self.to_h, return_changes: true).run(connection)
|
53
|
+
if !result["inserted"] || result["inserted"] != 1
|
54
|
+
raise Error, "Creation of database record for an instance of the #{self.class.name} class failed."
|
55
|
+
end
|
56
|
+
@properties[primary_key] = result["generated_keys"].first
|
57
|
+
else
|
58
|
+
result = r.table(table_name).update(self.to_h).run(connection)
|
59
|
+
if !result["replaced"] || !result["replaced"] == 1
|
60
|
+
raise Error, "Update of database record for an instance of the #{self.class.name} class failed."
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if action_type == :create
|
66
|
+
fire_events(events: [:after_save, :after_create])
|
67
|
+
else
|
68
|
+
fire_events(events: [:after_save, :after_update])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def [](property_name)
|
73
|
+
@properties[property_name.to_sym]
|
74
|
+
end
|
75
|
+
|
76
|
+
def []=(property_name, value)
|
77
|
+
@properties[property_name.to_sym] = value
|
78
|
+
value
|
79
|
+
end
|
80
|
+
|
81
|
+
def respond_to?(method_name, include_private=false)
|
82
|
+
@properties.include?(property_name(method_name)) || super
|
83
|
+
end
|
84
|
+
|
85
|
+
def method_missing(method_name, *arguments, &block)
|
86
|
+
if method_name.to_s[-1,1] != "="
|
87
|
+
if @properties.include?(property_name(method_name))
|
88
|
+
@properties[method_name]
|
89
|
+
else
|
90
|
+
super
|
91
|
+
end
|
92
|
+
else
|
93
|
+
@properties[property_name(method_name)] = arguments.first
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_h
|
98
|
+
{}.merge(@properties)
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.create(properties={})
|
102
|
+
object = self.new(properties)
|
103
|
+
object.save
|
104
|
+
object
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.all
|
108
|
+
Cursor.new(self, r.table(table_name))
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.filter(predicate=nil, &block)
|
112
|
+
if predicate.nil?
|
113
|
+
Cursor.new(self, r.table(table_name).filter(&block))
|
114
|
+
else
|
115
|
+
Cursor.new(self, r.table(table_name).filter(predicate))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def property_name(name)
|
122
|
+
name.to_s[-1,1] == "=" ? name.to_s[0...-1].to_sym : name
|
123
|
+
end
|
124
|
+
|
125
|
+
def ensure_table_exists(connection)
|
126
|
+
tables = r.table_list.run(connection)
|
127
|
+
if !tables.include?(table_name)
|
128
|
+
r.table_create(table_name, primary_key: primary_key).run(connection)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|