parby 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 86b9b440a1902cfc0bf63e95b212df61f2cd6844
4
+ data.tar.gz: 1a619e1eb0903d69a64d7db910d544ca82826370
5
+ SHA512:
6
+ metadata.gz: 4dde60b0aa97baa80b0536bc73cbd77c11fb2d195a5d3042c51bc3895192102a184c15c60ff8e9849c54287d6625f3324e2cebd9c336cf1fee36efcac8617d88
7
+ data.tar.gz: 12347688dc2b62840c14d25bfe3759df98bd3d80da46fd58f43ba84ede40adfff3dec6ccc64fd97c1213eb16876c19d367fe0cd7517a4fa40c8b249c870cf289
@@ -0,0 +1,86 @@
1
+ require 'parser'
2
+
3
+ module Parby
4
+ # Always yields the value passed to it, no matter the input
5
+ def of(value)
6
+ Parser.new { |input, index| Success.new(index, value, input) }
7
+ end
8
+
9
+ # Yields the first character that matches predicate, it fails with the given message, otherwise a generic one
10
+ def test(predicate, description = nil)
11
+ Parser.new do |input, index|
12
+ found = nil
13
+ input.split('').each do |character|
14
+ if predicate.call(character)
15
+ found = character
16
+ break
17
+ end
18
+ end
19
+
20
+ if found
21
+ Success.new(index, found, input)
22
+ else
23
+ Failure.new(index, [description || 'Matching a predicate'], input)
24
+ end
25
+ end
26
+ end
27
+
28
+ # Yields the input, if it matches the regex passed to it
29
+ def regexp(regex)
30
+ # We have to match from the beginning
31
+ real_regex = /^#{regex}/
32
+
33
+ Parser.new do |input, index|
34
+ match_data = real_regex.match(input)
35
+
36
+ if match_data.nil?
37
+ # We did not even match one letter
38
+ Failure.new(index, [regex], input)
39
+ else
40
+ Success.new(index, match_data[0], input)
41
+ end
42
+ end
43
+ end
44
+
45
+ # Searches in the input for one of the given characters (characters can be either a string or an array), and yields it
46
+ def one_of(characters)
47
+ expected = if characters.is_a?(Array) then characters else characters.split('') end
48
+ test(Proc.new { |c| expected.include?(c) }, expected)
49
+ end
50
+
51
+ def none_of(characters)
52
+ test(Proc.new { |c| !characters.include?(c) }, ["None of #{characters}"])
53
+ end
54
+
55
+ def string(str)
56
+ Parser.new do |input, index|
57
+ furthest = -1
58
+
59
+ str.split('').each_with_index do |expected_character, expected_index|
60
+ actual_character = input[expected_index]
61
+
62
+ if actual_character.nil?
63
+ # This means that the input is smaller than the expected string
64
+ furthest = expected_index - 1
65
+ break
66
+ elsif actual_character != expected_character
67
+ # This means the input does not match exactly the string
68
+ furthest = expected_index
69
+ break
70
+ end
71
+ end
72
+
73
+ if furthest == -1
74
+ Success.new(index, str)
75
+ else
76
+ Failure.new(furthest, [str])
77
+ end
78
+ end
79
+ end
80
+
81
+ def all
82
+ Parser.new do |input, index|
83
+ Success.new(index, input, nil)
84
+ end
85
+ end
86
+ end
data/lib/parser.rb ADDED
@@ -0,0 +1,70 @@
1
+ module Parby
2
+ class Parser
3
+ # { |input, index| } -> result or { |input| } -> result
4
+ def initialize(&block)
5
+ raise(ArgumentError, 'A bare parser must be initialized with a 1 or 2 argument block') unless block_given?
6
+
7
+ @block = block
8
+ end
9
+
10
+ def parse(*args)
11
+ if args.length == 1
12
+ @block.call(args[0], 0)
13
+ else
14
+ @block.call(args[0], args[1])
15
+ end
16
+ end
17
+
18
+ def or(another_parser)
19
+ Parser.new do |input, index|
20
+ first = parse(input, index)
21
+
22
+ if first.failed?
23
+ another_parser.parse(input, index)
24
+ else
25
+ first
26
+ end
27
+ end
28
+ end
29
+
30
+ def |(other)
31
+ self.or other
32
+ end
33
+
34
+ def and(another_parser)
35
+ Parser.new do |input, index|
36
+ first = parse(input, index)
37
+
38
+ if first.failed?
39
+ first
40
+ else
41
+ another_parser.parse(first.remaining, first.index)
42
+ end
43
+ end
44
+ end
45
+
46
+ def >>(other)
47
+ self.and other
48
+ end
49
+
50
+ def fmap(&block)
51
+ Parser.new do |input, index|
52
+ result = parse(input, index)
53
+
54
+ if result.succeed?
55
+ result.value = yield(result.value)
56
+ end
57
+
58
+ result
59
+ end
60
+ end
61
+
62
+ def map(&block)
63
+ fmap(&block)
64
+ end
65
+
66
+ def consuming
67
+ self >> all
68
+ end
69
+ end
70
+ end
data/lib/result.rb ADDED
@@ -0,0 +1,28 @@
1
+ module Parby
2
+ Result = Struct.new('Result', :status, :index, :value, :furthest, :expected, :remaining) do
3
+ def failed?
4
+ !status
5
+ end
6
+
7
+ def succeed?
8
+ status
9
+ end
10
+
11
+ def completed?
12
+ remaining.nil?
13
+ end
14
+ end
15
+
16
+ class Success < Result
17
+ def initialize(index, value, remaining = nil)
18
+ super(true, index, value, -1, [], remaining)
19
+ end
20
+ end
21
+
22
+ class Failure < Result
23
+ def initialize(furthest, expected, remaining = nil)
24
+ actual_expected = if expected.is_a?(Array) then expected else [expected] end
25
+ super(false, -1, nil, furthest, actual_expected, remaining)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+
3
+ describe Parby, '#of' do
4
+ describe '#of' do
5
+ parser = Parby.of(2)
6
+
7
+ it 'Always succeeds' do
8
+ first_result = parser.parse('something')
9
+ second_result = parser.parse('another one')
10
+
11
+ expect(first_result.value).to eq(second_result.value)
12
+ expect(first_result).to eq(Success.new(0, 2, 'something'))
13
+ end
14
+ end
15
+
16
+ describe '#regexp' do
17
+ parser = Parby.regexp(/[0-9]0+/)
18
+
19
+ it 'searches for a match in the input string, yields it' do
20
+ result = parser.parse('100j')
21
+
22
+ expect(result.succeed?).to be true
23
+ expect(result.completed?).to be false
24
+ expect(result.remaining).to eq('100j')
25
+
26
+ expect(result.value).to eq('100')
27
+ end
28
+
29
+ it 'fails when it does not match a regex from the start' do
30
+ result = parser.parse('j100')
31
+
32
+ expect(result.succeed?).to be false
33
+ expect(result.completed?).to be false
34
+
35
+ expect(result.expected).to eq([/[0-9]0+/])
36
+ end
37
+ end
38
+
39
+ describe '#test' do
40
+ context 'given a predicate' do
41
+ parser = Parby.test(Proc.new { |c| c.between?('a', 'z') }, 'Alphanumeric character') # This is how Parby#between is implemented
42
+
43
+ context 'applies the predicate to each character in input' do
44
+ context 'if it matches in the first character' do
45
+ matches_first = parser.parse 'airplane'
46
+
47
+ it 'yields and does not consume the input' do
48
+ expect(matches_first.succeed?).to be true
49
+ expect(matches_first.completed?).to be false
50
+ expect(matches_first.value).to eq 'a'
51
+ expect(matches_first.remaining).to eq 'airplane'
52
+ end
53
+ end
54
+
55
+ context 'if it matches in the middle of the string' do
56
+ matches_in_the_middle = parser.parse '_eval'
57
+
58
+ it 'yields and does not consume the input' do
59
+ expect(matches_in_the_middle.succeed?).to be true
60
+ expect(matches_in_the_middle.completed?).to be false
61
+ expect(matches_in_the_middle.value).to eq 'e'
62
+ expect(matches_in_the_middle.remaining).to eq '_eval'
63
+ end
64
+ end
65
+
66
+ context 'if it does not match' do
67
+ does_not_match = parser. parse '>>='
68
+
69
+ it 'fails with the given message and does not consume the input' do
70
+ expect(does_not_match.failed?).to be true
71
+ expect(does_not_match.expected).to eq ['Alphanumeric character']
72
+ expect(does_not_match.remaining).to eq '>>='
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ describe '#one_of' do
80
+ parser = Parby.one_of('123')
81
+
82
+ it 'looks for exactly one character from the given string, and yields that character' do
83
+ result = parser.parse('jmvbn2')
84
+
85
+ expect(result.succeed?).to be true
86
+ expect(result.remaining).to eq('jmvbn2')
87
+
88
+ expect(result.value).to eq('2')
89
+ end
90
+
91
+ it 'short circuits' do
92
+ result = parser.parse('123')
93
+
94
+ expect(result.succeed?).to be true
95
+ expect(result.remaining).to eq('123')
96
+
97
+ expect(result.value).to eq('1')
98
+ end
99
+ end
100
+
101
+ describe '#none_of' do
102
+ parser = Parby.none_of '123'
103
+
104
+ it 'succeeds when it founds a character not in the string, and yields it' do
105
+ result = parser.parse '1kl'
106
+
107
+ expect(result.succeed?).to be true
108
+ expect(result.value).to eq 'k'
109
+ expect(result.remaining).to eq '1kl'
110
+ end
111
+ end
112
+
113
+ describe '#string' do
114
+ parser = Parby.string('Hi!')
115
+
116
+ it 'matches whole string and consumes it' do
117
+ result = parser.parse 'Hi!'
118
+
119
+ expect(result.succeed?).to be true
120
+ expect(result.value).to eq 'Hi!'
121
+ end
122
+
123
+ it 'Fails gracefully: says the piece of the string that it matched' do
124
+ result = parser.parse 'Hi'
125
+
126
+ expect(result.failed?).to be true
127
+ expect(result.expected).to eq ['Hi!']
128
+ expect(result.furthest).to eq 1
129
+ end
130
+ end
131
+
132
+ describe '#all' do
133
+ it 'consumes the whole input, and yields it' do
134
+ result = Parby.all.parse 42
135
+
136
+ expect(result.succeed?).to be true
137
+ expect(result.completed?).to be true
138
+ expect(result.value).to eq 42
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Parser do
4
+ include Parby
5
+
6
+ describe '#initialize' do
7
+ context 'not passing a block' do
8
+ it 'raises' do
9
+ expect {
10
+ Parser.new(42)
11
+ }.to raise_error(ArgumentError)
12
+ end
13
+ end
14
+
15
+ context 'passing a block' do
16
+ it 'initializes fine' do
17
+ parser = Parser.new { |x| x }
18
+
19
+ expect(parser).to be_a(Parser)
20
+ end
21
+ end
22
+ end
23
+
24
+ describe '#parse' do
25
+ it 'just calls the proc' do
26
+ parser = Parser.new { |_, _| 'Hello' }
27
+
28
+ expect(parser.parse('some text')).to eq('Hello')
29
+ end
30
+ end
31
+
32
+ describe '#or' do
33
+ context 'Given two parsers' do
34
+ parser = Parby.regexp(/42/) | Parby.of(42)
35
+
36
+ it 'When the first one succeeds it yields, not calling the second one' do
37
+ result = parser.parse('42')
38
+
39
+ expect(result.succeed?).to be true
40
+ expect(result.value).to eq('42')
41
+ end
42
+
43
+ it 'When the first one succeeds it calls the second one' do
44
+ result = parser.parse('lol')
45
+
46
+ expect(result.succeed?).to be true
47
+ expect(result.value).to eq(42)
48
+ end
49
+ end
50
+ end
51
+
52
+ describe '#and' do
53
+ context 'Given two parsers' do
54
+ first_parser = Parby.regexp(/0/)
55
+
56
+ it 'Both should have to match to succeed, but only the second value is yielded' do
57
+ second_parser = Parby.regexp(/[0-9]+/)
58
+ parser = first_parser >> second_parser
59
+
60
+ result = parser.parse('00')
61
+
62
+ expect(result.succeed?).to be true
63
+ expect(result.value).to eq('00')
64
+ end
65
+
66
+ it 'When the first one fails, the other one is not called' do
67
+ second_parser = spy(Parby.regexp(/[0-9]+/))
68
+ parser = first_parser >> second_parser
69
+
70
+ result = parser.parse('lol')
71
+
72
+ expect(result.failed?).to be true
73
+ expect(second_parser).to_not have_received(:parse)
74
+ end
75
+ end
76
+ end
77
+
78
+ describe '#map' do
79
+ context 'Given a parser mapped with a mapping block' do
80
+ first_parser = Parser.regexp(/42/)
81
+ parser = first_parser.map(&:to_i)
82
+
83
+ it 'when the parser yields, it will apply the block to the resulting value' do
84
+ result = parser.parse('42')
85
+
86
+ expect(result.succeed?).to be true
87
+ expect(result.value).to eq(42)
88
+ end
89
+
90
+ # We still have to test for not calling the mapper function when the parser fails, but I can't mock the block :(
91
+ end
92
+ end
93
+
94
+ describe '#consuming' do
95
+ context 'Given a non-consuming parser' do
96
+ parser = regexp(/42/)
97
+ input = '42'
98
+ non_consuming_result = parser.parse input
99
+
100
+ it 'Converts it into a consuming one' do
101
+ consuming_result = parser.consuming.parse input
102
+
103
+ expect(consuming_result.succeed?).to be(non_consuming_result.succeed?)
104
+ expect(consuming_result.value).to eq(non_consuming_result.value)
105
+ expect(non_consuming_result.completed?).not_to be true
106
+ expect(consuming_result.completed?).to be true
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Result do
4
+ context 'did fail' do
5
+ result = Failure.new(10, ['stuff'])
6
+
7
+ it 'when the status is false' do
8
+ expect(result.failed?).to be true
9
+ expect(result.succeed?).to be false
10
+ end
11
+ end
12
+
13
+ context 'did succeed' do
14
+ result = Success.new(1, nil)
15
+
16
+ it 'when the status is true' do
17
+ expect(result.failed?).to be false
18
+ expect(result.succeed?).to be true
19
+ end
20
+ end
21
+
22
+ context 'did complete' do
23
+ success = Success.new(nil, nil, 'x')
24
+ failure = Success.new(nil, [], 'x')
25
+
26
+ it 'when there is still input to be consumed, and does not matter if it succeeded or not' do
27
+ expect(success.completed?).to be false
28
+ expect(failure.completed?).to be false
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,54 @@
1
+ require './lib/combinators.rb'
2
+ require './lib/parser.rb'
3
+ require './lib/result.rb'
4
+
5
+ include RSpec
6
+ include Parby
7
+
8
+ # This file was generated by the `rspec --init` command. Conventionally, all
9
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
10
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
11
+ # this file to always be loaded, without a need to explicitly require it in any
12
+ # files.
13
+ #
14
+ # Given that it is always loaded, you are encouraged to keep this file as
15
+ # light-weight as possible. Requiring heavyweight dependencies from this file
16
+ # will add to the boot time of your test suite on EVERY test run, even for an
17
+ # individual file that may not need all of that loaded. Instead, consider making
18
+ # a separate helper file that requires the additional dependencies and performs
19
+ # the additional setup, and require it from the spec files that actually need
20
+ # it.
21
+ #
22
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
23
+ RSpec.configure do |config|
24
+ # rspec-expectations config goes here. You can use an alternate
25
+ # assertion/expectation library such as wrong or the stdlib/minitest
26
+ # assertions if you prefer.
27
+ config.expect_with :rspec do |expectations|
28
+ # This option will default to `true` in RSpec 4. It makes the `description`
29
+ # and `failure_message` of custom matchers include text for helper methods
30
+ # defined using `chain`, e.g.:
31
+ # be_bigger_than(2).and_smaller_than(4).description
32
+ # # => "be bigger than 2 and smaller than 4"
33
+ # ...rather than:
34
+ # # => "be bigger than 2"
35
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
36
+ end
37
+
38
+ # rspec-mocks config goes here. You can use an alternate test double
39
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
40
+ config.mock_with :rspec do |mocks|
41
+ # Prevents you from mocking or stubbing a method that does not exist on
42
+ # a real object. This is generally recommended, and will default to
43
+ # `true` in RSpec 4.
44
+ mocks.verify_partial_doubles = true
45
+ end
46
+
47
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
48
+ # have no way to turn it off -- the option exists only for backwards
49
+ # compatibility in RSpec 3). It causes shared context metadata to be
50
+ # inherited by the metadata hash of host groups and examples, rather than
51
+ # triggering implicit auto-inclusion in groups with matching metadata.
52
+ config.shared_context_metadata_behavior = :apply_to_host_groups
53
+
54
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: parby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rodrigo Martin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-02-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ description:
28
+ email: rodrigoleonardomartin@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/combinators.rb
34
+ - lib/parser.rb
35
+ - lib/result.rb
36
+ - spec/combinators_spec.rb
37
+ - spec/parser_spec.rb
38
+ - spec/result_spec.rb
39
+ - spec/spec_helper.rb
40
+ homepage:
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ source_code_uri: https://github.com/rodr0m4/parby
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 2.5.2.3
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Happy little parser combinators
65
+ test_files: []