ame 0.1.1 → 1.0.1

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.
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