pod4 0.9.3 → 0.10.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 497cdee877587568f7dbf706092902684a780149
4
- data.tar.gz: baf92f8cc0c052e66bcb6afd59b51e8863fde2f7
2
+ SHA256:
3
+ metadata.gz: 4166ff590c49ce621e16234388a8ef366b1530d165da50793c108827bc0f9776
4
+ data.tar.gz: 7462bf1368e90706f568aa4cdcc42b59226f572fa9f9b8f9b1d55f419a4c129b
5
5
  SHA512:
6
- metadata.gz: ce0a49a11b357f032d2fea29f0e387f902457f9782916002fdb0b03e2de0be5e506eb75f2dd9f7ece0fdac848de2250c09b3f1fe6b8b173170be3fea851c951e
7
- data.tar.gz: aa208f77f71751b999d9a0b1fbac87b8d4c77fea417162d77aa66336b33738fc720cf95904fd58fbfebdae157c0bf329873131f15b500e233ae8cab9ee4bc859
6
+ metadata.gz: 51653424bb26ddb8e809afd1a838ba31b4237e8f7afe1dcb12aab79521881a9415149bb910f5279ca7f27ad90af602359131108a67f57ae450a91d65ce1f3d05
7
+ data.tar.gz: f167459fc8541f0d989c4cf7150e8dd2adb04d522971b186fb6f058f140cb6dc81b065512159312a70a60842fc0db27ed87cbba13fa9899005fa65dd48ad688d
data/.hgtags CHANGED
@@ -26,3 +26,4 @@ ccb4a8711560725e358f7fb87c76f4787eac5ed5 0.8.0
26
26
  f1b3a814e7a8c7d818b57f6d35ce7ed749573d76 0.9.1
27
27
  93b68b850a52781e2f269cb073c77aab174282e5 0.9.2
28
28
  4860714dd83ddb5b4ed0b7e32459fdaa845ac2fa 0.9.3
29
+ 77ad625b2ab048bd660194cf093015b1f2698069 0.10
data/README.md CHANGED
@@ -559,3 +559,11 @@ get the idea:
559
559
 
560
560
  end
561
561
 
