treequel 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/ChangeLog +354 -0
  2. data/LICENSE +27 -0
  3. data/README +66 -0
  4. data/Rakefile +345 -0
  5. data/Rakefile.local +43 -0
  6. data/bin/treeirb +14 -0
  7. data/bin/treequel +229 -0
  8. data/examples/company-directory.rb +112 -0
  9. data/examples/ldap-monitor.rb +143 -0
  10. data/examples/ldap-monitor/public/css/master.css +328 -0
  11. data/examples/ldap-monitor/public/images/card_small.png +0 -0
  12. data/examples/ldap-monitor/public/images/chain_small.png +0 -0
  13. data/examples/ldap-monitor/public/images/globe_small.png +0 -0
  14. data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
  15. data/examples/ldap-monitor/public/images/plug.png +0 -0
  16. data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
  17. data/examples/ldap-monitor/public/images/tick.png +0 -0
  18. data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
  19. data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
  20. data/examples/ldap-monitor/views/backends.erb +41 -0
  21. data/examples/ldap-monitor/views/connections.erb +74 -0
  22. data/examples/ldap-monitor/views/databases.erb +39 -0
  23. data/examples/ldap-monitor/views/dump_subsystem.erb +14 -0
  24. data/examples/ldap-monitor/views/index.erb +14 -0
  25. data/examples/ldap-monitor/views/layout.erb +35 -0
  26. data/examples/ldap-monitor/views/listeners.erb +30 -0
  27. data/examples/ldap_state.rb +62 -0
  28. data/lib/treequel.rb +145 -0
  29. data/lib/treequel/branch.rb +589 -0
  30. data/lib/treequel/branchcollection.rb +204 -0
  31. data/lib/treequel/branchset.rb +360 -0
  32. data/lib/treequel/constants.rb +604 -0
  33. data/lib/treequel/directory.rb +541 -0
  34. data/lib/treequel/exceptions.rb +32 -0
  35. data/lib/treequel/filter.rb +704 -0
  36. data/lib/treequel/mixins.rb +325 -0
  37. data/lib/treequel/schema.rb +245 -0
  38. data/lib/treequel/schema/attributetype.rb +252 -0
  39. data/lib/treequel/schema/ldapsyntax.rb +96 -0
  40. data/lib/treequel/schema/matchingrule.rb +124 -0
  41. data/lib/treequel/schema/matchingruleuse.rb +124 -0
  42. data/lib/treequel/schema/objectclass.rb +289 -0
  43. data/lib/treequel/sequel_integration.rb +26 -0
  44. data/lib/treequel/utils.rb +169 -0
  45. data/rake/191_compat.rb +26 -0
  46. data/rake/dependencies.rb +76 -0
  47. data/rake/helpers.rb +434 -0
  48. data/rake/hg.rb +261 -0
  49. data/rake/manual.rb +782 -0
  50. data/rake/packaging.rb +135 -0
  51. data/rake/publishing.rb +318 -0
  52. data/rake/rdoc.rb +30 -0
  53. data/rake/style.rb +62 -0
  54. data/rake/svn.rb +668 -0
  55. data/rake/testing.rb +187 -0
  56. data/rake/verifytask.rb +64 -0
  57. data/rake/win32.rb +190 -0
  58. data/spec/lib/constants.rb +93 -0
  59. data/spec/lib/helpers.rb +100 -0
  60. data/spec/treequel/branch_spec.rb +569 -0
  61. data/spec/treequel/branchcollection_spec.rb +213 -0
  62. data/spec/treequel/branchset_spec.rb +376 -0
  63. data/spec/treequel/directory_spec.rb +487 -0
  64. data/spec/treequel/filter_spec.rb +482 -0
  65. data/spec/treequel/mixins_spec.rb +330 -0
  66. data/spec/treequel/schema/attributetype_spec.rb +237 -0
  67. data/spec/treequel/schema/ldapsyntax_spec.rb +83 -0
  68. data/spec/treequel/schema/matchingrule_spec.rb +158 -0
  69. data/spec/treequel/schema/matchingruleuse_spec.rb +137 -0
  70. data/spec/treequel/schema/objectclass_spec.rb +262 -0
  71. data/spec/treequel/schema_spec.rb +118 -0
  72. data/spec/treequel/utils_spec.rb +49 -0
  73. data/spec/treequel_spec.rb +179 -0
  74. metadata +169 -0
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'rbconfig'
4
+ require 'erb'
5
+ require 'etc'
6
+ require 'logger'
7
+
8
+ require 'treequel'
9
+
10
+
11
+ #--
12
+ # A collection of mixins shared between Treequel classes. Stolen mostly from ThingFish.
13
+ #
14
+ module Treequel # :nodoc:
15
+
16
+ # A collection of various delegation code-generators that can be used to define
17
+ # delegation through other methods, to instance variables, etc.
18
+ module Delegation
19
+
20
+ #######
21
+ private
22
+ #######
23
+
24
+ ### Make the body of a delegator method that will delegate to the +name+ method
25
+ ### of the object returned by the +delegate+ method.
26
+ def make_method_delegator( delegate, name )
27
+ error_frame = caller(4)[0]
28
+ file, line = error_frame.split( ':', 2 )
29
+
30
+ # Ruby can't parse obj.method=(*args), so we have to special-case setters...
31
+ if name.to_s =~ /(\w+)=$/
32
+ name = $1
33
+ code = <<-END_CODE
34
+ lambda {|*args| self.#{delegate}.#{name} = *args }
35
+ END_CODE
36
+ else
37
+ code = <<-END_CODE
38
+ lambda {|*args| self.#{delegate}.#{name}(*args) }
39
+ END_CODE
40
+ end
41
+
42
+ return eval( code, nil, file, line.to_i )
43
+ end
44
+
45
+
46
+ ### Make the body of a delegator method that will delegate calls to the +name+
47
+ ### method to the given +ivar+.
48
+ def make_ivar_delegator( ivar, name )
49
+ error_frame = caller(4)[0]
50
+ file, line = error_frame.split( ':', 2 )
51
+
52
+ # Ruby can't parse obj.method=(*args), so we have to special-case setters...
53
+ if name.to_s =~ /(\w+)=$/
54
+ name = $1
55
+ code = <<-END_CODE
56
+ lambda {|*args| #{ivar}.#{name} = *args }
57
+ END_CODE
58
+ else
59
+ code = <<-END_CODE
60
+ lambda {|*args| #{ivar}.#{name}(*args) }
61
+ END_CODE
62
+ end
63
+
64
+ return eval( code, nil, file, line.to_i )
65
+ end
66
+
67
+
68
+ ###############
69
+ module_function
70
+ ###############
71
+
72
+ ### Define the given +delegated_methods+ as delegators to the like-named method
73
+ ### of the return value of the +delegate_method+. Example:
74
+ ###
75
+ ### class MyClass
76
+ ### extend Treequel::Delegation
77
+ ###
78
+ ### # Delegate the #bound?, #err, and #result2error methods to the connection
79
+ ### # object returned by the #connection method. This allows the connection
80
+ ### # to still be loaded on demand/overridden/etc.
81
+ ### def_method_delegators :connection, :bound?, :err, :result2error
82
+ ###
83
+ ### def connection
84
+ ### @connection ||= self.connect
85
+ ### end
86
+ ### end
87
+ ###
88
+ def def_method_delegators( delegate_method, *delegated_methods )
89
+ delegated_methods.each do |name|
90
+ body = make_method_delegator( delegate_method, name )
91
+ define_method( name, &body )
92
+ end
93
+ end
94
+
95
+
96
+ ### Define the given +delegated_methods+ as delegators to the like-named method
97
+ ### of the specified +ivar+. This is pretty much identical with how 'Forwardable'
98
+ ### from the stdlib does delegation, but it's reimplemented here for consistency.
99
+ ###
100
+ ### class MyClass
101
+ ### extend Treequel::Delegation
102
+ ###
103
+ ### # Delegate the #each method to the @collection ivar
104
+ ### def_ivar_delegators :@collection, :each
105
+ ###
106
+ ### end
107
+ ###
108
+ def def_ivar_delegators( ivar, *delegated_methods )
109
+ delegated_methods.each do |name|
110
+ body = make_ivar_delegator( ivar, name )
111
+ define_method( name, &body )
112
+ end
113
+ end
114
+
115
+
116
+ end # module Delegation
117
+
118
+
119
+ #
120
+ # Add logging to a Treequel class. Including classes get #log and #log_debug methods.
121
+ #
122
+ # == Version
123
+ #
124
+ # $Id$
125
+ #
126
+ # == Authors
127
+ #
128
+ # * Michael Granger <ged@FaerieMUD.org>
129
+ #
130
+ # :include: LICENSE
131
+ #
132
+ # --
133
+ #
134
+ # Please see the file LICENSE in the 'docs' directory for licensing details.
135
+ #
136
+ module Loggable
137
+
138
+ LEVEL = {
139
+ :debug => Logger::DEBUG,
140
+ :info => Logger::INFO,
141
+ :warn => Logger::WARN,
142
+ :error => Logger::ERROR,
143
+ :fatal => Logger::FATAL,
144
+ }
145
+
146
+ ### A logging proxy class that wraps calls to the logger into calls that include
147
+ ### the name of the calling class.
148
+ class ClassNameProxy # :nodoc:
149
+
150
+ ### Create a new proxy for the given +klass+.
151
+ def initialize( klass, force_debug=false )
152
+ @classname = klass.name
153
+ @force_debug = force_debug
154
+ end
155
+
156
+ ### Delegate calls the global logger with the class name as the 'progname'
157
+ ### argument.
158
+ def method_missing( sym, msg=nil, &block )
159
+ return super unless LEVEL.key?( sym )
160
+ sym = :debug if @force_debug
161
+ Treequel.logger.add( LEVEL[sym], msg, @classname, &block )
162
+ end
163
+ end # ClassNameProxy
164
+
165
+ #########
166
+ protected
167
+ #########
168
+
169
+ ### Copy constructor -- clear the original's log proxy.
170
+ def initialize_copy( original )
171
+ @log_proxy = @log_debug_proxy = nil
172
+ super
173
+ end
174
+
175
+ ### Return the proxied logger.
176
+ def log
177
+ @log_proxy ||= ClassNameProxy.new( self.class )
178
+ end
179
+
180
+ ### Return a proxied "debug" logger that ignores other level specification.
181
+ def log_debug
182
+ @log_debug_proxy ||= ClassNameProxy.new( self.class, true )
183
+ end
184
+ end # module Loggable
185
+
186
+
187
+ ### A collection of utilities for working with Hashes.
188
+ module HashUtilities
189
+
190
+ ###############
191
+ module_function
192
+ ###############
193
+
194
+ ### Return a version of the given +hash+ with its keys transformed
195
+ ### into Strings from whatever they were before.
196
+ def stringify_keys( hash )
197
+ newhash = {}
198
+
199
+ hash.each do |key,val|
200
+ if val.is_a?( Hash )
201
+ newhash[ key.to_s ] = stringify_keys( val )
202
+ else
203
+ newhash[ key.to_s ] = val
204
+ end
205
+ end
206
+
207
+ return newhash
208
+ end
209
+
210
+
211
+ ### Return a duplicate of the given +hash+ with its identifier-like keys
212
+ ### transformed into symbols from whatever they were before.
213
+ def symbolify_keys( hash )
214
+ newhash = {}
215
+
216
+ hash.each do |key,val|
217
+ keysym = key.to_s.dup.untaint.to_sym
218
+
219
+ if val.is_a?( Hash )
220
+ newhash[ keysym ] = symbolify_keys( val )
221
+ else
222
+ newhash[ keysym ] = val
223
+ end
224
+ end
225
+
226
+ return newhash
227
+ end
228
+ alias_method :internify_keys, :symbolify_keys
229
+
230
+
231
+ # Recursive hash-merge function
232
+ def merge_recursively( key, oldval, newval )
233
+ case oldval
234
+ when Hash
235
+ case newval
236
+ when Hash
237
+ oldval.merge( newval, &method(:merge_recursively) )
238
+ else
239
+ newval
240
+ end
241
+
242
+ when Array
243
+ case newval
244
+ when Array
245
+ oldval | newval
246
+ else
247
+ newval
248
+ end
249
+
250
+ else
251
+ newval
252
+ end
253
+ end
254
+
255
+ end # HashUtilities
256
+
257
+
258
+ ### A collection of utilities for working with Arrays.
259
+ module ArrayUtilities
260
+
261
+ ###############
262
+ module_function
263
+ ###############
264
+
265
+ ### Return a version of the given +array+ with any Symbols contained in it turned into
266
+ ### Strings.
267
+ def stringify_array( array )
268
+ return array.collect do |item|
269
+ case item
270
+ when Symbol
271
+ item.to_s
272
+ when Array
273
+ stringify_array( item )
274
+ else
275
+ item
276
+ end
277
+ end
278
+ end
279
+
280
+
281
+ ### Return a version of the given +array+ with any Strings contained in it turned into
282
+ ### Symbols.
283
+ def symbolify_array( array )
284
+ return array.collect do |item|
285
+ case item
286
+ when String
287
+ item.to_sym
288
+ when Array
289
+ symbolify_array( item )
290
+ else
291
+ item
292
+ end
293
+ end
294
+ end
295
+
296
+ end # module ArrayUtilities
297
+
298
+
299
+ ### A collection of attribute declaration functions
300
+ module AttributeDeclarations
301
+
302
+ ###############
303
+ module_function
304
+ ###############
305
+
306
+ ### Declare predicate accessors for the attributes associated with the specified
307
+ ### +symbols+.
308
+ def predicate_attr( *symbols )
309
+ symbols.each do |attrname|
310
+ define_method( "#{attrname}?" ) do
311
+ instance_variable_get( "@#{attrname}" ) ? true : false
312
+ end
313
+ define_method( "#{attrname}=" ) do |newval|
314
+ instance_variable_set( "@#{attrname}", newval ? true : false )
315
+ end
316
+ alias_method "is_#{attrname}?", "#{attrname}?"
317
+ end
318
+ end
319
+
320
+ end # module AttributeDeclarations
321
+
322
+ end # module Treequel
323
+
324
+ # vim: set nosta noet ts=4 sw=4:
325
+
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ldap'
4
+ require 'ldap/schema'
5
+
6
+ require 'treequel'
7
+ require 'treequel/constants'
8
+ require 'treequel/mixins'
9
+
10
+
11
+ # This is an object that is used to parse and query a directory's schema
12
+ #
13
+ # == Authors
14
+ #
15
+ # * Michael Granger <ged@FaerieMUD.org>
16
+ # * Mahlon E. Smith <mahlon@martini.nu>
17
+ #
18
+ # :include: LICENSE
19
+ #
20
+ #--
21
+ #
22
+ # Please see the file LICENSE in the base directory for licensing details.
23
+ #
24
+ class Treequel::Schema
25
+ include Treequel::Loggable,
26
+ Treequel::Constants::Patterns
27
+
28
+ require 'treequel/schema/objectclass'
29
+ require 'treequel/schema/attributetype'
30
+ require 'treequel/schema/matchingrule'
31
+ require 'treequel/schema/matchingruleuse'
32
+ require 'treequel/schema/ldapsyntax'
33
+
34
+
35
+ #################################################################
36
+ ### C L A S S M E T H O D S
37
+ #################################################################
38
+
39
+ ### Parse the given +oidstring+ into an Array of OIDs, with Strings for numeric OIDs and
40
+ ### Symbols for aliases.
41
+ def self::parse_oids( oidstring )
42
+ return [] unless oidstring
43
+
44
+ unless /^ #{OIDS} $/x.match( oidstring.strip )
45
+ raise Treequel::ParseError, "couldn't find an OIDLIST in %p" % [ oidstring ]
46
+ end
47
+
48
+ oids = $MATCH
49
+ # Treequel.logger.debug " found OIDs: %p" % [ oids ]
50
+
51
+ # If it's an OIDLIST, strip off leading and trailing parens and whitespace, then split
52
+ # on ' $ ' and parse each OID
53
+ if oids.include?( '$' )
54
+ parse_oid = self.method( :parse_oid )
55
+ return $MATCH[1..-2].strip.split( /#{WSP} #{DOLLAR} #{WSP}/x ).collect( &parse_oid )
56
+
57
+ else
58
+ return [ self.parse_oid(oids) ]
59
+
60
+ end
61
+
62
+ end
63
+
64
+
65
+ ### Parse a single OID into either a numeric OID string or a Symbol.
66
+ def self::parse_oid( oidstring )
67
+ if oidstring =~ NUMERICOID
68
+ return oidstring.untaint
69
+ else
70
+ return oidstring.untaint.to_sym
71
+ end
72
+ end
73
+
74
+
75
+ ### Parse the given short +names+ string (a 'qdescrs' in the BNF) into an Array of zero or
76
+ ### more Strings.
77
+ def self::parse_names( names )
78
+ # Treequel.logger.debug " parsing NAME attribute from: %p" % [ names ]
79
+
80
+ # Unspecified
81
+ if names.nil?
82
+ # Treequel.logger.debug " no NAME attribute"
83
+ return []
84
+
85
+ # Multi-value
86
+ elsif names =~ /#{LPAREN} #{WSP} (#{QDESCRLIST}) #{WSP} #{RPAREN}/x
87
+ # Treequel.logger.debug " parsing a NAME list from %p" % [ $1 ]
88
+ return $1.scan( QDESCR ).collect {|qd| qd[1..-2].untaint.to_sym }
89
+
90
+ # Single-value
91
+ else
92
+ # Return the name without the quotes
93
+ # Treequel.logger.debug " dequoting a single NAME"
94
+ return [ names[1..-2].untaint.to_sym ]
95
+ end
96
+ end
97
+
98
+
99
+ ### Return a new string which is +desc+ with quotes stripped and any escaped characters
100
+ ### un-escaped.
101
+ def self::unquote_desc( desc )
102
+ return nil if desc.nil?
103
+ return desc.gsub( QQ, "'" ).gsub( QS, '\\' )[ 1..-2 ]
104
+ end
105
+
106
+
107
+ #################################################################
108
+ ### I N S T A N C E M E T H O D S
109
+ #################################################################
110
+
111
+ ### Create a new Treequel::Schema from the specified +hash+. The +hash+ should be of the same
112
+ ### form as the one returned by LDAP::Conn.schema, i.e., a Hash of Arrays associated with the
113
+ ### keys "objectClasses", "ldapSyntaxes", "matchingRuleUse", "attributeTypes", and
114
+ ### "matchingRules".
115
+ def initialize( hash )
116
+ @object_classes = self.parse_objectclasses( hash['objectClasses'] )
117
+ @attribute_types = self.parse_attribute_types( hash['attributeTypes'] )
118
+ @ldap_syntaxes = self.parse_ldap_syntaxes( hash['ldapSyntaxes'] )
119
+ @matching_rules = self.parse_matching_rules( hash['matchingRules'] )
120
+ @matching_rule_uses = self.parse_matching_rule_uses( hash['matchingRuleUse'] )
121
+ end
122
+
123
+
124
+ ######
125
+ public
126
+ ######
127
+
128
+ # The Hash of Treequel::Schema::ObjectClass objects, keyed by OID and any associated NAME
129
+ # attributes (as Symbols), that describes the objectClasses in the directory's schema.
130
+ attr_reader :object_classes
131
+
132
+ # The hash of Treequel::Schema::AttributeType objects, keyed by OID and any associated NAME
133
+ # attributes (as Symbols), that describe the attributeTypes in the directory's schema.
134
+ attr_reader :attribute_types
135
+
136
+ # The hash of Treequel::Schema::LDAPSyntax objects, keyed by OID, that describe the
137
+ # syntaxes in the directory's schema.
138
+ attr_reader :ldap_syntaxes
139
+
140
+ # The hash of Treequel::Schema::MatchingRule objects, keyed by OID and any associated NAME
141
+ # attributes (as Symbols), that describe the matchingRules int he directory's schema.
142
+ attr_reader :matching_rules
143
+
144
+ # The hash of Treequel::Schema::MatchingRuleUse objects, keyed by OID and any associated NAME
145
+ # attributes (as Symbols), that describe the attributes to which a matchingRule can be applied.
146
+ attr_reader :matching_rule_uses
147
+
148
+
149
+ ### Return a human-readable representation of the object suitable for debugging.
150
+ def inspect
151
+ ivar_descs = self.instance_variables.sort.collect do |ivar|
152
+ len = self.instance_variable_get( ivar ).length
153
+ "%d %s" % [ len, ivar.gsub(/_/, ' ')[1..-1] ]
154
+ end
155
+ return %{#<%s:0x%0x %s>} % [
156
+ self.class.name,
157
+ self.object_id / 2,
158
+ ivar_descs.join(', '),
159
+ ]
160
+ end
161
+
162
+
163
+ #########
164
+ protected
165
+ #########
166
+
167
+ ### Parse the given objectClass +descriptions+ into Treequel::Schema::ObjectClass objects, and
168
+ ### return them as a Hash keyed both by numeric OID and by each of its NAME attributes (if it
169
+ ### has any).
170
+ def parse_objectclasses( descriptions )
171
+ return descriptions.inject( {} ) do |hash, desc|
172
+ oc = Treequel::Schema::ObjectClass.parse( self, desc ) or
173
+ raise Treequel::Error, "couldn't create an objectClass from %p" % [ desc ]
174
+
175
+ hash[ oc.oid ] = oc
176
+ oc.names.inject( hash ) {|h, name| h[name] = oc; h }
177
+
178
+ hash
179
+ end
180
+ end
181
+
182
+
183
+ ### Parse the given attributeType +descriptions+ into Treequel::Schema::AttributeType objects
184
+ ### and return them as a Hash keyed both by numeric OID and by each of its NAME attributes
185
+ ### (if it has any).
186
+ def parse_attribute_types( descriptions )
187
+ return descriptions.inject( {} ) do |hash, desc|
188
+ attrtype = Treequel::Schema::AttributeType.parse( self, desc ) or
189
+ raise Treequel::Error, "couldn't create an attributeType from %p" % [ desc ]
190
+
191
+ hash[ attrtype.oid ] = attrtype
192
+ attrtype.names.inject( hash ) {|h, name| h[name] = attrtype; h }
193
+
194
+ hash
195
+ end
196
+ end
197
+
198
+
199
+ ### Parse the given LDAP syntax +descriptions+ into Treequel::Schema::LDAPSyntax objects and
200
+ ### return them as a Hash keyed by numeric OID.
201
+ def parse_ldap_syntaxes( descriptions )
202
+ return descriptions.inject( {} ) do |hash, desc|
203
+ syntax = Treequel::Schema::LDAPSyntax.parse( self, desc ) or
204
+ raise Treequel::Error, "couldn't create an LDAPSyntax from %p" % [ desc ]
205
+
206
+ hash[ syntax.oid ] = syntax
207
+ hash
208
+ end
209
+ end
210
+
211
+
212
+ ### Parse the given matchingRule +descriptions+ into Treequel::Schema::MatchingRule objects
213
+ ### and return them as a Hash keyed both by numeric OID and by each of its NAME attributes
214
+ ### (if it has any).
215
+ def parse_matching_rules( descriptions )
216
+ return descriptions.inject( {} ) do |hash, desc|
217
+ rule = Treequel::Schema::MatchingRule.parse( self, desc ) or
218
+ raise Treequel::Error, "couldn't create an matchingRule from %p" % [ desc ]
219
+
220
+ hash[ rule.oid ] = rule
221
+ rule.names.inject( hash ) {|h, name| h[name] = rule; h }
222
+
223
+ hash
224
+ end
225
+ end
226
+
227
+
228
+ ### Parse the given matchingRuleUse +descriptions+ into Treequel::Schema::MatchingRuleUse objects
229
+ ### and return them as a Hash keyed both by numeric OID and by each of its NAME attributes
230
+ ### (if it has any).
231
+ def parse_matching_rule_uses( descriptions )
232
+ return descriptions.inject( {} ) do |hash, desc|
233
+ ruleuse = Treequel::Schema::MatchingRuleUse.parse( self, desc ) or
234
+ raise Treequel::Error, "couldn't create an matchingRuleUse from %p" % [ desc ]
235
+
236
+ hash[ ruleuse.oid ] = ruleuse
237
+ ruleuse.names.inject( hash ) {|h, name| h[name] = ruleuse; h }
238
+
239
+ hash
240
+ end
241
+ end
242
+
243
+
244
+ end # class Treequel::Schema
245
+