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 +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
|
+
|