treequel 1.0.4 → 1.1.0

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.
Files changed (53) hide show
  1. data/ChangeLog +130 -1
  2. data/Rakefile +8 -3
  3. data/Rakefile.local +2 -0
  4. data/bin/treeirb +6 -2
  5. data/bin/treequel +5 -4
  6. data/lib/treequel/branch.rb +133 -72
  7. data/lib/treequel/branchcollection.rb +16 -5
  8. data/lib/treequel/branchset.rb +37 -6
  9. data/lib/treequel/constants.rb +12 -0
  10. data/lib/treequel/directory.rb +96 -41
  11. data/lib/treequel/filter.rb +42 -1
  12. data/lib/treequel/model/objectclass.rb +111 -0
  13. data/lib/treequel/model.rb +363 -0
  14. data/lib/treequel/monkeypatches.rb +65 -0
  15. data/lib/treequel/schema/attributetype.rb +108 -18
  16. data/lib/treequel/schema/ldapsyntax.rb +15 -0
  17. data/lib/treequel/schema/matchingrule.rb +24 -0
  18. data/lib/treequel/schema/matchingruleuse.rb +24 -0
  19. data/lib/treequel/schema/objectclass.rb +70 -5
  20. data/lib/treequel/schema/table.rb +5 -15
  21. data/lib/treequel/schema.rb +64 -1
  22. data/lib/treequel.rb +5 -5
  23. data/rake/documentation.rb +27 -0
  24. data/rake/hg.rb +14 -2
  25. data/rake/manual.rb +1 -1
  26. data/spec/lib/constants.rb +9 -7
  27. data/spec/lib/control_behavior.rb +1 -0
  28. data/spec/lib/matchers.rb +1 -0
  29. data/spec/treequel/branch_spec.rb +229 -20
  30. data/spec/treequel/branchcollection_spec.rb +73 -39
  31. data/spec/treequel/branchset_spec.rb +59 -8
  32. data/spec/treequel/control_spec.rb +1 -0
  33. data/spec/treequel/controls/contentsync_spec.rb +1 -0
  34. data/spec/treequel/controls/pagedresults_spec.rb +1 -0
  35. data/spec/treequel/controls/sortedresults_spec.rb +1 -0
  36. data/spec/treequel/directory_spec.rb +46 -10
  37. data/spec/treequel/filter_spec.rb +28 -5
  38. data/spec/treequel/mixins_spec.rb +7 -14
  39. data/spec/treequel/model/objectclass_spec.rb +330 -0
  40. data/spec/treequel/model_spec.rb +433 -0
  41. data/spec/treequel/monkeypatches_spec.rb +118 -0
  42. data/spec/treequel/schema/attributetype_spec.rb +116 -0
  43. data/spec/treequel/schema/ldapsyntax_spec.rb +8 -0
  44. data/spec/treequel/schema/matchingrule_spec.rb +6 -1
  45. data/spec/treequel/schema/matchingruleuse_spec.rb +5 -0
  46. data/spec/treequel/schema/objectclass_spec.rb +41 -3
  47. data/spec/treequel/schema/table_spec.rb +31 -18
  48. data/spec/treequel/schema_spec.rb +13 -16
  49. data/spec/treequel_spec.rb +22 -0
  50. data.tar.gz.sig +1 -0
  51. metadata +40 -7
  52. metadata.gz.sig +0 -0
  53. data/spec/treequel/utils_spec.rb +0 -49
