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
@@ -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 >" % [