json_record 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT_LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Brian Durand
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,88 @@
1
+ = JSON Record
2
+
3
+ The purpose of this code is to add the ability to represent complex documents in ActiveRecord by using JSON to serialize the documents to a database field. This can be especially useful if you need a flexible schema, but don't want or have the means to utilize one of the new schemaless data stores that all the cool kids are talking about.
4
+
5
+ After all, relational databases are pretty rock solid and widely available technology. If you can't get the cool new thing installed, or if you just feel safe sticking with what you know, this gem may work for you. As an added advantage, it is just an extension on top of ActiveRecord, so you can still use all the features of ActiveRecord and add the schemaless functionality only to models where it makes sense.
6
+
7
+ == Serialized Fields
8
+
9
+ To define a complex document field, simply add this code to your ActiveRecord model definition:
10
+
11
+ serialize_to_json(:json_data) do |schema|
12
+ schema.key :name
13
+ schema.key :value, Integer
14
+ end
15
+
16
+ This will define for you accessors on your model for name and value and serialize those value in a database columns named json_data. These attributes will work just like other ActiveRecord attributes, so you will be able to track changes to them, include them in mass assignments, etc. Of course, that's not all that interesting since you could easily enough have added columns for name and value.
17
+
18
+ == Embedded Documents
19
+
20
+ To make you flexible schema really powerful, add some embedded documents to it. Embedded documents are Ruby classes that inherit from JsonRecord::EmbeddedDocument. They work very much like traditional ActiveRecord objects, except that instead of being serialized in a separate table, they are embedded right in the JSON field of their parent record. They can be used to replace has_many and has_one associations and can be far easier to work with.
21
+
22
+ Embedded documents have their own schema that is serialized to JSON. This schema can also contain embedded documents allowing you to easily create very rich data structures all with only one database table. And because there is only one table, you don't need to worry at all about ensuring your changes to embedded documents are saved along with the parent record.
23
+
24
+ == Example
25
+
26
+ class Post < ActiveRecord::Base
27
+ serialize_to_json(:json_data) do |schema|
28
+ schema.key :title, :required => true
29
+ schema.key :body, :required => true
30
+ schema.key :author, Person, :required => true
31
+ schema.many :comments, Comment
32
+ end
33
+ end
34
+
35
+ class Person < JsonRecord::EmbeddedDocument
36
+ schema.key :first_name, :required => true
37
+ schema.key :last_name
38
+ end
39
+
40
+ class Comment < JsonRecord::EmbeddedDocument
41
+ schema.key :author, Person, :required => true
42
+ schema.key :body, :required => true
43
+ schema.many :replies, Comment
44
+ end
45
+
46
+ Create a new post with a title and author:
47
+
48
+ post = Post.create!(:title => "What I think",
49
+ :body => "Stuff is good",
50
+ :author => {:first_name => "John", :last_name => "Doe"})
51
+
52
+ Change the authors first name:
53
+
54
+ post.author.first_name = "Bill"
55
+
56
+ Add a couple of comments:
57
+
58
+ post.comments.build(:author => {:first_name => "Tony"}, :body => "I like it")
59
+ post.comments.build(:author => {:first_name => "Jack"}, :body => "I don't like it")
60
+
61
+ Add a reply:
62
+
63
+ post.comments.first.replies.build(:author => {:first_name => "Ralph"}, :body => "You're and idiot")
64
+
65
+ And save it all:
66
+
67
+ post.save
68
+
69
+ Unlike with traditional association, you don't need any after_save callbacks to ensure that the associations are saved. If we want to remove the last comment, all we need to do is:
70
+
71
+ post.comments.pop
72
+ post.save
73
+
74
+ == Limitations
75
+
76
+ One thing you cannot do is index the fields in the serialized JSON. If you need to be able to search on those fields, you'll need a separate search engine (i.e. Solr or Sphinx). Or, you could just move it out of the JSON fields and make it a regular database column with an index on it. The interface will be exactly the same.
77
+
78
+ In order to conserve space and increase performance, blank values are not serialized to JSON. One side effect is that you cannot have fields with the empty string as the value. Also, if you look at the JSON stored in the database, you won't be able to deduce the schema since blank fields will be missing entirely. If you have any fields that are used to store Arrays or Hashes will never be nil and will always be initialized with an empty Array or Hash.
79
+
80
+ == Details, details, details
81
+
82
+ For optimal performance when working with JSON, you should really have the +json+ gem installed. This gem does not have a direct dependency on +json+ since it will work just fine without it, but if it is missing, you'll get a warning about it in the ActiveRecord log.
83
+
84
+ For performance, attributes from a JSON serialized field are only loaded when they are accessed. When a record is saved, the JSON attributes are translated back to JSON and stored in the serialized field. If a field is encountered in the JSON that has not been declared in the schema, it will persist, but will not be accessible via an accessor.
85
+
86
+ The fields you are using to store JSON must be large enough to handle any document. You should provide a :length attribute in your migration to ensure that text fields are long enough. By default, MySQL, for instance, will create a TEXT field limited to 32K. What you really want is a MEDIUMTEXT or LONGTEXT field.
87
+
88
+ Since JSON can be kind of wordy and take up a lot more space than a traditional column based approach, you can also specify that the JSON should be compressed when it is stored in the database. To do this, simply create the JSON column as a binary column type instead of a text column. This is the recommended set up unless you need to browse through your database outside of your Ruby application.
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ begin
9
+ require 'spec/rake/spectask'
10
+ desc 'Test json_record.'
11
+ Spec::Rake::SpecTask.new(:test) do |t|
12
+ t.spec_files = FileList.new('spec/**/*_spec.rb')
13
+ end
14
+ rescue LoadError
15
+ tast :test do
16
+ STDERR.puts "You must have rspec >= 1.2.9 to run the tests"
17
+ end
18
+ end
19
+
20
+ desc 'Generate documentation for json_record.'
21
+ Rake::RDocTask.new(:rdoc) do |rdoc|
22
+ rdoc.rdoc_dir = 'rdoc'
23
+ rdoc.options << '--title' << 'JSON Record' << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
24
+ rdoc.rdoc_files.include('README.rdoc')
25
+ rdoc.rdoc_files.include('lib/**/*.rb')
26
+ end
27
+
28
+ begin
29
+ require 'jeweler'
30
+ Jeweler::Tasks.new do |gem|
31
+ gem.name = "json_record"
32
+ gem.summary = %Q{ActiveRecord support for mapping complex documents within a single RDBMS record via JSON serialization.}
33
+ gem.email = "brian@embellishedvisions.com"
34
+ gem.homepage = "http://github.com/bdurand/json_record"
35
+ gem.authors = ["Brian Durand"]
36
+
37
+ gem.add_dependency('activerecord', '>= 2.2.2', '< 3.0')
38
+ gem.add_development_dependency('rspec', '>= 1.2.9')
39
+ gem.add_development_dependency('jeweler')
40
+ end
41
+
42
+ Jeweler::GemcutterTasks.new
43
+ rescue LoadError
44
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'json_record'
@@ -0,0 +1,72 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{json_record}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brian Durand"]
12
+ s.date = %q{2010-02-07}
13
+ s.email = %q{brian@embellishedvisions.com}
14
+ s.extra_rdoc_files = [
15
+ "README.rdoc"
16
+ ]
17
+ s.files = [
18
+ "MIT_LICENSE",
19
+ "README.rdoc",
20
+ "Rakefile",
21
+ "VERSION",
22
+ "init.rb",
23
+ "json_record.gemspec",
24
+ "lib/json_record.rb",
25
+ "lib/json_record/attribute_methods.rb",
26
+ "lib/json_record/embedded_document.rb",
27
+ "lib/json_record/embedded_document_array.rb",
28
+ "lib/json_record/field_definition.rb",
29
+ "lib/json_record/json_field.rb",
30
+ "lib/json_record/schema.rb",
31
+ "lib/json_record/serialized.rb",
32
+ "spec/embedded_document_array_spec.rb",
33
+ "spec/embedded_document_spec.rb",
34
+ "spec/field_definition_spec.rb",
35
+ "spec/serialized_spec.rb",
36
+ "spec/spec_helper.rb",
37
+ "spec/test_models.rb"
38
+ ]
39
+ s.homepage = %q{http://github.com/bdurand/json_record}
40
+ s.rdoc_options = ["--charset=UTF-8"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = %q{1.3.5}
43
+ s.summary = %q{ActiveRecord support for mapping complex documents within a single RDBMS record via JSON serialization.}
44
+ s.test_files = [
45
+ "spec/embedded_document_array_spec.rb",
46
+ "spec/embedded_document_spec.rb",
47
+ "spec/field_definition_spec.rb",
48
+ "spec/serialized_spec.rb",
49
+ "spec/spec_helper.rb",
50
+ "spec/test_models.rb"
51
+ ]
52
+
53
+ if s.respond_to? :specification_version then
54
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
55
+ s.specification_version = 3
56
+
57
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
58
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.2.2", "< 3.0"])
59
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
60
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
61
+ else
62
+ s.add_dependency(%q<activerecord>, [">= 2.2.2", "< 3.0"])
63
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
64
+ s.add_dependency(%q<jeweler>, [">= 0"])
65
+ end
66
+ else
67
+ s.add_dependency(%q<activerecord>, [">= 2.2.2", "< 3.0"])
68
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
69
+ s.add_dependency(%q<jeweler>, [">= 0"])
70
+ end
71
+ end
72
+
@@ -0,0 +1,55 @@
1
+ module JsonRecord
2
+ # Internal methods for reading and writing fields serialized from JSON.
3
+ module AttributeMethods
4
+ # Read a field. The field param must be a FieldDefinition and the context should be the record
5
+ # which is being read from.
6
+ def read_attribute (field, context)
7
+ if field.multivalued?
8
+ arr = json_attributes[field.name]
9
+ unless arr
10
+ arr = EmbeddedDocumentArray.new(field.type, context)
11
+ json_attributes[field.name] = arr
12
+ end
13
+ return arr
14
+ else
15
+ val = json_attributes[field.name]
16
+ if val.nil? and !field.default.nil?
17
+ val = field.default.dup rescue field.default
18
+ json_attributes[field.name] = val
19
+ end
20
+ return val
21
+ end
22
+ end
23
+
24
+ # Write a field. The field param must be a FieldDefinition and the context should be the record
25
+ # which is being read from. Track_changes indicates if changes to the object should be tracked.
26
+ def write_attribute (field, val, track_changes, context)
27
+ if field.multivalued?
28
+ val = val.values if val.is_a?(Hash)
29
+ json_attributes[field.name] = EmbeddedDocumentArray.new(field.type, context, val)
30
+ else
31
+ old_value = read_attribute(field, context)
32
+ converted_value = field.convert(val)
33
+ converted_value.parent = context if converted_value.is_a?(EmbeddedDocument)
34
+ unless old_value == converted_value
35
+ if track_changes
36
+ changes = changed_attributes
37
+ if changes.include?(field.name)
38
+ changes.delete(field.name) if converted_value == changes[field.name]
39
+ else
40
+ old_value = (old_value.clone rescue old_value) unless old_value.nil?
41
+ changes[field.name] = old_value
42
+ end
43
+ end
44
+ unless converted_value.nil?
45
+ json_attributes[field.name] = converted_value
46
+ else
47
+ json_attributes.delete(field.name)
48
+ end
49
+ end
50
+ context.attributes_before_type_cast[field.name] = val
51
+ end
52
+ return val
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,155 @@
1
+ module JsonRecord
2
+ # OK, this is ugly, but necessary to get ActiveRecord::Errors to be compatible with
3
+ # EmbeddedDocument. This will all be fixed with Rails 3 and ActiveModel. Until then
4
+ # we'll just live with this.
5
+ module ActiveRecordStub #:nodoc:
6
+ def self.included (base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def human_name (options = {})
12
+ name.split('::').last.humanize
13
+ end
14
+
15
+ def human_attribute_name (attribute, options = {})
16
+ attribute.to_s.humanize
17
+ end
18
+
19
+ def self_and_descendants_from_active_record
20
+ [self]
21
+ end
22
+
23
+ def self_and_descendents_from_active_record
24
+ [self]
25
+ end
26
+ end
27
+
28
+ def deprecated_callback_method (*args)
29
+ end
30
+
31
+ private
32
+ def save (*args); end;
33
+ def save! (*args); end;
34
+ def destroy (*args); end;
35
+ def create (*args); end;
36
+ def update (*args); end;
37
+ def new_record?; false; end;
38
+ end
39
+
40
+ # Subclasses of EmbeddedDocument can be used as the type for keys or many field definitions
41
+ # in Schema. Embedded documents are then extensions of the schema. In this way, complex
42
+ # documents represented in JSON can be deserialized as complex objects.
43
+ class EmbeddedDocument
44
+ include ActiveRecordStub
45
+ include ActiveRecord::Validations
46
+ include AttributeMethods
47
+
48
+ class_inheritable_reader :schema
49
+
50
+ class << self
51
+ # Define a field for the schema. This is a shortcut for calling schema.key.
52
+ # See Schema#key for details.
53
+ def key (name, *args)
54
+ write_inheritable_attribute(:schema, Schema.new(self, nil)) unless schema
55
+ schema.key(name, *args)
56
+ end
57
+
58
+ # Define a multivalued field for the schema. This is a shortcut for calling schema.many.
59
+ # See Schema#many for details.
60
+ def many (name, *args)
61
+ write_inheritable_attribute(:schema, Schema.new(self, nil)) unless schema
62
+ schema.many(name, *args)
63
+ end
64
+ end
65
+
66
+ # The parent object of the document.
67
+ attr_accessor :parent
68
+
69
+ # Create an embedded document with the specified attributes.
70
+ def initialize (attrs = {})
71
+ @attributes = {}
72
+ @json_attributes = {}
73
+ attrs.each_pair do |name, value|
74
+ field = schema.fields[name.to_s] || FieldDefinition.new(name, :type => value.class)
75
+ write_attribute(field, value, false, self)
76
+ end
77
+ end
78
+
79
+ # Get the attributes of the document.
80
+ def attributes
81
+ @json_attributes.reject{|k,v| !schema.fields.include?(k)}
82
+ end
83
+
84
+ # Get the attribute values of the document before they were type cast.
85
+ def attributes_before_type_cast
86
+ @attributes
87
+ end
88
+
89
+ # Determine if the document has been changed.
90
+ def changed?
91
+ !changed_attributes.empty?
92
+ end
93
+
94
+ # Get the list of attributes changed.
95
+ def changed
96
+ changed_attributes.keys
97
+ end
98
+
99
+ # Get a list of changes to the document.
100
+ def changes
101
+ changed.inject({}) {|h, attr| h[attr] = attribute_change(attr); h}
102
+ end
103
+
104
+ def to_json (*args)
105
+ @json_attributes.to_json(*args)
106
+ end
107
+
108
+ def eql? (val)
109
+ val.class == self.class && val.attributes == attributes && val.parent == parent
110
+ end
111
+
112
+ def == (val)
113
+ eql?(val)
114
+ end
115
+
116
+ def hash
117
+ attributes.hash + parent.hash
118
+ end
119
+
120
+ protected
121
+
122
+ def json_attributes
123
+ @json_attributes
124
+ end
125
+
126
+ def read_json_attribute (json_field_name, field)
127
+ read_attribute(field, self)
128
+ end
129
+
130
+ def write_json_attribute (json_field_name, field, value, track_changes)
131
+ write_attribute(field, value, track_changes, self)
132
+ end
133
+
134
+ def changed_attributes
135
+ @changed_attributes ||= {}
136
+ end
137
+
138
+ def read_attribute_before_type_cast (name)
139
+ @attributes[name.to_s]
140
+ end
141
+
142
+ def attribute_changed? (name)
143
+ changed_attributes.include?(name.to_s)
144
+ end
145
+
146
+ def attribute_change (name)
147
+ name = name.to_s
148
+ [changed_attributes[name], read_json_attribute(nil, schema.fields[name])] if attribute_changed?(name)
149
+ end
150
+
151
+ def attribute_was (name)
152
+ changed_attributes[name.to_s]
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,54 @@
1
+ module JsonRecord
2
+ # This is an array of EmbeddedDocument objects. All elments of the array must be of the same class
3
+ # and all belong to the same parent. If an array of hashes are passed in, they will all be converted
4
+ # to EmbeddedDocument objects of the class specified.
5
+ class EmbeddedDocumentArray < Array
6
+ def initialize (klass, parent, objects = [])
7
+ @klass = klass
8
+ @parent = parent
9
+ objects = [] unless objects
10
+ objects = [objects] unless objects.is_a?(Array)
11
+ objects = objects.collect do |obj|
12
+ obj = @klass.new(obj) if obj.is_a?(Hash)
13
+ if obj.is_a?(@klass)
14
+ obj.parent = parent
15
+ obj
16
+ else
17
+ raise ArgumentError.new("#{obj.inspect} is not a #{@klass}") unless obj.is_a?(@klass)
18
+ end
19
+ end
20
+ super(objects)
21
+ end
22
+
23
+ # Append an object to the array. The object must either be an EmbeddedDocument of the
24
+ # correct class, or a Hash.
25
+ def << (obj)
26
+ obj = @klass.new(obj) if obj.is_a?(Hash)
27
+ raise ArgumentError.new("#{obj.inspect} is not a #{@klass}") unless obj.is_a?(@klass)
28
+ obj.parent = @parent
29
+ super(obj)
30
+ end
31
+
32
+ # Concatenate an array of objects to the array. The objects must either be an EmbeddedDocument of the
33
+ # correct class, or a Hash.
34
+ def concat (objects)
35
+ objects = objects.collect do |obj|
36
+ obj = @klass.new(obj) if obj.is_a?(Hash)
37
+ raise ArgumentError.new("#{obj.inspect} is not a #{@klass}") unless obj.is_a?(@klass)
38
+ obj.parent = @parent
39
+ obj
40
+ end
41
+ super(objects)
42
+ end
43
+
44
+ # Similar add an EmbeddedDocument to the array and return the object. If the object passed
45
+ # in is a Hash, it will be used to make a new EmbeddedDocument.
46
+ def build (obj)
47
+ obj = @klass.new(obj) if obj.is_a?(Hash)
48
+ raise ArgumentError.new("#{obj.inspect} is not a #{@klass}") unless obj.is_a?(@klass)
49
+ obj.parent = @parent
50
+ self << obj
51
+ obj
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,87 @@
1
+ module JsonRecord
2
+ # A definition of a JSON field in a Schema.
3
+ class FieldDefinition
4
+ BOOLEAN_MAPPING = {
5
+ true => true, 'true' => true, 'TRUE' => true, 'True' => true, 't' => true, 'T' => true, '1' => true, 1 => true, 1.0 => true,
6
+ false => false, 'false' => false, 'FALSE' => false, 'False' => false, 'f' => false, 'F' => false, '0' => false, 0 => false, 0.0 => false, nil => false
7
+ }
8
+
9
+ attr_reader :name, :type
10
+
11
+ # Define a field. Options should include :type with the class of the field. Other options available are
12
+ # :multivalued and :default.
13
+ def initialize (name, options = {})
14
+ @name = name.to_s
15
+ @type = options[:type] || String
16
+ @multivalued = !!options[:multivalued]
17
+ @default = options[:default]
18
+ if [Hash, Array].include?(@type) and @default.nil?
19
+ @default = @type.new
20
+ end
21
+ end
22
+
23
+ # Get the default value.
24
+ def default
25
+ (@default.dup rescue @default )if @default
26
+ end
27
+
28
+ # Indicates the field is multivalued.
29
+ def multivalued?
30
+ @multivalued
31
+ end
32
+
33
+ # Convert a value to the proper class for storing it in the field. If the value can't be converted,
34
+ # the original value will be returned. Blank values are always translated to nil. Hashes will be converted
35
+ # to EmbeddedDocument objects if the field type extends from EmbeddedDocument.
36
+ def convert (val)
37
+ return nil if val.blank?
38
+ if @type == String
39
+ return val.to_s
40
+ elsif @type == Integer
41
+ return Kernel.Integer(val) rescue val
42
+ elsif @type == Float
43
+ return Kernel.Float(val) rescue val
44
+ elsif @type == Boolean
45
+ v = BOOLEAN_MAPPING[val]
46
+ v = val.to_s.downcase == 'true' if v.nil? # Check all mixed case spellings for true
47
+ return v
48
+ elsif @type == Date
49
+ if val.is_a?(Date)
50
+ return val
51
+ elsif val.is_a?(Time)
52
+ return val.to_date
53
+ else
54
+ return Date.parse(val.to_s) rescue val
55
+ end
56
+ elsif @type == Time
57
+ if val.is_a?(Time)
58
+ return Time.at((val.to_i / 60) * 60).utc
59
+ else
60
+ return Time.parse(val).utc rescue val
61
+ end
62
+ elsif @type == DateTime
63
+ if val.is_a?(DateTime)
64
+ return val.utc
65
+ else
66
+ return DateTime.parse(val).utc rescue val
67
+ end
68
+ elsif @type == Array
69
+ val = [val] unless val.is_a?(Array)
70
+ raise ArgumentError.new("#{name} must be an Array") unless val.is_a?(Array)
71
+ return val
72
+ elsif @type == Hash
73
+ raise ArgumentError.new("#{name} must be a Hash") unless val.is_a?(Hash)
74
+ return val
75
+ else
76
+ if val.is_a?(@type)
77
+ val
78
+ elsif val.is_a?(Hash) and (@type < EmbeddedDocument)
79
+ return @type.new(val)
80
+ else
81
+ raise ArgumentError.new("#{name} must be a #{@type}")
82
+ end
83
+ end
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,59 @@
1
+ require 'zlib'
2
+
3
+ module JsonRecord
4
+ class JsonField
5
+ include AttributeMethods
6
+
7
+ def initialize (record, name, schemas)
8
+ @record = record
9
+ @name = name
10
+ @schemas = schemas
11
+ @attributes = nil
12
+ @compressed = record.class.columns_hash[name].type == :binary
13
+ end
14
+
15
+ def serialize
16
+ if @attributes
17
+ stripped_attributes = {}
18
+ @attributes.each_pair{|k, v| stripped_attributes[k] = v unless v.blank?}
19
+ json = stripped_attributes.to_json
20
+ json = Zlib::Deflate.deflate(json) if json and @compressed
21
+ @record[@name] = json
22
+ end
23
+ end
24
+
25
+ def deserialize
26
+ @attributes = {}
27
+ @schemas.each do |schema|
28
+ schema.fields.values.each do |field|
29
+ @attributes[field.name] = field.multivalued? ? EmbeddedDocumentArray.new(field.type, self) : field.default
30
+ end
31
+ end
32
+
33
+ unless @record[@name].blank?
34
+ json = @record[@name]
35
+ json = Zlib::Inflate.inflate(json) if @compressed
36
+ ActiveSupport::JSON.decode(json).each_pair do |attr_name, attr_value|
37
+ field = nil
38
+ @schemas.each{|schema| field = schema.fields[attr_name]; break if field}
39
+ field = FieldDefinition.new(attr_name, :type => attr_value.class) unless field
40
+ write_attribute(field, attr_value, false, @record)
41
+ end
42
+ end
43
+ end
44
+
45
+ def json_attributes
46
+ deserialize unless @attributes
47
+ @attributes
48
+ end
49
+
50
+ def changes
51
+ @record.changes
52
+ end
53
+
54
+ def changed_attributes
55
+ @record.send(:changed_attributes)
56
+ end
57
+
58
+ end
59
+ end