treequel 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,363 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ require 'set'
5
+
6
+ require 'treequel'
7
+ require 'treequel/branch'
8
+ require 'treequel/branchset'
9
+
10
+
11
+ # An object interface to LDAP entries.
12
+ class Treequel::Model < Treequel::Branch
13
+ include Treequel::Loggable,
14
+ Treequel::Constants,
15
+ Treequel::Normalization,
16
+ Treequel::Constants::Patterns
17
+
18
+ require 'treequel/model/objectclass'
19
+
20
+
21
+ # A prototype Hash that autovivifies its members as Sets, for use in
22
+ # the objectclass_registry and the base_registry
23
+ SET_HASH = Hash.new {|h,k| h[k] = Set.new }
24
+
25
+
26
+ #################################################################
27
+ ### C L A S S M E T H O D S
28
+ #################################################################
29
+
30
+ @objectclass_registry = SET_HASH.dup
31
+ @base_registry = SET_HASH.dup
32
+
33
+ class << self
34
+ attr_reader :objectclass_registry
35
+ attr_reader :base_registry
36
+ end
37
+
38
+
39
+ ### Inheritance callback -- add a class-specific objectclass registry to inheriting classes.
40
+ ### @param [Class] subclass the inheriting class
41
+ def self::inherited( subclass )
42
+ super
43
+ subclass.instance_variable_set( :@objectclass_registry, SET_HASH.dup )
44
+ subclass.instance_variable_set( :@base_registry, SET_HASH.dup )
45
+ end
46
+
47
+
48
+ ### Register the given +mixin+ for the specified +objectclasses+. Instances that
49
+ ### have all the specified +objectclasses+ will be extended with the +mixin+.
50
+ ###
51
+ ### @param [Module] mixin the mixin to be applied; it should be extended with
52
+ ### Treequel::Model::ObjectClass.
53
+ def self::register_mixin( mixin )
54
+ objectclasses = mixin.model_objectclasses
55
+ bases = mixin.model_bases
56
+ bases << '' if bases.empty?
57
+
58
+ Treequel.logger.debug "registering %p [objectClasses: %p, base DNs: %p]" %
59
+ [ mixin, objectclasses, bases ]
60
+
61
+ # Register it with each of its objectClasses
62
+ objectclasses.each do |oc|
63
+ @objectclass_registry[ oc.to_sym ].add( mixin )
64
+ end
65
+
66
+ # ...and each of its bases
67
+ bases.each do |dn|
68
+ @base_registry[ dn.downcase ].add( mixin )
69
+ end
70
+ end
71
+
72
+
73
+ ### Unregister the given +mixin+ for the specified +objectclasses+.
74
+ ### @param [Module] mixin the mixin that should no longer be applied
75
+ def self::unregister_mixin( mixin )
76
+ objectclasses = mixin.model_objectclasses
77
+ bases = mixin.model_bases
78
+ bases << '' if bases.empty?
79
+
80
+ Treequel.logger.debug "un-registering %p [objectclasses: %p, base DNs: %p]" %
81
+ [ mixin, objectclasses, bases ]
82
+
83
+ # Unregister it from each of its bases
84
+ bases.each do |dn|
85
+ @base_registry[ dn.downcase ].delete( mixin )
86
+ end
87
+
88
+ # ...and each of its objectClasses
89
+ objectclasses.each do |oc|
90
+ @objectclass_registry[ oc.to_sym ].delete( mixin )
91
+ end
92
+ end
93
+
94
+
95
+ ### Return the mixins that should be applied to an entry with the given +objectclasses+.
96
+ ### @param [Array<Symbol>] objectclasses the objectclasses from the entry
97
+ ### @return [Set<Module>] the Set of mixin modules which apply
98
+ def self::mixins_for_objectclasses( *objectclasses )
99
+ ocsymbols = objectclasses.flatten.collect {|oc| oc.untaint.to_sym }
100
+
101
+ # Get the union of all of the mixin sets for the objectclasses in question
102
+ mixins = self.objectclass_registry.
103
+ values_at( *ocsymbols ).
104
+ inject {|set1,set2| set1 | set2 }
105
+
106
+ # Return the mixins whose objectClass requirements are met by the
107
+ # specified objectclasses
108
+ return mixins.delete_if do |mixin|
109
+ !mixin.model_objectclasses.all? {|oc| ocsymbols.include?(oc) }
110
+ end
111
+ end
112
+
113
+
114
+ ### Return the mixins that should be applied to an entry with the given +dn+.
115
+ ### @param [String] dn the DN of the entry
116
+ ### @return [Set<Module>] the Set of mixin modules which apply
117
+ def self::mixins_for_dn( dn )
118
+ dn_tuples = dn.downcase.split( /\s*,\s*/ )
119
+ dn_keys = dn_tuples.reverse.inject(['']) do |keys, dnpair|
120
+ dnpair += ',' + keys.last unless keys.last.empty?
121
+ keys << dnpair
122
+ end
123
+
124
+ # Get the union of all of the mixin sets for the DN and all of its parents
125
+ union = self.base_registry.
126
+ values_at( *dn_keys ).
127
+ inject {|set1,set2| set1 | set2 }
128
+
129
+ return union
130
+ end
131
+
132
+
133
+
134
+ #################################################################
135
+ ### I N S T A N C E M E T H O D S
136
+ #################################################################
137
+
138
+ ### Override the default to extend new instances with applicable mixins if their
139
+ ### entry is set.
140
+ def initialize( *args )
141
+ super
142
+ self.apply_applicable_mixins( @dn, @entry ) if @entry
143
+ end
144
+
145
+
146
+ ######
147
+ public
148
+ ######
149
+
150
+ ### Override Branch#search to inject the 'objectClass' attribute to the
151
+ ### selected attribute list if there is one.
152
+ def search( scope=:subtree, filter='(objectClass=*)', parameters={}, &block )
153
+ parameters[:selectattrs] |= ['objectClass'] unless
154
+ !parameters.key?( :selectattrs ) || parameters[ :selectattrs ].empty?
155
+
156
+ super
157
+ end
158
+
159
+
160
+ ### Returns +true+ if the receiver responds to the given method.
161
+ ### @param [Symbol,String] sym the name of the method to test for
162
+ ### @return [Boolean]
163
+ def respond_to?( sym, include_priv=false )
164
+ return super if caller(1).first =~ %r{/spec/} &&
165
+ caller(1).first !~ /respond_to/ # RSpec workaround
166
+ return true if super
167
+ plainsym, _ = attribute_from_method( sym )
168
+ return self.find_attribute_type( plainsym ) ? true : false
169
+ end
170
+
171
+
172
+ ### Return the Treequel::Model::ObjectClass mixins that have been applied to the receiver.
173
+ ### @return [Array<Module>]
174
+ def extensions
175
+ eigenclass = ( class << self; self; end )
176
+ return eigenclass.included_modules.find_all do |mod|
177
+ (class << mod; self; end).include?(Treequel::Model::ObjectClass)
178
+ end
179
+ end
180
+
181
+
182
+ ### Return a human-readable representation of the receiving object, suitable for debugging.
183
+ def inspect
184
+ return "#<%s:0x%x (%s)>" % [
185
+ self.class.name,
186
+ self.object_id * 2,
187
+ self.extensions.map( &:name ).join( ', ' )
188
+ ]
189
+ end
190
+
191
+
192
+ #########
193
+ protected
194
+ #########
195
+
196
+ ### Search for the Treequel::Schema::AttributeType associated with +sym+.
197
+ ###
198
+ ### @param [Symbol,String] name the name of the attribute to find
199
+ ### @return [Treequel::Schema::AttributeType,nil] the associated attributeType, or nil
200
+ ### if there isn't one
201
+ def find_attribute_type( name )
202
+ attrtype = nil
203
+
204
+ # If the attribute doesn't match as-is, try the camelCased version of it
205
+ camelcased_sym = name.to_s.gsub( /_(\w)/ ) { $1.upcase }.to_sym
206
+ attrtype = self.valid_attribute_type( name ) ||
207
+ self.valid_attribute_type( camelcased_sym )
208
+
209
+ return attrtype
210
+ end
211
+
212
+
213
+ ### Proxy method -- Handle calls to missing methods by searching for an attribute.
214
+ def method_missing( sym, *args )
215
+
216
+ # First, if the entry hasn't yet been loaded, try loading it to make sure the
217
+ # object is already extended with any applicable objectClass mixins. If that ends
218
+ # up defining the method in question, call it.
219
+ if !@entry && self.entry
220
+ meth = begin
221
+ self.method( sym )
222
+ rescue NoMethodError, NameError
223
+ nil
224
+ end
225
+ return meth.call( *args ) if meth
226
+ end
227
+
228
+ # Next, super to rdn-traversal if it looks like a reader but has arguments
229
+ plainsym, methodtype = attribute_from_method( sym )
230
+ return super if methodtype == :reader && !args.empty?
231
+
232
+ # Now make a method body for a new method based on what attributeType it is if
233
+ # it's a valid attribute
234
+ attrtype = self.find_attribute_type( plainsym ) or return super
235
+ methodbody = case methodtype
236
+ when :writer
237
+ self.make_writer( attrtype )
238
+ when :predicate
239
+ self.make_predicate( attrtype )
240
+ else
241
+ self.make_reader( attrtype )
242
+ end
243
+
244
+ # Define the new method and call it by fetching the corresponding Method object
245
+ # so we don't loop back through #method_missing if something goes wrong
246
+ self.class.send( :define_method, sym, &methodbody )
247
+ return self.method( sym ).call( *args )
248
+ end
249
+
250
+
251
+ ### Make a reader method body for the given +attrtype+.
252
+ ###
253
+ ### @param [Treequel::Mode::AttributeType] attrtype the attributeType to create the reader
254
+ ### for.
255
+ ### @return [Proc] the body of the reader method.
256
+ def make_reader( attrtype )
257
+ self.log.debug "Generating an attribute reader for %p" % [ attrtype ]
258
+ attrname = attrtype.name
259
+ return lambda {|*args|
260
+ if args.empty?
261
+ self[ attrname ]
262
+ else
263
+ self.traverse_branch( attrname, *args )
264
+ end
265
+ }
266
+ end
267
+
268
+
269
+ ### Make a writer method body for the given +attrtype+.
270
+ ###
271
+ ### @param [Treequel::Mode::AttributeType] attrtype the attributeType to create the accessor
272
+ ### for.
273
+ ### @return [Proc] the body of the writer method.
274
+ def make_writer( attrtype )
275
+ self.log.debug "Generating an attribute writer for %p" % [ attrtype ]
276
+ attrname = attrtype.name
277
+ if attrtype.single?
278
+ self.log.debug " attribute is SINGLE, so generating a scalar writer..."
279
+ return lambda {|newvalue| self[attrname] = newvalue }
280
+ else
281
+ self.log.debug " attribute isn't SINGLE, so generating an array writer..."
282
+ return lambda {|*newvalues| self[attrname] = newvalues }
283
+ end
284
+ end
285
+
286
+
287
+ ### Make a predicate method body for the given +attrtype+.
288
+ ###
289
+ ### @param [Treequel::Mode::AttributeType] attrtype the attributeType to create the method
290
+ ### for.
291
+ ### @return [Proc] the body of the predicate method.
292
+ def make_predicate( attrtype )
293
+ self.log.debug "Generating an attribute predicate for %p" % [ attrtype ]
294
+ attrname = attrtype.name
295
+ if attrtype.single?
296
+ self.log.debug " attribute is SINGLE, so generating a scalar predicate..."
297
+ return lambda { self[attrname] ? true : false }
298
+ else
299
+ self.log.debug " attribute isn't SINGLE, so generating an array predicate..."
300
+ return lambda { self[attrname].any? {|val| val} }
301
+ end
302
+ end
303
+
304
+
305
+ ### Overridden to apply applicable mixins to lazily-loaded objects once their entry
306
+ ### has been looked up.
307
+ ### @return [LDAP::Entry] the fetched entry object
308
+ def lookup_entry
309
+ if entry = super
310
+ self.log.debug " applying mixins to %p" % [ entry ]
311
+ self.apply_applicable_mixins( self.dn, entry )
312
+ end
313
+ return entry
314
+ end
315
+
316
+
317
+ ### Apply mixins that are applicable considering the receiver's DN and the
318
+ ### objectClasses from its entry.
319
+ def apply_applicable_mixins( dn, entry )
320
+ ocs = entry.object_classes.collect do |explicit_oc|
321
+ explicit_oc.ancestors.collect {|oc| oc.name }
322
+ end.flatten.uniq
323
+
324
+ oc_mixins = self.class.mixins_for_objectclasses( *ocs )
325
+ dn_mixins = self.class.mixins_for_dn( dn )
326
+
327
+ # The applicable mixins are those in the intersection of the ones
328
+ # inferred by its objectclasses and those that apply to its DN
329
+ mixins = ( oc_mixins & dn_mixins )
330
+
331
+ self.log.debug " %d mixins apply to %s: %p" % [ mixins.length, dn, mixins.to_a ]
332
+ mixins.each {|mod| self.extend(mod) }
333
+ end
334
+
335
+
336
+ #######
337
+ private
338
+ #######
339
+
340
+ ### Given the symbol from an attribute accessor or predicate, return the
341
+ ### name of the corresponding LDAP attribute/
342
+ ### @param [Symbol] methodname the method being called
343
+ ### @return [Symbol] the attribute name that corresponds to the method
344
+ def attribute_from_method( methodname )
345
+
346
+ case methodname.to_s
347
+ when /^(?:has_)?([a-z]\w+)\?$/i
348
+ return $1.to_sym, :predicate
349
+ when /^([a-z]\w+)(=)?$/i
350
+ return $1.to_sym, ($2 ? :writer : :reader )
351
+ end
352
+ end
353
+
354
+
355
+ ### Turn a String DN into a reversed set of DN attribute/value pairs
356
+ def make_dn_tuples( dn )
357
+ return dn.split( /\s*,\s*/ ).reverse
358
+ end
359
+
360
+ end # class Treequel::Model
361
+
362
+
363
+
@@ -26,4 +26,69 @@ class LDAP::Control
26
26
  end
27
27
 
28
28
 
29
+ ### Extensions to the Time class to add LDAP (RFC4517) Generalized Time syntax
30
+ module Treequel::TimeExtensions
31
+
32
+ ### Return +self+ as a String formatted as specified in RFC4517
33
+ ### (LDAP Generalized Time).
34
+ def ldap_generalized( fraction_digits=0 )
35
+ fractional_seconds =
36
+ if fraction_digits == 0
37
+ ''
38
+ elsif fraction_digits <= 6
39
+ '.' + sprintf('%06d', usec)[0, fraction_digits]
40
+ else
41
+ '.' + sprintf('%06d', usec) + '0' * (fraction_digits - 6)
42
+ end
43
+ tz =
44
+ if self.utc?
45
+ 'Z'
46
+ else
47
+ off = utc_offset
48
+ sign = off < 0 ? '-' : '+'
49
+ "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
50
+ end
51
+
52
+ return "%02d%02d%02d%02d%02d%02d%s%s" % [
53
+ year,
54
+ mon,
55
+ day,
56
+ hour,
57
+ min,
58
+ sec,
59
+ fractional_seconds,
60
+ tz
61
+ ]
62
+
63
+ end
64
+
65
+ ### Returns +self+ as a String formatted as specified in RFC4517
66
+ ### (UTC Time)
67
+ def ldap_utc
68
+ tz =
69
+ if utc?
70
+ 'Z'
71
+ else
72
+ off = utc_offset
73
+ sign = off < 0 ? '-' : '+'
74
+ "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
75
+ end
76
+
77
+ return "%02d%02d%02d%02d%02d%02d%s" % [
78
+ year.divmod(100).last,
79
+ mon,
80
+ day,
81
+ hour,
82
+ min,
83
+ sec,
84
+ tz
85
+ ]
86
+ end
87
+
88
+ end # module Treequel::TimeExtensions
89
+
90
+ class Time
91
+ include Treequel::TimeExtensions
92
+ end
93
+
29
94
 
