datoki 1.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.
data/lib/datoki.rb ADDED
@@ -0,0 +1,748 @@
1
+
2
+ require 'sequel'
3
+
4
+ module Datoki
5
+
6
+ UTC_NOW_DATE = ::Sequel.lit("CURRENT_DATE")
7
+ UTC_NOW_RAW = "timezone('UTC'::text, now())"
8
+ UTC_NOW = ::Sequel.lit("timezone('UTC'::text, now())")
9
+
10
+ Invalid = Class.new RuntimeError
11
+ Schema_Conflict = Class.new RuntimeError
12
+
13
+ Actions = [:all, :create, :read, :update, :update_or_create, :trash, :delete]
14
+ Char_Types = [:varchar, :text]
15
+ Numeric_Types = [:smallint, :integer, :bigint, :decimal, :numeric]
16
+ Types = Char_Types + Numeric_Types + [:datetime]
17
+
18
+ class << self
19
+
20
+ def included klass
21
+ klass.extend Def_Field
22
+ klass.initialize_def_field
23
+ end
24
+
25
+ def db db = :return
26
+ return @db if db == :return
27
+ @db = db
28
+ @tables = @db.tables
29
+ end
30
+
31
+ def db_type_to_ruby type, alt = nil
32
+ if Datoki::Types.include?( type.to_sym )
33
+ type.to_sym
34
+ elsif type['character varying']
35
+ :varchar
36
+ elsif Datoki::Types.include?(alt)
37
+ alt
38
+ else
39
+ fail("Unknown db type: #{type.inspect}")
40
+ end
41
+ end
42
+
43
+ end # === class self ===
44
+
45
+ module Def_Field
46
+
47
+ attr_reader :ons, :fields
48
+
49
+ def initialize_def_field
50
+ @record_errors = false
51
+ @ons = {}
52
+ @fields = {}
53
+ @current_field = nil
54
+ @schema = {}
55
+ @schema_match = false
56
+ @table_name = nil
57
+ name = self.to_s.downcase.to_sym
58
+ table(name) if Datoki.db.tables.include?(name)
59
+ end
60
+
61
+ def schema_match?
62
+ @schema_match
63
+ end
64
+
65
+ def record_errors?
66
+ @record_errors
67
+ end
68
+
69
+ def record_errors
70
+ @record_errors = true
71
+ end
72
+
73
+ def table name
74
+ if !@schema.empty? || @table_name
75
+ fail "Schema/table already defined: #{@table_name.inspect}"
76
+ end
77
+
78
+ db_schema = Datoki.db.schema(name)
79
+
80
+ if !db_schema
81
+ fail "Schema not found for: #{name.inspect}"
82
+ end
83
+
84
+ @table_name = name
85
+
86
+ db_schema.each { |pair|
87
+ @schema[pair.first] = pair.last
88
+ }
89
+
90
+ if @schema.empty?
91
+ @schema_match = true
92
+ end
93
+
94
+ schema
95
+ end
96
+
97
+ def html_escape
98
+ @html_escape ||= begin
99
+ fields.inject({}) { |memo, (name, meta)|
100
+ memo[name] = meta[:html_escape]
101
+ memo
102
+ }
103
+ end
104
+ end
105
+
106
+ def schema *args
107
+ case args.size
108
+
109
+ when 0
110
+ @schema
111
+
112
+ when 1
113
+ result = @schema[args.first]
114
+ fail "Unknown field: #{args.first.inspect}" unless result
115
+ result
116
+
117
+ else
118
+ fail "Unknown args: #{args.inspect}"
119
+
120
+ end
121
+ end
122
+
123
+ def inspect_field? target, name, *args
124
+ case target
125
+ when :type
126
+ meta = fields[name]
127
+ fail "Unknown field: #{name.inspect}" unless meta
128
+ return true if args.include?(meta[:type])
129
+ return true if args.include?(:chars) && Char_Types.include?(meta[:type])
130
+ args.include?(:numeric) && Numeric_Types.include?(meta[:type])
131
+ else
132
+ fail "Unknown arg: #{target.inspect}"
133
+ end
134
+ end
135
+
136
+ def field? *args
137
+ inspect_field?(:type, field[:name], *args)
138
+ end
139
+
140
+ def field *args
141
+ return fields[@current_field] if args.empty?
142
+ return fields[args.first] unless block_given?
143
+
144
+ name = args.first
145
+
146
+ fail "#{name.inspect} already defined." if fields[name]
147
+
148
+ fields[name] = {
149
+ :name => name,
150
+ :type => :unknown,
151
+ :english_name => name.to_s.freeze,
152
+ :allow => {:null => false},
153
+ :disable => {},
154
+ :cleaners => {},
155
+ :on => {}
156
+ }
157
+
158
+ @current_field = name
159
+
160
+ if field? :chars
161
+ field[:allow][:strip] = true
162
+ end
163
+
164
+ if schema[name]
165
+ if schema[name].has_key? :max_length
166
+ fields[name][:max] = schema[name][:max_length]
167
+ end
168
+ end
169
+
170
+ yield
171
+
172
+ fail("Type not specified for #{name.inspect}") if field[:type] == :unknown
173
+
174
+ # === check :allow_null and :min are not both set.
175
+ if field?(:chars) && field[:allow][:null] && field.has_key?(:min) && field[:min] < 1
176
+ fail "#{field[:type].inspect} can't be both: allow :null && :min = #{field[:min]}"
177
+ end
178
+
179
+ # === Ensure schema matches with field definition:
180
+ schema_match
181
+
182
+ field[:html_escape] = case
183
+ when field[:html_escape]
184
+ field[:html_escape]
185
+ when field?(:numeric)
186
+ :number
187
+ when field?(:chars)
188
+ :string
189
+ else
190
+ fail "Unknown html_escape for: #{field[:name].inspect}"
191
+ end
192
+
193
+ @current_field = nil
194
+ end # === def field
195
+
196
+ def schema_match target = :current
197
+ return true if !@table_name
198
+ return true if schema_match?
199
+
200
+ if target == :all # === do a schema match on entire table
201
+ schema.each { |name, db_schema|
202
+ orig_field = @current_field
203
+ @current_field = name
204
+ schema_match
205
+ @current_field = orig_field
206
+ }
207
+
208
+ @schema_match = true
209
+ return true
210
+ end # === if target
211
+
212
+ name = @current_field
213
+ db_schema = schema[@current_field]
214
+
215
+ if db_schema && !field && db_schema[:type] != :datetime
216
+ fail Schema_Conflict, "#{name}: #{name.inspect} has not been defined."
217
+ end
218
+
219
+ return true if field[:schema_match]
220
+
221
+ if db_schema[:allow_null] != field[:allow][:null]
222
+ fail Schema_Conflict, "#{name}: :allow_null: #{db_schema[:allow_null].inspect} != #{field[:allow][:null].inspect}"
223
+ end
224
+
225
+ if field?(:chars)
226
+ if !field[:min].is_a?(Numeric) || field[:min] < 0
227
+ fail ":min not properly defined for #{name.inspect}: #{field[:min].inspect}"
228
+ end
229
+
230
+ if !field[:max].is_a?(Numeric)
231
+ fail ":max not properly defined for #{name.inspect}: #{field[:max].inspect}"
232
+ end
233
+ end
234
+
235
+ if db_schema.has_key?(:max_length)
236
+ if field[:max] != db_schema[:max_length]
237
+ fail Schema_Conflict, "#{name}: :max: #{db_schema[:max_length].inspect} != #{field[:max].inspect}"
238
+ end
239
+ end
240
+
241
+ if !!db_schema[:primary_key] != !!field[:primary_key]
242
+ fail Schema_Conflict, "#{name}: :primary_key: #{db_schema[:primary_key].inspect} != #{field[:primary_key].inspect}"
243
+ end
244
+
245
+ # === match :type
246
+ db_type = Datoki.db_type_to_ruby db_schema[:db_type], db_schema[:type]
247
+ type = field[:type]
248
+ if db_type != type
249
+ fail Schema_Conflict, "#{name}: :type: #{db_type.inspect} != #{type.inspect}"
250
+ end
251
+
252
+ # === match :max_length
253
+ db_max = db_schema[:max_length]
254
+ max = field[:max]
255
+ if !db_max.nil? && db_max != max
256
+ fail Schema_Conflict, "#{name}: :max_length: #{db_max.inspect} != #{max.inspect}"
257
+ end
258
+
259
+ # === match :min_length
260
+ db_min = db_schema[:min_length]
261
+ min = field[:min]
262
+ if !db_min.nil? && db_min != min
263
+ fail Schema_Conflict, "#{name}: :min_length: #{db_min.inspect} != #{min.inspect}"
264
+ end
265
+
266
+ # === match :allow_null
267
+ if db_schema[:allow_null] != field[:allow][:null]
268
+ fail Schema_Conflict, "#{name}: :allow_null: #{db_schema[:allow_null].inspect} != #{field[:allow][:null].inspect}"
269
+ end
270
+
271
+ field[:schema_match] = true
272
+ end
273
+
274
+ def on action, meth_name_sym
275
+ fail "Invalid action: #{action.inspect}" unless Actions.include? action
276
+ if field
277
+ field[:on][action] ||= {}
278
+ field[:on][action][meth_name_sym] = true
279
+ else
280
+ @ons[action] ||= {}
281
+ @ons[action][meth_name_sym] = true
282
+ end
283
+ self
284
+ end
285
+
286
+ def primary_key
287
+ field[:primary_key] = true
288
+ if field?(:unknown)
289
+ if schema[field[:name]]
290
+ type schema[field[:name]][:type]
291
+ else
292
+ type :integer
293
+ end
294
+ end
295
+
296
+ true
297
+ end
298
+
299
+ def text *args
300
+ type :text, *args
301
+ end
302
+
303
+ def href *args
304
+ field[:html_escape] = :href
305
+ case args.map(&:class)
306
+ when []
307
+ varchar 0, 255
308
+ when [NilClass]
309
+ varchar nil, 1, (schema[field[:name]] ? schema[field[:name]][:max_length] : 255)
310
+ else
311
+ varchar *args
312
+ end
313
+ end
314
+
315
+ Types.each { |name|
316
+ eval <<-EOF
317
+ def #{name} *args
318
+ type :#{name}, *args
319
+ end
320
+ EOF
321
+ }
322
+
323
+ def type name, *args
324
+ field[:type] = name
325
+
326
+ if field? :chars
327
+
328
+ enable :strip
329
+
330
+ if field?(:text)
331
+ field[:max] ||= 4000
332
+ else
333
+ field[:max] ||= 255
334
+ end
335
+
336
+ if schema[name] && !schema[name][:allow_null]
337
+ field[:min] = 1
338
+ end
339
+
340
+ end # === if field? :chars
341
+
342
+ case args.map(&:class)
343
+
344
+ when []
345
+ # do nothing
346
+
347
+ when [Array]
348
+ field[:options] = args.first
349
+ enable(:null) if field[:options].include? nil
350
+ disable :min, :max
351
+
352
+ when [NilClass]
353
+ if field?(:chars)
354
+ fail "A :min and :max is required for String fields."
355
+ end
356
+
357
+ enable :null
358
+
359
+ when [NilClass, Fixnum, Fixnum]
360
+ field[:allow][:null] = true
361
+ field[:min] = args[-2]
362
+ field[:max] = args.last
363
+
364
+ when [Fixnum, Fixnum]
365
+ field[:min], field[:max] = args
366
+
367
+ else
368
+ fail "Unknown args: #{args.inspect}"
369
+
370
+ end # === case
371
+
372
+ end # === def
373
+
374
+ def enable *props
375
+ props.each { |prop|
376
+ case prop
377
+ when :strip, :null
378
+ field[:allow][prop] = true
379
+ else
380
+ field[:cleaners][prop] = true
381
+ end
382
+ }
383
+ end
384
+
385
+ def disable *props
386
+ props.each { |prop|
387
+ case prop
388
+ when :min, :max
389
+ field.delete prop
390
+ when :strip, :null
391
+ field[:allow][prop] = false
392
+ else
393
+ field[:cleaners][prop] = false
394
+ end
395
+ }
396
+ end
397
+
398
+ def set_to *args
399
+ field[:cleaners][:set_to] ||= []
400
+ field[:cleaners][:set_to].concat args
401
+ end
402
+
403
+ def equal_to *args
404
+ field[:cleaners][:equal_to] ||= []
405
+ field[:cleaners][:equal_to].concat args
406
+ end
407
+
408
+ def included_in arr
409
+ field[:cleaners][:included_in] ||= []
410
+ field[:cleaners][:included_in].concat arr
411
+ end
412
+
413
+ # === String-only methods ===========
414
+ %w{
415
+ upcase
416
+ to_i
417
+ }.each { |name|
418
+ eval <<-EOF
419
+ def #{name} *args
420
+ fail "Not allowed for \#{field[:type]}" unless field?(:chars)
421
+ enable :#{name}
422
+ end
423
+ EOF
424
+ }
425
+
426
+ def match *args
427
+ fail "Not allowed for #{field[:type].inspect}" unless field?(:chars)
428
+ field[:cleaners][:match] ||= []
429
+ field[:cleaners][:match] << args
430
+ end
431
+
432
+ def not_match *args
433
+ fail "Not allowed for #{field[:type].inspect}" unless field?(:chars)
434
+ field[:cleaners][:not_match] ||= []
435
+ field[:cleaners][:not_match] << args
436
+ self
437
+ end
438
+
439
+ def create h = {}
440
+ r = new
441
+ r.create h
442
+ end
443
+
444
+ end # === Def_Field
445
+
446
+ # ================= Instance Methods ===============
447
+
448
+ def initialize data = nil
449
+ @data = nil
450
+ @new_data = nil
451
+ @field_name = nil
452
+ @clean_data = nil
453
+ @errors = nil
454
+
455
+ self.class.schema_match(:all)
456
+ end
457
+
458
+ def errors
459
+ @errors ||= {}
460
+ end
461
+
462
+ def errors?
463
+ @errors && !@errors.empty?
464
+ end
465
+
466
+ def save_error msg
467
+ @errors ||= {}
468
+ @errors[field_name] ||= {}
469
+ @errors[field_name][:msg] = msg
470
+ @errors[field_name][:value] = val
471
+ end
472
+
473
+ def clean_data
474
+ @clean_data ||= {}
475
+ end
476
+
477
+ def new_data
478
+ @new_data ||= {}
479
+ end
480
+
481
+ def fail! msg
482
+ err_msg = msg.gsub(/!([a-z\_\-]+)/i) { |raw|
483
+ name = $1
484
+ case name
485
+ when "English_name"
486
+ self.class.fields[field_name][:english_name].capitalize.gsub('_', ' ')
487
+ when "ENGLISH_NAME"
488
+ self.class.fields[field_name][:english_name].upcase.gsub('_', ' ')
489
+ when "max", "min", "exact_size"
490
+ self.class.fields[field_name][name.downcase.to_sym]
491
+ else
492
+ fail "Unknown value: #{name}"
493
+ end
494
+ }
495
+
496
+ if self.class.record_errors?
497
+ save_error err_msg
498
+ throw :error_saved
499
+ else
500
+ fail Invalid, err_msg
501
+ end
502
+ end
503
+
504
+ def field_name *args
505
+ case args.size
506
+ when 0
507
+ fail "Field name not set." unless @field_name
508
+ @field_name
509
+ when 1
510
+ @field_name = args.first
511
+ else
512
+ fail "Unknown args: #{args.inspect}"
513
+ end
514
+ end
515
+
516
+ def val
517
+ if clean_data.has_key?(field_name)
518
+ clean_data[field_name]
519
+ else
520
+ new_data[field_name]
521
+ end
522
+ end
523
+
524
+ def val! new_val
525
+ clean_data[field_name] = new_val
526
+ end
527
+
528
+ def field *args
529
+ case args.size
530
+ when 0
531
+ self.class.fields[field_name]
532
+ when 1
533
+ self.class.fields[args.first]
534
+ else
535
+ fail "Unknown args: #{args.inspect}"
536
+ end
537
+ end
538
+
539
+ def field? *args
540
+ self.class.inspect_field? :type, field_name, *args
541
+ end
542
+
543
+ def run action
544
+ self.class.fields.each { |f_name, f_meta|
545
+
546
+ field_name f_name
547
+ is_set = new_data.has_key?(field_name)
548
+ is_update = action == :update
549
+ is_nil = is_set && new_data[field_name].nil?
550
+
551
+ # === Should the field be skipped? ===============
552
+ next if !is_set && is_update
553
+ next if !is_set && field[:primary_key]
554
+ next if field[:allow][:null] && (!is_set || is_nil)
555
+
556
+ if is_set
557
+ val! new_data[field_name]
558
+ elsif field.has_key?(:default)
559
+ val! field[:default]
560
+ end
561
+
562
+ if val.is_a?(String) && field[:allow][:strip]
563
+ val! val.strip
564
+ end
565
+
566
+ if field?(:chars) && !field.has_key?(:min) && val.is_a?(String) && field[:allow][:null]
567
+ val! nil
568
+ end
569
+
570
+ catch :error_saved do
571
+
572
+ if field?(:numeric) && val.is_a?(String)
573
+ clean_val = Integer(val) rescue String
574
+ if clean_val == String
575
+ fail! "!English_name must be numeric."
576
+ else
577
+ val! clean_val
578
+ end
579
+ end
580
+
581
+ # === check required. ============
582
+ if val.nil? && !field[:allow][:null]
583
+ fail! "!English_name is required."
584
+ end
585
+
586
+ if field?(:text) && val.is_a?(String) && val.empty? && field[:min].to_i > 0
587
+ fail! "!English_name is required."
588
+ end
589
+ # ================================
590
+
591
+ # === check min, max ======
592
+ if val.is_a?(String) || val.is_a?(Numeric)
593
+ case [field[:min], field[:max]].map(&:class)
594
+
595
+ when [NilClass, NilClass]
596
+ # do nothing
597
+
598
+ when [NilClass, Fixnum]
599
+ case
600
+ when val.is_a?(String) && val.size > field[:max]
601
+ fail! "!English_name can't be longer than !max characters."
602
+ when val.is_a?(Numeric) && val > field[:max]
603
+ fail! "!English_name can't be higher than !max."
604
+ end
605
+
606
+ when [Fixnum, NilClass]
607
+ case
608
+ when val.is_a?(String) && val.size < field[:min]
609
+ fail! "!English_name can't be shorter than !min characters."
610
+ when val.is_a?(Numeric) && val < field[:min]
611
+ fail! "!English_name can't be less than !min."
612
+ end
613
+
614
+ when [Fixnum, Fixnum]
615
+ case
616
+ when val.is_a?(String) && (val.size < field[:min] || val.size > field[:max])
617
+ fail! "!English_name must be between !min and !max characters."
618
+ when val.is_a?(Numeric) && (val < field[:min] || val > field[:max])
619
+ fail! "!English_name must be between !min and !max."
620
+ end
621
+
622
+ else
623
+ fail "Unknown values for :min, :max: #{field[:min].inspect}, #{field[:max].inspect}"
624
+ end
625
+ end # === if
626
+ # ================================
627
+
628
+ # === to_i if necessary ==========
629
+ if field?(:numeric)
630
+ val! val.to_i
631
+ end
632
+ # ================================
633
+
634
+ # === :strip if necessary ========
635
+ if field?(:chars) && field[:allow][:strip] && val.is_a?(String)
636
+ val! val.strip
637
+ end
638
+ # ================================
639
+
640
+ # === Is value in options? =======
641
+ if field[:options]
642
+ if !field[:options].include?(val)
643
+ fail! "!English_name can only be: #{field[:options].map(&:inspect).join ', '}"
644
+ end
645
+ end
646
+ # ================================
647
+
648
+ field[:cleaners].each { |cleaner, args|
649
+ next if args === false # === cleaner has been disabled.
650
+
651
+ case cleaner
652
+
653
+ when :type
654
+ case
655
+ when field?(:numeric) && !val.is_a?(Integer)
656
+ fail! "!English_name needs to be an integer."
657
+ when field?(:chars) && !val.is_a?(String)
658
+ fail! "!English_name needs to be a String."
659
+ end
660
+
661
+ when :exact_size
662
+ if val.size != field[:exact_size]
663
+ case
664
+ when field?(:chars) || val.is_a?(String)
665
+ fail! "!English_name needs to be !exact_size in length."
666
+ else
667
+ fail! "!English_name can only be !exact_size in size."
668
+ end
669
+ end
670
+
671
+ when :set_to
672
+ args.each { |meth|
673
+ val! send(meth)
674
+ }
675
+
676
+ when :equal_to
677
+ args.each { |pair|
678
+ meth, msg, other = pair
679
+ target = send(meth)
680
+ fail!(msg || "!English_name must be equal to: #{target.inspect}") unless val == target
681
+ }
682
+
683
+ when :included_in
684
+ arr, msg, other = args
685
+ fail!(msg || "!English_name must be one of these: #{arr.join ', '}") unless arr.include?(val)
686
+
687
+ when :upcase
688
+ val! val.upcase
689
+
690
+ when :match
691
+ args.each { |pair|
692
+ regex, msg, other = pair
693
+ if val !~ regex
694
+ fail!(msg || "!English_name must match #{regex.inspect}")
695
+ end
696
+ }
697
+
698
+ when :not_match
699
+ args.each { |pair|
700
+ regex, msg, other = pair
701
+ if val =~ regex
702
+ fail!(msg || "!English_name must not match #{regex.inspect}")
703
+ end
704
+ }
705
+
706
+ else
707
+ fail "Cleaner not implemented: #{cleaner.inspect}"
708
+ end # === case cleaner
709
+
710
+
711
+ } # === field[:cleaners].each
712
+
713
+ field[:on][action].each { |meth, is_enabled|
714
+ next unless is_enabled
715
+ send meth
716
+ } if field[:on][action]
717
+
718
+ end # === catch :error_saved
719
+ } # === field
720
+
721
+ return if errors?
722
+
723
+ self.class.ons.each { |action, meths|
724
+ meths.each { |meth, is_enabled|
725
+ next unless is_enabled
726
+ catch :error_saved do
727
+ send meth
728
+ end
729
+ }
730
+ }
731
+ end
732
+
733
+ def create new_data
734
+ @new_data = new_data
735
+ run :create
736
+ self
737
+ end
738
+
739
+ def update new_data
740
+ @new_data = new_data
741
+ run :update
742
+ self
743
+ end
744
+
745
+ end # === module Datoki ===
746
+
747
+
748
+