associates 0.0.2

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: 1de008a6d97d2dbf9f02295947c05d758d519191
4
+ data.tar.gz: 53485eb8cab825705f024869bd53c78417c441c4
5
+ SHA512:
6
+ metadata.gz: 0153eb8ef6935fc08ec4de4a53d126bf441d1500b1e32662d16203f806eb43cdf5f7a995d0763ef1fe09218fa9abeb1be9e23c1a0384217c02504f8b98749cd8
7
+ data.tar.gz: 2a37522a4e24f6ca6c3971d147c8d02c4d736e6e930e611e7b74f1c10163440c3b92eb08564dbf81098402f9a8b5eebbbfbb76adbd5d33397c2afcc01c7b726d
Binary file
Binary file
@@ -0,0 +1,175 @@
1
+ require 'active_support/concern'
2
+
3
+ require 'associates/version'
4
+ require 'associates/persistence'
5
+ require 'associates/validations'
6
+
7
+ module Associates
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ include ActiveModel::Model
12
+ include Persistence
13
+ include Validations
14
+
15
+ class_attribute :associates, instance_writer: false
16
+ self.associates = Array.new
17
+ end
18
+
19
+ BLACKLISTED_ATTRIBUTES = ['id', 'updated_at', 'created_at', 'deleted_at']
20
+
21
+ Item = Struct.new(:name, :klass, :attribute_names, :dependent_names, :options)
22
+
23
+ module ClassMethods
24
+
25
+ # Defines an associated model
26
+ #
27
+ # @example
28
+ # class User
29
+ # include Associates
30
+ #
31
+ # associate :user, only: :username
32
+ # end
33
+ #
34
+ # @param model [Symbol, Class]
35
+ # @param [Hash] options
36
+ # @option options [Symbol, Array] :only Only generate methods for the given attributes
37
+ #
38
+ # @option options [Symbol, Array] :except Generate all the model's methods except
39
+ # for the given attributes
40
+ #
41
+ # @option options [Symbol] :depends_on Specify one or more associate name on
42
+ # which the current associate model depends to be valid. Allow to automatically
43
+ # setup `belongs_to` associations between models
44
+ #
45
+ # @option options [String, Class] :class_name Specify the class name of the associate.
46
+ # Use it only if that name can’t be inferred from the associate's name
47
+ #
48
+ # @option options [Boolean] :delegate (true) Wether or not to delegate the associate's
49
+ # attributes getter and setters methods to the associate instance
50
+ def associate(model, options = {})
51
+ options = {
52
+ delegate: true
53
+ }.merge(options)
54
+
55
+ associate = build_associate(model, options)
56
+ self.associates << associate
57
+
58
+ define_associate_delegation(associate) if options[:delegate]
59
+ define_associate_instance_setter_method(associate)
60
+ define_associate_instance_getter_method(associate)
61
+ end
62
+
63
+
64
+ private
65
+
66
+ # Builds an associate
67
+ #
68
+ # @param model [Symbol, Class]
69
+ # @param options [Hash]
70
+ # @return [Item]
71
+ def build_associate(model, options = {})
72
+ model_name = model.to_s.underscore
73
+ model_klass = (options[:class_name] || model).to_s.classify.constantize
74
+ dependent_models_names = extract_attributes(options[:depends_on]) || []
75
+ dependent_models_names = dependent_models_names.map(&:to_s)
76
+
77
+ if options[:only]
78
+ attribute_names = extract_attributes(options[:only])
79
+ else
80
+ excluded = BLACKLISTED_ATTRIBUTES.to_a
81
+
82
+ if options[:except]
83
+ excluded += extract_attributes(options[:except]).map(&:to_s)
84
+ end
85
+
86
+ attribute_names = model_klass.attribute_names.reject { |name| excluded.include?(name) }
87
+ end
88
+
89
+ # Ensure associate name don't clash with already declared ones
90
+ if associates.map(&:name).include?(model_name)
91
+ raise NameError, "already defined associate name '#{model_name}' for #{name}(#{object_id})"
92
+ end
93
+
94
+ # Ensure associate attribute names don't clash with already declared ones
95
+ if options[:delegate]
96
+ attribute_names.each do |attribute_name|
97
+ if associates.map(&:attribute_names).include?(attribute_name)
98
+ raise NameError, "already defined attribute name '#{attribute_name}' for #{name}(#{object_id})"
99
+ end
100
+ end
101
+ end
102
+
103
+ # Ensure associate dependent names exists
104
+ dependent_models_names.each do |dependent_name|
105
+ unless associates.map(&:name).include?(dependent_name)
106
+ raise NameError, "undefined associated model '#{dependent_name}' for #{name}(#{object_id})"
107
+ end
108
+ end
109
+
110
+ Item.new(model_name, model_klass, attribute_names, dependent_models_names, options)
111
+ end
112
+
113
+ # Define associated model attribute methods delegation
114
+ #
115
+ # @param associate [Item]
116
+ def define_associate_delegation(associate)
117
+ methods = [associate.attribute_names, associate.attribute_names.map { |attr| "#{attr}=" }].flatten
118
+ send(:delegate, *methods, to: associate.name)
119
+ end
120
+
121
+ # Define associated model instance setter method
122
+ #
123
+ # @example
124
+ #
125
+ # @association.user = User.new
126
+ #
127
+ # @param associate [Item]
128
+ def define_associate_instance_setter_method(associate)
129
+ define_method "#{associate.name}=" do |object|
130
+ unless object.is_a?(associate.klass)
131
+ raise ArgumentError, "#{associate.klass}(##{associate.klass.object_id}) expected, got #{object.class}(##{object.class.object_id})"
132
+ end
133
+
134
+ instance = instance_variable_set("@#{associate.name}", object)
135
+
136
+ depending = associates.select { |_associate| _associate.dependent_names.include?(associate.name) }
137
+ depending.each do |_associate|
138
+ send(_associate.name).send("#{associate.name}=", instance)
139
+ end
140
+
141
+ instance
142
+ end
143
+ end
144
+
145
+ # Define associated model instance getter method
146
+ #
147
+ # @example
148
+ #
149
+ # @association.user
150
+ #
151
+ # @param associate [Item]
152
+ def define_associate_instance_getter_method(associate)
153
+ define_method associate.name do
154
+ instance = instance_variable_get("@#{associate.name}") || instance_variable_set("@#{associate.name}", associate.klass.new)
155
+
156
+ depending = associates.select { |_associate| _associate.dependent_names.include?(associate.name) }
157
+ depending.each do |_associate|
158
+ existing = send(_associate.name).send(associate.name)
159
+ send(_associate.name).send("#{associate.name}=", instance) unless existing
160
+ end
161
+
162
+ instance
163
+ end
164
+ end
165
+
166
+ # Allow to accept single or multiple elements as arguments. Ensures a collection
167
+ # is always returned when there is one or more elements.
168
+ #
169
+ # @return [Nil, Array]
170
+ def extract_attributes(object)
171
+ return nil if object.blank?
172
+ object.is_a?(Enumerable) ? object : [object]
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,35 @@
1
+ require 'active_record'
2
+
3
+ module Associates
4
+ module Persistence
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ end
9
+
10
+ module ClassMethods
11
+ end
12
+
13
+ # Persists each associated model
14
+ #
15
+ # @return [Boolean] Wether or not all models are valid and persited
16
+ def save(*args)
17
+ return false unless valid?
18
+
19
+ ActiveRecord::Base.transaction do
20
+ begin
21
+ associates.all? do |associate|
22
+ send(associate.name).send(:save!, *args)
23
+ end
24
+ rescue ActiveRecord::RecordInvalid
25
+ false
26
+ end
27
+ end
28
+ end
29
+
30
+ # @return [True, ActiveRecord::RecordInvalid]
31
+ def save!
32
+ save || raise(ActiveRecord::RecordInvalid)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ module Associates
2
+ module Validations
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ alias_method_chain :valid?, :associates
7
+ end
8
+
9
+ module ClassMethods
10
+ end
11
+
12
+ # Runs the model validations plus the associated models validations and
13
+ # merges each messages in the errors hash
14
+ #
15
+ # @return [Boolean]
16
+ def valid_with_associates?(context = nil)
17
+ # Model validations
18
+ valid_without_associates?(context)
19
+
20
+ # Associated models validations
21
+ self.class.associates.each do |associate|
22
+ model = send(associate.name)
23
+ model.valid?(context)
24
+
25
+ model.errors.each_entry do |attribute, message|
26
+ # Do not include association presence validation errors
27
+ if associate.dependent_names.include?(attribute.to_s)
28
+ next
29
+ elsif respond_to?(attribute)
30
+ errors.add(attribute, message)
31
+ else
32
+ errors.add(:base, model.errors.full_messages_for(attribute))
33
+ end
34
+ end
35
+ end
36
+
37
+ errors.messages.values.each(&:uniq!)
38
+ errors.none?
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module Associates
2
+ VERSION = "0.0.2"
3
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: associates
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Philippe Dionne
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDfDCCAmSgAwIBAgIBATANBgkqhkiG9w0BAQUFADBCMRQwEgYDVQQDDAtkaW9u
14
+ bmUucGhpbDEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPyLGQBGRYD
15
+ Y29tMB4XDTEzMDMwOTEzMjMzNVoXDTE0MDMwOTEzMjMzNVowQjEUMBIGA1UEAwwL
16
+ ZGlvbm5lLnBoaWwxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixk
17
+ ARkWA2NvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtGK2NoFyVX
18
+ seKlfHb8nuhBy8rw1nP3JVHWoyqalLs0t3axUWe6sDCgQXAOL3FtkfYPYlyX6X4s
19
+ FggcSySUt0PvGdas0ym/lW+sfGT6gXDBKbPvkWFNtpXaIJ8FLhqnJR2m57b8R6pf
20
+ VMBDka95N80Xu9NQrAesF+wW/c6ZiAON2VDsWabZEfj146y7ZvviB7pZHlPznI4W
21
+ GVfOk0MCl0Q60Jyk7WwNec+NA9rG7vE/dWdGOKfxU6ZJnC6drC3kBsBKkRZs+GRh
22
+ 6XoCAsrKbRwb/ZI3A1S/J9yvq/NQ1B/U3CisBFht+CbdspeXVUDx5dffDVxnxdo/
23
+ w2R3wJMvD2MCAwEAAaN9MHswCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
24
+ BBYEFA+69dGxvFtLxJ+aH8l4UO9yy853MCAGA1UdEQQZMBeBFWRpb25uZS5waGls
25
+ QGdtYWlsLmNvbTAgBgNVHRIEGTAXgRVkaW9ubmUucGhpbEBnbWFpbC5jb20wDQYJ
26
+ KoZIhvcNAQEFBQADggEBAD7Vpuym0VvnXOydoCBkv/w8u32Njigt3bF3i/P91108
27
+ aDvORUymYrPxaBe+AsSJUkJB0MHywJdMc4MF3YISelCvUFGza8LYvvUIgyVKFom+
28
+ TN8WCPZLo4mYaYo4sStkF/G5v8I3ruZObr7o/f20xG5r8E4ND2GEGTeFJhgpMcHO
29
+ Ir1uYn/LLeGG5g2EAKCdYy2XoohtcSPYApbeFQgWOGKoioQVRCDG3cHmakfVAV5M
30
+ DuNlzvjhWtPDwE5mYO5x5XquWQuENw78urt1aioNrIe0/15dHpDoIEDILa6zU46B
31
+ XDtp1YxdeVGIBuNoP1vjDSvNKYj0pMjRAEPrqK39jKE=
32
+ -----END CERTIFICATE-----
33
+ date: 2013-10-27 00:00:00.000000000 Z
34
+ dependencies:
35
+ - !ruby/object:Gem::Dependency
36
+ name: activerecord
37
+ requirement: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: 3.2.14
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: 3.2.14
49
+ - !ruby/object:Gem::Dependency
50
+ name: activesupport
51
+ requirement: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - '>='
54
+ - !ruby/object:Gem::Version
55
+ version: 3.2.14
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: 3.2.14
63
+ description: Associate multiple models together and make them behave as one.
64
+ email:
65
+ - dionne.phil@gmail.com
66
+ executables: []
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - lib/associates.rb
71
+ - lib/associates/persistence.rb
72
+ - lib/associates/validations.rb
73
+ - lib/associates/version.rb
74
+ homepage: https://github.com/phildionne/associates
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.1.9
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Associate multiple models together and make them behave as one.
98
+ test_files: []
99
+ has_rdoc:
Binary file