ame 0.1.1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  2. data/README +541 -3
  3. data/Rakefile +15 -6
  4. data/lib/ame-1.0.rb +31 -0
  5. data/lib/ame-1.0/argument.rb +63 -0
  6. data/lib/ame-1.0/arguments.rb +44 -0
  7. data/lib/ame-1.0/arguments/complete.rb +37 -0
  8. data/lib/ame-1.0/arguments/optional.rb +34 -0
  9. data/lib/ame-1.0/arguments/undefined.rb +71 -0
  10. data/lib/ame-1.0/class.rb +436 -0
  11. data/lib/ame-1.0/flag.rb +101 -0
  12. data/lib/{ame → ame-1.0}/help.rb +1 -1
  13. data/lib/ame-1.0/help/delegate.rb +19 -0
  14. data/lib/ame-1.0/help/terminal.rb +132 -0
  15. data/lib/ame-1.0/method.rb +75 -0
  16. data/lib/ame-1.0/method/undefined.rb +184 -0
  17. data/lib/ame-1.0/methods.rb +40 -0
  18. data/lib/ame-1.0/multioption.rb +36 -0
  19. data/lib/ame-1.0/option.rb +37 -0
  20. data/lib/ame-1.0/optional.rb +31 -0
  21. data/lib/ame-1.0/options.rb +68 -0
  22. data/lib/ame-1.0/options/undefined.rb +114 -0
  23. data/lib/ame-1.0/root.rb +174 -0
  24. data/lib/ame-1.0/splat.rb +16 -0
  25. data/lib/ame-1.0/splus.rb +22 -0
  26. data/lib/ame-1.0/switch.rb +39 -0
  27. data/lib/ame-1.0/types.rb +60 -0
  28. data/lib/ame-1.0/types/boolean.rb +13 -0
  29. data/lib/ame-1.0/types/enumeration.rb +40 -0
  30. data/lib/ame-1.0/types/float.rb +11 -0
  31. data/lib/{ame → ame-1.0}/types/integer.rb +3 -3
  32. data/lib/{ame → ame-1.0}/types/string.rb +2 -2
  33. data/lib/ame-1.0/types/symbol.rb +9 -0
  34. data/lib/ame-1.0/version.rb +62 -0
  35. data/test/unit/ame-1.0.rb +4 -0
  36. data/test/unit/ame-1.0/argument.rb +46 -0
  37. data/test/unit/ame-1.0/arguments.rb +63 -0
  38. data/test/unit/ame-1.0/arguments/complete.rb +4 -0
  39. data/test/unit/ame-1.0/arguments/optional.rb +4 -0
  40. data/test/unit/ame-1.0/arguments/undefined.rb +63 -0
  41. data/test/unit/ame-1.0/class.rb +4 -0
  42. data/test/unit/ame-1.0/flag.rb +31 -0
  43. data/test/unit/ame-1.0/help.rb +4 -0
  44. data/test/unit/ame-1.0/help/delegate.rb +4 -0
  45. data/test/unit/{ame/help/console.rb → ame-1.0/help/terminal.rb} +34 -23
  46. data/test/unit/ame-1.0/method.rb +4 -0
  47. data/test/unit/ame-1.0/method/undefined.rb +33 -0
  48. data/test/unit/ame-1.0/methods.rb +9 -0
  49. data/test/unit/ame-1.0/multioption.rb +4 -0
  50. data/test/unit/ame-1.0/option.rb +11 -0
  51. data/test/unit/ame-1.0/optional.rb +9 -0
  52. data/test/unit/ame-1.0/options.rb +149 -0
  53. data/test/unit/ame-1.0/options/undefined.rb +33 -0
  54. data/test/unit/ame-1.0/root.rb +4 -0
  55. data/test/unit/ame-1.0/splat.rb +9 -0
  56. data/test/unit/ame-1.0/splus.rb +4 -0
  57. data/test/unit/ame-1.0/switch.rb +15 -0
  58. data/test/unit/ame-1.0/types.rb +4 -0
  59. data/test/{ame → unit/ame-1.0}/types/boolean.rb +0 -0
  60. data/test/unit/ame-1.0/types/enumeration.rb +4 -0
  61. data/test/unit/ame-1.0/types/float.rb +7 -0
  62. data/test/{ame → unit/ame-1.0}/types/integer.rb +0 -0
  63. data/test/{ame → unit/ame-1.0}/types/string.rb +0 -0
  64. data/test/unit/ame-1.0/types/symbol.rb +5 -0
  65. data/test/unit/ame-1.0/version.rb +4 -0
  66. metadata +690 -60
  67. data/lib/ame.rb +0 -26
  68. data/lib/ame/argument.rb +0 -56
  69. data/lib/ame/arguments.rb +0 -65
  70. data/lib/ame/class.rb +0 -117
  71. data/lib/ame/help/console.rb +0 -96
  72. data/lib/ame/method.rb +0 -94
  73. data/lib/ame/methods.rb +0 -30
  74. data/lib/ame/option.rb +0 -50
  75. data/lib/ame/options.rb +0 -102
  76. data/lib/ame/root.rb +0 -57
  77. data/lib/ame/splat.rb +0 -12
  78. data/lib/ame/types.rb +0 -29
  79. data/lib/ame/types/array.rb +0 -16
  80. data/lib/ame/types/boolean.rb +0 -16
  81. data/lib/ame/version.rb +0 -5
  82. data/test/ame/types/array.rb +0 -13
  83. data/test/unit/ame/argument.rb +0 -66
  84. data/test/unit/ame/arguments.rb +0 -106
  85. data/test/unit/ame/method.rb +0 -40
  86. data/test/unit/ame/methods.rb +0 -10
  87. data/test/unit/ame/option.rb +0 -75
  88. data/test/unit/ame/options.rb +0 -136
  89. data/test/unit/ame/root.rb +0 -15
  90. data/test/unit/ame/splat.rb +0 -11
