ame 0.1.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/README +4 -0
- data/Rakefile +9 -0
- data/lib/ame.rb +26 -0
- data/lib/ame/argument.rb +56 -0
- data/lib/ame/arguments.rb +65 -0
- data/lib/ame/class.rb +117 -0
- data/lib/ame/help.rb +5 -0
- data/lib/ame/help/console.rb +96 -0
- data/lib/ame/method.rb +94 -0
- data/lib/ame/methods.rb +30 -0
- data/lib/ame/option.rb +50 -0
- data/lib/ame/options.rb +102 -0
- data/lib/ame/root.rb +57 -0
- data/lib/ame/splat.rb +12 -0
- data/lib/ame/types.rb +29 -0
- data/lib/ame/types/array.rb +16 -0
- data/lib/ame/types/boolean.rb +16 -0
- data/lib/ame/types/integer.rb +11 -0
- data/lib/ame/types/string.rb +9 -0
- data/lib/ame/version.rb +5 -0
- data/test/ame/types/array.rb +13 -0
- data/test/ame/types/boolean.rb +17 -0
- data/test/ame/types/integer.rb +7 -0
- data/test/ame/types/string.rb +6 -0
- data/test/unit/ame/argument.rb +66 -0
- data/test/unit/ame/arguments.rb +106 -0
- data/test/unit/ame/help/console.rb +163 -0
- data/test/unit/ame/method.rb +40 -0
- data/test/unit/ame/methods.rb +10 -0
- data/test/unit/ame/option.rb +75 -0
- data/test/unit/ame/options.rb +136 -0
- data/test/unit/ame/root.rb +15 -0
- data/test/unit/ame/splat.rb +11 -0
- metadata +150 -0
data/README
ADDED
data/Rakefile
ADDED
data/lib/ame.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Ame
|
4
|
+
AbortAllProcessing = :AmeAbortAllProcessing
|
5
|
+
AbortProcessing = :AmeAbortProcessing
|
6
|
+
|
7
|
+
Error = Class.new(StandardError)
|
8
|
+
UnrecognizedMethod = Class.new(Error)
|
9
|
+
MalformedArgument = Class.new(Error)
|
10
|
+
MissingArgument = Class.new(Error)
|
11
|
+
SuperfluousArgument = Class.new(Error)
|
12
|
+
UnrecognizedOption = Class.new(Error)
|
13
|
+
|
14
|
+
autoload :Argument, 'ame/argument'
|
15
|
+
autoload :Arguments, 'ame/arguments'
|
16
|
+
autoload :Class, 'ame/class'
|
17
|
+
autoload :Help, 'ame/help'
|
18
|
+
autoload :Method, 'ame/method'
|
19
|
+
autoload :Methods, 'ame/methods'
|
20
|
+
autoload :Option, 'ame/option'
|
21
|
+
autoload :Options, 'ame/options'
|
22
|
+
autoload :Root, 'ame/root'
|
23
|
+
autoload :Splat, 'ame/splat'
|
24
|
+
autoload :Types, 'ame/types'
|
25
|
+
autoload :Version, 'ame/version'
|
26
|
+
end
|
data/lib/ame/argument.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
class Ame::Argument
|
4
|
+
def initialize(name, description, options = {}, &validate)
|
5
|
+
@name, @description, @validate = name.to_sym, description, validate || DefaultValidate
|
6
|
+
@optional = options.fetch(:optional, false)
|
7
|
+
@type = Ame::Types[[options[:type], options[:default], String].find{ |o| !o.nil? }]
|
8
|
+
set_default options[:default], options[:type] if options.include? :default
|
9
|
+
end
|
10
|
+
|
11
|
+
def arity
|
12
|
+
1
|
13
|
+
end
|
14
|
+
|
15
|
+
def process(options, processed, argument)
|
16
|
+
raise Ame::MissingArgument, 'missing argument: %s' % self if required? and argument.nil?
|
17
|
+
validate(options, processed, argument)
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :name, :description, :default
|
21
|
+
|
22
|
+
def optional?
|
23
|
+
@optional
|
24
|
+
end
|
25
|
+
|
26
|
+
def required?
|
27
|
+
not optional?
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
name.to_s.upcase
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
DefaultValidate = proc{ |options, processed, argument| argument }
|
37
|
+
|
38
|
+
def set_default(value, type)
|
39
|
+
raise ArgumentError,
|
40
|
+
'default value can only be set if optional' unless optional?
|
41
|
+
raise ArgumentError,
|
42
|
+
'default value %s is not of type %s' %
|
43
|
+
[value, type] unless value.nil? or type.nil? or value.is_a? type
|
44
|
+
@default = value
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse(argument)
|
48
|
+
argument.nil? ? default : @type.parse(argument)
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate(options, processed, argument)
|
52
|
+
@validate.call(options, processed, parse(argument))
|
53
|
+
rescue Ame::MalformedArgument, ArgumentError, TypeError => e
|
54
|
+
raise Ame::MalformedArgument, '%s: %s' % [self, e]
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
class Ame::Arguments
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@arguments = []
|
8
|
+
@splat = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def argument(name, description, options = {}, &block)
|
12
|
+
argument = Ame::Argument.new(name, description, options, &block)
|
13
|
+
raise ArgumentError,
|
14
|
+
'argument %s must come before splat argument %s' %
|
15
|
+
[argument.name, splat.name] if @splat
|
16
|
+
raise ArgumentError,
|
17
|
+
'optional argument %s may not precede required argument %s' %
|
18
|
+
[first_optional.name, argument.name] if argument.required? and first_optional
|
19
|
+
@arguments << argument
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def splat(name = nil, description = nil, options = {}, &validate)
|
24
|
+
return @splat unless name
|
25
|
+
splat = Ame::Splat.new(name, description, options, &validate)
|
26
|
+
raise ArgumentError,
|
27
|
+
'splat argument %s already defined: %s' % [@splat.name, splat.name] if @splat
|
28
|
+
raise ArgumentError,
|
29
|
+
'optional argument %s may not precede required splat argument %s' %
|
30
|
+
[first_optional.name, splat.name] if splat.required? and first_optional
|
31
|
+
@splat = splat
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def arity
|
36
|
+
required = @arguments.select{ |a| a.required? }.size +
|
37
|
+
(@splat && @splat.required? ? 1 : 0)
|
38
|
+
@splat || first_optional ? -required - 1 : required
|
39
|
+
end
|
40
|
+
|
41
|
+
def process(options, arguments)
|
42
|
+
unprocessed = arguments.dup
|
43
|
+
reduce([]){ |processed, argument|
|
44
|
+
processed << argument.process(options, processed,
|
45
|
+
argument.arity < 0 ? unprocessed : unprocessed.shift)
|
46
|
+
}.tap{
|
47
|
+
raise Ame::SuperfluousArgument,
|
48
|
+
'superfluous arguments: %s' % unprocessed.join(' ') unless unprocessed.empty?
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def each
|
53
|
+
@arguments.each do |argument|
|
54
|
+
yield argument
|
55
|
+
end
|
56
|
+
yield @splat if @splat
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def first_optional
|
63
|
+
@arguments.find{ |a| a.optional? }
|
64
|
+
end
|
65
|
+
end
|
data/lib/ame/class.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
class Ame::Class
|
4
|
+
class << self
|
5
|
+
def basename(basename = nil)
|
6
|
+
@basename = basename if basename
|
7
|
+
return @basename if defined? @basename
|
8
|
+
name.split('::').last.scan(/[[:upper:]][[:lower:]]*/).join('-').downcase
|
9
|
+
end
|
10
|
+
|
11
|
+
def fullname
|
12
|
+
[].tap{ |names|
|
13
|
+
klass = self
|
14
|
+
until klass.nil? or klass.basename.empty?
|
15
|
+
names << klass.basename
|
16
|
+
klass = klass.parent
|
17
|
+
end
|
18
|
+
}.reverse.join(' ')
|
19
|
+
end
|
20
|
+
|
21
|
+
def description(description = nil)
|
22
|
+
return method.description(description) if description
|
23
|
+
defined?(@description) ? @description : ''
|
24
|
+
end
|
25
|
+
|
26
|
+
def help_for_dispatch(method, subclass)
|
27
|
+
parent.help_for_dispatch(method, subclass)
|
28
|
+
end
|
29
|
+
|
30
|
+
def help_for_method(method)
|
31
|
+
parent.help_for_method(method)
|
32
|
+
end
|
33
|
+
|
34
|
+
def methods
|
35
|
+
@methods ||= Ame::Methods.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def dispatch(klass, options = {})
|
39
|
+
klass.parent = self
|
40
|
+
description klass.description
|
41
|
+
options_must_precede_arguments
|
42
|
+
dispatch = method
|
43
|
+
option :help, 'Display help for this method', :ignore => true do
|
44
|
+
help_for_dispatch dispatch, klass
|
45
|
+
throw Ame::AbortAllProcessing
|
46
|
+
end unless method.options.include? :help
|
47
|
+
method.arguments.arity.zero? or
|
48
|
+
raise ArgumentError,
|
49
|
+
'arguments may not be defined for a dispatch: %s' % klass
|
50
|
+
argument :method, 'Method to run', options.include?(:default) ?
|
51
|
+
{:optional => true, :default => options[:default]} :
|
52
|
+
{}
|
53
|
+
splat :arguments, 'Arguments to pass to METHOD', :optional => true
|
54
|
+
define_method Ame::Method.ruby_name(klass.basename) do |method, arguments|
|
55
|
+
klass.new.process method, arguments
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
attr_accessor :parent
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def options_must_precede_arguments
|
66
|
+
method.options_must_precede_arguments
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def option(name, description, options = {}, &validate)
|
71
|
+
method.option name, description, options, &validate
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def argument(name, description, options = {}, &validate)
|
76
|
+
method.argument name, description, options, &validate
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def splat(name, description, options = {}, &validate)
|
81
|
+
method.splat name, description, options, &validate
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def method
|
86
|
+
@method ||= Ame::Method.new(self)
|
87
|
+
end
|
88
|
+
|
89
|
+
def method_added(name)
|
90
|
+
if name == :initialize
|
91
|
+
@description = method.define(name).description
|
92
|
+
elsif [:process, :call].include? name
|
93
|
+
method.valid? and
|
94
|
+
raise NameError, 'method name reserved by Ame: %s' % name
|
95
|
+
elsif public_method_defined? name
|
96
|
+
methods << method.define(name)
|
97
|
+
elsif method.valid?
|
98
|
+
raise ArgumentError, 'non-public method cannot be used by Ame: %s' % name
|
99
|
+
end
|
100
|
+
@method = Ame::Method.new(self)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def process(name, arguments = [])
|
105
|
+
catch Ame::AbortProcessing do
|
106
|
+
self.class.methods[name].process self, arguments
|
107
|
+
end
|
108
|
+
self
|
109
|
+
end
|
110
|
+
|
111
|
+
def call(name, arguments = nil, options = nil)
|
112
|
+
catch Ame::AbortProcessing do
|
113
|
+
self.class.methods[name].call self, arguments, options
|
114
|
+
end
|
115
|
+
self
|
116
|
+
end
|
117
|
+
end
|
data/lib/ame/help.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
class Ame::Help::Console
|
4
|
+
def initialize(io = $stdout, exit_on_error = true)
|
5
|
+
@io, @exit_on_error = io, exit_on_error
|
6
|
+
end
|
7
|
+
|
8
|
+
def for_dispatch(method, subclass)
|
9
|
+
@io.puts for_method_s(method).tap{ |result|
|
10
|
+
append_group result, 'Methods', :method, subclass.methods.sort_by{ |m| method(m) }
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def for_method(method)
|
15
|
+
@io.puts for_method_s(method)
|
16
|
+
end
|
17
|
+
|
18
|
+
def version(klass, method)
|
19
|
+
@io.puts '%s %s' % [method.name, klass.const_get(:Version)]
|
20
|
+
end
|
21
|
+
|
22
|
+
def for_error(method, error)
|
23
|
+
(@io == $stdout ? $stderr : @io).puts '%s: %s' % [method, error]
|
24
|
+
if @exit_on_error
|
25
|
+
exit 1
|
26
|
+
else
|
27
|
+
raise error
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def for_method_s(method)
|
34
|
+
['Usage:'].tap{ |result|
|
35
|
+
append result, ' ', method.qualified_name
|
36
|
+
append result, ' ', options_usage(method.options)
|
37
|
+
append result, ' ', arguments_usage(method.arguments)
|
38
|
+
result << "\n"
|
39
|
+
append result, ' ', method.description
|
40
|
+
append_group result, 'Arguments', :argument, method.arguments
|
41
|
+
append_group result, 'Options', :option, method.options.sort_by{ |o| (o.short or o.long).to_s }
|
42
|
+
}.join('')
|
43
|
+
end
|
44
|
+
|
45
|
+
def append(result, prefix, string)
|
46
|
+
result << prefix << string unless string.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
def append_group(result, heading, display, objects)
|
50
|
+
longest = objects.map{ |o| send(display, o).length }.max
|
51
|
+
append result, "\n\n%s:\n" % heading,
|
52
|
+
objects.map{ |o| ' %-*s %s' % [longest, send(display, o), o.description] }.join("\n")
|
53
|
+
end
|
54
|
+
|
55
|
+
def options_usage(options)
|
56
|
+
options.count > 0 ? '[OPTIONS]...' : ''
|
57
|
+
end
|
58
|
+
|
59
|
+
def arguments_usage(arguments)
|
60
|
+
arguments.map{ |a|
|
61
|
+
if a.optional? and a.arity < 0 then '[%s]...'
|
62
|
+
elsif a.optional? then '[%s]'
|
63
|
+
elsif a.arity < 0 then '%s...'
|
64
|
+
else '%s'
|
65
|
+
end % a
|
66
|
+
}.join(' ')
|
67
|
+
end
|
68
|
+
|
69
|
+
def argument(argument)
|
70
|
+
result = argument.to_s
|
71
|
+
result << '=%s' % argument.default if argument.default
|
72
|
+
result = '[%s]' % result if argument.optional?
|
73
|
+
result << '...' if argument.arity < 0
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
def option(option)
|
78
|
+
if not option.long and option.argument_name.empty?
|
79
|
+
'-%s' % option.short
|
80
|
+
elsif not option.long
|
81
|
+
'-%s=%s' % [option.short, option.argument_name.upcase]
|
82
|
+
elsif option.short and option.argument_name.empty?
|
83
|
+
'-%s, --%s' % [option.short, option.long]
|
84
|
+
elsif option.short
|
85
|
+
'-%s, --%s=%s' % [option.short, option.long, option.argument_name.upcase]
|
86
|
+
elsif option.argument_name.empty?
|
87
|
+
' --%s' % option.long
|
88
|
+
else
|
89
|
+
' --%s=%s' % [option.long, option.argument_name.upcase]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def method(method)
|
94
|
+
method.name.to_s
|
95
|
+
end
|
96
|
+
end
|
data/lib/ame/method.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
class Ame::Method
|
4
|
+
class << self
|
5
|
+
def ruby_name(name)
|
6
|
+
name.to_s.gsub('-', '_').to_sym
|
7
|
+
end
|
8
|
+
|
9
|
+
def name(name)
|
10
|
+
name.to_s.gsub('_', '-').to_sym
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(klass)
|
15
|
+
@class = klass
|
16
|
+
@description = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def description(description = nil)
|
20
|
+
return @description unless description
|
21
|
+
@description = description
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def options_must_precede_arguments
|
26
|
+
self.options.options_must_precede_arguments
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def option(name, description, options = {}, &validate)
|
31
|
+
self.options.option name, description, options, &validate
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def argument(name, description, options = {}, &validate)
|
36
|
+
arguments.argument name, description, options, &validate
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def splat(name, description, options = {}, &validate)
|
41
|
+
arguments.splat name, description, options, &validate
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def arity
|
46
|
+
arguments.arity
|
47
|
+
end
|
48
|
+
|
49
|
+
def define(name)
|
50
|
+
self.name = name
|
51
|
+
option :help, 'Display help for this method', :ignore => true do
|
52
|
+
@class.help_for_method self
|
53
|
+
throw Ame::AbortAllProcessing
|
54
|
+
end unless options.include? :help
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid?
|
59
|
+
not description.nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
def process(instance, arguments)
|
63
|
+
options, remainder = self.options.process(arguments)
|
64
|
+
call(instance, self.arguments.process(options, remainder), options)
|
65
|
+
end
|
66
|
+
|
67
|
+
def call(instance, arguments = nil, options = nil)
|
68
|
+
options, remainder = self.options.process([]) unless options
|
69
|
+
arguments ||= self.arguments.process(options, [])
|
70
|
+
instance.send ruby_name, *(arguments + (options.empty? ? [] : [options]))
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_reader :name, :ruby_name
|
75
|
+
|
76
|
+
def qualified_name
|
77
|
+
[@class.fullname, name.to_s].reject{ |n| n.empty? }.join(' ')
|
78
|
+
end
|
79
|
+
|
80
|
+
def options
|
81
|
+
@options ||= Ame::Options.new
|
82
|
+
end
|
83
|
+
|
84
|
+
def arguments
|
85
|
+
@arguments ||= Ame::Arguments.new
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def name=(name)
|
91
|
+
@ruby_name = name
|
92
|
+
@name = self.class.name(name)
|
93
|
+
end
|
94
|
+
end
|