associates 0.0.2

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: 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