data/ChangeLog CHANGED
@@ -1,4 +1,133 @@
1
- 261[tip] 0188c2ba5b7e 2010-07-07 23:21 -0700 ged
1
+ 304[tip] 92c28b14730a 2010-09-20 13:51 -0700 ged
2
+ Added tag 1.1.0 for changeset b415e0fce774
3
+
4
+ 303[1.1.0] b415e0fce774 2010-09-20 13:51 -0700 ged
5
+ Added signature for changeset 4ba782a3a7e4
6
+
7
+ 302 4ba782a3a7e4 2010-09-17 09:09 -0700 ged
8
+ Include mixins for inherited objectClasses, too.
9
+
10
+ 301 f075a27a35eb 2010-09-17 09:03 -0700 ged
11
+ Updated build system
12
+
13
+ 300 c77bac0bf816 2010-09-17 09:02 -0700 ged
14
+ Manual updates for the Model section (paired with Mahlon)
15
+
16
+ 299 1b375c5dc123 2010-09-15 16:46 -0700 ged
17
+ Use the #syntax method instead of the syntax_oid in #inspect output so inherited syntaxes are shown
18
+
19
+ 298 0786411cd707 2010-09-15 16:43 -0700 ged
20
+ Use the system LDAP config if no URI is given
21
+
22
+ 297 d3a6c89d8004 2010-09-15 16:42 -0700 ged
23
+ Un-spam debugging on entry lookup
24
+
25
+ 296 2101482df41f 2010-09-15 16:41 -0700 ged
26
+ Overriding #inspect in Treequel::Model to show the list of extensions applied to the entry
27
+
28
+ 295 a384412c0e6f 2010-08-25 16:56 -0700 ged
29
+ Rescue the right error in Treequel::Model#method_missing and add a spec for the negative case.
30
+
31
+ 294 b220a83d31b5 2010-08-25 16:48 -0700 ged
32
+ Load the entry from Model's #method_missing to catch calls to methods that are added via
33
+
34
+ 293 2753f8405caf 2010-08-23 17:12 -0700 ged
35
+ Fixing object/attribute mapping for attributes whose types that inherit their syntax from their superclass
36
+
37
+ 292 2e2b035d3721 2010-08-23 15:19 -0700 ged
38
+ Coverage improvements, manual work
39
+
40
+ 291 693a163bd068 2010-08-23 08:36 -0700 ged
41
+ Started catching the manual up to 1.1.0.
42
+
43
+ 290 df0b0594cb62 2010-08-23 08:36 -0700 ged
44
+ Made Treequel::Branch#delete with no arguments delete the entry
45
+
46
+ 289 31fc0ea1ea31 2010-08-19 16:35 -0700 ged
47
+ Fixes for Ruby 1.9.2.
48
+
49
+ 288 7c7d3e2034b0 2010-08-19 07:21 -0700 ged
50
+ Build system update; fixed ruby-termios dependency
51
+
52
+ 287 f9151315d37f 2010-08-18 17:35 -0700 ged
53
+ Implement Branchset operators
54
+
55
+ 286 cd3ef7e3ffb0 2010-08-16 17:20 -0700 ged
56
+ Reworked operational attributes to use the 'USAGE' attribute at Mahlon's suggestion.
57
+
58
+ 285 dfb032f5e5c1 2010-08-16 17:19 -0700 ged
59
+ Fixes for Treequel::Model instantiation and lookup.
60
+
61
+ 284 844ee21c6916 2010-08-16 17:05 -0700 ged
62
+ Made Treequel::Schema::Table Enumerable
63
+
64
+ 283 ff2629196b4e 2010-08-06 16:11 -0700 ged
65
+ Don't add the objectClass attribute when searching through Treequel::Model if
66
+
67
+ 282 d4e58760ee34 2010-08-06 15:02 -0700 ged
68
+ Handle Sequel's Ruby1.9 Symbol-operator workaround in filter attributes
69
+
70
+ 281 d60c6b83db01 2010-08-06 15:02 -0700 ged
71
+ Also output empty-string attributes as non-binary
72
+
73
+ 280 bb182f705fe5 2010-08-05 14:07 -0700 ged
74
+ Unwrap base64ed LDIF lines before wrapping them to the new line length.
75
+
76
+ 279 32084630894f 2010-08-05 11:14 -0700 ged
77
+ Fix LDIF-generation for really reals.
78
+
79
+ 278 aabbab99b093 2010-08-05 10:40 -0700 ged
80
+ Optimizations, logging cleanup.
81
+
82
+ 277 7a4f2e5bef0a 2010-08-02 08:45 -0700 ged
83
+ Bugfixes.
84
+
85
+ 276 2f741e5294bf 2010-07-29 14:34 -0700 ged
86
+ Filter component symmetry, Model refactor
87
+
88
+ 275 bc0bc3aea136 2010-07-28 09:06 -0700 ged
89
+ Added support for Sequel-style #or: branchset.filter( :something ).or( :somethingelse ).
90
+
91
+ 274 30091794b910 2010-07-28 07:14 -0700 ged
92
+ Added Treequel::Model#respond_to?
93
+
94
+ 273 e9c908b1f426 2010-07-27 08:05 -0700 ged
95
+ Bugfix, make object conversion work for setting attributes too.
96
+
97
+ 272 30ba889041a8 2010-07-26 20:04 -0700 ged
98
+ Bugfixes, coverage of LDIF-generation.
99
+
100
+ 271 a6dd7b685a6a 2010-07-16 18:32 -0700 ged
101
+ Fixing the "cd .." special case
102
+
103
+ 270 2793fa802dad 2010-07-14 20:16 -0700 ged
104
+ Untaint objectClasses passed to Treequel::Model::mixins_for_objectclasses.
105
+
106
+ 269 b76dd9ca1f3f 2010-07-13 19:14 -0700 ged
107
+ The workaround for two-param syntax mapping Procs didn't work after all. Modifying
108
+
109
+ 268 70cc87a200ad 2010-07-13 19:08 -0700 ged
110
+ Added schema-object roundtripping, with a script/specs to test it.
111
+
112
+ 267 47e865bfef9e 2010-07-13 12:31 -0700 ged
113
+ Added the ability to set the default results class on a per-directory basis.
114
+
115
+ 266 c61373e3dc49 2010-07-13 08:03 -0700 ged
116
+ Fixes for Apache DS and bugfix in Treequel::Model.
117
+
118
+ 265 601003ff3077 2010-07-10 17:16 -0700 ged
119
+ Treequel::Model bugfix
120
+
121
+ 264 a76065cfeba0 2010-07-09 07:19 -0700 ged
122
+ Adding Treequel::Model, bumping version to 1.1.0.
123
+
124
+ 263:262,261 6dd6cb37f5ce 2010-07-09 07:16 -0700 ged
125
+ Merged with 47bb9cd7af3b
126
+
127
+ 262:258 3f4134d20d9d 2010-07-08 09:40 -0700 ged
128
+ Delegate the Treequel::Directory#root_dse method through its #conn
129
+
130
+ 261 0188c2ba5b7e 2010-07-07 23:21 -0700 ged
2
131
  Added tag 1.0.4 for changeset 0ec4ff0ce67f
