mustermann-strscan 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 06f8fd306c3c984eba6afc9ca05e88efe2e03f78
4
+ data.tar.gz: 9f3b1baf5062f5f6ef3d1ff10607cddb4d48a8c7
5
+ SHA512:
6
+ metadata.gz: 930c1721880ad0c61a8110181f5f14be48f6a8d0b35891c3234da4f0a20b7d7c3ebdf464d9899283a064b3b353b96fe2b358c4be765720406c3b470e3d312037
7
+ data.tar.gz: 31b9fefd04dba3e49f9231653476adb4744c4879327004f390a56404c5c31ccf23152a31afe5017d1064e2af4094fabf8f043487729a6a290cf4624993f3f239
@@ -0,0 +1,38 @@
1
+ # String Scanner for Mustermann
2
+
3
+ This gem implements `Mustermann::StringScanner`, a tool inspired by Ruby's [`StringScanner`]() class.
4
+
5
+ ``` ruby
6
+ require 'mustermann/string_scanner'
7
+ scanner = Mustermann::StringScanner.new("here is our example string")
8
+
9
+ scanner.scan("here") # => "here"
10
+ scanner.getch # => " "
11
+
12
+ if scanner.scan(":verb our")
13
+ scanner.scan(:noun, capture: :word)
14
+ scanner[:verb] # => "is"
15
+ scanner[:nound] # => "example"
16
+ end
17
+
18
+ scanner.rest # => "string"
19
+ ```
20
+
21
+ You can pass it pattern objects directly:
22
+
23
+ ``` ruby
24
+ pattern = Mustermann.new(':name')
25
+ scanner.check(pattern)
26
+ ```
27
+
28
+ Or have `#scan` (and other methods) check these for you.
29
+
30
+ ``` ruby
31
+ scanner.check('{name}', type: :template)
32
+ ```
33
+
34
+ You can also pass in default options for ad hoc patterns when creating the scanner:
35
+
36
+ ``` ruby
37
+ scanner = Mustermann::StringScanner.new(input, type: :shell)
38
+ ```
@@ -0,0 +1,313 @@
1
+ require 'mustermann'
2
+ require 'mustermann/pattern_cache'
3
+ require 'delegate'
4
+
5
+ module Mustermann
6
+ # Class inspired by Ruby's StringScanner to scan an input string using multiple patterns.
7
+ #
8
+ # @example
9
+ # require 'mustermann/string_scanner'
10
+ # scanner = Mustermann::StringScanner.new("here is our example string")
11
+ #
12
+ # scanner.scan("here") # => "here"
13
+ # scanner.getch # => " "
14
+ #
15
+ # if scanner.scan(":verb our")
16
+ # scanner.scan(:noun, capture: :word)
17
+ # scanner[:verb] # => "is"
18
+ # scanner[:nound] # => "example"
19
+ # end
20
+ #
21
+ # scanner.rest # => "string"
22
+ #
23
+ # @note
24
+ # This structure is not thread-safe, you should not scan on the same StringScanner instance concurrently.
25
+ # Even if it was thread-safe, scanning concurrently would probably lead to unwanted behaviour.
26
+ class StringScanner
27
+ # Exception raised if scan/unscan operation cannot be performed.
28
+ ScanError = Class.new(::ScanError)
29
+ PATTERN_CACHE = PatternCache.new
30
+ private_constant :PATTERN_CACHE
31
+
32
+ # Patterns created by {#scan} will be globally cached, since we assume that there is a finite number
33
+ # of different patterns used and that they are more likely to be reused than not.
34
+ # This method allows clearing the cache.
35
+ #
36
+ # @see Mustermann::PatternCache
37
+ def self.clear_cache
38
+ PATTERN_CACHE.clear
39
+ end
40
+
41
+ # @return [Integer] number of cached patterns
42
+ # @see clear_cache
43
+ # @api private
44
+ def self.cache_size
45
+ PATTERN_CACHE.size
46
+ end
47
+
48
+ # Encapsulates return values for {StringScanner#scan}, {StringScanner#check}, and friends.
49
+ # Behaves like a String (the substring which matched the pattern), but also exposes its position
50
+ # in the main string and any params parsed from it.
51
+ class ScanResult < DelegateClass(String)
52
+ # The scanner this result came from.
53
+ # @example
54
+ # require 'mustermann/string_scanner'
55
+ # scanner = Mustermann::StringScanner.new('foo/bar')
56
+ # scanner.scan(:name).scanner == scanner # => true
57
+ attr_reader :scanner
58
+
59
+ # @example
60
+ # require 'mustermann/string_scanner'
61
+ # scanner = Mustermann::StringScanner.new('foo/bar')
62
+ # scanner.scan(:name).position # => 0
63
+ # scanner.getch.position # => 3
64
+ # scanner.scan(:name).position # => 4
65
+ #
66
+ # @return [Integer] position the substring starts at
67
+ attr_reader :position
68
+ alias_method :pos, :position
69
+
70
+ # @example
71
+ # require 'mustermann/string_scanner'
72
+ # scanner = Mustermann::StringScanner.new('foo/bar')
73
+ # scanner.scan(:name).length # => 3
74
+ # scanner.getch.length # => 1
75
+ # scanner.scan(:name).length # => 3
76
+ #
77
+ # @return [Integer] length of the substring
78
+ attr_reader :length
79
+
80
+ # Params parsed from the substring.
81
+ # Will not include params from previous scan results.
82
+ #
83
+ # @example
84
+ # require 'mustermann/string_scanner'
85
+ # scanner = Mustermann::StringScanner.new('foo/bar')
86
+ # scanner.scan(:name).params # => { "name" => "foo" }
87
+ # scanner.getch.params # => {}
88
+ # scanner.scan(:name).params # => { "name" => "bar" }
89
+ #
90
+ # @see Mustermann::StringScanner#params
91
+ # @see Mustermann::StringScanner#[]
92
+ #
93
+ # @return [Hash] params parsed from the substring
94
+ attr_reader :params
95
+
96
+ # @api private
97
+ def initialize(scanner, position, length, params = {})
98
+ @scanner, @position, @length, @params = scanner, position, length, params
99
+ end
100
+
101
+ # @api private
102
+ # @!visibility private
103
+ def __getobj__
104
+ @__getobj__ ||= scanner.to_s[position, length]
105
+ end
106
+ end
107
+
108
+ # @return [Hash] default pattern options used for {#scan} and similar methods
109
+ # @see #initialize
110
+ attr_reader :pattern_options
111
+
112
+ # Params from all previous matches from {#scan} and {#scan_until},
113
+ # but not from {#check} and {#check_until}. Changes can be reverted
114
+ # with {#unscan} and it can be completely cleared via {#reset}.
115
+ #
116
+ # @return [Hash] current params
117
+ attr_reader :params
118
+
119
+ # @return [Integer] current scan position on the input string
120
+ attr_accessor :position
121
+ alias_method :pos, :position
122
+ alias_method :pos=, :position=
123
+
124
+ # @example with different default type
125
+ # require 'mustermann/string_scanner'
126
+ # scanner = Mustermann::StringScanner.new("foo/bar/baz", type: :shell)
127
+ # scanner.scan('*') # => "foo"
128
+ # scanner.scan('**/*') # => "/bar/baz"
129
+ #
130
+ # @param [String] string the string to scan
131
+ # @param [Hash] pattern_options default options used for {#scan}
132
+ def initialize(string = "", **pattern_options)
133
+ @pattern_options = pattern_options
134
+ @string = String(string).dup
135
+ reset
136
+ end
137
+
138
+ # Resets the {#position} to the start and clears all {#params}.
139
+ # @return [Mustermann::StringScanner] the scanner itself
140
+ def reset
141
+ @position = 0
142
+ @params = {}
143
+ @history = []
144
+ self
145
+ end
146
+
147
+ # Moves the position to the end of the input string.
148
+ # @return [Mustermann::StringScanner] the scanner itself
149
+ def terminate
150
+ track_result ScanResult.new(self, @position, size - @position)
151
+ self
152
+ end
153
+
154
+ # Checks if the given pattern matches any substring starting at the current position.
155
+ #
156
+ # If it does, it will advance the current {#position} to the end of the substring and merges any params parsed
157
+ # from the substring into {#params}.
158
+ #
159
+ # @param (see Mustermann.new)
160
+ # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
161
+ def scan(pattern, **options)
162
+ track_result check(pattern, **options)
163
+ end
164
+
165
+ # Checks if the given pattern matches any substring starting at any position after the current position.
166
+ #
167
+ # If it does, it will advance the current {#position} to the end of the substring and merges any params parsed
168
+ # from the substring into {#params}.
169
+ #
170
+ # @param (see Mustermann.new)
171
+ # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
172
+ def scan_until(pattern, **options)
173
+ result, prefix = check_until_with_prefix(pattern, **options)
174
+ track_result(prefix, result)
175
+ end
176
+
177
+ # Reverts the last operation that advanced the position.
178
+ #
179
+ # Operations advancing the position: {#terminate}, {#scan}, {#scan_until}, {#getch}.
180
+ # @return [Mustermann::StringScanner] the scanner itself
181
+ def unscan
182
+ raise ScanError, 'unscan failed: previous match record not exist' if @history.empty?
183
+ previous = @history[0..-2]
184
+ reset
185
+ previous.each { |r| track_result(*r) }
186
+ self
187
+ end
188
+
189
+ # Checks if the given pattern matches any substring starting at the current position.
190
+ #
191
+ # Does not affect {#position} or {#params}.
192
+ #
193
+ # @param (see Mustermann.new)
194
+ # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
195
+ def check(pattern, **options)
196
+ params, length = create_pattern(pattern, **options).peek_params(rest)
197
+ ScanResult.new(self, @position, length, params) if params
198
+ end
199
+
200
+ # Checks if the given pattern matches any substring starting at any position after the current position.
201
+ #
202
+ # Does not affect {#position} or {#params}.
203
+ #
204
+ # @param (see Mustermann.new)
205
+ # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
206
+ def check_until(pattern, **options)
207
+ check_until_with_prefix(pattern, **options).first
208
+ end
209
+
210
+ def check_until_with_prefix(pattern, **options)
211
+ start = @position
212
+ @position += 1 until eos? or result = check(pattern, **options)
213
+ prefix = ScanResult.new(self, start, @position - start) if result
214
+ [result, prefix]
215
+ ensure
216
+ @position = start
217
+ end
218
+
219
+ # Reads a single character and advances the {#position} by one.
220
+ # @return [Mustermann::StringScanner::ScanResult, nil] the character, nil if at end of string
221
+ def getch
222
+ track_result ScanResult.new(self, @position, 1) unless eos?
223
+ end
224
+
225
+ # Appends the given string to the string being scanned
226
+ #
227
+ # @example
228
+ # require 'mustermann/string_scanner'
229
+ # scanner = Mustermann::StringScanner.new
230
+ # scanner << "foo"
231
+ # scanner.scan(/.+/) # => "foo"
232
+ #
233
+ # @param [String] string will be appended
234
+ # @return [Mustermann::StringScanner] the scanner itself
235
+ def <<(string)
236
+ @string << string
237
+ self
238
+ end
239
+
240
+ # @return [true, false] whether or not the end of the string has been reached
241
+ def eos?
242
+ @position >= @string.size
243
+ end
244
+
245
+ # @return [true, false] whether or not the current position is at the start of a line
246
+ def beginning_of_line?
247
+ @position == 0 or @string[@position - 1] == "\n"
248
+ end
249
+
250
+ # @return [String] outstanding string not yet matched, empty string at end of input string
251
+ def rest
252
+ @string[@position..-1] || ""
253
+ end
254
+
255
+ # @return [Integer] number of character remaining to be scanned
256
+ def rest_size
257
+ @position > size ? 0 : size - @position
258
+ end
259
+
260
+ # Allows to peek at a number of still unscanned characters without advacing the {#position}.
261
+ #
262
+ # @param [Integer] length how many characters to look at
263
+ # @return [String] the substring
264
+ def peek(length = 1)
265
+ @string[@position, length]
266
+ end
267
+
268
+ # Shorthand for accessing {#params}. Accepts symbols as keys.
269
+ def [](key)
270
+ params[key.to_s]
271
+ end
272
+
273
+ # (see #params)
274
+ def to_h
275
+ params.dup
276
+ end
277
+
278
+ # @return [String] the input string
279
+ # @see #initialize
280
+ # @see #<<
281
+ def to_s
282
+ @string.dup
283
+ end
284
+
285
+ # @return [Integer] size of the input string
286
+ def size
287
+ @string.size
288
+ end
289
+
290
+ # @!visibility private
291
+ def inspect
292
+ "#<%p %d/%d @ %p>" % [ self.class, @position, @string.size, @string ]
293
+ end
294
+
295
+ # @!visibility private
296
+ def create_pattern(pattern, **options)
297
+ PATTERN_CACHE.create_pattern(pattern, **options, **pattern_options)
298
+ end
299
+
300
+ # @!visibility private
301
+ def track_result(*results)
302
+ results.compact!
303
+ @history << results if results.any?
304
+ results.each do |result|
305
+ @params.merge! result.params
306
+ @position += result.length
307
+ end
308
+ results.last
309
+ end
310
+
311
+ private :create_pattern, :track_result, :check_until_with_prefix
312
+ end
313
+ end
@@ -0,0 +1 @@
1
+ require 'mustermann/string_scanner'
@@ -0,0 +1,18 @@
1
+ $:.unshift File.expand_path("../../mustermann/lib", __FILE__)
2
+ require "mustermann/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "mustermann-strscan"
6
+ s.version = Mustermann::VERSION
7
+ s.author = "Konstantin Haase"
8
+ s.email = "konstantin.mailinglists@googlemail.com"
9
+ s.homepage = "https://github.com/rkh/mustermann"
10
+ s.summary = %q{StringScanner for Mustermann}
11
+ s.description = %q{Implements a version of Ruby's StringScanner that works with Mustermann patterns}
12
+ s.license = 'MIT'
13
+ s.required_ruby_version = '>= 2.1.0'
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.add_dependency 'mustermann', Mustermann::VERSION
18
+ end
@@ -0,0 +1,271 @@
1
+ require 'support'
2
+ require 'mustermann/string_scanner'
3
+
4
+ describe Mustermann::StringScanner do
5
+ include Support::ScanMatcher
6
+
7
+ subject(:scanner) { Mustermann::StringScanner.new(example_string) }
8
+ let(:example_string) { "foo bar" }
9
+
10
+ describe :scan do
11
+ it { should scan("foo") }
12
+ it { should scan(/foo/) }
13
+ it { should scan(:name) }
14
+ it { should scan(":name") }
15
+
16
+ it { should_not scan(" ") }
17
+ it { should_not scan("bar") }
18
+
19
+ example do
20
+ should scan("foo")
21
+ should scan(" ")
22
+ should scan("bar")
23
+ end
24
+
25
+ example do
26
+ scanner.position = 4
27
+ should scan("bar")
28
+ end
29
+
30
+ example do
31
+ should scan("foo")
32
+ scanner.reset
33
+ should scan("foo")
34
+ end
35
+ end
36
+
37
+ describe :check do
38
+ it { should check("foo") }
39
+ it { should check(/foo/) }
40
+ it { should check(:name) }
41
+ it { should check(":name") }
42
+
43
+ it { should_not check(" ") }
44
+ it { should_not check("bar") }
45
+
46
+ example do
47
+ should check("foo")
48
+ should_not check(" ")
49
+ should_not check("bar")
50
+ should check("foo")
51
+ end
52
+
53
+ example do
54
+ scanner.position = 4
55
+ should check("bar")
56
+ end
57
+ end
58
+
59
+ describe :scan_until do
60
+ it { should scan_until("foo") }
61
+ it { should scan_until(":name") }
62
+ it { should scan_until(" ") }
63
+ it { should scan_until("bar") }
64
+ it { should_not scan_until("baz") }
65
+
66
+ example do
67
+ should scan_until(" ")
68
+ should check("bar")
69
+ end
70
+
71
+ example do
72
+ should scan_until(" ")
73
+ scanner.reset
74
+ should scan("foo")
75
+ end
76
+ end
77
+
78
+ describe :check_until do
79
+ it { should check_until("foo") }
80
+ it { should check_until(":name") }
81
+ it { should check_until(" ") }
82
+ it { should check_until("bar") }
83
+ it { should_not check_until("baz") }
84
+
85
+ example do
86
+ should check_until(" ")
87
+ should_not check("bar")
88
+ end
89
+ end
90
+
91
+ describe :getch do
92
+ example { scanner.getch.should be == "f" }
93
+
94
+ example do
95
+ scanner.scan("foo")
96
+ scanner.getch.should be == " "
97
+ should scan("bar")
98
+ end
99
+
100
+ example do
101
+ scanner.getch
102
+ scanner.reset
103
+ should scan("foo")
104
+ end
105
+ end
106
+
107
+ describe :<< do
108
+ example do
109
+ should_not scan_until("baz")
110
+ scanner << " baz"
111
+ scanner.to_s.should be == "foo bar baz"
112
+ should scan_until("baz")
113
+ end
114
+ end
115
+
116
+ describe :eos? do
117
+ it { should_not be_eos }
118
+ example do
119
+ scanner.position = 7
120
+ should be_eos
121
+ end
122
+ end
123
+
124
+ describe :beginning_of_line? do
125
+ let(:example_string) { "foo\nbar" }
126
+ it { should be_beginning_of_line }
127
+
128
+ example do
129
+ scanner.position = 2
130
+ should_not be_beginning_of_line
131
+ end
132
+
133
+ example do
134
+ scanner.position = 3
135
+ should_not be_beginning_of_line
136
+ end
137
+
138
+ example do
139
+ scanner.position = 4
140
+ should be_beginning_of_line
141
+ end
142
+ end
143
+
144
+ describe :rest do
145
+ example { scanner.rest.should be == "foo bar" }
146
+ example do
147
+ scanner.position = 4
148
+ scanner.rest.should be == "bar"
149
+ end
150
+ end
151
+
152
+ describe :rest_size do
153
+ example { scanner.rest_size.should be == 7 }
154
+ example do
155
+ scanner.position = 4
156
+ scanner.rest_size.should be == 3
157
+ end
158
+ end
159
+
160
+ describe :peek do
161
+ example { scanner.peek(3).should be == "foo" }
162
+
163
+ example do
164
+ scanner.peek(3).should be == "foo"
165
+ scanner.peek(3).should be == "foo"
166
+ end
167
+
168
+ example do
169
+ scanner.position = 4
170
+ scanner.peek(3).should be == "bar"
171
+ end
172
+ end
173
+
174
+ describe :inspect do
175
+ example { scanner.inspect.should be == '#<Mustermann::StringScanner 0/7 @ "foo bar">' }
176
+ example do
177
+ scanner.position = 4
178
+ scanner.inspect.should be == '#<Mustermann::StringScanner 4/7 @ "foo bar">'
179
+ end
180
+ end
181
+
182
+ describe :[] do
183
+ example do
184
+ should scan(:name)
185
+ scanner['name'].should be == "foo bar"
186
+ end
187
+
188
+ example do
189
+ should scan(:name, capture: /\S+/)
190
+ scanner['name'].should be == "foo"
191
+ should scan(" :name", capture: /\S+/)
192
+ scanner['name'].should be == "bar"
193
+ end
194
+
195
+ example do
196
+ should scan(":a", capture: /\S+/)
197
+ should scan(" :b", capture: /\S+/)
198
+ scanner['a'].should be == "foo"
199
+ scanner['b'].should be == "bar"
200
+ end
201
+
202
+ example do
203
+ a = scanner.scan(":a", capture: /\S+/)
204
+ b = scanner.scan(" :b", capture: /\S+/)
205
+ a.params['a'].should be == 'foo'
206
+ b.params['b'].should be == 'bar'
207
+ a.params['b'].should be_nil
208
+ b.params['a'].should be_nil
209
+ end
210
+
211
+ example do
212
+ result = scanner.check(":a", capture: /\S+/)
213
+ result.params['a'].should be == 'foo'
214
+ scanner['a'].should be_nil
215
+ end
216
+
217
+ example do
218
+ should scan(:name)
219
+ scanner.reset
220
+ scanner['name'].should be_nil
221
+ end
222
+ end
223
+
224
+ describe :unscan do
225
+ example do
226
+ should scan(:name, capture: /\S+/)
227
+ scanner['name'].should be == "foo"
228
+ should scan(" :name", capture: /\S+/)
229
+ scanner['name'].should be == "bar"
230
+ scanner.unscan
231
+ scanner['name'].should be == "foo"
232
+ scanner.rest.should be == " bar"
233
+ end
234
+
235
+ example do
236
+ should scan_until(" ")
237
+ scanner.unscan
238
+ scanner.rest.should be == "foo bar"
239
+ end
240
+
241
+ example do
242
+ expect { scanner.unscan }.to raise_error(Mustermann::StringScanner::ScanError,
243
+ 'unscan failed: previous match record not exist')
244
+ end
245
+ end
246
+
247
+ describe :terminate do
248
+ example do
249
+ scanner.terminate
250
+ scanner.should be_eos
251
+ end
252
+ end
253
+
254
+ describe :to_h do
255
+ example { scanner.to_h.should be == {} }
256
+ example do
257
+ end
258
+ end
259
+
260
+ describe :to_s do
261
+ example { scanner.to_s.should be == "foo bar" }
262
+ end
263
+
264
+ describe :clear_cache do
265
+ example do
266
+ scanner.scan("foo")
267
+ Mustermann::StringScanner.clear_cache
268
+ Mustermann::StringScanner.cache_size.should be == 0
269
+ end
270
+ end
271
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mustermann-strscan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Haase
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mustermann
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.0
27
+ description: Implements a version of Ruby's StringScanner that works with Mustermann
28
+ patterns
29
+ email: konstantin.mailinglists@googlemail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - README.md
35
+ - lib/mustermann/string_scanner.rb
36
+ - lib/mustermann/strscan.rb
37
+ - mustermann-strscan.gemspec
38
+ - spec/string_scanner_spec.rb
39
+ homepage: https://github.com/rkh/mustermann
40
+ licenses:
41
+ - MIT
42
+ metadata: {}
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 2.1.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 2.4.3
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: StringScanner for Mustermann
63
+ test_files:
64
+ - spec/string_scanner_spec.rb
65
+ has_rdoc: