ruflet_record 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,419 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RufletRecord
4
+ class Base
5
+ @abstract_class = true
6
+ @descendants = []
7
+
8
+ class << self
9
+ attr_writer :connection, :table_name, :primary_key
10
+ attr_accessor :abstract_class
11
+
12
+ def inherited(subclass)
13
+ super
14
+ Base.descendants << subclass
15
+ subclass.instance_variable_set(:@connection, @connection)
16
+ subclass.instance_variable_set(:@primary_key, @primary_key || "id")
17
+ subclass.instance_variable_set(:@validators, validators.dup)
18
+ subclass.instance_variable_set(:@abstract_class, false)
19
+ end
20
+
21
+ def descendants
22
+ @descendants ||= []
23
+ end
24
+
25
+ def establish_connection(config)
26
+ @connection = Adapters::SQLiteAdapter.new(config)
27
+ RufletRecord.connection = @connection if self == Base
28
+ reset_column_information
29
+ @connection
30
+ end
31
+
32
+ def connection
33
+ @connection || RufletRecord.connection
34
+ end
35
+
36
+ def table_name
37
+ @table_name ||= Inflector.pluralize(Inflector.underscore(name.to_s.split("::").last))
38
+ end
39
+
40
+ def primary_key
41
+ @primary_key ||= "id"
42
+ end
43
+
44
+ def columns
45
+ @columns ||= connection.columns(table_name)
46
+ end
47
+
48
+ def columns_hash
49
+ @columns_hash ||= begin
50
+ result = {}
51
+ columns.each { |column| result[column.name] = column }
52
+ result
53
+ end
54
+ end
55
+
56
+ def column_names
57
+ columns.map(&:name)
58
+ end
59
+
60
+ def reset_column_information
61
+ @columns = nil
62
+ @columns_hash = nil
63
+ end
64
+
65
+ def instantiate(row)
66
+ record = allocate
67
+ record.send(:initialize_from_database, row)
68
+ record
69
+ end
70
+
71
+ def all
72
+ Relation.new(self)
73
+ end
74
+
75
+ def where(conditions = nil, *binds); all.where(conditions, *binds); end
76
+ def order(*values); all.order(*values); end
77
+ def reorder(*values); all.reorder(*values); end
78
+ def limit(value); all.limit(value); end
79
+ def offset(value); all.offset(value); end
80
+ def select(*values); all.select(*values); end
81
+ def distinct(value = true); all.distinct(value); end
82
+ def joins(value); all.joins(value); end
83
+ def find(id); all.find(id); end
84
+ def find_by(conditions); all.find_by(conditions); end
85
+ def find_by!(conditions); all.find_by!(conditions); end
86
+ def first(count = nil); all.first(count); end
87
+ def last(count = nil); all.last(count); end
88
+ def count(column = nil); all.count(column); end
89
+ def sum(column); all.sum(column); end
90
+ def average(column); all.average(column); end
91
+ def minimum(column); all.minimum(column); end
92
+ def maximum(column); all.maximum(column); end
93
+ def exists?(conditions = nil); all.exists?(conditions); end
94
+ def pluck(*columns); all.pluck(*columns); end
95
+ def pick(*columns); all.pick(*columns); end
96
+ def ids; all.ids; end
97
+ def find_each(batch_size: 1000, &block); all.find_each(batch_size: batch_size, &block); end
98
+
99
+ def create(attributes = {})
100
+ record = new(attributes)
101
+ record.save
102
+ record
103
+ end
104
+
105
+ def create!(attributes = {})
106
+ record = new(attributes)
107
+ record.save!
108
+ record
109
+ end
110
+
111
+ def find_or_initialize_by(attributes)
112
+ find_by(attributes) || new(attributes)
113
+ end
114
+
115
+ def find_or_create_by(attributes)
116
+ find_by(attributes) || create(attributes)
117
+ end
118
+
119
+ def find_or_create_by!(attributes)
120
+ find_by(attributes) || create!(attributes)
121
+ end
122
+
123
+ def transaction(&block)
124
+ connection.transaction(&block)
125
+ end
126
+
127
+ def delete_all
128
+ all.delete_all
129
+ end
130
+
131
+ def destroy_all
132
+ all.destroy_all
133
+ end
134
+
135
+ def scope(name, callable = nil, &block)
136
+ body = callable || block
137
+ raise ArgumentError, "scope requires a callable" unless body
138
+ define_singleton_method(name) do |*args|
139
+ value = body.call(*args)
140
+ value.is_a?(Relation) ? value : all
141
+ end
142
+ end
143
+
144
+ def validators
145
+ @validators ||= []
146
+ end
147
+
148
+ def validates_presence_of(*attributes)
149
+ attributes.each { |attribute| validators << [:presence, attribute.to_s, {}] }
150
+ end
151
+
152
+ def validates_uniqueness_of(*attributes)
153
+ options = attributes.last.is_a?(Hash) ? attributes.pop : {}
154
+ attributes.each { |attribute| validators << [:uniqueness, attribute.to_s, options] }
155
+ end
156
+
157
+ def belongs_to(name, options = {})
158
+ class_name = options[:class_name] || Inflector.classify(name)
159
+ foreign_key = (options[:foreign_key] || "#{name}_id").to_s
160
+ define_method(name) do
161
+ identifier = read_attribute(foreign_key)
162
+ identifier.nil? ? nil : Inflector.constantize(class_name).find_by(Inflector.constantize(class_name).primary_key => identifier)
163
+ end
164
+ define_method("#{name}=") do |record|
165
+ write_attribute(foreign_key, record && record.public_send(record.class.primary_key))
166
+ instance_variable_set("@#{name}", record)
167
+ end
168
+ end
169
+
170
+ def has_many(name, options = {})
171
+ class_name = options[:class_name] || Inflector.classify(name)
172
+ configured_foreign_key = options[:foreign_key]
173
+ define_method(name) do
174
+ klass = Inflector.constantize(class_name)
175
+ owner_name = Inflector.underscore(self.class.name.to_s.split("::").last)
176
+ foreign_key = (configured_foreign_key || "#{owner_name}_id").to_s
177
+ klass.where(foreign_key => public_send(self.class.primary_key))
178
+ end
179
+ end
180
+
181
+ def has_one(name, options = {})
182
+ class_name = options[:class_name] || Inflector.classify(name)
183
+ configured_foreign_key = options[:foreign_key]
184
+ define_method(name) do
185
+ owner_name = Inflector.underscore(self.class.name.to_s.split("::").last)
186
+ foreign_key = (configured_foreign_key || "#{owner_name}_id").to_s
187
+ Inflector.constantize(class_name).find_by(foreign_key => public_send(self.class.primary_key))
188
+ end
189
+ end
190
+ end
191
+
192
+ attr_reader :errors
193
+
194
+ def initialize(attributes = {})
195
+ @attributes = {}
196
+ @original_attributes = {}
197
+ @new_record = true
198
+ @destroyed = false
199
+ @errors = Errors.new
200
+ self.class.columns.each do |column|
201
+ @attributes[column.name] = cast_default(column)
202
+ end
203
+ assign_attributes(attributes)
204
+ end
205
+
206
+ def attributes
207
+ @attributes.dup
208
+ end
209
+
210
+ def assign_attributes(values)
211
+ values.each { |name, value| write_attribute(name, value) }
212
+ self
213
+ end
214
+
215
+ def read_attribute(name)
216
+ @attributes[name.to_s]
217
+ end
218
+ alias [] read_attribute
219
+
220
+ def write_attribute(name, value)
221
+ key = name.to_s
222
+ column = self.class.columns_hash[key]
223
+ raise UnknownAttributeError, "unknown attribute '#{key}' for #{self.class.name}" unless column
224
+ @attributes[key] = column.cast(value)
225
+ end
226
+
227
+ def []=(name, value)
228
+ write_attribute(name, value)
229
+ end
230
+
231
+ def method_missing(name, *args)
232
+ value = name.to_s
233
+ if value.end_with?("=") && args.length == 1
234
+ attribute = value[0...-1]
235
+ return write_attribute(attribute, args.first) if self.class.columns_hash.key?(attribute)
236
+ elsif args.empty? && self.class.columns_hash.key?(value)
237
+ return read_attribute(value)
238
+ end
239
+ super
240
+ end
241
+
242
+ def respond_to_missing?(name, include_private = false)
243
+ value = name.to_s
244
+ attribute = value.end_with?("=") ? value[0...-1] : value
245
+ self.class.columns_hash.key?(attribute) || super
246
+ end
247
+
248
+ def new_record?
249
+ @new_record
250
+ end
251
+
252
+ def persisted?
253
+ !@new_record && !@destroyed
254
+ end
255
+
256
+ def destroyed?
257
+ @destroyed
258
+ end
259
+
260
+ def changed?
261
+ @attributes != @original_attributes
262
+ end
263
+
264
+ def changes
265
+ result = {}
266
+ @attributes.each do |name, value|
267
+ original = @original_attributes[name]
268
+ result[name] = [original, value] unless original == value
269
+ end
270
+ result
271
+ end
272
+
273
+ def valid?
274
+ @errors.clear
275
+ self.class.validators.each do |kind, attribute, options|
276
+ value = read_attribute(attribute)
277
+ if kind == :presence
278
+ @errors.add(attribute, "can't be blank") if value.nil? || (value.respond_to?(:empty?) && value.empty?)
279
+ elsif kind == :uniqueness && !value.nil?
280
+ relation = self.class.where(attribute => value)
281
+ relation = relation.where.not(self.class.primary_key => read_attribute(self.class.primary_key)) if persisted?
282
+ @errors.add(attribute, "has already been taken") if relation.exists?
283
+ end
284
+ end
285
+ @errors.empty?
286
+ end
287
+
288
+ def save
289
+ return false unless valid?
290
+ save_without_validation
291
+ true
292
+ rescue StatementInvalid => error
293
+ @errors.add(:base, error.message)
294
+ false
295
+ end
296
+
297
+ def save!
298
+ raise RecordInvalid, self unless valid?
299
+ save_without_validation
300
+ true
301
+ rescue StatementInvalid => error
302
+ raise RecordNotSaved, error.message
303
+ end
304
+
305
+ def update(attributes)
306
+ assign_attributes(attributes)
307
+ save
308
+ end
309
+
310
+ def update!(attributes)
311
+ assign_attributes(attributes)
312
+ save!
313
+ end
314
+
315
+ def destroy
316
+ return self unless persisted?
317
+ primary = self.class.primary_key
318
+ self.class.where(primary => read_attribute(primary)).delete_all
319
+ @destroyed = true
320
+ self
321
+ end
322
+
323
+ def delete
324
+ destroy
325
+ end
326
+
327
+ def update_columns(attributes)
328
+ raise RecordNotSaved, "cannot update a new record" unless persisted?
329
+ primary = self.class.primary_key
330
+ values = {}
331
+ attributes.each do |name, value|
332
+ write_attribute(name, value)
333
+ values[name.to_s] = read_attribute(name)
334
+ end
335
+ self.class.where(primary => read_attribute(primary)).update_all(values)
336
+ @original_attributes = @attributes.dup
337
+ true
338
+ end
339
+
340
+ def touch
341
+ raise RecordNotSaved, "cannot touch a new record" unless persisted?
342
+ return true unless @attributes.key?("updated_at")
343
+ update_columns("updated_at" => Time.now.utc)
344
+ end
345
+
346
+ def reload
347
+ primary = self.class.primary_key
348
+ fresh = self.class.find(read_attribute(primary))
349
+ initialize_from_database(fresh.attributes)
350
+ self
351
+ end
352
+
353
+ def ==(other)
354
+ return true if equal?(other)
355
+ return false unless other.is_a?(self.class)
356
+ primary = self.class.primary_key
357
+ persisted? && other.persisted? && read_attribute(primary) == other.read_attribute(primary)
358
+ end
359
+
360
+ private
361
+
362
+ def initialize_from_database(row)
363
+ @attributes = {}
364
+ self.class.columns.each do |column|
365
+ @attributes[column.name] = column.cast(row[column.name])
366
+ end
367
+ @original_attributes = @attributes.dup
368
+ @new_record = false
369
+ @destroyed = false
370
+ @errors = Errors.new
371
+ self
372
+ end
373
+
374
+ def save_without_validation
375
+ touch_timestamps
376
+ new_record? ? create_record : update_record
377
+ @original_attributes = @attributes.dup
378
+ end
379
+
380
+ def create_record
381
+ primary = self.class.primary_key
382
+ values = @attributes.reject { |name, value| name == primary && value.nil? }
383
+ sql, binds = SQL.insert(self.class.table_name, values)
384
+ identifier = self.class.connection.insert(sql, binds)
385
+ @attributes[primary] = identifier if @attributes.key?(primary) && @attributes[primary].nil?
386
+ @new_record = false
387
+ end
388
+
389
+ def update_record
390
+ primary = self.class.primary_key
391
+ changed = changes
392
+ changed.delete(primary)
393
+ return if changed.empty?
394
+ attributes = {}
395
+ changed.each { |name, pair| attributes[name] = pair.last }
396
+ relation = self.class.where(primary => read_attribute(primary))
397
+ relation.update_all(attributes)
398
+ end
399
+
400
+ def touch_timestamps
401
+ now = Time.now.utc
402
+ if @attributes.key?("updated_at")
403
+ @attributes["updated_at"] = now
404
+ end
405
+ if new_record? && @attributes.key?("created_at") && @attributes["created_at"].nil?
406
+ @attributes["created_at"] = now
407
+ end
408
+ end
409
+
410
+ def cast_default(column)
411
+ value = column.default
412
+ return nil if value.nil?
413
+ if value.length >= 2 && value[0, 1] == "'" && value[-1, 1] == "'"
414
+ value = value[1...-1].gsub("''", "'")
415
+ end
416
+ column.cast(value)
417
+ end
418
+ end
419
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RufletRecord
4
+ class Column
5
+ attr_reader :name, :sql_type, :default
6
+
7
+ def initialize(name, sql_type, null, default, primary)
8
+ @name = name.to_s.freeze
9
+ @sql_type = sql_type.to_s.freeze
10
+ @null = null
11
+ @default = default
12
+ @primary = primary
13
+ end
14
+
15
+ def null?
16
+ @null
17
+ end
18
+
19
+ def primary?
20
+ @primary
21
+ end
22
+
23
+ def type
24
+ value = @sql_type.upcase
25
+ return :boolean if value.include?("BOOL")
26
+ return :integer if value.include?("INT")
27
+ return :float if value.include?("REAL") || value.include?("FLOA") || value.include?("DOUB")
28
+ return :decimal if value.include?("DEC") || value.include?("NUM")
29
+ return :datetime if value.include?("DATE") || value.include?("TIME")
30
+ return :binary if value.include?("BLOB")
31
+
32
+ :string
33
+ end
34
+
35
+ def cast(value)
36
+ return nil if value.nil?
37
+
38
+ case type
39
+ when :boolean
40
+ value == true || value.to_s == "1" || value.to_s.downcase == "true"
41
+ when :integer
42
+ value.to_i
43
+ when :float, :decimal
44
+ value.to_f
45
+ when :datetime
46
+ cast_time(value)
47
+ else
48
+ value
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def cast_time(value)
55
+ return value if value.is_a?(Time)
56
+ match = /\A(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?/.match(value.to_s)
57
+ return value unless match
58
+
59
+ Time.utc(
60
+ match[1].to_i, match[2].to_i, match[3].to_i,
61
+ (match[4] || "0").to_i, (match[5] || "0").to_i,
62
+ (match[6] || "0").to_i
63
+ )
64
+ rescue StandardError
65
+ value
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RufletRecord
4
+ class Error < StandardError; end
5
+ class ConnectionNotEstablished < Error; end
6
+ class StatementInvalid < Error; end
7
+ class RecordNotFound < Error; end
8
+ class RecordInvalid < Error
9
+ attr_reader :record
10
+
11
+ def initialize(record)
12
+ @record = record
13
+ super("Validation failed: #{record.errors.full_messages.join(', ')}")
14
+ end
15
+ end
16
+ class RecordNotSaved < Error; end
17
+ class UnknownAttributeError < Error; end
18
+
19
+ class Errors
20
+ def initialize
21
+ @messages = {}
22
+ end
23
+
24
+ def add(attribute, message)
25
+ key = attribute.to_sym
26
+ (@messages[key] ||= []) << message.to_s
27
+ end
28
+
29
+ def [](attribute)
30
+ @messages[attribute.to_sym] || []
31
+ end
32
+
33
+ def empty?
34
+ @messages.empty?
35
+ end
36
+
37
+ def any?
38
+ !empty?
39
+ end
40
+
41
+ def clear
42
+ @messages.clear
43
+ end
44
+
45
+ def to_hash
46
+ @messages.dup
47
+ end
48
+
49
+ def full_messages
50
+ messages = []
51
+ @messages.each do |attribute, entries|
52
+ entries.each do |message|
53
+ if attribute == :base
54
+ messages << message
55
+ else
56
+ name = attribute.to_s.tr("_", " ")
57
+ messages << "#{name[0, 1].upcase}#{name[1..-1]} #{message}"
58
+ end
59
+ end
60
+ end
61
+ messages
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RufletRecord
4
+ module Inflector
5
+ class << self
6
+
7
+ def underscore(value)
8
+ word = value.to_s.gsub("::", "/")
9
+ word = word.gsub(/([A-Z]+)([A-Z][a-z])/, "\\1_\\2")
10
+ word = word.gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
11
+ word.tr("-", "_").downcase
12
+ end
13
+
14
+ def pluralize(value)
15
+ word = value.to_s
16
+ return "#{word[0...-1]}ies" if word.end_with?("y") && word.length > 1
17
+ return "#{word}es" if word.end_with?("s", "x", "z", "ch", "sh")
18
+
19
+ "#{word}s"
20
+ end
21
+
22
+ def singularize(value)
23
+ word = value.to_s
24
+ return "#{word[0...-3]}y" if word.end_with?("ies")
25
+ return word[0...-2] if word.end_with?("ches", "shes", "xes", "zes")
26
+ return word[0...-1] if word.end_with?("s") && !word.end_with?("ss")
27
+
28
+ word
29
+ end
30
+
31
+ def classify(value)
32
+ singularize(value.to_s).split("_").map { |part| "#{part[0, 1].upcase}#{part[1..-1]}" }.join
33
+ end
34
+
35
+ def constantize(value)
36
+ names = value.to_s.split("::")
37
+ names.shift if names.first == ""
38
+ names.inject(Object) { |scope, name| scope.const_get(name) }
39
+ end
40
+ end
41
+ end
42
+ end