3
132
 
4
133
  260[1.0.4] 0ec4ff0ce67f 2010-07-07 23:21 -0700 ged
data/Rakefile CHANGED
@@ -18,6 +18,7 @@ BEGIN {
18
18
  libdir = basedir + "lib"
19
19
  extdir = libdir + Config::CONFIG['sitearch']
20
20
 
21
+ $LOAD_PATH.unshift( basedir.to_s ) unless $LOAD_PATH.include?( basedir.to_s )
21
22
  $LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
22
23
  $LOAD_PATH.unshift( extdir.to_s ) unless $LOAD_PATH.include?( extdir.to_s )
23
24
  }
@@ -76,7 +77,7 @@ elsif VERSION_FILE.exist?
76
77
  PKG_VERSION = VERSION_FILE.read[ /VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/, 1 ]
77
78
  end
78
79
 
79
- PKG_VERSION = '0.0.0' unless defined?( PKG_VERSION )
80
+ PKG_VERSION = '0.0.0' unless defined?( PKG_VERSION ) && !PKG_VERSION.nil?
80
81
 
81
82
  PKG_FILE_NAME = "#{PKG_NAME.downcase}-#{PKG_VERSION}"
82
83
  GEM_FILE_NAME = "#{PKG_FILE_NAME}.gem"
@@ -169,7 +170,7 @@ include RakefileHelpers
169
170
 
170
171
  # Set the build ID if the mercurial executable is available
171
172
  if hg = which( 'hg' )
172
- id = IO.read('|-') or exec hg.to_s, 'id', '-n'
173
+ id = `#{hg} id -n`.chomp
173
174
  PKG_BUILD = "pre%03d" % [(id.chomp[ /^[[:xdigit:]]+/ ] || '1')]
174
175
  else
175
176
  PKG_BUILD = 'pre000'
@@ -226,9 +227,9 @@ DEVELOPMENT_DEPENDENCIES = {
226
227
  'text-format' => '>= 1.0.0',
227
228
  'tmail' => '>= 1.2.3.1',
228
229
  'diff-lcs' => '>= 1.1.2',
229
- 'termios' => '>= 0.9.4',
230
230
  'columnize' => '>= 0.3.1',
231
231
  'diff-lcs' => '>= 1.1.2',
232
+ 'ruby-termios' => '>= 0.9.6',
232
233
  'ruby-terminfo' => '>= 0.1.1',
233
234
  }
234
235
 
@@ -277,6 +278,10 @@ GEMSPEC = Gem::Specification.new do |gem|
277
278
  gem.files = RELEASE_FILES
278
279
  gem.test_files = SPEC_FILES
279
280
 
281
+ # signing key and certificate chain
282
+ gem.signing_key = '/Volumes/Keys/ged-private_gem_key.pem'
283
+ gem.cert_chain = [File.expand_path('~/.gem/ged-public_gem_cert.pem')]
284
+
280
285
  DEPENDENCIES.each do |name, version|
281
286
  version = '>= 0' if version.length.zero?
