attr_pouch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/attr_pouch.rb ADDED
@@ -0,0 +1,407 @@
1
+ require 'pg'
2
+ require 'sequel'
3
+ require 'attr_pouch/errors'
4
+
5
+ module AttrPouch
6
+ def self.configure
7
+ @@config ||= Config.new
8
+ yield @@config
9
+ end
10
+
11
+ def self.config
12
+ @@config ||= Config.new
13
+ end
14
+
15
+ def self.included(base)
16
+ base.extend(ClassMethods)
17
+ end
18
+
19
+ class Field
20
+ attr_reader :name, :type, :raw_type, :opts
21
+
22
+ def self.encode(type, &block)
23
+ @@encoders ||= {}
24
+ @@encoders[type] = block
25
+ end
26
+
27
+ def self.decode(type, &block)
28
+ @@decoders ||= {}
29
+ @@decoders[type] = block
30
+ end
31
+
32
+ def self.encoders; @@encoders; end
33
+ def self.decoders; @@decoders; end
34
+
35
+ def self.infer_type(field=nil, &block)
36
+ if block_given?
37
+ @@type_inferrer = block
38
+ else
39
+ if @@type_inferrer.nil?
40
+ raise InvalidFieldError, "No type inference configured"
41
+ else
42
+ type = @@type_inferrer.call(field)
43
+ if type.nil?
44
+ raise InvalidFieldError, "Could not infer type of field #{field}"
45
+ end
46
+ type
47
+ end
48
+ end
49
+ end
50
+
51
+ def initialize(name, opts)
52
+ @name = name
53
+ if opts.has_key?(:type)
54
+ @type = to_class(opts.fetch(:type))
55
+ else
56
+ @type = self.class.infer_type(self)
57
+ end
58
+ @raw_type = type
59
+ @opts = opts
60
+ end
61
+
62
+ def alias_as(new_name)
63
+ if new_name == name
64
+ self
65
+ else
66
+ self.class.new(new_name, opts)
67
+ end
68
+ end
69
+
70
+ def required?
71
+ !(has_default? || deletable?)
72
+ end
73
+
74
+ def has_default?
75
+ opts.has_key?(:default)
76
+ end
77
+
78
+ def default
79
+ opts.fetch(:default, nil)
80
+ end
81
+
82
+ def mutable?
83
+ opts.fetch(:mutable, true)
84
+ end
85
+
86
+ def deletable?
87
+ opts.fetch(:deletable, false)
88
+ end
89
+
90
+ def previous_aliases
91
+ was = opts.fetch(:was, [])
92
+ if was.respond_to?(:to_a)
93
+ was.to_a
94
+ else
95
+ [ was ]
96
+ end
97
+ end
98
+
99
+ def all_names
100
+ [ name ] + previous_aliases
101
+ end
102
+
103
+ def write(store, value, encode: true)
104
+ if store.has_key?(name)
105
+ raise ImmutableFieldUpdateError unless mutable?
106
+ end
107
+ if encode
108
+ value = self.encode(value)
109
+ end
110
+ if !store.has_key?(name) || value != store[name]
111
+ store[name] = value
112
+ previous_aliases.each { |a| store.delete(a) }
113
+ true
114
+ else
115
+ false
116
+ end
117
+ end
118
+
119
+ def read(store, decode: true)
120
+ present_as = all_names.find { |n| !store.nil? && store.has_key?(n) }
121
+ if store.nil? || present_as.nil?
122
+ if required?
123
+ raise MissingRequiredFieldError,
124
+ "Expected field #{inspect} to exist"
125
+ else
126
+ default if decode
127
+ end
128
+ elsif present_as == name
129
+ raw = store.fetch(name)
130
+ if decode
131
+ decode(raw)
132
+ else
133
+ raw
134
+ end
135
+ else
136
+ alias_as(present_as).read(store)
137
+ end
138
+ end
139
+
140
+ def decode(value)
141
+ decoder.call(self, value) unless value.nil?
142
+ end
143
+
144
+ def encode(value)
145
+ encoder.call(self, value) unless value.nil?
146
+ end
147
+
148
+ def decoder
149
+ @decoder ||= self.class.decoders
150
+ .find(method(:ensure_decoder)) do |decoder_type, _|
151
+ compatible_codec?(decoder_type)
152
+ end.last
153
+ end
154
+
155
+ def encoder
156
+ @encoder ||= self.class.encoders
157
+ .find(method(:ensure_encoder)) do |encoder_type, _|
158
+ compatible_codec?(encoder_type)
159
+ end.last
160
+ end
161
+
162
+ private
163
+
164
+ def compatible_codec?(codec_type)
165
+ if self.type.is_a?(Class) && codec_type.is_a?(Class)
166
+ self.type <= codec_type
167
+ else
168
+ self.type == codec_type
169
+ end
170
+ rescue
171
+ false
172
+ end
173
+
174
+ def ensure_encoder
175
+ raise MissingCodecError,
176
+ "No encoder found for #{inspect}"
177
+ end
178
+
179
+ def ensure_decoder
180
+ raise MissingCodecError,
181
+ "No decoder found for #{inspect}"
182
+ end
183
+
184
+ def to_class(type)
185
+ return type if type.is_a?(Class) || type.is_a?(Symbol)
186
+ type.to_s.split('::').inject(Object) do |moodule, klass|
187
+ moodule.const_get(klass)
188
+ end
189
+ end
190
+ end
191
+
192
+ class Config
193
+ def initialize
194
+ @encoders = {}
195
+ @decoders = {}
196
+ end
197
+
198
+ def infer_type(&block)
199
+ if block_given?
200
+ Field.infer_type(&block)
201
+ else
202
+ raise ArgumentError, "Expected block to infer types with"
203
+ end
204
+ end
205
+
206
+ def encode(type, &block)
207
+ Field.encode(type, &block)
208
+ end
209
+
210
+ def decode(type, &block)
211
+ Field.decode(type, &block)
212
+ end
213
+ end
214
+
215
+ class Pouch
216
+ VALID_FIELD_NAME_REGEXP = %r{\A[a-zA-Z0-9_]+\??\z}
217
+
218
+ def initialize(host, storage_field, default_pouch: Sequel.hstore({}))
219
+ @host = host
220
+ @storage_field = storage_field
221
+ @default_pouch = default_pouch
222
+ @fields = {}
223
+ end
224
+
225
+ def field_definition(name)
226
+ @fields[name]
227
+ end
228
+
229
+ def field(name, opts={})
230
+ unless VALID_FIELD_NAME_REGEXP.match(name)
231
+ raise InvalidFieldError, "Field name must match #{VALID_FIELD_NAME_REGEXP}"
232
+ end
233
+
234
+ field = Field.new(name, opts)
235
+ @fields[name] = field
236
+
237
+ storage_field = @storage_field
238
+ default = @default_pouch
239
+
240
+ @host.class_eval do
241
+ def_dataset_method(:where_pouch) do |pouch_field, expr_hash|
242
+ # TODO: encode the values so we can query properly
243
+ ds = self
244
+ expr_hash.each do |key, value|
245
+ pouch = model.pouch(pouch_field)
246
+ if pouch.nil?
247
+ raise ArgumentError,
248
+ "No pouch defined for #{pouch_field}"
249
+ end
250
+ field = pouch.field_definition(key)
251
+ if field.nil?
252
+ raise ArgumentError,
253
+ "No field #{key} defined for pouch #{pouch_field}"
254
+ end
255
+
256
+ if value.respond_to?(:each)
257
+ value.each_with_index do |v,i|
258
+ encoded_val = field.encode(v)
259
+ if i == 0
260
+ ds = ds.where(Sequel.hstore(pouch_field)
261
+ .contains(Sequel.hstore(key => encoded_val)))
262
+ else
263
+ ds = ds.or(Sequel.hstore(pouch_field)
264
+ .contains(Sequel.hstore(key => encoded_val)))
265
+ end
266
+ end
267
+ elsif value.nil?
268
+ ds = ds.where(Sequel.hstore(pouch_field).has_key?(key.to_s) => false)
269
+ .or(Sequel.hstore(pouch_field)
270
+ .contains(Sequel.hstore(key => nil)))
271
+ else
272
+ ds = ds.where(Sequel.hstore(pouch_field)
273
+ .contains(Sequel.hstore(key => field.encode(value))))
274
+ end
275
+ end
276
+ ds
277
+ end
278
+
279
+ define_method(name) do
280
+ store = self[storage_field]
281
+ field.read(store)
282
+ end
283
+
284
+ define_method("#{name.to_s.sub(/\?\z/, '')}=") do |value|
285
+ store = self[storage_field]
286
+ was_nil = store.nil?
287
+ store = default if was_nil
288
+ changed = field.write(store, value)
289
+ if was_nil
290
+ self[storage_field] = store
291
+ else
292
+ modified! storage_field if changed
293
+ end
294
+ end
295
+
296
+ if field.deletable?
297
+ delete_method = "delete_#{name.to_s.sub(/\?\z/, '')}"
298
+ define_method(delete_method) do
299
+ store = self[storage_field]
300
+ unless store.nil?
301
+ field.all_names.each { |a| store.delete(a) }
302
+ modified! storage_field
303
+ end
304
+ end
305
+
306
+ define_method("#{delete_method}!") do
307
+ self.public_send(delete_method)
308
+ save_changes
309
+ end
310
+ end
311
+
312
+ if opts.has_key?(:raw_field)
313
+ raw_name = opts[:raw_field]
314
+
315
+ define_method(raw_name) do
316
+ store = self[storage_field]
317
+ field.read(store, decode: false)
318
+ end
319
+
320
+ define_method("#{raw_name.to_s.sub(/\?\z/, '')}=") do |value|
321
+ store = self[storage_field]
322
+ was_nil = store.nil?
323
+ store = default if was_nil
324
+ changed = field.write(store, value, encode: false)
325
+ if was_nil
326
+ self[storage_field] = store
327
+ else
328
+ modified! storage_field if changed
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ module ClassMethods
337
+ def pouch(field, &block)
338
+ if block_given?
339
+ pouch = Pouch.new(self, field)
340
+ @pouches ||= {}
341
+ @pouches[field] = pouch
342
+ pouch.instance_eval(&block)
343
+ else
344
+ @pouches[field]
345
+ end
346
+ end
347
+ end
348
+ end
349
+
350
+ AttrPouch.configure do |config|
351
+ config.encode(String) do |field, value|
352
+ value.to_s
353
+ end
354
+ config.decode(String) do |field, value|
355
+ value
356
+ end
357
+
358
+ config.encode(Integer) do |field, value|
359
+ value.to_s
360
+ end
361
+ config.decode(Integer) do |field, value|
362
+ Integer(value)
363
+ end
364
+
365
+ config.encode(Float) do |field, value|
366
+ value.to_s
367
+ end
368
+ config.decode(Float) do |field, value|
369
+ Float(value)
370
+ end
371
+
372
+ config.encode(Time) do |field, value|
373
+ value.strftime('%Y-%m-%d %H:%M:%S.%N')
374
+ end
375
+ config.decode(Time) do |field, value|
376
+ Time.parse(value)
377
+ end
378
+
379
+ config.encode(:bool) do |field, value|
380
+ value.to_s
381
+ end
382
+ config.decode(:bool) do |field, value|
383
+ value == 'true'
384
+ end
385
+
386
+ config.encode(Sequel::Model) do |field, value|
387
+ klass = field.type
388
+ value[klass.primary_key]
389
+ end
390
+ config.decode(Sequel::Model) do |field, value|
391
+ klass = field.type
392
+ klass[value]
393
+ end
394
+
395
+ config.infer_type do |field|
396
+ case field.name
397
+ when /\Anum_|_(?:count|size)\z/
398
+ Integer
399
+ when /_(?:at|by)\z/
400
+ Time
401
+ when /\?\z/
402
+ :bool
403
+ else
404
+ String
405
+ end
406
+ end
407
+ end