dolos 0.1.3 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/setup'
3
+ require 'dolos'
4
+ require 'dolos_common_parsers/common_parsers'
5
+ require 'benchmark/ips'
6
+
7
+ include Dolos
8
+
9
+ # Include common parsers
10
+ # In future this can be more structured, moved them to separate module to prevent breaking changes
11
+ include Dolos::CommonParsers
12
+
13
+ # Library usage example
14
+ # Parse out a name and address from a letter
15
+ # For higher difficulty, we will not split this into multiple lines, but instead parse it all at once
16
+ letter = <<-LETTER
17
+ Mr. Vardeniui Pavardeniui
18
+ AB „Lietuvos Paštas“
19
+ Totorių g. 8
20
+ 01121 Vilnius
21
+ LETTER
22
+
23
+ # Combine with 'or'
24
+ honorific = c("Mr. ") | c("Mrs. ") | c("Ms. ")
25
+
26
+ # Can be parsed any_char which will include needed letters
27
+ # Or combine LT letters with latin alphabet
28
+ alpha_with_lt = char_in("ąčęėįšųūžĄČĘĖĮŠŲŪŽ") | alpha
29
+
30
+ # Capture all letters in a row and join them,
31
+ # because they are captured as elements of array by each alpha_with_lt parser.
32
+ first_name = alpha_with_lt.rep.map(&:join).capture!
33
+ last_name = alpha_with_lt.rep.map(&:join).capture!
34
+
35
+ # Combine first line parsers
36
+ # Consume zero or more whitespace, after that honorific must follow and so on
37
+ name_line = ws_rep0 & honorific & first_name & ws & last_name & eol
38
+
39
+ # Next line is company info
40
+ # We could choose to accept UAB and AB or just AB and etc.
41
+ # 'c("AB")' is for case-sensitive string. 'string' can also be used
42
+ company_type = c("AB")
43
+ quote_open = c("„")
44
+ quote_close = c("“")
45
+
46
+ # Consume LT alphabet with whitespace
47
+ company_name = (alpha_with_lt | ws).rep.map(&:join).capture!
48
+ company_info = company_type & ws_rep0 & quote_open & company_name & quote_close
49
+ second_line = ws_rep0 & company_info & eol
50
+
51
+ # Address line
52
+ # 'char_while' will consume characters while passed predicate is true
53
+ # This could be an alternative to previous 'alpha_with_lt' approach
54
+ # After that result is captured and mapped to hash
55
+ # Mapping to hash so at the end its easy to tell tuples apart
56
+ # Also while mapping, doing some cleaning with '.strip'
57
+ street_name = char_while(->(char) { !char.match(/\d/) }).map { |s| { street: s.strip } }.capture!
58
+ building = digits.map { |s| { building: s.strip } }.capture!
59
+ address_line = ws_rep0 & street_name & building & eol
60
+
61
+ # City line
62
+ # All digits can be matched here or 'digits.rep(5)' could be used. Also joining with map.
63
+ postcode = digits.map { |s| { postcode: s.strip } }.capture!
64
+ city = alpha_with_lt.rep.map(&:join).map { |s| { city: s.strip } }.capture!
65
+ city_line = ws_rep0 & postcode & ws & city & eol
66
+
67
+ # Full letter parser which is combined from all previous parsers. All previous parsers can be ran separately.
68
+ letter_parser = name_line & second_line & address_line & city_line
69
+ result = letter_parser.run(letter)
70
+
71
+ puts result.success?
72
+
73
+ Benchmark.ips do |x|
74
+ x.report('letter benchmark') do
75
+ letter_parser.run(letter)
76
+ end
77
+ x.compare!
78
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require_relative 'dolos'
3
- require_relative 'dolos_common_parsers/common_parsers'
2
+ require 'dolos'
3
+ require 'dolos_common_parsers/common_parsers'
4
4
 
5
5
  include Dolos
6
6
 
