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