562
+ Extensions
563
+ ----------
564
+
565
+ There are now some mixins that you can use to extend the functionality of Pod4 models. Have a look
566
+ at the comments at the top of the mixin in question if you want details.
567
+
568
+ * typecasting -- force columns to be a specific ruby type, validation helpers, some encoding stuff
569
+ * encrypting -- encrypt text columns
@@ -0,0 +1,225 @@
1
+ require "openssl"
2
+ require "pod4/errors"
3
+ require "pod4/metaxing"
4
+
5
+
6
+ module Pod4
7
+
8
+
9
+ ##
10
+ # A mixin to give you basic encryption, transparently.
11
+ #
12
+ # Example
13
+ # -------
14
+ #
15
+ # class Foo < Pod4::Model
16
+ # include Pod4::Encrypting
17
+ #
18
+ # set_key $encryption_key
19
+ # set_iv_column :nonce
20
+ # encrypted_columns :one, :two, :three
21
+ #
22
+ # ...
23
+ # end
24
+ #
25
+ # So, this adds `set_key`, `set_iv_column`, and `encrypted_columns` to the model DSL. Only
26
+ # `set_iv_column` is optional, and it is **highly** recommended.
27
+ #
28
+ # set_key
29
+ # -------
30
+ #
31
+ # Can be any string you like, but should ideally be long and random. If it's not long enough you
32
+ # will get an exception. The key is used for all encryption on the model.
33
+ #
34
+ # You probably have a single key for the entire database and pass it to your application via an
35
+ # environment variable. But we don't care about that.
36
+ #
37
+ # set_iv_column
38
+ # -------------
39
+ #
40
+ # The name of a text column on the table which holds the initialisation vector, or nonce, for the
41
+ # record. IVs don't have to be secret, but they should be different for each record; we take
42
+ # care of creating them for you.
43
+ #
44
+ # If you don't provide an IV column, then we fall back to insecure ECB mode for the encryption.
45
+ # Don't make us do that.
46
+ #
47
+ # encrypted_columns
48
+ # -----------------
49
+ #
50
+ # The list of columns to encrypt. In addition, it acts just the same as attr_columns, so you can
51
+ # name the column there too, or not. Up to you.
52
+ #
53
+ # Changes to Behaviour of Model
54
+ # -----------------------------
55
+ #
56
+ # `map_to_interface`: data going from the model to the interface has the relevant columns
57
+ # encrypted. If the IV column is nil, we set it to a good IV.
58
+ #
59
+ # `map_to_model`: data going from the interface to the model has the relevant columns decrypted.
60
+ #
61
+ # Assumptions / limitations:
62
+ #
63
+ # * One key for all the data in the model.
64
+ #
65
+ # * a column on the table holding an initiation vector (IV, nonce) for each record. See above.
66
+ #
67
+ # * we only store encrypted data in text columns, and we can't guarantee that the encrypted data
68
+ # will be the same length as when unencrypted.
69
+ #
70
+ # Additional Methods
71
+ # ------------------
72
+ #
73
+ # You will almost certainly never need to use these.
74
+ #
75
+ # * `encryption_iv` returns the value of the IV column of the record, whatever it is.
76
+ #
77
+ # Notes
78
+ # -----
79
+ #
80
+ # Encryption is provided by OpenSSL::Cipher. For more information, you should read the official
81
+ # Ruby docs for this; they are really helpful.
82
+ #
83
+ module Encrypting
84
+ CIPHER_IV = "AES-128-CBC"
85
+ CIPHER_NO_IV = "AES-128-ECB"
86
+
87
+ ##
88
+ # A little bit of magic, for which I apologise.
89
+ #
90
+ # When you include this module it actually adds the methods in ClassMethods to the class as if
91
+ # you had called `extend Encrypting:ClassMethds` *AND* adds the methods in InstanceMethods as
92
+ # if you had written `prepend Encrypting::InstanceMethods`.
93
+ #
94
+ # In my defence: I didn't want to have to make you remember to do that...
95
+ #
96
+ def self.included(base)
97
+ base.extend ClassMethods
98
+ base.send(:prepend, InstanceMethods)
99
+ end
100
+
101
+
102
+ module ClassMethods
103
+ include Metaxing
104
+
105
+
106
+ def set_key(key)
107
+ define_class_method(:encryption_key) {key}
108
+ end
109
+
110
+ def set_iv_column(column)
111
+ define_class_method(:encryption_iv_column) {column}
112
+ attr_columns column unless columns.include? column
113
+ end
114
+
115
+ def encrypted_columns(*ecolumns)
116
+ ec = encryption_columns.dup + ecolumns
117
+ define_class_method(:encryption_columns) {ec}
118
+ attr_columns( *(ec - columns) )
119
+ end
120
+
121
+ def encryption_key; nil; end
122
+ def encryption_iv_column; nil; end
123
+ def encryption_columns; []; end
124
+
125
+ end # of ClassMethods
126
+
127
+
128
+ module InstanceMethods
129
+
130
+ ##
131
+ # When mapping to the interface, encrypt the encryptable columns from the model
132
+ #
133
+ def map_to_interface
134
+ hash = super.to_h
135
+ cipher = get_cipher(:encrypt)
136
+
137
+ # If the IV is not set we need to set it both in the model object AND the hash, since we've
138
+ # already obtained the hash from the model object.
139
+ if use_iv? && encryption_iv.nil?
140
+ set_encryption_iv( cipher.random_iv )
141
+ hash[self.class.encryption_iv_column] = encryption_iv
142
+ end
143
+
144
+ self.class.encryption_columns.each do |col|
145
+ hash[col] = crypt(cipher, encryption_iv, hash[col].to_s)
146
+ end
147
+
148
+ Octothorpe.new(hash)
149
+ end
150
+
151
+ ##
152
+ # When mapping to the model, decrypt the encrypted columns from the interface
153
+ #
154
+ def map_to_model(ot)
155
+ hash = ot.to_h
156
+ cipher = get_cipher(:decrypt)
157
+ iv = hash[self.class.encryption_iv_column] # not yet set on the model
158
+
159
+ self.class.encryption_columns.each do |col|
160
+ hash[col] = crypt(cipher, iv, hash[col])
161
+ end
162
+
163
+ super Octothorpe.new(hash)
164
+ end
165
+
166
+ ##
167
+ # The value of the IV field (whatever it is) _as currently stored on the model_
168
+ #
169
+ def encryption_iv
170
+ return nil unless use_iv?
171
+ instance_variable_get( "@#{self.class.encryption_iv_column}".to_sym )
172
+ end
173
+
174
+ private
175
+
176
+ ##
177
+ # Set the iv column on the model, whatever it is
178
+ #
179
+ def set_encryption_iv(iv)
180
+ return unless use_iv?
181
+ instance_variable_set( "@#{self.class.encryption_iv_column}".to_sym, iv )
182
+ end
183
+
184
+ ##
185
+ # If we have declared an IV column, we can use IV in encryption
186
+ #
187
+ def use_iv?
188
+ !self.class.encryption_iv_column.nil?
189
+ end
190
+
191
+ ##
192
+ # Return the correct OpenSSL Cipher object
193
+ #
194
+ def get_cipher(direction)
195
+ cipher = OpenSSL::Cipher.new(use_iv? ? CIPHER_IV : CIPHER_NO_IV)
196
+ case direction
197
+ when :encrypt then cipher.encrypt
198
+ when :decrypt then cipher.decrypt
199
+ end
200
+ cipher
201
+
202
+ rescue OpenSSL::Cipher::CipherError
203
+ raise Pod4::Pod4Error, $!
204
+ end
205
+
206
+ ##
207
+ # Encrypt / decrypt
208
+ #
209
+ def crypt(cipher, iv, string)
210
+ return string if use_iv? and iv.nil?
211
+ cipher.key = self.class.encryption_key
212
+ cipher.iv = iv if use_iv?
213
+ cipher.update(string) + cipher.final
214
+
215
+ rescue OpenSSL::Cipher::CipherError
216
+ raise Pod4::Pod4Error, $!
217
+ end
218
+
219
+ end # of InstanceMethods
220
+
221
+
222
+ end # of Encrypting
223
+
224
+ end
225
+
@@ -1,3 +1,5 @@
1
+ #require 'BigDecimal'
2
+ require 'time'
1
3
  require 'pod4/errors'
2
4
  require 'pod4/metaxing'
3
5
 
@@ -6,22 +8,111 @@ module Pod4
6
8
 
7
9
 
8
10
  ##
9
- # A mixin to give you some more options to control how Pod4 maps the interface to the model.
11
+ # A mixin to give you some more options to control how Pod4 deals with data types.
10
12
  #
11
- # Eventually we will actually have typecasting in here. For now all this allows you to do is
12
- # enforce an encoding -- which will be of use if you are dealing with MSSQL, or with certain
13
- # interfaces which appear to deal with the code page poorly:
13
+ # Example
14
+ # -------
14
15
  #
15
- # class FOo < Pod4::Model
16
+ # class Foo < Pod4::Model
16
17
  # include Pod4::TypeCasting
17
- #
18
+ #
19
+ # class Interface < Pod4::SomeInterface
20
+ # # blah blah blah
21
+ # end
22
+ # set_interface Interface.new($stuff)
23
+ #
24
+ # attr_columns :name, :issue, :created, :due, :last_update, :completed, :thing
25
+ #
26
+ # # Now the meat
18
27
  # force_encoding Encoding::UTF-8
19
- #
20
- # ...
28
+ # typecast :issue, as: Integer, strict: true
29
+ # typecast :created, :due, as: Date
30
+ # typecast :last_update, as: Time
31
+ # typecast :completed, as: BigDecimal, ot_as: Float
32
+ # typecast :thing, use: mymethod
21
33
  # end
22
34
  #
35
+ # So this adds two commands to the model DSL: force_encoding, and typecast. Both are optional.
36
+ #
37
+ # Force Encoding
38
+ # --------------
39
+ #
40
+ # Pass this a Ruby encoding, and it will call force the encoding of each incoming value from the
41
+ # database to match. It is to work around problems with some data sources like MSSQL, which may
42
+ # deal with encoding poorly.
43
+ #
44
+ # Typecasting
45
+ # -----------
46
+ #
47
+ # This has the syntax: `typecast <attr> [,...], <options>`.
48
+ #
49
+ # Options are `as:`, `ot_as:`, `strict:` and `use:`. You must specify either `as:` or `use:`.
50
+ #
51
+ # Valid types are BigDecimal, Float, Integer, Date, Time, and :boolean.
52
+ #
53
+ # Changes to Behaviour of Model
54
+ # -----------------------------
55
+ #
56
+ # General: Any attributes named using `typecast` are set `attr_reader` if they are not already
57
+ # so.
58
+ #
59
+ # `map_to_model`: incoming data from the data source is coerced to the given encoding if
60
+ # `force_encoding` has been used.
61
+ #
62
+ # `set()`: typecast attributes are cast as per their settings, or if they cannot be cast, are left
63
+ # alone. (Unless you have specified `strict: true`, in which case they are set to nil.)
64
+ #
65
+ # `to_ot()`: any typecast attributes with `ot_as` are cast that way in the outgoing OT, and set
66
+ # guard that way too (see Octothorpe#guard) to give a reasonable default value instead of nil.
67
+ #
68
+ # `map_to_interface()`: typecast attributes are cast as per their settings, or if they cannot be
69
+ # cast, are set to nil.
70
+ #
71
+ # Additional methods
72
+ # ------------------
73
+ #
74
+ # The following are provided:
75
+ #
76
+ # * `typecast?(:columnname, value)` returns true if the value can be cast; value defaults to the
77
+ # column value if not given.
78
+ #
79
+ # * `typecast(type, value, options)` returns a typecast value, or either the original value, or
80
+ # nil if options[:strict] is true.
81
+ #
82
+ # * `guard(octothorpe)` sets guard conditions on the given octothorpe, based on the attributes
83
+ # typecast knows about. If the value was nil, it will be a reasonable default for the type
84
+ # instead.
85
+ #
86
+ # Custom Typecasting Methods
87
+ # --------------------------
88
+ #
89
+ # By specifying `use: my_method` you are telling Pod4 that you have a method that will return the
90
+ # typecast value for the type. This method will be called as `my_method(value, options)`,
91
+ # where value is the value to be typecast, and options is the hash of options you specified for
92
+ # that column. Pod4 will set the column to whatever your method returns.
93
+ #
94
+ # What you don't get
95
+ # ------------------
96
+ #
97
+ # None of this has any direct effect on validation, although of course we do provide methods such
98
+ # as `typecast?()` to specifically help you with validation.
99
+ #
100
+ # Naming an attribute using `typecast` does not automatically make is a Pod4 column; you need to
101
+ # use `attr_column`, just as in plain Pod4. Furthermore, *only* Pod4 columns can be named in the
102
+ # typecast command, although you can use the `typecast` instance method, etc., to help you roll
103
+ # your own typecasting for non-column attributes.
104
+ #
105
+ # Loss of information. If your column is typecast to Integer, then setting it to 12.34 will not
106
+ # round it to 12. Likewise, I know that Time.to_date is a thing, but we don't support it.
107
+ #
108
+ # Protection from nil, except when using `ot_as:`. A column is always allowed to be nil,
109
+ # regardless of how it is typecast. (On the contrary: by forcing strict columns to nil if they
110
+ # fail typecasting, we help you validate.)
111
+ #
23
112
  module TypeCasting
24
113
 
114
+ TYPES = [ Date, Time, Integer, Float, BigDecimal, :boolean ]
115
+
25
116
  ##
26
117
  # A little bit of magic, for which I apologise.
27
118
  #
@@ -46,8 +137,29 @@ module Pod4
46
137
  end
47
138
 
48
139
  def encoding; nil; end
49
- end
50
- ##
140
+
141
+ def typecast(*args)
142
+ options = args.pop
143
+ raise Pod4Error, "Bad Type" \
144
+ unless options.keys.include?(:use) || TYPES.include?(options[:as])
145
+
146
+ raise Pod4Error, "Bad Typecasting" unless options.is_a?(Hash) \
147
+ && options.keys.any?{|o| %i|as use|.include? o} \
148
+ && args.size >= 1
149
+
150
+ # Modify self.typecasts to look like: {foo: {as: Date}, bar: {as: Time, strict: true}, ...}
151
+ c = typecasts.dup
152
+ args.each do |f|
153
+ raise Pod4Error, "Unknown column '#{f}'" unless columns.include?(f)
154
+ c[f] = options
155
+ end
156
+
157
+ define_class_method(:typecasts) {c}
158
+ end
159
+
160
+ def typecasts; {}; end
161
+
162
+ end # of ClassMethods
51
163
 
52
164
 
53
165
  module InstanceMethods
@@ -55,17 +167,171 @@ module Pod4
55
167
  def map_to_model(ot)
56
168
  enc = self.class.encoding
57
169
 
58
- ot.each do |_,v|
170
+ ot.each_value do |v|
59
171
  v.force_encoding(enc) if v.kind_of?(String) && enc
60
172
  end
61
173
 
62
174
  super(ot)
63
175
  end
64
176
 
65
- end
66
- ##
177
+ def set(ot)
178
+ hash = typecast_ot(ot)
179
+ super(ot.merge hash)
180
+ end
181
+
182
+ def map_to_interface
183
+ ot = super
184
+ hash = typecast_ot(ot, strict: true)
185
+ ot.merge hash
186
+ end
187
+
188
+ def to_ot
189
+ ot = super
190
+ ot2 = ot.merge typecast_ot_to_ot(ot)
191
+
192
+ self.class.typecasts.each do |fld, tc|
193
+ set_guard(ot2, fld, tc[:ot_as]) if tc[:ot_as]
194
+ end
195
+
196
+ ot2
197
+ end
198
+
199
+ ##
200
+ # Return thing cast to type. If opt[:strict] is true, then return nil if thing cannot be
201
+ # cast to type; otherwise return thing unchanged.
202
+ #
203
+ def typecast(type, thing, opt={})
204
+ # Nothing to do
205
+ return thing if type.is_a?(Class) && thing.is_a?(type)
206
+
207
+ # Nothing wrong with nil for our purposes; it's always allowed
208
+ return thing if thing.nil?
209
+
210
+ # For all current cases, attempting to typecast a blank string should return nil
211
+ return nil if thing =~ /\A\s*\Z/
212
+
213
+ # The order we try these in matters
214
+ return tc_bigdecimal(thing) if type == BigDecimal
215
+ return tc_float(thing) if type == Float
216
+ return tc_integer(thing) if type == Integer
217
+ return tc_date(thing) if type == Date
218
+ return tc_time(thing) if type == Time
219
+ return tc_boolean(thing) if type == :boolean
220
+
221
+ fail Pod4Error, "Bad type passed to typecast()"
222
+ rescue ArgumentError
223
+ return (opt[:strict] ? nil : thing)
224
+ end
225
+
226
+ ##
227
+ # Return true if the attribute can be cast to the given value.
228
+ # You must name an attribute you specified in a typecast declaration, or you will get an
229
+ # exception.
230
+ # You may pass a value to test, or failing that, we take the current value of the attribute.
231
+ #
232
+ def typecast?(attr, val=nil)
233
+ fail Pod4Error, "Unknown column passed to typecast?()" \
234
+ unless (tc = self.class.typecasts[attr])
235
+
236
+ val = instance_variable_get("@#{attr}".to_sym) if val.nil?
237
+ !typecast_one(val, tc.merge(strict: true)).nil?
238
+ end
239
+
240
+ ##
241
+ # set Octothorpe Guards for everything in the given OT, based on the typecast settings.
242
+ #
243
+ def guard(ot)
244
+ self.class.typecasts.each do |fld, tc|
245
+ type = tc[:ot_as] || tc[:as]
246
+ set_guard(ot, fld, type) if type
247
+ end
248
+ end
249
+
250
+ private
251
+
252
+ ##
253
+ # Return a hash of changes for an OT based on our settings
254
+ #
255
+ def typecast_ot(ot, opts={})
256
+ hash = {}
257
+ ot.each do |k,v|
258
+ tc = self.class.typecasts[k]
259
+ hash[k] = typecast_one(v, tc.merge(opts)) if tc
260
+ end
261
+ hash
262
+ end
263
+
264
+ ##
265
+ # As typecast_ot, but this is a specific helper for to_ot
266
+ #
267
+ def typecast_ot_to_ot(ot)
268
+ hash = {}
269
+ ot.each do |k,v|
270
+ tc = self.class.typecasts[k]
271
+ hash[k] = (tc && tc[:ot_as]) ? typecast(tc[:ot_as], v) : v
272
+ end
273
+ hash
274
+ end
275
+
276
+ ##
277
+ # Helper for typecast_ot: cast one attribute
278
+ #
279
+ def typecast_one(val, tc)
280
+ if tc[:use]
281
+ self.__send__(tc[:use], val, tc)
282
+ else
283
+ typecast(tc[:as], val, tc)
284
+ end
285
+ end
286
+
287
+ ##
288
+ # Set the guard clause for one attribute
289
+ # Note that Time.new returns now, and Date.new returns some date in antiquity. We don't
290
+ # consider those helpful, so we give you 1900-1-1 in both cases
291
+ #
292
+ def set_guard(ot, fld, tc)
293
+ case tc.to_s
294
+ when "BigDecimal" then ot.guard(fld) { BigDecimal.new("0") }
295
+ when "Float" then ot.guard(fld) { Float(0) }
296
+ when "Integer" then ot.guard(fld) { Integer(0) }
297
+ when "Date" then ot.guard(fld) { Date.new(1900, 1, 1) }
298
+ when "Time" then ot.guard(fld) { Time.new(1900, 1, 1) }
299
+ when "boolean" then ot.guard(fld) { false }
300
+ end
301
+ end
302
+
303
+ def tc_bigdecimal(thing)
304
+ Float(thing) # BigDecimal sucks at catching bad decimals
305
+ BigDecimal.new(thing.to_s)
306
+ end
307
+
308
+ def tc_float(thing)
309
+ Float(thing)
310
+ end
311
+
312
+ def tc_integer(thing)
313
+ Integer(thing.to_s, 10)
314
+ end
315
+
316
+ def tc_date(thing)
317
+ fail ArgumentError, "Can't cast Time to Date" if thing.is_a?(Time)
318
+ thing.respond_to?(:to_date) ? thing.to_date : Date.parse(thing.to_s)
319
+ end
320
+
321
+ def tc_time(thing)
322
+ thing.respond_to?(:to_time) ? thing.to_time : Time.parse(thing.to_s)
323
+ end
324
+
325
+ def tc_boolean(thing)
326
+ return thing if thing == true || thing == false
327
+ return true if %w|true yes y on t 1|.include?(thing.to_s.downcase)
328
+ return false if %w|false no n off f 0|.include?(thing.to_s.downcase)
329
+ fail ArgumentError, "Cannot typecast string to Boolean"
330
+ end
331
+
332
+ end # of InstanceMethods
67
333
 
