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.
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ require 'treequel'
5
+ require 'treequel/model'
6
+ require 'treequel/mixins'
7
+ require 'treequel/constants'
8
+
9
+
10
+ # Mixin that provides Treequel::Model characteristics to a mixin module.
11
+ #
12
+ # The ideas and a large portion of the implementation of this class is borrowed from
13
+ # Sequel under the following license terms:
14
+ #
15
+ # Copyright (c) 2007-2008 Sharon Rosner
16
+ # Copyright (c) 2008-2010 Jeremy Evans
17
+ #
18
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ # of this software and associated documentation files (the "Software"), to
20
+ # deal in the Software without restriction, including without limitation the
21
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
22
+ # sell copies of the Software, and to permit persons to whom the Software is
23
+ # furnished to do so, subject to the following conditions:
24
+ #
25
+ # The above copyright notice and this permission notice shall be included in
26
+ # all copies or substantial portions of the Software.
27
+ #
28
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
31
+ # THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
32
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
33
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34
+ #
35
+ class Treequel::Model::Errors < ::Hash
36
+ include Treequel::HashUtilities,
37
+ Treequel::Loggable
38
+
39
+ # The word to use between attributes in error messages
40
+ ATTRIBUTE_CONJUNCTION = ' and '
41
+
42
+
43
+ ### Set the initializer block to auto-create Array values.
44
+ def initialize( *args )
45
+ block = lambda {|h,k| h[k] = [] }
46
+ super( *args, &block )
47
+ end
48
+
49
+
50
+ ### Adds an error for the given +attribute+.
51
+ ### @param [Symbol, #to_sym] attribute the attribute with an error
52
+ ### @param [String] message the description of the error condition
53
+ def add( attribute, message )
54
+ self[ attribute ] << message
55
+ end
56
+
57
+
58
+ ### Get the number of errors that have been registered.
59
+ ### @Return [Fixnum] the number of errors
60
+ def count
61
+ return self.values.inject( 0 ) {|num, val| num + val.length }
62
+ end
63
+
64
+
65
+ ### Get an Array of messages describing errors which have occurred.
66
+ ### @example
67
+ ### errors.full_messages
68
+ ### # => ['cn is not valid',
69
+ ### # 'uid is not at least 2 letters']
70
+ def full_messages
71
+ return self.inject([]) do |full_messages, (attribute, messages)|
72
+ subject = Array( attribute ).join( ATTRIBUTE_CONJUNCTION )
73
+ messages.each {|part| full_messages << "#{subject} #{part}" }
74
+ full_messages
75
+ end
76
+ end
77
+
78
+ end # class Treequel::Model::Errors
79
+
@@ -9,12 +9,12 @@ require 'treequel/constants'
9
9
 
10
10
  # Mixin that provides Treequel::Model characteristics to a mixin module.
11
11
  module Treequel::Model::ObjectClass
12
+ include Treequel::HashUtilities
13
+
12
14
 
13
15
  ### Extension callback -- add data structures to the extending +mod+.
14
16
  ### @param [Module] mod the mixin module to be extended
15
17
  def self::extended( mod )
16
- # mod.instance_variable_set( :@model_directory, nil )
17
- # mod.instance_variable_set( :@model_bases, [] )
18
18
  mod.instance_variable_set( :@model_class, Treequel::Model )
19
19
  mod.instance_variable_set( :@model_objectclasses, [] )
20
20
  mod.instance_variable_set( :@model_bases, [] )
@@ -75,6 +75,30 @@ module Treequel::Model::ObjectClass
75
75
  end
76
76
 
77
77
 
78
+ ### Instantiate a new Treequel::Model object with given +dn+ and the objectclasses
79
+ ### specified by the receiving module.
80
+ ### @param [#to_s] dn the DN of the new model object
81
+ ### @param [Hash] entryhash attributes to set on the new entry
82
+ def create( directory, dn, entryhash={} )
83
+ entryhash = stringify_keys( entryhash )
84
+
85
+ # Add the objectclasses from the mixin
86
+ entryhash['objectClass'] ||= []
87
+ entryhash['objectClass'].collect!( &:to_s )
88
+ entryhash['objectClass'] |= self.model_objectclasses.map( &:to_s )
89
+
90
+ # Add all the attribute pairs from the RDN bit of the DN to the entry
91
+ rdn_pair, _ = dn.split( /\s*,\s*/, 2 )
92
+ rdn_pair.split( /\+/ ).each do |attrpair|
93
+ k, v = attrpair.split( /\s*=\s*/ )
94
+ entryhash[ k ] ||= []
95
+ entryhash[ k ] << v
96
+ end
97
+
98
+ return self.model_class.new( directory, dn, entryhash )
99
+ end
100
+
101
+
78
102
  ### Return a Branchset (or BranchCollection if the receiver has more than one
