treequel 1.2.2 → 1.3.0pre384
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.
- data.tar.gz.sig +0 -0
- data/ChangeLog +3374 -0
- data/History.md +39 -0
- data/LICENSE +27 -0
- data/README.md +25 -2
- data/Rakefile +64 -29
- data/bin/treequel +16 -25
- data/bin/treewhat +318 -0
- data/lib/treequel.rb +13 -3
- data/lib/treequel/behavior/control.rb +40 -0
- data/lib/treequel/branch.rb +56 -28
- data/lib/treequel/branchset.rb +10 -2
- data/lib/treequel/controls/pagedresults.rb +4 -2
- data/lib/treequel/directory.rb +40 -50
- data/lib/treequel/exceptions.rb +18 -0
- data/lib/treequel/mixins.rb +44 -14
- data/lib/treequel/model.rb +338 -21
- data/lib/treequel/model/errors.rb +79 -0
- data/lib/treequel/model/objectclass.rb +26 -2
- data/lib/treequel/model/schemavalidations.rb +69 -0
- data/lib/treequel/monkeypatches.rb +99 -17
- data/lib/treequel/schema.rb +19 -5
- data/spec/lib/constants.rb +20 -2
- data/spec/lib/helpers.rb +25 -8
- data/spec/treequel/branch_spec.rb +73 -10
- data/spec/treequel/controls/contentsync_spec.rb +2 -11
- data/spec/treequel/controls/pagedresults_spec.rb +25 -9
- data/spec/treequel/controls/sortedresults_spec.rb +8 -10
- data/spec/treequel/directory_spec.rb +74 -63
- data/spec/treequel/model/errors_spec.rb +77 -0
- data/spec/treequel/model/objectclass_spec.rb +107 -35
- data/spec/treequel/model/schemavalidations_spec.rb +112 -0
- data/spec/treequel/model_spec.rb +294 -81
- data/spec/treequel/monkeypatches_spec.rb +49 -3
- metadata +28 -16
- metadata.gz.sig +0 -0
- data/spec/lib/control_behavior.rb +0 -47
data/lib/treequel/exceptions.rb
CHANGED
@@ -26,6 +26,24 @@ module Treequel
|
|
26
26
|
### other problem.
|
27
27
|
class ModelError < Treequel::Error; end
|
28
28
|
|
29
|
+
### Exception class raised when +raise_on_save_failure+ is set and validation fails
|
30
|
+
class ValidationFailed < Treequel::ModelError
|
31
|
+
|
32
|
+
### Create a new Treequel::ValidationFailed exception with the given +errors+.
|
33
|
+
### @param [Treequel::Model::Errors, String] errors the validaton errors
|
34
|
+
def initialize( errors )
|
35
|
+
if errors.respond_to?( :full_messages )
|
36
|
+
@errors = errors
|
37
|
+
super( errors.full_messages.join(', ') )
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Treequel::Model::Errors] the validation errors
|
44
|
+
attr_reader :errors
|
45
|
+
end
|
46
|
+
|
29
47
|
end # module Treequel
|
30
48
|
|
31
49
|
|
data/lib/treequel/mixins.rb
CHANGED
@@ -153,14 +153,6 @@ module Treequel
|
|
153
153
|
### Add logging to a Treequel class. Including classes get #log and #log_debug methods.
|
154
154
|
module Loggable
|
155
155
|
|
156
|
-
LEVEL = {
|
157
|
-
:debug => Logger::DEBUG,
|
158
|
-
:info => Logger::INFO,
|
159
|
-
:warn => Logger::WARN,
|
160
|
-
:error => Logger::ERROR,
|
161
|
-
:fatal => Logger::FATAL,
|
162
|
-
}
|
163
|
-
|
164
156
|
### A logging proxy class that wraps calls to the logger into calls that include
|
165
157
|
### the name of the calling class.
|
166
158
|
### @private
|
@@ -172,13 +164,34 @@ module Treequel
|
|
172
164
|
@force_debug = force_debug
|
173
165
|
end
|
174
166
|
|
175
|
-
### Delegate
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
167
|
+
### Delegate debug messages to the global logger with the appropriate class name.
|
168
|
+
def debug( msg=nil, &block )
|
169
|
+
Treequel.logger.add( Logger::DEBUG, msg, @classname, &block )
|
170
|
+
end
|
171
|
+
|
172
|
+
### Delegate info messages to the global logger with the appropriate class name.
|
173
|
+
def info( msg=nil, &block )
|
174
|
+
return self.debug( msg, &block ) if @force_debug
|
175
|
+
Treequel.logger.add( Logger::INFO, msg, @classname, &block )
|
176
|
+
end
|
177
|
+
|
178
|
+
### Delegate warn messages to the global logger with the appropriate class name.
|
179
|
+
def warn( msg=nil, &block )
|
180
|
+
return self.debug( msg, &block ) if @force_debug
|
181
|
+
Treequel.logger.add( Logger::WARN, msg, @classname, &block )
|
182
|
+
end
|
183
|
+
|
184
|
+
### Delegate error messages to the global logger with the appropriate class name.
|
185
|
+
def error( msg=nil, &block )
|
186
|
+
return self.debug( msg, &block ) if @force_debug
|
187
|
+
Treequel.logger.add( Logger::ERROR, msg, @classname, &block )
|
188
|
+
end
|
189
|
+
|
190
|
+
### Delegate fatal messages to the global logger with the appropriate class name.
|
191
|
+
def fatal( msg=nil, &block )
|
192
|
+
Treequel.logger.add( Logger::FATAL, msg, @classname, &block )
|
181
193
|
end
|
194
|
+
|
182
195
|
end # ClassNameProxy
|
183
196
|
|
184
197
|
#########
|
@@ -200,6 +213,7 @@ module Treequel
|
|
200
213
|
def log_debug
|
201
214
|
@log_debug_proxy ||= ClassNameProxy.new( self.class, true )
|
202
215
|
end
|
216
|
+
|
203
217
|
end # module Loggable
|
204
218
|
|
205
219
|
|
@@ -271,6 +285,22 @@ module Treequel
|
|
271
285
|
end
|
272
286
|
end
|
273
287
|
|
288
|
+
### Normalize the attributes in +hash+ to be of the form expected by the
|
289
|
+
### LDAP library (i.e., keys as Strings, values as Arrays of Strings)
|
290
|
+
def normalize_attributes( hash )
|
291
|
+
normhash = {}
|
292
|
+
hash.each do |key,val|
|
293
|
+
val = [ val ] unless val.is_a?( Array )
|
294
|
+
val.collect! {|obj| obj.to_s }
|
295
|
+
|
296
|
+
normhash[ key.to_s ] = val
|
297
|
+
end
|
298
|
+
|
299
|
+
normhash.delete( 'dn' )
|
300
|
+
|
301
|
+
return normhash
|
302
|
+
end
|
303
|
+
|
274
304
|
end # HashUtilities
|
275
305
|
|
276
306
|
|
data/lib/treequel/model.rb
CHANGED
@@ -10,12 +10,15 @@ require 'treequel/branchset'
|
|
10
10
|
|
11
11
|
# An object interface to LDAP entries.
|
12
12
|
class Treequel::Model < Treequel::Branch
|
13
|
+
require 'treequel/model/objectclass'
|
14
|
+
require 'treequel/model/errors'
|
15
|
+
require 'treequel/model/schemavalidations'
|
16
|
+
|
13
17
|
include Treequel::Loggable,
|
14
18
|
Treequel::Constants,
|
15
19
|
Treequel::Normalization,
|
16
|
-
Treequel::Constants::Patterns
|
17
|
-
|
18
|
-
require 'treequel/model/objectclass'
|
20
|
+
Treequel::Constants::Patterns,
|
21
|
+
Treequel::Model::SchemaValidations
|
19
22
|
|
20
23
|
|
21
24
|
# A prototype Hash that autovivifies its members as Sets, for use in
|
@@ -23,6 +26,34 @@ class Treequel::Model < Treequel::Branch
|
|
23
26
|
SET_HASH = Hash.new {|h,k| h[k] = Set.new }
|
24
27
|
|
25
28
|
|
29
|
+
# The hooks that are called before an action
|
30
|
+
BEFORE_HOOKS = [
|
31
|
+
:before_create,
|
32
|
+
:before_update,
|
33
|
+
:before_save,
|
34
|
+
:before_destroy,
|
35
|
+
:before_validation,
|
36
|
+
]
|
37
|
+
|
38
|
+
# The hooks that are called after an action
|
39
|
+
AFTER_HOOKS = [
|
40
|
+
:after_initialize,
|
41
|
+
:after_create,
|
42
|
+
:after_update,
|
43
|
+
:after_save,
|
44
|
+
:after_destroy,
|
45
|
+
:after_validation,
|
46
|
+
]
|
47
|
+
|
48
|
+
# Hooks the user can override
|
49
|
+
HOOKS = BEFORE_HOOKS + AFTER_HOOKS
|
50
|
+
|
51
|
+
# Defaults for #validate options
|
52
|
+
DEFAULT_VALIDATION_OPTIONS = {
|
53
|
+
:with_schema => true,
|
54
|
+
}
|
55
|
+
|
56
|
+
|
26
57
|
#################################################################
|
27
58
|
### C L A S S M E T H O D S
|
28
59
|
#################################################################
|
@@ -131,6 +162,25 @@ class Treequel::Model < Treequel::Branch
|
|
131
162
|
end
|
132
163
|
|
133
164
|
|
165
|
+
### Never freeze converted values in Model objects.
|
166
|
+
def self::freeze_converted_values?; false; end
|
167
|
+
|
168
|
+
|
169
|
+
### Create a new Treequel::Model object with the given +entry+ hash from the
|
170
|
+
### specified +directory+. Overrides Treequel::Branch.new_from_entry to pass the
|
171
|
+
### +from_directory+ flag to mark it as unmodified.
|
172
|
+
###
|
173
|
+
### @see Treequel::Branch.new_from_entry
|
174
|
+
def self::new_from_entry( entry, directory )
|
175
|
+
entry = Treequel::HashUtilities.stringify_keys( entry )
|
176
|
+
dnvals = entry.delete( 'dn' ) or
|
177
|
+
raise ArgumentError, "no 'dn' attribute for entry"
|
178
|
+
|
179
|
+
Treequel.logger.debug "Creating %p from entry: %p in directory: %s" %
|
180
|
+
[ self, dnvals.first, directory ]
|
181
|
+
return self.new( directory, dnvals.first, entry, true )
|
182
|
+
end
|
183
|
+
|
134
184
|
|
135
185
|
#################################################################
|
136
186
|
### I N S T A N C E M E T H O D S
|
@@ -138,9 +188,25 @@ class Treequel::Model < Treequel::Branch
|
|
138
188
|
|
139
189
|
### Override the default to extend new instances with applicable mixins if their
|
140
190
|
### entry is set.
|
141
|
-
def initialize(
|
191
|
+
def initialize( directory, dn, entry=nil, from_directory=false )
|
192
|
+
if from_directory
|
193
|
+
super( directory, dn, entry )
|
194
|
+
else
|
195
|
+
super( directory, dn )
|
196
|
+
@values = entry ? symbolify_keys( entry ) : {}
|
197
|
+
@dirty = true
|
198
|
+
end
|
199
|
+
|
200
|
+
self.apply_applicable_mixins( @dn, @entry )
|
201
|
+
self.after_initialize
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
### Copy initializer -- re-apply mixins to duplicates, too.
|
206
|
+
def initialize_copy( other )
|
142
207
|
super
|
143
|
-
self.apply_applicable_mixins( @dn, @entry )
|
208
|
+
self.apply_applicable_mixins( @dn, @entry )
|
209
|
+
self.after_initialize
|
144
210
|
end
|
145
211
|
|
146
212
|
|
@@ -148,6 +214,229 @@ class Treequel::Model < Treequel::Branch
|
|
148
214
|
public
|
149
215
|
######
|
150
216
|
|
217
|
+
### Set up the empty hook methods
|
218
|
+
HOOKS.each do |hook|
|
219
|
+
define_method( hook ) do |*args|
|
220
|
+
self.log.debug "#{hook} default hook called."
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
### Tests whether the object has been modified since it was loaded from
|
226
|
+
### the directory.
|
227
|
+
def modified?
|
228
|
+
return @dirty ? true : false
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
### Mark the object as unmodified.
|
233
|
+
def reset_dirty_flag
|
234
|
+
@dirty = false
|
235
|
+
end
|
236
|
+
|
237
|
+
|
238
|
+
### Index set operator -- set attribute +attrname+ to a new +value+.
|
239
|
+
### Overridden to make Model objects defer writing changes until
|
240
|
+
### {Treequel::Model#save} is called.
|
241
|
+
###
|
242
|
+
### @param [Symbol, String] attrname attribute name
|
243
|
+
### @param [Object] value the attribute value
|
244
|
+
def []=( attrname, value )
|
245
|
+
attrtype = self.find_attribute_type( attrname.to_sym ) or
|
246
|
+
raise ArgumentError, "unknown attribute %p" % [ attrname ]
|
247
|
+
value = Array( value ) unless attrtype.single?
|
248
|
+
|
249
|
+
self.mark_dirty
|
250
|
+
@values[ attrtype.name.to_sym ] = value
|
251
|
+
|
252
|
+
# If the objectClasses change, we (may) need to re-apply mixins
|
253
|
+
if attrname.to_s.downcase == 'objectclass'
|
254
|
+
self.log.debug " objectClass change -- reapplying mixins"
|
255
|
+
self.apply_applicable_mixins( self.dn )
|
256
|
+
else
|
257
|
+
self.log.debug " no objectClass changes -- no need to reapply mixins"
|
258
|
+
end
|
259
|
+
|
260
|
+
return value
|
261
|
+
end
|
262
|
+
|
263
|
+
|
264
|
+
### Make the changes to the object specified by the given +attributes+.
|
265
|
+
### Overridden to make Model objects defer writing changes until
|
266
|
+
### {Treequel::Model#save} is called.
|
267
|
+
###
|
268
|
+
### @param attributes (see Treequel::Directory#modify)
|
269
|
+
### @return [TrueClass] if the merge succeeded
|
270
|
+
def merge( attributes )
|
271
|
+
attributes.each do |attrname, value|
|
272
|
+
self[ attrname ] = value
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
|
277
|
+
### Delete the specified attributes.
|
278
|
+
### Overridden to make Model objects defer writing changes until
|
279
|
+
### {Treequel::Model#save} is called.
|
280
|
+
###
|
281
|
+
### @see Treequel::Branch#delete
|
282
|
+
def delete( *attributes )
|
283
|
+
return super if attributes.empty?
|
284
|
+
|
285
|
+
self.log.debug "Deleting attributes: %p" % [ attributes ]
|
286
|
+
self.mark_dirty
|
287
|
+
|
288
|
+
attributes.flatten.each do |attribute|
|
289
|
+
|
290
|
+
# With a hash, delete each value for each key
|
291
|
+
if attribute.is_a?( Hash )
|
292
|
+
self.delete_specific_values( attribute )
|
293
|
+
|
294
|
+
# With an array of attributes to delete, replace
|
295
|
+
# MULTIPLE attribute types with an empty array, and SINGLE
|
296
|
+
# attribute types with nil
|
297
|
+
elsif attribute.respond_to?( :to_sym )
|
298
|
+
attrtype = self.find_attribute_type( attribute.to_sym )
|
299
|
+
if attrtype.single?
|
300
|
+
@values[ attribute.to_sym ] = nil
|
301
|
+
else
|
302
|
+
@values[ attribute.to_sym ] = []
|
303
|
+
end
|
304
|
+
else
|
305
|
+
raise ArgumentError,
|
306
|
+
"can't convert a %p to a Symbol or a Hash" % [ attribute.class ]
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
return true
|
311
|
+
end
|
312
|
+
|
313
|
+
|
314
|
+
### Returns the validation errors associated with this object.
|
315
|
+
### @see Treequel::Model::Errors.
|
316
|
+
def errors
|
317
|
+
return @errors ||= Treequel::Model::Errors.new
|
318
|
+
end
|
319
|
+
|
320
|
+
|
321
|
+
### Return +true+ if the model object passes all of its validations.
|
322
|
+
def valid?( opts={} )
|
323
|
+
self.errors.clear
|
324
|
+
|
325
|
+
return false if self.before_validation == false
|
326
|
+
self.validate
|
327
|
+
self.after_validation
|
328
|
+
|
329
|
+
return self.errors.empty? ? true : false
|
330
|
+
end
|
331
|
+
|
332
|
+
|
333
|
+
### Validate the object with the specified +options+.
|
334
|
+
### @param [Hash] options options for validation.
|
335
|
+
### @option options [Boolean] :with_schema whether or not to run the schema validations
|
336
|
+
def validate( options={} )
|
337
|
+
options = DEFAULT_VALIDATION_OPTIONS.merge( options )
|
338
|
+
|
339
|
+
self.errors.add( :objectClass, 'must have at least one' ) if self.object_classes.empty?
|
340
|
+
|
341
|
+
super( options )
|
342
|
+
end
|
343
|
+
|
344
|
+
|
345
|
+
### Write any pending changes in the model object to the directory.
|
346
|
+
def save
|
347
|
+
self.log.debug "Saving %s..." % [ self.dn ]
|
348
|
+
raise Treequel::ValidationFailed, self.errors unless self.valid?
|
349
|
+
self.log.debug " validation succeeded."
|
350
|
+
|
351
|
+
unless mods = self.modifications
|
352
|
+
self.log.debug " no modifications... no save necessary."
|
353
|
+
return false
|
354
|
+
end
|
355
|
+
|
356
|
+
self.log.debug " got %d modifications." % [ mods.length ]
|
357
|
+
self.before_save( mods ) or return nil
|
358
|
+
|
359
|
+
if self.exists?
|
360
|
+
self.log.debug " entry already exists: updating..."
|
361
|
+
self.before_update( mods ) or return nil
|
362
|
+
self.modify( mods )
|
363
|
+
self.after_update( mods )
|
364
|
+
|
365
|
+
else
|
366
|
+
self.log.debug " entry doesn't exist: creating..."
|
367
|
+
self.before_create( mods ) or return nil
|
368
|
+
self.create( mods )
|
369
|
+
self.after_create( mods )
|
370
|
+
end
|
371
|
+
|
372
|
+
self.after_save( mods )
|
373
|
+
|
374
|
+
return true
|
375
|
+
end
|
376
|
+
|
377
|
+
|
378
|
+
### Return any pending changes in the model object.
|
379
|
+
### @return [Array<LDAP::Mod>] the changes as LDAP modifications
|
380
|
+
def modifications
|
381
|
+
return unless self.modified?
|
382
|
+
self.log.debug "Gathering modifications..."
|
383
|
+
|
384
|
+
mods = []
|
385
|
+
entry = self.entry || {}
|
386
|
+
self.log.debug " directory entry is: %p" % [ entry ]
|
387
|
+
|
388
|
+
@values.sort_by {|k, _| k.to_s }.each do |attribute, vals|
|
389
|
+
vals = [ vals ] unless vals.is_a?( Array )
|
390
|
+
vals = vals.compact
|
391
|
+
vals.collect! {|val| self.get_converted_attribute(attribute, val) }
|
392
|
+
self.log.debug " comparing %s values to entry: %p vs. %p" %
|
393
|
+
[ attribute, vals, entry[attribute.to_s] ]
|
394
|
+
|
395
|
+
entryvals = (entry[attribute.to_s] || [])
|
396
|
+
attrmods = { :add => [], :delete => [] }
|
397
|
+
|
398
|
+
Diff::LCS.sdiff( entryvals.sort, vals.sort ) do |change|
|
399
|
+
self.log.debug " found a change: %p" % [ change ]
|
400
|
+
if change.adding?
|
401
|
+
attrmods[:add] << change.new_element
|
402
|
+
elsif change.changed?
|
403
|
+
attrmods[:add] << change.new_element
|
404
|
+
attrmods[:delete] << change.old_element
|
405
|
+
elsif change.deleting?
|
406
|
+
attrmods[:delete] << change.old_element
|
407
|
+
# else
|
408
|
+
# self.log.debug " no mod necessary for %p" % [ change.action ]
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
mods << LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, attribute.to_s, attrmods[:delete] ) unless
|
413
|
+
attrmods[:delete].empty?
|
414
|
+
mods << LDAP::Mod.new( LDAP::LDAP_MOD_ADD, attribute.to_s, attrmods[:add] ) unless
|
415
|
+
attrmods[:add].empty?
|
416
|
+
end
|
417
|
+
|
418
|
+
self.log.debug " mods are: %p" % [ mods ]
|
419
|
+
|
420
|
+
return mods
|
421
|
+
end
|
422
|
+
|
423
|
+
|
424
|
+
### Return the pending modifications for the object as an LDIF string.
|
425
|
+
def modification_ldif
|
426
|
+
mods = self.modifications
|
427
|
+
return LDAP::LDIF.mods_to_ldif( self.dn, mods )
|
428
|
+
end
|
429
|
+
|
430
|
+
|
431
|
+
### Revert to the attributes in the directory, discarding any pending changes.
|
432
|
+
def revert
|
433
|
+
self.clear_caches
|
434
|
+
@dirty = false
|
435
|
+
|
436
|
+
return true
|
437
|
+
end
|
438
|
+
|
439
|
+
|
151
440
|
### Override Branch#search to inject the 'objectClass' attribute to the
|
152
441
|
### selected attribute list if there is one.
|
153
442
|
def search( scope=:subtree, filter='(objectClass=*)', parameters={}, &block )
|
@@ -197,6 +486,29 @@ class Treequel::Model < Treequel::Branch
|
|
197
486
|
protected
|
198
487
|
#########
|
199
488
|
|
489
|
+
|
490
|
+
### Mark the object as having been modified.
|
491
|
+
def mark_dirty
|
492
|
+
@dirty = true
|
493
|
+
end
|
494
|
+
|
495
|
+
|
496
|
+
### Delete specific key/value +pairs+ from the entry.
|
497
|
+
### @param [Hash] pairs key/value pairs to delete from the entry.
|
498
|
+
def delete_specific_values( pairs )
|
499
|
+
self.log.debug " hash-delete..."
|
500
|
+
|
501
|
+
# Ensure the value exists, and its values converted and cached, as
|
502
|
+
# the delete needs Ruby object instead of string comparison
|
503
|
+
pairs.each do |key, vals|
|
504
|
+
next unless self[ key ]
|
505
|
+
self.log.debug " deleting %p: %p" % [ key, vals ]
|
506
|
+
|
507
|
+
@values[ key ].delete_if {|val| vals.include?(val) }
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
|
200
512
|
### Search for the Treequel::Schema::AttributeType associated with +sym+.
|
201
513
|
###
|
202
514
|
### @param [Symbol,String] name the name of the attribute to find
|
@@ -205,7 +517,7 @@ class Treequel::Model < Treequel::Branch
|
|
205
517
|
def find_attribute_type( name )
|
206
518
|
attrtype = nil
|
207
519
|
|
208
|
-
#
|
520
|
+
# Try both the name as-is, and the camelCased version of it
|
209
521
|
camelcased_sym = name.to_s.gsub( /_(\w)/ ) { $1.upcase }.to_sym
|
210
522
|
attrtype = self.valid_attribute_type( name ) ||
|
211
523
|
self.valid_attribute_type( camelcased_sym )
|
@@ -268,13 +580,13 @@ class Treequel::Model < Treequel::Branch
|
|
268
580
|
def make_reader( attrtype )
|
269
581
|
self.log.debug "Generating an attribute reader for %p" % [ attrtype ]
|
270
582
|
attrname = attrtype.name
|
271
|
-
return lambda
|
583
|
+
return lambda do |*args|
|
272
584
|
if args.empty?
|
273
585
|
self[ attrname ]
|
274
586
|
else
|
275
587
|
self.traverse_branch( attrname, *args )
|
276
588
|
end
|
277
|
-
|
589
|
+
end
|
278
590
|
end
|
279
591
|
|
280
592
|
|
@@ -329,30 +641,35 @@ class Treequel::Model < Treequel::Branch
|
|
329
641
|
|
330
642
|
|
331
643
|
### Apply mixins that are applicable considering the receiver's DN and the
|
332
|
-
### objectClasses from
|
333
|
-
def apply_applicable_mixins( dn, entry )
|
334
|
-
|
644
|
+
### objectClasses from the given entry merged with any unsaved values.
|
645
|
+
def apply_applicable_mixins( dn, entry=nil )
|
646
|
+
entry ||= {}
|
647
|
+
entry.merge!( stringify_keys(@values) )
|
648
|
+
return unless entry['objectClass']
|
649
|
+
|
650
|
+
# self.log.debug "Applying mixins applicable to %s" % [ dn ]
|
335
651
|
schema = self.directory.schema
|
336
652
|
|
653
|
+
# self.log.debug " entry is: %p" % [ entry ]
|
337
654
|
ocs = entry['objectClass'].collect do |oc_oid|
|
338
655
|
explicit_oc = schema.object_classes[ oc_oid ]
|
339
656
|
explicit_oc.ancestors.collect {|oc| oc.name }
|
340
657
|
end.flatten.uniq
|
341
|
-
self.log.debug " got %d candidate objectClasses: %p" % [ ocs.length, ocs ]
|
658
|
+
# self.log.debug " got %d candidate objectClasses: %p" % [ ocs.length, ocs ]
|
342
659
|
|
343
660
|
oc_mixins = self.class.mixins_for_objectclasses( *ocs )
|
344
661
|
dn_mixins = self.class.mixins_for_dn( dn )
|
345
|
-
self.log.debug " found %d mixins by objectclass (%s), and %d by base (%s)" % [
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
]
|
662
|
+
# self.log.debug " found %d mixins by objectclass (%s), and %d by base (%s)" % [
|
663
|
+
# oc_mixins.length,
|
664
|
+
# oc_mixins.map(&:name).join(', '),
|
665
|
+
# dn_mixins.length,
|
666
|
+
# dn_mixins.map(&:name).join(', ')
|
667
|
+
# ]
|
351
668
|
|
352
669
|
# The applicable mixins are those in the intersection of the ones
|
353
670
|
# inferred by its objectclasses and those that apply to its DN
|
354
671
|
mixins = ( oc_mixins & dn_mixins )
|
355
|
-
self.log.debug " %d mixins remain after intersection: %p" % [ mixins.length, mixins ]
|
672
|
+
# self.log.debug " %d mixins remain after intersection: %p" % [ mixins.length, mixins ]
|
356
673
|
|
357
674
|
mixins.each {|mod| self.extend(mod) }
|
358
675
|
end
|
@@ -369,9 +686,9 @@ class Treequel::Model < Treequel::Branch
|
|
369
686
|
def attribute_from_method( methodname )
|
370
687
|
|
371
688
|
case methodname.to_s
|
372
|
-
when /^(?:has_)?([a-z]\w
|
689
|
+
when /^(?:has_)?([a-z]\w*)\?$/i
|
373
690
|
return $1.to_sym, :predicate
|
374
|
-
when /^([a-z]\w
|
691
|
+
when /^([a-z]\w*)(=)?$/i
|
375
692
|
return $1.to_sym, ($2 ? :writer : :reader )
|
376
693
|
end
|
377
694
|
end
|