json_record 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,128 @@
1
+ module JsonRecord
2
+ # Definition of a document schema. Defining a schema will define accessor methods for each field and potentially some
3
+ # validators.
4
+ class Schema
5
+ attr_reader :fields, :json_field_name
6
+
7
+ # Create a schema on the class for a particular field.
8
+ def initialize (klass, json_field_name)
9
+ @json_field_name = json_field_name
10
+ @klass = klass
11
+ @fields = {}
12
+ end
13
+
14
+ # Define a single valued field in the schema.
15
+ # The first argument should be the field name. This must be unique for the class accross all attributes.
16
+ #
17
+ # The optional second argument can be used to specify the class of the field values. It will default to
18
+ # String if not specified. Valid types are String, Integer, Float, Date, Time, DateTime, Boolean, Array, Hash,
19
+ # or any class that inherits from EmbeddedDocument.
20
+ #
21
+ # The last argument can be a hash with any of these keys:
22
+ # * :default -
23
+ # * :required -
24
+ # * :length -
25
+ # * :format -
26
+ # * :in -
27
+ def key (name, *args)
28
+ options = args.extract_options!
29
+ name = name.to_s
30
+ json_type = args.first || String
31
+
32
+ raise ArgumentError.new("too many arguments (must be 1 or 2 plus an options hash)") if args.length > 1
33
+
34
+ field = FieldDefinition.new(name, :type => json_type, :default => options[:default])
35
+ fields[name] = field
36
+ add_json_validations(field, options)
37
+ define_json_accessor(field, json_field_name)
38
+ end
39
+
40
+ def many (name, *args)
41
+ name = name.to_s
42
+ options = args.extract_options!
43
+ type = args.first || name.singularize.classify.constantize
44
+ field = FieldDefinition.new(name, :type => type, :multivalued => true)
45
+ fields[name] = field
46
+ add_json_validations(field, options)
47
+ define_many_json_accessor(field, json_field_name)
48
+ end
49
+
50
+ private
51
+
52
+ def add_json_validations (field, options)
53
+ @klass.validates_presence_of(field.name) if options[:required]
54
+ @klass.validates_format_of(field.name, :with => options[:format], :allow_blank => true) if options[:format]
55
+
56
+ if options[:length]
57
+ case options[:length]
58
+ when Fixnum
59
+ @klass.validates_length_of(field.name, :maximum => options[:length], :allow_blank => true)
60
+ when Range
61
+ @klass.validates_length_of(field.name, :in => options[:length], :allow_blank => true)
62
+ when Hash
63
+ @klass.validates_length_of(field.name, options[:length].merge(:allow_blank => true))
64
+ end
65
+ end
66
+
67
+ if options[:in]
68
+ @klass.validates_inclusion_of(field.name, :in => options[:in], :allow_blank => true)
69
+ end
70
+
71
+ if field.type == Integer
72
+ @klass.validates_numericality_of(field.name, :only_integer => true, :allow_blank => true)
73
+ elsif field.type == Float
74
+ @klass.validates_numericality_of(field.name, :allow_blank => true)
75
+ elsif [Date, Time, DateTime].include?(field.type)
76
+ @klass.validates_each(field.name) do |record, name, value|
77
+ unless value.is_a?(field.type) or value.blank?
78
+ record.errors.add(name, :invalid, :value => value)
79
+ end
80
+ end
81
+ end
82
+
83
+ if field.multivalued?
84
+ @klass.validates_each(field.name) do |record, name, value|
85
+ record.errors.add(name, :invalid) unless value.all?{|v| v.valid?}
86
+ end
87
+ elsif field.type < EmbeddedDocument
88
+ @klass.validates_each(field.name) do |record, name, value|
89
+ if value.is_a?(field.type)
90
+ record.errors.add(name, :invalid) unless value.valid?
91
+ end
92
+ end
93
+ end
94
+
95
+ if field.multivalued? and !options[:unique].blank?
96
+ @klass.validates_each(field.name) do |record, name, value|
97
+ used = {}
98
+ error_found = false
99
+ value.each do |v|
100
+ fval = v.attributes[options[:unique].to_s]
101
+ if used[fval]
102
+ v.errors.add(options[:unique].to_s, :taken, :value => fval)
103
+ error_found = true
104
+ else
105
+ used[fval] = true
106
+ end
107
+ end
108
+ record.errors.add(name, :invalid) if error_found
109
+ end
110
+ end
111
+ end
112
+
113
+ def define_json_accessor (field, json_field_name)
114
+ @klass.send(:define_method, field.name) {read_json_attribute(json_field_name, field)}
115
+ @klass.send(:define_method, "#{field.name}?") {!!read_json_attribute(json_field_name, field)} if field.type == Boolean
116
+ @klass.send(:define_method, "#{field.name}=") {|val| write_json_attribute(json_field_name, field, val, true)}
117
+ @klass.send(:define_method, "#{field.name}_changed?") {self.send(:attribute_changed?, field.name)}
118
+ @klass.send(:define_method, "#{field.name}_change") {self.send(:attribute_change, field.name)}
119
+ @klass.send(:define_method, "#{field.name}_was") {self.send(:attribute_was, field.name)}
120
+ @klass.send(:define_method, "#{field.name}_before_type_cast") {self.read_attribute_before_type_cast(field.name)}
121
+ end
122
+
123
+ def define_many_json_accessor (field, json_field_name)
124
+ @klass.send(:define_method, field.name) {self.read_json_attribute(json_field_name, field)}
125
+ @klass.send(:define_method, "#{field.name}=") {|val| self.write_json_attribute(json_field_name, field, val, true)}
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,106 @@
1
+ module JsonRecord
2
+ # Adds the serialized JSON behavior to ActiveRecord.
3
+ module Serialized
4
+ def self.included (base)
5
+ base.class_inheritable_accessor :json_serialized_fields
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ # Specify a field name that contains serialized JSON. The block will be yielded to with a
11
+ # Schema object that can then be used to define the fields in the JSON document. A class
12
+ # can have multiple fields that store JSON documents if necessary.
13
+ def serialize_to_json (field_name, &block)
14
+ field_name = field_name.to_s
15
+ self.json_serialized_fields ||= {}
16
+ include InstanceMethods unless include?(InstanceMethods)
17
+ schema = Schema.new(self, field_name)
18
+ field_schemas = json_serialized_fields[field_name]
19
+ if field_schemas
20
+ field_schemas = field_schemas.dup
21
+ else
22
+ field_schemas = []
23
+ end
24
+ json_serialized_fields[field_name] = field_schemas
25
+ field_schemas << schema
26
+ block.call(schema) if block
27
+ end
28
+
29
+ # Get the field definition of the JSON field from the schema it is defined in.
30
+ def json_field_definition (name)
31
+ field = nil
32
+ if json_serialized_fields
33
+ name = name.to_s
34
+ json_serialized_fields.values.flatten.each{|schema| field = schema.fields[name]; break if field}
35
+ end
36
+ return field
37
+ end
38
+ end
39
+
40
+ module InstanceMethods
41
+ def self.included (base)
42
+ base.before_save :serialize_json_attributes
43
+ base.alias_method_chain :reload, :serialized_json
44
+ base.alias_method_chain :attributes, :serialized_json
45
+ end
46
+
47
+ # Get the JsonField objects for the record.
48
+ def json_fields
49
+ unless @json_fields
50
+ @json_fields = {}
51
+ json_serialized_fields.each_pair do |name, schemas|
52
+ @json_fields[name] = JsonField.new(self, name, schemas)
53
+ end
54
+ end
55
+ @json_fields
56
+ end
57
+
58
+ def reload_with_serialized_json (*args)
59
+ @json_fields = nil
60
+ reload_without_serialized_json(*args)
61
+ end
62
+
63
+ def attributes_with_serialized_json
64
+ attrs = json_attributes.reject{|k,v| !json_field_names.include?(k)}
65
+ attrs.merge!(attributes_without_serialized_json)
66
+ json_serialized_fields.keys.each{|name| attrs.delete(name)}
67
+ return attrs
68
+ end
69
+
70
+ protected
71
+
72
+ # Returns a hash of all the JsonField objects merged together.
73
+ def json_attributes
74
+ attrs = {}
75
+ json_fields.values.each do |field|
76
+ attrs.merge!(field.json_attributes)
77
+ end
78
+ attrs
79
+ end
80
+
81
+ def json_field_names
82
+ @json_field_names = json_serialized_fields.values.flatten.collect{|s| s.fields.keys}.flatten
83
+ end
84
+
85
+ # Read a field value from a JsonField
86
+ def read_json_attribute (json_field_name, field)
87
+ json_fields[json_field_name].read_attribute(field, self)
88
+ end
89
+
90
+ # Write a field value to a JsonField
91
+ def write_json_attribute (json_field_name, field, value, track_changes)
92
+ json_fields[json_field_name].write_attribute(field, value, track_changes, self)
93
+ end
94
+
95
+ # Serialize the JSON in the record into JsonField objects.
96
+ def serialize_json_attributes
97
+ json_fields.values.each{|field| field.serialize} if @json_fields
98
+ end
99
+
100
+ # Write out the JSON representation of the JsonField objects to the database fields.
101
+ def deserialize_json_attributes
102
+ json_fields.values.each{|field| field.deserialize} if @json_fields
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,20 @@
1
+ require 'active_record'
2
+
3
+ begin
4
+ require 'json'
5
+ rescue LoadError
6
+ ActiveRecord::Base.logger.warn("*** You really should install the json gem for optimal performance with json_record ***")
7
+ end
8
+
9
+ unless defined?(Boolean)
10
+ class Boolean
11
+ end
12
+ end
13
+
14
+ require File.expand_path(File.join(File.dirname(__FILE__), 'json_record', 'attribute_methods'))
15
+ require File.expand_path(File.join(File.dirname(__FILE__), 'json_record', 'embedded_document'))
16
+ require File.expand_path(File.join(File.dirname(__FILE__), 'json_record', 'embedded_document_array'))
17
+ require File.expand_path(File.join(File.dirname(__FILE__), 'json_record', 'field_definition'))
18
+ require File.expand_path(File.join(File.dirname(__FILE__), 'json_record', 'json_field'))
19
+ require File.expand_path(File.join(File.dirname(__FILE__), 'json_record', 'schema'))
20
+ require File.expand_path(File.join(File.dirname(__FILE__), 'json_record', 'serialized'))
@@ -0,0 +1,64 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
+
3
+ describe JsonRecord::EmbeddedDocumentArray do
4
+ it "should be an Array" do
5
+ a = JsonRecord::EmbeddedDocumentArray.new(JsonRecord::Test::Trait, nil)
6
+ a.is_a?(Array).should == true
7
+ end
8
+
9
+ it "should convert hashes to objects on initialize and set the parent" do
10
+ parent = JsonRecord::Test::Trait.new(:name => "name")
11
+ obj = JsonRecord::Test::Trait.new(:name => "n1", :value => "v1")
12
+ hash = {:name => "n2", :value => "v2"}
13
+ a = JsonRecord::EmbeddedDocumentArray.new(JsonRecord::Test::Trait, parent, [obj, hash])
14
+ obj.parent.should == parent
15
+ a.collect{|t| [t.name, t.value, t.parent]}.should == [["n1", "v1", parent], ["n2", "v2", parent]]
16
+ end
17
+
18
+ it "should convert hashes to objects on concat and set the parent" do
19
+ parent = JsonRecord::Test::Trait.new(:name => "name")
20
+ obj = JsonRecord::Test::Trait.new(:name => "n1", :value => "v1")
21
+ hash = {:name => "n2", :value => "v2"}
22
+ a = JsonRecord::EmbeddedDocumentArray.new(JsonRecord::Test::Trait, parent)
23
+ a.concat([obj, hash])
24
+ obj.parent.should == parent
25
+ a.collect{|t| [t.name, t.value, t.parent]}.should == [["n1", "v1", parent], ["n2", "v2", parent]]
26
+ end
27
+
28
+ it "should convert hashes to objects on append and set the parent" do
29
+ parent = JsonRecord::Test::Trait.new(:name => "name")
30
+ obj = JsonRecord::Test::Trait.new(:name => "n1", :value => "v1")
31
+ hash = {:name => "n2", :value => "v2"}
32
+ a = JsonRecord::EmbeddedDocumentArray.new(JsonRecord::Test::Trait, parent)
33
+ a << obj
34
+ a << hash
35
+ obj.parent.should == parent
36
+ a.collect{|t| [t.name, t.value, t.parent]}.should == [["n1", "v1", parent], ["n2", "v2", parent]]
37
+ end
38
+
39
+ it "should have a build method that appends the values and set the parent" do
40
+ parent = JsonRecord::Test::Trait.new(:name => "name")
41
+ obj = JsonRecord::Test::Trait.new(:name => "n1", :value => "v1")
42
+ hash = {:name => "n2", :value => "v2"}
43
+ a = JsonRecord::EmbeddedDocumentArray.new(JsonRecord::Test::Trait, parent)
44
+ a.build(obj).should == obj
45
+ obj.parent.should == parent
46
+ obj_2 = a.build(hash)
47
+ obj_2.name.should == "n2"
48
+ obj_2.value.should == "v2"
49
+ obj_2.parent.should == parent
50
+ a.collect{|t| [t.name, t.value, t.parent]}.should == [["n1", "v1", parent], ["n2", "v2", parent]]
51
+ end
52
+
53
+ it "should not accept objects that are not the correct class" do
54
+ parent = JsonRecord::Test::Trait.new(:name => "name")
55
+ obj = JsonRecord::Test::Trait.new(:name => "n1", :value => "v1")
56
+ hash = {:name => "n2", :value => "v2"}
57
+ lambda{a = JsonRecord::EmbeddedDocumentArray.new(JsonRecord::Test::Trait, parent, "bad object")}.should raise_error(ArgumentError)
58
+ a = JsonRecord::EmbeddedDocumentArray.new(JsonRecord::Test::Trait, parent)
59
+ lambda{a.concat(["bad object"])}.should raise_error(ArgumentError)
60
+ lambda{a << "bad object"}.should raise_error(ArgumentError)
61
+ lambda{a.build("bad object")}.should raise_error(ArgumentError)
62
+ end
63
+
64
+ end
@@ -0,0 +1,65 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
+
3
+ describe JsonRecord::EmbeddedDocument do
4
+
5
+ it "should have a schema" do
6
+ JsonRecord::Test::Trait.schema.should_not == nil
7
+ end
8
+
9
+ it "should have a parent object" do
10
+ trait = JsonRecord::Test::Trait.new(:name => "n1", :value => "v1")
11
+ trait.parent.should == nil
12
+ trait.parent = :parent
13
+ trait.parent.should == :parent
14
+ end
15
+
16
+ it "should have attributes" do
17
+ trait = JsonRecord::Test::Trait.new(:name => "n1", :value => "v1")
18
+ trait.name.should == "n1"
19
+ trait.value.should == "v1"
20
+ trait.attributes.should == {"name" => "n1", "value" => "v1"}
21
+ trait.name = "n2"
22
+ trait.value = "v2"
23
+ trait.name.should == "n2"
24
+ trait.value.should == "v2"
25
+ trait.attributes.should == {"name" => "n2", "value" => "v2"}
26
+ end
27
+
28
+ it "should have attributes before type cast" do
29
+ trait = JsonRecord::Test::Trait.new
30
+ trait.name = 1
31
+ trait.count = "12"
32
+ trait.name_before_type_cast.should == 1
33
+ trait.count_before_type_cast.should == "12"
34
+ trait.name.should == "1"
35
+ trait.count.should == 12
36
+ end
37
+
38
+ it "should track changes to attributes" do
39
+ trait = JsonRecord::Test::Trait.new
40
+ trait.name = "test"
41
+ trait.name_was.should == nil
42
+ trait.name_changed?.should == true
43
+ trait.name_change.should == [nil, "test"]
44
+ trait.changes.should == {"name" => [nil, "test"]}
45
+ trait.name = nil
46
+ trait.name_changed?.should == false
47
+ trait.name_was.should == nil
48
+ trait.name_change.should == nil
49
+ trait.changes.should == {}
50
+ end
51
+
52
+ it "should convert to attributes to json" do
53
+ trait = JsonRecord::Test::Trait.new(:name => "n1", :value => "v1")
54
+ ActiveSupport::JSON.decode(trait.to_json).should == {"name" => "n1", "value" => "v1"}
55
+ end
56
+
57
+ it "should consider it equal to another EmbeddedDocument with the same attributes and parent" do
58
+ trait_1 = JsonRecord::Test::Trait.new(:name => "n1", :value => "v1")
59
+ trait_2 = JsonRecord::Test::Trait.new(:name => "n1", :value => "v1")
60
+ (trait_1 == trait_2).should == true
61
+ trait_1.eql?(trait_2).should == true
62
+ (trait_1.hash == trait_2.hash).should == true
63
+ end
64
+
65
+ end
@@ -0,0 +1,29 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
+
3
+ describe JsonRecord::FieldDefinition do
4
+
5
+ it "should have a name that is a string" do
6
+ field = JsonRecord::FieldDefinition.new("name", :type => Integer)
7
+ field.name.should == "name"
8
+ end
9
+
10
+ it "should have a type" do
11
+ field = JsonRecord::FieldDefinition.new("name", :type => Integer)
12
+ field.type.should == Integer
13
+ end
14
+
15
+ it "should have a default" do
16
+ field = JsonRecord::FieldDefinition.new("name", :type => Integer)
17
+ field.default.should == nil
18
+ field = JsonRecord::FieldDefinition.new("name", :type => Integer, :default => 10)
19
+ field.default.should == 10
20
+ end
21
+
22
+ it "can be multivalued" do
23
+ field = JsonRecord::FieldDefinition.new("name", :type => Integer)
24
+ field.multivalued?.should == false
25
+ field = JsonRecord::FieldDefinition.new("name", :type => Integer, :multivalued => true)
26
+ field.multivalued?.should == true
27
+ end
28
+
29
+ end