correlate 0.1.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/.document +5 -0
- data/.gitignore +23 -0
- data/LICENSE +20 -0
- data/README.rdoc +137 -0
- data/Rakefile +48 -0
- data/correlate.gemspec +101 -0
- data/lib/correlate.rb +97 -0
- data/lib/correlate/correlation.rb +116 -0
- data/lib/correlate/links.rb +89 -0
- data/lib/correlate/relationships.rb +36 -0
- data/lib/correlate/relationships/active_record.rb +146 -0
- data/lib/correlate/relationships/active_record/collection_proxy.rb +32 -0
- data/lib/correlate/relationships/couchrest.rb +171 -0
- data/lib/correlate/validator.rb +49 -0
- data/spec/active_record_spec.rb +30 -0
- data/spec/activerecord_helper.rb +17 -0
- data/spec/correlate_spec.rb +132 -0
- data/spec/fixtures/article.rb +8 -0
- data/spec/fixtures/blank_doc.rb +4 -0
- data/spec/fixtures/comment.rb +11 -0
- data/spec/fixtures/course.rb +5 -0
- data/spec/fixtures/crawler.rb +6 -0
- data/spec/fixtures/news_feed.rb +13 -0
- data/spec/fixtures/note.rb +2 -0
- data/spec/fixtures/person.rb +11 -0
- data/spec/fixtures/project.rb +2 -0
- data/spec/fixtures/reader.rb +11 -0
- data/spec/fixtures/student.rb +11 -0
- data/spec/links_spec.rb +27 -0
- data/spec/relationships_spec.rb +18 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/validation_spec.rb +38 -0
- metadata +144 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
module Correlate
|
2
|
+
# Thin proxy around the array of links.
|
3
|
+
class Links
|
4
|
+
|
5
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ }
|
6
|
+
|
7
|
+
def initialize( klass, array = [] )
|
8
|
+
@klass = klass
|
9
|
+
@target_array = array
|
10
|
+
end
|
11
|
+
|
12
|
+
# Extract all the matching links for rel.
|
13
|
+
#
|
14
|
+
# @param [String] rel
|
15
|
+
# @return [ Correlate::Links ] for matching links
|
16
|
+
def rel( rel )
|
17
|
+
clone( @target_array.select { |link| link['rel'] == rel } )
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add an object to the list
|
21
|
+
def <<( obj )
|
22
|
+
write_target.push({ 'rel' => rel_for_object( obj ), 'href' => id_for_object( obj ) })
|
23
|
+
end
|
24
|
+
|
25
|
+
alias :push :<<
|
26
|
+
alias :concat :<<
|
27
|
+
|
28
|
+
# Replace a matching +rel+ with this object
|
29
|
+
def replace( obj )
|
30
|
+
delete( obj )
|
31
|
+
|
32
|
+
self.<< obj
|
33
|
+
end
|
34
|
+
|
35
|
+
# Delete this object from the list
|
36
|
+
def delete( obj )
|
37
|
+
rel = rel_for_object( obj )
|
38
|
+
|
39
|
+
write_target.reject! { |l| l['rel'] == rel }
|
40
|
+
end
|
41
|
+
|
42
|
+
def __debug__
|
43
|
+
{
|
44
|
+
:object_id => object_id,
|
45
|
+
:klass => @klass,
|
46
|
+
:target_array => @target_array,
|
47
|
+
:original_copy => @original_copy,
|
48
|
+
:original_copy_object_id => @original_copy.object_id
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def clone( array )
|
55
|
+
Links.new( @klass, array ).subset_of( @original_copy || self )
|
56
|
+
end
|
57
|
+
|
58
|
+
def subset_of( links_instance )
|
59
|
+
@original_copy = links_instance
|
60
|
+
end
|
61
|
+
|
62
|
+
def rel_for_object( obj )
|
63
|
+
c = @klass.correlation_for( obj )
|
64
|
+
|
65
|
+
if c.nil? && obj.instance_of?( Hash )
|
66
|
+
obj['rel']
|
67
|
+
else
|
68
|
+
c.rel
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def id_for_object( obj )
|
73
|
+
c = @klass.correlation_for( obj )
|
74
|
+
if c.nil? && obj.instance_of?( Hash )
|
75
|
+
obj['href']
|
76
|
+
else
|
77
|
+
obj.send( c.id_method )
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def write_target
|
82
|
+
@original_copy || @target_array
|
83
|
+
end
|
84
|
+
|
85
|
+
def method_missing( name, *args, &block )
|
86
|
+
@target_array.send( name, *args, &block )
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Correlate
|
2
|
+
module Relationships
|
3
|
+
|
4
|
+
autoload :CouchRest, 'correlate/relationships/couchrest'
|
5
|
+
autoload :ActiveRecord, 'correlate/relationships/active_record'
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def configure!( klass, &block )
|
10
|
+
if klass.ancestors.include?( ::CouchRest::ExtendedDocument )
|
11
|
+
Correlate::Relationships::CouchRest.configure! klass, &block
|
12
|
+
else
|
13
|
+
if defined?( ::ActiveRecord ) && klass.ancestors.include?( ::ActiveRecord::Base )
|
14
|
+
Correlate::Relationships::ActiveRecord.configure! klass, &block
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_correlation( name, type, opts )
|
20
|
+
correlation = Correlation.new
|
21
|
+
correlation.name = name
|
22
|
+
correlation.type = type
|
23
|
+
correlation.target = opts[:class]
|
24
|
+
correlation.source = opts[:source]
|
25
|
+
correlation.rel = opts[:rel]
|
26
|
+
correlation.id_method = opts[:id_method]
|
27
|
+
correlation.requires = opts[:requires]
|
28
|
+
correlation.required = opts[:required]
|
29
|
+
|
30
|
+
correlation
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Correlate
|
2
|
+
module Relationships
|
3
|
+
|
4
|
+
# Used to define relationships between ActiveRecord::Base models and
|
5
|
+
# CouchRest::ExtendedDocument classes.
|
6
|
+
#
|
7
|
+
# == Important note
|
8
|
+
#
|
9
|
+
# Unlike normal correlations, when correlating an ActiveRecord model with
|
10
|
+
# a CouchRest document, the 'reverse correlation' needs to be specified in
|
11
|
+
# in the CouchRest model.
|
12
|
+
#
|
13
|
+
# @see #a
|
14
|
+
# @see #some
|
15
|
+
class ActiveRecord
|
16
|
+
|
17
|
+
autoload :CollectionProxy, 'correlate/relationships/active_record/collection_proxy'
|
18
|
+
|
19
|
+
class << self
|
20
|
+
|
21
|
+
def configure!( klass, &block )
|
22
|
+
|
23
|
+
# Setup our conveniences
|
24
|
+
relationships = new( klass )
|
25
|
+
relationships.instance_eval( &block )
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize( klass )
|
31
|
+
@klass = klass
|
32
|
+
end
|
33
|
+
|
34
|
+
# Specify that this model will have multiple documents of said relationship.
|
35
|
+
# This is akin to ActiveRecord's +has_many associations+
|
36
|
+
#
|
37
|
+
# @example Defining correlations and using them
|
38
|
+
# class MyModel < ActiveRecord::Base
|
39
|
+
# include Correlate
|
40
|
+
#
|
41
|
+
# related_to do
|
42
|
+
# some :other_documents, :class => 'OtherDocument', :rel => 'my_model'
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# class OtherDocument < CouchRest::ExtendedDocument
|
47
|
+
# include Correlate
|
48
|
+
#
|
49
|
+
# related_to do
|
50
|
+
# a :my_model, :class => 'MyModel'
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# doc = OtherDocument.new
|
55
|
+
# doc.my_model # => nil
|
56
|
+
#
|
57
|
+
# me = MyModel.find( 1 )
|
58
|
+
# doc.my_model = me
|
59
|
+
#
|
60
|
+
# doc.me_model # => <MyModel#12121212>
|
61
|
+
#
|
62
|
+
# me.other_documents # => [ <OtherDocument#121212121> ]
|
63
|
+
#
|
64
|
+
# @param [Symbol] name of the relationship
|
65
|
+
# @param [Hash] options for the relationship
|
66
|
+
# @option options [String] :class the class of the related document/model
|
67
|
+
# @option options [String] :rel the value of the +rel+ key in the related hash
|
68
|
+
# @option options [Symbol] :id_method (:id) name of a method use to retrieve the 'foreign key' value from documents added to the relationship
|
69
|
+
# @option options [Symbol] :load_via (:get) name of the class method used to retreive related documents
|
70
|
+
def some( *args )
|
71
|
+
name = args.shift
|
72
|
+
opts = args.empty? ? {} : args.last
|
73
|
+
opts[:source] = @klass
|
74
|
+
|
75
|
+
correlation = Correlate::Relationships.build_correlation( name, :some, opts )
|
76
|
+
@klass.correlations << correlation
|
77
|
+
|
78
|
+
@klass.class_eval <<-EOF, __FILE__, __LINE__
|
79
|
+
def #{name}( raw = false )
|
80
|
+
local_correlation = self.class.correlations.detect { |c| c.name == :#{name} }
|
81
|
+
|
82
|
+
Correlate::Relationships::ActiveRecord::CollectionProxy.new self, local_correlation
|
83
|
+
end
|
84
|
+
EOF
|
85
|
+
end
|
86
|
+
|
87
|
+
# Specify that this model will have a single document of said relationship.
|
88
|
+
# This is akin to ActiveRecord's +belongs_to or has_one associations+.
|
89
|
+
#
|
90
|
+
# @example Defining correlations and using them
|
91
|
+
# class MyModel < ActiveRecord::Base
|
92
|
+
# include Correlate
|
93
|
+
#
|
94
|
+
# related_to do
|
95
|
+
# a :journal, :class => 'Journal', :rel => 'my_model'
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# class Journal < CouchRest::ExtendedDocument
|
100
|
+
# include Correlate
|
101
|
+
#
|
102
|
+
# related_to do
|
103
|
+
# some :my_models, :class => 'MyModel', :rel => 'my_model'
|
104
|
+
# end
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# doc = Journal.new
|
108
|
+
# doc.my_models # => []
|
109
|
+
#
|
110
|
+
# me = MyModel.find( 1 )
|
111
|
+
# doc.my_models << me
|
112
|
+
#
|
113
|
+
# doc.my_models # => [ <MyModel#12121212> ]
|
114
|
+
#
|
115
|
+
# me.journal # => <Journal#121212121>
|
116
|
+
#
|
117
|
+
# @param [Symbol] name of the relationship
|
118
|
+
# @param [Hash] options for the relationship
|
119
|
+
# @option options [String] :class the class of the related document/model
|
120
|
+
# @option options [String] :rel (name) the value of the +rel+ key in the related hash
|
121
|
+
# @option options [Symbol] :id_method (:id) name of a method use to retrieve the 'foreign key' value from documents added to the relationship
|
122
|
+
# @option options [Symbol] :load_via (:get) name of the class method used to retreive related documents
|
123
|
+
def a( name, options = {} )
|
124
|
+
options[:source] = @klass
|
125
|
+
|
126
|
+
@klass.correlations << Correlate::Relationships.build_correlation( name, :a, options )
|
127
|
+
|
128
|
+
@klass.class_eval <<-EOF, __FILE__, __LINE__
|
129
|
+
def #{name}=( object )
|
130
|
+
self.links.replace( object )
|
131
|
+
end
|
132
|
+
|
133
|
+
def #{name}( raw = false )
|
134
|
+
correlation = self.links.rel( '#{name}' ).first
|
135
|
+
return if correlation.nil?
|
136
|
+
|
137
|
+
correlation = self.class.correlation_for( correlation ).correlate( correlation ) unless raw
|
138
|
+
|
139
|
+
correlation
|
140
|
+
end
|
141
|
+
EOF
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Correlate
|
2
|
+
module Relationships
|
3
|
+
class ActiveRecord
|
4
|
+
class CollectionProxy
|
5
|
+
|
6
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ }
|
7
|
+
|
8
|
+
def initialize( source, correlation )
|
9
|
+
@source = source
|
10
|
+
@correlation = correlation
|
11
|
+
@collection = correlation.correlate( source )
|
12
|
+
end
|
13
|
+
|
14
|
+
def <<( object )
|
15
|
+
correlation = @correlation.opposite
|
16
|
+
|
17
|
+
object.send( "#{correlation.name}=", @source )
|
18
|
+
object.save
|
19
|
+
end
|
20
|
+
|
21
|
+
alias :push :<<
|
22
|
+
alias :concat :<<
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def method_missing( name, *args, &block )
|
27
|
+
@collection.send( name, *args, &block )
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module Correlate
|
2
|
+
module Relationships
|
3
|
+
|
4
|
+
# Configure relationships between CouchRest::ExtendedDocument classes, as
|
5
|
+
# well as between CouchRest::ExtendedDocument & ActiveRecord::Base classes.
|
6
|
+
#
|
7
|
+
# Using this correlation in a CouchRest::ExtendedDocument creates a +links+
|
8
|
+
# property that is used for storing the correlations between this document
|
9
|
+
# and others.
|
10
|
+
#
|
11
|
+
# It also creates a +rel+ view for the class that emits two keys: +rel+ &
|
12
|
+
# +href+, which is used for bulk lookups and loading of documents.
|
13
|
+
#
|
14
|
+
# == Notes
|
15
|
+
#
|
16
|
+
# To use the validations provided by Correlate you need to include
|
17
|
+
# +CouchRest::Validation+ into your classes.
|
18
|
+
#
|
19
|
+
# @see #a
|
20
|
+
# @see #some
|
21
|
+
class CouchRest
|
22
|
+
|
23
|
+
class << self
|
24
|
+
|
25
|
+
def configure!( klass, &block )
|
26
|
+
|
27
|
+
# Add our property
|
28
|
+
klass.property :links, :type => 'Correlate::Links', :default => Correlate::Links.new( klass )
|
29
|
+
|
30
|
+
# Setup our conveniences
|
31
|
+
relationships = new( klass )
|
32
|
+
relationships.instance_eval( &block )
|
33
|
+
relationships.build_validators
|
34
|
+
relationships.build_views
|
35
|
+
|
36
|
+
# Make sure our links array is properly casted
|
37
|
+
klass.class_eval <<-EOF, __FILE__, __LINE__
|
38
|
+
def links=( array )
|
39
|
+
self[:links] = Correlate::Links.new( self.class, array )
|
40
|
+
end
|
41
|
+
EOF
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize( klass )
|
47
|
+
@klass = klass
|
48
|
+
end
|
49
|
+
|
50
|
+
# Specify that this document will have multiple documents of said relationship.
|
51
|
+
#
|
52
|
+
# @example Defining correlations and using them
|
53
|
+
# class SomeDocument < CouchRest::ExtendedDocument
|
54
|
+
# include Correlate
|
55
|
+
#
|
56
|
+
# related_to do
|
57
|
+
# some :other_documents, :class => 'OtherDocument, :rel => 'other_document'
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# doc = SomeDocument.new
|
62
|
+
# doc.other_documents # => []
|
63
|
+
#
|
64
|
+
# other_doc = OtherDocument.get( '885387e892e63f4b6d31dbc877533099' )
|
65
|
+
# doc.other_documents << other_doc
|
66
|
+
#
|
67
|
+
# doc.other_documents # => [ <OtherDocument#12121212> ]
|
68
|
+
#
|
69
|
+
# @param [Symbol] name of the relationship
|
70
|
+
# @param [Hash] options for the relationship
|
71
|
+
# @option options [String] :class the class of the related document/model
|
72
|
+
# @option options [String] :rel the value of the +rel+ key in the related hash
|
73
|
+
# @option options [Fixnum] :requires (nil) a number of documents required
|
74
|
+
# @option options [Symbol] :id_method (:id) name of a method use to retrieve the 'foreign key' value from documents added to the relationship
|
75
|
+
# @option options [Symbol] :load_via (:get/:find) name of the class method used to retreive related documents (defaults to :get for CouchRest::ExtendedDocument, :find for ActiveRecord::Base)
|
76
|
+
def some( name, options = {} )
|
77
|
+
options[:source] = @klass
|
78
|
+
|
79
|
+
@klass.correlations << Correlate::Relationships.build_correlation( name, :some, options )
|
80
|
+
|
81
|
+
@klass.class_eval <<-EOF, __FILE__, __LINE__
|
82
|
+
def #{name}( raw = false )
|
83
|
+
correlations = self.links.rel( '#{name}' )
|
84
|
+
|
85
|
+
#correlations.map! do |c|
|
86
|
+
# self.links.correlation_for_object( c ).correlate( c )
|
87
|
+
#end unless raw
|
88
|
+
unless raw
|
89
|
+
c = self.class.correlation_for( correlations.first )
|
90
|
+
correlations = c.bulk_correlate( correlations ) unless c.nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
correlations
|
94
|
+
end
|
95
|
+
EOF
|
96
|
+
end
|
97
|
+
|
98
|
+
# Specify that this document will have a single document of said relationship.
|
99
|
+
#
|
100
|
+
# @example Defining correlations and using them
|
101
|
+
# class OtherDocument < CouchRest::ExtendedDocument
|
102
|
+
# include Correlate
|
103
|
+
#
|
104
|
+
# related_to do
|
105
|
+
# a :some_document, :class => 'SomeDocument'
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# doc = OtherDocument.new
|
110
|
+
# doc.some_document # => nil
|
111
|
+
#
|
112
|
+
# some_doc = SomeDocument.get( '885387e892e63f4b6d31dbc877533099' )
|
113
|
+
# doc.some_document = some_doc
|
114
|
+
#
|
115
|
+
# doc.some_document # => [ <SomeDocument#12121212> ]
|
116
|
+
#
|
117
|
+
# @param [Symbol] name of the relationship
|
118
|
+
# @param [Hash] options for the relationship
|
119
|
+
# @option options [String] :class the class of the related document/model
|
120
|
+
# @option options [String] :rel (name) the value of the +rel+ key in the related hash
|
121
|
+
# @option options [Fixnum] :required (false) whether required or not
|
122
|
+
# @option options [Symbol] :id_method (:id) name of a method use to retrieve the 'foreign key' value from documents added to the relationship
|
123
|
+
# @option options [Symbol] :load_via (:get/:find) name of the class method used to retreive related documents (defaults to :get for CouchRest::ExtendedDocument, :find for ActiveRecord::Base)
|
124
|
+
def a( name, options = {} )
|
125
|
+
options[:source] = @klass
|
126
|
+
|
127
|
+
@klass.correlations << Correlate::Relationships.build_correlation( name, :a, options )
|
128
|
+
|
129
|
+
@klass.class_eval <<-EOF, __FILE__, __LINE__
|
130
|
+
def #{name}=( object )
|
131
|
+
self.links.replace( object )
|
132
|
+
end
|
133
|
+
|
134
|
+
def #{name}( raw = false )
|
135
|
+
correlation = self.links.rel( '#{name}' ).first
|
136
|
+
return if correlation.nil?
|
137
|
+
|
138
|
+
correlation = self.class.correlation_for( correlation ).correlate( correlation ) unless raw
|
139
|
+
|
140
|
+
correlation
|
141
|
+
end
|
142
|
+
EOF
|
143
|
+
end
|
144
|
+
|
145
|
+
# @private
|
146
|
+
def build_validators
|
147
|
+
if @klass.included_modules.include?( ::CouchRest::Validation )
|
148
|
+
|
149
|
+
fields = [ :links ]
|
150
|
+
opts = @klass.opts_from_validator_args( fields )
|
151
|
+
@klass.add_validator_to_context( opts, fields, Correlate::Validator )
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# @private
|
156
|
+
def build_views
|
157
|
+
@klass.view_by :rel,
|
158
|
+
:map => <<-MAP
|
159
|
+
function( doc ) {
|
160
|
+
if( doc['couchrest-type'] == '#{@klass}' ) {
|
161
|
+
doc.links.forEach(function(link) {
|
162
|
+
emit([ link.rel, link.href ], 1);
|
163
|
+
});
|
164
|
+
}
|
165
|
+
}
|
166
|
+
MAP
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|