282
287
  gem.add_runtime_dependency( name, version )
data/Rakefile.local CHANGED
@@ -13,6 +13,8 @@ require 'pp'
13
13
 
14
14
  MANUAL_PAGES = Pathname.glob( MANUALDIR + '**/*.page' )
15
15
 
16
+ RCOV_EXCLUDES.replace( RCOV_EXCLUDES + ',sequel_integration\.rb' )
17
+
16
18
  task :local => :check_manual
17
19
  task :manual => :check_manual
18
20
 
data/bin/treeirb CHANGED
@@ -6,8 +6,12 @@ require 'irb/cmd/nop'
6
6
  require 'treequel'
7
7
 
8
8
 
9
- uri = ARGV.shift or raise "usage: #$0 [LDAPURI]"
10
- $dir = Treequel.directory( uri )
9
+ if uri = ARGV.shift
10
+ $dir = Treequel.directory( uri )
11
+ else
12
+ $dir = Treequel.directory_from_config
13
+ end
14
+
11
15
  $stderr.puts "Directory is in $dir:", ' ' + $dir.inspect
12
16
 
13
17
  IRB.start( $0 )
data/bin/treequel CHANGED
@@ -571,7 +571,7 @@ class Treequel::Shell
571
571
  # Calcuate column widths
572
572
  oclen = children.map do |subbranch|
573
573
  subbranch.include_operational_attrs = true
574
- subbranch[:structuralObjectClass].length
574
+ subbranch[:structuralObjectClass] ? subbranch[:structuralObjectClass].length : 0
575
575
  end.max
576
576
 
577
577
  # Set up sorting by collecting all the requested sort criteria as Proc objects which
@@ -610,7 +610,7 @@ class Treequel::Shell
610
610
  return
611
611
  end
612
612
 
613
- return self.parent_command if rdn == '..'
613
+ return self.parent_command( options ) if rdn == '..'
614
614
 
615
615
  raise "invalid RDN %p" % [ rdn ] unless RELATIVE_DISTINGUISHED_NAME.match( rdn )
616
616
 
@@ -868,9 +868,10 @@ class Treequel::Shell
868
868
  metadatalen = oclen + 16 + 6 # oc + timestamp + whitespace
869
869
  maxdesclen = self.columns - metadatalen - rdn.length - 5
870
870
 
