serializable_attributes 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +56 -0
- data/Rakefile +132 -0
- data/gemfiles/ar-2.2.gemfile +6 -0
- data/gemfiles/ar-2.3.gemfile +6 -0
- data/gemfiles/ar-3.0.gemfile +6 -0
- data/gemfiles/ar-3.1.gemfile +6 -0
- data/init.rb +2 -0
- data/lib/serializable_attributes/duplicable.rb +65 -0
- data/lib/serializable_attributes/format/active_support_json.rb +29 -0
- data/lib/serializable_attributes/schema.rb +188 -0
- data/lib/serializable_attributes/types.rb +139 -0
- data/lib/serializable_attributes.rb +66 -0
- data/rails_init.rb +3 -0
- data/script/setup +1 -0
- data/serializable_attributes.gemspec +67 -0
- data/test/serialized_attributes_test.rb +382 -0
- data/test/test_helper.rb +107 -0
- data/test/types_test.rb +42 -0
- metadata +80 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
module SerializableAttributes
|
2
|
+
class AttributeType
|
3
|
+
def initialize(options = {})
|
4
|
+
@default = options[:default]
|
5
|
+
end
|
6
|
+
|
7
|
+
def encode(s) s end
|
8
|
+
|
9
|
+
def type_for(key)
|
10
|
+
SerializableAttributes.const_get(key.to_s.classify).new
|
11
|
+
end
|
12
|
+
|
13
|
+
def default
|
14
|
+
@default && @default.duplicable? ? @default.dup : @default
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Integer < AttributeType
|
19
|
+
attr_reader :default
|
20
|
+
def parse(input) input.blank? ? nil : input.to_i end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Float < AttributeType
|
24
|
+
attr_reader :default
|
25
|
+
def parse(input) input.blank? ? nil : input.to_f end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Boolean < AttributeType
|
29
|
+
attr_reader :default
|
30
|
+
def parse(input)
|
31
|
+
return nil if input == ""
|
32
|
+
input && input.respond_to?(:to_i) ? (input.to_i > 0) : input
|
33
|
+
end
|
34
|
+
|
35
|
+
def encode(input)
|
36
|
+
return nil if input.to_s.empty?
|
37
|
+
return 0 if input == 'false'
|
38
|
+
input ? 1 : 0
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class String < AttributeType
|
43
|
+
# converts unicode (\u003c) to the actual character
|
44
|
+
# http://rishida.net/tools/conversion/
|
45
|
+
def parse(str)
|
46
|
+
return nil if str.nil?
|
47
|
+
str.to_s.gsub(/\\u([0-9a-fA-F]{4})/) do |s|
|
48
|
+
int = $1.to_i(16)
|
49
|
+
if int.zero? && s != "0000"
|
50
|
+
s
|
51
|
+
else
|
52
|
+
[int].pack("U")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Time < AttributeType
|
59
|
+
def parse(input)
|
60
|
+
return nil if input.blank?
|
61
|
+
case input
|
62
|
+
when ::Time then input
|
63
|
+
when ::String then ::Time.parse(input)
|
64
|
+
else input.to_time
|
65
|
+
end
|
66
|
+
end
|
67
|
+
def encode(input) input ? input.utc.xmlschema : nil end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Array < AttributeType
|
71
|
+
def initialize(options = {})
|
72
|
+
super
|
73
|
+
@item_type = type_for(options[:type] || "String")
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse(input)
|
77
|
+
if input.nil?
|
78
|
+
nil
|
79
|
+
elsif input.blank?
|
80
|
+
[]
|
81
|
+
else
|
82
|
+
input.map! { |item| @item_type.parse(item) }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def encode(input)
|
87
|
+
if input.nil?
|
88
|
+
nil
|
89
|
+
elsif input.blank?
|
90
|
+
[]
|
91
|
+
else
|
92
|
+
input.map! { |item| @item_type.encode(item) }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Hash < AttributeType
|
98
|
+
def initialize(options = {})
|
99
|
+
super
|
100
|
+
@key_type = String.new
|
101
|
+
@types = (options[:types] || {})
|
102
|
+
@types.keys.each do |key|
|
103
|
+
value = @types.delete(key)
|
104
|
+
@types[key.to_s] = type_for(value)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def parse(input)
|
109
|
+
return nil if input.blank?
|
110
|
+
input.keys.each do |key|
|
111
|
+
value = input.delete(key)
|
112
|
+
key_s = @key_type.parse(key)
|
113
|
+
type = @types[key_s] || @key_type
|
114
|
+
input[key_s] = type.parse(value)
|
115
|
+
end
|
116
|
+
input
|
117
|
+
end
|
118
|
+
|
119
|
+
def encode(input)
|
120
|
+
return nil if input.blank?
|
121
|
+
input.each do |key, value|
|
122
|
+
type = @types[key] || @key_type
|
123
|
+
input[key] = type.encode(value)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class << self
|
129
|
+
attr_accessor :types
|
130
|
+
def add_type(type, object = nil)
|
131
|
+
types[type] = object
|
132
|
+
Schema.send(:define_method, type) do |*names|
|
133
|
+
field type, *names
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
self.types = {}
|
138
|
+
end
|
139
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# create a BLOB column and setup your field types for conversion
|
2
|
+
# and convenient attr methods
|
3
|
+
#
|
4
|
+
# class Profile < ActiveRecord::Base
|
5
|
+
# # not needed if used as a rails plugin
|
6
|
+
# SerializableAttributes.setup(self)
|
7
|
+
#
|
8
|
+
# # assumes #data serializes to raw_data blob field
|
9
|
+
# serialize_attributes do
|
10
|
+
# string :title, :description
|
11
|
+
# integer :age
|
12
|
+
# float :rank, :percentage
|
13
|
+
# time :birthday
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# # Serializes #data to assumed raw_data blob field
|
17
|
+
# serialize_attributes :data do
|
18
|
+
# string :title, :description
|
19
|
+
# integer :age
|
20
|
+
# float :rank, :percentage
|
21
|
+
# time :birthday
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # set the blob field
|
25
|
+
# serialize_attributes :data, :blob => :serialized_field do
|
26
|
+
# string :title, :description
|
27
|
+
# integer :age
|
28
|
+
# float :rank, :percentage
|
29
|
+
# time :birthday
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
module SerializableAttributes
|
34
|
+
VERSION = "0.9.0"
|
35
|
+
|
36
|
+
require File.expand_path('../serializable_attributes/types', __FILE__)
|
37
|
+
require File.expand_path('../serializable_attributes/schema', __FILE__)
|
38
|
+
|
39
|
+
if nil.respond_to?(:duplicable?)
|
40
|
+
require File.expand_path('../serializable_attributes/duplicable', __FILE__)
|
41
|
+
end
|
42
|
+
|
43
|
+
module Format
|
44
|
+
autoload :ActiveSupportJson, File.expand_path('../serializable_attributes/format/active_support_json', __FILE__)
|
45
|
+
end
|
46
|
+
|
47
|
+
add_type :string, String
|
48
|
+
add_type :integer, Integer
|
49
|
+
add_type :float, Float
|
50
|
+
add_type :time, Time
|
51
|
+
add_type :boolean, Boolean
|
52
|
+
add_type :array, Array
|
53
|
+
add_type :hash, Hash
|
54
|
+
|
55
|
+
module ModelMethods
|
56
|
+
def serialize_attributes(field = :data, options = {}, &block)
|
57
|
+
schema = Schema.new(self, field, options)
|
58
|
+
schema.instance_eval(&block)
|
59
|
+
schema.fields.freeze
|
60
|
+
schema
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Object.const_set :SerializedAttributes, SerializableAttributes
|
66
|
+
|
data/rails_init.rb
ADDED
data/script/setup
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
bundle install --binstubs --path vendor/gems
|
@@ -0,0 +1,67 @@
|
|
1
|
+
## This is the rakegem gemspec template. Make sure you read and understand
|
2
|
+
## all of the comments. Some sections require modification, and others can
|
3
|
+
## be deleted if you don't need them. Once you understand the contents of
|
4
|
+
## this file, feel free to delete any comments that begin with two hash marks.
|
5
|
+
## You can find comprehensive Gem::Specification documentation, at
|
6
|
+
## http://docs.rubygems.org/read/chapter/20
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
9
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.3.5") if s.respond_to? :required_rubygems_version=
|
10
|
+
|
11
|
+
## Leave these as is they will be modified for you by the rake gemspec task.
|
12
|
+
## If your rubyforge_project name is different, then edit it and comment out
|
13
|
+
## the sub! line in the Rakefile
|
14
|
+
s.name = 'serializable_attributes'
|
15
|
+
s.version = '0.9.0'
|
16
|
+
s.date = '2011-12-05'
|
17
|
+
s.rubyforge_project = 'serializable_attributes'
|
18
|
+
|
19
|
+
## Make sure your summary is short. The description may be as long
|
20
|
+
## as you like.
|
21
|
+
s.summary = "Store a serialized hash of attributes in a single ActiveRecord column."
|
22
|
+
s.description = "A bridge between using AR and a full blown schema-free db."
|
23
|
+
|
24
|
+
## List the primary authors. If there are a bunch of authors, it's probably
|
25
|
+
## better to set the email to an email list or something. If you don't have
|
26
|
+
## a custom homepage, consider using your GitHub URL or the like.
|
27
|
+
s.authors = ["Rick Olson"]
|
28
|
+
s.email = 'technoweenie@gmail.com'
|
29
|
+
s.homepage = 'http://github.com/technoweenie/serialized_attributes'
|
30
|
+
|
31
|
+
## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
|
32
|
+
## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
|
33
|
+
s.require_paths = %w[lib]
|
34
|
+
|
35
|
+
s.add_dependency "activerecord", [">= 2.2.0", "< 3.2.0"]
|
36
|
+
|
37
|
+
## Leave this section as-is. It will be automatically generated from the
|
38
|
+
## contents of your Git repository via the gemspec task. DO NOT REMOVE
|
39
|
+
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
40
|
+
# = MANIFEST =
|
41
|
+
s.files = %w[
|
42
|
+
LICENSE
|
43
|
+
README.md
|
44
|
+
Rakefile
|
45
|
+
gemfiles/ar-2.2.gemfile
|
46
|
+
gemfiles/ar-2.3.gemfile
|
47
|
+
gemfiles/ar-3.0.gemfile
|
48
|
+
gemfiles/ar-3.1.gemfile
|
49
|
+
init.rb
|
50
|
+
lib/serializable_attributes.rb
|
51
|
+
lib/serializable_attributes/duplicable.rb
|
52
|
+
lib/serializable_attributes/format/active_support_json.rb
|
53
|
+
lib/serializable_attributes/schema.rb
|
54
|
+
lib/serializable_attributes/types.rb
|
55
|
+
rails_init.rb
|
56
|
+
script/setup
|
57
|
+
serializable_attributes.gemspec
|
58
|
+
test/serialized_attributes_test.rb
|
59
|
+
test/test_helper.rb
|
60
|
+
test/types_test.rb
|
61
|
+
]
|
62
|
+
# = MANIFEST =
|
63
|
+
|
64
|
+
## Test files will be grabbed from the file list. Make sure the path glob
|
65
|
+
## matches what you actually use.
|
66
|
+
s.test_files = s.files.select { |path| path =~ %r{^test/*/.+\.rb} }
|
67
|
+
end
|
@@ -0,0 +1,382 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require File.expand_path("../test_helper", __FILE__)
|
3
|
+
|
4
|
+
formatters = [SerializedAttributes::Format::ActiveSupportJson]
|
5
|
+
formatters.each do |fmt|
|
6
|
+
Object.const_set("SerializedAttributeWithSerializedDataTestWith#{fmt.name.demodulize}", Class.new(ActiveSupport::TestCase)).class_eval do
|
7
|
+
class << self
|
8
|
+
attr_accessor :format, :current_time, :raw_hash, :raw_data
|
9
|
+
end
|
10
|
+
self.format = fmt
|
11
|
+
self.current_time = Time.now.utc.midnight
|
12
|
+
self.raw_hash = {:title => 'abc', :age => 5, :average => 5.1, :birthday => current_time.xmlschema, :active => true, :names => %w(d e f), :lottery_picks => [1, 8, 7], :extras => {'b' => 'two'}}
|
13
|
+
self.raw_data = format.encode(raw_hash)
|
14
|
+
|
15
|
+
def setup
|
16
|
+
SerializedRecordWithDefaults.data_schema.formatter = SerializedRecord.data_schema.formatter = self.class.format
|
17
|
+
@newbie = SerializedRecordWithDefaults.new
|
18
|
+
@record = SerializedRecord.new
|
19
|
+
@changed = SerializedRecord.new
|
20
|
+
@record.raw_data = self.class.raw_data
|
21
|
+
@changed.raw_data = self.class.raw_data
|
22
|
+
@changed.title = 'def'
|
23
|
+
@changed.age = 6
|
24
|
+
end
|
25
|
+
|
26
|
+
test "schema lists attribute names" do
|
27
|
+
%w(title body age average birthday active default_in_my_favor names
|
28
|
+
lottery_picks extras).each do |attr|
|
29
|
+
assert SerializedRecord.data_schema.all_column_names.include?(attr),
|
30
|
+
"#{attr} attribute not found"
|
31
|
+
assert SerializedRecord.attribute_names.include?(attr),
|
32
|
+
"#{attr} attribute not found"
|
33
|
+
end
|
34
|
+
assert !SerializedRecord.data_schema.all_column_names.include?('raw_data'),
|
35
|
+
"raw_data attribute found"
|
36
|
+
end
|
37
|
+
|
38
|
+
test "existing model respects defaults from missing key" do
|
39
|
+
assert !@record.data.key?('default_in_my_favor')
|
40
|
+
assert @record.default_in_my_favor?
|
41
|
+
assert_equal true, @record.data['default_in_my_favor']
|
42
|
+
@record.default_in_my_favor = false
|
43
|
+
assert !@record.default_in_my_favor?
|
44
|
+
@record.default_in_my_favor = nil
|
45
|
+
assert @record.default_in_my_favor?
|
46
|
+
end
|
47
|
+
|
48
|
+
test "new model respects array defaults" do
|
49
|
+
assert_equal %w(a b c), @newbie.names
|
50
|
+
end
|
51
|
+
|
52
|
+
test "new model respects hash defaults" do
|
53
|
+
assert_equal({:a => 1}, @newbie.extras)
|
54
|
+
end
|
55
|
+
|
56
|
+
test "new model respects integer defaults" do
|
57
|
+
assert_equal 18, @newbie.age
|
58
|
+
end
|
59
|
+
|
60
|
+
test "new model respects string defaults" do
|
61
|
+
assert_equal 'blank', @newbie.title
|
62
|
+
assert_equal 'blank', @newbie.body
|
63
|
+
end
|
64
|
+
|
65
|
+
test "new model respects float defaults" do
|
66
|
+
assert_equal 5.2, @newbie.average
|
67
|
+
end
|
68
|
+
|
69
|
+
test "new model respects boolean defaults" do
|
70
|
+
assert @newbie.active?
|
71
|
+
end
|
72
|
+
|
73
|
+
test "new model respects date defaults" do
|
74
|
+
assert_equal Time.utc(2009, 1, 1), @newbie.birthday
|
75
|
+
end
|
76
|
+
|
77
|
+
test "reloads serialized data" do
|
78
|
+
@changed.id = 481516
|
79
|
+
assert_equal @record.title, @changed.reload(2342).title
|
80
|
+
assert_equal @record.age, @changed.age
|
81
|
+
end
|
82
|
+
|
83
|
+
test "initialized model is not changed" do
|
84
|
+
@record.data
|
85
|
+
assert !@record.data_changed?
|
86
|
+
end
|
87
|
+
|
88
|
+
test "#attribute_names contains serialized fields" do
|
89
|
+
assert_equal %w(active age average birthday extras lottery_picks names title), @record.attribute_names
|
90
|
+
@record.body = 'a'
|
91
|
+
assert_equal %w(active age average birthday body extras lottery_picks names title), @record.attribute_names
|
92
|
+
end
|
93
|
+
|
94
|
+
test "initialization does not call writers" do
|
95
|
+
def @record.title=(v)
|
96
|
+
raise ArgumentError
|
97
|
+
end
|
98
|
+
assert_not_nil @record.data
|
99
|
+
end
|
100
|
+
|
101
|
+
test "ignores data with extra keys" do
|
102
|
+
@record.raw_data = self.class.format.encode(self.class.raw_hash.merge(:foo => :bar))
|
103
|
+
assert_not_nil @record.title # no undefined foo= error
|
104
|
+
assert_equal false, @record.save # extra before_save cancels the operation
|
105
|
+
assert_equal self.class.raw_hash.merge(:active => 1).stringify_keys.keys.sort, self.class.format.decode(@record.raw_data).keys.sort
|
106
|
+
end
|
107
|
+
|
108
|
+
test "reads strings" do
|
109
|
+
assert_equal self.class.raw_hash[:title], @record.title
|
110
|
+
end
|
111
|
+
|
112
|
+
test "parses strings with unicode characters" do
|
113
|
+
@record.title = "Encöded ɐ \\u003c \\Upload \\upload" # test unicode char, \u**** code, and legit \U... string
|
114
|
+
assert_equal "Encöded ɐ < \\Upload \\upload", @record.title
|
115
|
+
end
|
116
|
+
|
117
|
+
test "clears strings with nil" do
|
118
|
+
assert @record.data.key?('title')
|
119
|
+
@record.title = nil
|
120
|
+
assert !@record.data.key?('title')
|
121
|
+
end
|
122
|
+
|
123
|
+
test "reads arrays" do
|
124
|
+
assert_equal self.class.raw_hash[:names], @record.names
|
125
|
+
end
|
126
|
+
|
127
|
+
test "reads arrays with custom type" do
|
128
|
+
assert_equal self.class.raw_hash[:lottery_picks], @record.lottery_picks
|
129
|
+
end
|
130
|
+
|
131
|
+
test "clears arrays with nil" do
|
132
|
+
assert @record.data.key?('names')
|
133
|
+
@record.names = nil
|
134
|
+
assert !@record.data.key?('names')
|
135
|
+
end
|
136
|
+
|
137
|
+
test "reads hashes" do
|
138
|
+
assert_equal self.class.raw_hash[:extras].stringify_keys, @record.extras
|
139
|
+
end
|
140
|
+
|
141
|
+
test "reads hashes with custom types" do
|
142
|
+
now = Time.utc(Time.now.year, 1, 1)
|
143
|
+
@record.raw_data = self.class.format.encode('extras' => {:num => "7", :foo => :bar, :started_at => now})
|
144
|
+
assert_equal({'num' => 7, 'started_at' => now, 'foo' => 'bar'}, @record.extras)
|
145
|
+
end
|
146
|
+
|
147
|
+
test "clears hashes with nil" do
|
148
|
+
assert @record.data.key?('extras')
|
149
|
+
@record.extras = nil
|
150
|
+
assert !@record.data.key?('extras')
|
151
|
+
end
|
152
|
+
|
153
|
+
test "reads integers" do
|
154
|
+
assert_equal self.class.raw_hash[:age], @record.age
|
155
|
+
end
|
156
|
+
|
157
|
+
test "parses integers from strings" do
|
158
|
+
@record.age = '5.5'
|
159
|
+
assert_equal 5, @record.age
|
160
|
+
end
|
161
|
+
|
162
|
+
test "clears integers with nil" do
|
163
|
+
assert @record.data.key?('age')
|
164
|
+
@record.age = nil
|
165
|
+
assert !@record.data.key?('age')
|
166
|
+
end
|
167
|
+
|
168
|
+
test "clears integers with blank" do
|
169
|
+
assert @record.data.key?('age')
|
170
|
+
@record.age = ''
|
171
|
+
assert !@record.data.key?('age')
|
172
|
+
end
|
173
|
+
|
174
|
+
test "reads floats" do
|
175
|
+
assert_equal self.class.raw_hash[:average], @record.average
|
176
|
+
end
|
177
|
+
|
178
|
+
test "parses floats from strings" do
|
179
|
+
@record.average = '5.5'
|
180
|
+
assert_equal 5.5, @record.average
|
181
|
+
end
|
182
|
+
|
183
|
+
test "clears floats with nil" do
|
184
|
+
assert @record.data.key?('average')
|
185
|
+
@record.average = nil
|
186
|
+
assert !@record.data.key?('average')
|
187
|
+
end
|
188
|
+
|
189
|
+
test "clears floats with blank" do
|
190
|
+
assert @record.data.key?('average')
|
191
|
+
@record.average = ''
|
192
|
+
assert !@record.data.key?('average')
|
193
|
+
end
|
194
|
+
|
195
|
+
test "reads times" do
|
196
|
+
assert_equal self.class.current_time, @record.birthday
|
197
|
+
end
|
198
|
+
|
199
|
+
test "parses times from strings" do
|
200
|
+
t = 5.years.ago.utc.midnight
|
201
|
+
@record.birthday = t.xmlschema
|
202
|
+
assert_equal t, @record.birthday
|
203
|
+
end
|
204
|
+
|
205
|
+
test "clears times with nil" do
|
206
|
+
assert @record.data.key?('birthday')
|
207
|
+
@record.birthday = nil
|
208
|
+
assert !@record.data.key?('birthday')
|
209
|
+
end
|
210
|
+
|
211
|
+
test "clears times with blank" do
|
212
|
+
assert @record.data.key?('birthday')
|
213
|
+
@record.birthday = ''
|
214
|
+
assert !@record.data.key?('birthday')
|
215
|
+
end
|
216
|
+
|
217
|
+
test "reads booleans" do
|
218
|
+
assert_equal true, @record.active
|
219
|
+
end
|
220
|
+
|
221
|
+
test "parses booleans from strings" do
|
222
|
+
@record.active = '1'
|
223
|
+
assert_equal true, @record.active
|
224
|
+
@record.active = '0'
|
225
|
+
assert_equal false, @record.active
|
226
|
+
end
|
227
|
+
|
228
|
+
test "parses booleans from integers" do
|
229
|
+
@record.active = 1
|
230
|
+
assert_equal true, @record.active
|
231
|
+
@record.active = 0
|
232
|
+
assert_equal false, @record.active
|
233
|
+
end
|
234
|
+
|
235
|
+
test "converts booleans to false with nil" do
|
236
|
+
assert @record.data.key?('active')
|
237
|
+
@record.active = nil
|
238
|
+
assert !@record.data.key?('active')
|
239
|
+
end
|
240
|
+
|
241
|
+
test "ignores empty strings for booleans" do
|
242
|
+
@newbie.clearance = ""
|
243
|
+
assert_nil @newbie.clearance
|
244
|
+
end
|
245
|
+
|
246
|
+
test "attempts to re-encode data when saving" do
|
247
|
+
assert_not_nil @record.title
|
248
|
+
@record.raw_data = nil
|
249
|
+
assert_equal false, @record.save # extra before_save cancels the operation
|
250
|
+
expected = self.class.raw_hash.merge \
|
251
|
+
:active => true,
|
252
|
+
:birthday => Time.parse(self.class.raw_hash[:birthday])
|
253
|
+
assert_equal expected.stringify_keys, @record.class.data_schema.decode(@record.raw_data)
|
254
|
+
end
|
255
|
+
|
256
|
+
test "knows untouched record is not changed" do
|
257
|
+
assert !@record.data_changed?
|
258
|
+
assert_equal [], @record.data_changed
|
259
|
+
end
|
260
|
+
|
261
|
+
test "knows updated record is changed" do
|
262
|
+
assert @changed.data_changed?
|
263
|
+
assert_equal %w(age title), @changed.data_changed.sort
|
264
|
+
end
|
265
|
+
|
266
|
+
test "tracks if field has changed" do
|
267
|
+
assert !@record.title_changed?
|
268
|
+
assert @changed.title_changed?
|
269
|
+
end
|
270
|
+
|
271
|
+
test "tracks field changes" do
|
272
|
+
assert_nil @record.title_change
|
273
|
+
assert_equal %w(abc def), @changed.title_change
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
Object.const_set("SerializedAttributeTest#{fmt.name.demodulize}", Class.new(ActiveSupport::TestCase)).class_eval do
|
278
|
+
class << self
|
279
|
+
attr_accessor :format
|
280
|
+
end
|
281
|
+
self.format = fmt
|
282
|
+
|
283
|
+
def setup
|
284
|
+
SerializedRecord.data_schema.formatter = self.class.format
|
285
|
+
@record = SerializedRecord.new
|
286
|
+
end
|
287
|
+
|
288
|
+
test "encodes and decodes data successfully" do
|
289
|
+
hash = {'a' => 1, 'b' => 2}
|
290
|
+
encoded = self.class.format.encode(hash)
|
291
|
+
assert_equal self.class.format.decode(encoded), hash
|
292
|
+
end
|
293
|
+
|
294
|
+
test "defines #data method on the model" do
|
295
|
+
assert @record.respond_to?(:data)
|
296
|
+
assert_equal @record.data, {'default_in_my_favor' => true}
|
297
|
+
end
|
298
|
+
|
299
|
+
attributes = {:string => [:title, :body], :integer => [:age], :float => [:average], :time => [:birthday], :boolean => [:active], :array => [:names, :lottery_picks], :hash => [:extras]}
|
300
|
+
attributes.values.flatten.each do |attr|
|
301
|
+
test "defines ##{attr} method on the model" do
|
302
|
+
assert @record.respond_to?(attr)
|
303
|
+
assert_nil @record.send(attr)
|
304
|
+
end
|
305
|
+
|
306
|
+
next if attr == :active
|
307
|
+
test "defines ##{attr}_before_type_cast method on the model" do
|
308
|
+
assert @record.respond_to?("#{attr}_before_type_cast")
|
309
|
+
assert_equal "", @record.send("#{attr}_before_type_cast")
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
test "defines #active_before_type_cast method on the model" do
|
314
|
+
assert @record.respond_to?(:active_before_type_cast)
|
315
|
+
assert_equal "", @record.active_before_type_cast
|
316
|
+
end
|
317
|
+
|
318
|
+
attributes[:string].each do |attr|
|
319
|
+
test "defines ##{attr}= method for string fields" do
|
320
|
+
assert @record.respond_to?("#{attr}=")
|
321
|
+
assert_equal 'abc', @record.send("#{attr}=", "abc")
|
322
|
+
assert_equal 'abc', @record.data[attr.to_s]
|
323
|
+
end
|
324
|
+
|
325
|
+
test "does not define ##{attr}? method for string fields" do
|
326
|
+
assert !@record.respond_to?("#{attr}?")
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
attributes[:integer].each do |attr|
|
331
|
+
test "defines ##{attr}= method for integer fields" do
|
332
|
+
assert @record.respond_to?("#{attr}=")
|
333
|
+
assert_equal 0, @record.send("#{attr}=", "abc")
|
334
|
+
assert_equal 1, @record.send("#{attr}=", "1.2")
|
335
|
+
assert_equal 1, @record.data[attr.to_s]
|
336
|
+
end
|
337
|
+
|
338
|
+
test "does not define ##{attr}? method for integer fields" do
|
339
|
+
assert !@record.respond_to?("#{attr}?")
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
attributes[:float].each do |attr|
|
344
|
+
test "defines ##{attr}= method for float fields" do
|
345
|
+
assert @record.respond_to?("#{attr}=")
|
346
|
+
assert_equal 0.0, @record.send("#{attr}=", "abc")
|
347
|
+
assert_equal 1.2, @record.send("#{attr}=", "1.2")
|
348
|
+
assert_equal 1.2, @record.data[attr.to_s]
|
349
|
+
end
|
350
|
+
|
351
|
+
test "does not define ##{attr}? method for float fields" do
|
352
|
+
assert !@record.respond_to?("#{attr}?")
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
attributes[:time].each do |attr|
|
357
|
+
test "defines ##{attr}= method for time fields" do
|
358
|
+
assert @record.respond_to?("#{attr}=")
|
359
|
+
t = Time.now.utc.midnight
|
360
|
+
assert_equal t, @record.send("#{attr}=", t.xmlschema)
|
361
|
+
assert_equal t, @record.data[attr.to_s]
|
362
|
+
end
|
363
|
+
|
364
|
+
test "does not define ##{attr}? method for boolean fields" do
|
365
|
+
assert !@record.respond_to?("#{attr}?")
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
attributes[:boolean].each do |attr|
|
370
|
+
test "defines ##{attr}= method for boolean fields" do
|
371
|
+
assert @record.respond_to?("#{attr}=")
|
372
|
+
assert_equal false, @record.send("#{attr}=", 0)
|
373
|
+
assert_equal true, @record.send("#{attr}=", "1.2")
|
374
|
+
assert_equal true, @record.data[attr.to_s]
|
375
|
+
end
|
376
|
+
|
377
|
+
test "defines ##{attr}? method for float fields" do
|
378
|
+
assert @record.respond_to?("#{attr}?")
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|