virtus-relations 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a8d2c028409e9b6f8e746c754c9bf30b527e4d36
4
+ data.tar.gz: 28a863c25d519a3bd1f17026b3ffc85063b2c43d
5
+ SHA512:
6
+ metadata.gz: 9fb675108952c9f28a78aa09b44807cb5b43dc4b461534fd3dfeb232cedecccf59cb59b90034154a2de0d5ff69148daf856f5fa33ad48426cf440a2573f845ef
7
+ data.tar.gz: 66b4929df4ab1176919fbe0c43c6fd8921299c8a40331c985376b02bb8df168deeca3b981b18c799a62e2b7d7ab05ae694f7f723463040ae6b57387ebee15c1d
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in virtus-relations.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Simeon Manolov
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.
@@ -0,0 +1,75 @@
1
+ # Virtus::Relations
2
+
3
+ Adds relations to Virtus objects.
4
+
5
+ ## Installation
6
+
7
+ In your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'virtus-relations'
11
+ ```
12
+
13
+ In your source code:
14
+
15
+ ```ruby
16
+ require 'virtus/relations'
17
+ ```
18
+
19
+ ## Usage examples
20
+ Given the following classes:
21
+ ```ruby
22
+ require 'virtus/relations'
23
+
24
+ class Kid
25
+ include Virtus.model
26
+
27
+ attribute :name, String
28
+ end
29
+
30
+ class Mother
31
+ include Virtus.model
32
+ include Virtus.relations(as: :mom)
33
+
34
+ attribute :name, String
35
+ attribute :kid, Kid, relation: true, lazy: true, default: :load_kid
36
+
37
+ def load_kid
38
+ { name: 'Billy' }
39
+ end
40
+ end
41
+ ```
42
+
43
+ You can do the following:
44
+ ```ruby
45
+ ### Explicit assignment
46
+ alice = Mother.new(name: 'Alice')
47
+ alice.kid = { name: 'Danny' }
48
+ alice.kid.mom
49
+ # => <Mother:0x007fa43415fda8 @name="Alice", ...>
50
+
51
+ ### Mass-assignment
52
+ emma = Mother.new(name: 'Emma', kid: { name: 'Johnny' })
53
+ emma.kid.mom
54
+ # => #<Mother:0x007fc40dbbdbf8 @name="Emma", ...>
55
+
56
+ ### Lazy assignment
57
+ mia = Mother.new(name: 'Mia')
58
+ mia.kid.mom
59
+ # => #<Mother:0x007fa435d27130 @name="Mia", ...>
60
+
61
+ ### Objects can still be created without a parent
62
+ orphan = Kid.new(name: 'Deirdre')
63
+ orphan.parent
64
+ # => NoMethodError: undefined method `parent' for #<Kid:0x007...
65
+ ```
66
+
67
+ The `:as` option defaults to `parent` when omitted
68
+
69
+ ## Contributing
70
+
71
+ 1. Fork it
72
+ 2. Create your feature branch
73
+ 3. Comply with the [ruby style guide](https://github.com/bbatsov/ruby-style-guide)
74
+ 4. Add tests for your new feature/bugfix. This is important so I don't break it in a future version unintentionally.
75
+ 5. Submit a pull request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,186 @@
1
+ require 'virtus'
2
+ require 'virtus/relations/version'
3
+
4
+ module Virtus
5
+ def self.relations(params = {})
6
+ rel_name = params.fetch(:as, :parent)
7
+ fail TypeError, 'Symbol expected' unless rel_name.is_a?(Symbol)
8
+
9
+ mod = Module.new
10
+ mod.instance_variable_set(:@rel_name, rel_name)
11
+
12
+ mod.module_eval do
13
+ def self.class_methods
14
+ Module.new do
15
+ # During instantiation, .relation_attributes is called
16
+ # If class was instantiated before, just return the list of related
17
+ # attributes
18
+ #
19
+ # If instantiated for the first time, then monkey-patch some instance
20
+ # methods to allow relation attributes to work with accessor methods.
21
+ #
22
+ #
23
+ # patch Model#attribute=
24
+ #
25
+ # 1. create {list}
26
+ # 2. for each {attribute} in {list}
27
+ # 2.1. patch the accessor method
28
+ # 2.2. patch the lazy initializer method (if any)
29
+ #
30
+ def relation_attributes
31
+ @relation_attributes ||= attribute_set.select do |attribute|
32
+ next unless attribute.options[:relation] == true
33
+
34
+ patch_accessor_method(attribute)
35
+ patch_lazy_initializer(attribute)
36
+ true
37
+ end
38
+ end
39
+
40
+ #
41
+ # When the original method returns something, we need to
42
+ # actually set ourselves as its parent
43
+ # If that something is a collection, do it for each element
44
+ #
45
+ def relate(object, method_return)
46
+ if object.kind_of?(Array)
47
+ object.each { |o| relate(o, method_return) }
48
+ else
49
+ object.define_singleton_method(relation_name) { method_return }
50
+ object
51
+ end
52
+ end
53
+
54
+
55
+ protected
56
+ # 1. rename method "{attribute}=" to "{attribute}_not_related="
57
+ # 2. define method "{attribute}=", which:
58
+ # 2.1. calls "{attribute}_not_related=", takes the {return_object}
59
+ # 2.2. defines {return_object}.parent which returns self
60
+ # 2.3. returns {return_object}
61
+ # 3. return the list
62
+ def patch_accessor_method(attribute)
63
+ if [Numeric, Symbol].any? { |c| attribute.primitive.ancestors.include?(c) }
64
+ fail "Relations don't work with Numeric and Symbol types"
65
+ end
66
+
67
+ old_method = "#{attribute.name}_not_related="
68
+ new_method = "#{attribute.name}="
69
+
70
+ # Suffix the original method with '_not_related'
71
+ define_method(old_method, instance_method(new_method))
72
+ define_method(new_method) do |value|
73
+ return_value = send(old_method, value)
74
+ self.class.relate(return_value, self)
75
+ end
76
+
77
+ private old_method
78
+ visibility = attribute.options[:writer]
79
+ send(visibility, new_method)
80
+ end
81
+
82
+ # (see above for the explanation of step 2.)
83
+ # Wrap the original method/proc into a new one, which:
84
+ # 1. calls the original one, takes the {return_object}
85
+ # 2. coerces {return_object}
86
+ # 3. defines {return_object}.parent which returns self
87
+ # 4. returns {return_object}
88
+ #
89
+ def patch_lazy_initializer(attribute)
90
+ return unless attribute.lazy?
91
+
92
+ case attribute.default_value.value
93
+ when Proc
94
+ old_proc = attribute.default_value.value
95
+ new_proc = proc do |object, *args|
96
+ return_value = attribute.coerce(old_proc.call(*[object, *args]))
97
+ object.class.relate(return_value, object)
98
+ end
99
+
100
+ attribute.default_value.instance_variable_set(:@value, new_proc)
101
+ else
102
+ old_method = "#{attribute.default_value.value}_not_related"
103
+ new_method = attribute.default_value.value
104
+ visibility = if private_method_defined?(new_method)
105
+ :private
106
+ elsif protected_method_defined?(new_method)
107
+ :protected
108
+ else
109
+ :public
110
+ end
111
+
112
+ define_method(old_method, instance_method(new_method))
113
+ define_method(new_method) do
114
+ return_value = attribute.coerce(send(old_method))
115
+ self.class.relate(return_value, self)
116
+ return_value
117
+ end
118
+
119
+ private old_method
120
+ send(visibility, new_method)
121
+ end
122
+ end
123
+ end # Module.new
124
+ end # def
125
+
126
+ def self.instance_methods
127
+ Module.new do
128
+ #
129
+ # Enhance the initialize process to allow
130
+ # related attributes to work with mass-assignment
131
+ #
132
+ def initialize(mass_assignment_attributes = {})
133
+ super
134
+
135
+ self.class.relation_attributes.each do |ra|
136
+ # set self as the child's parent only if child was mass-
137
+ # assigned during self.initialize (e.g. hash has child's key)
138
+ # This prevents children from doing this against the parents
139
+ # when they also have relation: true on their attributes
140
+ # (since their initializers will not receive the parent as
141
+ # a part of the mass-assignment)
142
+
143
+ if mass_assignment_attributes.key?(ra.name)
144
+ self.class.relate(ra.get(self), self)
145
+ end
146
+ end
147
+ end
148
+
149
+ alias_method :dup_not_related, :dup
150
+
151
+ # Add #parent to duped objects, when available
152
+ def dup
153
+ rel = self.class.relation_name
154
+ if respond_to?(rel)
155
+ self.class.relate(dup_not_related, rel)
156
+ else
157
+ dup_not_related
158
+ end
159
+ end
160
+ end # Module.new
161
+ end # def
162
+
163
+ def self.included(base)
164
+ required = [
165
+ Virtus::InstanceMethods::MassAssignment,
166
+ Virtus::InstanceMethods::Constructor
167
+ ]
168
+
169
+ unless required.all? { |mod| base.included_modules.include?(mod) }
170
+ fail 'Virtus.model must be included prior to Virtus.relations'
171
+ end
172
+
173
+ # Using a local variable in the parent scope
174
+ # with the same name as the singleton method
175
+ # prevents recursion
176
+ relation_name = @rel_name
177
+ base.define_singleton_method(:relation_name) { relation_name }
178
+
179
+ base.send(:include, instance_methods)
180
+ base.extend(class_methods)
181
+ end
182
+
183
+ mod
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,5 @@
1
+ module Virtus
2
+ module Relations
3
+ VERSION = '1.0.0'
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ Bundler.require(:default, :development)
2
+
3
+ RSpec.configure do |config|
4
+ config.expect_with :rspec do |c|
5
+ c.syntax = :expect
6
+ end
7
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+ require 'active_support/core_ext/object/instance_variables'
3
+
4
+ describe Virtus::Relations do
5
+ # The child
6
+ class C
7
+ include Virtus.model
8
+ end
9
+
10
+ # The parent, referenced by Child#parent
11
+ class P
12
+ include Virtus.model
13
+ include Virtus.relations
14
+
15
+ attribute :c_1, C, relation: true
16
+ attribute :c_2, C, relation: true, lazy: true, default: :load_c_2
17
+ attribute :c_3, C, relation: true, lazy: true, default: proc { {} }
18
+ attribute :c_4, Array[C], relation: true
19
+ attribute :c_5, Array[C], relation: true, lazy: true, default: :load_c_5
20
+ attribute :c_6, Array[C], relation: true, lazy: true, default: proc { [{}] }
21
+
22
+ def load_c_2
23
+ {}
24
+ end
25
+
26
+ def load_c_5
27
+ [{}]
28
+ end
29
+ end
30
+
31
+ # Another parent, but referenced by Child#father
32
+ class F
33
+ include Virtus.model
34
+ include Virtus.relations(as: :father)
35
+
36
+ attribute :c, C, relation: true
37
+ end
38
+
39
+ it 'allow to specify a relation name' do
40
+ f = F.new
41
+ f.c = {}
42
+
43
+ expect(f.c.father).to be(f)
44
+ end
45
+
46
+ describe 'Object#parent' do
47
+ context 'when initialized via a related attribute' do
48
+ context 'of non-array type' do
49
+ context 'and is mass-assigned' do
50
+ let(:p) { P.new(c_1: {}) }
51
+
52
+ it 'returns the attribute owner' do
53
+ expect(p.c_1.parent).to be(p)
54
+ end
55
+
56
+ it 'does not store its return value in an instance variable' do
57
+ expect(p.c_1.instance_values.values).not_to include(p)
58
+ end
59
+ end
60
+
61
+ context 'and is strictly initialized' do
62
+ it 'returns the attribute owner' do
63
+ p = P.new
64
+ p.c_1 = {}
65
+ expect(p.c_1.parent).to be(p)
66
+ end
67
+ end
68
+
69
+ context 'and is lazily initialized via a method' do
70
+ it 'returns the attribute owner' do
71
+ p = P.new
72
+ expect(p.c_2.parent).to be(p)
73
+ end
74
+ end
75
+
76
+ context 'and is lazily initialized via a proc' do
77
+ it 'returns the attribute owner' do
78
+ p = P.new
79
+ p.c_3
80
+ expect(p.c_3.parent).to be(p)
81
+ end
82
+ end
83
+ end
84
+
85
+ context 'of an array type' do
86
+ context 'and is mass-assigned' do
87
+ it 'returns the attribute owner' do
88
+ p = P.new(c_4: [{}])
89
+ expect(p.c_4.first.parent).to be(p)
90
+ end
91
+ end
92
+
93
+ context 'and is strictly initialized' do
94
+ it 'returns the attribute owner' do
95
+ p = P.new
96
+ p.c_4 = [{}]
97
+ expect(p.c_4.first.parent).to be(p)
98
+ end
99
+ end
100
+
101
+ context 'and is lazily initialized via a method' do
102
+ it 'returns the attribute owner' do
103
+ p = P.new
104
+ expect(p.c_5.first.parent).to be(p)
105
+ end
106
+ end
107
+
108
+ context 'and is lazily initialized via a proc' do
109
+ it 'returns the attribute owner' do
110
+ p = P.new
111
+ expect(p.c_6.first.parent).to be(p)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ describe ''
119
+
120
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'virtus/relations/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'virtus-relations'
8
+ spec.version = Virtus::Relations::VERSION
9
+ spec.authors = ['Simeon Manolov']
10
+ spec.email = ['s.manolloff@gmail.com']
11
+ spec.summary = %q{Relations for Virtus models}
12
+ spec.homepage = 'https://github.com/smanolloff/virtus-relations'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_runtime_dependency 'virtus', '~> 1'
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.7'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'rspec', '~> 3.2'
25
+ spec.add_development_dependency 'pry-byebug', '~> 3.1'
26
+ spec.add_development_dependency 'activesupport', '~> 4.2'
27
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: virtus-relations
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Simeon Manolov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: virtus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.1'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activesupport
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.2'
97
+ description:
98
+ email:
99
+ - s.manolloff@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - Gemfile
106
+ - LICENSE.txt
107
+ - README.md
108
+ - Rakefile
109
+ - lib/virtus/relations.rb
110
+ - lib/virtus/relations/version.rb
111
+ - spec/spec_helper.rb
112
+ - spec/virtus/relations_spec.rb
113
+ - virtus-relations.gemspec
114
+ homepage: https://github.com/smanolloff/virtus-relations
115
+ licenses:
116
+ - MIT
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 2.4.3
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Relations for Virtus models
138
+ test_files:
139
+ - spec/spec_helper.rb
140
+ - spec/virtus/relations_spec.rb