treequel 1.2.2 → 1.3.0pre384

Sign up to get free protection for your applications and to get access to all the features.
data/History.md CHANGED
@@ -1,3 +1,42 @@
1
+ ## 1.3.0 [2011-01-07] Michael Granger <ged@FaerieMUD.org>
2
+
3
+ Enhancements:
4
+
5
+ * Made Treequel::Model act more like an ORM -- changes made to the object aren't synced
6
+ with the directory until #save is called. New methods:
7
+ - Treequel::Model#save
8
+ - Treequel::Model#modifications
9
+ - Treequel::Model#modifications_ldif
10
+ - Treequel::Model#validate
11
+ - Treequel::Model#valid?
12
+ - Treequel::Model#errors
13
+ - Treequel::Model#revert
14
+ - Treequel::Model#modified?
15
+ New classes:
16
+ - Treequel::Model::Errors
17
+ - Treequel::ValidationFailed
18
+ * Extracted the controls behavior and rewrote the control specs to use it. This is
19
+ so people who may wish to implement their own controls can ensure that it's
20
+ compatible with Treequel.
21
+ * Added a directory-introspection tool (treewhat)
22
+ * Added Treequel::Model::ObjectClass.create for easy creation of entries that conform
23
+ to an objectClass mixin's criteria
24
+ * Treequel::Directory.root_dse now returns Treequel::Branches
25
+ * Added Treequel::Directory#reconnect.
26
+
27
+ Bugfixes:
28
+
29
+ * Fixed a bug in Treequel::Branch#merge for values that need conversion
30
+ * Simplified and removed duplication from the logging code
31
+ * Fixed a bug in the proxy method for single-letter attribute names.
32
+ * Monkeypatched Date for LDAP time type conversions
33
+ * Change the return values of unset attributes to distinguish between SINGLE and non-SINGLE
34
+ attributes
35
+ * Treequel::Branch
36
+ - Check for explicit nil DN in .new
37
+ - Check for nil parent_dn in #parent
38
+ - Use 'top' instead of :top as objectClass default
39
+
1
40
 
2
41
  ## 1.2.2 [2010-12-14] Michael Granger <ged@FaerieMUD.org>
3
42
 
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2008-2010, Michael Granger and Mahlon E. Smith
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of the author/s, nor the names of the project's
15
+ contributors may be used to endorse or promote products derived from this
16
+ software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -75,11 +75,34 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
75
75
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
76
76
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
77
77
 
78
+ This software includes some code from the Sequel database toolkit, used under
79
+ the following license terms:
80
+
81
+ Copyright (c) 2007-2008 Sharon Rosner
82
+ Copyright (c) 2008-2010 Jeremy Evans
83
+
84
+ Permission is hereby granted, free of charge, to any person obtaining a copy
85
+ of this software and associated documentation files (the "Software"), to
86
+ deal in the Software without restriction, including without limitation the
87
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
88
+ sell copies of the Software, and to permit persons to whom the Software is
89
+ furnished to do so, subject to the following conditions:
90
+
91
+ The above copyright notice and this permission notice shall be included in
92
+ all copies or substantial portions of the Software.
93
+
94
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
95
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
96
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
97
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
98
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
99
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
100
+
78
101
 
79
102
  ## Authors
80
103
 
81
- * Michael Granger
82
- * Mahlon E. Smith
104
+ * Michael Granger <ged@FaerieMUD.org>
105
+ * Mahlon E. Smith <mahlon@martini.nu>
83
106
 
84
107
 
85
108
  ## Contributors
data/Rakefile CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  require 'hoe'
4
4
 
5
- Hoe.plugin :hg
5
+ Hoe.plugin :mercurial
6
6
  Hoe.plugin :yard
7
7
  Hoe.plugin :signing
8
+ Hoe.plugin :manualgen
8
9
 
9
10
  Hoe.plugins.delete :rubyforge
10
11
 
@@ -15,47 +16,81 @@ hoespec = Hoe.spec 'treequel' do
15
16
  self.developer 'Michael Granger', 'ged@FaerieMUD.org'
16
17
  self.developer 'Mahlon E. Smith', 'mahlon@martini.nu'
17
18
 
