rasti-db 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/.coveralls.yml +2 -0
- data/.gitignore +9 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +11 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +161 -0
- data/Rakefile +25 -0
- data/lib/rasti/db/collection.rb +263 -0
- data/lib/rasti/db/helpers.rb +18 -0
- data/lib/rasti/db/model.rb +105 -0
- data/lib/rasti/db/query.rb +113 -0
- data/lib/rasti/db/relations.rb +159 -0
- data/lib/rasti/db/type_converter.rb +52 -0
- data/lib/rasti/db/version.rb +5 -0
- data/lib/rasti/db.rb +10 -0
- data/lib/rasti-db.rb +1 -0
- data/rasti-db.gemspec +44 -0
- data/spec/collection_spec.rb +494 -0
- data/spec/coverage_helper.rb +5 -0
- data/spec/minitest_helper.rb +113 -0
- data/spec/model_spec.rb +82 -0
- data/spec/query_spec.rb +122 -0
- data/spec/relations_spec.rb +144 -0
- data/spec/type_converter_spec.rb +98 -0
- metadata +231 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fa66e94f3945ca058846ce6031b4720516d58646
|
4
|
+
data.tar.gz: 87e086acc3972e6a553aea23e9741a8d74235022
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6d0add21909af7df516237b904484e65b68042ec9da5f792ecc4c517a63d175fc98a19389ee2ca063f8a028457e9fd0adab0c2f4a46ea323ea93ab3148be8030
|
7
|
+
data.tar.gz: db6e89b5c05617e65aaa07dab882de5376acf0dd3bda0d94001eaf5e24f482772b1fb3d8617c8647aeb72e26aeb5d62ccf15706ab3ea6617872adc0bccc696e7
|
data/.coveralls.yml
ADDED
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rasti-db
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.3.0
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Gabriel Naiman
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
# Rasti::DB
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/rasti-db)
|
4
|
+
[](https://travis-ci.org/gabynaiman/rasti-db)
|
5
|
+
[](https://coveralls.io/github/gabynaiman/rasti-db?branch=master)
|
6
|
+
[](https://codeclimate.com/github/gabynaiman/rasti-db)
|
7
|
+
[](https://gemnasium.com/gabynaiman/rasti-db)
|
8
|
+
|
9
|
+
Database collections and relations
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'rasti-db'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install rasti-db
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Database connection
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
DB = Sequel.connect ...
|
33
|
+
```
|
34
|
+
|
35
|
+
### Database schema
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
DB.create_table :users do
|
39
|
+
primary_key :id
|
40
|
+
String :name, null: false, unique: true
|
41
|
+
end
|
42
|
+
|
43
|
+
DB.create_table :posts do
|
44
|
+
primary_key :id
|
45
|
+
String :title, null: false, unique: true
|
46
|
+
String :body, null: false
|
47
|
+
foreign_key :user_id, :users, null: false, index: true
|
48
|
+
end
|
49
|
+
|
50
|
+
DB.create_table :comments do
|
51
|
+
primary_key :id
|
52
|
+
String :text, null: false
|
53
|
+
foreign_key :user_id, :users, null: false, index: true
|
54
|
+
foreign_key :post_id, :posts, null: false, index: true
|
55
|
+
end
|
56
|
+
|
57
|
+
DB.create_table :categories do
|
58
|
+
primary_key :id
|
59
|
+
String :name, null: false, unique: true
|
60
|
+
end
|
61
|
+
|
62
|
+
DB.create_table :categories_posts do
|
63
|
+
foreign_key :category_id, :categories, null: false, index: true
|
64
|
+
foreign_key :post_id, :posts, null: false, index: true
|
65
|
+
primary_key [:category_id, :post_id]
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
### Models
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
User = Rasti::DB::Model[:id, :name, :posts, :comments]
|
73
|
+
Post = Rasti::DB::Model[:id, :title, :body, :user_id, :user, :comments, :categories]
|
74
|
+
Comment = Rasti::DB::Model[:id, :text, :user_id, :user, :post_id, :post]
|
75
|
+
Category = Rasti::DB::Model[:id, :name, :posts]
|
76
|
+
```
|
77
|
+
|
78
|
+
### Collections
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class Users < Rasti::DB::Collection
|
82
|
+
one_to_many :posts
|
83
|
+
one_to_many :comments
|
84
|
+
end
|
85
|
+
|
86
|
+
class Posts < Rasti::DB::Collection
|
87
|
+
many_to_one :user
|
88
|
+
many_to_many :categories
|
89
|
+
one_to_many :comments
|
90
|
+
|
91
|
+
query :created_by do |user_id|
|
92
|
+
where user_id: user_id
|
93
|
+
end
|
94
|
+
|
95
|
+
query :entitled, -> (title) { where title: title }
|
96
|
+
|
97
|
+
query :commented_by do |user_id|
|
98
|
+
chainable do
|
99
|
+
dataset.join(with_schema(:comments), post_id: :id)
|
100
|
+
.where(with_schema(:comments, :user_id) => user_id)
|
101
|
+
.select_all(with_schema(:posts))
|
102
|
+
.distinct
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Comments < Rasti::DB::Collection
|
108
|
+
many_to_one :user
|
109
|
+
many_to_one :post
|
110
|
+
end
|
111
|
+
|
112
|
+
class Categories < Rasti::DB::Collection
|
113
|
+
many_to_many :posts
|
114
|
+
end
|
115
|
+
|
116
|
+
users = Users.new DB
|
117
|
+
posts = Posts.new DB
|
118
|
+
comments = Comments.new DB
|
119
|
+
categories = Categories.new DB
|
120
|
+
```
|
121
|
+
|
122
|
+
### Persistence
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
DB.transaction do
|
126
|
+
id = users.insert name: 'User 1'
|
127
|
+
users.update id, name: 'User updated'
|
128
|
+
users.delete id
|
129
|
+
|
130
|
+
users.bulk_insert [{name: 'User 1'}, {name: 'User 2'}]
|
131
|
+
users.bulk_update(name: 'User updated') { where id: [1,2] }
|
132
|
+
users.bulk_delete { where id: [1,2] }
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
### Queries
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
posts.all # => [Post, ...]
|
140
|
+
posts.first # => Post
|
141
|
+
posts.count # => 1
|
142
|
+
posts.where(id: [1,2]) # => [Post, ...]
|
143
|
+
posts.where{id > 1}.limit(10).offset(20) } # => [Post, ...]
|
144
|
+
posts.graph(:user, :categories, 'comments.user') # => [Post(User, [Categories, ...], [Comments(User)]), ...]
|
145
|
+
posts.created_by(1) # => [Post, ...]
|
146
|
+
posts.created_by(1).entitled('...').commented_by(2) # => [Post, ...]
|
147
|
+
posts.where(id: [1,2]).raw # => [{id:1, ...}, {id:2, ...}]
|
148
|
+
posts.where(id: [1,2]).primary_keys # => [1,2]
|
149
|
+
posts.where(id: [1,2]).pluck(:id) # => [1,2]
|
150
|
+
posts.where(id: [1,2]).pluck(:id, :title) # => [[1, ...], [2, ...]]
|
151
|
+
```
|
152
|
+
|
153
|
+
## Contributing
|
154
|
+
|
155
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/gabynaiman/rasti-db.
|
156
|
+
|
157
|
+
|
158
|
+
## License
|
159
|
+
|
160
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
161
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Rake::TestTask.new(:spec) do |t|
|
5
|
+
t.libs << 'spec'
|
6
|
+
t.libs << 'lib'
|
7
|
+
t.pattern = ENV['DIR'] ? File.join(ENV['DIR'], '**', '*_spec.rb') : 'spec/**/*_spec.rb'
|
8
|
+
t.verbose = false
|
9
|
+
t.warning = false
|
10
|
+
t.loader = nil if ENV['TEST']
|
11
|
+
ENV['TEST'], ENV['LINE'] = ENV['TEST'].split(':') if ENV['TEST'] && !ENV['LINE']
|
12
|
+
t.options = ''
|
13
|
+
t.options << "--name=/#{ENV['NAME']}/ " if ENV['NAME']
|
14
|
+
t.options << "-l #{ENV['LINE']} " if ENV['LINE'] && ENV['TEST']
|
15
|
+
end
|
16
|
+
|
17
|
+
task default: :spec
|
18
|
+
|
19
|
+
desc 'Pry console'
|
20
|
+
task :console do
|
21
|
+
require 'rasti-db'
|
22
|
+
require 'pry'
|
23
|
+
ARGV.clear
|
24
|
+
Pry.start
|
25
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
module Rasti
|
2
|
+
module DB
|
3
|
+
class Collection
|
4
|
+
|
5
|
+
QUERY_METHODS = (Query::DATASET_CHAINED_METHODS + [:graph, :count, :all, :first]).freeze
|
6
|
+
|
7
|
+
include Helpers::WithSchema
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
include Sequel::Inflections
|
12
|
+
|
13
|
+
def collection_name
|
14
|
+
@collection_name ||= implicit_collection_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def primary_key
|
18
|
+
@primary_key ||= :id
|
19
|
+
end
|
20
|
+
|
21
|
+
def model
|
22
|
+
if @model.is_a? Class
|
23
|
+
@model
|
24
|
+
elsif @model
|
25
|
+
@model = Consty.get @model, self
|
26
|
+
else
|
27
|
+
@model = Consty.get demodulize(singularize(name)), self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def relations
|
32
|
+
@relations ||= {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def queries
|
36
|
+
@queries ||= {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def implicit_collection_name
|
40
|
+
underscore(demodulize(name)).to_sym
|
41
|
+
end
|
42
|
+
|
43
|
+
def implicit_foreign_key_name
|
44
|
+
"#{singularize(collection_name)}_id".to_sym
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def set_collection_name(collection_name)
|
50
|
+
@collection_name = collection_name.to_sym
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_primary_key(primary_key)
|
54
|
+
@primary_key = primary_key
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_model(model)
|
58
|
+
@model = model
|
59
|
+
end
|
60
|
+
|
61
|
+
def one_to_many(name, options={})
|
62
|
+
relations[name] = Relations::OneToMany.new name, self, options
|
63
|
+
end
|
64
|
+
|
65
|
+
def many_to_one(name, options={})
|
66
|
+
relations[name] = Relations::ManyToOne.new name, self, options
|
67
|
+
end
|
68
|
+
|
69
|
+
def many_to_many(name, options={})
|
70
|
+
relations[name] = Relations::ManyToMany.new name, self, options
|
71
|
+
end
|
72
|
+
|
73
|
+
def query(name, lambda=nil, &block)
|
74
|
+
queries[name] = lambda || block
|
75
|
+
|
76
|
+
define_method name do |*args|
|
77
|
+
query.instance_exec *args, &self.class.queries[name]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
attr_reader :db, :schema
|
84
|
+
|
85
|
+
def initialize(db, schema=nil)
|
86
|
+
@db = db
|
87
|
+
@schema = schema
|
88
|
+
end
|
89
|
+
|
90
|
+
def dataset
|
91
|
+
db[qualified_collection_name]
|
92
|
+
end
|
93
|
+
|
94
|
+
def insert(attributes)
|
95
|
+
db.transaction do
|
96
|
+
db_attributes = type_converter.apply_to attributes
|
97
|
+
collection_attributes, relations_primary_keys = split_related_attributes db_attributes
|
98
|
+
primary_key = dataset.insert collection_attributes
|
99
|
+
save_relations primary_key, relations_primary_keys
|
100
|
+
primary_key
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def bulk_insert(attributes, options={})
|
105
|
+
db_attributes = type_converter.apply_to attributes
|
106
|
+
dataset.multi_insert db_attributes, options
|
107
|
+
end
|
108
|
+
|
109
|
+
def insert_relations(primary_key, relations)
|
110
|
+
relations.each do |relation_name, relation_primary_keys|
|
111
|
+
relation = self.class.relations[relation_name]
|
112
|
+
insert_relation_table relation, primary_key, relation_primary_keys
|
113
|
+
end
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
def update(primary_key, attributes)
|
118
|
+
db.transaction do
|
119
|
+
db_attributes = type_converter.apply_to attributes
|
120
|
+
collection_attributes, relations_primary_keys = split_related_attributes db_attributes
|
121
|
+
dataset.where(self.class.primary_key => primary_key).update(collection_attributes) unless collection_attributes.empty?
|
122
|
+
save_relations primary_key, relations_primary_keys
|
123
|
+
end
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def bulk_update(attributes, &block)
|
128
|
+
db_attributes = type_converter.apply_to attributes
|
129
|
+
build_query(&block).instance_eval { dataset.update db_attributes }
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
|
133
|
+
def delete(primary_key)
|
134
|
+
dataset.where(self.class.primary_key => primary_key).delete
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def bulk_delete(&block)
|
139
|
+
build_query(&block).instance_eval { dataset.delete }
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
|
143
|
+
def delete_relations(primary_key, relations)
|
144
|
+
db.transaction do
|
145
|
+
relations.each do |relation_name, relation_primary_keys|
|
146
|
+
relation = self.class.relations[relation_name]
|
147
|
+
delete_relation_table relation, primary_key, relation_primary_keys
|
148
|
+
end
|
149
|
+
end
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
|
153
|
+
def delete_cascade(*primary_keys)
|
154
|
+
db.transaction do
|
155
|
+
delete_cascade_relations primary_keys
|
156
|
+
bulk_delete { |q| q.where self.class.primary_key => primary_keys }
|
157
|
+
end
|
158
|
+
nil
|
159
|
+
end
|
160
|
+
|
161
|
+
def find(primary_key)
|
162
|
+
where(self.class.primary_key => primary_key).first
|
163
|
+
end
|
164
|
+
|
165
|
+
def find_graph(primary_key, *relations)
|
166
|
+
where(self.class.primary_key => primary_key).graph(*relations).first
|
167
|
+
end
|
168
|
+
|
169
|
+
def query
|
170
|
+
Query.new self.class, dataset, [], schema
|
171
|
+
end
|
172
|
+
|
173
|
+
QUERY_METHODS.each do |method|
|
174
|
+
define_method method do |*args, &block|
|
175
|
+
query.public_send method, *args, &block
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def exists?(filter=nil, &block)
|
180
|
+
build_query(filter, &block).count > 0
|
181
|
+
end
|
182
|
+
|
183
|
+
def detect(filter=nil, &block)
|
184
|
+
build_query(filter, &block).first
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def type_converter
|
190
|
+
@type_converter ||= TypeConverter.new db, qualified_collection_name
|
191
|
+
end
|
192
|
+
|
193
|
+
def qualified_collection_name
|
194
|
+
schema.nil? ? self.class.collection_name : Sequel.qualify(schema, self.class.collection_name)
|
195
|
+
end
|
196
|
+
|
197
|
+
def build_query(filter=nil, &block)
|
198
|
+
raise ArgumentError, 'must specify filter hash or block' if filter.nil? && block.nil?
|
199
|
+
if filter
|
200
|
+
query.where filter
|
201
|
+
else
|
202
|
+
block.arity == 0 ? query.instance_eval(&block) : block.call(query)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def split_related_attributes(attributes)
|
207
|
+
relation_names = self.class.relations.values.select(&:many_to_many?).map(&:name)
|
208
|
+
|
209
|
+
collection_attributes = attributes.reject { |n,v| relation_names.include? n }
|
210
|
+
relations_ids = attributes.select { |n,v| relation_names.include? n }
|
211
|
+
|
212
|
+
[collection_attributes, relations_ids]
|
213
|
+
end
|
214
|
+
|
215
|
+
def save_relations(primary_key, relations_primary_keys)
|
216
|
+
relations_primary_keys.each do |relation_name, relation_primary_keys|
|
217
|
+
relation = self.class.relations[relation_name]
|
218
|
+
delete_relation_table relation, [primary_key]
|
219
|
+
insert_relation_table relation, primary_key, relation_primary_keys
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def delete_cascade_relations(primary_keys)
|
224
|
+
relations = self.class.relations.values
|
225
|
+
|
226
|
+
relations.select(&:many_to_many?).each do |relation|
|
227
|
+
delete_relation_table relation, primary_keys
|
228
|
+
end
|
229
|
+
|
230
|
+
relations.select(&:one_to_many?).each do |relation|
|
231
|
+
relation_collection_name = with_schema(relation.target_collection_class.collection_name)
|
232
|
+
relations_ids = db[relation_collection_name].where(relation.foreign_key => primary_keys)
|
233
|
+
.select(relation.target_collection_class.primary_key)
|
234
|
+
.map(relation.target_collection_class.primary_key)
|
235
|
+
|
236
|
+
target_collection = relation.target_collection_class.new db, schema
|
237
|
+
target_collection.delete_cascade *relations_ids unless relations_ids.empty?
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def insert_relation_table(relation, primary_key, relation_primary_keys)
|
242
|
+
relation_collection_name = relation.qualified_relation_collection_name(schema)
|
243
|
+
|
244
|
+
values = relation_primary_keys.map do |relation_pk|
|
245
|
+
{
|
246
|
+
relation.source_foreign_key => primary_key,
|
247
|
+
relation.target_foreign_key => relation_pk
|
248
|
+
}
|
249
|
+
end
|
250
|
+
|
251
|
+
db[relation_collection_name].multi_insert values
|
252
|
+
end
|
253
|
+
|
254
|
+
def delete_relation_table(relation, primary_keys, relation_primary_keys=nil)
|
255
|
+
relation_collection_name = relation.qualified_relation_collection_name(schema)
|
256
|
+
ds = db[relation_collection_name].where(relation.source_foreign_key => primary_keys)
|
257
|
+
ds = ds.where(relation.target_foreign_key => relation_primary_keys) if relation_primary_keys
|
258
|
+
ds.delete
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rasti
|
2
|
+
module DB
|
3
|
+
module Helpers
|
4
|
+
|
5
|
+
module WithSchema
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def with_schema(table, field=nil)
|
10
|
+
qualified_table = schema ? Sequel.qualify(schema, table) : table
|
11
|
+
field ? Sequel.qualify(qualified_table, field) : qualified_table
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Rasti
|
2
|
+
module DB
|
3
|
+
class Model
|
4
|
+
|
5
|
+
class UninitializedAttributeError < StandardError
|
6
|
+
|
7
|
+
attr_reader :attribute
|
8
|
+
|
9
|
+
def initialize(attribute)
|
10
|
+
@attribute = attribute
|
11
|
+
super "Uninitialized attribute #{attribute}"
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
class << self
|
18
|
+
|
19
|
+
def [](*attribute_names)
|
20
|
+
Class.new(self) do
|
21
|
+
attribute *attribute_names
|
22
|
+
|
23
|
+
def self.inherited(subclass)
|
24
|
+
subclass.instance_variable_set :@attributes, attributes.dup
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def attributes
|
30
|
+
@attributes ||= []
|
31
|
+
end
|
32
|
+
|
33
|
+
def model_name
|
34
|
+
name || self.superclass.name
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
"#{model_name}[#{attributes.join(', ')}]"
|
39
|
+
end
|
40
|
+
alias_method :inspect, :to_s
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def attribute(*names)
|
45
|
+
names.each do |name|
|
46
|
+
raise ArgumentError, "Attribute #{name} already exists" if attributes.include?(name)
|
47
|
+
|
48
|
+
attributes << name
|
49
|
+
|
50
|
+
define_method name do
|
51
|
+
attributes.key?(name) ? attributes[name] : raise(UninitializedAttributeError, name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def initialize(attributes)
|
60
|
+
invalid_attributes = attributes.keys - self.class.attributes
|
61
|
+
raise "#{self.class.model_name} invalid attributes: #{invalid_attributes.join(', ')}" unless invalid_attributes.empty?
|
62
|
+
@attributes = attributes
|
63
|
+
end
|
64
|
+
|
65
|
+
def eql?(other)
|
66
|
+
instance_of?(other.class) && to_h.eql?(other.to_h)
|
67
|
+
end
|
68
|
+
|
69
|
+
def ==(other)
|
70
|
+
other.kind_of?(self.class) && to_h == other.to_h
|
71
|
+
end
|
72
|
+
|
73
|
+
def hash
|
74
|
+
attributes.map(&:hash).hash
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
"#<#{self.class.model_name}[#{attributes.map { |n,v| "#{n}: #{v.inspect}" }.join(', ')}]>"
|
79
|
+
end
|
80
|
+
alias_method :inspect, :to_s
|
81
|
+
|
82
|
+
def to_h
|
83
|
+
self.class.attributes.each_with_object({}) do |name, hash|
|
84
|
+
if attributes.key? name
|
85
|
+
case attributes[name]
|
86
|
+
when Model
|
87
|
+
hash[name] = attributes[name].to_h
|
88
|
+
when Array
|
89
|
+
hash[name] = attributes[name].map do |e|
|
90
|
+
e.is_a?(Model) ? e.to_h : e
|
91
|
+
end
|
92
|
+
else
|
93
|
+
hash[name] = attributes[name]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
attr_reader :attributes
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|