79
103
  ### base) that can be used to search the given +directory+ for entries to which
80
104
  ### the receiver applies.
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ #encoding: utf-8
3
+
4
+ require 'treequel/model'
5
+
6
+
7
+ # A collection of schema-based validations for LDAP model objects.
8
+ module Treequel::Model::SchemaValidations
9
+
10
+ ### Entrypoint -- run all the validations, adding any errors to the
11
+ ### object's #error collector.
12
+ def validate( options={} )
13
+ return unless options[:with_schema]
14
+
15
+ self.validate_must_attributes
16
+ self.validate_may_attributes
17
+ self.validate_attribute_syntax
18
+ end
19
+
20
+
21
+ ### Validate that all attributes that MUST be included according to the entry's
22
+ ### objectClasses have at least one value.
23
+ def validate_must_attributes
24
+ self.must_attribute_types.each do |attrtype|
25
+ oid = attrtype.name
26
+ if attrtype.single?
27
+ self.errors.add( oid, "MUST have a value" ) unless self[ oid ]
28
+ else
29
+ self.errors.add( oid, "MUST have at least one value" ) if self[ oid ].empty?
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+ ### Validate that all attributes present in the entry are allowed by either a
36
+ ### MUST or a MAY rule of one of its objectClasses.
37
+ def validate_may_attributes
38
+ hash = (self.entry || {} ).merge( @values )
39
+ attributes = hash.keys.map( &:to_sym ).uniq
40
+ valid_attributes = self.valid_attribute_oids
41
+
42
+ self.log.debug "Validating MAY attributes: %p against the list of valid OIDs: %p" %
43
+ [ attributes, valid_attributes ]
44
+ unknown_attributes = attributes - valid_attributes
45
+ unknown_attributes.each do |oid|
46
+ self.errors.add( oid, "is not allowed by entry's objectClasses" )
47
+ end
48
+ end
49
+
50
+
51
+ ### Validate that the attribute values present in the entry are all valid according to
52
+ ### the syntax rule for it.
53
+ def validate_attribute_syntax
54
+ @values.each do |attribute, values|
55
+ [ values ].flatten.each do |value|
56
+ begin
57
+ self.get_converted_attribute( attribute.to_sym, value )
58
+ rescue => err
59
+ self.log.error "validation for %p failed: %s: %s" %
60
+ [ attribute, err.class.name, err.message ]
61
+ attrtype = self.find_attribute_type( attribute )
62
+ self.errors.add( attribute, "isn't a valid %s value" % [attrtype.syntax.desc] )
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ end # module Treequel::Model::SchemaValidations
69
+
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'date'
4
+
5
+ require 'diff/lcs'
6
+ require 'diff/lcs/change'
7
+
3
8
  require 'ldap'
4
9
  require 'ldap/control'
5
10
 
@@ -57,26 +62,26 @@ module Treequel::TimeExtensions
57
62
  if fraction_digits == 0
58
63
  ''
59
64
  elsif fraction_digits <= 6
60
- '.' + sprintf('%06d', usec)[0, fraction_digits]
65
+ '.' + sprintf('%06d', self.usec)[0, fraction_digits]
61
66
  else
62
- '.' + sprintf('%06d', usec) + '0' * (fraction_digits - 6)
67
+ '.' + sprintf('%06d', self.usec) + '0' * (fraction_digits - 6)
63
68
  end
64
69
  tz =
65
70
  if self.utc?
66
71
  'Z'
67
72
  else
68
- off = utc_offset
73
+ off = self.utc_offset
69
74
  sign = off < 0 ? '-' : '+'
70
75
  "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
71
76
  end
72
77
 
73
78
  return "%02d%02d%02d%02d%02d%02d%s%s" % [
74
- year,
75
- mon,
76
- day,
77
- hour,
78
- min,
79
- sec,
79
+ self.year,
80
+ self.mon,
81
+ self.day,
82
+ self.hour,
83
+ self.min,
84
+ self.sec,
80
85
  fractional_seconds,
81
86
  tz
82
87
  ]
@@ -87,21 +92,21 @@ module Treequel::TimeExtensions
87
92
  ### (UTC Time)
88
93
  def ldap_utc
89
94
  tz =
90
- if utc?
95
+ if self.utc?
91
96
  'Z'
92
97
  else
93
- off = utc_offset
98
+ off = self.utc_offset
94
99
  sign = off < 0 ? '-' : '+'
95
100
  "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
96
101
  end
97
102
 
98
103
  return "%02d%02d%02d%02d%02d%02d%s" % [
99
- year.divmod(100).last,
100
- mon,
101
- day,
102
- hour,
103
- min,
104
- sec,
104
+ self.year.divmod(100).last,
105
+ self.mon,
106
+ self.day,
107
+ self.hour,
108
+ self.min,
109
+ self.sec,
105
110
  tz
106
111
  ]
107
112
  end
@@ -113,3 +118,80 @@ class Time
113
118
  end
114
119
 
115
120
 
121
+ ### Extensions to the Date class to add LDAP (RFC4517) Generalized Time syntax
122
+ module Treequel::DateExtensions
123
+
124
+ ### Return +self+ as a String formatted as specified in RFC4517
125
+ ### (LDAP Generalized Time).
126
+ def ldap_generalized( fraction_digits=0 )
127
+ fractional_seconds =
128
+ if fraction_digits == 0
129
+ ''
130
+ else
131
+ '.' + ('0' * fraction_digits)
132
+ end
133
+
134
+ off = Time.now.utc_offset
135
+ sign = off < 0 ? '-' : '+'
136
+ tz = "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
137
+
138
+ return "%02d%02d%02d%02d%02d%02d%s%s" % [
139
+ self.year,
140
+ self.mon,
141
+ self.day,
142
+ 0,
143
+ 0,
144
+ 1,
145
+ fractional_seconds,
146
+ tz
147
+ ]
148
+
149
+ end
150
+
151
+ ### Returns +self+ as a String formatted as specified in RFC4517
152
+ ### (UTC Time)
153
+ def ldap_utc
154
+ off = Time.now.utc_offset
155
+ sign = off < 0 ? '-' : '+'
156
+ tz = "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
157
+
158
+ return "%02d%02d%02d%02d%02d%02d%s" % [
159
+ self.year.divmod(100).last,
160
+ self.mon,
161
+ self.day,
162
+ 0,
163
+ 0,
164
+ 1,
165
+ tz
166
+ ]
167
+ end
168
+
169
+ end # module Treequel::TimeExtensions
170
+
171
+ class Date
172
+ include Treequel::DateExtensions
173
+ end
174
+
175
+
176
+ ### These three predicates use the wrong instance variable in the library.
177
+ ### :TODO: Submit a patch!
178
+ module Treequel::DiffLCSChangeTypeTestFixes
179
+
180
+ def changed?
181
+ @action == '!'
182
+ end
183
+
184
+ def finished_a?
185
+ @action == '>'
186
+ end
187
+
188
+ def finished_b?
189
+ @action == '<'
190
+ end
191
+
192
+ end
193
+
194
+ class Diff::LCS::ContextChange
195
+ include Treequel::DiffLCSChangeTypeTestFixes
196
+ end
197
+
@@ -209,17 +209,21 @@ class Treequel::Schema
209
209
  end
210
210
 
211
211
 
212
+ ### Return the schema as a human-readable english string.
213
+ def to_s
214
+ parts = [ "Schema:" ]
215
+ parts << self.ivar_descriptions.collect {|desc| ' ' + desc }
216
+ return parts.join( $/ )
217
+ end
218
+
219
+
212
220
  ### Return a human-readable representation of the object suitable for debugging.
213
221
  ### @return [String]
214
222
  def inspect
215
- ivar_descs = self.instance_variables.sort.collect do |ivar|
216
- len = self.instance_variable_get( ivar ).length
217
- "%d %s" % [ len, ivar.to_s.gsub(/_/, ' ')[1..-1] ]
218
- end
219
223
  return %{#<%s:0x%0x %s>} % [
220
224
  self.class.name,
221
225
  self.object_id / 2,
222
- ivar_descs.join(', '),
226
+ self.ivar_descriptions.join( ', ' ),
223
227
  ]
224
228
  end
225
229
 
@@ -308,5 +312,15 @@ class Treequel::Schema
308
312
  end
309
313
 
310
314
 
