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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/lib/associates.rb +175 -0
- data/lib/associates/persistence.rb +35 -0
- data/lib/associates/validations.rb +41 -0
- data/lib/associates/version.rb +3 -0
- metadata +99 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
@@ -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
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data.tar.gz.sig
ADDED
Binary file
|
data/lib/associates.rb
ADDED
@@ -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
|
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:
|
metadata.gz.sig
ADDED
Binary file
|