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 +5 -5
- data/.hgtags +1 -0
- data/README.md +8 -0
- data/lib/pod4/encrypting.rb +225 -0
- data/lib/pod4/typecasting.rb +280 -14
- data/lib/pod4/version.rb +1 -1
- data/md/typecasting.md +80 -0
- data/spec/common/model_plus_encrypting_spec.rb +379 -0
- data/spec/common/model_plus_typecasting_spec.rb +434 -22
- metadata +16 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4166ff590c49ce621e16234388a8ef366b1530d165da50793c108827bc0f9776
|
4
|
+
data.tar.gz: 7462bf1368e90706f568aa4cdcc42b59226f572fa9f9b8f9b1d55f419a4c129b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 51653424bb26ddb8e809afd1a838ba31b4237e8f7afe1dcb12aab79521881a9415149bb910f5279ca7f27ad90af602359131108a67f57ae450a91d65ce1f3d05
|
7
|
+
data.tar.gz: f167459fc8541f0d989c4cf7150e8dd2adb04d522971b186fb6f058f140cb6dc81b065512159312a70a60842fc0db27ed87cbba13fa9899005fa65dd48ad688d
|
data/.hgtags
CHANGED
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
|
+
|
data/lib/pod4/typecasting.rb
CHANGED
@@ -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
|
11
|
+
# A mixin to give you some more options to control how Pod4 deals with data types.
|
10
12
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# interfaces which appear to deal with the code page poorly:
|
13
|
+
# Example
|
14
|
+
# -------
|
14
15
|
#
|
15
|
-
# class
|
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
|
-
|
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.
|
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
|
-
|
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
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
|
+
|