ruby-activeldap 0.7.4 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +375 -0
- data/COPYING +340 -0
- data/LICENSE +58 -0
- data/Manifest.txt +33 -0
- data/README +63 -0
- data/Rakefile +37 -0
- data/TODO +31 -0
- data/benchmark/bench-al.rb +152 -0
- data/lib/{activeldap.rb → active_ldap.rb} +280 -263
- data/lib/active_ldap/adaptor/base.rb +29 -0
- data/lib/active_ldap/adaptor/ldap.rb +466 -0
- data/lib/active_ldap/association/belongs_to.rb +38 -0
- data/lib/active_ldap/association/belongs_to_many.rb +40 -0
- data/lib/active_ldap/association/collection.rb +80 -0
- data/lib/active_ldap/association/has_many.rb +48 -0
- data/lib/active_ldap/association/has_many_wrap.rb +56 -0
- data/lib/active_ldap/association/proxy.rb +89 -0
- data/lib/active_ldap/associations.rb +162 -0
- data/lib/active_ldap/attributes.rb +199 -0
- data/lib/active_ldap/base.rb +1343 -0
- data/lib/active_ldap/callbacks.rb +19 -0
- data/lib/active_ldap/command.rb +46 -0
- data/lib/active_ldap/configuration.rb +96 -0
- data/lib/active_ldap/connection.rb +137 -0
- data/lib/{activeldap → active_ldap}/ldap.rb +1 -1
- data/lib/active_ldap/object_class.rb +70 -0
- data/lib/active_ldap/schema.rb +258 -0
- data/lib/{activeldap → active_ldap}/timeout.rb +0 -0
- data/lib/{activeldap → active_ldap}/timeout_stub.rb +0 -0
- data/lib/active_ldap/user_password.rb +92 -0
- data/lib/active_ldap/validations.rb +78 -0
- data/rails/plugin/active_ldap/README +54 -0
- data/rails/plugin/active_ldap/init.rb +6 -0
- data/test/TODO +2 -0
- data/test/al-test-utils.rb +337 -0
- data/test/command.rb +62 -0
- data/test/config.yaml +8 -0
- data/test/config.yaml.sample +6 -0
- data/test/run-test.rb +17 -0
- data/test/test-unit-ext.rb +2 -0
- data/test/test_associations.rb +334 -0
- data/test/test_attributes.rb +71 -0
- data/test/test_base.rb +345 -0
- data/test/test_base_per_instance.rb +32 -0
- data/test/test_bind.rb +53 -0
- data/test/test_callback.rb +35 -0
- data/test/test_connection.rb +38 -0
- data/test/test_connection_per_class.rb +50 -0
- data/test/test_find.rb +36 -0
- data/test/test_groupadd.rb +50 -0
- data/test/test_groupdel.rb +46 -0
- data/test/test_groupls.rb +107 -0
- data/test/test_groupmod.rb +51 -0
- data/test/test_lpasswd.rb +75 -0
- data/test/test_object_class.rb +32 -0
- data/test/test_reflection.rb +173 -0
- data/test/test_schema.rb +166 -0
- data/test/test_user.rb +209 -0
- data/test/test_user_password.rb +93 -0
- data/test/test_useradd-binary.rb +59 -0
- data/test/test_useradd.rb +55 -0
- data/test/test_userdel.rb +48 -0
- data/test/test_userls.rb +86 -0
- data/test/test_usermod-binary-add-time.rb +62 -0
- data/test/test_usermod-binary-add.rb +61 -0
- data/test/test_usermod-binary-del.rb +64 -0
- data/test/test_usermod-lang-add.rb +57 -0
- data/test/test_usermod.rb +56 -0
- data/test/test_validation.rb +38 -0
- metadata +94 -21
- data/lib/activeldap/associations.rb +0 -170
- data/lib/activeldap/base.rb +0 -1456
- data/lib/activeldap/configuration.rb +0 -59
- data/lib/activeldap/schema2.rb +0 -217
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'active_ldap/association/proxy'
|
2
|
+
|
3
|
+
module ActiveLdap
|
4
|
+
module Association
|
5
|
+
class BelongsTo < Proxy
|
6
|
+
def replace(entry)
|
7
|
+
if entry.nil?
|
8
|
+
@target = @owner[@options[:foreign_key_name]] = nil
|
9
|
+
else
|
10
|
+
@target = (Proxy === entry ? entry.target : entry)
|
11
|
+
unless entry.new_entry?
|
12
|
+
@owner[@options[:foreign_key_name]] = entry[primary_key]
|
13
|
+
end
|
14
|
+
@updated = true
|
15
|
+
end
|
16
|
+
|
17
|
+
loaded
|
18
|
+
entry
|
19
|
+
end
|
20
|
+
|
21
|
+
def updated?
|
22
|
+
@updated
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def have_foreign_key?
|
27
|
+
not @owner[@options[:foreign_key_name]].nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_target
|
31
|
+
filter = "(#{primary_key}=#{@owner[@options[:foreign_key_name]]})"
|
32
|
+
result = foreign_class.find(:all, :filter => filter, :limit => 1)
|
33
|
+
raise EntryNotFound if result.empty?
|
34
|
+
result.first
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'active_ldap/association/collection'
|
2
|
+
|
3
|
+
module ActiveLdap
|
4
|
+
module Association
|
5
|
+
class BelongsToMany < Collection
|
6
|
+
private
|
7
|
+
def insert_entry(entry)
|
8
|
+
old_value = entry[@options[:many], true]
|
9
|
+
new_value = old_value + @owner[@options[:foreign_key_name], true]
|
10
|
+
new_value = new_value.uniq.sort
|
11
|
+
if old_value != new_value
|
12
|
+
entry[@options[:many]] = new_value
|
13
|
+
entry.save
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete_entries(entries)
|
18
|
+
entries.each do |entry|
|
19
|
+
old_value = entry[@options[:many], true]
|
20
|
+
new_value = old_value - @owner[@options[:foreign_key_name], true]
|
21
|
+
new_value = new_value.uniq.sort
|
22
|
+
if old_value != new_value
|
23
|
+
entry[@options[:many]] = new_value
|
24
|
+
entry.save
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_target
|
30
|
+
key = @options[:many]
|
31
|
+
filter = @owner[@options[:foreign_key_name], true].reject do |value|
|
32
|
+
value.nil?
|
33
|
+
end.collect do |value|
|
34
|
+
"(#{key}=#{value})"
|
35
|
+
end.join
|
36
|
+
foreign_class.find(:all, :filter => "(|#{filter})")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'active_ldap/association/proxy'
|
2
|
+
|
3
|
+
module ActiveLdap
|
4
|
+
module Association
|
5
|
+
class Collection < Proxy
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def to_ary
|
9
|
+
load_target
|
10
|
+
@target.to_ary
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset
|
14
|
+
@target = []
|
15
|
+
@loaded = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def <<(*entries)
|
19
|
+
add_entries(*entries)
|
20
|
+
end
|
21
|
+
alias_method(:push, :<<)
|
22
|
+
alias_method(:concat, :<<)
|
23
|
+
|
24
|
+
def each(&block)
|
25
|
+
to_ary.each(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(*entries)
|
29
|
+
entries = flatten_deeper(entries).reject do |entry|
|
30
|
+
@target.delete(entry) if entry.new_entry?
|
31
|
+
entry.new_entry?
|
32
|
+
end
|
33
|
+
return if entries.empty?
|
34
|
+
|
35
|
+
delete_entries(entries)
|
36
|
+
entries.each do |entry|
|
37
|
+
@target.delete(entry)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def replace(others)
|
42
|
+
load_target
|
43
|
+
deleted_entries = @target - others
|
44
|
+
added_entries = others - @target
|
45
|
+
|
46
|
+
delete(deleted_entries)
|
47
|
+
concat(added_entries)
|
48
|
+
end
|
49
|
+
|
50
|
+
def exists?
|
51
|
+
load_target
|
52
|
+
not @target.empty?
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def flatten_deeper(array)
|
57
|
+
array.collect do |element|
|
58
|
+
element.respond_to?(:flatten) ? element.flatten : element
|
59
|
+
end.flatten
|
60
|
+
end
|
61
|
+
|
62
|
+
def insert_entry(entry)
|
63
|
+
entry[@options[:foreign_key_name]] = @owner[@options[:local_key_name]]
|
64
|
+
entry.save
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_entries(*entries)
|
68
|
+
result = true
|
69
|
+
load_target
|
70
|
+
|
71
|
+
flatten_deeper(entries).each do |entry|
|
72
|
+
result &&= insert_entry(entry) unless @owner.new_entry?
|
73
|
+
@target << entry
|
74
|
+
end
|
75
|
+
|
76
|
+
result && self
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_ldap/association/collection'
|
2
|
+
|
3
|
+
module ActiveLdap
|
4
|
+
module Association
|
5
|
+
class HasMany < Collection
|
6
|
+
private
|
7
|
+
def insert_entry(entry)
|
8
|
+
entry[primary_key] = @owner[@options[:foreign_key_name]]
|
9
|
+
entry.save
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_target
|
13
|
+
foreign_base_key = primary_key
|
14
|
+
filter = @owner[@options[:foreign_key_name], true].collect do |value|
|
15
|
+
key = val = nil
|
16
|
+
if foreign_base_key == "dn"
|
17
|
+
key, val = value.split(",")[0].split("=") unless value.empty?
|
18
|
+
else
|
19
|
+
key, val = foreign_base_key, value
|
20
|
+
end
|
21
|
+
[key, val]
|
22
|
+
end.reject do |key, val|
|
23
|
+
key.nil? or val.nil?
|
24
|
+
end.collect do |key, val|
|
25
|
+
"(#{key}=#{val})"
|
26
|
+
end.join
|
27
|
+
foreign_class.find(:all, :filter => "(|#{filter})")
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete_entries(entries)
|
31
|
+
key = primary_key
|
32
|
+
dn_attribute = foreign_class.dn_attribute
|
33
|
+
filter = @owner[@options[:foreign_key_name], true].reject do |value|
|
34
|
+
value.nil?
|
35
|
+
end.collect do |value|
|
36
|
+
"(#{key}=#{value})"
|
37
|
+
end.join
|
38
|
+
filter = "(&#{filter})"
|
39
|
+
entry_filter = entries.collect do |entry|
|
40
|
+
"(#{dn_attribute}=#{entry.id})"
|
41
|
+
end.join
|
42
|
+
entry_filter = "(|#{entry_filter})"
|
43
|
+
foreign_class.update_all({primary_key => []},
|
44
|
+
"(&#{filter}#{entry_filter})")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'active_ldap/association/collection'
|
2
|
+
|
3
|
+
module ActiveLdap
|
4
|
+
module Association
|
5
|
+
class HasManyWrap < Collection
|
6
|
+
private
|
7
|
+
def insert_entry(entry)
|
8
|
+
old_value = @owner[@options[:wrap], true]
|
9
|
+
new_value = (old_value + entry[primary_key, true]).uniq.sort
|
10
|
+
if old_value != new_value
|
11
|
+
@owner[@options[:wrap]] = new_value
|
12
|
+
@owner.save
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete_entries(entries)
|
17
|
+
old_value = @owner[@options[:wrap], true]
|
18
|
+
new_value = old_value - entries.collect {|entry| entry[primary_key]}
|
19
|
+
new_value = new_value.uniq.sort
|
20
|
+
if old_value != new_value
|
21
|
+
@owner[@options[:wrap]] = new_value
|
22
|
+
@owner.save
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_target
|
27
|
+
foreign_base_key = primary_key
|
28
|
+
requested_targets = @owner[@options[:wrap], true]
|
29
|
+
|
30
|
+
filter = requested_targets.collect do |value|
|
31
|
+
key = val = nil
|
32
|
+
if foreign_base_key == "dn"
|
33
|
+
key, val = value.split(",")[0].split("=") unless value.empty?
|
34
|
+
else
|
35
|
+
key, val = foreign_base_key, value
|
36
|
+
end
|
37
|
+
[key, val]
|
38
|
+
end.reject do |key, val|
|
39
|
+
key.nil? or val.nil?
|
40
|
+
end.collect do |key, val|
|
41
|
+
"(#{key}=#{val})"
|
42
|
+
end.join
|
43
|
+
|
44
|
+
klass = foreign_class
|
45
|
+
found_targets = {}
|
46
|
+
klass.find(:all, :filter => "(|#{filter})").each do |target|
|
47
|
+
found_targets[target.send(foreign_base_key)] ||= target
|
48
|
+
end
|
49
|
+
|
50
|
+
requested_targets.collect do |name|
|
51
|
+
found_targets[name] || klass.new(name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module ActiveLdap
|
2
|
+
module Association
|
3
|
+
class Proxy
|
4
|
+
alias_method :proxy_respond_to?, :respond_to?
|
5
|
+
alias_method :proxy_extend, :extend
|
6
|
+
|
7
|
+
def initialize(owner, options)
|
8
|
+
@owner = owner
|
9
|
+
@options = options
|
10
|
+
extend(options[:extend]) if options[:extend]
|
11
|
+
reset
|
12
|
+
end
|
13
|
+
|
14
|
+
def respond_to?(symbol, include_priv=false)
|
15
|
+
proxy_respond_to?(symbol, include_priv) or
|
16
|
+
(load_target && @target.respond_to?(symbol, include_priv))
|
17
|
+
end
|
18
|
+
|
19
|
+
def ===(other)
|
20
|
+
load_target and other === @target
|
21
|
+
end
|
22
|
+
|
23
|
+
def reset
|
24
|
+
@target = nil
|
25
|
+
@loaded = false
|
26
|
+
end
|
27
|
+
|
28
|
+
def reload
|
29
|
+
reset
|
30
|
+
load_target
|
31
|
+
end
|
32
|
+
|
33
|
+
def loaded?
|
34
|
+
@loaded
|
35
|
+
end
|
36
|
+
|
37
|
+
def loaded
|
38
|
+
@loaded = true
|
39
|
+
end
|
40
|
+
|
41
|
+
def target
|
42
|
+
@target
|
43
|
+
end
|
44
|
+
|
45
|
+
def target=(target)
|
46
|
+
@target = target
|
47
|
+
loaded
|
48
|
+
end
|
49
|
+
|
50
|
+
def exists?
|
51
|
+
load_target
|
52
|
+
not @target.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def method_missing(method, *args, &block)
|
57
|
+
load_target
|
58
|
+
@target.send(method, *args, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def foreign_class
|
62
|
+
klass = @owner.class.associated_class(@options[:association_id])
|
63
|
+
klass = @owner.class.module_eval(klass) if klass.is_a?(String)
|
64
|
+
klass
|
65
|
+
end
|
66
|
+
|
67
|
+
def have_foreign_key?
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def primary_key
|
72
|
+
@options[:primary_key_name] || foreign_class.dn_attribute
|
73
|
+
end
|
74
|
+
|
75
|
+
def load_target
|
76
|
+
if !@owner.new_entry? or have_foreign_key?
|
77
|
+
begin
|
78
|
+
@target = find_target unless loaded?
|
79
|
+
rescue EntryNotFound
|
80
|
+
reset
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
loaded if target
|
85
|
+
target
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'active_ldap/association/belongs_to'
|
2
|
+
require 'active_ldap/association/belongs_to_many'
|
3
|
+
require 'active_ldap/association/has_many'
|
4
|
+
require 'active_ldap/association/has_many_wrap'
|
5
|
+
|
6
|
+
module ActiveLdap
|
7
|
+
# Associations
|
8
|
+
#
|
9
|
+
# Associations provides the class methods needed for
|
10
|
+
# the extension classes to create methods using
|
11
|
+
# belongs_to and has_many
|
12
|
+
module Associations
|
13
|
+
def self.append_features(base)
|
14
|
+
super
|
15
|
+
base.extend(ClassMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def set_associated_class(name, klass)
|
20
|
+
@associated_classes ||= {}
|
21
|
+
@associated_classes[name.to_s] = klass
|
22
|
+
end
|
23
|
+
|
24
|
+
def associated_class(name)
|
25
|
+
@associated_classes[name.to_s]
|
26
|
+
end
|
27
|
+
|
28
|
+
# belongs_to
|
29
|
+
#
|
30
|
+
# This defines a method for an extension class map its DN key
|
31
|
+
# attribute value on to multiple items which reference it by
|
32
|
+
# |:foreign_key| in the other LDAP entry covered by class |:class_name|.
|
33
|
+
#
|
34
|
+
# Example:
|
35
|
+
# belongs_to :groups, :class_name => "Group",
|
36
|
+
# :many => "memberUid" # Group#memberUid
|
37
|
+
# # :foreign_key => "uid" # User#uid
|
38
|
+
# # dn attribute value is used by default
|
39
|
+
# belongs_to :primary_group, :class_name => "Group",
|
40
|
+
# :foreign_key => "gidNumber", # User#gidNumber
|
41
|
+
# :primary_key => "gidNumber" # Group#gidNumber
|
42
|
+
#
|
43
|
+
def belongs_to(association_id, options={})
|
44
|
+
validate_belongs_to_options(options)
|
45
|
+
klass = options[:class] || Inflector.classify(association_id)
|
46
|
+
foreign_key = options[:foreign_key]
|
47
|
+
primary_key = options[:primary_key]
|
48
|
+
many = options[:many]
|
49
|
+
set_associated_class(association_id, klass)
|
50
|
+
|
51
|
+
opts = {
|
52
|
+
:association_id => association_id,
|
53
|
+
:foreign_key_name => foreign_key,
|
54
|
+
:primary_key_name => primary_key,
|
55
|
+
:many => many,
|
56
|
+
:extend => options[:extend],
|
57
|
+
}
|
58
|
+
if opts[:many]
|
59
|
+
association_class = Association::BelongsToMany
|
60
|
+
opts[:foreign_key_name] ||= dn_attribute
|
61
|
+
else
|
62
|
+
association_class = Association::BelongsTo
|
63
|
+
opts[:foreign_key_name] ||= "#{association_id}_id"
|
64
|
+
|
65
|
+
before_save(<<-EOC)
|
66
|
+
if defined?(@#{association_id})
|
67
|
+
association = @#{association_id}
|
68
|
+
if association and association.updated?
|
69
|
+
self[association.__send__(:primary_key)] =
|
70
|
+
association[#{opts[:foreign_key_name].dump}]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
EOC
|
74
|
+
end
|
75
|
+
|
76
|
+
association_accessor(association_id) do |target|
|
77
|
+
association_class.new(target, opts)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# has_many
|
83
|
+
#
|
84
|
+
# This defines a method for an extension class expand an
|
85
|
+
# existing multi-element attribute into ActiveLdap objects.
|
86
|
+
# This discards any calls which result in entries that
|
87
|
+
# don't exist in LDAP!
|
88
|
+
#
|
89
|
+
# Example:
|
90
|
+
# has_many :primary_members, :class_name => "User",
|
91
|
+
# :primary_key => "gidNumber", # User#gidNumber
|
92
|
+
# :foreign_key => "gidNumber" # Group#gidNumber
|
93
|
+
# has_many :members, :class_name => "User",
|
94
|
+
# :wrap => "memberUid" # Group#memberUid
|
95
|
+
def has_many(association_id, options = {})
|
96
|
+
validate_has_many_options(options)
|
97
|
+
klass = options[:class] || Inflector.classify(association_id)
|
98
|
+
foreign_key = options[:foreign_key] || association_id.to_s + "_id"
|
99
|
+
primary_key = options[:primary_key]
|
100
|
+
set_associated_class(association_id, klass)
|
101
|
+
|
102
|
+
opts = {
|
103
|
+
:association_id => association_id,
|
104
|
+
:foreign_key_name => foreign_key,
|
105
|
+
:primary_key_name => primary_key,
|
106
|
+
:wrap => options[:wrap],
|
107
|
+
:extend => options[:extend],
|
108
|
+
}
|
109
|
+
if opts[:wrap]
|
110
|
+
association_class = Association::HasManyWrap
|
111
|
+
else
|
112
|
+
association_class = Association::HasMany
|
113
|
+
end
|
114
|
+
|
115
|
+
association_accessor(association_id) do |target|
|
116
|
+
association_class.new(target, opts)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
def association_accessor(name, &make_association)
|
122
|
+
define_method("__make_#{name}") do
|
123
|
+
make_association.call(self)
|
124
|
+
end
|
125
|
+
association_reader(name, &make_association)
|
126
|
+
association_writer(name, &make_association)
|
127
|
+
end
|
128
|
+
|
129
|
+
def association_reader(name, &make_association)
|
130
|
+
class_eval(<<-EOM, __FILE__, __LINE__ + 1)
|
131
|
+
def #{name}
|
132
|
+
@#{name} ||= __make_#{name}
|
133
|
+
end
|
134
|
+
EOM
|
135
|
+
end
|
136
|
+
|
137
|
+
def association_writer(name, &make_association)
|
138
|
+
class_eval(<<-EOM, __FILE__, __LINE__ + 1)
|
139
|
+
def #{name}=(new_value)
|
140
|
+
association = defined?(@#{name}) ? @#{name} : nil
|
141
|
+
association ||= __make_#{name}
|
142
|
+
association.replace(new_value)
|
143
|
+
@#{name} = new_value.nil? ? nil : association
|
144
|
+
@#{name}
|
145
|
+
end
|
146
|
+
EOM
|
147
|
+
end
|
148
|
+
|
149
|
+
VALID_BELONGS_TO_OPTIONS = [:class, :foreign_key, :primary_key, :many,
|
150
|
+
:extend]
|
151
|
+
def validate_belongs_to_options(options)
|
152
|
+
options.assert_valid_keys(VALID_BELONGS_TO_OPTIONS)
|
153
|
+
end
|
154
|
+
|
155
|
+
VALID_HAS_MANY_OPTIONS = [:class, :foreign_key, :primary_key, :wrap,
|
156
|
+
:extend]
|
157
|
+
def validate_has_many_options(options)
|
158
|
+
options.assert_valid_keys(VALID_HAS_MANY_OPTIONS)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|