drudge 0.4.0

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