join_collection 0.0.1

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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in join_collection.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Mason Chang
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # JoinCollection
2
+
3
+ Join an array of mongoid docs with target objects by specified relation and delegation fields
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'join_collection'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install join_collection
19
+
20
+
21
+ ## Usage
22
+
23
+ ### Initialize a JoinCollection object
24
+
25
+ The class JoinCollection itself is a wrapper around an array of mongoid docs, so to initialize it, simply put
26
+
27
+ user_collection = JoinCollection.new(users) # where users is an array of mongoid docs
28
+
29
+ We will call what we put inside the initilizer the source objects.
30
+
31
+ ### Specifiy parameters to join docs
32
+
33
+ To use any of the `join_*` functions, make sure you provide the following parameters.
34
+
35
+ 1. The 1st parameter is the `target_name`, which is used as the prefix for the delegation fields.
36
+ 2. The 2nd parameter is the `target_class`, which is used to query target objects.
37
+ 3. The 3rd parameter is the `options`, which is not optional and it must be a hash containing a `:relation` key and a `:delegation` key.
38
+ - The key `:relation` points to another hash, which specifies the foreign key to primary key for the type of join relation.
39
+ - The key `:delegation` also points to a hash, which specifies the `:fields` of the target object to be delegated to the doc in the source objects.
40
+ In a has_many relation, you can also privide a `:if` conditional block to specify which target object to delegate if there are many target objects.
41
+
42
+ ## Examples
43
+
44
+ ### Join docs with `belongs_to` relation
45
+
46
+ Assume we have a `user belongs_to site` relationship, and the Site class has the field `url`.
47
+
48
+ user_collection = JoinCollection.new(users)
49
+ user_collection.join_to(:site, Site, :relation => {:site_id => :id}, :delegation => {:fields => [:url]})
50
+
51
+ After this, all the source objects, the users, in the user_collection will have the field `site_url`
52
+
53
+ user_collection.source_objects.first.site_url # => "http://..."
54
+
55
+ ### Join docs with `has_one` relation
56
+
57
+ Assume we have a `user has_one profile` relationship, and the Profile class has the field `twitter`.
58
+
59
+ user_collection = JoinCollection.new(users)
60
+ user_collection.join_one(:profile, Profile, :relation => {:user_id => :id}, :delegation => {:fields => [:twitter]})
61
+
62
+ After this, all the source objects, the users, in the user_collection will have the field `profile_twitter`
63
+
64
+ user_collection.source_objects.first.profile_twitter # => "https://twitter.com/..."
65
+
66
+ ### Join docs with `has_many` relation
67
+
68
+ Assume we have a `user has_many contacts` relationship, and the Contact class has the field `phone`.
69
+
70
+ user_collection = JoinCollection.new(users)
71
+ user_collection.join_many(:contacts, Contact, :relation => {:user_id => :id},
72
+ :delegation => {:if => lambda { |x| x.is_active? }, :fields => [:phone]})
73
+
74
+ After this, all the source objects, the users, in the user_collection will have the field `contact_phone`
75
+
76
+ user_collection.source_objects.first.contact_phone # => "0987-654-321"
77
+
78
+ ### Notes
79
+
80
+ If you specify a delegation field which is identical to the target name of that join type, the whole object(s) will be captured.
81
+
82
+ user_collection.join_to(:site, Site, :relation => {:site_id => :id}, :delegation => {:fields => [:site]})
83
+ user_collection.source_objects.first.site # get the site the user belongs to
84
+
85
+ user_collection.join_one(:profile, Profile, :relation => {:user_id => :id}, :delegation => {:fields => [:profile]})
86
+ user_collection.source_objects.first.profile # get the profile the user has
87
+
88
+ user_collection.join_many(:contacts, Contact, :relation => {:user_id => :id}, :delegation => {:fields => [:contacts]})
89
+ user_collection.source_objects.first.contacts # get all contacts the user has
90
+
91
+
92
+ ## Contributing
93
+
94
+ 1. Fork it
95
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
96
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
97
+ 4. Push to the branch (`git push origin my-new-feature`)
98
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'join_collection'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "join_collection"
8
+ spec.version = JoinCollection::VERSION
9
+ spec.authors = ["Mason Chang"]
10
+ spec.email = ["changmason@gmail.com"]
11
+ spec.description = %q{Joining mongoid docs with specified relation}
12
+ spec.summary = %q{Joining mongoid docs with specified relation}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "mongoid"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rspec"
25
+ end
@@ -0,0 +1,82 @@
1
+ class JoinCollection
2
+
3
+ VERSION = "0.0.1"
4
+
5
+ attr_reader :source_objects
6
+ attr_accessor :join_type, :singular_target, :plural_target
7
+
8
+ def initialize(collection)
9
+ @source_objects = collection
10
+ end
11
+
12
+ def join_to(target, target_class, options)
13
+ self.join_type = :join_to
14
+ fk, pk, delegate_if, delegate_fields = extract_options(target, options)
15
+
16
+ source_fks = source_objects.map(&fk).compact
17
+ target_objects = target_class.where(pk.in => source_fks).to_a
18
+
19
+ mapper = target_objects.group_by(&pk)
20
+ mapper.default = []
21
+ join_data(mapper, fk, delegate_if, delegate_fields)
22
+ end
23
+
24
+ def join_one(target, target_class, options)
25
+ self.join_type = :join_one
26
+ join_many(target, target_class, options)
27
+ end
28
+
29
+ def join_many(target, target_class, options)
30
+ self.join_type = :join_many unless self.join_type == :join_one
31
+ fk, pk, delegate_if, delegate_fields = extract_options(target, options)
32
+
33
+ source_pks = source_objects.map(&pk).compact
34
+ target_objects = target_class.where(fk.in => source_pks).to_a
35
+
36
+ mapper = target_objects.group_by(&fk)
37
+ mapper.default = []
38
+ join_data(mapper, pk, delegate_if, delegate_fields)
39
+ end
40
+
41
+ private
42
+
43
+ def extract_options(target, options)
44
+ self.singular_target = target.to_s.singularize.to_sym
45
+ self.plural_target = target.to_s.pluralize.to_sym
46
+
47
+ relation = options[:relation]
48
+ delegation = options[:delegation]
49
+ raise ArgumentError.new('Relation hash not found in options') unless relation.is_a?(Hash)
50
+ raise ArgumentError.new('Delegation hash not found in options') unless delegation.is_a?(Hash)
51
+
52
+ fk = relation.keys.first
53
+ pk = relation.values.first
54
+
55
+ if_block = delegation[:if] || lambda { |x| true }
56
+ fields = delegation[:fields] || []
57
+
58
+ return fk, pk, if_block, fields
59
+ end
60
+
61
+ def join_data(mapper, key, delegate_if, delegate_fields)
62
+ join_type = self.join_type
63
+ singular_target = self.singular_target
64
+ plural_target = self.plural_target
65
+
66
+ source_objects.each do |doc|
67
+ target_objects = mapper[doc[key]]
68
+ target_object = (target_objects.find &delegate_if) || target_objects.first
69
+
70
+ delegate_fields.each do |field|
71
+ case field
72
+ when singular_target
73
+ doc[singular_target] = target_object unless join_type == :join_many
74
+ when plural_target
75
+ doc[plural_target] = target_objects if join_type == :join_many
76
+ else
77
+ doc["#{singular_target}_#{field}"] = target_object.send(field)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,116 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ class User; include Mongoid::Document; end
5
+ class Post; include Mongoid::Document; end
6
+
7
+ describe JoinCollection do
8
+ let!(:user1) { User.new :mysql_id => 1, :name => 'Bob' }
9
+ let!(:user2) { User.new :mysql_id => 2, :name => 'Joe' }
10
+
11
+ let!(:post1) { Post.new :mysql_id => 1, :user_id => 1, :content => 'text 1', :published => true }
12
+ let!(:post2) { Post.new :mysql_id => 2, :user_id => 2, :content => 'text 2', :published => true }
13
+ let!(:post3) { Post.new :mysql_id => 3, :user_id => 2, :content => 'text 3', :published => false }
14
+
15
+
16
+ context 'extract options' do
17
+ before do
18
+ @user_collection = JoinCollection.new([user1])
19
+ end
20
+
21
+ it 'raise an argument error if relation hash is not found in options' do
22
+ expect{@user_collection.join_many(:post, Post, :delegation => {})}.to raise_error(ArgumentError)
23
+ end
24
+
25
+ it 'raise an argument error if delegation hash is not found in options' do
26
+ expect{@user_collection.join_many(:post, Post, :relation => {})}.to raise_error(ArgumentError)
27
+ end
28
+ end
29
+
30
+ # post1 belongs_to user1
31
+ describe '#join_to' do
32
+ before do
33
+ @post_collection = JoinCollection.new([post1])
34
+ User.stub(:where).and_return([user1])
35
+ end
36
+
37
+ it 'should call User.where' do
38
+ User.should_receive(:where).with(:mysql_id.in => [1]).and_return([user1])
39
+ @post_collection.join_to(:user, User, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:mysql_id]})
40
+ end
41
+
42
+ it 'should catch the target object if the delegation field name equals to the target' do
43
+ @post_collection.join_to(:user, User, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:user]})
44
+ expect(@post_collection.source_objects.first.user).to eq(user1)
45
+ end
46
+
47
+ it 'should have correct value in delegation field if the target object has the field' do
48
+ @post_collection.join_to(:user, User, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:name]})
49
+ expect(@post_collection.source_objects.first.user_name).to eq('Bob')
50
+ end
51
+
52
+ it 'should raise no method error if the target object does not have the field' do
53
+ expect{
54
+ @post_collection.join_to(:user, User, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:email]})
55
+ }.to raise_error(NoMethodError)
56
+ end
57
+ end
58
+
59
+ # user1 has_one post1
60
+ describe '#join_one' do
61
+ before do
62
+ @user_collection = JoinCollection.new([user1])
63
+ Post.stub(:where).and_return([post1])
64
+ end
65
+
66
+ it 'should call Post.where' do
67
+ Post.should_receive(:where).with(:user_id.in => [1]).and_return([post1])
68
+ @user_collection.join_one(:post, Post, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:mysql_id]})
69
+ end
70
+
71
+ it 'should catch the target object if the delegation field name equals to the target name' do
72
+ @user_collection.join_one(:post, Post, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:post]})
73
+ expect(@user_collection.source_objects.first.post).to eq(post1)
74
+ end
75
+
76
+ it 'should have correct value in delegation field if the target object has the field' do
77
+ @user_collection.join_one(:post, Post, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:content]})
78
+ expect(@user_collection.source_objects.first.post_content).to eq('text 1')
79
+ end
80
+
81
+ it 'should raise no method error if the target object does not have the field' do
82
+ expect{
83
+ @user_collection.join_one(:post, Post, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:location]})
84
+ }.to raise_error(NoMethodError)
85
+ end
86
+ end
87
+
88
+ # user2 has_many posts which are post2 and post3
89
+ describe '#join_many' do
90
+ before do
91
+ @user_collection = JoinCollection.new([user2])
92
+ Post.stub(:where).and_return([post2, post3])
93
+ end
94
+
95
+ it 'should catch the whole target objects if the delegation field name equals to the plural target name' do
96
+ @user_collection.join_many(:post, Post,
97
+ :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:posts]})
98
+ expect(@user_collection.source_objects.first.posts).to eq([post2, post3])
99
+ end
100
+
101
+ it 'should have correct values in delegation fields if no conditional block given' do
102
+ @user_collection.join_many(:post, Post,
103
+ :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:content, :published]})
104
+ expect(@user_collection.source_objects.first.post_content).to eq('text 2')
105
+ expect(@user_collection.source_objects.first.post_published).to be_true
106
+ end
107
+
108
+ it 'should have correct values in delegation fields if a conditional block given' do
109
+ @user_collection.join_many(:post, Post,
110
+ :relation => {:user_id => :mysql_id},
111
+ :delegation => {:if => lambda { |x| x.published == false }, :fields => [:content, :published]})
112
+ expect(@user_collection.source_objects.first.post_content).to eq('text 3')
113
+ expect(@user_collection.source_objects.first.post_published).to be_false
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'mongoid'
5
+ require 'join_collection'
6
+
7
+ RSpec.configure do |config|
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: join_collection
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mason Chang
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-07-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: mongoid
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Joining mongoid docs with specified relation
79
+ email:
80
+ - changmason@gmail.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - .gitignore
86
+ - Gemfile
87
+ - LICENSE.txt
88
+ - README.md
89
+ - Rakefile
90
+ - join_collection.gemspec
91
+ - lib/join_collection.rb
92
+ - spec/join_collection_spec.rb
93
+ - spec/spec_helper.rb
94
+ homepage: ''
95
+ licenses:
96
+ - MIT
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 1.8.23
116
+ signing_key:
117
+ specification_version: 3
118
+ summary: Joining mongoid docs with specified relation
119
+ test_files:
120
+ - spec/join_collection_spec.rb
121
+ - spec/spec_helper.rb