@@ -27,12 +27,12 @@ alpha_with_lt = char_in("ąčęėįšųūžĄČĘĖĮŠŲŪŽ") | alpha
27
27
 
28
28
  # Capture all letters in a row and join them,
29
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)
30
+ first_name = alpha_with_lt.rep.map(&:join).capture!
31
+ last_name = alpha_with_lt.rep.map(&:join).capture!
32
32
 
33
33
  # Combine first line parsers
34
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
35
+ name_line = ws.rep0 & honorific & first_name & ws & last_name & eol
36
36
 
37
37
  # Next line is company info
38
38
  # We could choose to accept UAB and AB or just AB and etc.
@@ -42,9 +42,9 @@ quote_open = c("„")
42
42
  quote_close = c("“")
43
43
 
44
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
45
+ company_name = (alpha_with_lt | ws).rep.map(&:join).capture!
46
+ company_info = company_type & ws.rep0 & quote_open & company_name & quote_close
47
+ second_line = ws.rep0 & company_info & eol
48
48
 
49
49
  # Address line
50
50
  # 'char_while' will consume characters while passed predicate is true
@@ -52,18 +52,18 @@ second_line = ws.rep0 >> company_info >> eol
52
52
  # After that result is captured and mapped to hash
53
53
  # Mapping to hash so at the end its easy to tell tuples apart
54
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
55
+ street_name = char_while(->(char) { !char.match(/\d/) }).map { |s| { street: s.strip } }.capture!
56
+ building = digits.map { |s| { building: s.strip } }.capture!
57
+ address_line = ws.rep0 & street_name & building & eol
58
58
 
59
59
  # City line
60
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
61
+ postcode = digits.map { |s| { postcode: s.strip } }.capture!
62
+ city = alpha_with_lt.rep.map(&:join).map { |s| { city: s.strip } }.capture!
63
+ city_line = ws.rep0 & postcode & ws & city & eol
64
64
 
65
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
66
+ letter_parser = name_line & second_line & address_line & city_line
67
67
  result = letter_parser.run(letter)
68
68
 
69
69
  pp result.captures
data/lib/dolos/parsers.rb CHANGED
@@ -3,9 +3,10 @@
3
3
  module Dolos
4
4
  module Parsers
5
5
  def string(str)
6
+ utf8_str = str.encode('UTF-8')
7
+
6
8
  Parser.new do |state|
7
9
  state.input.mark_offset
8
- utf8_str = str.encode('UTF-8')
9
10
  if state.input.matches?(utf8_str)
10
11
  Success.new(utf8_str, str.bytesize)
11
12
  else
@@ -13,7 +14,7 @@ module Dolos
13
14
  got_error = state.input.io.string.byteslice(state.input.backup, advanced)
14
15
  state.input.rollback
15
16
  Failure.new(
16
- "Expected #{str.inspect} but got #{got_error.inspect}",
17
+ -> { "Expected #{str.inspect} but got #{got_error.inspect}" },
17
18
  advanced,
18
19
  state
19
20
  )
@@ -32,7 +33,7 @@ module Dolos
32
33
  advanced = state.input.offset
33
34
  state.input.rollback
34
35
  Failure.new(
35
- "Expected pattern #{pattern.inspect} but got #{state.input.io.string.inspect}",
36
+ -> { "Expected pattern #{pattern.inspect} but got #{state.input.io.string.inspect}" },
36
37
  advanced,
37
38
  state
38
39
  )
@@ -52,7 +53,7 @@ module Dolos
52
53
  advanced = state.input.offset
53
54
  state.input.rollback
54
55
  Failure.new(
55
- 'Expected any character but got end of input',
56
+ -> { 'Expected any character but got end of input' },
56
57
  advanced,
57
58
  state
58
59
  )
@@ -64,20 +65,20 @@ module Dolos
64
65
  # Example:
65
66
  # char_in('abc').run('b') # => Success.new('b', 1)
66
67
  def char_in(characters_string)
67
- characters_array = characters_string.chars
68
+ characters_set = characters_string.chars
68
69
 