data/Rakefile CHANGED
@@ -1,9 +1,18 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- require 'lookout/rake/tasks'
4
- require 'yard'
3
+ require 'inventory-rake-1.0'
5
4
 
6
- Lookout::Rake::Tasks::Test.new
7
- Lookout::Rake::Tasks::Gem.new
8
- Lookout::Rake::Tasks::Tags.new
9
- YARD::Rake::YardocTask.new
5
+ load File.expand_path('../lib/ame-1.0/version.rb', __FILE__)
6
+
7
+ Inventory::Rake::Tasks.define Ame::Version
8
+
9
+ Inventory::Rake::Tasks.unless_installing_dependencies do
10
+ require 'lookout-rake-3.0'
11
+ Lookout::Rake::Tasks::Test.new
12
+
13
+ require 'inventory-rake-tasks-yard-1.0'
14
+ Inventory::Rake::Tasks::YARD.new do |t|
15
+ t.options += %w'--plugin yard-heuristics-1.0'
16
+ t.globals[:source_code_url] = 'https://github.com/now/%s/blob/v%s/%%s#L%%d' % [t.inventory.package, t.inventory]
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Namespace for Ame.
4
+ # @api developer/user
5
+ module Ame
6
+ # Value thrown to abort processing of a {Root.process} or {Root.call}.
7
+ AbortAllProcessing = :AmeAbortAllProcessing
8
+
9
+ # Value thrown to abort processing of a {Class.process} or {Class.call}.
10
+ AbortProcessing = :AmeAbortProcessing
11
+
12
+ Error = Class.new(StandardError)
13
+
14
+ # Error raised when a dispatch is invoked on a method that doesn’t exist.
15
+ UnrecognizedMethod = Class.new(Error)
16
+
17
+ # Error raised when an argument can’t be parsed into the desired type.
18
+ MalformedArgument = Class.new(Error)
19
+
20
+ # Error raised when a required argument is missing.
21
+ MissingArgument = Class.new(Error)
22
+
23
+ # Error raised when too many arguments have been given.
24
+ SuperfluousArgument = Class.new(Error)
25
+
26
+ # Error raised when an unrecognized option has been given.
27
+ UnrecognizedOption = Class.new(Error)
28
+
29
+ load File.expand_path('../ame-1.0/version.rb', __FILE__)
30
+ Version.load
31
+ end
@@ -0,0 +1,63 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Represents an argument to a {Method}. It has a {#name} and a {#description}.
4
+ # It will be called upon to process an argument before the method this argument
5
+ # is associated with gets called.
6
+ # @api developer
7
+ class Ame::Argument
8
+ # @api internal
9
+ # @param [String] name
10
+ # @param [::Class] type
11
+ # @param [String] description
12
+ # @yield [?]
13
+ # @yieldparam [Hash<String, Object>] options
14
+ # @yieldparam [Array<String>] processed
15
+ # @yieldparam [Object] argument
16
+ # @raise [ArgumentError] If TYPE isn’t one that Ame knows how to parse
17
+ def initialize(name, type, description, &validate)
18
+ @name, @description, @validate = name, description, validate || proc{ |_, _, a| a }
19
+ @type = Ame::Types[[type, String].reject(&:nil?).first]
20
+ end
21
+
22
+ # @return [String] The name of the receiver
23
+ attr_reader :name
24
+
25
+ # @return [String] The description of the receiver
26
+ attr_reader :description
27
+
28
+ # @return [String] The upcasing of the {#name} of the receiver
29
+ def to_s
30
+ @to_s ||= name.upcase
31
+ end
32
+
33
+ # Invokes the optional block passed to the receiver when it was created for
34
+ # additional validation and parsing after optionally parsing one or more of
35
+ # the ARGUMENTS passed to it (see subclasses’ {#parse} methods for their
36
+ # behaviour).
37
+ # @api internal
38
+ # @param [Hash<String, Object>] options
39
+ # @param [Array<String>] processed
40
+ # @param [Array<String>] arguments
41
+ # @raise [MissingArgument] If a required argument is missing
42
+ # @raise [MalformedArgument] If an argument can’t be parsed
43
+ # @return [Object]
44
+ def process(options, processed, arguments)
45
+ @validate.call(options, processed, parse(arguments))
46
+ rescue Ame::MalformedArgument, ArgumentError, TypeError => e
47
+ raise Ame::MalformedArgument, '%s: %s' % [self, e]
48
+ end
49
+
50
+ private
51
+
52
+ # Returns the parsed value of the result of ARGUMENTS#shift
53
+ # Should be overridden by subclasses that want different behaviour for
54
+ # missing arguments.
55
+ # @api internal
56
+ # @param [Array<String>] arguments
57
+ # @return [Object] The parsed value of the result of ARGUMENTS#shift
58
+ # @raise [MissingArgument] If ARGUMENTS#empty?
59
+ # @raise [MalformedArgument] If ARGUMENTS#shift is non-nil and can’t be parsed
60
+ def parse(arguments)
61
+ @type.parse(arguments.shift || raise(Ame::MissingArgument, 'missing argument: %s' % self))
62
+ end
63
+ end
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # The arguments to a method in its {Method defined state}. Does the processing
4
+ # of arguments to the method and also enumerates {#each} of the arguments to
5
+ # the method for, for example, help output.
6
+ # @api developer
7
+ class Ame::Arguments
8
+ include Enumerable
9
+
10
+ def initialize(arguments)
11
+ @arguments = arguments
12
+ end
13
+
14
+ # @api internal
15
+ # @param [Hash<String, Object>] options
16
+ # @param [Array<String>] arguments
17
+ # @raise [SuperfluousArgument] If more arguments than required/optional have
18
+ # been given
19
+ # @raise (see Argument#process)
20
+ # @return [Array<Object>] The {Argument#process}ed arguments
21
+ def process(options, arguments)
22
+ unprocessed = arguments.dup
23
+ reduce([]){ |processed, argument|
24
+ processed << argument.process(options, processed, unprocessed)
25
+ }.tap{
26
+ raise Ame::SuperfluousArgument,
27
+ 'superfluous arguments: %s' % unprocessed.join(' ') unless unprocessed.empty?
28
+ }
29
+ end
30
+
31
+ # @overload
32
+ # Enumerates the arguments.
33
+ #
34
+ # @yieldparam [Argument] argument
35
+ # @overload
36
+ # @return [Enumerator<Argument>] An Enumerator over the arguments
37
+ def each
38
+ return enum_for(__method__) unless block_given?
39
+ @arguments.each do |argument|
40
+ yield argument
41
+ end
42
+ self
43
+ end
44
+ end
@@ -0,0 +1,37 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # The arguments to a method in its {Method::Undefined undefined state} where a
4
+ # splat or splus has been added and thus no further arguments are allowed.
5
+ # @api internal
6
+ class Ame::Arguments::Complete < Ame::Arguments::Undefined
7
+ def initialize(arguments, splat_command, splat)
8
+ @arguments, @splat_command, @splat = arguments + [splat], splat_command, splat
9
+ end
10
+
11
+ # @raise [ArgumentError] If a splat or splus argument has been defined
12
+ def argument(name, type, description, &validate)
13
+ error 'argument', name
14
+ end
15
+
16
+ # @raise [ArgumentError] If a splat or splus argument has been defined
17
+ def optional(name, default, description, &validate)
18
+ error 'optional', name
19
+ end
20
+
21
+ # @raise [ArgumentError] If a splat or splus argument has been defined
22
+ def splat(name, type, description, &validate)
23
+ error 'splat', name
24
+ end
25
+
26
+ # @raise [ArgumentError] If a splat or splus argument has been defined
27
+ def splus(name, type, description, &validate)
28
+ error 'splus', name
29
+ end
30
+
31
+ private
32
+
33
+ def error(command, name)
34
+ raise ArgumentError, "%s '%s', … may not follow %s '%s', …" %
35
+ [command, name, @splat_command, @splat.name]
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # The arguments to a method in its {Method::Undefined undefined state} where at
4
+ # least one optional argument has been defined and only further
5
+ # {Optional#optional #optional} or {Undefined#splat #splat} arguments are
6
+ # allowed.
7
+ # @api internal
8
+ class Ame::Arguments::Optional < Ame::Arguments::Undefined
9
+ def initialize(arguments, optional)
10
+ @arguments, @optional = arguments + [optional], optional
11
+ end
12
+
13
+ # @raise [ArgumentError] If an optional argument has been defined
14
+ def argument(name, type, description, &validate)
15
+ raise ArgumentError,
16
+ "argument '%s', … may not follow optional '%s', …" % [name, @optional.name]
17
+ end
18
+
19
+ # @raise [ArgumentError] If an optional argument has been defined
20
+ def splus(name, default, description, &validate)
21
+ raise ArgumentError,
22
+ "splus '%s', … may not follow optional '%s', …" % [name, @optional.name]
23
+ end
24
+
25
+ # Adds a new {Optional} argument to the receiver.
26
+ # @param (see Undefined#optional)
27
+ # @yield (see Undefined#optional)
28
+ # @yieldparam (see Undefined#optional)
29
+ # @raise (see Undefined#optional)
30
+ # @return [self]
31
+ def optional(name, default, description, &validate)
32
+ self << Ame::Optional.new(name, default, description, &validate)
33
+ end
34
+ end
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # The arguments to a method in its {Method::Undefined undefined state} before
4
+ # any optional or splat/splus arguments have been defined. When an {#optional}
5
+ # argument has been added, it’ll enter an {Optional} state where only
6
+ # additional {#optional} and {#splat} arguments are allowed. When a {#splat}
7
+ # or {#splus} has been added, it’ll enter a {Complete} state where no
8
+ # additional arguments are allowed.
9
+ # @api internal
10
+ class Ame::Arguments::Undefined
11
+ def initialize
12
+ @arguments = []
13
+ end
14
+
15
+ # Adds a new {Argument} to the receiver.
16
+ # @param (see Argument#initialize)
17
+ # @yield (see Argument#initialize)
18
+ # @yieldparam (see Argument#initialize)
19
+ # @raise (see Argument#initialize)
20
+ # @return [self]
21
+ def argument(name, type, description, &validate)
22
+ self << Ame::Argument.new(name, type, description, &validate)
23
+ end
24
+
25
+ # Adds a new {Optional} argument to the receiver.
26
+ # @param (see Ame::Optional#initialize)
27
+ # @yield (see Ame::Optional#initialize)
28
+ # @yieldparam (see Ame::Optional#initialize)
29
+ # @raise (see Ame::Optional#initialize)
30
+ # @return [Optional]
31
+ def optional(name, default, description, &validate)
32
+ Ame::Arguments::Optional.new(@arguments, Ame::Optional.new(name, default, description, &validate))
33
+ end
34
+
35
+ # Adds a new {Splat} to the receiver.
36
+ # @param (see Argument#initialize)
37
+ # @yield (see Argument#initialize)
38
+ # @yieldparam (see Argument#initialize)
39
+ # @raise (see Argument#initialize)
40
+ # @return [Complete]
41
+ def splat(name, type, description, &validate)
42
+ Ame::Arguments::Complete.new(@arguments, 'splat', Ame::Splat.new(name, type, description, &validate))
43
+ end
44
+
45
+ # Adds a new {Splus} (required splat) to the receiver.
46
+ # @param (see Argument#initialize)
47
+ # @yield (see Argument#initialize)
48
+ # @yieldparam (see Argument#initialize)
49
+ # @raise (see Argument#initialize)
50
+ # @return [Complete]
51
+ def splus(name, type, description, &validate)
52
+ Ame::Arguments::Complete.new(@arguments, 'splus', Ame::Splus.new(name, type, description, &validate))
53
+ end
54
+
55
+ # @return True if now arguments have been added to the receiver
56
+ def empty?
57
+ @arguments.empty?
58
+ end
59
+
60
+ # @return [Arguments] The defined version of the receiver
61
+ def define
62
+ Ame::Arguments.new(@arguments)
63
+ end
64
+
65
+ protected
66
+
67
+ def <<(argument)
68
+ @arguments << argument
69
+ self
70
+ end
71
+ end
@@ -0,0 +1,436 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # The superclass of a Ruby class that wants to be able to be invoked from the
4
+ # command line (or with any list of String options and arguments). Subclassed
5
+ # by {Root}, which should be used as the root of any command-line processing
6
+ # interface. See {Root} for an example.
7
+ class Ame::Class
8
+ class << self
9
+ # Process ARGUMENTS as a list of options and arguments, then call METHOD
10
+ # with the results of this processing on a new instance of the receiver.
11
+ # This method catches {AbortProcessing}. Options are arguments that begin
12
+ # with `-` or `--`. If {.options_must_precede_arguments} has been called
13
+ # on the receiver, then options must precede arguments. Either way, a `--`
14
+ # argument will always end the processing of options and begin processing
15
+ # of arguments instead.
16
+ # @param [#to_sym] method
17
+ # @param [Array<String>] arguments
18
+ # @raise (see Method#process)
19
+ # @raise [UnrecognizedMethod] If the method argument to a dispatch isn’t a
20
+ # known method
21
+ # @return [self]
22
+ def process(method, arguments = [])
23
+ catch Ame::AbortProcessing do
24
+ methods[method].process new, arguments
25
+ end
26
+ self
27
+ end
28
+
29
+ # Call METHOD with ARGUMENTS and OPTIONS on a new instance of the receiver.
30
+ # This method catches {AbortProcessing}. Options are arguments that begin
31
+ # with `-` or `--`. If {.options_must_precede_arguments} has been called
32
+ # on the receiver, then options must precede arguments. Either way, a `--`
33
+ # argument will always end the processing of options and begin processing
34
+ # of arguments instead.
35
+ # @param [#to_sym] method
36
+ # @param [Array] arguments
37
+ # @param [Hash<String, Object>] options
38
+ # @raise (see Method#call)
39
+ # @raise [UnrecognizedMethod] If the method argument to a dispatch isn’t a
40
+ # known method
41
+ # @return [self]
42
+ def call(method, arguments = nil, options = nil)
43
+ catch Ame::AbortProcessing do
44
+ methods[method].call new, arguments, options
45
+ end
46
+ self
47
+ end
48
+
49
+ # Sets the DESCRIPTION of the method about to be defined, or returns it if
50
+ # DESCRIPTION is nil. The description is used in help output and similar
51
+ # circumstances. A description can only be given to a public method, as
52
+ # only public methods can be used as Ame methods.
53
+ # @param [String, nil] description
54
+ # @return [String]
55
+ # @example Set The Description of the Format-patch Method
56
+ # class Git::CLI::Git::FormatPatch < Ame::Class
57
+ # description 'Prepare patches for e-mail submission'
58
+ # def format_patch
59
+ def description(description = nil)
60
+ return method.description(description) if description
61
+ defined?(@description) ? @description : ''
62
+ end
63
+
64
+ private
65
+
66
+ # Forces options to the method about to be defined to precede any
67
+ # arguments, lest they be seen as arguments. If not given, the behaviour
68
+ # will depend on whether `ENV['POSIXLY_CORRECT']` has been set or not.
69
+ # @return [self]
70
+ def options_must_precede_arguments
71
+ method.options_must_precede_arguments
72
+ self
73
+ end
74
+
75
+ # Defines SHORT and/or LONG as a boolean flag with DEFAULT and DESCRIPTION.
76
+ # An optional block will be used for any validation or further processing
77
+ # of the parsed value of the argument, where OPTIONS are the options
78
+ # processed so far and their values and VALUE is the parsed value itself or
79
+ # the inverse of DEFAULT, if no argument was passed. An argument isn’t
80
+ # required, but if one is to be explicitly given, it must be passed as
81
+ # `-SHORT=ARGUMENT` or `--LONG=ARGUMENT` and may be given as any of “true”,
82
+ # “yes”, “on” or “false”, “no”, “off”. Multiple short flags may be
83
+ # juxtaposed as `-abc`.
84
+ # @param (see Method::Undefined#flag)
85
+ # @yield (see Method::Undefined#flag)
86
+ # @yieldparam (see Method::Undefined#flag)
87
+ # @raise (see Method::Undefined#flag)
88
+ # @return [self]
89
+ # @example A Couple of Flags
90
+ # class Git::CLI::Git::FormatPatch < Ame::Class
91
+ # flag ?n, 'numbered', false, 'Name output in [PATCH n/m] format'
92
+ # flag ?N, 'no-numbered', nil, 'Name output in [PATCH] format' do |opt|
93
+ # opt['numbered'] = false
94
+ # end
95
+ # end
96
+ def flag(short, long, default, description, &validate)
97
+ method.flag short, long, default, description, &validate
98
+ self
99
+ rescue; $!.set_backtrace(caller[1..-1]); raise
100
+ end
101
+
102
+ # Defines SHORT and/or LONG as a boolean toggle with DEFAULT and
103
+ # DESCRIPTION. A toggle acts like a flag, but also supports `--no-LONG`
104
+ # for explicitly specifying that the inverse of the inverse of the default
105
+ # should be used. An optional block will be used for any validation or
106
+ # further processing of the parsed value of the argument, where OPTIONS are
107
+ # the options processed so far and their values and VALUE is the parsed
108
+ # value itself or the inverse of DEFAULT, if no argument was passed. An
109
+ # argument isn’t required, but if one is to be explicitly given, it must be
110
+ # passed as `-SHORT=ARGUMENT` or `--LONG=ARGUMENT` and may be given as any
111
+ # of “true”, “yes”, “on” or “false”, “no”, “off”. Multiple short toggles
112
+ # may be juxtaposed as `-abc`.
113
+ # @param (see Method::Undefined#toggle)
114
+ # @yield (see Method::Undefined#toggle)
115
+ # @yieldparam (see Method::Undefined#toggle)
116
+ # @raise (see Method::Undefined#toggle)
117
+ # @return [self]
118
+ # @example A Toggle
119
+ # class Git::CLI::Git::FormatPatch < Ame::Class
120
+ # toggle ?s, 'signoff', false,
121
+ # 'Add Signed-off-by: line to the commit message'
122
+ # end
123
+ def toggle(short, long, default, description, &validate)
124
+ method.toggle short, long, default, description, &validate
125
+ self
126
+ rescue; $!.set_backtrace(caller[1..-1]); raise
127
+ end
128
+
129
+ # Defines SHORT and/or LONG as a switch with DEFAULT taking ARGUMENT with
130
+ # ARGUMENT_DEFAULT and DESCRIPTION. A switch acts like an option, but the
131
+ # argument is optional and defaults to ARGUMENT_DEFAULT. An optional block
132
+ # will be used for any validation or further processing of the parsed value
133
+ # of the argument, where OPTIONS are the options processed so far and their
134
+ # values and VALUE is the parsed value itself or ARGUMENT_DEFAULT, if no
135
+ # argument was passed. An argument must be passed as `-SHORT=ARG`, or
136
+ # `--LONG=ARG`. Multiple short switches may be juxtaposed as `-abc`. The
137
+ # type of the argument is determined by the type of ARGUMENT_DEFAULT, or
138
+ # the type of DEFAULT if ARGUMENT_DEFAULT is nil. If both are nil, String
139
+ # will be used. Also, if ARGUMENT_DEFAULT’s type responds to \#default,
140
+ # the result of invoking it will be used as the default value of the
141
+ # argument. See {Types::Enumeration} for an example of a type that abuses
142
+ # this fact.
143
+ # @param (see Method::Undefined#switch)
144
+ # @yield (see Method::Undefined#switch)
145
+ # @yieldparam (see Method::Undefined#switch)
146
+ # @raise (see Method::Undefined#switch)
147
+ # @return [self]
148
+ # @example A Switch Using An Enumeration
149
+ # class Git::CLI::Git::FormatPatch < Ame::Class
150
+ # switch '', 'thread', 'STYLE', nil,
151
+ # Ame::Types::Enumeration[:shallow, :deep],
152
+ # 'Controls addition of In-Reply-To and References headers'
153
+ # flag '', 'no-thread', nil,
154
+ # 'Disables addition of In-Reply-To and Reference headers' do |o, _|
155
+ # o.delete 'thread'
156
+ # end
157
+ # end
158
+ def switch(short, long, argument, default, argument_default, description, &validate)
159
+ method.switch short, long, argument, default, argument_default, description, &validate
160
+ self
161
+ rescue; $!.set_backtrace(caller[1..-1]); raise
162
+ end
163
+
164
+ # Defines SHORT and/or LONG as an option with DEFAULT and DESCRIPTION. An
165
+ # option always takes an argument. An optional block will be used for any
166
+ # validation or further processing of the parsed value of the argument,
167
+ # where OPTIONS are the options processed so far and their values and VALUE
168
+ # is the parsed value itself. An argument may be passed as
169
+ # <code>-<em>short</em>ARG</code>, `-SHORT=ARG`, or `--LONG=ARG` or as
170
+ # `-SHORT ARG` or `--LONG ARG`. Multiple short options can thus not be
171
+ # juxtaposed, as anything following the short option will be used as an
172
+ # argument. The type of the argument is determined by the type of DEFAULT,
173
+ # or String if it’s nil.
174
+ # @param (see Method::Undefined#option)
175
+ # @yield (see Method::Undefined#option)
176
+ # @yieldparam (see Method::Undefined#option)
177
+ # @raise (see Method::Undefined#option)
178
+ # @return [self]
179
+ # @example An Option
180
+ # class Git::CLI::Git::FormatPatch < Ame::Class
181
+ # option '', 'start-number', 'N', 1,
182
+ # 'Start numbering the patches at N instead of 1'
183
+ # end
184
+ def option(short, long, argument, default, description, &validate)
185
+ method.option short, long, argument, default, description, &validate
186
+ self
187
+ rescue; $!.set_backtrace(caller[1..-1]); raise
188
+ end
189
+
190
+ # Defines SHORT and/or LONG as a multi-option that takes arguments of TYPE
191
+ # and has DESCRIPTION. A multi-option always takes an argument and may be
192
+ # given any number of times. Each parsed value of each argument will be
193
+ # added to an Array that’s stored in the OPTIONS Hash instead of the usual
194
+ # atom types used for the other forms of options. An optional block will
195
+ # be used for any validation or further processing of the parsed value of
196
+ # the argument, where OPTIONS are the options processed so far and their
197
+ # values and VALUE is the parsed value itself. An argument may be passed as
198
+ # <code>-<em>short</em>ARG</code>, `-SHORT=ARG`, or `--LONG=ARG` or as
199
+ # `-SHORT ARG` or `--long ARG`. Multiple short options can thus not be
200
+ # juxtaposed, as anything following the short option will be used as an
201
+ # argument.
202
+ # @param (see Method::Undefined#multioption)
203
+ # @yield (see Method::Undefined#multioption)
204
+ # @yieldparam (see Method::Undefined#multioption)
205
+ # @raise (see Method::Undefined#multioption)
206
+ # @return [self]
207
+ # @example An Option
208
+ # class Git::CLI::Git::FormatPatch < Ame::Class
209
+ # multioption '', 'to', 'address', String,
210
+ # 'Add a To: header to the email headers'
211
+ # end
212
+ def multioption(short, long, argument, type, description, &validate)
213
+ method.multioption short, long, argument, type, description, &validate
214
+ self
215
+ rescue; $!.set_backtrace(caller[1..-1]); raise
216
+ end
217
+
218
+ # Defines argument NAME of TYPE with DESCRIPTION. An optional block will
219
+ # be used for any validation or further processing of the parsed value of
220
+ # the argument, where OPTIONS are the options processed so far and their
221
+ # values, PROCESSED are the values of the arguments processed so far, and
222
+ # VALUE is the parsed value itself.
223
+ # @param (see Method::Undefined#argument)
224
+ # @yield (see Method::Undefined#argument)
225
+ # @yieldparam (see Method::Undefined#argument)
226
+ # @raise (see Method::Undefined#argument)
227
+ # @return [self]
228
+ # @example An Argument
229
+ # class Git::CLI::Git::Annotate < Ame::Class
230
+ # argument 'FILE', String, 'File to annotate'
231
+ # end
232
+ def argument(name, type, description, &validate)
233
+ method.argument name, type, description, &validate
234
+ self
235
+ rescue; $!.set_backtrace(caller[1..-1]); raise
236
+ end
237
+
238
+ # Defines optional argument NAME with DEFAULT and DESCRIPTION. An optional
239
+ # block will be used for any validation or further processing of the parsed
240
+ # value of the argument, where OPTIONS are the options processed so far and
241
+ # their values, PROCESSED are the values of the arguments processed so far,
242
+ # and VALUE is the parsed value itself or DEFAULT, if no argument was
243
+ # given.
244
+ # @param (see Method::Undefined#optional)
245
+ # @yield (see Method::Undefined#optional)
246
+ # @yieldparam (see Method::Undefined#optional)
247
+ # @raise (see Method::Undefined#optional)
248
+ # @return [self]
249
+ # @example An Optional Argument
250
+ # class Git::CLI::Git::FormatPatch < Ame::Class
251
+ # optional 'SINCE', String, 'Generate patches for commits after SINCE'
252
+ # end
253
+ def optional(name, default, description, &validate)
254
+ method.optional name, default, description, &validate
255
+ self
256
+ rescue; $!.set_backtrace(caller[1..-1]); raise
257
+ end
258
+
259
+ # Defines splat argument NAME of TYPE with DESCRIPTION. A splat argument
260
+ # may be given zero or more times. An optional block will be used for any
261
+ # validation or further processing, where OPTIONS are the options processed
262
+ # so far and their values, PROCESSED are the values of the arguments
263
+ # processed so far, and ARGUMENT is the parsed value itself.
264
+ # @param (see Method::Undefined#splat)
265
+ # @yield (see Method::Undefined#splat)
266
+ # @yieldparam (see Method::Undefined#splat)
267
+ # @raise (see Method::Undefined#splat)
268
+ # @return [self]
269
+ # @example A Splat Argument
270
+ # class Git::CLI::Git::Add < Ame::Class
271
+ # splat 'PATHSPEC', String, 'Files to add content from'
272
+ # end
273
+ def splat(name, type, description, &validate)
274
+ method.splat name, type, description, &validate
275
+ self
276
+ rescue; $!.set_backtrace(caller[1..-1]); raise
277
+ end
278
+
279
+ # Defines required splat argument NAME of TYPE with DESCRIPTION. An
280
+ # optional block will be used for any validation or further processing,
281
+ # where OPTIONS are the options processed so far and their values,
282
+ # PROCESSED are the values of the arguments processed so far, and ARGUMENT
283
+ # is the parsed value itself.
284
+ # @param (see Method::Undefined#splus)
285
+ # @yield (see Method::Undefined#splus)
286
+ # @yieldparam (see Method::Undefined#splus)
287
+ # @raise (see Method::Undefined#splus)
288
+ # @return [self]
289
+ # @example A Splus Argument
290
+ # class Git::CLI::Git::CheckAttr < Ame::Class
291
+ # splus 'PATHNAME', String, 'Files to list attributes of'
292
+ # end
293
+ def splus(name, type, description, &validate)
294
+ method.splus name, type, description, &validate
295
+ self
296
+ rescue; $!.set_backtrace(caller[1..-1]); raise
297
+ end
298
+
299
+ # Sets up a dispatch method to KLASS. A dispatch method delegates
300
+ # processing to another class based on the first argument passed to it.
301
+ # This is useful if you want to support sub-commands in a simple manner.
302
+ # The description of KLASS will be used as the description of the dispatch
303
+ # method.
304
+ # @param [::Class] klass
305
+ # @param [Hash<String, Object>] options
306
+ # @option options [String] :default The default method to run; if given,
307
+ # method argument will be optional
308
+ # @raise [ArgumentError] If any arguments have been defined on the method
309
+ # @return [self]
310
+ # @example A Git-like Command-line Interface
311
+ # class Git::CLI::Git < Ame::Class
312
+ # description 'The stupid content tracker'
313
+ # def initialize; end
314
+ #
315
+ # dispatch Remote
316
+ # end
317
+ # class Git::CLI::Git::Remote < Ame::Class
318
+ # description 'Manage set of remote repositories'
319
+ # def initialize; end
320
+ #
321
+ # description 'Adds a remote named NAME for the repository at URL'
322
+ # argument :name, 'Name of the remote to add'
323
+ # argument :url, 'URL to the repository of the remote to add'
324
+ # def add(name, url)
325
+ # …
326
+ # end
327
+ # end
328
+ def dispatch(klass, options = {})
329
+ klass.parent = self
330
+ description klass.description
331
+ options_must_precede_arguments
332
+ flag '', 'help', nil, 'Display help for this method' do
333
+ help.dispatch methods[klass.basename], klass
334
+ throw Ame::AbortAllProcessing
335
+ end unless method.option? :help
336
+ method.arguments? and
337
+ raise ArgumentError,
338
+ 'arguments may not be defined for a dispatch: %s' % klass
339
+ if options[:default]
340
+ optional 'method', options[:default], 'Method to run'
341
+ else
342
+ argument 'method', String, 'Method to run'
343
+ end
344
+ splat 'arguments', String, 'Arguments to pass to METHOD'
345
+ define_method Ame::Method.ruby_name(klass.basename) do |method, arguments|
346
+ klass.process method, arguments
347
+ end
348
+ self
349
+ rescue; $!.set_backtrace(caller[1..-1]); raise
350
+ end
351
+
352
+ # @api developer
353
+ # @return [Method::Undefined] The undefined method about to be defined
354
+ def method
355
+ @method ||= Ame::Method::Undefined.new(self)
356
+ end
357
+
358
+ protected
359
+
360
+ # @api developer
361
+ # @return [Class] The parent of the receiver
362
+ attr_accessor :parent
363
+
364
+ public
365
+
366
+ # Sets the HELP object to use for displaying usage information, or returns
367
+ # it if HELP is nil. The default is to delegate the request to the
368
+ # {.parent}.
369
+ # @api developer
370
+ # @param [#method, #dispatch, #error, #version] help
371
+ # @return [#method, #dispatch, #error, #version]
372
+ def help(help = nil)
373
+ return @help = help if help
374
+ @help ||= Ame::Help::Delegate.new(parent.help)
375
+ end
376
+
377
+ # Sets or returns, depending on if BASENAME is nil or not, the basename
378
+ # of the receiver. The basename is the downcased last component of the
379
+ # double-colon-separated name of the class with all camel-cased sub-words
380
+ # separated by dashes.
381
+ # @api developer
382
+ # @example Basename of A::B::CToTheD
383
+ # class A::B::CToTheD < Ame::Class; end
384
+ # A::B::CToTheD.basename # ⇒ "c-to-the-d"
385
+ # @param [String, nil] basename
386
+ # @return [String]
387
+ def basename(basename = nil)
388
+ @basename = basename if basename
389
+ return @basename if defined? @basename
390
+ name.split('::').last.scan(/[[:upper:]][[:lower:]]*/).join('-').downcase
391
+ end
392
+
393
+ # @api developer
394
+ # @return [String] The full name of the space-separated concatenation of
395
+ # the basenames of the receiver and its {.parent}s
396
+ # @example Fullname of A::B::CToTheD
397
+ # class A::B::CToTheD < Ame::Class; end
398
+ # A::B::CToTheD.fullname # ⇒ "a b c-to-the-d"
399
+ def fullname
400
+ [].tap{ |names|
401
+ klass = self
402
+ until klass.nil? or klass.basename.empty?
403
+ names << klass.basename
404
+ klass = klass.parent
405
+ end
406
+ }.reverse.join(' ')
407
+ end
408
+
409
+ # @api developer
410
+ # @return [Methods] The methods defined on the receiver
411
+ def methods
412
+ @methods ||= Ame::Methods.new
413
+ end
414
+
415
+ private
416
+
417
+ # Defines the previously undefined {.method} now that it’s been added to
418
+ # the class.
419
+ # @api internal
420
+ # @param [Symbol] ruby_name
421
+ # @raise [ArgumentError] If RUBY_NAME is the name of a non-public method
422
+ # that’s being defined
423
+ # @return [self]
424
+ def method_added(ruby_name)
425
+ if ruby_name == :initialize
426
+ @description = method.define(ruby_name).description
427
+ elsif public_method_defined? ruby_name
428
+ methods << method.define(ruby_name)
429
+ elsif method.valid?
430
+ raise ArgumentError, 'non-public method cannot be used by Ame: %s' % ruby_name, caller
431
+ end
432
+ @method = nil
433
+ self
434
+ end
435
+ end
436
+ end