871
+ modtime = branch[:modifyTimestamp] || branch[:createTimestamp]
871
872
  return "%#{oclen}s %s %s%s %s" % [
872
- branch[:structuralObjectClass],
873
- branch[:modifyTimestamp].strftime('%Y-%m-%d %H:%M'),
873
+ branch[:structuralObjectClass] || '',
874
+ modtime.strftime('%Y-%m-%d %H:%M'),
874
875
  rdn,
875
876
  branch[:hasSubordinates] ? '/' : '',
876
877
  single_line_description( branch, maxdesclen )
@@ -69,7 +69,8 @@ class Treequel::Branch
69
69
  ### @param [String] dn The DN of the entry the Branch is wrapping.
70
70
  ### @param [LDAP::Entry, Hash] entry The entry object if it's already been fetched.
71
71
  def initialize( directory, dn, entry=nil )
72
- raise ArgumentError, "invalid DN" unless dn.match( Patterns::DISTINGUISHED_NAME )
72
+ raise ArgumentError, "invalid DN" unless
73
+ dn.match( Patterns::DISTINGUISHED_NAME ) || dn.empty?
73
74
  raise ArgumentError, "can't cast a %s to an LDAP::Entry" % [entry.class.name] unless
74
75
  entry.nil? || entry.is_a?( Hash )
75
76
 
@@ -104,7 +105,7 @@ class Treequel::Branch
104
105
 
105
106
  # Whether or not to include operational attributes when fetching the Branch's entry
106
107
  predicate_attr :include_operational_attrs
107
-
108
+ alias_method :include_operational_attributes?, :include_operational_attrs?
108
109
 
109
110
  ### Change the DN the Branch uses to look up its entry.
110
111
  ###
@@ -124,6 +125,7 @@ class Treequel::Branch
124
125
  self.clear_caches
125
126
  @include_operational_attrs = new_setting ? true : false
126
127
  end
128
+ alias_method :include_operational_attributes=, :include_operational_attrs=
127
129
 
128
130
 
129
131
  ### Return the attribute/s which make up this Branch's RDN.
@@ -181,6 +183,7 @@ class Treequel::Branch
181
183
  ### @return [String]
182
184
  def parent_dn
183
185
  return nil if self.dn == self.directory.base_dn
186
+ return '' if self.dn.index( ',' ).nil?
184
187
  return self.split_dn( 2 ).last
185
188
  end
186
189
 
@@ -260,35 +263,21 @@ class Treequel::Branch
260
263
  end
261
264
 
262
265
 
263
- ### Make LDIF for the given +attribute+ and its +values+, wrapping at the given
264
- ### +width+.
265
- ###
266
- ### @param [String] attribute the attribute
267
- ### @param [Array<String>] values the values for the given +attribute+
268
- ### @param [Fixnum] width the maximum width of the lines to return
269
- def ldif_for_attr( attribute, values, width )
270
- ldif = ''
271
-
272
- Array( values ).each do |val|
273
- line = "#{attribute}:"
266
+ ### Return the Branch as a Hash.
267
+ ### @see Treequel::Branch#[]
268
+ ### @return [Hash] the entry as a Hash with converted values
269
+ def to_hash
270
+ entry = self.entry || self.valid_attributes_hash
271
+ self.log.debug " making a Hash from an entry: %p" % [ entry ]
274
272
 
275
- if val =~ /^#{LDIF_SAFE_STRING}$/
276
- line << ' ' << val.to_s
273
+ return entry.keys.inject({}) do |hash, attribute|
274
+ if attribute == 'dn'
275
+ hash[ attribute ] = self.dn
277
276
  else
278
- line << ': ' << [ val ].pack( 'm' ).chomp
277
+ hash[ attribute ] = self[ attribute ]
279
278
  end
280
-
281
- # calculate how many times the line needs to be split, then add any
282
- # additional splits that need to be added because of the additional
283
- # fold characters
284
- splits = ( line.length / width )
285
- splits += ( splits * LDIF_FOLD_SEPARATOR.length ) / width
286
- splits.times {|i| line[ width * (i+1), 0 ] = LDIF_FOLD_SEPARATOR }
287
-
288
- ldif << line << "\n"
279
+ hash
289
280
  end
290
-
291
- return ldif
292
281
  end
293
282
 
294
283
 
@@ -298,32 +287,9 @@ class Treequel::Branch
298
287
  attrsym = attrname.to_sym
299
288
 
300
289
  unless @values.key?( attrsym )
301
- directory = self.directory
302
- entry = self.entry or return nil
303
- return nil unless (( value = entry[attrsym.to_s] ))
304
-
305
- self.log.debug " value is not cached; checking its attributeType"
306
- if attribute = directory.schema.attribute_types[ attrsym ]
307
- self.log.debug " attribute exists; checking the entry for a value"
308
-
309
- syntax_oid = attribute.syntax_oid
310
-
311
- if attribute.single?
312
- self.log.debug " attributeType is SINGLE; unwrapping the Array"
313
- @values[ attrsym ] = directory.convert_syntax_value( syntax_oid, value.first )
314
- else
315
- self.log.debug " attributeType is not SINGLE; keeping the Array"
316
- @values[ attrsym ] = value.collect do |raw|
317
- directory.convert_syntax_value( syntax_oid, raw )
318
- end
319
- @values[ attrsym ].freeze if @values[ attrsym ].is_a?( Array )
320
- end
321
-
322
- else
323
- self.log.info "no attributeType for %p" % [ attrsym ]
324
- @values[ attrsym ] = value
325
- @values[ attrsym ].freeze
326
- end
290
+ value = self.get_converted_object( attrsym )
291
+ value.freeze if value.respond_to?( :freeze )
292
+ @values[ attrsym ] = value
327
293
  else
328
294
  self.log.debug " value is cached."
329
295
  end
@@ -349,6 +315,7 @@ class Treequel::Branch
349
315
  ### @param [Object] value the attribute value
350
316
  def []=( attrname, value )
351
317
  value = [ value ] unless value.is_a?( Array )
318
+ value.collect! {|val| self.get_converted_attribute(attrname, val) }
352
319
  self.log.debug "Modifying %s to %p" % [ attrname, value ]
353
320
  self.directory.modify( self, attrname.to_s => value )
354
321
  @values.delete( attrname.to_sym )
@@ -384,19 +351,25 @@ class Treequel::Branch
384
351
  ###
385
352
  ### @return [TrueClass] if the delete succeeded
386
353
  def delete( *attributes )
387
- self.log.debug "Deleting attributes: %p" % [ attributes ]
388
- mods = attributes.flatten.collect do |attribute|
389
- if attribute.is_a?( Hash )
390
- attribute.collect do |key,vals|
391
- vals = Array( vals ).collect {|val| val.to_s }
392
- LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, key.to_s, vals )
354
+ if attributes.empty?
355
+ self.log.info "No attributes specified; deleting entire entry for %s" % [ self.dn ]
356
+ self.directory.delete( self )
357
+ else
358
+ self.log.debug "Deleting attributes: %p" % [ attributes ]
359
+ mods = attributes.flatten.collect do |attribute|
360
+ if attribute.is_a?( Hash )
361
+ attribute.collect do |key,vals|
362
+ vals = Array( vals ).collect {|val| val.to_s }
363
+ LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, key.to_s, vals )
364
+ end
365
+ else
366
+ LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, attribute.to_s, [] )
393
367
  end
