json_record 1.0.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.
@@ -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