68
- end
334
+ end # of TypeCasting
69
335
 
70
336
 
71
337
  end
data/lib/pod4/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pod4
2
- VERSION = '0.9.3'
2
+ VERSION = '0.10.0'
3
3
  end
data/md/typecasting.md ADDED
@@ -0,0 +1,80 @@
1
+ TypeCasting
2
+ ===========
3
+
4
+ Example
5
+ -------
6
+
7
+ ```
8
+ require 'pod4'
9
+ require 'pod4/someinterface'
10
+ require 'pod4/typecasting'
11
+
12
+ class Foo < Pod4::Model
13
+ include Pod4::TypeCasting
14
+
15
+ class Interface < Pod4::SomeInterface
16
+ # blah blah blah
17
+ end
18
+
19
+ set_interface Interface.new($stuff)
20
+
21
+ attr_columns :name, :issue, :created, :due, :last_update, :completed, :thing
22
+
23
+ # Now the meat
24
+ typecast :issue, as: Integer
25
+ typecast :created, :due, as: Date
26
+ typecast :last_update, as: Time
27
+ typecast :completed, as: BigDecimal, ot_as: Float
28
+ typecast :thing, use: mymethod
29
+ end
30
+ ```
31
+
32
+ What You Get
33
+ ------------
34
+
35
+ ### Every attribute named in a typecast gets:
36
+
37
+ * An accessor. (Probably it already has one, if it is named in attr_columns, but it doesn't have to
38
+ be. Note, though, that we don't add the attribute to the column list and it does not get output
39
+ in to_ot by default.)
40
+
41
+ * An attempt to force the value to that data type on set(). If the value cannot be coerced, it is
42
+ *untouched*.
43
+
44
+ * A second attempt to cast on to_interface(). This time, if the value cannot be coerced, it is set
45
+ to nil.
46
+
47
+ * if the optional `ot_as` type is set, then we cast a third time in the `to_ot()` method;
48
+ additionally we guard the OT with the base type using Octothorpe.guard. Note that this only
49
+ effects to_ot().
50
+
51
+ ### Additionally the user can call these methods:
52
+
53
+ * `typecast?(:columnname, value)` returns true if the value can be cast; value defaults to the
54
+ column value if not given.
55
+
56
+ * `typecast(type, value, strict)` returns a typecast value, or either the original value, or nil if
57
+ strict is `:strict'.
58
+
59
+ * `guard(octothorpe)` will set guard conditions for nil values on the given octothorpe, based on
60
+ the attributes typecast knows about.
61
+
62
+ ### The following types will be supported:
63
+
64
+ * Integer
65
+ * BigDecimal
66
+ * Float
67
+ * Date
68
+ * Time
69
+ * :boolean
70
+
71
+ Also: custom typecasting (`use: mymethod`, above). This must accept two parameters: the value, and
72
+ an option hash.
73
+
74
+
75
+ What You Don't Get
76
+ ------------------
77
+
78
+ Validation. It's entirely up to you to decide how to validate and we won't second guess that. But
79
+ we do provide the `typecast?` method to help.
80
+