virtus-relations 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.
@@ -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