drudge 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +59 -0
- data/LICENSE.txt +22 -0
- data/README.md +107 -0
- data/Rakefile +27 -0
- data/drudge.gemspec +35 -0
- data/features/optional-arguments.feature +64 -0
- data/features/simple-commands.feature +185 -0
- data/features/step_definitions/scripts_steps.rb +19 -0
- data/features/support/env.rb +5 -0
- data/features/variable-length-argument-lists.feature +111 -0
- data/lib/drudge.rb +8 -0
- data/lib/drudge/class_dsl.rb +106 -0
- data/lib/drudge/command.rb +100 -0
- data/lib/drudge/dispatch.rb +41 -0
- data/lib/drudge/errors.rb +30 -0
- data/lib/drudge/ext.rb +17 -0
- data/lib/drudge/kit.rb +45 -0
- data/lib/drudge/parsers.rb +91 -0
- data/lib/drudge/parsers/parse_results.rb +254 -0
- data/lib/drudge/parsers/primitives.rb +278 -0
- data/lib/drudge/parsers/tokenizer.rb +70 -0
- data/lib/drudge/version.rb +3 -0
- data/spec/drudge/class_dsl_spec.rb +125 -0
- data/spec/drudge/command_spec.rb +81 -0
- data/spec/drudge/kit_spec.rb +50 -0
- data/spec/drudge/parsers/parse_results_spec.rb +47 -0
- data/spec/drudge/parsers/primitives_spec.rb +262 -0
- data/spec/drudge/parsers/tokenizer_spec.rb +71 -0
- data/spec/drudge/parsers_spec.rb +149 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/support/capture.rb +16 -0
- data/spec/support/fixtures.rb +13 -0
- data/spec/support/parser_matchers.rb +42 -0
- metadata +219 -0
@@ -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,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
|