tabry 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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/bin/tabry-bash +48 -0
  3. data/bin/tabry-help +17 -0
  4. data/bin/tabry-test-options +9 -0
  5. data/bin/tabry-test-parse +20 -0
  6. data/lib/tabry/cli/arg_proxy.rb +71 -0
  7. data/lib/tabry/cli/base.rb +34 -0
  8. data/lib/tabry/cli/builder.rb +104 -0
  9. data/lib/tabry/cli/flag_proxy.rb +45 -0
  10. data/lib/tabry/cli/internals.rb +10 -0
  11. data/lib/tabry/cli/util/config.rb +48 -0
  12. data/lib/tabry/cli/util.rb +51 -0
  13. data/lib/tabry/config_loader.rb +55 -0
  14. data/lib/tabry/core_ext/string/colors.rb +91 -0
  15. data/lib/tabry/machine.rb +124 -0
  16. data/lib/tabry/models/arg.rb +43 -0
  17. data/lib/tabry/models/arg_base.rb +10 -0
  18. data/lib/tabry/models/arg_include.rb +9 -0
  19. data/lib/tabry/models/arg_includes.rb +14 -0
  20. data/lib/tabry/models/args_list.rb +31 -0
  21. data/lib/tabry/models/config.rb +44 -0
  22. data/lib/tabry/models/config_error.rb +8 -0
  23. data/lib/tabry/models/config_list.rb +48 -0
  24. data/lib/tabry/models/config_object.rb +78 -0
  25. data/lib/tabry/models/config_string_hash.rb +44 -0
  26. data/lib/tabry/models/const_option.rb +17 -0
  27. data/lib/tabry/models/dir_option.rb +25 -0
  28. data/lib/tabry/models/file_option.rb +25 -0
  29. data/lib/tabry/models/flag.rb +55 -0
  30. data/lib/tabry/models/flags_list.rb +47 -0
  31. data/lib/tabry/models/include_arg.rb +18 -0
  32. data/lib/tabry/models/include_flag.rb +18 -0
  33. data/lib/tabry/models/include_option.rb +22 -0
  34. data/lib/tabry/models/include_sub.rb +18 -0
  35. data/lib/tabry/models/option.rb +33 -0
  36. data/lib/tabry/models/option_base.rb +20 -0
  37. data/lib/tabry/models/option_includes.rb +14 -0
  38. data/lib/tabry/models/options_list.rb +18 -0
  39. data/lib/tabry/models/shell_option.rb +13 -0
  40. data/lib/tabry/models/sub.rb +59 -0
  41. data/lib/tabry/models/subs_list.rb +28 -0
  42. data/lib/tabry/options_finder.rb +87 -0
  43. data/lib/tabry/result.rb +110 -0
  44. data/lib/tabry/runner.rb +27 -0
  45. data/lib/tabry/state.rb +14 -0
  46. data/lib/tabry/usage_generator.rb +137 -0
  47. data/lib/tabry/util.rb +15 -0
  48. data/sh/tabry_bash.sh +61 -0
  49. data/sh/tabry_bash_help.sh +7 -0
  50. data/spec/fixtures/basiccli.json +1 -0
  51. data/spec/fixtures/basiccli.tabry +5 -0
  52. data/spec/fixtures/basiccli2.tabry +5 -0
  53. data/spec/fixtures/basiccli2.yml +7 -0
  54. data/spec/fixtures/vehicles.tabry +60 -0
  55. data/spec/fixtures/vehicles.yaml +135 -0
  56. data/spec/spec_helper.rb +10 -0
  57. data/spec/tabry/cli/arg_proxy_spec.rb +76 -0
  58. data/spec/tabry/cli/builder_spec.rb +226 -0
  59. data/spec/tabry/config_loader_spec.rb +69 -0
  60. data/spec/tabry/machine_spec.rb +109 -0
  61. data/spec/tabry/models/args_list_spec.rb +36 -0
  62. data/spec/tabry/models/config_spec.rb +62 -0
  63. data/spec/tabry/models/const_option_spec.rb +17 -0
  64. data/spec/tabry/models/dir_option_spec.rb +16 -0
  65. data/spec/tabry/models/file_option_spec.rb +16 -0
  66. data/spec/tabry/models/options_list_spec.rb +47 -0
  67. data/spec/tabry/models/shell_option_spec.rb +19 -0
  68. data/spec/tabry/models/subs_list_spec.rb +24 -0
  69. data/spec/tabry/options_finder_spec.rb +91 -0
  70. data/spec/tabry/result_spec.rb +236 -0
  71. data/spec/tabry/runner_spec.rb +35 -0
  72. data/spec/tabry/usage_generator_spec.rb +67 -0
  73. data/tabry.gemspec +27 -0
  74. metadata +189 -0
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../../lib/tabry/models/args_list"
5
+
6
+ describe Tabry::Models::ArgsList do
7
+ subject do
8
+ described_class.new(root: double, raw: [
9
+ { "name" => "foo", "description" => "the foo arg" },
10
+ { "name" => "bar", "description" => "the bar arg" },
11
+ { "name" => "things", "description" => "the waz arg", "varargs" => true },
12
+ ])
13
+ end
14
+
15
+ describe "#[]" do
16
+ it "provides access to arguments by index" do
17
+ expect(subject[0].name).to eq("foo")
18
+ expect(subject[0]).to be_a(Tabry::Models::Arg)
19
+ expect(subject[1].name).to eq("bar")
20
+ expect(subject[2].name).to eq("things")
21
+ expect(subject[2]).to be_a(Tabry::Models::Arg)
22
+ expect(subject[2].varargs?).to be(true)
23
+ end
24
+ end
25
+
26
+ describe "#varargs_arg" do
27
+ it "returns the vararg if there is one" do
28
+ expect(subject.varargs_arg).to eq(subject[2])
29
+ end
30
+
31
+ it "returns nil if there is no varargs" do
32
+ args_list = described_class.new(root: double, raw: [{ "name" => "foo" }])
33
+ expect(args_list.varargs_arg).to be_nil
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../../lib/tabry/models/config"
5
+
6
+ describe Tabry::Models::Config do
7
+ subject { described_class.new(raw: JSON.parse(config_hash.to_json)) }
8
+
9
+ let(:config_hash) do
10
+ {
11
+ cmd: "foo",
12
+ main: {
13
+ subs: [
14
+ {
15
+ name: "sub1",
16
+ description: "first-level sub",
17
+ subs: [
18
+ {
19
+ name: "sub1.1",
20
+ description: "sub under a sub"
21
+ }
22
+ ]
23
+ },
24
+ {
25
+ name: "sub2",
26
+ description: "first-level sub 2",
27
+ subs: [
28
+ { name: "sub21", subs: [{ name: "subs211" }] }
29
+ ],
30
+ }
31
+ ]
32
+ }
33
+ }
34
+ end
35
+
36
+ describe "#dig_sub_array" do
37
+ it "returns the array of subcommands leading up to the specified subcommand" do
38
+ res = subject.dig_sub_array(%w[sub1 sub1.1])
39
+ expect(res.map(&:name)).to eq([nil, "sub1", "sub1.1"])
40
+ expect(res).to eq([
41
+ subject.main,
42
+ subject.main.subs[0],
43
+ subject.main.subs[0].subs[0]
44
+ ])
45
+ end
46
+
47
+ it "returns an array with just the main subcommand if an empty array is passed in" do
48
+ res = subject.dig_sub_array(%w[])
49
+ expect(res).to eq([subject.main])
50
+ end
51
+ end
52
+
53
+ describe "#dig_sub" do
54
+ it "returns the sub referenced" do
55
+ expect(subject.dig_sub(%w[sub2])).to eq(subject.main.subs[1])
56
+ end
57
+
58
+ it "returns the main sub if an empty array is passed" do
59
+ expect(subject.dig_sub(%w[])).to eq(subject.main)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../../lib/tabry/models/const_option"
5
+
6
+ describe Tabry::Models::ConstOption do
7
+ subject do
8
+ described_class.new(root: double, raw: { "type" => "const", "value" => "foobar" })
9
+ end
10
+
11
+ it "filters by prefix" do
12
+ expect(subject.options("")).to eq(%w[foobar])
13
+ expect(subject.options("f")).to eq(%w[foobar])
14
+ expect(subject.options("foo")).to eq(%w[foobar])
15
+ expect(subject.options("w")).to eq(%w[])
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../../lib/tabry/models/dir_option"
5
+
6
+ describe Tabry::Models::DirOption do
7
+ subject do
8
+ described_class.new(root: double, raw: { "type" => "dir" })
9
+ end
10
+
11
+ # Handled by tabru=bash/tabry-bash.sh/shell, we just return a symbol to
12
+ # communicate to tabry-bash
13
+ it "returns a array with a symbol" do
14
+ expect(subject.options("whatever")).to eq([:directory])
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../../lib/tabry/models/file_option"
5
+
6
+ describe Tabry::Models::FileOption do
7
+ subject do
8
+ described_class.new(root: double, raw: { "type" => "file" })
9
+ end
10
+
11
+ # Handled by tabru=bash/tabry-bash.sh/shell, we just return a symbol to
12
+ # communicate to tabry-bash
13
+ it "returns a array with a symbol" do
14
+ expect(subject.options("whatever")).to eq([:file])
15
+ end
16
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../../lib/tabry/models/options_list"
5
+
6
+ describe Tabry::Models::OptionsList do
7
+ subject do
8
+ described_class.new(root: double, raw: JSON.parse(raw_array.to_json))
9
+ end
10
+
11
+ let(:raw_array) do
12
+ [
13
+ { type: "file" },
14
+ { type: "shell", value: "echo a && echo b" },
15
+ { type: "const", value: "foo" },
16
+ { type: "const", value: "bar" },
17
+ { type: "dir" },
18
+ ]
19
+ end
20
+
21
+ it "is enumerable over the options it creates" do
22
+ expect(subject.any? { |opt| opt.type == "dir" }).to be(true)
23
+ expect(subject.map(&:class)).to eq([
24
+ Tabry::Models::FileOption,
25
+ Tabry::Models::ShellOption,
26
+ Tabry::Models::ConstOption,
27
+ Tabry::Models::ConstOption,
28
+ Tabry::Models::DirOption,
29
+ ])
30
+ end
31
+
32
+ describe "options" do
33
+ it "aggregates options from all options" do
34
+ opts_results = [
35
+ %w[foo bar waz],
36
+ %w[a b c],
37
+ %w[1 a foo],
38
+ %w[ok ok ok],
39
+ %w[d c b a]
40
+ ]
41
+ subject.each_with_index do |opt, i|
42
+ expect(opt).to receive(:options).with("some-token").and_return(opts_results[i])
43
+ end
44
+ expect(subject.options("some-token")).to match_array(%w[a b c d foo bar waz ok 1])
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../../lib/tabry/models/shell_option"
5
+
6
+ describe Tabry::Models::ShellOption do
7
+ subject do
8
+ described_class.new(root: double, raw: {
9
+ "type" => "shell", "value" => "echo a && echo a1 && echo b"
10
+ })
11
+ end
12
+
13
+ it "filters by prefix" do
14
+ expect(subject.options("")).to match_array(%w[a a1 b])
15
+ expect(subject.options("a")).to match_array(%w[a a1])
16
+ expect(subject.options("a1")).to match_array(%w[a1])
17
+ expect(subject.options("a1 ")).to match_array(%w[])
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../../lib/tabry/models/subs_list"
5
+
6
+ describe Tabry::Models::SubsList do
7
+ describe "#by_name" do
8
+ subject do
9
+ described_class.new(root: double, raw: [
10
+ { "name" => "foo", "description" => "the foo sub" },
11
+ { "name" => "bar", "description" => "the bar subcommand" },
12
+ { "name" => "waz", "description" => "the waz subcommand" },
13
+ ])
14
+ end
15
+
16
+ it "finds subcommands by name" do
17
+ bar = subject.by_name["bar"]
18
+ expect(bar).to be_a(Tabry::Models::Sub)
19
+ expect(bar.name).to eq("bar")
20
+ expect(bar.description).to eq("the bar subcommand")
21
+ expect(subject.by_name["waz"].name).to eq("waz")
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../lib/tabry/config_loader"
4
+ require_relative "../../lib/tabry/options_finder"
5
+ require_relative "../../lib/tabry/result"
6
+ require_relative "../../lib/tabry/state"
7
+
8
+ describe Tabry::OptionsFinder do
9
+ let(:config_fixture) { "#{__dir__}/../fixtures/vehicles.yaml" }
10
+ let(:config) { Tabry::ConfigLoader.load(name: config_fixture) }
11
+ let(:state) { {} }
12
+
13
+ examples = {
14
+ "lists possible subcommands of the main command" => [
15
+ %w[build list-vehicles move sub-with-sub-or-arg sub-with-mandatory-flag],
16
+ {}
17
+ ],
18
+ "lists possible subcommands of a subcommand" => [
19
+ %w[go stop crash freeway-crash],
20
+ subs: %w[move]
21
+ ],
22
+ "lists possible arguments (const)" => [
23
+ %w[car bike],
24
+ subs: %w[move go]
25
+ ],
26
+ "lists both possible args and subs if a subcommand can take either" => [
27
+ %w[x y z subsub],
28
+ subs: %w[sub-with-sub-or-arg]
29
+ ],
30
+ "lists possible flags if the last token starts with a dash" => [
31
+ %w[--verbose --speed --output-to-file --output-to-directory --dry-run],
32
+ subs: %w[move crash],
33
+ token: "-"
34
+ ],
35
+ "doesn't list a flag if it has already been given" => [
36
+ %w[--verbose --speed --output-to-file --output-to-directory],
37
+ flags: { "dry-run" => true },
38
+ subs: %w[move crash],
39
+ token: "-"
40
+ ],
41
+ "doesn't suggests flags if '--' has been used" => [
42
+ [],
43
+ dashdash: true,
44
+ subs: %w[move crash],
45
+ token: "-",
46
+ ],
47
+ "lists only a mandatory flag if it hasn't been given yet" => [
48
+ %w[--mandatory],
49
+ subs: %w[sub-with-mandatory-flag],
50
+ ],
51
+ "lists other args after a mandatory flag has been given" => [
52
+ %w[a b c],
53
+ subs: %w[sub-with-mandatory-flag],
54
+ flags: { "mandatory" => "foo" }
55
+ ],
56
+ "lists possibilities for a flag arguments (shell)" => [
57
+ %w[fast slow],
58
+ subs: %w[move crash],
59
+ mode: :flagarg,
60
+ current_flag: "speed",
61
+ ],
62
+ "lists possibilities for a flag arguments (file, const)" => [
63
+ [:file, "-"],
64
+ subs: %w[move crash],
65
+ mode: :flagarg,
66
+ current_flag: "output-to-file",
67
+ ],
68
+ "lists possibilities for a flag arguments (dir)" => [
69
+ [:directory],
70
+ subs: %w[move crash],
71
+ mode: :flagarg,
72
+ current_flag: "output-to-directory",
73
+ ],
74
+ "lists nothing if no options are defined" => [
75
+ [],
76
+ subs: %w[sub-with-sub-or-arg],
77
+ mode: :flagarg,
78
+ current_flag: "mandatory"
79
+ ],
80
+ }
81
+
82
+ examples.each do |name, (expected_options, hash)|
83
+ it name do
84
+ token = hash.delete(:token)
85
+ hash[:subcommand_stack] = hash.delete(:subs) || []
86
+ defaults = { mode: :subcommand, args: [], flags: {}, current_flag: nil, dashdash: nil }
87
+ result = Tabry::Result.new(config, Tabry::State.new(defaults.merge(hash)))
88
+ expect(described_class.options(result, token)).to match_array expected_options
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../lib/tabry/result"
4
+ require_relative "../../lib/tabry/config_loader"
5
+ require_relative "../../lib/tabry/state"
6
+
7
+ describe Tabry::Result do
8
+ subject do
9
+ described_class.new(config, state)
10
+ end
11
+
12
+ let(:config_fixture) { "#{__dir__}/../fixtures/vehicles.yaml" }
13
+ let(:config) { Tabry::ConfigLoader.load(name: config_fixture) }
14
+
15
+ describe "#current_sub" do
16
+ let(:state) { Tabry::State.new(subcommand_stack: %w[move crash]) }
17
+
18
+ it "returns the last subcommand" do
19
+ expect(subject.current_sub).to be_a(Tabry::Models::Sub)
20
+ expect(subject.current_sub.name).to eq("crash")
21
+ end
22
+ end
23
+
24
+ describe "#invalid_usage_reason" do
25
+ def reason(state_hash)
26
+ state = { mode: :subcommand, args: [], flags: {} }.merge(state_hash)
27
+ described_class.new(config, Tabry::State.new(**state)).invalid_usage_reason
28
+ end
29
+
30
+ it "returns nil if everything is fine (subcommand with no args)" do
31
+ expect(reason(
32
+ subcommand_stack: %w[list-vehicles]
33
+ )).to be_nil
34
+ end
35
+
36
+ it "complains if mandatory args are not given" do
37
+ expect(reason(
38
+ subcommand_stack: %w[move go]
39
+ )).to eq('missing argument "vehicle-type"')
40
+ end
41
+
42
+ it "complains if a mandatory subcommand (of the main command) is not given" do
43
+ expect(reason(
44
+ subcommand_stack: %w[]
45
+ )).to eq("missing subcommand")
46
+ end
47
+
48
+ it "complains if a mandatory subcommand (of a subcommand) is not given" do
49
+ expect(reason(
50
+ subcommand_stack: %w[move]
51
+ )).to eq("missing subcommand")
52
+ end
53
+
54
+ it "complains if a mandatory subcommand or arg is not given" do
55
+ expect(reason(
56
+ subcommand_stack: %w[sub-with-sub-or-arg]
57
+ )).to eq("missing subcommand or arg(s)")
58
+ end
59
+
60
+ it "doesn't complain if mandatory args are given" do
61
+ expect(reason(
62
+ subcommand_stack: %w[move go],
63
+ args: %w[vehicle1],
64
+ flags: { "dry-run" => true, "output-to-file" => "some-file" }
65
+ )).to be_nil
66
+
67
+ expect(reason(
68
+ subcommand_stack: %w[sub-with-sub-or-arg],
69
+ args: %w[arg]
70
+ )).to be_nil
71
+ end
72
+
73
+ it "complains if mandatory varargs are not given" do
74
+ expect(reason(
75
+ subcommand_stack: %w[build]
76
+ )).to eq("missing one or more args")
77
+ end
78
+
79
+ it "doesn't complain if one vararg is given" do
80
+ expect(reason(
81
+ subcommand_stack: %w[build],
82
+ args: %w[vehicle1]
83
+ )).to be_nil
84
+
85
+ expect(reason(
86
+ subcommand_stack: %w[move freeway-crash],
87
+ args: %w[vehicle1 vehicle2]
88
+ )).to be_nil
89
+ end
90
+
91
+ it "doesn't complain if multiple varargs are given" do
92
+ expect(reason(
93
+ subcommand_stack: %w[build],
94
+ args: %w[vehicle1 vehicle2 vehicle3]
95
+ )).to be_nil
96
+
97
+ expect(reason(
98
+ subcommand_stack: %w[move freeway-crash],
99
+ args: %w[vehicle1 vehicle2 vehicle3 vehicle4]
100
+ )).to be_nil
101
+ end
102
+
103
+ it "doesn't complain if an optional arg is not given" do
104
+ expect(reason(
105
+ subcommand_stack: %w[move crash],
106
+ args: %w[vehicle1]
107
+ )).to be_nil
108
+ end
109
+
110
+ it "doesn't complain if an optional arg is given" do
111
+ expect(reason(
112
+ subcommand_stack: %w[move crash],
113
+ args: %w[vehicle1 vehicle2]
114
+ )).to be_nil
115
+ end
116
+
117
+ it "complains if too many args are given" do
118
+ expect(reason(
119
+ subcommand_stack: %w[move crash],
120
+ args: %w[vehicle1 vehicle2 vehicle3]
121
+ )).to eq("got 3 args, must be between 1 and 2")
122
+ end
123
+
124
+ # TODO: test also in machine
125
+ it "complains if a mandatory flag is not given" do
126
+ expect(reason(
127
+ subcommand_stack: %w[sub-with-mandatory-flag]
128
+ )).to eq("missing required flag mandatory")
129
+ end
130
+
131
+ it "doesn't complain if a mandatory flag is given" do
132
+ expect(reason(
133
+ subcommand_stack: %w[sub-with-mandatory-flag],
134
+ flags: { "mandatory" => "foo" }
135
+ )).to be_nil
136
+ end
137
+ end
138
+
139
+ describe "#usage" do
140
+ it "uses UsageGenerator" do
141
+ state = { mode: :subcommand, args: [], flags: {}, subcommand_stack: %w[move go] }
142
+ expect(Tabry::UsageGenerator).to receive(:new) do |sub_stack, cmd|
143
+ expect(sub_stack.map(&:class)).to eq([Tabry::Models::Sub, Tabry::Models::Sub, Tabry::Models::Sub])
144
+ expect(cmd).to eq("vehicles")
145
+ instance_double(Tabry::UsageGenerator, usage: "foobar")
146
+ end
147
+ usage = described_class.new(config, Tabry::State.new(**state)).usage
148
+ expect(usage).to eq("foobar")
149
+ end
150
+
151
+ it "passes in passed-in command to UsageGenerator" do
152
+ state = { mode: :subcommand, args: [], flags: {}, subcommand_stack: %w[] }
153
+ expect(Tabry::UsageGenerator).to receive(:new) do |_sub_stack, cmd|
154
+ expect(cmd).to eq("customcmd")
155
+ instance_double(Tabry::UsageGenerator, usage: "foobar")
156
+ end
157
+ usage = described_class.new(config, Tabry::State.new(**state)).usage("customcmd")
158
+ expect(usage).to eq("foobar")
159
+ end
160
+ end
161
+
162
+ describe "#top_level?" do
163
+ context "there is a subcommand" do
164
+ let(:state) { Tabry::State.new(subcommand_stack: %w[move crash]) }
165
+
166
+ it { expect(subject.top_level?).to be(false) }
167
+ end
168
+
169
+ context "if there is no subcommand" do
170
+ let(:state) { Tabry::State.new(subcommand_stack: %w[], args: %w[foo]) }
171
+
172
+ it { expect(subject.top_level?).to be(true) }
173
+ end
174
+ end
175
+
176
+ describe "#help?" do
177
+ context "if help is true in the state" do
178
+ let(:state) { Tabry::State.new(subcommand_stack: %w[move crash], help: true) }
179
+
180
+ it { expect(subject.help?).to be(true) }
181
+ end
182
+
183
+ context "if help is absent/nil from the state" do
184
+ let(:state) { Tabry::State.new(subcommand_stack: %w[move crash]) }
185
+
186
+ it { expect(subject.help?).to be_falsey }
187
+ end
188
+ end
189
+
190
+ describe "#named_args" do
191
+ let(:state) do
192
+ Tabry::State.new(subcommand_stack: %w[move crash], args: %w[car bicycle])
193
+ end
194
+
195
+ it "returns a hash of all the named args" do
196
+ expect(subject.named_args).to eq(
197
+ "vehicle-type" => "car",
198
+ "crash-into-vehicle" => "bicycle"
199
+ )
200
+ end
201
+
202
+ context "with varargs" do
203
+ let(:state) { Tabry::State.new(subcommand_stack: %w[build], args: %w[car jeep moped]) }
204
+
205
+ it "puts the varargs in the hash as an array" do
206
+ expect(subject.named_args).to eq("vehicle-types" => %w[car jeep moped])
207
+ end
208
+ end
209
+
210
+ context "with optional, non-empty varargs" do
211
+ let(:state) do
212
+ Tabry::State.new(subcommand_stack: %w[move freeway-crash], args: %w[car jeep moped])
213
+ end
214
+
215
+ it "puts the varargs in the hash as an array" do
216
+ expect(subject.named_args).to eq(
217
+ "vehicle-type" => "car",
218
+ "crash-into-vehicles" => %w[jeep moped]
219
+ )
220
+ end
221
+ end
222
+
223
+ context "with empty optional varargs" do
224
+ let(:state) do
225
+ Tabry::State.new(subcommand_stack: %w[move freeway-crash], args: %w[car])
226
+ end
227
+
228
+ it "puts the varargs in the hash as an array" do
229
+ expect(subject.named_args).to eq(
230
+ "vehicle-type" => "car",
231
+ "crash-into-vehicles" => %w[]
232
+ )
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../lib/tabry/runner"
4
+
5
+ describe Tabry::Runner do
6
+ subject do
7
+ described_class.new(config_name: "configname")
8
+ end
9
+
10
+ let(:config) { instance_double(Tabry::Models::Config) }
11
+ let(:state) { instance_double(Tabry::State) }
12
+
13
+ before do
14
+ allow(Tabry::ConfigLoader).to receive(:load).with(name: "configname").and_return(config)
15
+ end
16
+
17
+ describe "#parse" do
18
+ it "calls the machine and makes a result" do
19
+ expect(Tabry::Machine).to receive(:run).with(config, %w[foo bar]).and_return state
20
+ res = subject.parse(%w[foo bar])
21
+ expect(res).to be_a(Tabry::Result)
22
+ expect(res.state).to eq(state)
23
+ expect(res.config).to eq(config)
24
+ end
25
+ end
26
+
27
+ describe "#options" do
28
+ it "runs OptionsFinder" do
29
+ res = instance_double(Tabry::Result)
30
+ expect(subject).to receive(:parse).with(%w[foo bar]).and_return res
31
+ expect(Tabry::OptionsFinder).to receive(:options).with(res, "waz").and_return %w[a b c]
32
+ expect(subject.options(%w[foo bar], "waz")).to eq(%w[a b c])
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../lib/tabry/usage_generator"
4
+ require_relative "../../lib/tabry/config_loader"
5
+
6
+ describe Tabry::UsageGenerator do
7
+ let(:config_fixture) { "#{__dir__}/../fixtures/vehicles.yaml" }
8
+ let(:config) { Tabry::ConfigLoader.load(name: config_fixture) }
9
+ let(:usage) do
10
+ str = described_class.new(config.dig_sub_array(sub_stack), config.cmd).usage
11
+ decolorified = str.gsub(/\e\[(\d+)m/, "")
12
+ decolorified
13
+ end
14
+
15
+ describe "args" do
16
+ let(:sub_stack) { %w[move crash] }
17
+
18
+ it "shows args" do
19
+ expect(usage).to match(/^ARGS.*^ *vehicle-type/m)
20
+ end
21
+
22
+ it "shows optional args and descriptions" do
23
+ expect(usage).to match(/^ *crash-into-vehicle \(optional\)\n^ *Crash into another vehicle/m)
24
+ end
25
+
26
+ it "shows args in the Usage line" do
27
+ expect(usage).to include "Usage: vehicles move crash <vehicle-type> [<crash-into-vehicle>]"
28
+ end
29
+ end
30
+
31
+ describe "varargs" do
32
+ let(:sub_stack) { %w[move freeway-crash] }
33
+
34
+ it "shows varargs with the title" do
35
+ expect(usage).to include \
36
+ "vehicles move freeway-crash <vehicle-type> [<vehicle to crash into> [<vehicle to crash into>...]]"
37
+ end
38
+ end
39
+
40
+ describe "subcommands" do
41
+ let(:sub_stack) { %w[move] }
42
+
43
+ it "lists subcommands" do
44
+ expect(usage).to match(/^SUBCOMMANDS.*^ *crash/m)
45
+ end
46
+
47
+ it "shows a subcommand alias" do
48
+ expect(usage).to include "go (alias: g)"
49
+ end
50
+
51
+ it "shows subcommand aliases" do
52
+ expect(usage).to include "freeway-crash (aliases: pileup, p)"
53
+ end
54
+ end
55
+
56
+ describe "flags" do
57
+ let(:sub_stack) { %w[move crash] }
58
+
59
+ it "shows flags with titles" do
60
+ expect(usage).to match(/^ *FLAGS.*^ *--dry-run.*Don't actually crash/m)
61
+ end
62
+
63
+ it "shows flags aliases and args" do
64
+ expect(usage).to include "--output-to-file, -f <arg>"
65
+ end
66
+ end
67
+ end