394
- else
395
- LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, attribute.to_s, [] )
396
- end
397
- end.flatten
368
+ end.flatten
369
+
370
+ self.directory.modify( self, mods )
371
+ end
398
372
 
399
- self.directory.modify( self, mods )
400
373
  self.clear_caches
401
374
 
402
375
  return true
@@ -478,7 +451,8 @@ class Treequel::Branch
478
451
  ### @param [String] rdn The RDN of the child to fetch.
479
452
  ### @return [Treequel::Branch]
480
453
  def get_child( rdn )
481
- newdn = [ rdn, self.dn ].join( ',' )
454
+ self.log.debug "Getting child %p from base = %p" % [ rdn, self.dn ]
455
+ newdn = [ rdn, self.dn ].reject {|part| part.empty? }.join( ',' )
482
456
  return self.class.new( self.directory, newdn )
483
457
  end
484
458
 
@@ -517,6 +491,21 @@ class Treequel::Branch
517
491
  end
518
492
 
519
493
 
494
+ ### Return the receiver's operational attributes as attributeType schema objects.
495
+ ###
496
+ ### @return [Array<Treequel::Schema::AttributeType>] the operational attributes
497
+ def operational_attribute_types
498
+ return self.directory.schema.operational_attribute_types
499
+ end
500
+
501
+
502
+ ### Return OIDs (numeric OIDs as Strings, named OIDs as Symbols) for each of the
503
+ ### receiver's operational attributes.
504
+ def operational_attribute_oids
505
+ return self.operational_attribute_types.map( &:oid )
506
+ end
507
+
508
+
520
509
  ### Return Treequel::Schema::AttributeType instances for each of the receiver's
521
510
  ### objectClass's MUST attributeTypes. If any +additional_object_classes+ are given,
522
511
  ### include the MUST attributeTypes for them as well. This can be used to predict what
@@ -626,22 +615,29 @@ class Treequel::Branch
626
615
  end
627
616
  end
628
617
 
629
- attrhash[ :objectClass ] |= additional_object_classes
618
+ # :FIXME: Does the resulting hash need the additional objectClasses? objectClass is
619
+ # MUST via 'top', so it should already exist in that hash when merged with
620
+ # this one...
621
+ # attrhash[ :objectClass ] |= additional_object_classes
622
+
630
623
  return attrhash
631
624
  end
632
625
 
633
626
 
634
627
  ### Return Treequel::Schema::AttributeType instances for the set of all of the receiver's
635
- ### MUST and MAY attributeTypes.
628
+ ### MUST and MAY attributeTypes plus the operational attributes.
636
629
  ###
637
630
  ### @return [Array<Treequel::Schema::AttributeType>]
638
631
  def valid_attribute_types
639
- return self.must_attribute_types | self.may_attribute_types
632
+ return self.must_attribute_types |
633
+ self.may_attribute_types |
634
+ self.operational_attribute_types
640
635
  end
641
636
 
642
637
 
643
638
  ### Return a uniqified Array of OIDs (numeric OIDs as Strings, named OIDs as Symbols) for
644
- ### the set of all of the receiver's MUST and MAY attributeTypes.
639
+ ### the set of all of the receiver's MUST and MAY attributeTypes plus the operational
640
+ ### attributes.
645
641
  ###
646
642
  ### @return [Array<String, Symbol>]
647
643
  def valid_attribute_oids
@@ -649,8 +645,10 @@ class Treequel::Branch
649
645
  end
650
646
 
651
647
 
652
- ### If the given +attroid+ is a valid attributeType name or numeric OID, return the
648
+ ### If the attribute associated with the given +attroid+ is in the list of valid
649
+ ### attributeTypes for the receiver given its objectClasses, return the
653
650
  ### AttributeType object that corresponds with it. If it isn't valid, return nil.
651
+ ### Includes operational attributes.
654
652
  ###
