correlate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,23 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ doc/
23
+ .yardoc
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Kenneth Kalmer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,137 @@
1
+ = Correlate
2
+
3
+ Correlate is an experiment in loosely expressing relationsips between documents
4
+ stored in CouchDB using couchrest.
5
+
6
+ Correlate stores all the relationships in an array of links, similar to the link
7
+ elements found in HTML:
8
+
9
+ {
10
+ ...
11
+ "links" : [
12
+ { "rel" : "another_doc", "href" : "<uuid>" },
13
+ { "rel" : "important", "href" : "<uuid>" }
14
+ ...
15
+ ]
16
+ ...
17
+ }
18
+
19
+ Inspired by looking for ways to express the loosened relationships that exists
20
+ between documents inside CouchDB, but to not mimic the nature of an RDBMS.
21
+
22
+ == Documentation
23
+
24
+ * http://rdoc.info/projects/kennethkalmer/correlata
25
+ * spec directory
26
+
27
+ == Installation and dependencies
28
+
29
+ Use:
30
+
31
+ * couchrest 0.33 or later
32
+
33
+ Development:
34
+
35
+ * jeweler
36
+ * rspec
37
+ * activerecord
38
+ * yard
39
+
40
+ Installation:
41
+
42
+ $ gem install correlate
43
+
44
+ Correlate does not depend on ActiveRecord and can be safely used in any project
45
+ that doesn't use ActiveRecord.
46
+
47
+ == Usage
48
+
49
+ require 'correlate'
50
+
51
+ class MyDocument < CouchRest::ExtendedDocument
52
+
53
+ include Correlate
54
+
55
+ related_to do
56
+ some :other_documents, :class => 'OtherDocument', :rel => 'other_doc'
57
+ end
58
+ end
59
+
60
+ This sets up a +links+ property on the document, with some accessors for loading
61
+ and ammending relationships:
62
+
63
+ my_doc = MyDocument.new
64
+
65
+ my_doc.other_documents << other_document_instance
66
+
67
+ my_doc.other_documents #=> [ other_document_instance ]
68
+
69
+ == Associations with ActiveRecord models
70
+
71
+ When transitioning from ActiveRecord to CouchDB it is inevitable that relationships
72
+ will exist across the boundaries of each database. Correlate helps you to express
73
+ these relationships:
74
+
75
+ == CouchDB to ActiveRecord
76
+
77
+ Pretty much the same as the examples above:
78
+
79
+ class MyDocument < CouchRest::ExtendedDocument
80
+
81
+ include Correlate
82
+
83
+ related_to do
84
+ a :model, :class => 'Model', :rel => 'model'
85
+ end
86
+ end
87
+
88
+ == ActiveRecord to CouchDB
89
+
90
+ When using an 'a' relationship, you don't need to specify any additional
91
+ information, but when specifying a 'some' relationship you'll want to supply
92
+ the name of the view.
93
+
94
+ class SomeModel < ActiveRecord::Base
95
+
96
+ include Correlate
97
+
98
+ related_to do
99
+ some :documents, :class => 'Document', :view => 'some_model_id', :key => :id
100
+ some :notes, :class => 'Note', :rel => 'some_model'
101
+ end
102
+ end
103
+
104
+ class Document < CouchRest::ExtendedDocument
105
+
106
+ property :some_model_id
107
+
108
+ view_by :some_model_id
109
+ end
110
+
111
+ class Note < CouchRest::ExtendedDocument
112
+
113
+ include Correlate
114
+
115
+ related_to do
116
+ a :some_model, :class => 'SomeModel', :rel => 'some_model'
117
+ end
118
+ end
119
+
120
+ == Status
121
+
122
+ This project is highly experimental, but is in use in a few applications in one
123
+ form or another.
124
+
125
+ == Note on Patches/Pull Requests
126
+
127
+ * Fork the project.
128
+ * Make your feature addition or bug fix.
129
+ * Add tests for it. This is important so I don't break it in a
130
+ future version unintentionally.
131
+ * Commit, do not mess with rakefile, version, or history.
132
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
133
+ * Send me a pull request. Bonus points for topic branches.
134
+
135
+ == Copyright
136
+
137
+ Copyright (c) 2009 Kenneth Kalmer. See LICENSE for details.
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require File.dirname(__FILE__) + '/lib/correlate'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.version = Correlate::VERSION
9
+ gem.name = "correlate"
10
+ gem.summary = %Q{Help correlate individual documents in a No/Less-SQL environment}
11
+ gem.email = "kenneth.kalmer@gmail.com"
12
+ gem.homepage = "http://github.com/kennethkalmer/correlate"
13
+ gem.authors = ["Kenneth Kalmer"]
14
+ gem.add_dependency "couchrest", ">= 0.33"
15
+ gem.add_development_dependency "rspec", ">= 1.2.9"
16
+ gem.add_development_dependency "yard", ">= 0"
17
+ gem.add_development_dependency "activerecord", ">= 2.3.2"
18
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
19
+ end
20
+ Jeweler::GemcutterTasks.new
21
+ rescue LoadError
22
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
23
+ end
24
+
25
+ require 'spec/rake/spectask'
26
+ Spec::Rake::SpecTask.new(:spec) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.spec_files = FileList['spec/**/*_spec.rb']
29
+ end
30
+
31
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
32
+ spec.libs << 'lib' << 'spec'
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.rcov = true
35
+ end
36
+
37
+ task :spec => :check_dependencies
38
+
39
+ task :default => :spec
40
+
41
+ begin
42
+ require 'yard'
43
+ YARD::Rake::YardocTask.new
44
+ rescue LoadError
45
+ task :yardoc do
46
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
47
+ end
48
+ end
@@ -0,0 +1,101 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{correlate}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Kenneth Kalmer"]
12
+ s.date = %q{2009-12-13}
13
+ s.email = %q{kenneth.kalmer@gmail.com}
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ ".document",
20
+ ".gitignore",
21
+ "LICENSE",
22
+ "README.rdoc",
23
+ "Rakefile",
24
+ "correlate.gemspec",
25
+ "lib/correlate.rb",
26
+ "lib/correlate/correlation.rb",
27
+ "lib/correlate/links.rb",
28
+ "lib/correlate/relationships.rb",
29
+ "lib/correlate/relationships/active_record.rb",
30
+ "lib/correlate/relationships/active_record/collection_proxy.rb",
31
+ "lib/correlate/relationships/couchrest.rb",
32
+ "lib/correlate/validator.rb",
33
+ "spec/active_record_spec.rb",
34
+ "spec/activerecord_helper.rb",
35
+ "spec/correlate_spec.rb",
36
+ "spec/fixtures/article.rb",
37
+ "spec/fixtures/blank_doc.rb",
38
+ "spec/fixtures/comment.rb",
39
+ "spec/fixtures/course.rb",
40
+ "spec/fixtures/crawler.rb",
41
+ "spec/fixtures/news_feed.rb",
42
+ "spec/fixtures/note.rb",
43
+ "spec/fixtures/person.rb",
44
+ "spec/fixtures/project.rb",
45
+ "spec/fixtures/reader.rb",
46
+ "spec/fixtures/student.rb",
47
+ "spec/links_spec.rb",
48
+ "spec/relationships_spec.rb",
49
+ "spec/spec.opts",
50
+ "spec/spec_helper.rb",
51
+ "spec/validation_spec.rb"
52
+ ]
53
+ s.homepage = %q{http://github.com/kennethkalmer/correlate}
54
+ s.rdoc_options = ["--charset=UTF-8"]
55
+ s.require_paths = ["lib"]
56
+ s.rubygems_version = %q{1.3.5}
57
+ s.summary = %q{Help correlate individual documents in a No/Less-SQL environment}
58
+ s.test_files = [
59
+ "spec/active_record_spec.rb",
60
+ "spec/activerecord_helper.rb",
61
+ "spec/correlate_spec.rb",
62
+ "spec/fixtures/article.rb",
63
+ "spec/fixtures/blank_doc.rb",
64
+ "spec/fixtures/comment.rb",
65
+ "spec/fixtures/course.rb",
66
+ "spec/fixtures/crawler.rb",
67
+ "spec/fixtures/news_feed.rb",
68
+ "spec/fixtures/note.rb",
69
+ "spec/fixtures/person.rb",
70
+ "spec/fixtures/project.rb",
71
+ "spec/fixtures/reader.rb",
72
+ "spec/fixtures/student.rb",
73
+ "spec/links_spec.rb",
74
+ "spec/relationships_spec.rb",
75
+ "spec/spec_helper.rb",
76
+ "spec/validation_spec.rb"
77
+ ]
78
+
79
+ if s.respond_to? :specification_version then
80
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
81
+ s.specification_version = 3
82
+
83
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
84
+ s.add_runtime_dependency(%q<couchrest>, [">= 0.33"])
85
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
86
+ s.add_development_dependency(%q<yard>, [">= 0"])
87
+ s.add_development_dependency(%q<activerecord>, [">= 2.3.2"])
88
+ else
89
+ s.add_dependency(%q<couchrest>, [">= 0.33"])
90
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
91
+ s.add_dependency(%q<yard>, [">= 0"])
92
+ s.add_dependency(%q<activerecord>, [">= 2.3.2"])
93
+ end
94
+ else
95
+ s.add_dependency(%q<couchrest>, [">= 0.33"])
96
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
97
+ s.add_dependency(%q<yard>, [">= 0"])
98
+ s.add_dependency(%q<activerecord>, [">= 2.3.2"])
99
+ end
100
+ end
101
+
@@ -0,0 +1,97 @@
1
+ # = Correlate
2
+ #
3
+ # Correlate is an experiment in loosely defining relationships between documents
4
+ # stored in CouchDB using CouchRest's ExtendedDocument, as well as relationships
5
+ # between these documents and ActiveRecord models.
6
+ #
7
+ # == Overview
8
+ #
9
+ # Relationships are defined as an array of hashes, each hash containing a +rel+
10
+ # and +href+ key. This is modelled on HTML's success in loosely defining
11
+ # relationships between different documents.
12
+ #
13
+ # A sample might look like this:
14
+ #
15
+ #
16
+ # {
17
+ # ...
18
+ # "links" : [
19
+ # { "rel" : "another_doc", "href" : "<uuid>" },
20
+ # { "rel" : "important", "href" : "<uuid>" }
21
+ # ...
22
+ # ]
23
+ # ...
24
+ # }
25
+ #
26
+ # == Using correlate
27
+ #
28
+ # Correlate presents itself as a mixin for CouchRest::ExtendedDocument classes,
29
+ # as well as ActiveRecord models.
30
+ #
31
+ # === Using with CouchRest::ExtendedDocument
32
+ #
33
+ # class SomeDocument < CouchRest::ExtendedDocument
34
+ # include Correlate
35
+ #
36
+ # related_to do
37
+ # some :other_documents, :class => 'OtherDocument', :rel => 'other_document'
38
+ # a :parent_document, :class => 'ParentDocument'
39
+ # end
40
+ # end
41
+ #
42
+ #
43
+ # === Using with ActiveRecord::Base
44
+ #
45
+ # class Model < ActiveRecord::Base
46
+ # include Correlate
47
+ #
48
+ # related_to do
49
+ # some :documents, :class => 'Document', :rel => 'model'
50
+ # end
51
+ # end
52
+ #
53
+ # When correlating ActiveRecord models with CouchRest documents, it is required
54
+ # that the 'reverse correlation' is specified in the CouchRest class.
55
+ #
56
+ # @see Correlate::Relationships::CouchRest
57
+ # @see Correlate::Relationships::ActiveRecord
58
+ module Correlate
59
+
60
+ VERSION = '0.1.0'
61
+
62
+ autoload :Relationships, 'correlate/relationships'
63
+ autoload :Links, 'correlate/links'
64
+ autoload :Correlation, 'correlate/correlation'
65
+ autoload :Validator, 'correlate/validator'
66
+
67
+ def self.included( base )
68
+ base.extend( ClassMethods )
69
+ end
70
+
71
+ module ClassMethods
72
+
73
+ # Depending on the containing class, this method configures either a
74
+ # Correlate::Relationships::CouchRest or a
75
+ # Correlate::Relationships::ActiveRecord.
76
+ #
77
+ # @see Correlate::Relationships::CouchRest
78
+ # @see Correlate::Relationships::ActiveRecord
79
+ def related_to( &block )
80
+ Correlate::Relationships.configure!( self, &block )
81
+ end
82
+
83
+ # @private
84
+ def correlations
85
+ @correlations ||= []
86
+ end
87
+
88
+ # Determine the matching correlation for the provided object.
89
+ # @return [ Correlate::Correlation, nil ]
90
+ # @see Correlate::Correlation#matches?
91
+ def correlation_for( object )
92
+ self.correlations.detect { |c| c.matches?( object ) }
93
+ end
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,116 @@
1
+ module Correlate
2
+ class Correlation
3
+ # Class owning the correlation
4
+ attr_accessor :source
5
+
6
+ # Name of the correlation
7
+ attr_accessor :name
8
+
9
+ # Type of relationship
10
+ attr_accessor :type
11
+
12
+ # Class the relationship targets
13
+ attr_accessor :target
14
+
15
+ # +rel+ attribute used to distinguish this correlation
16
+ attr_accessor :rel
17
+
18
+ # Method used to extract the 'id' from instances add to the correlation (defaults to +id+)
19
+ attr_accessor :id_method
20
+
21
+ # For +some+ relationships specify how many instances are required
22
+ attr_accessor :requires
23
+
24
+ # For +a+ relationships specify if required
25
+ attr_accessor :required
26
+
27
+ # Class method on the target class used when loading single instances
28
+ # (defaults to +get+ for CouchRest docs, and +find+ for ActiveRecord models)
29
+ attr_accessor :load_via
30
+
31
+ # Name of a view used to load documents from ActiveRecord's side
32
+ attr_accessor :view
33
+
34
+ # Determine if this correlation can be used for the provided object
35
+ def matches?( obj )
36
+ case obj
37
+ when CouchRest::ExtendedDocument
38
+ target == obj['couchrest-type']
39
+ when Hash
40
+ obj['rel'] == rel
41
+ else
42
+ target == obj.class.name
43
+ end
44
+ end
45
+
46
+ # Return the +rel+ used for this correlation. Uses #name if #rel is nil
47
+ def rel
48
+ (@rel || @name).to_s
49
+ end
50
+
51
+ # Return the method name used for determining the href value of the correlation.
52
+ # Defaults to +:id+.
53
+ def id_method
54
+ @id_method || :id
55
+ end
56
+
57
+ # Correlate the object from its rel/href values.
58
+ def correlate( object )
59
+
60
+ if direction == { :active_record => :couchdb }
61
+
62
+ raise ArgumentError, "#{target} doesn't correlate with #{source}" unless bidirectional?
63
+
64
+ return target_class.by_rel :key => [ opposite.rel, object.send( opposite.id_method ) ]
65
+ end
66
+
67
+ return target_class.send( load_via, object['href'] )
68
+ end
69
+
70
+ # Correlate the objects from their rel/href values
71
+ def bulk_correlate( objects )
72
+ results = objects.inject([]) do |results, obj|
73
+ results << target_class.send( load_via, obj['href'] )
74
+ end.compact
75
+ end
76
+
77
+ # Do we have a mutual correlation (ie defined in both classes)
78
+ def bidirectional?
79
+ target_class.included_modules.include?( Correlate ) && !opposite.nil?
80
+ end
81
+
82
+ # Determine the 'reverse correlation' from the target class
83
+ def opposite
84
+ target_class.correlations.detect { |c| c.target == source.name }
85
+ end
86
+
87
+ # Conveniently show the 'direction' of the correlation in terms of classes
88
+ # @return [ Hash ] { :couchdb => :activerecord } or vice versa or couch to couch.
89
+ def direction
90
+ f = source_class.ancestors.include?( CouchRest::ExtendedDocument ) ? :couchdb : :active_record
91
+ t = target_class.ancestors.include?( CouchRest::ExtendedDocument ) ? :couchdb : :active_record
92
+
93
+ { f => t }
94
+ end
95
+
96
+ private
97
+
98
+ # The class method used to load instances of the documents.
99
+ def load_via
100
+ @load_via ||= (
101
+ target_class.ancestors.include?( CouchRest::ExtendedDocument ) ? :get : :find
102
+ )
103
+ end
104
+
105
+ def target_class
106
+ raise "Class #{target} not found" if !Object.const_defined?( target.to_sym )
107
+
108
+ Object.const_get( target.to_sym )
109
+ end
110
+
111
+ def source_class
112
+ source
113
+ end
114
+
115
+ end
116
+ end