drudge 0.4.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.
@@ -0,0 +1,70 @@
1
+ class Drudge
2
+ module Parsers
3
+
4
+ # tokenization of commandline arguments.
5
+ module Tokenizer
6
+ extend self
7
+
8
+ # tokenizes the arg-v list into an array of sexps
9
+ # the sexps are then suitable for the Drudge::parsers parser
10
+ # combinators
11
+ def tokenize(argv)
12
+ argv.map.with_index do |arg, index|
13
+ [:val, arg, loc(index, arg.length)]
14
+ end
15
+ end
16
+
17
+ # given an array of sexps (as returned by tokenize) produce
18
+ # a string representatio of that
19
+ def untokenize(sexps)
20
+ sexps.map do |type, arg, *_|
21
+ case type
22
+ when :val
23
+ arg
24
+ end
25
+ end.join(" ")
26
+ end
27
+
28
+ # produces a string that underlines a specific token
29
+ # if no token is provided, the end of string is underlined
30
+ def underline_token(input, token, underline_char: '~')
31
+ line = untokenize(input)
32
+
33
+ if token
34
+ _, _, meta = token
35
+ location = meta[:loc]
36
+ _, _, token_length = location
37
+ white_space = index_of_sexp_in_untokenized(input, location)
38
+ else
39
+ white_space = line.length + 1
40
+ token_length = 1
41
+ underline_char = '^'
42
+ end
43
+
44
+ " " * white_space + underline_char * token_length
45
+ end
46
+
47
+
48
+ private
49
+
50
+ def loc(index, start = 0, len)
51
+ {loc: [index, start, len]}
52
+ end
53
+
54
+ def index_of_sexp_in_untokenized(input, loc)
55
+ l_index, l_start, l_len = loc
56
+
57
+ prefix =
58
+ if l_index == 0
59
+ 0
60
+ else
61
+ input[0..l_index - 1].map { |_, _, meta| meta[:loc] }
62
+ .reduce(0) { |sum, (_, _, len)| sum + len + 1 }
63
+ end
64
+
65
+ prefix + l_start
66
+ end
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,3 @@
1
+ class Drudge
2
+ VERSION = "0.4.0"
3
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ require 'drudge/class_dsl'
4
+
5
+
6
+ class Drudge
7
+
8
+ class Sample
9
+ include ClassDSL
10
+
11
+ desc "An action with no params"
12
+ def verify
13
+ puts "Verified."
14
+ end
15
+ end
16
+
17
+ class AnotherSample < Sample
18
+
19
+ desc "a second action"
20
+ def second
21
+ puts "Second."
22
+ end
23
+ end
24
+
25
+
26
+ class OverridenCommand < Sample
27
+
28
+ desc "a second action"
29
+ def second
30
+ puts "Second."
31
+ end
32
+
33
+ def verify
34
+ puts "From overriden."
35
+ super
36
+ end
37
+
38
+ end
39
+
40
+ class OverridenWithDesc < Sample
41
+
42
+ desc "Refined description"
43
+ def verify
44
+ puts "From overriden."
45
+ super
46
+ end
47
+
48
+ end
49
+
50
+ describe ClassDSL do
51
+
52
+ describe "defining actions" do
53
+
54
+ context "the 'desc' keyword marks a command" do
55
+
56
+ describe "the kit built from this class" do
57
+ subject(:kit) { Sample.new.to_kit(:cli) }
58
+
59
+ its(:name) { should eq :cli }
60
+ its(:commands) { should have(1).items }
61
+
62
+ describe "the command 'verify'" do
63
+ subject(:command) { kit.commands[0] }
64
+
65
+ its(:name) { should eq :verify }
66
+ its(:params) { should be_empty }
67
+ its(:desc) { should eq "An action with no params" }
68
+
69
+ it "has a body that invokes the verify method" do
70
+ expect_capture { command.dispatch }.to eq("Verified.\n")
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+
78
+ describe "inheritance" do
79
+ describe "a sub-class adds to the actions from its parent" do
80
+
81
+ describe "the kit built from the sub-class" do
82
+ subject(:kit) { AnotherSample.new.to_kit(:cli) }
83
+
84
+ its(:commands) { should have(2).items }
85
+
86
+ it "should contain the command from the superclass as first element" do
87
+ expect(kit.commands[0].name).to eq :verify
88
+ end
89
+
90
+ it "should contain the command from the sub class as second element (because it was defined later)" do
91
+ expect(kit.commands[1].name).to eq :second
92
+ end
93
+ end
94
+
95
+ describe "the kit built from the parent class" do
96
+ subject(:kit) { Sample.new.to_kit(:cli) }
97
+
98
+ its(:commands) { should have(1).items }
99
+ end
100
+ end
101
+
102
+ describe "a command whose corresponding method is overriden" do
103
+ it "invokes the metod in the sub-class and that method can call 'super' in normal fashion" do
104
+ kit = OverridenCommand.new.to_kit
105
+
106
+ expect_capture { kit.dispatch :verify }.to eq("From overriden.\nVerified.\n")
107
+ end
108
+ end
109
+
110
+ describe "kit with a command whose corresponding method and metdadata is overriden" do
111
+ subject(:kit) { OverridenWithDesc.new.to_kit(:cli) }
112
+
113
+ its(:commands) { should have(1).item }
114
+
115
+ describe "the overriden command" do
116
+ subject { kit.commands[0] }
117
+
118
+ its(:desc) { should eq "Refined description" }
119
+ end
120
+ end
121
+
122
+ end
123
+ end
124
+
125
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ require 'drudge/command'
4
+
5
+ class Drudge
6
+
7
+ describe Command do
8
+
9
+ context "command execution" do
10
+
11
+ describe "a command with no parameters" do
12
+ subject do
13
+ Command.new(:verify, -> { puts "Verified." })
14
+ end
15
+
16
+ it "can be executed by calling the dispatch method" do
17
+ expect_capture { subject.dispatch }.to eq("Verified.\n")
18
+ end
19
+
20
+ describe "#dispatch" do
21
+ context "with no arguments"
22
+ it "doesn't accept normal arguments" do
23
+ expect { subject.dispatch(1) }.to raise_error(CommandArgumentError)
24
+ end
25
+
26
+ it "doesn't accept keyword arguments" do
27
+ expect { subject.dispatch(greeting: "hello") }.to raise_error(CommandArgumentError)
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ describe "a command with a couple of parameters" do
35
+ subject do
36
+ Command.new(:greet,
37
+ [ Param.any(:greeter),
38
+ Param.any(:greeted)],
39
+ -> (greeter, greeted) { puts "#{greeter} says 'hello' to #{greeted}" })
40
+ end
41
+
42
+ describe "#dispatch" do
43
+ it "accepts two arguments" do
44
+ expect_capture { subject.dispatch("Santa", "Superman") }.to eq("Santa says 'hello' to Superman\n")
45
+ end
46
+
47
+ it "raises an error when called with a wrong number of arguments" do
48
+ expect { subject.dispatch }.to raise_error(CommandArgumentError)
49
+ expect { subject.dispatch("Santa") }.to raise_error(CommandArgumentError)
50
+ end
51
+ end
52
+ end
53
+
54
+ describe "The command's description" do
55
+ subject do
56
+ Command.new(:verify, -> { puts "Verified." }, desc: "Verification")
57
+ end
58
+
59
+ its(:desc) { should eq "Verification" }
60
+ end
61
+
62
+ describe "Argument parsers" do
63
+ describe "a command called 'greet' with one parameter" do
64
+ subject(:command) do
65
+ Command.new(:greet, [Param.any(:greeted)], -> { puts "Hello" })
66
+ end
67
+
68
+ describe "the argument parser generated by this command" do
69
+ subject(:parser) do
70
+ command.argument_parser.collated_arguments
71
+ end
72
+
73
+ it { should tokenize_and_parse(%w[Joe]).as({args: %w[Joe]}) }
74
+ it { should_not tokenize_and_parse(%w[]) }
75
+ it { should_not tokenize_and_parse(%w[Joe Green]) }
76
+ end
77
+
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ require 'drudge/kit'
4
+
5
+ class Drudge
6
+
7
+ describe Kit do
8
+ describe "command execution" do
9
+
10
+ describe "a Kit with two zero-arg commands" do
11
+ subject(:kit) do
12
+ Kit.new(:cli,
13
+ [dummy_cmd(:hello),
14
+ dummy_cmd(:goodbye)])
15
+ end
16
+
17
+ it "executes the known command 'hello'" do
18
+ expect_capture { subject.dispatch "hello" }.to eq("hello\n")
19
+ end
20
+
21
+ it "requries a command to run" do
22
+ expect { subject.dispatch }.to raise_error
23
+ end
24
+
25
+ it "reports an error for an unknown command" do
26
+ expect { subject.dispatch("bla") }.to raise_error(UnknownCommandError)
27
+ end
28
+
29
+ it "doesn't accept extra arguments" do
30
+ expect { subject.dispatch "hello", "dear", "sir" }.to raise_error(CommandArgumentError)
31
+ end
32
+
33
+ describe "#argument_parser" do
34
+ subject { kit.argument_parser }
35
+
36
+ it { should tokenize_and_parse(%w[cli hello]).as({args: %w[cli hello]}) }
37
+ it { should tokenize_and_parse(%w[cli goodbye]).as({args: %w[cli goodbye]}) }
38
+
39
+ it { should_not tokenize_and_parse(%w[cli foo]) }
40
+ it { should_not tokenize_and_parse(%w[cli hello someone]) }
41
+
42
+ it { should_not tokenize_and_parse([]) }
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+ require 'drudge/parsers/parse_results'
3
+
4
+ class Drudge
5
+ module Parsers
6
+
7
+ describe ParseResults do
8
+ include ParseResults
9
+
10
+ describe ParseResult do
11
+
12
+ describe ".+" do
13
+ context "Success() + Success()" do
14
+ it "is a a Success where '+' is applied to the underlying ParseValue" do
15
+ expect(Success(Single(1), [2, 3]) + Success(Single(2), [3])).to eq Success(Seq([1, 2]), [3])
16
+ expect(Success(Empty(), [2, 3]) + Success(Single(2), [3])).to eq Success(Single(2), [3])
17
+ expect(Success(Seq([1, 2]), [3, 4]) + Success(Single(3), [4])).to eq (Success(Seq([1, 2, 3]), [4]))
18
+ end
19
+ end
20
+ context "Success() + NoSuccess()" do
21
+ it "is a Failure()" do
22
+ expect(Success(Single(1), [2, 3]) + Failure("error", [3])).to eq Failure("error", [3])
23
+ end
24
+ end
25
+
26
+ context "NoSuccess() + Success()" do
27
+ it "is a Failure()" do
28
+ expect(Failure("error", [3]) + Success(Single(2), [3, 4])).to eq Failure("error", [3])
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ describe ".success?" do
35
+ it "returns true for a Success()" do
36
+ expect(Success(Single(1), []).success?).to be_true
37
+ end
38
+
39
+ it "returns false for all NoSuccess() parse results" do
40
+ expect(Failure("error", []).success?).to be_false
41
+ expect(Error("error", []).success?).to be_false
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,262 @@
1
+ require 'spec_helper'
2
+ require 'drudge/parsers/primitives'
3
+
4
+
5
+ class Drudge
6
+ module Parsers
7
+
8
+ describe Primitives do
9
+ include Primitives
10
+
11
+ describe ".parser: a parser function (lambda) that recognizes the next token on the input that wrapped in a ParseResult" do
12
+
13
+ subject(:p) do
14
+ parser { |input| if input[0][0] == :val then Success(Single(input[0][1]), input.drop(1)) else Failure("f", input) end }
15
+ end
16
+
17
+ it "accepts an enum of sexps (obtained from tokenize) as its single argument" do
18
+ expect { p[[[:val, "test"]]] }.not_to raise_error
19
+ end
20
+
21
+ context "given the input [[:val, 'test']]" do
22
+ it "parses the value and consumes the input that produced it" do
23
+ expect(p[[[:val, "test"]]]).to eq(Success(Single("test"), []))
24
+ end
25
+ end
26
+
27
+ context "given the input [[:val, 'test'], [:foo, 'bar']]" do
28
+ it "parses the value and return the remaining input" do
29
+ expect(p[[[:val, "test"], [:foo, "bar"]]]).to eq(Success(Single("test"), [[:foo, "bar"]]))
30
+ end
31
+ end
32
+
33
+ context "given the input [[:foo, 'bar'], [:val, 'test']]" do
34
+ it "doesn't parse the input and returns Failure" do
35
+ input = [[:foo, 'bar'], [:val, 'test']]
36
+ expect(p[input]).to eq(Failure("f", input))
37
+ end
38
+ end
39
+ end
40
+
41
+ # a parser that expects a value declared in +expected
42
+ def value(expected)
43
+ parser do |input|
44
+ first, *rest = input
45
+
46
+ case
47
+ when first.nil?
48
+ Failure("Expected a value", input)
49
+ when first[0] == :val && expected === first[1]
50
+ Success(Single(first[1]), rest)
51
+ else
52
+ Failure("'#{first[1]}' doesn't match #{expected}", input)
53
+ end
54
+ end.describe expected
55
+ end
56
+
57
+ describe ".commit" do
58
+ let(:prs) do
59
+ p = parser do |input|
60
+ Failure("fail", input)
61
+ end
62
+
63
+ commit(p)
64
+ end
65
+
66
+ it "converts parser Failures into Errors" do
67
+ input = [[:val, "Hello"]]
68
+ result = prs[input]
69
+
70
+ expect(result).to be_kind_of(Error)
71
+ expect(result.message).to eq("fail")
72
+ end
73
+ end
74
+
75
+ describe "parser combinators" do
76
+
77
+ describe ".mapv" do
78
+ context "applied on a value('something') parser" do
79
+ subject { value('something').mapv { |r| { args: [r] } } }
80
+
81
+ it { should parse([[:val, "something"]]).as({ args: ['something']}) }
82
+ it { should_not parse([[:val, "something else"]]) }
83
+ end
84
+ end
85
+
86
+ describe ".>" do
87
+ context "value('something') > value(/-t.+/)" do
88
+ subject { value('something') > value(/-t.+/) }
89
+
90
+ it { should parse([[:val, 'something'], [:val, '-tower']]).as(['something', '-tower']) }
91
+ it { should parse([[:val, 'something'], [:val, '-tower']]) }
92
+ it { should_not parse([[:val, 'something']])}
93
+ end
94
+
95
+ context "value('something') > value('followed by') > value('else')" do
96
+ subject { value('something') > value('followed by') > value('else') }
97
+
98
+ it { should parse([[:val, 'something'], [:val, 'followed by'], [:val, 'else']]).as(['something', 'followed by', 'else']) }
99
+ it { should_not parse([[:val, 'something']]) }
100
+ it { should_not parse([:val, 'something'], [:val, 'other'], [:val, 'else']) }
101
+ end
102
+ end
103
+
104
+ describe ".>=" do
105
+ context "value('something') >= value('else')" do
106
+ subject { value('something') >= value('else') }
107
+
108
+ it { should tokenize_and_parse(%w[something else]).as('else') }
109
+
110
+ it { should_not tokenize_and_parse(%w[something other]) }
111
+ it { should_not tokenize_and_parse(%w[other else]) }
112
+ it { should_not tokenize_and_parse([]) }
113
+ end
114
+ end
115
+
116
+
117
+ describe ".<=" do
118
+ context "value('something') <= value('else')" do
119
+ subject { value('something') <= value('else') }
120
+
121
+ it { should tokenize_and_parse(%w[something else]).as('something') }
122
+
123
+ it { should_not tokenize_and_parse(%w[something other]) }
124
+ it { should_not tokenize_and_parse(%w[other else]) }
125
+ it { should_not tokenize_and_parse([]) }
126
+ end
127
+ end
128
+
129
+ describe ".|" do
130
+ context "value('something') | value('else')" do
131
+ subject { value('something') | value('else') }
132
+
133
+ it { should tokenize_and_parse(%w[something]).as('something') }
134
+ it { should tokenize_and_parse(%w[else]).as('else') }
135
+ it { should_not tokenize_and_parse(%w[other stuff]) }
136
+
137
+ its(:to_s) { should eq("something | else") }
138
+ end
139
+ end
140
+
141
+ describe ".optonal" do
142
+ context "value('something').optional" do
143
+ subject { value('something').optional }
144
+
145
+ it { should tokenize_and_parse(%w[something]).as('something') }
146
+ it { should tokenize_and_parse(%w[]).as(nil) }
147
+ it { should tokenize_and_parse(%w[other]).as(nil) }
148
+ end
149
+
150
+ context "value('something').optional > value(/.+/)" do
151
+ subject { value('something').optional > value(/.+/) }
152
+
153
+ it { should tokenize_and_parse(%w[something other]).as(['something', 'other']) }
154
+ it { should tokenize_and_parse(%w[other]).as('other') }
155
+
156
+ it { should_not tokenize_and_parse(%w[something]).as('something') }
157
+ it { should_not tokenize_and_parse(%w[]) }
158
+ end
159
+ end
160
+
161
+
162
+ describe ".repeats" do
163
+ shared_examples "repetitive parser" do |word|
164
+ it { should tokenize_and_parse([word]).as(word) }
165
+ it { should tokenize_and_parse([word, word]).as([word, word]) }
166
+ it { should tokenize_and_parse([word, word, word]).as([word, word, word]) }
167
+ it { should tokenize_and_parse([word, word, "not-#{word}"]).as([word, word]) }
168
+ end
169
+
170
+ context "zero or more repetitions" do
171
+ shared_examples "parser for zero or more repetitions" do |word|
172
+ it { should tokenize_and_parse(["not-#{word}"]).as(nil) }
173
+ it { should tokenize_and_parse([]).as(nil) }
174
+ end
175
+
176
+ describe "no arguments means repeats(:*)" do
177
+ subject { value('hi').repeats }
178
+ it_behaves_like "repetitive parser", 'hi'
179
+ it_behaves_like "parser for zero or more repetitions" , 'hi'
180
+ end
181
+
182
+ describe "repeats(:*)" do
183
+ subject { value('hi').repeats(:*) }
184
+
185
+ it_behaves_like "repetitive parser", 'hi'
186
+ it_behaves_like "parser for zero or more repetitions" , 'hi'
187
+ end
188
+ end
189
+
190
+ context "one or more repetitions" do
191
+ describe "repeats(:+)" do
192
+ subject { value('hi').repeats(:+) }
193
+
194
+ it_behaves_like "repetitive parser", 'hi'
195
+
196
+ it { should_not tokenize_and_parse(["not-hi"]) }
197
+ it { should_not tokenize_and_parse([]) }
198
+ end
199
+ end
200
+
201
+ context "with till:" do
202
+ shared_examples "terminated repeating parser" do |word, terminal|
203
+ it { should tokenize_and_parse([word, terminal]).as(word) }
204
+ it { should tokenize_and_parse([word, word, terminal]).as([word, word]) }
205
+ it { should tokenize_and_parse([word, word, terminal, word]).as([word, word]) }
206
+
207
+ end
208
+
209
+
210
+ describe "value('hi').repeats(till: value('no'))" do
211
+ subject { value('hi').repeats(till: value('no')) }
212
+
213
+ it_behaves_like "terminated repeating parser", "hi", "no"
214
+
215
+ it { should tokenize_and_parse(%w[no]).as(nil) }
216
+ end
217
+
218
+ describe "value('hi').repeats(:+, till: value('no')" do
219
+ subject { value('hi').repeats(:+, till: value('no')) }
220
+
221
+ it_behaves_like "terminated repeating parser", "hi", "no"
222
+
223
+ it { should_not tokenize_and_parse(%w[no]) }
224
+
225
+ it { should_not tokenize_and_parse(%w[hi hi]) }
226
+ it { should_not tokenize_and_parse([]) }
227
+
228
+ end
229
+
230
+ describe "terminated repeating parser is non-greedy" do
231
+ shared_examples "non-greedy terminated repeating parser" do |word, terminal_sequence|
232
+ it { should tokenize_and_parse([word, *terminal_sequence]).as(word) }
233
+ it { should tokenize_and_parse([word, *terminal_sequence, *terminal_sequence]).as(word) }
234
+ end
235
+
236
+ describe "value('hi').repeats(till: value('hi') > value('no'))" do
237
+ subject { value('hi').repeats(till: (value('hi') > value('no'))) }
238
+
239
+ it_behaves_like "non-greedy terminated repeating parser", 'hi', ['hi', 'no']
240
+
241
+ it { should tokenize_and_parse(%w[hi hi]).as(nil) }
242
+ it { should tokenize_and_parse(%w[hi]).as(nil) }
243
+ end
244
+
245
+ describe "value('hi').repeats(:+, till: value('hi') > value('no'))" do
246
+ subject { value('hi').repeats(:+, till: (value('hi') > value('no'))) }
247
+
248
+ it_behaves_like "non-greedy terminated repeating parser", 'hi', ['hi', 'no']
249
+
250
+ it { should_not tokenize_and_parse(%w[hi hi]) }
251
+ it { should_not tokenize_and_parse(%w[hi]) }
252
+ end
253
+
254
+
255
+ end
256
+ end
257
+ end
258
+
259
+ end
260
+ end
261
+ end
262
+ end