loquacious 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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