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.
- 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
|