655
653
  ### @param [String,Symbol] attroid a numeric OID (as a String) or a named OID (as a Symbol)
656
654
  ### @return [Treequel::Schema::AttributeType] the validated attributeType
@@ -660,7 +658,7 @@ class Treequel::Branch
660
658
 
661
659
 
662
660
  ### Return +true+ if the specified +attrname+ is a valid attributeType given the
663
- ### receiver's current objectClasses.
661
+ ### receiver's current objectClasses. Does not include operational attributes.
664
662
  ###
665
663
  ### @param [String, Symbol] the OID (numeric or name) of the attribute in question
666
664
  ### @return [Boolean]
@@ -736,7 +734,6 @@ class Treequel::Branch
736
734
 
737
735
  ### Fetch the entry from the Branch's directory.
738
736
  def lookup_entry
739
- self.log.debug "Looking up entry for %p" % [ self ]
740
737
  if self.include_operational_attrs?
741
738
  self.log.debug " including operational attributes."
742
739
  return self.directory.get_extended_entry( self )
@@ -747,6 +744,42 @@ class Treequel::Branch
747
744
  end
748
745
 
749
746
 
747
+ ### Get the value associated with +attrsym+, convert it to a Ruby object if the Branch's
748
+ ### directory has a conversion rule, and return it.
749
+ def get_converted_object( attrsym )
750
+ return nil unless self.entry
751
+ value = self.entry[ attrsym.to_s ] or return nil
752
+
753
+ if attribute = self.directory.schema.attribute_types[ attrsym ]
754
+ if attribute.single?
755
+ value = self.directory.convert_to_object( attribute.syntax.oid, value.first )
756
+ else
757
+ value = value.collect do |raw|
758
+ self.directory.convert_to_object( attribute.syntax.oid, raw )
759
+ end
760
+ end
761
+ else
762
+ self.log.info "no attributeType for %p" % [ attrsym ]
763
+ end
764
+
765
+ return value
766
+ end
767
+
768
+
769
+ ### Convert the specified +object+ according to the Branch's directory's conversion rules,
770
+ ### and return it.
771
+ def get_converted_attribute( attrsym, object )
772
+ if attribute = self.directory.schema.attribute_types[ attrsym ]
773
+ self.log.debug "converting %p object to a %p attribute" %
774
+ [ attrsym, attribute.syntax.oid ]
775
+ return self.directory.convert_to_attribute( attribute.syntax.oid, object )
776
+ else
777
+ self.log.info "no attributeType for %p" % [ attrsym ]
778
+ return object.to_s
779
+ end
780
+ end
781
+
782
+
750
783
  ### Clear any cached values when the structural state of the object changes.
751
784
  ### @return [void]
752
785
  def clear_caches
@@ -793,6 +826,34 @@ class Treequel::Branch
793
826
  end
794
827
  end
795
828
 
829
+
830
+ ### Make LDIF for the given +attribute+ and its +values+, wrapping at the given
831
+ ### +width+.
832
+ ###
833
+ ### @param [String] attribute the attribute
834
+ ### @param [Array<String>] values the values for the given +attribute+
835
+ ### @param [Fixnum] width the maximum width of the lines to return
836
+ def ldif_for_attr( attribute, value, width )
837
+ unsplit_line = "#{attribute}:"
838
+
839
+ if value.empty? || value =~ /\A#{LDIF_SAFE_STRING}\Z/
840
+ unsplit_line << ' ' << value.to_s
841
+ else
842
+ unsplit_line << ': ' << [ value ].pack( 'm' ).chomp
843
+ end
844
+ unsplit_line.gsub!( /\n/, '' )
845
+
846
+ ldif = ''
847
+ ldif << unsplit_line.slice!( 0, width ) << LDIF_FOLD_SEPARATOR until
848
+ unsplit_line.empty?
849
+
850
+ ldif.rstrip!
851
+ ldif << "\n"
852
+
853
+ return ldif
854
+ end
855
+
856
+
796
857
  end # class Treequel::Branch
797
858
 
798
859
 
@@ -178,19 +178,30 @@ class Treequel::BranchCollection
178
178
  end
179
179
 
180
180
 
181
- ### Return a new Treequel::BranchCollection that includes both the receiver's Branchsets and
182
- ### those in +other_object+ (or +other_object+ itself if it's a Branchset).
181
+ ### Return either a new Treequel::BranchCollection that includes both the receiver's
182
+ ### Branchsets and those in +other_object+ (if it responds_to #branchsets), or the results
183
+ ### from executing the BranchCollection's search with +other_object+ appended if it doesn't.
184
+ ### @param [Object] other_object
183
185
  def +( other_object )
