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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +173 -0
- data/lib/ruflet_record/adapters/sqlite_adapter.rb +210 -0
- data/lib/ruflet_record/base.rb +419 -0
- data/lib/ruflet_record/column.rb +68 -0
- data/lib/ruflet_record/errors.rb +64 -0
- data/lib/ruflet_record/inflector.rb +42 -0
- data/lib/ruflet_record/relation.rb +292 -0
- data/lib/ruflet_record/schema.rb +211 -0
- data/lib/ruflet_record/sql.rb +175 -0
- data/lib/ruflet_record/version.rb +5 -0
- data/lib/ruflet_record.rb +36 -0
- metadata +67 -0
|
@@ -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
|