69
70
  Parser.new do |state|
70
71
  state.input.mark_offset
71
72
 
72
73
  char, bytesize = state.input.peek(1)
73
74
 
74
- if char && characters_array.include?(char)
75
+ if char && characters_set.include?(char)
75
76
  Success.new(char, bytesize)
76
77
  else
77
78
  advanced = state.input.offset
78
79
  state.input.rollback
79
80
  Failure.new(
80
- "Expected one of #{characters_array.inspect} but got #{char.inspect}",
81
+ -> { "Expected one of #{characters_set.to_a.inspect} but got #{char.inspect}" },
81
82
  advanced,
82
83
  state
83
84
  )
@@ -90,18 +91,18 @@ module Dolos
90
91
  state.input.mark_offset
91
92
 
92
93
  buffer = String.new
93
- loop do
94
- char, bytesize = state.input.peek(1)
95
- break if char.nil? || !predicate.call(char)
94
+ char, bytesize = state.input.peek(1)
96
95
 
96
+ while char && predicate.call(char)
97
97
  buffer << char
98
98
  state.input.advance(bytesize)
99
+ char, bytesize = state.input.peek(1)
99
100
  end
100
101
 
101
102
  if buffer.empty?
102
103
  advanced = state.input.offset
103
104
  Failure.new(
104
- "Predicate never returned true",
105
+ -> { "Predicate never returned true" },
105
106
  advanced,
106
107
  state
107
108
  )
@@ -111,5 +112,23 @@ module Dolos
111
112
  end
112
113
  end
113
114
 
115
+ def recursive(&block)
116
+ recursive_parser = nil
117
+
118
+ placeholder = Parser.new do |state|
119
+ raise "Recursive parser accessed before it was initialized!" if recursive_parser.nil?
120
+
121
+ recursive_parser.call.run_with_state(state).tap do |result|
122
+ if result.failure?
123
+ error_msg = -> { "Error in recursive structure around position #{state.input.offset}: #{result.message}" }
124
+ Failure.new(error_msg, state.input.offset, state)
125
+ end
126
+ end
127
+ end
128
+
129
+ recursive_parser = -> { block.call(placeholder) }
130
+ placeholder
131
+ end
132
+
114
133
  end
115
134
  end
data/lib/dolos/result.rb CHANGED
@@ -10,20 +10,21 @@ module Dolos
10
10
  def initialize(value, length, captures = [])
11
11
  @value = value
12
12
  @length = length
13
- # @captures = captures || value
14
13
  @captures = captures
15
14
  end
16
15
 
17
- def capture!
18
- if value.is_a?(Array)
19
- value.each do |v|
20
- captures << v
21
- end
16
+ # can be some named capture, :street, {:street => capture }
17
+ # or an array, [], [capture]
18
+ def capture!(wrap_in = nil)
19
+ mapped_value = self.value # use the transformed value here
20
+
21
+ if wrap_in.is_a?(Array)
22
+ save_capture([mapped_value])
23
+ elsif wrap_in.is_a?(Symbol)
24
+ save_capture({ wrap_in => mapped_value })
22
25
  else
23
- captures << value
26
+ save_capture(mapped_value)
24
27
  end
25
-
26
- Success.new(value, length, captures)
27
28
  end
28
29
 
29
30
  def inspect
@@ -37,15 +38,38 @@ module Dolos
37
38
  def failure?
38
39
  false
39
40
  end
41
+
42
+ private
43
+
44
+ def save_capture(val)
45
+ if val.is_a?(Array)
46
+ val.each do |v|
47
+ captures << v
48
+ end
49
+ else
50
+ captures << val
51
+ end
52
+
53
+ Success.new(val, length, captures)
54
+ end
40
55
  end
41
56
 
42
57
  class Failure < Result
43
- attr_reader :message, :error_position, :state
58
+ attr_reader :error_position, :state
44
59
 
45
- def initialize(message, error_position, state)
46
- @message = message
60
+ def initialize(message_proc, error_position, state)
61
+ @message_proc = message_proc
47
62
  @error_position = error_position
48
63
  @state = state
64
+ @message_evaluated = false
65
+ end
66
+
67
+ def message
68
+ unless @message_evaluated
69
+ @message_value = @message_proc.call
70
+ @message_evaluated = true
71
+ end
72
+ @message_value
49
73
  end
50
74
 
51
75
  def inspect
@@ -22,12 +22,7 @@ module Dolos
22
22
 
23
23
  def matches?(utf8_str)
24
24
  read = io.read(utf8_str.bytesize)
25
-
26
- if read.nil?
27
- false
28
- else
29
- read.force_encoding('UTF-8') == utf8_str
30
- end
25
+ !read.nil? && read.force_encoding('UTF-8') == utf8_str
31
26
  end
32
27
 
33
28
  def advance(bytesize)
@@ -61,8 +56,8 @@ module Dolos
61
56
  remaining_data = io.read
62
57
  io.seek(current_position)
63
58
 
64
- if (match_data = remaining_data.match(/\A#{pattern}/))
65
- matched_string = match_data[0]
59
+ if remaining_data =~ /\A#{pattern}/
60
+ matched_string = $&
66
61
  io.seek(current_position + matched_string.bytesize)
67
62
  return matched_string
68
63
  end
data/lib/dolos/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dolos
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/dolos.rb CHANGED
@@ -10,9 +10,7 @@ module Dolos
10
10
  include Parsers
11
11
 
12
12
  class Parser
13
-
14
13
  attr_accessor :parser_proc
15
-
16
14
  def initialize(&block)
17
15
  @parser_proc = block
18
16
  end
@@ -22,54 +20,42 @@ module Dolos
22
20
  end
23
21
 
24
22
  def run_with_state(state)
25
- result = parser_proc.call(state)
26
- if result.success?
27
- state.last_success_position = state.input.offset
28
- end
23
+ result = @parser_proc.call(state)
24
+ state.last_success_position = state.input.offset if result.success?
29
25
  result
30
26
  end
31
27
 
32
- def capture!
28
+ def capture!(wrap_in = nil)
33
29
  Parser.new do |state|
34
30
  result = run_with_state(state)
35
- if result.success?
36
- result.capture!
37
- else
38
- result
39
- end
31
+ result.success? ? result.capture!(wrap_in) : result
40
32
  end
41
33
  end
42
34
 
43
- def map(&block)
35
+ # Will call `map` on captures
36
+ def map_captures(&block)
44
37
  Parser.new do |state|
45
38
  result = run_with_state(state)
46
- if result.success?
47
- Success.new(result.value, result.length, block.call(result.captures))
48
- else
49
- result
50
- end
39
+ result.success? ? Success.new(result.value, result.length, block.call(result.captures)) : result
51
40
  end
52
41
  end
53
42
 
54
- def map_value(&block)
43
+ # Will call block on tuple of value
44
+ def map(&block)
55
45
  Parser.new do |state|
56
46
  result = run_with_state(state)
57
- if result.success?
58
- Success.new(block.call(result.value), result.length, result.captures)
59
- else
60
- result
61
- end
47
+ result.success? ? Success.new(block.call(result.value), result.length, result.captures) : result
62
48
  end
63
49
  end
64
50
 
65
- def flat_map(&block)
51
+ def combine(&block)
66
52
  Parser.new do |state|
67
53
  result = run_with_state(state)
54
+
68
55
  if result.success?
56
+ state.input.advance(result.length)
69
57
  new_parser = block.call(result.value, result.captures)
70
- new_state = state.dup
71
- new_state.input.advance(result.length)
72
- new_parser.run_with_state(new_state)
58
+ new_parser.run_with_state(state)
73
59
  else
74
60
  result
75
61
  end
@@ -77,22 +63,44 @@ module Dolos
77
63
  end
78
64
 
79
65
  def flatten
80
- map do |captures|
66
+ map_captures do |captures|
81
67
  captures.flatten
82
68
  end
83
69
  end
84
70
 
85
71
  def product(other_parser)
86
- flat_map do |value1, capture1|
87
- other_parser.map_value do |value2|
72
+ combine do |value1, capture1|
73
+ other_parser.map do |value2|
88
74
  [value1, value2]
89
- end.map do |capture2|
75
+ end.map_captures do |capture2|
90
76
  [capture1, capture2].flatten
91
77
  end
92
78
  end
93
79
  end
80
+ alias_method :&, :product
94
81
 
95
- alias_method :>>, :product
82
+ def product_l(other_parser)
83
+ combine do |value1, capture1|
84
+ other_parser.map do |_|
85
+ value1
86
+ end.map_captures do |capture2|
87
+ [capture1, capture2].flatten
88
+ end
89
+ end
90
+ end
91
+
92
+ def product_r(other_parser)
93
+ combine do |_, capture1|
94
+ other_parser.map do |value2|
95
+ value2
96
+ end.map_captures do |capture2|
97
+ [capture1, capture2].flatten
98
+ end
99
+ end
100
+ end
101
+
102
+ alias_method :<<, :product_l
103
+ alias_method :>>, :product_r
96
104
 
97
105
  def choice(other_parser)
98
106
  Parser.new do |state|
@@ -111,29 +119,36 @@ module Dolos
111
119
  # rep(n = 2) # exactly 2
112
120
  # repeat(n_min: 2, n_max: 4) # 2 to 4
113
121
  # repeat(n_min: 2) # 2 or more
114
- def repeat(n_min:, n_max: Float::INFINITY)
122
+ def repeat(n_min:, n_max: Float::INFINITY, separator: nil)
115
123
  Parser.new do |state|
116
124
  values = []
117
125
  captures = []
118
126
  count = 0
119
- state.input.mark_offset
120
127
 
121
- while count < n_max
122
- result = run_with_state(state.dup)
128
+ loop do
129
+ result = run_with_state(state) # Removing .dup for performance. Be cautious of side effects.
123
130
 
124
- break if result.failure?
131
+ if result.failure? || count >= n_max
132
+ break
133
+ end
125
134
 
126
135
  values << result.value
127
136
  captures.concat(result.captures)
128
137
  state.input.advance(result.length)
129
138
  count += 1
139
+
140
+ if separator && count < n_max
141
+ sep_result = separator.run_with_state(state) # Removing .dup for performance. Be cautious of side effects.
142
+ break if sep_result.failure?
143
+
144
+ state.input.advance(sep_result.length)
145
+ end
130
146
  end
131
147
 
132
148
  if count < n_min
133
- error_pos = state.input.offset
134
149
  Failure.new(
135
- "Expected parser to match at least #{n_min} times but matched only #{count} times",
136
- error_pos,
150
+ -> { "Expected parser to match at least #{n_min} times but matched only #{count} times" },
151
+ state.input.offset,
137
152
  state
138
153
  )
139
154
  else
@@ -141,7 +156,6 @@ module Dolos
141
156
  end
142
157
  end
143
158
  end
144
-
145
159
  def zero_or_more
146
160
  repeat(n_min: 0, n_max: Float::INFINITY)
147
161
  end
@@ -168,5 +182,15 @@ module Dolos
168
182
  end
169
183
  alias_method :opt, :optional
170
184
 
185
+ # Used to declare lazy parser to avoid infinite loops in recursive parsers
186
+ def lazy
187
+ parser_memo = nil
188
+
189
+ Parser.new do |state|
190
+ parser_memo ||= self
191
+ parser_memo.run_with_state(state)
192
+ end
193
+ end
194
+
171
195
  end
172
196
  end
@@ -6,13 +6,20 @@ module Dolos
6
6
  regex(/\s/)
7
7
  end
8
8
 
9
+ def ws_rep0
10
+ regex(/\s*/)
11
+ end
12
+
9
13
  def eol
10
14
  regex(/\n|\r\n|\r/)
11
15
  end
12
16
 
13
- # Capture as String and convert to integer
14
17
  def digit
15
- regex(/\d/).capture!.map { |capt| capt.map(&:to_i) }
18
+ regex(/\d/)
19
+ end
20
+
21
+ def int
22
+ digit.map(&:to_i)
16
23
  end
17
24
 
18
25
  # Capture as string
@@ -1,5 +1,16 @@
1
1
  module Dolos
2
2
  module CommonParsers
3
+ def digit: -> Parser[String]
4
+ def digits: -> Parser[String]
5
+
6
+ def int: -> Parser[Integer]
7
+
8
+ def eol: -> Parser[String]
9
+
3
10
  def ws: -> Parser[String]
11
+ def ws_rep0: -> Parser[String]
12
+
13
+ def alpha: -> Parser[String]
14
+ def alphanum: -> Parser[String]
4
15
  end
5
16
  end
data/sig/dolos/parser.rbs CHANGED
@@ -4,16 +4,20 @@ module Dolos
4
4
  def initialize: (^(ParserState) -> Result[A]) -> Parser[A]
5
5
  def capture!: -> Parser[A]
6
6
  def choice: [B](Parser[B])-> Parser[A | B]
7
+ def combine: [B](^(A, B) -> Parser[B]) -> Parser[B]
7
8
  def flat_map: [B](Parser[A], ^(A) -> Parser[B]) -> Parser[B]
8
9
  def flatten: -> Parser[A]
9
10
  def map: [B](^(A) -> B) -> Parser[B]
10
- def map_value: [B](^(A) -> B) -> Parser[B]
11
+ def map_captures: [B](^(A) -> B) -> Parser[B]
11
12
  def optional: -> Parser[A?]
12
13
  def product: [B](Parser[A]) -> Parser[B]
14
+ def product_l: [B](Parser[B]) -> Parser[B]
15
+ def product_r: [B](Parser[B]) -> Parser[A]
13
16
  def run: (String) -> Result[A]
14
17
  def run_with_state: (ParserState) -> Result[A]
15
- def repeat: (Integer, Integer)-> Parser[Array[A]]
18
+ def repeat: [B](Integer, Integer, Parser[B]?)-> Parser[Array[A]]
16
19
  def zero_or_more: -> Parser[Array[A]]
17
20
  def one_or_more: (Integer?) -> Parser[Array[A]]
21
+ def lazy: -> Parser[A]
18
22
  end
19
23
  end
@@ -1,6 +1,6 @@
1
1
  module Dolos
2
2
  class ParserState
3
- attr_reader input: Dolos::StringIOWrapper
3
+ attr_reader input: StringIOWrapper
4
4
  attr_accessor last_success_position: Integer
5
5
 
6
6
  def initialize: (String) -> void
@@ -1,6 +1,10 @@
1
1
  module Dolos
2
2
  module Parsers
3
3
  def any_char: -> Parser[String]
4
+ def char_in: -> Parser[String]
5
+ def char_while : -> Parser[String]
6
+ def recursive: [A,B,C]() { (Parser[A]) -> Parser[B] } -> Parser[C]
7
+
4
8
  def regex: (Regexp) -> Parser[String]
5
9
  def string: (String)-> Parser[String]
6
10
  end
data/sig/dolos/result.rbs CHANGED
@@ -15,6 +15,11 @@ module Dolos
15
15
  end
16
16
 
17
17
  class Failure < Result[bot]
18
+ @message_proc: ^-> String
19
+ @message_evaluated: bool
20
+ @message_value: String
21
+ @state: ParserState
22
+
18
23
  attr_reader committed: bool
19
24
  attr_reader error_position: Integer
20
25
  attr_reader message: String
@@ -25,6 +30,8 @@ module Dolos
25
30
 
26
31
  def map: [B](^(bot) -> B) -> Result[B]
27
32
 
33
+ def pretty_print: -> String
34
+
28
35
  def success?: -> bool
29
36
  end
30
37
  end