pod4 0.9.3 → 0.10.0

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