184
186
  if other_object.respond_to?( :branchsets )
185
187
  return self.class.new( self.branchsets + other_object.branchsets )
186
- elsif other_object.respond_to?( :branchset )
187
- return self.class.new( self.branchsets + [other_object.branchset] )
188
- else
188
+ elsif other_object.respond_to?( :collection )
189
189
  return self.class.new( self.branchsets + [other_object] )
190
+ else
191
+ return self.all + Array( other_object )
190
192
  end
191
193
  end
192
194
 
193
195
 
196
+ ### Return the results from each of the receiver's Branchsets without the +other_object+.
197
+ ### @param [#dn] other_object the object to omit from the results. Must respond_to #dn.
198
+ ### @return [Array<Treequel::Branch>]
199
+ def -( other_object )
200
+ other_dn = other_object.dn
201
+ return self.reject {|branch| branch.dn == other_dn }
202
+ end
203
+
204
+
194
205
  ### Return a new Treequel::BranchCollection that contains the union of the branchsets from both
195
206
  ### collections.
196
207
  def &( other_collection )
@@ -176,12 +176,26 @@ class Treequel::Branchset
176
176
  end
177
177
 
178
178
 
179
- ### Create a BranchCollection from the receiver and the +other_branchset+ and return
180
- ### it.
181
- ### @param [Treequel::Branchset] other_branchset the branchset to combine with
182
- ### @return [Treequel::BranchCollection] the resulting collection object
183
- def +( other_branchset )
184
- return Treequel::BranchCollection.new( self, other_branchset )
179
+ ### If given another Branchset or a BranchCollection, return a BranchCollection that includes
180
+ ### them both. If given anything else, execute the search and return
181
+ ### @param [Treequel::Branchset, Treequel::Branch] other the branchset or branch to combine
182
+ ### with
183
+ ### @return [Treequel::BranchCollection, Array<Treequel::Branch>]
184
+ def +( other )
185
+ if other.is_a?( Treequel::BranchCollection ) || other.is_a?( Treequel::Branchset )
186
+ return Treequel::BranchCollection.new( self, other )
187
+ else
188
+ return self.all + Array( other )
189
+ end
190
+ end
191
+
192
+
193
+ ### Return the results of executing the search without the +other_object+.
194
+ ### @param [#dn] other_object the object to omit from the results; must respond_to #dn.
195
+ ### @return [Array<Treequel::Branch>]
196
+ def -( other_object )
197
+ other_dn = other_object.dn
198
+ return self.reject {|branch| branch.dn == other_dn }
185
199
  end
186
200
 
187
201
 
@@ -276,6 +290,23 @@ class Treequel::Branchset
276
290
  end
277
291
 
278
292
 
293
+ ### Add an alternate filter to an existing filter by ORing them.
294
+ ### @param filterspec the filter spec to OR with the existing filter
295
+ ### @raises [Treequel::InvalidOperation] if there is no existing filter
296
+ ### @see Treequel::Filter.new for specifics on what +filterspec+ can be
297
+ def or( *filterspec )
298
+ opts = self.options
299
+ existing_filter = self.filter
300
+ raise Treequel::ExpressionError, "no existing filter" if
301
+ existing_filter.promiscuous?
302
+
303
+ newfilter = Treequel::Filter.new( *filterspec )
304
+
305
+ self.log.debug "cloning %p with alternative filterspec: %p" % [ self, filterspec ]
306
+ return self.clone( :filter => (self.filter | newfilter) )
307
+ end
308
+
309
+
279
310
  ### If called with no argument, returns the current scope of the Branchset. If
280
311
  ### called with an argument (which should be one of the keys of
281
312
  ### Treequel::Constants::SCOPE), returns a clone of the receiving Branchset
@@ -141,6 +141,18 @@ module Treequel::Constants
141
141
  FEATURE_NAMES = FEATURE_OIDS.invert.freeze
142
142
 
143
143
 
144
+ ### The list of RFC4512 operational attributes that "SHOULD" be maintained.
145
+ ### (http://tools.ietf.org/html/rfc4512#section-3.4)
146
+ MINIMAL_OPERATIONAL_ATTRIBUTES = [
147
+ :creatorsName,
148
+ :createTimestamp,
149
+ :modifiersName,
150
+ :modifyTimestamp,
151
+ :structuralObjectClass,
152
+ :governingStructureRule,
153
+ ]
154
+
155
+
144
156
  ### OIDs of RFC values
145
157
  module OIDS
146
158