@@ -28,6 +28,30 @@ class Treequel::Schema::AttributeType
28
28
 
29
29
  extend Treequel::AttributeDeclarations
30
30
 
31
+ # Regex for splitting a syntax OID from its length specifier
32
+ OID_SPLIT_PATTERN = /
33
+ ^
34
+ #{SQUOTE}?
35
+ (#{OID}) # OID = $1
36
+ #{SQUOTE}?
37
+ (?:
38
+ #{LCURLY}
39
+ (#{LEN}) # Length = $2
40
+ #{RCURLY}
41
+ )?$
42
+ /x
43
+
44
+ # The default USAGE type: "Usage of userApplications, the default,
45
+ # indicates that attributes of this type represent user information.
46
+ # That is, they are user attributes." (RFC4512)
47
+ DEFAULT_USAGE_TYPE = 'userApplications'.freeze
48
+
49
+ # A usage of directoryOperation, distributedOperation, or dSAOperation
50
+ # indicates that attributes of this type represent operational and/or
51
+ # administrative information. That is, they are operational
52
+ # attributes. (RFC4512)
53
+ OPERATIONAL_ATTRIBUTE_USAGES = %w[directoryOperation distributedOperation dSAOperation].freeze
54
+
31
55
 
32
56
  #############################################################
33
57
  ### C L A S S M E T H O D S
@@ -63,7 +87,7 @@ class Treequel::Schema::AttributeType
63
87
  :single => single,
64
88
  :collective => collective,
65
89
  :user_modifiable => nousermod ? false : true,
66
- :usagetype => usagetype,
90
+ :usagetype => usagetype || DEFAULT_USAGE_TYPE,
67
91
  :extensions => extensions
68
92
  )
69
93
  end
@@ -92,7 +116,7 @@ class Treequel::Schema::AttributeType
92
116
  @usagetype = attributes[:usagetype]
93
117
  @extensions = attributes[:extensions]
94
118
 
95
- @syntax_oid, @syntax_len = self.split_syntax_oid( attributes[:syntax_oid] ) if
119
+ @syntax_oid, @syntax_len = split_syntax_oid( attributes[:syntax_oid] ) if
96
120
  attributes[:syntax_oid]
97
121
 
98
122
  super()
@@ -147,6 +171,8 @@ class Treequel::Schema::AttributeType
147
171
 
148
172
  # The application of this attributeType
149
173
  attr_accessor :usagetype
174
+ alias_method :usage, :usagetype
175
+ alias_method :usage=, :usagetype=
150
176
 
151
177
  # The attributeType's extensions (as a String)
152
178
  attr_accessor :extensions
@@ -181,16 +207,43 @@ class Treequel::Schema::AttributeType
181
207
  end
182
208
 
183
209
 
210
+ ### Returns the attributeType as a String, which is the RFC4512-style schema
211
+ ### description.
212
+ def to_s
213
+ parts = [ self.oid ]
214
+
215
+ parts << "NAME %s" % Treequel::Schema.qdescrs( self.names ) unless self.names.empty?
216
+
217
+ parts << "DESC '%s'" % [ self.desc ] if self.desc
218
+ parts << "OBSOLETE" if self.obsolete?
219
+ parts << "SUP %s" % [ self.sup_oid ] if self.sup_oid
220
+ parts << "EQUALITY %s" % [ self.eqmatch_oid ] if self.eqmatch_oid
221
+ parts << "ORDERING %s" % [ self.ordmatch_oid ] if self.ordmatch_oid
222
+ parts << "SUBSTR %s" % [ self.submatch_oid ] if self.submatch_oid
223
+ if self.syntax_oid
224
+ parts << "SYNTAX %s" % [ self.syntax_oid ]
225
+ parts.last << "{%d}" % [ self.syntax_len ] if self.syntax_len
226
+ end
227
+ parts << "SINGLE-VALUE" if self.is_single?
228
+ parts << "COLLECTIVE" if self.is_collective?
229
+ parts << "NO-USER-MODIFICATION" unless self.is_user_modifiable?
230
+ parts << "USAGE %s" % [ self.usagetype ] if self.usagetype
231
+ parts << self.extensions.strip unless self.extensions.empty?
232
+
233
+ return "( %s )" % [ parts.join(' ') ]
234
+ end
235
+
236
+
184
237
  ### Return a human-readable representation of the object suitable for debugging
185
238
  def inspect
186
- return "#<%s:0x%0x %s(%s) %p %sSYNTAX: %p (length: %s)>" % [
239
+ return "#<%s:0x%0x %s(%s) %p %sSYNTAX: %s (length: %s)>" % [
187
240
  self.class.name,
188
241
  self.object_id / 2,
189
242
  self.name,
190
243
  self.oid,
191
244
  self.desc,
192
245
  self.is_single? ? '(SINGLE) ' : '',
193
- self.syntax_oid,
246
+ self.syntax,
194
247
  self.syntax_len ? self.syntax_len : 'unlimited',
195
248
  ]
196
249
  end
@@ -247,21 +300,58 @@ class Treequel::Schema::AttributeType
247
300
  end
248
301
 
249
302
 
250
- #########
251
- protected
252
- #########
303
+ # Usage of userApplications, the default, indicates that attributes of
304
+ # this type represent user information. That is, they are user
305
+ # attributes.
253
306
 
254
- OID_SPLIT_PATTERN = /
255
- ^
256
- #{SQUOTE}?
257
- (#{OID}) # OID = $1
258
- #{SQUOTE}?
259
- (?:
260
- #{LCURLY}
261
- (#{LEN}) # Length = $2
262
- #{RCURLY}
263
- )?$
264
- /x
307
+ ### Test whether or not the attrinbute is a user applications attribute.
308
+ ### @return [Boolean] true if the attribute's USAGE is 'userApplications' (or nil)
309
+ def is_user?
310
+ return !self.is_operational?
311
+ end
312
+ alias_method :user?, :is_user?
313
+
314
+
315
+ ### Test whether or not the attribute is an operational attribute.
316
+ ### @return [Boolean] true if the attribute's usage is one of the OPERATIONAL_ATTRIBUTE_USAGES
317
+ def is_operational?
318
+ usage_type = self.usage || DEFAULT_USAGE_TYPE
319
+ return OPERATIONAL_ATTRIBUTE_USAGES.map( &:downcase ).include?( usage_type.downcase )
320
+ end
321
+ alias_method :operational?, :is_operational?
322
+
323
+
324
+ ### Test whether or not the attribute is a directory operational attribute.
325
+ ### @return [Boolean] true if the attribute's usage is 'directoryOperation'
326
+ def is_directory_operational?
327
+ usage_type = self.usage || DEFAULT_USAGE_TYPE
328
+ return usage_type == 'directoryOperation'
329
+ end
330
+ alias_method :directory_operational?, :is_directory_operational?
331
+
332
+
333
+ ### Test whether or not the attribute is a distributed operational attribute.
334
+ ### @return [Boolean] true if the attribute's usage is 'distributedOperation'
335
+ def is_distributed_operational?
336
+ usage_type = self.usage || DEFAULT_USAGE_TYPE
337
+ return usage_type == 'distributedOperation'
338
+ end
339
+ alias_method :distributed_operational?, :is_distributed_operational?
340
+
341
+
342
+ ### Test whether or not the attribute is a DSA-specific operational attribute.
343
+ ### @return [Boolean] true if the attribute's usage is 'dSAOperation'
344
+ def is_dsa_operational?
345
+ usage_type = self.usage || DEFAULT_USAGE_TYPE
346
+ return usage_type == 'dSAOperation'
347
+ end
348
+ alias_method :dsa_operational?, :is_dsa_operational?
349
+ alias_method :dsa_specific?, :is_dsa_operational?
350
+
351
+
352
+ #######
353
+ private
354
+ #######
265
355
 
266
356
  ### Split a numeric OID with an optional length qualifier into a numeric OID and length. If
267
357
  ### no length qualifier is present, it will be nil.
@@ -69,6 +69,21 @@ class Treequel::Schema::LDAPSyntax
69
69
  # The syntax's extensions (as a String)
70
70
  attr_accessor :extensions
71
71
 
72
+ # SyntaxDescription = LPAREN WSP
73
+ # numericoid ; object identifier
74
+ # [ SP "DESC" SP qdstring ] ; description
75
+ # extensions WSP RPAREN ; extensions
76
+
77
+ ### Returns the SyntaxDescription as a String, which is the RFC4512-style schema
78
+ ### description.
79
+ def to_s
80
+ parts = [ self.oid ]
81
+ parts << "DESC '%s'" % [ self.desc ] if self.desc
82
+ parts << self.extensions.strip unless self.extensions.empty?
83
+
84
+ return "( %s )" % [ parts.join(' ') ]
85
+ end
86
+
72
87
 
73
88
  ### Return a human-readable representation of the object suitable for debugging
74
89
  def inspect
@@ -89,6 +89,30 @@ class Treequel::Schema::MatchingRule
89
89
  end
90
90
 
91
91
 
92
+ # MatchingRuleDescription = LPAREN WSP
93
+ # numericoid ; object identifier
94
+ # [ SP "NAME" SP qdescrs ] ; short names (descriptors)
95
+ # [ SP "DESC" SP qdstring ] ; description
96
+ # [ SP "OBSOLETE" ] ; not active
97
+ # SP "SYNTAX" SP numericoid ; assertion syntax
98
+ # extensions WSP RPAREN ; extensions
99
+
100
+ ### Returns the matchingRule as a String, which is the RFC4512-style schema
101
+ ### description.
102
+ def to_s
103
+ parts = [ self.oid ]
104
+
105
+ parts << "NAME %s" % Treequel::Schema.qdescrs( self.names ) unless self.names.empty?
106
+
107
+ parts << "DESC '%s'" % [ self.desc ] if self.desc
108
+ parts << "OBSOLETE" if self.obsolete?
109
+ parts << "SYNTAX %s" % [ self.syntax_oid ]
110
+ parts << self.extensions.strip unless self.extensions.empty?
111
+
112
+ return "( %s )" % [ parts.join(' ') ]
113
+ end
114
+
115
+
92
116
  ### Return a human-readable representation of the object suitable for debugging
93
117
  def inspect
94
118
  return "#<%s:0x%0x %s(%s) %s %sSYNTAX: %p>" % [
@@ -90,6 +90,30 @@ class Treequel::Schema::MatchingRuleUse
90
90
  end
91
91
 
92
92
 
93
+ # MatchingRuleUseDescription = LPAREN WSP
94
+ # numericoid ; object identifier
95
+ # [ SP "NAME" SP qdescrs ] ; short names (descriptors)
96
+ # [ SP "DESC" SP qdstring ] ; description
97
+ # [ SP "OBSOLETE" ] ; not active
98
+ # SP "APPLIES" SP oids ; attribute types
99
+ # extensions WSP RPAREN ; extensions
100
+
101
+ ### Returns the matchingRuleUse as a String, which is the RFC4512-style schema
102
+ ### description.
103
+ def to_s
104
+ parts = [ self.oid ]
105
+
106
+ parts << "NAME %s" % Treequel::Schema.qdescrs( self.names ) unless self.names.empty?
107
+
108
+ parts << "DESC '%s'" % [ self.desc ] if self.desc
109
+ parts << "OBSOLETE" if self.obsolete?
110
+ parts << "APPLIES %s" % [ Treequel::Schema.oids(self.attr_oids) ]
111
+ parts << self.extensions.strip unless self.extensions.empty?
112
+
113
+ return "( %s )" % [ parts.join(' ') ]
114
+ end
115
+
116
+
93
117
  ### Return a human-readable representation of the object suitable for debugging
94
118
  def inspect
95
119
  return "#<%s:0x%0x %s(%s) %p -> %p >" % [