mustermann-strscan 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.
@@ -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: