dolos 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 21d4d37cda494b06bfa565d59b3d9d2a157fa4d0c71b09fc1d252e8867fb17f9
4
+ data.tar.gz: 9abf404eb2a0da18548f42e5de345776dd8f5c11296b77cee4c81114f3e8ba1e
5
+ SHA512:
6
+ metadata.gz: 1a5fc2ba17cd43ccb87f3c1b7b8948e27148dfecebfc53a9e46ddf92cb97e1c0c98b5841a87437fe2ab2e9b5b71f65d5753f006c96f9da506290e7fb2d6299a9
7
+ data.tar.gz: 419cd3a8d8b64a6eb384dd706647f58182154f6ccb84aa2e83485912374261bc6ee89ea827e394dda621ffb43a2a6fa5bc07c8107de3e9b9727880f4a56c1bac
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # Changelog
data/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2023 Zygimantas Benetis
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Dolos
2
+
3
+ <img height="256" src="docs/dolos_stable_diff.png" width="256"/>
4
+
5
+
6
+ ### Disclaimer
7
+ 🚧 Under development, not stable yet 🚧
8
+
9
+ ### Parser combinator library for Ruby
10
+
11
+ It does not use exceptions and instead returns a result object.
12
+ Library is composable and concise.
13
+
14
+ ```ruby
15
+ include Dolos
16
+
17
+ parser = c("Parsers") >> ws >> c("are") >> ws >> c("great!")
18
+ parser.parse("Parsers are great!") # <Result::Success>
19
+
20
+ greeter = c("Hello")
21
+ greet_and_speak = greeter >> c(", ") >> parser
22
+ greet_and_speak.parse("Hello, Parsers are great!") # <Result::Success>
23
+ ```
24
+
25
+ ### Letter address parser example
26
+ ```ruby
27
+ require 'dolos'
28
+ require 'dolos_common_parsers/common_parsers'
29
+
30
+ include Dolos
31
+
32
+ # Include common parsers
33
+ # In future this can be more structured, moved them to separate module to prevent breaking changes
34
+ include Dolos::CommonParsers
35
+
36
+ # Library usage example
37
+ # Parse out a name and address from a letter
38
+ # For higher difficulty, we will not split this into multiple lines, but instead parse it all at once
39
+ letter = <<-LETTER
40
+ Mr. Vardeniui Pavardeniui
41
+ AB „Lietuvos Paštas“
42
+ Totorių g. 8
43
+ 01121 Vilnius
44
+ LETTER
45
+
46
+ # Combine with 'or'
47
+ honorific = c("Mr. ") | c("Mrs. ") | c("Ms. ")
48
+
49
+ # Can be parsed any_char which will include needed letters
50
+ # Or combine LT letters with latin alphabet
51
+ alpha_with_lt = char_in("ąčęėįšųūžĄČĘĖĮŠŲŪŽ") | alpha
52
+
53
+ # Capture all letters in a row and join them,
54
+ # because they are captured as elements of array by each alpha_with_lt parser.
55
+ first_name = alpha_with_lt.rep.capture!.map(&:join)
56
+ last_name = alpha_with_lt.rep.capture!.map(&:join)
57
+
58
+ # Combine first line parsers
59
+ # Consume zero or more whitespace, after that honorific must follow and so on
60
+ name_line = ws.rep0 >> honorific >> first_name >> ws >> last_name >> eol
61
+
62
+ # Next line is company info
63
+ # We could choose to accept UAB and AB or just AB and etc.
64
+ # 'c("AB")' is for case-sensitive string. 'string' can also be used
65
+ company_type = c("AB")
66
+ quote_open = c("„")
67
+ quote_close = c("“")
68
+
69
+ # Consume LT alphabet with whitespace
70
+ company_name = (alpha_with_lt | ws).rep.capture!.map(&:join)
71
+ company_info = company_type >> ws.rep0 >> quote_open >> company_name >> quote_close
72
+ second_line = ws.rep0 >> company_info >> eol
73
+
74
+ # Address line
75
+ # 'char_while' will consume characters while passed predicate is true
76
+ # This could be an alternative to previous 'alpha_with_lt' approach
77
+ # After that result is captured and mapped to hash
78
+ # Mapping to hash so at the end its easy to tell tuples apart
79
+ # Also while mapping, doing some cleaning with '.strip'
80
+ street_name = char_while(->(char) { !char.match(/\d/) }).capture!.map(&:first).map { |s| { street: s.strip } }
81
+ building = digits.capture!.map(&:first).map { |s| { building: s.strip } }
82
+ address_line = ws.rep0 >> street_name >> building >> eol
83
+
84
+ # City line
85
+ # All digits can be matched here or 'digits.rep(5)' could be used. Also joining with map.
86
+ postcode = digits.capture!.map(&:join).map { |s| { postcode: s.strip } }
87
+ city = alpha_with_lt.rep.capture!.map(&:join).map { |s| { city: s.strip } }
88
+ city_line = ws.rep0 >> postcode >> ws >> city >> eol
89
+
90
+ # Full letter parser which is combined from all previous parsers. All previous parsers can be ran separately.
91
+ letter_parser = name_line >> second_line >> address_line >> city_line
92
+ result = letter_parser.run(letter)
93
+
94
+ pp result.captures
95
+
96
+ ```
97
+
98
+ ### Contributing
99
+ Contributors are welcome. Note: since library is not yet stable, I recommend getting in touch with me before starting to work on something.
100
+
101
+ #### Other parser combinator libraries
102
+ - [Fastparse](https://com-lihaoyi.github.io/fastparse/) (Scala)
103
+ - [Parsby](https://github.com/jolmg/parsby) (Ruby)
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
Binary file
data/dolos.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/dolos/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "dolos"
7
+ spec.version = Dolos::VERSION
8
+ spec.authors = ["benetis"]
9
+ spec.licenses = ['MIT']
10
+ spec.email = ["git@benetis.me"]
11
+ spec.files = Dir["lib/**/*"]
12
+
13
+ spec.summary = "Parser combinator library for Ruby."
14
+ spec.homepage = "https://github.com/benetis/dolos"
15
+ spec.required_ruby_version = ">= 3.1.0"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/benetis/dolos"
21
+ spec.metadata["changelog_uri"] = "https://github.com/benetis/dolos/blob/master/CHANGELOG.md"
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (File.expand_path(f) == __FILE__) ||
28
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
29
+ end
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ # Uncomment to register a new dependency of your gem
36
+ # spec.add_dependency "example-gem", "~> 1.0"
37
+
38
+ # For more information and examples about making a new gem, check out our
39
+ # guide at: https://bundler.io/guides/creating_gem.html
40
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dolos
4
+ class ParserState
5
+ attr_reader :input
6
+
7
+ def initialize(input)
8
+ @input = StringIOWrapper.new(input)
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dolos
4
+ module Parsers
5
+ def string(str)
6
+ Parser.new do |state|
7
+ state.input.mark_offset
8
+ utf8_str = str.encode('UTF-8')
9
+ if state.input.matches?(utf8_str)
10
+ Success.new(utf8_str, str.bytesize)
11
+ else
12
+ advanced = state.input.offset
13
+ state.input.rollback
14
+ Failure.new(
15
+ "Expected #{str.inspect} but got #{state.input.io.string.inspect}",
16
+ advanced
17
+ )
18
+ end
19
+ end
20
+ end
21
+ alias_method :c, :string
22
+
23
+ def regex(pattern)
24
+ Parser.new do |state|
25
+ state.input.mark_offset
26
+ if (matched_string = state.input.matches_regex?(pattern))
27
+ Success.new(matched_string, matched_string.bytesize)
28
+ else
29
+ advanced = state.input.offset
30
+ state.input.rollback
31
+ Failure.new(
32
+ "Expected pattern #{pattern.inspect} but got #{state.input.io.string.inspect}",
33
+ advanced
34
+ )
35
+ end
36
+ end
37
+ end
38
+
39
+
40
+ def any_char
41
+ Parser.new do |state|
42
+ state.input.mark_offset
43
+
44
+ char, = state.input.peek(1)
45
+
46
+ if char && !char.empty?
47
+ Success.new(char, char.bytesize)
48
+ else
49
+ advanced = state.input.offset
50
+ state.input.rollback
51
+ Failure.new('Expected any character but got end of input', advanced)
52
+ end
53
+ end
54
+ end
55
+
56
+ # Matches any character in a string
57
+ # Example:
58
+ # char_in('abc').run('b') # => Success.new('b', 1)
59
+ def char_in(characters_string)
60
+ characters_array = characters_string.chars
61
+
62
+ Parser.new do |state|
63
+ state.input.mark_offset
64
+
65
+ char, bytesize = state.input.peek(1)
66
+
67
+ if char && characters_array.include?(char)
68
+ Success.new(char, bytesize)
69
+ else
70
+ advanced = state.input.offset
71
+ state.input.rollback
72
+ Failure.new(
73
+ "Expected one of #{characters_array.inspect} but got #{char.inspect}",
74
+ advanced
75
+ )
76
+ end
77
+ end
78
+ end
79
+
80
+ def char_while(predicate)
81
+ Parser.new do |state|
82
+ state.input.mark_offset
83
+
84
+ buffer = String.new
85
+ loop do
86
+ char, bytesize = state.input.peek(1)
87
+ break if char.nil? || !predicate.call(char)
88
+
89
+ buffer << char
90
+ state.input.advance(bytesize)
91
+ end
92
+
93
+ if buffer.empty?
94
+ advanced = state.input.offset
95
+ Failure.new("Predicate never returned true", advanced)
96
+ else
97
+ Success.new(buffer, 0)
98
+ end
99
+ end
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dolos
4
+ class Result
5
+ end
6
+
7
+ class Success < Result
8
+ attr_reader :value, :length, :captures
9
+
10
+ def initialize(value, length, captures = [])
11
+ @value = value
12
+ @length = length
13
+ # @captures = captures || value
14
+ @captures = captures
15
+ end
16
+
17
+ def capture!
18
+ if value.is_a?(Array)
19
+ value.each do |v|
20
+ captures << v
21
+ end
22
+ else
23
+ captures << value
24
+ end
25
+
26
+ Success.new(value, length, captures)
27
+ end
28
+
29
+ def inspect
30
+ "Success(value: '#{value}',length: #{length}, capture: '#{captures}')"
31
+ end
32
+
33
+ def success?
34
+ true
35
+ end
36
+
37
+ def failure?
38
+ false
39
+ end
40
+ end
41
+
42
+ class Failure < Result
43
+ attr_reader :message, :committed
44
+
45
+ def initialize(message, committed)
46
+ @message = message
47
+ @committed = committed
48
+ end
49
+
50
+ def inspect
51
+ [
52
+ "Failure",
53
+ "message: #{message}",
54
+ "committed: #{committed}"
55
+ ].join("\n")
56
+ end
57
+
58
+ def map
59
+ self
60
+ end
61
+
62
+ def success?
63
+ false
64
+ end
65
+
66
+ def failure?
67
+ true
68
+ end
69
+
70
+ def captures
71
+ []
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module Dolos
6
+ class StringIOWrapper
7
+ attr_reader :io, :offset, :backup
8
+
9
+ def initialize(str)
10
+ @io = StringIO.new(str.encode('UTF-8'))
11
+ @offset = 0
12
+ end
13
+
14
+ def mark_offset
15
+ @backup = offset
16
+ end
17
+
18
+ def rollback
19
+ @offset = backup
20
+ io.seek(offset)
21
+ end
22
+
23
+ def matches?(utf8_str)
24
+ read = io.read(utf8_str.bytesize)
25
+ io.seek(offset)
26
+
27
+ if read.nil?
28
+ false
29
+ else
30
+ read.force_encoding('UTF-8') == utf8_str
31
+ end
32
+ end
33
+
34
+ def advance(bytesize)
35
+ @offset += bytesize
36
+ io.seek(offset)
37
+ end
38
+
39
+ # A bit tricky, like this whole library
40
+ # Since utf8 characters can be multiple bytes long, we need to
41
+ # read the next byte and check if it's a valid utf8 character
42
+ def peek(bytesize)
43
+ current_position = io.pos
44
+ data = io.read(bytesize)
45
+ io.seek(current_position)
46
+
47
+ return nil if data.nil?
48
+
49
+ while !data.force_encoding('UTF-8').valid_encoding? && bytesize < 4 # a UTF-8 character can be at most 4 bytes long
50
+ bytesize += 1
51
+ data = io.read(bytesize)
52
+ io.seek(current_position)
53
+ end
54
+
55
+ [data.force_encoding('UTF-8'), bytesize]
56
+ end
57
+
58
+
59
+ def matches_regex?(pattern)
60
+ current_position = io.pos
61
+
62
+ remaining_data = io.read
63
+ io.seek(current_position)
64
+
65
+ if (match_data = remaining_data.match(/\A#{pattern}/))
66
+ matched_string = match_data[0]
67
+ io.seek(current_position + matched_string.bytesize)
68
+ return matched_string
69
+ end
70
+
71
+ nil
72
+ end
73
+
74
+
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dolos
4
+ VERSION = "0.1.0"
5
+ end
data/lib/dolos.rb ADDED
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dolos/version"
4
+ require_relative "dolos/parser_state"
5
+ require_relative "dolos/result"
6
+ require_relative "dolos/string_io_wrapper"
7
+ require_relative "dolos/parsers"
8
+
9
+ module Dolos
10
+ include Parsers
11
+
12
+ class Parser
13
+
14
+ attr_accessor :parser_proc
15
+
16
+ def initialize(&block)
17
+ @parser_proc = block
18
+ end
19
+
20
+ def run(input)
21
+ run_with_state(ParserState.new(input))
22
+ end
23
+
24
+ def run_with_state(state)
25
+ parser_proc.call(state)
26
+ end
27
+
28
+ def capture!
29
+ Parser.new do |state|
30
+ result = run_with_state(state)
31
+ if result.success?
32
+ result.capture!
33
+ else
34
+ result
35
+ end
36
+ end
37
+ end
38
+
39
+ def map(&block)
40
+ Parser.new do |state|
41
+ result = run_with_state(state)
42
+ if result.success?
43
+ Success.new(result.value, result.length, block.call(result.captures))
44
+ else
45
+ result
46
+ end
47
+ end
48
+ end
49
+
50
+ def map_value(&block)
51
+ Parser.new do |state|
52
+ result = run_with_state(state)
53
+ if result.success?
54
+ Success.new(block.call(result.value), result.length, result.captures)
55
+ else
56
+ result
57
+ end
58
+ end
59
+ end
60
+
61
+ def flat_map(&block)
62
+ Parser.new do |state|
63
+ result = run_with_state(state)
64
+ if result.success?
65
+ new_parser = block.call(result.value, result.captures)
66
+ new_state = state.dup
67
+ new_state.input.advance(result.length)
68
+ new_parser.run_with_state(new_state)
69
+ else
70
+ result
71
+ end
72
+ end
73
+ end
74
+
75
+ def flatten
76
+ map do |captures|
77
+ captures.flatten
78
+ end
79
+ end
80
+
81
+ def product(other_parser)
82
+ flat_map do |value1, capture1|
83
+ other_parser.map_value do |value2|
84
+ [value1, value2]
85
+ end.map do |capture2|
86
+ [capture1, capture2].flatten
87
+ end
88
+ end
89
+ end
90
+
91
+ alias_method :>>, :product
92
+
93
+ def choice(other_parser)
94
+ Parser.new do |state|
95
+ result = run_with_state(state)
96
+ if result.success?
97
+ result
98
+ else
99
+ other_parser.run_with_state(state)
100
+ end
101
+ end
102
+ end
103
+ alias_method :|, :choice
104
+
105
+ # rep0 # 0 or more
106
+ # rep # 1 or more
107
+ # rep(n = 2) # exactly 2
108
+ # repeat(n_min: 2, n_max: 4) # 2 to 4
109
+ # repeat(n_min: 2) # 2 or more
110
+ def repeat(n_min:, n_max: Float::INFINITY)
111
+ Parser.new do |state|
112
+ results = []
113
+ count = 0
114
+
115
+ while count < n_max
116
+ result = run_with_state(state.dup)
117
+
118
+ break if result.failure?
119
+
120
+ results << result.value
121
+ state.input.advance(result.length)
122
+ count += 1
123
+ end
124
+
125
+ if count < n_min
126
+ Failure.new("Expected parser to match at least #{n_min} times but matched only #{count} times", false)
127
+ else
128
+ Success.new(results, 0) # Passing 0, because we already advanced the input and flatmap will advance it again
129
+ end
130
+ end
131
+ end
132
+
133
+ def zero_or_more
134
+ repeat(n_min: 0, n_max: Float::INFINITY)
135
+ end
136
+ alias_method :rep0, :zero_or_more
137
+
138
+ def one_or_more(exactly = nil)
139
+ if exactly.nil?
140
+ repeat(n_min: 1, n_max: Float::INFINITY)
141
+ else
142
+ repeat(n_min: exactly, n_max: exactly)
143
+ end
144
+ end
145
+ alias_method :rep, :one_or_more
146
+
147
+ def optional
148
+ Parser.new do |state|
149
+ result = run_with_state(state.dup)
150
+ if result.success?
151
+ result
152
+ else
153
+ Success.new([], 0)
154
+ end
155
+ end
156
+ end
157
+ alias_method :opt, :optional
158
+
159
+ end
160
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dolos
4
+ module CommonParsers
5
+ def ws
6
+ regex(/\s/)
7
+ end
8
+
9
+ def eol
10
+ regex(/\n|\r\n|\r/)
11
+ end
12
+
13
+ # Capture as String and convert to integer
14
+ def digit
15
+ regex(/\d/).capture!.map { |capt| capt.map(&:to_i) }
16
+ end
17
+
18
+ # Capture as string
19
+ def digits
20
+ regex(/\d+/)
21
+ end
22
+
23
+ def alpha_num
24
+ regex(/[a-zA-Z0-9]/)
25
+ end
26
+
27
+ def alpha
28
+ regex(/[a-zA-Z]/)
29
+ end
30
+ end
31
+ end
data/lib/example.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'dolos'
3
+ require_relative 'dolos_common_parsers/common_parsers'
4
+
5
+ include Dolos
6
+
7
+ # Include common parsers
8
+ # In future this can be more structured, moved them to separate module to prevent breaking changes
9
+ include Dolos::CommonParsers
10
+
11
+ # Library usage example
12
+ # Parse out a name and address from a letter
13
+ # For higher difficulty, we will not split this into multiple lines, but instead parse it all at once
14
+ letter = <<-LETTER
15
+ Mr. Vardeniui Pavardeniui
16
+ AB „Lietuvos Paštas“
17
+ Totorių g. 8
18
+ 01121 Vilnius
19
+ LETTER
20
+
21
+ # Combine with 'or'
22
+ honorific = c("Mr. ") | c("Mrs. ") | c("Ms. ")
23
+
24
+ # Can be parsed any_char which will include needed letters
25
+ # Or combine LT letters with latin alphabet
26
+ alpha_with_lt = char_in("ąčęėįšųūžĄČĘĖĮŠŲŪŽ") | alpha
27
+
28
+ # Capture all letters in a row and join them,
29
+ # because they are captured as elements of array by each alpha_with_lt parser.
30
+ first_name = alpha_with_lt.rep.capture!.map(&:join)
31
+ last_name = alpha_with_lt.rep.capture!.map(&:join)
32
+
33
+ # Combine first line parsers
34
+ # Consume zero or more whitespace, after that honorific must follow and so on
35
+ name_line = ws.rep0 >> honorific >> first_name >> ws >> last_name >> eol
36
+
37
+ # Next line is company info
38
+ # We could choose to accept UAB and AB or just AB and etc.
39
+ # 'c("AB")' is for case-sensitive string. 'string' can also be used
40
+ company_type = c("AB")
41
+ quote_open = c("„")
42
+ quote_close = c("“")
43
+
44
+ # Consume LT alphabet with whitespace
45
+ company_name = (alpha_with_lt | ws).rep.capture!.map(&:join)
46
+ company_info = company_type >> ws.rep0 >> quote_open >> company_name >> quote_close
47
+ second_line = ws.rep0 >> company_info >> eol
48
+
49
+ # Address line
50
+ # 'char_while' will consume characters while passed predicate is true
51
+ # This could be an alternative to previous 'alpha_with_lt' approach
52
+ # After that result is captured and mapped to hash
53
+ # Mapping to hash so at the end its easy to tell tuples apart
54
+ # Also while mapping, doing some cleaning with '.strip'
55
+ street_name = char_while(->(char) { !char.match(/\d/) }).capture!.map(&:first).map { |s| { street: s.strip } }
56
+ building = digits.capture!.map(&:first).map { |s| { building: s.strip } }
57
+ address_line = ws.rep0 >> street_name >> building >> eol
58
+
59
+ # City line
60
+ # All digits can be matched here or 'digits.rep(5)' could be used. Also joining with map.
61
+ postcode = digits.capture!.map(&:join).map { |s| { postcode: s.strip } }
62
+ city = alpha_with_lt.rep.capture!.map(&:join).map { |s| { city: s.strip } }
63
+ city_line = ws.rep0 >> postcode >> ws >> city >> eol
64
+
65
+ # Full letter parser which is combined from all previous parsers. All previous parsers can be ran separately.
66
+ letter_parser = name_line >> second_line >> address_line >> city_line
67
+ result = letter_parser.run(letter)
68
+
69
+ pp result.captures
@@ -0,0 +1,5 @@
1
+ module Dolos
2
+ module CommonParsers
3
+ def ws: -> Parser[String]
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ module Dolos
2
+ class Parser[A]
3
+ attr_accessor parser_proc: ^(ParserState) -> Result[A]
4
+ def initialize: (^(ParserState) -> Result[A]) -> Parser[A]
5
+ def capture!: -> Parser[A]
6
+ def choice: [B](Parser[B])-> Parser[A | B]
7
+ def flat_map: [B](Parser[A], ^(A) -> Parser[B]) -> Parser[B]
8
+ def flatten: -> Parser[A]
9
+ def map: [B](^(A) -> B) -> Parser[B]
10
+ def map_value: [B](^(A) -> B) -> Parser[B]
11
+ def optional: -> Parser[A?]
12
+ def product: [B](Parser[A]) -> Parser[B]
13
+ def run: (String) -> Result[A]
14
+ def run_with_state: (ParserState) -> Result[A]
15
+ def repeat: (Integer, Integer)-> Parser[Array[A]]
16
+ def zero_or_more: -> Parser[Array[A]]
17
+ def one_or_more: (Integer?) -> Parser[Array[A]]
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ module Dolos
2
+ class ParserState
3
+ attr_reader input: Dolos::StringIOWrapper
4
+
5
+ def initialize: (String) -> void
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Dolos
2
+ module Parsers
3
+ def any_char: -> Parser[String]
4
+ def regex: (Regexp) -> Parser[String]
5
+ def string: (String)-> Parser[String]
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ module Dolos
2
+ class Result[A]
3
+
4
+ end
5
+
6
+ class Success[A] < Result[A]
7
+ attr_reader captures: Array[untyped]
8
+ attr_reader length: Integer
9
+ attr_reader value: A
10
+
11
+ def capture!: -> Success[A]
12
+
13
+ def failure?: -> bool
14
+ def success?: -> bool
15
+ end
16
+
17
+ class Failure < Result[bot]
18
+ attr_reader committed: bool
19
+ attr_reader message: String
20
+
21
+ def captures: -> []
22
+
23
+ def failure?: -> bool
24
+
25
+ def map: [B](^(bot) -> B) -> Result[B]
26
+
27
+ def success?: -> bool
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ module Dolos
2
+ class StringIOWrapper
3
+ attr_reader io: StringIO
4
+ attr_reader offset: Integer
5
+ attr_reader backup: Integer
6
+
7
+ def initialize: (String) -> void
8
+
9
+ def advance: (Integer)-> void
10
+
11
+ def mark_offset: -> void
12
+
13
+ def matches?: (String) -> bool
14
+
15
+ def peek: -> [String?, Integer]
16
+
17
+ def rollback: -> void
18
+ end
19
+ end
data/sig/dolos.rbs ADDED
@@ -0,0 +1,5 @@
1
+ module Dolos
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+
5
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dolos
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - benetis
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-08-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - git@benetis.me
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rspec"
21
+ - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ - Rakefile
25
+ - docs/dolos_stable_diff.png
26
+ - dolos.gemspec
27
+ - lib/dolos.rb
28
+ - lib/dolos/parser_state.rb
29
+ - lib/dolos/parsers.rb
30
+ - lib/dolos/result.rb
31
+ - lib/dolos/string_io_wrapper.rb
32
+ - lib/dolos/version.rb
33
+ - lib/dolos_common_parsers/common_parsers.rb
34
+ - lib/example.rb
35
+ - sig/dolos.rbs
36
+ - sig/dolos/common_parsers.rbs
37
+ - sig/dolos/parser.rbs
38
+ - sig/dolos/parser_state.rbs
39
+ - sig/dolos/parsers.rbs
40
+ - sig/dolos/result.rbs
41
+ - sig/dolos/string_io_wrapper.rbs
42
+ homepage: https://github.com/benetis/dolos
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ allowed_push_host: https://rubygems.org
47
+ homepage_uri: https://github.com/benetis/dolos
48
+ source_code_uri: https://github.com/benetis/dolos
49
+ changelog_uri: https://github.com/benetis/dolos/blob/master/CHANGELOG.md
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.1.0
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.4.15
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: Parser combinator library for Ruby.
69
+ test_files: []