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