315
+ ### Return descriptions of the schema's artifacts.
316
+ ### @return [Array<String>] the descriptions of the schema's artifacts, and how many of each
317
+ ### it has.
318
+ def ivar_descriptions
319
+ self.instance_variables.sort.collect do |ivar|
320
+ len = self.instance_variable_get( ivar ).length
321
+ "%d %s" % [ len, ivar.to_s.gsub(/_/, ' ')[1..-1] ]
322
+ end
323
+ end
324
+
311
325
  end # class Treequel::Schema
312
326
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'yaml'
3
4
  require 'ldap'
4
5
  require 'treequel'
5
6
 
@@ -40,7 +41,8 @@ module Treequel::TestConstants # :nodoc:all
40
41
  "2.16.840.1.113730.3.4.2", "1.3.6.1.4.1.4203.1.10.1",
41
42
  "1.2.840.113556.1.4.319", "1.2.826.0.1.334810.2.3",
42
43
  "1.2.826.0.1.3344810.2.3", "1.3.6.1.1.13.2",
43
- "1.3.6.1.1.13.1", "1.3.6.1.1.12"
44
+ "1.3.6.1.1.13.1", "1.3.6.1.1.12",
45
+ "1.2.840.113556.1.4.473", "1.2.840.113556.1.4.474"
44
46
  ],
45
47
  "supportedExtension" => [
46
48
  "1.3.6.1.4.1.1466.20037", "1.3.6.1.4.1.4203.1.11.1",
@@ -50,6 +52,9 @@ module Treequel::TestConstants # :nodoc:all
50
52
  }]
51
53
  TEST_DSE.first.keys.each {|key| TEST_DSE.first[key].freeze }
52
54
 
55
+ SCHEMA_DUMPFILE = Pathname( __FILE__ ).dirname.parent + 'data' + 'schema.yml'
56
+ SCHEMAHASH = LDAP::Schema.new( YAML.load_file(SCHEMA_DUMPFILE) )
57
+ SCHEMA = Treequel::Schema.new( SCHEMAHASH )
53
58
 
54
59
  TEST_HOSTS_DN_ATTR = 'ou'
55
60
  TEST_HOSTS_DN_VALUE = 'Hosts'
@@ -82,7 +87,7 @@ module Treequel::TestConstants # :nodoc:all
82
87
  TEST_PEOPLE_DN = "#{TEST_PEOPLE_RDN},#{TEST_BASE_DN}"
83
88
 
84
89
  TEST_PERSON_DN_ATTR = 'uid'
85
- TEST_PERSON_DN_VALUE = 'arogers'
90
+ TEST_PERSON_DN_VALUE = 'jrandom'
86
91
  TEST_PERSON_RDN = "#{TEST_PERSON_DN_ATTR}=#{TEST_PERSON_DN_VALUE}"
87
92
  TEST_PERSON_DN = "#{TEST_PERSON_RDN},#{TEST_PEOPLE_DN}"
88
93
 
@@ -106,6 +111,19 @@ module Treequel::TestConstants # :nodoc:all
106
111
  TEST_ROOM_RDN = "#{TEST_ROOM_DN_ATTR}=#{TEST_ROOM_DN_VALUE}"
107
112
  TEST_ROOM_DN = "#{TEST_ROOM_RDN},#{TEST_ROOMS_DN}"
108
113
 
114
+ # Multivalue DN
115
+ TEST_HOST_MULTIVALUE_DN_ATTR1 = 'cn'
116
+ TEST_HOST_MULTIVALUE_DN_VALUE1 = 'honcho'
117
+ TEST_HOST_MULTIVALUE_DN_ATTR2 = 'l'
118
+ TEST_HOST_MULTIVALUE_DN_VALUE2 = 'sandiego'
119
+ TEST_HOST_MULTIVALUE_RDN = "%s=%s+%s=%s" % [
120
+ TEST_HOST_MULTIVALUE_DN_ATTR1,
121
+ TEST_HOST_MULTIVALUE_DN_VALUE1,
122
+ TEST_HOST_MULTIVALUE_DN_ATTR2,
123
+ TEST_HOST_MULTIVALUE_DN_VALUE2,
124
+ ]
125
+ TEST_HOST_MULTIVALUE_DN = "#{TEST_HOST_MULTIVALUE_RDN},#{TEST_HOSTS_DN}"
126
+
109
127
  constants.each do |cname|
110
128
  const_get(cname).freeze
111
129
  end