TwP-loquacious 1.0.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.
- data/History.txt +4 -0
- data/README.rdoc +234 -0
- data/Rakefile +43 -0
- data/examples/gutters.rb +30 -0
- data/examples/nested.rb +47 -0
- data/examples/simple.rb +20 -0
- data/lib/loquacious.rb +75 -0
- data/lib/loquacious/configuration.rb +197 -0
- data/lib/loquacious/configuration/help.rb +151 -0
- data/lib/loquacious/configuration/iterator.rb +152 -0
- data/lib/loquacious/core_ext/string.rb +75 -0
- data/loquacious.gemspec +37 -0
- data/spec/configuration_spec.rb +152 -0
- data/spec/help_spec.rb +323 -0
- data/spec/iterator_spec.rb +62 -0
- data/spec/loquacious_spec.rb +22 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +59 -0
- data/spec/string_spec.rb +53 -0
- metadata +92 -0
@@ -0,0 +1,197 @@
|
|
1
|
+
|
2
|
+
module Loquacious
|
3
|
+
|
4
|
+
#
|
5
|
+
#
|
6
|
+
class Configuration
|
7
|
+
|
8
|
+
# :stopdoc:
|
9
|
+
class Error < StandardError; end
|
10
|
+
@table = Hash.new
|
11
|
+
# :startdoc:
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# call-seq:
|
15
|
+
# Configuration.for( name )
|
16
|
+
# Configuration.for( name ) { block }
|
17
|
+
#
|
18
|
+
# Returns the configuration associated with the given _name_. If a
|
19
|
+
# _block_ is given, then it will be used to create the configuration.
|
20
|
+
#
|
21
|
+
# The same _name_ can be used multiple times with different
|
22
|
+
# configuration blocks. Each different block will be used to add to the
|
23
|
+
# configuration; i.e. the configurations are additive.
|
24
|
+
#
|
25
|
+
def for( name, &block )
|
26
|
+
if block.nil?
|
27
|
+
return @table.has_key?(name) ? @table[name] : nil
|
28
|
+
end
|
29
|
+
|
30
|
+
cfg = DSL.evaluate(&block)
|
31
|
+
|
32
|
+
if @table.has_key? name
|
33
|
+
@table[name].merge! cfg
|
34
|
+
else
|
35
|
+
@table[name] = cfg
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# call-seq:
|
40
|
+
# Configuration.help_for( name, opts = {} )
|
41
|
+
#
|
42
|
+
# Returns a Help instance for the configuration associated with the
|
43
|
+
# given _name_. See the Help#initialize method for the options that
|
44
|
+
# can be used with this method.
|
45
|
+
#
|
46
|
+
def help_for( name, opts = {} )
|
47
|
+
::Loquacious::Configuration::Help.new(name, opts)
|
48
|
+
end
|
49
|
+
alias :help :help_for
|
50
|
+
end
|
51
|
+
|
52
|
+
exceptions = %w[instance_of? kind_of? equal?]
|
53
|
+
instance_methods.each do |m|
|
54
|
+
undef_method m unless m[%r/^__/] or exceptions.include? m.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
# Accessor for the description hash.
|
58
|
+
attr_reader :__desc
|
59
|
+
|
60
|
+
# Create a new configuration object and initialize it using an optional
|
61
|
+
# _block_ of code.
|
62
|
+
#
|
63
|
+
def initialize( &block )
|
64
|
+
@__desc = Hash.new
|
65
|
+
self.merge!(DSL.evaluate(&block)) if block
|
66
|
+
end
|
67
|
+
|
68
|
+
# When invoked, an attribute reader and writer are defined for the
|
69
|
+
# _method_. Any arguments given are used to set the value of the
|
70
|
+
# attributes. If a _block_ is given, then the attribute is a nested
|
71
|
+
# configuration and the _block_ is evaluated in the context of a new
|
72
|
+
# configuration object.
|
73
|
+
#
|
74
|
+
def method_missing( method, *args, &block )
|
75
|
+
m = method.to_s.delete('=').to_sym
|
76
|
+
|
77
|
+
__eigenclass_eval "attr_writer :#{m}"
|
78
|
+
__eigenclass_eval <<-CODE
|
79
|
+
def #{m}( *args, &block )
|
80
|
+
v = (1 == args.length ? args.first : args)
|
81
|
+
v = nil if args.empty?
|
82
|
+
v = DSL.evaluate(&block) if block
|
83
|
+
|
84
|
+
return @#{m} unless v
|
85
|
+
|
86
|
+
if @#{m}.kind_of?(Configuration)
|
87
|
+
@#{m}.merge! v
|
88
|
+
else
|
89
|
+
@#{m} = v
|
90
|
+
end
|
91
|
+
return @#{m}
|
92
|
+
end
|
93
|
+
CODE
|
94
|
+
|
95
|
+
__desc[m]
|
96
|
+
self.__send__("#{m}=", nil)
|
97
|
+
self.__send__("#{m}", *args, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Evaluate the given _code_ string in the context of this object's
|
101
|
+
# eigenclass (singleton class).
|
102
|
+
#
|
103
|
+
def __eigenclass_eval( code )
|
104
|
+
ec = class << self; self; end
|
105
|
+
ec.module_eval code
|
106
|
+
rescue StandardError
|
107
|
+
raise Error, "cannot evalutate this code:\n#{code}\n"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Merge the contents of the _other_ configuration into this one. Values
|
111
|
+
# from the _other_ configuratin will overwite values in this
|
112
|
+
# configuration.
|
113
|
+
#
|
114
|
+
# This function is recursive. Nested configurations will be merged with
|
115
|
+
# their counterparts in the _other_ configuration.
|
116
|
+
#
|
117
|
+
def merge!( other )
|
118
|
+
return self if other.equal? self
|
119
|
+
raise Error, "can only merge another Configuration" unless other.kind_of?(Configuration)
|
120
|
+
|
121
|
+
other.__desc.each do |key,desc|
|
122
|
+
value = other.__send__(key)
|
123
|
+
if self.__send__(key).kind_of?(Configuration)
|
124
|
+
self.__send__(key).merge! value
|
125
|
+
else
|
126
|
+
self.__send__("#{key}=", value)
|
127
|
+
end
|
128
|
+
__desc[key] = desc
|
129
|
+
end
|
130
|
+
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
# Implementation of a doman specific language for creating configuration
|
135
|
+
# objects. Blocks of code are evaluted by the DSL which returns a new
|
136
|
+
# configuration object.
|
137
|
+
#
|
138
|
+
class DSL
|
139
|
+
alias :__instance_eval :instance_eval
|
140
|
+
|
141
|
+
instance_methods.each do |m|
|
142
|
+
undef_method m unless m[%r/^__/]
|
143
|
+
end
|
144
|
+
|
145
|
+
# Create a new DSL and evaluate the given _block_ in the context of
|
146
|
+
# the DSL. Returns a newly created configuration object.
|
147
|
+
#
|
148
|
+
def self.evaluate( &block )
|
149
|
+
dsl = self.new(&block)
|
150
|
+
dsl.__config
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns the configuration object.
|
154
|
+
attr_reader :__config
|
155
|
+
|
156
|
+
# Creates a new DSL and evaluates the given _block_ in the context of
|
157
|
+
# the DSL.
|
158
|
+
#
|
159
|
+
def initialize( &block )
|
160
|
+
@description = nil
|
161
|
+
@__config = Configuration.new
|
162
|
+
self.__instance_eval(&block) if block
|
163
|
+
end
|
164
|
+
|
165
|
+
# Dynamically adds the given _method_ to the configuration as an
|
166
|
+
# attribute. The _args_ will be used to set the value of the
|
167
|
+
# attribute. If a _block_ is given then the _args_ are ignored and the
|
168
|
+
# attribute will be a nested configuration object.
|
169
|
+
#
|
170
|
+
def method_missing( method, *args, &block )
|
171
|
+
m = method.to_s.delete('=').to_sym
|
172
|
+
|
173
|
+
opts = args.last.instance_of?(Hash) ? args.pop : {}
|
174
|
+
self.desc(opts[:desc]) if opts.has_key? :desc
|
175
|
+
|
176
|
+
__config.__send__(m, *args, &block)
|
177
|
+
__config.__desc[m] = @description
|
178
|
+
|
179
|
+
@description = nil
|
180
|
+
end
|
181
|
+
|
182
|
+
# Store the _string_ as the description for the next attribute that
|
183
|
+
# will be configured. This description will be overwritten if the
|
184
|
+
# attribute has a description passed as an options hash.
|
185
|
+
#
|
186
|
+
def desc( string )
|
187
|
+
string = string.to_s
|
188
|
+
string.strip!
|
189
|
+
string.gutter!
|
190
|
+
@description = string.empty? ? nil : string
|
191
|
+
end
|
192
|
+
end # class DSL
|
193
|
+
|
194
|
+
end # class Configuration
|
195
|
+
end # module Loquacious
|
196
|
+
|
197
|
+
# EOF
|
@@ -0,0 +1,151 @@
|
|
1
|
+
|
2
|
+
require 'pp'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
class Loquacious::Configuration
|
6
|
+
|
7
|
+
# Generate nicely formatted help messages for a configuration. The Help
|
8
|
+
# class iterates over all the attributes in a configuration and outputs
|
9
|
+
# the name, value, and description to an IO stream. The format of the
|
10
|
+
# messages can be configured, and the description and/or value of the
|
11
|
+
# attribute can be shown or hidden independently.
|
12
|
+
#
|
13
|
+
class Help
|
14
|
+
|
15
|
+
# :stopdoc:
|
16
|
+
@@defaults = {
|
17
|
+
:io => $stdout,
|
18
|
+
:name_leader => ' - '.freeze,
|
19
|
+
:name_length => 0,
|
20
|
+
:name_value_sep => ' => '.freeze,
|
21
|
+
:desc_leader => ' '.freeze
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
class Error < StandardError; end
|
25
|
+
# :startdoc:
|
26
|
+
|
27
|
+
# Create a new Help instance for the given configuration where _config_
|
28
|
+
# can be either a Configuration instance or a configuration name or
|
29
|
+
# symbol. Several options can be provided to determine how the
|
30
|
+
# configuration information will be printed to the IO stream.
|
31
|
+
#
|
32
|
+
# :name_leader String appearing before the attribute name
|
33
|
+
# :name_length Maximum length for an attribute name
|
34
|
+
# :name_value_sep String separating the attribute name from the value
|
35
|
+
# :desc_leader String appearing before the description
|
36
|
+
# :io The IO object where help will be written
|
37
|
+
#
|
38
|
+
# The description is printed before each attribute name and value on its
|
39
|
+
# own line.
|
40
|
+
#
|
41
|
+
def initialize( config, opts = {} )
|
42
|
+
opts = @@defaults.merge opts
|
43
|
+
@config = config.kind_of?(::Loquacious::Configuration) ? config :
|
44
|
+
::Loquacious::Configuration.for(config)
|
45
|
+
|
46
|
+
@io = opts[:io]
|
47
|
+
@name_length = Integer(opts[:name_length])
|
48
|
+
@desc_leader = opts[:desc_leader]
|
49
|
+
|
50
|
+
unless @name_length > 0
|
51
|
+
Iterator.new(@config).each do |node|
|
52
|
+
length = node.name.length
|
53
|
+
@name_length = length if length > @name_length
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
name_leader = opts[:name_leader]
|
58
|
+
name_value_sep = opts[:name_value_sep]
|
59
|
+
extra_length = name_leader.length + name_value_sep.length
|
60
|
+
name_value_sep = name_value_sep.gsub('%', '%%')
|
61
|
+
|
62
|
+
@value_length = 78 - @name_length - extra_length
|
63
|
+
@value_leader = "\n" + ' '*(@name_length + extra_length)
|
64
|
+
@format = "#{name_leader}%-#{@name_length}s#{name_value_sep}%s"
|
65
|
+
@name_format = "#{name_leader}%s"
|
66
|
+
|
67
|
+
@desc_leader.freeze
|
68
|
+
@value_leader.freeze
|
69
|
+
@format.freeze
|
70
|
+
@name_format.freeze
|
71
|
+
end
|
72
|
+
|
73
|
+
# call-seq:
|
74
|
+
# show_attribute( name = nil, opts = {} )
|
75
|
+
#
|
76
|
+
# TODO: finish comments and docos
|
77
|
+
#
|
78
|
+
# show available attributes (with/without descriptions)
|
79
|
+
# show current config
|
80
|
+
# show everything
|
81
|
+
#
|
82
|
+
def show_attribute( name = nil, opts = {} )
|
83
|
+
name, opts = nil, name if name.is_a?(Hash)
|
84
|
+
opts = {
|
85
|
+
:descriptions => true,
|
86
|
+
:values => false
|
87
|
+
}.merge!(opts)
|
88
|
+
|
89
|
+
name = normalize_attr(name)
|
90
|
+
show_description = opts[:descriptions]
|
91
|
+
show_value = opts[:values]
|
92
|
+
|
93
|
+
Iterator.new(@config).each(name) do |node|
|
94
|
+
print_node(node, show_description, show_value)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
alias :show :show_attribute
|
98
|
+
|
99
|
+
# Show all attributes for the configuration. The same options allowed by
|
100
|
+
# the +show+ method are also supported by this method.
|
101
|
+
#
|
102
|
+
def show_all( opts = {} )
|
103
|
+
show_attribute(nil, opts)
|
104
|
+
end
|
105
|
+
alias :show_attributes :show_all
|
106
|
+
|
107
|
+
# Normalize the attribute _name_.
|
108
|
+
#
|
109
|
+
def normalize_attr( name )
|
110
|
+
case name
|
111
|
+
when String, nil; name
|
112
|
+
when Symbol; name.to_s
|
113
|
+
when Array; name.join('.')
|
114
|
+
else
|
115
|
+
raise Error, "cannot convert #{name.inspect} into an attribute identifier"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Format the attribute name, value, and description and print the
|
120
|
+
# results. The value can be printed or not by setting the _show_value_
|
121
|
+
# flag to either +true+ or +false+. The description can be printed or
|
122
|
+
# not by setting the _show_description_ flag to either +true+ or
|
123
|
+
# +false+.
|
124
|
+
#
|
125
|
+
def print_node( node, show_description, show_value )
|
126
|
+
desc = node.desc.to_s.dup
|
127
|
+
show_description = false if desc.empty?
|
128
|
+
@io.puts(desc.indent(@desc_leader)) if show_description
|
129
|
+
@io.puts(format_name(node, show_value))
|
130
|
+
@io.puts if show_description
|
131
|
+
end
|
132
|
+
|
133
|
+
# Format the name of the attribute pointed at by the given _node_. If
|
134
|
+
# the _show_value_ flag is set to +true+, then the attribute value will
|
135
|
+
# also be included in the returned string.
|
136
|
+
#
|
137
|
+
def format_name( node, show_value )
|
138
|
+
name = node.name.reduce @name_length
|
139
|
+
return @name_format % name if node.config? or !show_value
|
140
|
+
|
141
|
+
sio = StringIO.new
|
142
|
+
PP.pp(node.obj, sio, @value_length)
|
143
|
+
sio.seek 0
|
144
|
+
obj = sio.read.chomp.gsub("\n", @value_leader)
|
145
|
+
@format % [name, obj]
|
146
|
+
end
|
147
|
+
|
148
|
+
end # class Help
|
149
|
+
end # module Loquacious
|
150
|
+
|
151
|
+
# EOF
|
@@ -0,0 +1,152 @@
|
|
1
|
+
|
2
|
+
class Loquacious::Configuration
|
3
|
+
|
4
|
+
# Provides an external iteraotr for a Loquacious::Configuration object.
|
5
|
+
# The iterator allows the user to retrieve all the configuration settings
|
6
|
+
# along with their descriptions and values.
|
7
|
+
#
|
8
|
+
# cfg = Configuration.for('foo') {
|
9
|
+
# bar 'value', :desc => 'the bar attribute'
|
10
|
+
# baz 42, :desc => 'the baz attribute'
|
11
|
+
# }
|
12
|
+
#
|
13
|
+
# i = Iterator.new(cfg)
|
14
|
+
# i.each do |node|
|
15
|
+
# puts "#{node.name} :: #{node.desc}"
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Results in
|
19
|
+
#
|
20
|
+
# bar :: the bar attribute
|
21
|
+
# baz :: the baz attribute
|
22
|
+
#
|
23
|
+
class Iterator
|
24
|
+
|
25
|
+
# :stopdoc:
|
26
|
+
attr_reader :stack
|
27
|
+
private :stack
|
28
|
+
# :startdoc:
|
29
|
+
|
30
|
+
# Create a new iterator that will operate on the _config_ (configuration
|
31
|
+
# object). The iterator allows the attributes of the configuration object
|
32
|
+
# to be accessed -- this includes nested configuration objects.
|
33
|
+
#
|
34
|
+
def initialize( config )
|
35
|
+
@config = config
|
36
|
+
@stack = []
|
37
|
+
reset
|
38
|
+
end
|
39
|
+
|
40
|
+
# Iterate over each node in the configuration object yielding each to
|
41
|
+
# the supplied block in turn. The return value of the block is returned
|
42
|
+
# from this method. +nil+ is returned if there are no nodes in the
|
43
|
+
# iterator.
|
44
|
+
#
|
45
|
+
# If an _attribute_ is given, then the iteration starts at that
|
46
|
+
# particular attribute and recurse if it is a nested configuration.
|
47
|
+
# Otherwise, only that attribute is yielded to the block.
|
48
|
+
#
|
49
|
+
def each( attribute = nil )
|
50
|
+
reset
|
51
|
+
rv = nil
|
52
|
+
|
53
|
+
if attribute and !attribute.empty?
|
54
|
+
node = while (n = next_node) do
|
55
|
+
break n if n.name == attribute
|
56
|
+
end
|
57
|
+
return if node.nil?
|
58
|
+
|
59
|
+
rv = yield node
|
60
|
+
return rv unless node.config?
|
61
|
+
|
62
|
+
stack.clear
|
63
|
+
stack << new_frame(node.obj, node.name) if node.config?
|
64
|
+
end
|
65
|
+
|
66
|
+
while (node = next_node) do
|
67
|
+
rv = yield node
|
68
|
+
end
|
69
|
+
return rv
|
70
|
+
end
|
71
|
+
|
72
|
+
# Find the given named _attribute_ in the iterator. Returns a node
|
73
|
+
# representing the attribute; or +nil+ is returned if the named
|
74
|
+
# attribute could not be found.
|
75
|
+
#
|
76
|
+
def find( attribute )
|
77
|
+
attribute = attribute.to_s
|
78
|
+
return if attribute.empty?
|
79
|
+
|
80
|
+
node = self.each {|n| break n if n.name == attribute}
|
81
|
+
reset
|
82
|
+
return node
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Reset the iterator back to the beginning.
|
88
|
+
#
|
89
|
+
def reset
|
90
|
+
stack.clear
|
91
|
+
stack << new_frame(@config)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the next node from the current iteration stack frame. Returns
|
95
|
+
# +nil+ if there are no more nodes in the iterator.
|
96
|
+
#
|
97
|
+
def next_node
|
98
|
+
frame = stack.last
|
99
|
+
node = new_node(frame)
|
100
|
+
|
101
|
+
while node.nil?
|
102
|
+
stack.pop
|
103
|
+
return if stack.empty?
|
104
|
+
frame = stack.last
|
105
|
+
node = new_node(frame)
|
106
|
+
end
|
107
|
+
|
108
|
+
frame.index += 1
|
109
|
+
stack << new_frame(node.obj, node.name) if node.config?
|
110
|
+
|
111
|
+
return node
|
112
|
+
end
|
113
|
+
|
114
|
+
# Create a new stack frame from the given _cfg_ (configuration object)
|
115
|
+
# and the optional _prefix_. The _prefix_ is used to complete the full
|
116
|
+
# name for each attribute key in the configuration object.
|
117
|
+
#
|
118
|
+
def new_frame( cfg, prefix = nil )
|
119
|
+
keys = cfg.__desc.keys.map {|k| k.to_s}
|
120
|
+
keys.sort!
|
121
|
+
keys.map! {|k| k.to_sym}
|
122
|
+
|
123
|
+
Frame.new(cfg, prefix.to_s, keys, 0)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Create the next iteration node from the given stack _frame_. Returns
|
127
|
+
# +nil+ when there are no more nodes in the _frame_.
|
128
|
+
#
|
129
|
+
def new_node( frame )
|
130
|
+
key = frame.keys[frame.index]
|
131
|
+
return if key.nil?
|
132
|
+
|
133
|
+
cfg = frame.config
|
134
|
+
name = frame.prefix.empty? ? key.to_s : frame.prefix + ".#{key}"
|
135
|
+
Node.new(cfg, name, cfg.__desc[key], key)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Structure describing a single iteration stack frame. A new stack frame
|
139
|
+
# is created when we descend into a nested Configuration object.
|
140
|
+
#
|
141
|
+
Frame = Struct.new( :config, :prefix, :keys, :index )
|
142
|
+
|
143
|
+
# This is a single node in a Configuration object. It corresponds to a
|
144
|
+
# single configuration attribute.
|
145
|
+
#
|
146
|
+
Node = Struct.new( :config, :name, :desc, :key ) {
|
147
|
+
def obj() config.__send__(key); end
|
148
|
+
def config?() obj.kind_of? ::Loquacious::Configuration; end
|
149
|
+
}
|
150
|
+
|
151
|
+
end # class Iterator
|
152
|
+
end # class Loquacious::Configuration
|