18
-
19
- self.extra_deps <<
20
- ['ruby-ldap', '~> 0.9.11']
21
- self.extra_dev_deps <<
22
- ['rspec', '~> 2.1.0'] <<
23
- ['ruby-termios', '~> 0.9.6'] <<
24
- ['ruby-terminfo', '~> 0.1.1'] <<
25
- ['yard', '~> 0.6.1']
19
+ self.extra_deps.push *{
20
+ 'ruby-ldap' => '~> 0.9.11'
21
+ }
22
+ self.extra_dev_deps.push *{
23
+ 'rspec' => '~> 2.4.0',
24
+ 'ruby-termios' => '~> 0.9.6',
25
+ 'ruby-terminfo' => '~> 0.1.1',
26
+ 'columnize' => '~> 0.3.1',
27
+ }
26
28
 
27
29
  self.spec_extras[:licenses] = ["BSD"]
28
30
  self.spec_extras[:post_install_message] = [
29
- "If you want to use the included 'treequel' LDAP shell, you'll need to install",
30
- "the following libraries as well:",
31
- " - termios",
32
- " - ruby-terminfo",
33
- " - columnize",
34
- ].join( "\n" )
35
- self.spec_extras[:signing_key] = '/Volumes/Keys/ged-private_gem_key.pem'
31
+ "If you want to use the included 'treequel' LDAP shell, you'll need to install",
32
+ "the following libraries as well:",
33
+ '',
34
+ " - termios",
35
+ " - ruby-terminfo",
36
+ " - columnize",
37
+ '',
38
+ "You can install those automatically if you use the --development flag when",
39
+ "installing Treequel."
40
+ ].join( "\n" )
41
+ self.spec_extras[:signing_key] = '/Volumes/Keys/ged-private_gem_key.pem'
36
42
 
37
43
  self.require_ruby_version( '>=1.8.7' )
38
44
 
39
- self.hg_sign_tags = true
45
+ self.hg_sign_tags = true if self.respond_to?( :hg_sign_tags= )
46
+ self.manual_source_dir = 'src' if self.respond_to?( :manual_source_dir= )
47
+ self.yard_opts = [ '--use-cache', '--protected', '--verbose' ] if
48
+ self.respond_to?( :yard_opts= )
40
49
 
41
- self.yard_opts = [ '--use-cache', '--protected', '--verbose' ]
50
+ self.rdoc_locations << "deveiate:/usr/local/www/public/code/#{remote_rdoc_dir}"
42
51
  end
43
52
 
44
53
  ENV['VERSION'] ||= hoespec.spec.version.to_s
45
54
 
46
- include Hoe::MercurialHelpers
55
+ begin
56
+ include Hoe::MercurialHelpers
57
+
58
+ task 'hg:precheckin' => :spec
47
59
 
48
- ### Task: prerelease
49
- desc "Append the package build number to package versions"
50
- task :pre do
51
- rev = get_numeric_rev()
52
- trace "Current rev is: %p" % [ rev ]
53
- hoespec.spec.version.version << "pre#{rev}"
54
- Rake::Task[:gem].clear
60
+ ### Task: prerelease
61
+ desc "Append the package build number to package versions"
62
+ task :pre do
63
+ rev = get_numeric_rev()
64
+ trace "Current rev is: %p" % [ rev ]
65
+ hoespec.spec.version.version << "pre#{rev}"
66
+ Rake::Task[:gem].clear
55
67
 
56
- Gem::PackageTask.new( hoespec.spec ) do |pkg|
57
- pkg.need_zip = true
58
- pkg.need_tar = true
68
+ Gem::PackageTask.new( hoespec.spec ) do |pkg|
69
+ pkg.need_zip = true
70
+ pkg.need_tar = true
71
+ end
59
72
  end
73
+
74
+ ### Make the ChangeLog update if the repo has changed since it was last built
75
+ file '.hg/branch'
76
+ file 'ChangeLog' => '.hg/branch' do |task|
77
+ $stderr.puts "Updating the changelog..."
78
+ content = make_changelog()
79
+ File.open( task.name, 'w', 0644 ) do |fh|
80
+ fh.print( content )
81
+ end
82
+ end
83
+
84
+ # Rebuild the ChangeLog immediately before release
85
+ task :prerelease => 'ChangeLog'
86
+
87
+ rescue NameError => err
88
+ task :no_hg_helpers do
89
+ fail "Couldn't define the :pre task: %s: %s" % [ err.class.name, err.message ]
90
+ end
91
+
92
+ task :pre => :no_hg_helpers
93
+ task 'ChangeLog' => :no_hg_helpers
94
+
60
95
  end
61
96
 
data/bin/treequel CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'rubygems'
4
-
5
3
  require 'abbrev'
6
4
  require 'columnize'
7
5
  require 'diff/lcs'
@@ -44,9 +42,9 @@ module IRB # :nodoc:
44
42
  def self.start_session( obj )
45
43
  unless @__initialized
46
44
  args = ARGV
47
- ARGV.replace(ARGV.dup)
48
- IRB.setup(nil)
49
- ARGV.replace(args)
45
+ ARGV.replace( ARGV.dup )
46
+ IRB.setup( nil )
47
+ ARGV.replace( args )
50
48
  @__initialized = true
51
49
  end
52
50
 
@@ -91,16 +89,6 @@ class Treequel::Shell
91
89
  CLEAR_TO_EOL = "\e[K"
92
90
  CLEAR_CURRENT_LINE = "\e[2K"
93
91
 
94
- # Log levels
95
- LOG_LEVELS = {
96
- 'debug' => Logger::DEBUG,
97
- 'info' => Logger::INFO,
98
- 'warn' => Logger::WARN,
99
- 'error' => Logger::ERROR,
100
- 'fatal' => Logger::FATAL,
101
- }.freeze
102
- LOG_LEVEL_NAMES = LOG_LEVELS.invert.freeze
103
-
104
92
  # Valid connect-type arguments
105
93
  VALID_CONNECT_TYPES = %w[tls ssl plain]
106
94
 
@@ -137,6 +125,10 @@ class Treequel::Shell
137
125
  ### the LDAP URI.
138
126
  def self::parse_options( argv )
139
127
  progname = File.basename( $0 )
128
+ loglevels = Treequel::LOG_LEVELS.
129
+ sort_by {|_,lvl| lvl }.
130
+ collect {|name,lvl| name.to_s }.
131
+ join(', ')
140
132
  bind_as = nil
141
133
 
142
134
  oparser = OptionParser.new( "Usage: #{progname} [OPTIONS] [LDAPURL]" ) do |oparser|
@@ -146,10 +138,9 @@ class Treequel::Shell
146
138
  bind_as = dn
147
139
  end
148
140
 
149
- oparser.on( '--loglevel=LEVEL', '-l LEVEL', Treequel::Loggable::LEVEL.keys,
150
- "Set the logging level. Should be one of:",
151
- Treequel::Loggable::LEVEL.keys.collect {|lvl| lvl.to_s } ) do |lvl|
152
- Treequel.logger.level = Treequel::Loggable::LEVEL[ lvl.to_sym ] or
141
+ oparser.on( '--loglevel=LEVEL', '-l LEVEL', Treequel::LOG_LEVELS.keys,
142
+ "Set the logging level. Should be one of:", loglevels ) do |lvl|
143
+ Treequel.logger.level = Treequel::LOG_LEVELS[ lvl ] or
153
144
  raise "Invalid logging level %p" % [ lvl ]
154
145
  end
155
146
 
@@ -234,7 +225,7 @@ class Treequel::Shell
234
225
  # If the user said to bind as someone on the command line, invoke a
235
226
  # 'bind' command before dropping into the command line
236
227
  if bind_as
237
- options = OpenStruct.new
228
+ options = OpenStruct.new # dummy options object
238
229
  self.bind_command( options, bind_as )
239
230
  end
240
231
 
@@ -454,23 +445,23 @@ class Treequel::Shell
454
445
  def log_command( options, *args )
455
446
  newlevel = args.shift
456
447
  if newlevel
457
- if LOG_LEVELS.key?( newlevel )
458
- Treequel.logger.level = LOG_LEVELS[ newlevel ]
448
+ if Treequel::LOG_LEVELS.key?( newlevel )
449
+ Treequel.logger.level = Treequel::LOG_LEVELS[ newlevel ]
459
450
  message "Set log level to: %s" % [ newlevel ]
460
451
  else
461
- levelnames = LOG_LEVEL_NAMES.keys.sort.join(', ')
452
+ levelnames = Treequel::LOG_LEVEL_NAMES.keys.sort.join(', ')
462
453
  raise "Invalid log level %p: valid values are:\n %s" % [ newlevel, levelnames ]
463
454
  end
464
455
  else
465
456
  message "Log level is currently: %s" %
466
- [ LOG_LEVEL_NAMES[Treequel.logger.level] ]
457
+ [ Treequel::LOG_LEVEL_NAMES[Treequel.logger.level] ]
467
458
  end
468
459
  end
469
460
  set_options :log do |oparser, options|
470
461
  oparser.banner = "log [LEVEL]"
471
462
  oparser.separator 'Set the logging level, or display the current level if no level ' +
472
463
  "is given. Valid log levels are: %s" %
473
- LOG_LEVEL_NAMES.keys.sort.join(', ')
464
+ Treequel::LOG_LEVEL_NAMES.keys.sort.join(', ')
474
465
  end
475
466
 
476
467
 
data/bin/treewhat ADDED
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+ require 'abbrev'
5
+ require 'trollop'
6
+ require 'highline'
7
+ require 'shellwords'
8
+ require 'sysexits'
9
+
10
+ require 'treequel'
11
+ require 'treequel/mixins'
12
+ require 'treequel/constants'
13
+
14
+
15
+ # A tool for displaying information about a directory's records and schema artifacts.
16
+ class Treequel::What
17
+ extend Sysexits
18
+ include Sysexits,
19
+ Treequel::Loggable,
20
+ Treequel::ANSIColorUtilities,
21
+ Treequel::Constants::Patterns,
22
+ Treequel::HashUtilities
23
+
24
+ COLOR_SCHEME = HighLine::ColorScheme.new do |scheme|
25
+ scheme[:header] = [ :bold, :yellow ]
26
+ scheme[:subheader] = [ :bold, :white ]
27
+ scheme[:key] = [ :white ]
28
+ scheme[:value] = [ :bold, :white ]
29
+ scheme[:error] = [ :red ]
30
+ scheme[:warning] = [ :yellow ]
31
+ scheme[:message] = [ :reset ]
32
+ end
33
+
34
+
35
+ ### Run the utility with the given +args+.
36
+ def self::run( args )
37
+ HighLine.color_scheme = COLOR_SCHEME
38
+
39
+ oparser = self.make_option_parser
40
+ opts = Trollop.with_standard_exception_handling( oparser ) do
41
+ oparser.parse( args )
42
+ end
43
+
44
+ pattern = oparser.leftovers.join( ' ' ) if oparser.leftovers
45
+
46
+ self.new( opts ).run( pattern )
47
+ exit :ok
48
+
49
+ rescue => err
50
+ Treequel.logger.fatal "Oops: %s: %s" % [ err.class.name, err.message ]
51
+ Treequel.logger.debug { ' ' + err.backtrace.join("\n ") }
52
+
53
+ exit :software_error
54
+ end
55
+
56
+
57
+ ### Create and configure a command-line option parser for the command.
58
+ ### @return [Trollop::Parser] the option parser
59
+ def self::make_option_parser
60
+ progname = File.basename( $0 )
61
+ default_directory = Treequel.directory_from_config
62
+ loglevels = Treequel::LOG_LEVELS.
63
+ sort_by {|name,lvl| lvl }.
64
+ collect {|name,lvl| name.to_s }.
65
+ join( ', ' )
66
+
67
+ return Trollop::Parser.new do
68
+ banner "Usage: #{progname} [OPTIONS] [PATTERN]"
69
+
70
+ text ''
71
+ text %{Search for an object in an LDAP directory that matches PATTERN and } +
72
+ %{display some information about it.}
73
+ text ''
74
+ text %{The PATTERN can be the DN (or RDN relative to the base) of an entry, } +
75
+ %{a search filter, or the name of an artifact in the directory's schema, } +
76
+ %{such as an objectClass, matching rule, syntax, etc.}
77
+ text ''
78
+ text %{If no PATTERN is specified, general information about the directory is } +
79
+ %{output instead.}
80
+ text ''
81
+
82
+ text 'Options:'
83
+ opt :ldapurl, "Specify the directory to connect to.",
84
+ :default => default_directory.uri.to_s
85
+ opt :debug, "Turn debugging on. Also sets the --loglevel to 'debug'."
86
+ opt :loglevel, "Set the logging level. Must be one of: #{loglevels}",
87
+ :default => Treequel::LOG_LEVEL_NAMES[ Treequel.logger.level ]
88
+ opt :binddn, "The DN of the user to bind as. Defaults to anonymous binding.",
89
+ :type => :string
90
+ end
91
+ end
92
+
93
+
94
+ #################################################################
95
+ ### I N S T A N C E M E T H O D S
96
+ #################################################################
97
+
98
+ ### Create a new instance of the command and set it up with the given
99
+ ### +options+.
100
+ def initialize( options )
101
+ Treequel.logger.formatter = Treequel::ColorLogFormatter.new( Treequel.logger )
102
+
103
+ if options.debug
104
+ $DEBUG = true
105
+ $VERBOSE = true
106
+ Treequel.logger.level = Logger::DEBUG
107
+ elsif options.loglevel
108
+ Treequel.logger.level = Treequel::LOG_LEVELS[ options.loglevel ]
109
+ end
110
+
111
+ @options = options
112
+ @prompt = HighLine.new
113
+ @directory = Treequel.directory( options.ldapurl )
114
+
115
+ self.log.debug "Created new treewhat command object for %s" % [ @directory ]
116
+ end
117
+
118
+
119
+ ######
120
+ public
121
+ ######
122
+
123
+ # The LDAP directory the command will connect to
124
+ attr_reader :directory
125
+
126
+ # The Trollop options hash the command will read its configuration from
127
+ attr_reader :options
128
+
129
+ # The HighLine object to use for prompting and displaying stuff
130
+ attr_reader :prompt
131
+
132
+
133
+ ### Display an +object+ highlighted as a header.
134
+ def print_header( object )
135
+ self.prompt.say( self.prompt.color(object.to_s, :header) )
136
+ end
137
+
138
+
139
+
140
+ ### Run the command with the specified +pattern+.
141
+ def run( pattern=nil )
142
+ self.log.debug "Running with pattern = %p" % [ pattern ]
143
+
144
+ self.bind_to_directory if self.options.binddn
145
+
146
+ case pattern
147
+
148
+ # No argument
149
+ when NilClass, ''
150
+ self.show_directory_overview
151
+
152
+ # DN/RDN or filter if it contains a '='
153
+ when /=/
154
+ self.show_entry( pattern )
155
+
156
+ # Otherwise, try to find a schema item that matches
157
+ else
158
+ self.show_schema_artifact( pattern )
159
+ end
160
+
161
+ end
162
+
163
+
164
+ ### Prompt for a password and then bind to the command's directory using the binddn in
165
+ ### the options.
166
+ def bind_to_directory
167
+ binddn = self.options.binddn or
168
+ raise ArgumentError, "no binddn in the options hash?!"
169
+ self.log.debug "Attempting to bind to the directory as %s" % [ binddn ]
170
+
171
+ pass = self.prompt.ask( "password: " ) {|q| q.echo = '*' }
172
+ user = Treequel::Branch.new( self.directory, binddn )
173
+
174
+ self.directory.bind_as( user, pass )
175
+ self.log.debug " bound as %s" % [ user ]
176
+
177
+ return true
178
+ end
179
+
180
+
181
+ ### Show general information about the directory if the user doesn't give a pattern on
182
+ ### the command line.
183
+ def show_directory_overview
184
+ pr = self.prompt
185
+ dir = self.directory
186
+
187
+ self.print_header( dir.uri.to_s )
188
+ pr.say( "\n" )
189
+ pr.say( dir.schema.to_s )
190
+
191
+ end
192
+
193
+
194
+
195
+ #
196
+ # 'Show entry' mode
197
+ #
198
+
199
+ ### Fetch an entry from the directory and display it like Treequel's editing mode.
200
+ def show_entry( pattern )
201
+ dir = self.directory
202
+ branch = Treequel::Branch.new( dir, pattern )
203
+
204
+ if !branch.exists?
205
+ branch = Treequel::Branch.new( dir, pattern + ',' + dir.base_dn )
206
+ end
207
+
208
+ if !branch.exists?
209
+ branch = dir.filter( pattern ).first
210
+ end
211
+
212
+ if !branch
213
+ self.prompt.say( self.prompt.color("No match.", :error) )
214
+ end
215
+
216
+ yaml = self.branch_as_yaml( branch )
217
+ self.prompt.say( yaml )
218
+ end
219
+
220
+
221
+ ### Return the specified Treequel::Branch object as YAML. If +include_operational+ is true,
222
+ ### include the entry's operational attributes. If +extra_objectclasses+ contains
223
+ ### one or more objectClass OIDs, include their MUST and MAY attributes when building the
224
+ ### YAML representation of the branch.
225
+ def branch_as_yaml( object, include_operational=false )
226
+ object.include_operational_attrs = include_operational
227
+
228
+ # Make sure the displayed entry has the MUST attributes
229
+ entryhash = stringify_keys( object.must_attributes_hash )
230
+ entryhash.merge!( object.entry || {} )
231
+ entryhash['objectClass'] ||= []
232
+
233
+ entryhash.delete( 'dn' ) # Special attribute, can't be edited
234
+
235
+ yaml = entryhash.to_yaml
236
+ yaml[ 5, 0 ] = self.prompt.color( "# #{object.dn}\n", :header )
237
+
238
+ # Make comments out of MAY attributes that are unset
239
+ mayhash = stringify_keys( object.may_attributes_hash )
240
+ self.log.debug "MAY hash is: %p" % [ mayhash ]
241
+ mayhash.delete_if {|attrname,val| entryhash.key?(attrname) }
242
+ yaml << mayhash.to_yaml[5..-1].gsub( /\n\n/, "\n" ).gsub( /^/, '# ' )
243
+
244
+ return yaml
245
+ end
246
+
247
+
248
+
249
+
250
+ #
251
+ # 'Show schema artifact' mode
252
+ #
253
+
254
+ SCHEMA_ARTIFACT_TYPES = [
255
+ :object_classes,
256
+ :attribute_types,
257
+ :ldap_syntaxes,
258
+ :matching_rules,
259
+ :matching_rule_uses,
260
+ ]
261
+
262
+ ### Find an artifact in the directory's schema that matches +pattern+, and display it
263
+ ### if it exists.
264
+ def show_schema_artifact( pattern )
265
+ pr = self.prompt
266
+ schema = self.directory.schema
267
+ artifacts = SCHEMA_ARTIFACT_TYPES.
268
+ collect {|type| schema.send( type ).values.uniq }.flatten
269
+
270
+ if match = find_exact_matching_artifact( artifacts, pattern )
271
+ self.display_schema_artifact( match )
272
+ elsif match = find_substring_matching_artifact( artifacts, pattern )
273
+ pr.say( "No exact match. Falling back to substring match:" )
274
+ self.display_schema_artifact( match )
275
+ else
276
+ pr.say( pr.color("No match.", :error) )
277
+ end
278
+ end
279
+
280
+
281
+ ### Display a schema artifact in a readable way.
282
+ def display_schema_artifact( artifact )
283
+ self.prompt.say( artifact.class.name.sub(/.*::/, '') + ' ' )
284
+ self.prompt.say( artifact.to_s )
285
+ end
286
+
287
+
288
+ ### Try to find an artifact in +artifacts+ whose name or oid matches +pattern+ exactly.
289
+ ### Returns the first matching artifact.
290
+ def find_exact_matching_artifact( artifacts, pattern )
291
+ self.log.debug "Trying to find an exact match for %p in %d artifacts." %
292
+ [ pattern, artifacts.length ]
293
+ return artifacts.find do |obj|
294
+ (obj.respond_to?( :names ) && obj.names.map(&:to_s).include?(pattern) ) ||
295
+ (obj.respond_to?( :name ) && obj.name.to_s == pattern ) ||
296
+ (obj.respond_to?( :oid ) && obj.oid == pattern )
297
+ end
298
+ end
299
+
300
+
301
+ ### Try to find an artifact in +artifacts+ whose name or oid contains +pattern+.
302
+ ### Returns the first matching artifact.
303
+ def find_substring_matching_artifact( artifacts, pattern )
304
+ pattern = Regexp.new( Regexp.escape(pattern), Regexp::IGNORECASE )
305
+
306
+ return artifacts.find do |obj|
307
+ (obj.respond_to?( :names ) && obj.names.find {|name| name.to_s =~ pattern} ) ||
308
+ (obj.respond_to?( :name ) && obj.name.to_s =~ pattern ) ||
309
+ (obj.respond_to?( :oid ) && obj.oid =~ pattern )
310
+ end
311
+ end
312
+
313
+
314
+ end # class Treequel::What
315
+
316
+
317
+ Treequel::What.run( ARGV.dup )
318
+