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 +20 -0
- data/README.rdoc +88 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/json_record.gemspec +72 -0
- data/lib/json_record/attribute_methods.rb +55 -0
- data/lib/json_record/embedded_document.rb +155 -0
- data/lib/json_record/embedded_document_array.rb +54 -0
- data/lib/json_record/field_definition.rb +87 -0
- data/lib/json_record/json_field.rb +59 -0
- data/lib/json_record/schema.rb +128 -0
- data/lib/json_record/serialized.rb +106 -0
- data/lib/json_record.rb +20 -0
- data/spec/embedded_document_array_spec.rb +64 -0
- data/spec/embedded_document_spec.rb +65 -0
- data/spec/field_definition_spec.rb +29 -0
- data/spec/serialized_spec.rb +430 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/test_models.rb +70 -0
- metadata +111 -0
@@ -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
|
data/lib/json_record.rb
ADDED
@@ -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
|