mini_model 0.0.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/.gems +3 -0
- data/.gitignore +1 -0
- data/LICENSE +20 -0
- data/README.md +238 -0
- data/Rakefile +16 -0
- data/examples/associations.rb +61 -0
- data/examples/callbacks.rb +39 -0
- data/examples/multi_database.rb +15 -0
- data/examples/single_database.rb +13 -0
- data/examples/soft_deletes.rb +15 -0
- data/lib/mini_model.rb +237 -0
- data/mini_model.gemspec +16 -0
- data/test/all.rb +467 -0
- metadata +99 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a8e0f797ba49f5f53cd9f8951740f3be6cfd0106
|
4
|
+
data.tar.gz: 60127c7c9cf2997c7ffcc33b0eb99a8ac69293ba
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b28d1b7ea7b1d3d3fb356eca45294c528beaab5f6d96addc8ceb633a76a003e2437c3160e24182b500f0c0d455c5c576d6250f0dbd8aecde05c13001256d7d74
|
7
|
+
data.tar.gz: 9ce5cc6657617da3953f50897cdc2e71432efe0f419821c652a4bf26bb2b850ac4fb0e49a54fdb80deffad8849ed7c14f1b26002bf28a29d6f05dddb7dc133c8
|
data/.gems
ADDED
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/.gs
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Michel Martens and Damian Janowski
|
2
|
+
Copyright (c) 2017 Steven Weiss
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
# MiniModel
|
2
|
+
|
3
|
+
MiniModel is an alternative active record implementation to Sequel::Model.
|
4
|
+
It is designed to work with Sequel::Dataset, but aims to be a much smaller api
|
5
|
+
than Sequel::Model. The api is largely based on the lovely [Ohm][ohm] gem,
|
6
|
+
an ORM in Ruby for Redis.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
`$ gem install mini_model`
|
11
|
+
|
12
|
+
You must also install [Sequel][sequel] and the database of your choice.
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
Checkout the tests and `./examples` for more advanced use cases,
|
17
|
+
but the basics look something like:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
require 'mini_model'
|
21
|
+
require 'sequel'
|
22
|
+
require 'sqlite' # Or any Sequel supported database.
|
23
|
+
|
24
|
+
DB = Sequel.sqlite
|
25
|
+
|
26
|
+
class Model
|
27
|
+
include MiniModel
|
28
|
+
|
29
|
+
# ...
|
30
|
+
end
|
31
|
+
|
32
|
+
Model.dataset = DB[:models]
|
33
|
+
```
|
34
|
+
|
35
|
+
## API
|
36
|
+
|
37
|
+
### Class Methods
|
38
|
+
|
39
|
+
`::dataset=` Sets the dataset for the given model.
|
40
|
+
|
41
|
+
`::dataset` Gets the assigned dataset.
|
42
|
+
|
43
|
+
`::attribute` Macro for generating an accessor for the attributes hash.
|
44
|
+
|
45
|
+
`::build` Builds a new instance of the model when given attributes.
|
46
|
+
|
47
|
+
`::create` Convenience for `new(attributes).create`.
|
48
|
+
|
49
|
+
`::[]` Fetches a record for the given id via Sequel::Datase#first.
|
50
|
+
|
51
|
+
`::first` Fetches the first record for the given args, see Sequel::Dataset#first.
|
52
|
+
|
53
|
+
`::all` Fetch all records for the current dataset, see Sequel::Dataset#all.
|
54
|
+
|
55
|
+
`::where` Fetch records for the given conditions, see Sequel::Dataset#where.
|
56
|
+
|
57
|
+
`::to_foreign_key` Contains the covention for converting class names into foreign keys.
|
58
|
+
`Person` becomes `person_id`.
|
59
|
+
|
60
|
+
`::children` Association macro for "1 to n". See the section on associations below.
|
61
|
+
|
62
|
+
`::child` Association macro for "1 to 1" where the foreign key is on the other table.
|
63
|
+
|
64
|
+
`::parent` Association macro for "1 to 1/n" where the foreign key is on our table.
|
65
|
+
|
66
|
+
### Instance Methods
|
67
|
+
|
68
|
+
`#initialize` Sets the given attributes on the model instance, note that if the id
|
69
|
+
is in the attributes hash it will also be assigned.
|
70
|
+
|
71
|
+
`#dataset` Delegates to the class' dataset.
|
72
|
+
|
73
|
+
`#id` The current id value, if it exists, or raises an error.
|
74
|
+
Raising the error is to stop anything right away that may depend on the id.
|
75
|
+
We don't want to be assigning nil all over our associations and what have you.
|
76
|
+
|
77
|
+
`#id=` Assigns the id.
|
78
|
+
|
79
|
+
`#attributes` Gets the attributes hash.
|
80
|
+
|
81
|
+
`#attributes=` For each key/value in the given hash, sends the writer to self.
|
82
|
+
This means it'll be a missing method if you have not created the writer via the
|
83
|
+
`::attribute` macro, or manually. This is not safe to use with user input.
|
84
|
+
|
85
|
+
`#==` Compares two models to see if they are equals. It ensures that the class,
|
86
|
+
id, and attributes of each are the same.
|
87
|
+
|
88
|
+
`#persisted?` Used to check if a model has an id or not. It is assumed if
|
89
|
+
it has an id it's in the database.
|
90
|
+
|
91
|
+
`#save` Delegates to the proper persistence method.
|
92
|
+
|
93
|
+
`#create` Inserts attributes in the database. Returns self on success, nil
|
94
|
+
on failure.
|
95
|
+
|
96
|
+
`#update` Updates attributes in the database for id. Returns self on success,
|
97
|
+
nil on failure.
|
98
|
+
|
99
|
+
`#delete` Deletes itself from the database and unassigns the id. If you want
|
100
|
+
to do anything with the id after deletion, copy it before calling delete.
|
101
|
+
|
102
|
+
## Attributes
|
103
|
+
|
104
|
+
Attributes in MiniModel are all stored internally inside the `@attributes` hash.
|
105
|
+
The `::attribute` macro is really just an easy way of defining accessors
|
106
|
+
similar to `attr_accessor`, but getting and setting on that hash.
|
107
|
+
|
108
|
+
There is plans to implement a more robust attribute api,
|
109
|
+
but right now it is not implemented.
|
110
|
+
|
111
|
+
## Associations
|
112
|
+
|
113
|
+
Associations in MiniModel are largely inspired by [Ohm][ohm].
|
114
|
+
They are pretty much the same, but where Ohm uses a collection/reference metaphor,
|
115
|
+
MiniModel uses parent/child(ren).
|
116
|
+
|
117
|
+
Note that associations finders are not cached at this time, a small caching
|
118
|
+
layer will be added in the future.
|
119
|
+
|
120
|
+
### Parent
|
121
|
+
|
122
|
+
Lets take a look at the `::parent` macro.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class Photo
|
126
|
+
include MiniModel
|
127
|
+
|
128
|
+
# It turns this...
|
129
|
+
|
130
|
+
parent :user, :User
|
131
|
+
|
132
|
+
# Into something like this...
|
133
|
+
|
134
|
+
def user_id
|
135
|
+
@attributes[:user_id]
|
136
|
+
end
|
137
|
+
|
138
|
+
def user_id=(user_id)
|
139
|
+
@attributes[:user_id] = user_id
|
140
|
+
end
|
141
|
+
|
142
|
+
def user
|
143
|
+
User[user_id]
|
144
|
+
end
|
145
|
+
|
146
|
+
def user=(user)
|
147
|
+
self.user_id = user.id
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
This means that when we say `parent :user, :User` user is our parent, and the foreign
|
153
|
+
key is on our table. You can customize the foreign key by passing a third symbol argument,
|
154
|
+
but with this convention the primary key on the parent must be id. You can always
|
155
|
+
skip the parent macro and implement things manually if you're using a different
|
156
|
+
primary key.
|
157
|
+
|
158
|
+
### Children
|
159
|
+
|
160
|
+
Children are even simpler than parents, as it's just a finder.
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class User
|
164
|
+
include MiniModel
|
165
|
+
|
166
|
+
children :photos, :Photo
|
167
|
+
|
168
|
+
# Roughly becomes...
|
169
|
+
|
170
|
+
def photos
|
171
|
+
Photo.where(user_id: id)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
176
|
+
Once again you can customize the foreign key (user_id) with a third argument,
|
177
|
+
but not that it's referring to the id on ourself. These macros are just
|
178
|
+
conveniences for the 90% use case, unique situations are easy to implement
|
179
|
+
yourself like...
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
def photos
|
183
|
+
Photo.where(user_email: email)
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
An **important** thing to note when dealing with associations is that MiniModel
|
188
|
+
only provides the association writer on the "child" side of the relationship. That
|
189
|
+
means the parent must be saved to start assign associations, but also you can't do
|
190
|
+
something like `user.photos << photo` and have it persist. Another note is the relationship
|
191
|
+
will not be persisted until calling save (or create/update) on the child.
|
192
|
+
|
193
|
+
On our User model, if we want to work from that side of the association, you could
|
194
|
+
create delegation methods like so:
|
195
|
+
|
196
|
+
```
|
197
|
+
def add_photo(photo)
|
198
|
+
photo.user_id = id
|
199
|
+
end
|
200
|
+
```
|
201
|
+
|
202
|
+
### Child
|
203
|
+
|
204
|
+
The final association macro is `child`. It works the same exact way as
|
205
|
+
`children` though uses the `.first` finder to get a single record.
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
class User
|
209
|
+
include MiniModel
|
210
|
+
|
211
|
+
child :profile, :Profile
|
212
|
+
|
213
|
+
# This roughly expands to...
|
214
|
+
|
215
|
+
def profile
|
216
|
+
Profile.first(user_id: id)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
```
|
220
|
+
|
221
|
+
## Non-SQL Databases
|
222
|
+
|
223
|
+
MiniModel is designed to work with Sequel (and SQL Databases),
|
224
|
+
though it will work with anything that implements a subset of Sequel::DataSet.
|
225
|
+
|
226
|
+
If you would like your write your own adapter/dataset for a non SQL database,
|
227
|
+
MiniMapper depends on the following Sequel::DataSet methods:
|
228
|
+
|
229
|
+
`Sequel::Dataset#all`
|
230
|
+
`Sequel::Dataset#delete`
|
231
|
+
`Sequel::Dataset#first`
|
232
|
+
`Sequel::Dataset#insert`
|
233
|
+
`Sequel::Dataset#update`
|
234
|
+
`Sequel::Dataset#where`
|
235
|
+
|
236
|
+
[ohm]: http://github.com/soveran/ohm
|
237
|
+
[sequel]: https://github.com/jeremyevans/sequel
|
238
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
desc 'open irb in gs context'
|
2
|
+
task :console do
|
3
|
+
sh 'gs irb'
|
4
|
+
end
|
5
|
+
|
6
|
+
desc 'installs gems'
|
7
|
+
task :install do
|
8
|
+
sh 'mkdir -p .gs & gs dep install'
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'tests the given [test].rb'
|
12
|
+
task :test, :name do |t, args|
|
13
|
+
name = args[:name] || '*'
|
14
|
+
|
15
|
+
sh "gs cutest -r ./test/#{name}.rb"
|
16
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class User
|
2
|
+
include MiniModel
|
3
|
+
|
4
|
+
attribute :email
|
5
|
+
|
6
|
+
child :profile, :Profile
|
7
|
+
|
8
|
+
children :photos, :Photo
|
9
|
+
|
10
|
+
children :photo_comments, :PhotoComment
|
11
|
+
|
12
|
+
children :videos, :Video
|
13
|
+
|
14
|
+
children :video_comments, :VideoComment
|
15
|
+
|
16
|
+
def comments
|
17
|
+
photo_comments + video_comments
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Profile
|
22
|
+
include MiniModel
|
23
|
+
|
24
|
+
parent :user, :User
|
25
|
+
end
|
26
|
+
|
27
|
+
class Photo
|
28
|
+
include MiniModel
|
29
|
+
|
30
|
+
attribute :url
|
31
|
+
|
32
|
+
parent :user, :User
|
33
|
+
|
34
|
+
children :comments, :PhotoComment
|
35
|
+
end
|
36
|
+
|
37
|
+
class Video
|
38
|
+
include MiniModel
|
39
|
+
|
40
|
+
attribute :url
|
41
|
+
|
42
|
+
parent :user, :User
|
43
|
+
|
44
|
+
children :comments, :VideoComment
|
45
|
+
end
|
46
|
+
|
47
|
+
class Comment
|
48
|
+
include MiniModel
|
49
|
+
|
50
|
+
attribute :body
|
51
|
+
|
52
|
+
parent :user, :User
|
53
|
+
end
|
54
|
+
|
55
|
+
class PhotoComment < Comment
|
56
|
+
parent :photo, :Photo
|
57
|
+
end
|
58
|
+
|
59
|
+
class VideoComment < Comment
|
60
|
+
parent :video, :Video
|
61
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Model
|
2
|
+
include MiniModel
|
3
|
+
|
4
|
+
attribute :created_at
|
5
|
+
|
6
|
+
attribute :updated_at
|
7
|
+
|
8
|
+
def before_create
|
9
|
+
self.created_at = Time.noew
|
10
|
+
end
|
11
|
+
|
12
|
+
def after_create; end
|
13
|
+
|
14
|
+
def create
|
15
|
+
before_create
|
16
|
+
|
17
|
+
ret = super
|
18
|
+
|
19
|
+
after_create
|
20
|
+
|
21
|
+
ret
|
22
|
+
end
|
23
|
+
|
24
|
+
def before_update
|
25
|
+
self.updated_at = Time.noew
|
26
|
+
end
|
27
|
+
|
28
|
+
def after_update; end
|
29
|
+
|
30
|
+
def update
|
31
|
+
before_update
|
32
|
+
|
33
|
+
ret = super
|
34
|
+
|
35
|
+
after_update
|
36
|
+
|
37
|
+
ret
|
38
|
+
end
|
39
|
+
end
|
data/lib/mini_model.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
module MiniModel
|
2
|
+
VERSION = '0.0.0'
|
3
|
+
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class MissingId < Error; end
|
7
|
+
|
8
|
+
def self.included(model)
|
9
|
+
model.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def dataset
|
14
|
+
@dataset
|
15
|
+
end
|
16
|
+
|
17
|
+
def dataset=(dataset)
|
18
|
+
@dataset = dataset
|
19
|
+
end
|
20
|
+
|
21
|
+
# Defines an accessor for the attributes hash. The whole point
|
22
|
+
# of the attributes hash vs. instance variables is for easily
|
23
|
+
# passing a hash to the dataset for persistence. Maybe this is a bad
|
24
|
+
# idea and we should use plain ol' attr_accessor and build the hash
|
25
|
+
# when needed.
|
26
|
+
def attribute(key, type = nil)
|
27
|
+
reader = :"#{key}"
|
28
|
+
writer = :"#{key}="
|
29
|
+
|
30
|
+
define_method(reader) do
|
31
|
+
self.attributes[reader]
|
32
|
+
end
|
33
|
+
|
34
|
+
define_method(writer) do |value|
|
35
|
+
self.attributes[reader] = value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def build(attributes)
|
40
|
+
if attributes
|
41
|
+
new(attributes)
|
42
|
+
else
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Convenience for initializin and persisting a
|
48
|
+
# new model instance.
|
49
|
+
def create(attributes = {})
|
50
|
+
new(attributes).create
|
51
|
+
end
|
52
|
+
|
53
|
+
def [](id)
|
54
|
+
build(dataset.first(id: id))
|
55
|
+
end
|
56
|
+
|
57
|
+
def first(*args, &block)
|
58
|
+
build(dataset.first(*args, &block))
|
59
|
+
end
|
60
|
+
|
61
|
+
def all(&block)
|
62
|
+
dataset.all(&block).map { |each| build(each) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def where(*args, &block)
|
66
|
+
dataset.where(*args, &block).map { |each| build(each) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_foreign_key
|
70
|
+
name.
|
71
|
+
to_s.
|
72
|
+
match(/^(?:.*::)*(.*)$/)[1].
|
73
|
+
gsub(/([a-z\d])([A-Z])/, '\1_\2').
|
74
|
+
downcase.
|
75
|
+
concat('_id').
|
76
|
+
to_sym
|
77
|
+
end
|
78
|
+
|
79
|
+
def children(association, model_name, foreign_key = to_foreign_key)
|
80
|
+
define_method(association) do
|
81
|
+
model = self.class.const_get(model_name)
|
82
|
+
|
83
|
+
model.where(foreign_key => id)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def child(association, model_name, foreign_key = to_foreign_key)
|
88
|
+
define_method(association) do
|
89
|
+
model = self.class.const_get(model_name)
|
90
|
+
|
91
|
+
model.first(foreign_key => id)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def parent(association, model_name, foreign_key = :"#{association}_id")
|
96
|
+
reader = foreign_key
|
97
|
+
writer = :"#{foreign_key}="
|
98
|
+
|
99
|
+
define_method(reader) do
|
100
|
+
self.attributes[reader]
|
101
|
+
end
|
102
|
+
|
103
|
+
define_method(writer) do |value|
|
104
|
+
self.attributes[reader] = value
|
105
|
+
end
|
106
|
+
|
107
|
+
define_method(association) do
|
108
|
+
model = self.class.const_get(model_name)
|
109
|
+
|
110
|
+
model[send(foreign_key)]
|
111
|
+
end
|
112
|
+
|
113
|
+
define_method(:"#{association}=") do |value|
|
114
|
+
if value
|
115
|
+
send(writer, value.id)
|
116
|
+
else
|
117
|
+
send(writer, value)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def initialize(attributes = {})
|
124
|
+
self.attributes = attributes # Will set the id if it exists.
|
125
|
+
end
|
126
|
+
|
127
|
+
def dataset
|
128
|
+
self.class.dataset
|
129
|
+
end
|
130
|
+
|
131
|
+
def id
|
132
|
+
if @id
|
133
|
+
@id
|
134
|
+
else
|
135
|
+
# If our model does not have an id, raise at the first occurence
|
136
|
+
# of anyone expecting it. This prevents us from assigning associations
|
137
|
+
# and other logical paths for things that do not exist in the db.
|
138
|
+
raise MissingId
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def id=(id)
|
143
|
+
@id = id
|
144
|
+
end
|
145
|
+
|
146
|
+
def attributes
|
147
|
+
@attributes
|
148
|
+
end
|
149
|
+
|
150
|
+
# #attributes= is vulnerable to mass assignment attacks if used
|
151
|
+
# directly with user input. Some sort of filter must be in place
|
152
|
+
# before setting attributes or initializing a new model. Sending
|
153
|
+
# a key in the hash argument that doesn't have an accessor raises an error.
|
154
|
+
def attributes=(attributes)
|
155
|
+
@attributes = {}
|
156
|
+
|
157
|
+
attributes.each do |key, value|
|
158
|
+
send(:"#{key}=", value)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Strap in, the is probably the most complicated method
|
163
|
+
# in the entire library.
|
164
|
+
def ==(other)
|
165
|
+
# If the classes don't match, they cannot possibly be equal.
|
166
|
+
if self.class != other.class
|
167
|
+
return false
|
168
|
+
end
|
169
|
+
|
170
|
+
# If the persisted state doesn't match, they also can never be equal.
|
171
|
+
if persisted? != other.persisted?
|
172
|
+
return false
|
173
|
+
end
|
174
|
+
|
175
|
+
# When persisted, check the other's id to see if it's the same,
|
176
|
+
# cannot possible be equals if they have different ids.
|
177
|
+
if persisted? && id != other.id
|
178
|
+
return false
|
179
|
+
end
|
180
|
+
|
181
|
+
# Finally, compare the attributes hash. If all key/values match,
|
182
|
+
# they are considered equal.
|
183
|
+
attributes == other.attributes
|
184
|
+
end
|
185
|
+
|
186
|
+
def persisted?
|
187
|
+
!!@id
|
188
|
+
end
|
189
|
+
|
190
|
+
# Use #save to write generic persistence code in things like form objects
|
191
|
+
# so you don't have to reach inside the model to determine the proper
|
192
|
+
# method to call.
|
193
|
+
def save
|
194
|
+
if persisted?
|
195
|
+
update
|
196
|
+
else
|
197
|
+
create
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# #create (as well as #update, and #delete) return self on
|
202
|
+
# success and nil on failure. This lets us use these actions
|
203
|
+
# as if conditions which is convenience though dangerous.
|
204
|
+
def create
|
205
|
+
id = dataset.insert(attributes)
|
206
|
+
|
207
|
+
if id
|
208
|
+
self.id = id
|
209
|
+
|
210
|
+
self
|
211
|
+
else
|
212
|
+
nil
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def update
|
217
|
+
count = dataset.where(id: id).update(attributes)
|
218
|
+
|
219
|
+
if count.to_i > 0
|
220
|
+
self
|
221
|
+
else
|
222
|
+
nil
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def delete
|
227
|
+
count = dataset.where(id: id).delete
|
228
|
+
|
229
|
+
if count.to_i > 0
|
230
|
+
self.id = nil
|
231
|
+
|
232
|
+
self
|
233
|
+
else
|
234
|
+
nil
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
data/mini_model.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative "./lib/mini_model"
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "mini_model"
|
5
|
+
s.summary = "MiniModel"
|
6
|
+
s.version = MiniModel::VERSION
|
7
|
+
s.authors = ["Steve Weiss"]
|
8
|
+
s.email = ["weissst@mail.gvsu.edu"]
|
9
|
+
s.homepage = "https://github.com/sirscriptalot/mini_model"
|
10
|
+
s.license = "MIT"
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
|
13
|
+
s.add_development_dependency "cutest", "~> 1.2"
|
14
|
+
s.add_development_dependency "sequel", "~> 5.1"
|
15
|
+
s.add_development_dependency "sqlite3", "~> 1.3"
|
16
|
+
end
|
data/test/all.rb
ADDED
@@ -0,0 +1,467 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'sqlite3'
|
3
|
+
require_relative '../lib/mini_model'
|
4
|
+
|
5
|
+
db = Sequel.sqlite
|
6
|
+
|
7
|
+
db.create_table :users do
|
8
|
+
primary_key :id
|
9
|
+
|
10
|
+
String :email, size: 255, unique: true, not_null: true
|
11
|
+
end
|
12
|
+
|
13
|
+
db.create_table :profiles do
|
14
|
+
primary_key :id
|
15
|
+
|
16
|
+
foreign_key :user_id, :users, on_delete: :cascade
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
db.create_table :photos do
|
21
|
+
primary_key :id
|
22
|
+
|
23
|
+
foreign_key :user_id, :users, on_delete: :cascade
|
24
|
+
|
25
|
+
String :url, unique: true, not_null: true
|
26
|
+
end
|
27
|
+
|
28
|
+
db.create_table :videos do
|
29
|
+
primary_key :id
|
30
|
+
|
31
|
+
foreign_key :user_id, :users, on_delete: :cascade
|
32
|
+
|
33
|
+
String :url, unique: true, not_null: true
|
34
|
+
end
|
35
|
+
|
36
|
+
db.create_table :photo_comments do
|
37
|
+
primary_key :id
|
38
|
+
|
39
|
+
foreign_key :user_id, :users, on_delete: :cascade
|
40
|
+
|
41
|
+
foreign_key :photo_id, :photos, on_delete: :cascade
|
42
|
+
|
43
|
+
# Make sure we can spam user comments to Twitter, we need attention.
|
44
|
+
String :body, size: 140, nut_null: true
|
45
|
+
end
|
46
|
+
|
47
|
+
db.create_table :video_comments do
|
48
|
+
primary_key :id
|
49
|
+
|
50
|
+
foreign_key :user_id, :users, on_delete: :cascade
|
51
|
+
|
52
|
+
foreign_key :video_id, :videos, on_delete: :cascade
|
53
|
+
|
54
|
+
# Make sure we can spam user comments to Twitter, we need attention.
|
55
|
+
String :body, size: 140, nut_null: true
|
56
|
+
end
|
57
|
+
|
58
|
+
require_relative '../examples/associations'
|
59
|
+
|
60
|
+
User.dataset = db[:users]
|
61
|
+
|
62
|
+
Photo.dataset = db[:photos]
|
63
|
+
|
64
|
+
Video.dataset = db[:videos]
|
65
|
+
|
66
|
+
PhotoComment.dataset = db[:photo_comments]
|
67
|
+
|
68
|
+
VideoComment.dataset = db[:video_comments]
|
69
|
+
|
70
|
+
prepare do
|
71
|
+
# Ensure each test runs with a clean database.
|
72
|
+
[User, Photo, Video, PhotoComment, VideoComment].each do |model|
|
73
|
+
model.dataset.delete
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
test '::dataset= sets dataset' do
|
78
|
+
original = User.dataset
|
79
|
+
|
80
|
+
User.dataset = :fake
|
81
|
+
|
82
|
+
assert_equal User.dataset, :fake
|
83
|
+
|
84
|
+
User.dataset = original
|
85
|
+
end
|
86
|
+
|
87
|
+
test '::dataset gets dataset' do
|
88
|
+
assert_equal User.dataset, db[:users]
|
89
|
+
end
|
90
|
+
|
91
|
+
test '::attribute defines accessor methods' do
|
92
|
+
user = User.new
|
93
|
+
|
94
|
+
assert user.respond_to?(:email)
|
95
|
+
assert user.respond_to?(:email=)
|
96
|
+
end
|
97
|
+
|
98
|
+
test '::build returns an instance when given a hash' do
|
99
|
+
user = User.build({})
|
100
|
+
|
101
|
+
assert user.is_a?(User)
|
102
|
+
end
|
103
|
+
|
104
|
+
test '::build returns nil when given a falsey value' do
|
105
|
+
user = User.build(false)
|
106
|
+
|
107
|
+
assert user.nil?
|
108
|
+
end
|
109
|
+
|
110
|
+
test '::create creates an instance and persists it in the database' do
|
111
|
+
user = User.create(email: 'name@example.com')
|
112
|
+
|
113
|
+
assert user.is_a?(User)
|
114
|
+
assert user.persisted?
|
115
|
+
end
|
116
|
+
|
117
|
+
test '::[] fetchs record for a given id' do
|
118
|
+
left = User.create(email: 'name@example.com')
|
119
|
+
|
120
|
+
right = User[left.id]
|
121
|
+
|
122
|
+
assert_equal left, right
|
123
|
+
end
|
124
|
+
|
125
|
+
test '::first fetchs one record for for the give conditions' do
|
126
|
+
email = 'name@example.com'
|
127
|
+
|
128
|
+
user = User.create(email: email)
|
129
|
+
|
130
|
+
assert_equal User.first(email: email), user
|
131
|
+
end
|
132
|
+
|
133
|
+
test '::all fetchs all records in the dataset' do
|
134
|
+
one = User.create(email: 'one@example.com')
|
135
|
+
|
136
|
+
two = User.create(email: 'two@example.com')
|
137
|
+
|
138
|
+
all = User.all
|
139
|
+
|
140
|
+
assert all.include?(one)
|
141
|
+
assert all.include?(two)
|
142
|
+
end
|
143
|
+
|
144
|
+
test '::where fetch all records in the dataset matching condition' do
|
145
|
+
email = 'one@example.com'
|
146
|
+
|
147
|
+
one = User.create(email: email)
|
148
|
+
|
149
|
+
two = User.create(email: 'two@example.com')
|
150
|
+
|
151
|
+
assert User.where(email: email).include?(one)
|
152
|
+
end
|
153
|
+
|
154
|
+
test '::to_foreign_key snake cases and appends _id to model name' do
|
155
|
+
assert_equal User.to_foreign_key, :user_id
|
156
|
+
assert_equal PhotoComment.to_foreign_key, :photo_comment_id
|
157
|
+
end
|
158
|
+
|
159
|
+
test '::children defines getter' do
|
160
|
+
user = User.new
|
161
|
+
|
162
|
+
assert user.respond_to?(:photos)
|
163
|
+
assert user.respond_to?(:photo_comments)
|
164
|
+
end
|
165
|
+
|
166
|
+
test '::child defines getter' do
|
167
|
+
user = User.new
|
168
|
+
|
169
|
+
assert user.respond_to?(:profile)
|
170
|
+
end
|
171
|
+
|
172
|
+
test '::parent defines getters and setters' do
|
173
|
+
profile = Profile.new
|
174
|
+
|
175
|
+
assert profile.respond_to?(:user)
|
176
|
+
assert profile.respond_to?(:user=)
|
177
|
+
assert profile.respond_to?(:user_id)
|
178
|
+
assert profile.respond_to?(:user_id=)
|
179
|
+
end
|
180
|
+
|
181
|
+
test '#dataset is delegates to class' do
|
182
|
+
user = User.new
|
183
|
+
|
184
|
+
assert_equal user.dataset, User.dataset
|
185
|
+
end
|
186
|
+
|
187
|
+
test '#id returns id when it exists' do
|
188
|
+
id = 1
|
189
|
+
|
190
|
+
user = User.new(id: id)
|
191
|
+
|
192
|
+
assert_equal user.id, id
|
193
|
+
end
|
194
|
+
|
195
|
+
test '#id raises when missing an id' do
|
196
|
+
user = User.new
|
197
|
+
|
198
|
+
assert_raise MiniModel::MissingId do
|
199
|
+
user.id
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
test '#id= sets the id' do
|
204
|
+
id = 2
|
205
|
+
|
206
|
+
user = User.new
|
207
|
+
|
208
|
+
user.id = id
|
209
|
+
|
210
|
+
assert_equal user.id, id
|
211
|
+
end
|
212
|
+
|
213
|
+
test '#attributes returns a hash' do
|
214
|
+
user = User.new
|
215
|
+
|
216
|
+
assert user.attributes.is_a?(Hash)
|
217
|
+
end
|
218
|
+
|
219
|
+
test '#attributes= sets key/value in attributes hash that exist' do
|
220
|
+
id = 1
|
221
|
+
|
222
|
+
email = 'name@example.com'
|
223
|
+
|
224
|
+
user = User.new
|
225
|
+
|
226
|
+
user.attributes = { id: id, email: email }
|
227
|
+
|
228
|
+
assert_equal user.id, id
|
229
|
+
assert_equal user.email, email
|
230
|
+
assert_equal user.email, user.attributes[:email]
|
231
|
+
end
|
232
|
+
|
233
|
+
test '#attributes= resets all attributes, but not id' do
|
234
|
+
id = 1
|
235
|
+
|
236
|
+
email = 'name@example.com'
|
237
|
+
|
238
|
+
user = User.new
|
239
|
+
|
240
|
+
user.attributes = { id: id, email: email }
|
241
|
+
|
242
|
+
assert_equal user.id, id
|
243
|
+
assert_equal user.email, email
|
244
|
+
assert_equal user.email, user.attributes[:email]
|
245
|
+
|
246
|
+
user.attributes = {}
|
247
|
+
|
248
|
+
assert_equal user.id, id
|
249
|
+
assert_equal user.email, nil
|
250
|
+
assert_equal user.email, user.attributes[:email]
|
251
|
+
end
|
252
|
+
|
253
|
+
test '#attributes= raises when there is no accessor for a key' do
|
254
|
+
user = User.new
|
255
|
+
|
256
|
+
assert_raise do
|
257
|
+
user.attributes = { foo: :bar }
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
setup do
|
262
|
+
one = User.new(id: 1, email: 'one')
|
263
|
+
two = User.new(id: 1, email: 'one')
|
264
|
+
|
265
|
+
[one, two]
|
266
|
+
end
|
267
|
+
|
268
|
+
test '#== must have class the same' do |one, two|
|
269
|
+
assert_equal one.class, two.class
|
270
|
+
assert_equal one, two
|
271
|
+
end
|
272
|
+
|
273
|
+
test '#== must have id the same' do |one, two|
|
274
|
+
assert_equal one.id, two.id
|
275
|
+
assert_equal one, two
|
276
|
+
|
277
|
+
two.id = 2
|
278
|
+
|
279
|
+
assert one != two
|
280
|
+
end
|
281
|
+
|
282
|
+
test '#== must have attributes the same' do |one, two|
|
283
|
+
assert_equal one.attributes, two.attributes
|
284
|
+
assert_equal one, two
|
285
|
+
|
286
|
+
two.attributes = { email: 'two' }
|
287
|
+
|
288
|
+
assert one != two
|
289
|
+
end
|
290
|
+
|
291
|
+
test '#persisted? checks if the model has an id or not' do
|
292
|
+
user = User.new
|
293
|
+
|
294
|
+
assert !user.persisted?
|
295
|
+
|
296
|
+
user.id = 1
|
297
|
+
|
298
|
+
assert user.persisted?
|
299
|
+
end
|
300
|
+
|
301
|
+
test '#save delegates to the proper persistence method' do
|
302
|
+
klass = Class.new(User) do
|
303
|
+
def created?
|
304
|
+
@did_create
|
305
|
+
end
|
306
|
+
|
307
|
+
def create
|
308
|
+
@did_create = true
|
309
|
+
end
|
310
|
+
|
311
|
+
def updated?
|
312
|
+
@did_update
|
313
|
+
end
|
314
|
+
|
315
|
+
def update
|
316
|
+
@did_update = true
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
to_create = klass.new
|
321
|
+
|
322
|
+
to_create.save
|
323
|
+
|
324
|
+
assert to_create.created? && !to_create.updated?
|
325
|
+
|
326
|
+
to_update = klass.new
|
327
|
+
|
328
|
+
to_update.id = 1
|
329
|
+
|
330
|
+
to_update.save
|
331
|
+
|
332
|
+
assert !to_update.created? && to_update.updated?
|
333
|
+
end
|
334
|
+
|
335
|
+
class SuccessDataset
|
336
|
+
def insert(*args)
|
337
|
+
9
|
338
|
+
end
|
339
|
+
|
340
|
+
def where(*args)
|
341
|
+
self
|
342
|
+
end
|
343
|
+
|
344
|
+
def update(*args)
|
345
|
+
1
|
346
|
+
end
|
347
|
+
|
348
|
+
def delete(*args)
|
349
|
+
1
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
class FailureDataset
|
354
|
+
def insert(*args)
|
355
|
+
nil
|
356
|
+
end
|
357
|
+
|
358
|
+
def where(*args)
|
359
|
+
self
|
360
|
+
end
|
361
|
+
|
362
|
+
def update(*args)
|
363
|
+
0
|
364
|
+
end
|
365
|
+
|
366
|
+
def delete(*args)
|
367
|
+
0
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
setup do
|
372
|
+
klass = Class.new(User)
|
373
|
+
|
374
|
+
klass.dataset = SuccessDataset.new
|
375
|
+
|
376
|
+
klass.new
|
377
|
+
end
|
378
|
+
|
379
|
+
test '#create assigns id on success' do |instance|
|
380
|
+
instance.create
|
381
|
+
|
382
|
+
assert instance.id
|
383
|
+
end
|
384
|
+
|
385
|
+
test '#create returns self on success' do |instance|
|
386
|
+
ret = instance.create
|
387
|
+
|
388
|
+
assert_equal ret, instance
|
389
|
+
end
|
390
|
+
|
391
|
+
setup do
|
392
|
+
klass = Class.new(User)
|
393
|
+
|
394
|
+
klass.dataset = FailureDataset.new
|
395
|
+
|
396
|
+
klass.new
|
397
|
+
end
|
398
|
+
|
399
|
+
test '#create returns nil on failure' do |instance|
|
400
|
+
ret = instance.create
|
401
|
+
|
402
|
+
assert ret.nil?
|
403
|
+
end
|
404
|
+
|
405
|
+
setup do
|
406
|
+
klass = Class.new(User)
|
407
|
+
|
408
|
+
klass.dataset = SuccessDataset.new
|
409
|
+
|
410
|
+
klass.new(id: 1)
|
411
|
+
end
|
412
|
+
|
413
|
+
test '#update returns self on success' do |instance|
|
414
|
+
ret = instance.update
|
415
|
+
|
416
|
+
assert_equal ret, instance
|
417
|
+
end
|
418
|
+
|
419
|
+
setup do
|
420
|
+
klass = Class.new(User)
|
421
|
+
|
422
|
+
klass.dataset = FailureDataset.new
|
423
|
+
|
424
|
+
klass.new(id: 1)
|
425
|
+
end
|
426
|
+
|
427
|
+
test '#update returns nil on failure' do |instance|
|
428
|
+
ret = instance.update
|
429
|
+
|
430
|
+
assert ret.nil?
|
431
|
+
end
|
432
|
+
|
433
|
+
setup do
|
434
|
+
klass = Class.new(User)
|
435
|
+
|
436
|
+
klass.dataset = SuccessDataset.new
|
437
|
+
|
438
|
+
klass.new(id: 1)
|
439
|
+
end
|
440
|
+
|
441
|
+
test '#delete unassigns id on success' do |instance|
|
442
|
+
assert instance.persisted?
|
443
|
+
|
444
|
+
instance.delete
|
445
|
+
|
446
|
+
assert !instance.persisted?
|
447
|
+
end
|
448
|
+
|
449
|
+
test '#delete returns self on success' do |instance|
|
450
|
+
ret = instance.delete
|
451
|
+
|
452
|
+
assert_equal ret, instance
|
453
|
+
end
|
454
|
+
|
455
|
+
setup do
|
456
|
+
klass = Class.new(User)
|
457
|
+
|
458
|
+
klass.dataset = FailureDataset.new
|
459
|
+
|
460
|
+
klass.new(id: 1)
|
461
|
+
end
|
462
|
+
|
463
|
+
test '#delete returns nil on failure' do |instance|
|
464
|
+
ret = instance.delete
|
465
|
+
|
466
|
+
assert ret.nil?
|
467
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mini_model
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Steve Weiss
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: cutest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sequel
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.1'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- weissst@mail.gvsu.edu
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gems"
|
63
|
+
- ".gitignore"
|
64
|
+
- LICENSE
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- examples/associations.rb
|
68
|
+
- examples/callbacks.rb
|
69
|
+
- examples/multi_database.rb
|
70
|
+
- examples/single_database.rb
|
71
|
+
- examples/soft_deletes.rb
|
72
|
+
- lib/mini_model.rb
|
73
|
+
- mini_model.gemspec
|
74
|
+
- test/all.rb
|
75
|
+
homepage: https://github.com/sirscriptalot/mini_model
|
76
|
+
licenses:
|
77
|
+
- MIT
|
78
|
+
metadata: {}
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 2.6.11
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: MiniModel
|
99
|
+
test_files: []
|