datoki 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+