impression 0.10 → 0.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 718a8a8eba181a00e5519e970d34bfc2e666ea15bacf0e72bffba450f3c9788d
4
- data.tar.gz: fd665341a6fdcf66af4c564a269022f5fce014243a8736cf228188f9b3ca3e1f
3
+ metadata.gz: 7dff68cd22a426ae305742591ca09a3aa737cf453af1f186a54170671170f250
4
+ data.tar.gz: 920c5ca0698212f318dcc967cc09405e0b2f25e8d8521580c2fce331a4bfe982
5
5
  SHA512:
6
- metadata.gz: 32ce3395a8f74e463b935e48711ae2b3fccd756b1bd3ec11912539ed2d969e0691dc044933548c5321a89749ad4faf3ddfc546958a83716d339b6fbeea0739b8
7
- data.tar.gz: c33b519b8fd5a2a5da76e0d1a4a78854a0178dee62d90b143bf7cd8358150e69127af9d42832cb6158b41835cf6fa08cb6fc3efc09d0caa437e03b1e43722cde
6
+ metadata.gz: 98d8b4da9955eb4ac433cb74c45b9edeae759850e3a2b28cb4933f4445d554c25a360f968323f1a7a6db0bd9e748a2570048efc698a88803ac86920d77452757
7
+ data.tar.gz: e9e91a9fb0e9754fef82946f6a26368e53eb6c131d1e5d2ac0fe6e46274b75787cd2c47ee352a4dd015fd402ee2895fbfef2be1b7b01d84b8e3327ff91d18010
@@ -16,7 +16,7 @@ jobs:
16
16
  runs-on: ${{matrix.os}}
17
17
 
18
18
  env:
19
- POLYPHONY_USE_LIBEV: "1"
19
+ POLYPHONY_LIBEV: "1"
20
20
 
21
21
  steps:
22
22
  - name: Setup machine
@@ -25,8 +25,7 @@ jobs:
25
25
  uses: ruby/setup-ruby@v1
26
26
  with:
27
27
  ruby-version: ${{matrix.ruby}}
28
- bundler-cache: true # 'bundle install' and cache
29
- cache-version: 1
30
-
28
+ bundler-cache: true
29
+ cache-version: 4
31
30
  - name: Run tests
32
31
  run: bundle exec rake test
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.13 2022-04-05
2
+
3
+ - Prevent race condiution on loading Ruby modules
4
+
5
+ ## 0.12 2022-02-16
6
+
7
+ - Implement `RackApp` resource class (#16)
8
+
9
+ ## 0.11 2022-02-10
10
+
11
+ - Update Tipi
12
+
1
13
  ## 0.10 2022-02-10
2
14
 
3
15
  - Add support for resource modules in Jamstack (#13)
data/Gemfile CHANGED
@@ -1,8 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
-
5
- # %w{polyphony tipi qeweney papercraft}.each do |dep|
6
- # dir = "../#{dep}"
7
- # gem(dep, path: dir) if File.directory?(dir)
8
- # end
data/Gemfile.lock CHANGED
@@ -1,12 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- impression (0.10)
4
+ impression (0.13)
5
5
  modulation (~> 1.1)
6
- papercraft (~> 0.19)
7
- polyphony (~> 0.77)
8
- qeweney (~> 0.17)
9
- tipi (~> 0.49)
6
+ papercraft (~> 0.23)
7
+ polyphony (~> 0.93)
8
+ qeweney (~> 0.18)
9
+ tipi (~> 0.52)
10
10
 
11
11
  GEM
12
12
  remote: https://rubygems.org/
@@ -16,8 +16,8 @@ GEM
16
16
  docile (1.4.0)
17
17
  escape_utils (1.2.1)
18
18
  ever (0.1)
19
- extralite (1.11)
20
- faraday (1.9.3)
19
+ extralite (1.14)
20
+ faraday (1.10.0)
21
21
  faraday-em_http (~> 1.0)
22
22
  faraday-em_synchrony (~> 1.0)
23
23
  faraday-excon (~> 1.1)
@@ -40,25 +40,25 @@ GEM
40
40
  faraday-patron (1.0.0)
41
41
  faraday-rack (1.0.0)
42
42
  faraday-retry (1.0.3)
43
- h1p (0.3)
43
+ h1p (0.5)
44
44
  http-2 (0.11.0)
45
45
  json (2.6.1)
46
- kramdown (2.3.1)
46
+ kramdown (2.3.2)
47
47
  rexml
48
48
  kramdown-parser-gfm (1.1.0)
49
49
  kramdown (~> 2.0)
50
50
  localhost (1.1.9)
51
51
  minitest (5.11.3)
52
52
  modulation (1.1)
53
- msgpack (1.4.4)
53
+ msgpack (1.4.5)
54
54
  multipart-post (2.1.1)
55
- papercraft (0.19)
55
+ papercraft (0.24)
56
56
  escape_utils (~> 1.2.1)
57
57
  kramdown (~> 2.3.1)
58
58
  kramdown-parser-gfm (~> 1.1.0)
59
59
  rouge (~> 3.27.0)
60
- polyphony (0.77)
61
- qeweney (0.17)
60
+ polyphony (0.93)
61
+ qeweney (0.18)
62
62
  escape_utils (~> 1.2.1)
63
63
  rack (2.2.3)
64
64
  rake (12.3.3)
@@ -70,16 +70,16 @@ GEM
70
70
  json (>= 1.8, < 3)
71
71
  simplecov-html (~> 0.10.0)
72
72
  simplecov-html (0.10.2)
73
- tipi (0.49)
73
+ tipi (0.52)
74
74
  acme-client (~> 2.0.9)
75
75
  ever (~> 0.1)
76
- extralite (~> 1.2)
77
- h1p (~> 0.3)
76
+ extralite (~> 1.14)
77
+ h1p (~> 0.4)
78
78
  http-2 (~> 0.11)
79
79
  localhost (~> 1.1.4)
80
80
  msgpack (~> 1.4.2)
81
- polyphony (~> 0.77)
82
- qeweney (~> 0.16)
81
+ polyphony (~> 0.80)
82
+ qeweney (~> 0.18)
83
83
  rack (>= 2.0.8, < 2.3.0)
84
84
  websocket (~> 1.2.8)
85
85
  websocket (1.2.9)
data/README.md CHANGED
@@ -19,20 +19,90 @@
19
19
  ## What is Impression
20
20
 
21
21
  > Impression is still in a very early stage of development. Things might not
22
- > correctly, or not at all.
22
+ > work correctly.
23
23
 
24
24
  Impression is a modern web framework for Ruby. Unlike other web framework,
25
25
  Impression does not impose any rigid structure or paradigm, but instead provides
26
- a set of tools letting you build any kind of web app, by freely mixing different
27
- kinds of web resources, be they static files, structured templates, Jamstack
28
- sites, or dynamic APIs.
26
+ a minimalistic set of tools, letting you build any kind of web app, by freely
27
+ mixing different kinds of web resources, be they static files, structured
28
+ templates, Jamstack sites, or dynamic APIs.
29
29
 
30
- In Impression, resources can be composed into a hierarchical tree, allowing
31
- combining of multiple web apps, or web subsystems, into a single URL hierarchy.
30
+ ## Resources
31
+
32
+ The main abstraction in Impression is the resource - which represents an web
33
+ endpoint that is mounted at a specific location in the URL namespace, and
34
+ responds to requests. Resources can be nested in order to create arbitrarily
35
+ complex routing trees. Impression provides multiple resource types, each
36
+ customized for a specific use case, be it a JSON API, a set of MVC-style
37
+ controllers, or a Markdown-based blog with static content.
38
+
39
+ Finally, any kind of resource can be used as an Impression app. Routing is
40
+ performed automatically according to the resource tree, starting from the root
41
+ resource.
42
+
43
+ ## The request-response cycle
44
+
45
+ The handling of incoming HTTP requests is done in two stages. First the request
46
+ is routed to the corresponding resource, which then handles the request by
47
+ generating a response.
48
+
49
+ HTTP requests and responses use the
50
+ [Qeweney](https://github.com/digital-fabric/qeweney) API.
51
+
52
+ ## Resource types
53
+
54
+ Impression provides the following resources:
55
+
56
+ - `Resource` - a generic resource.
57
+ - `FileTree` - a resource serving static files from the given directory.
58
+ - `App` - a resource serving static files, markdown files with layouts and Ruby
59
+ modules from the given directory.
60
+ - `RackApp` - a resource serving the given Rack app.
61
+
62
+ ## Setting up a basic resource
63
+
64
+ To setup a generic resource, call `Impression.resource` and provide a request
65
+ handler:
66
+
67
+ ```
68
+ app = Impression.resource { |req| req.respond('Hello, world!') }
69
+ ```
70
+
71
+ ## Running your app with Tipi
32
72
 
33
73
  Impression is made for running on top of
34
- [Tipi](https://github.com/digital-fabric/tipi), a new all-in-one web server for
35
- Ruby.
74
+ [Tipi](https://github.com/digital-fabric/tipi). Your Tipi app file would like
75
+ something like the following:
76
+
77
+ ```ruby
78
+ # app.rb
79
+ app = Impression.resource { |req| req.respond('Hello, world!') }
80
+ Tipi.run(&app)
81
+ ```
82
+
83
+ You can then start Tipi by running `tipi run app.rb`.
84
+
85
+ ## Running your app with a Rack app server
86
+
87
+ You can also run your app on any Rack app server, using something like the
88
+ following:
89
+
90
+ ```ruby
91
+ app = Impression.resource { |req| req.respond('Hello, world!') }
92
+ run Qeweney.rack(&app)
93
+ ```
94
+
95
+ ## Creating a routing map with resources
96
+
97
+ A resource can be mounted at any point in the app's URL space. Resources can be
98
+ nested within other resources by passing a `parent:` argument when creating a
99
+ resource:
100
+
101
+ ```ruby
102
+ app = Impression.app { |req| req.respond('Homepage') }
103
+ greeter = Impression.resource(parent: app, path: 'greeter')
104
+ static = Impression.file_tree(parent: app, path: 'static', directory: __dir__)
105
+ ```
36
106
 
37
107
  ## I want to know more
38
108
 
data/impression.gemspec CHANGED
@@ -20,11 +20,11 @@ Gem::Specification.new do |s|
20
20
  s.require_paths = ["lib"]
21
21
  s.required_ruby_version = '>= 2.6'
22
22
 
23
- s.add_runtime_dependency 'polyphony', '~>0.77'
24
- s.add_runtime_dependency 'tipi', '~>0.49'
25
- s.add_runtime_dependency 'qeweney', '~>0.17'
23
+ s.add_runtime_dependency 'polyphony', '~>0.93'
24
+ s.add_runtime_dependency 'tipi', '~>0.52'
25
+ s.add_runtime_dependency 'qeweney', '~>0.18'
26
26
 
27
- s.add_runtime_dependency 'papercraft', '~>0.19'
27
+ s.add_runtime_dependency 'papercraft', '~>0.23'
28
28
  s.add_runtime_dependency 'modulation', '~>1.1'
29
29
 
30
30
 
@@ -1,9 +1,330 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
4
+ require 'date'
5
+ require 'yaml'
6
+ require 'modulation'
7
+ require 'papercraft'
8
+
9
+ require_relative './resource'
10
+ require_relative './file_tree'
11
+
3
12
  module Impression
4
- class App < Resource
5
- def initialize(path: '/', **opts)
6
- super(path: path, **opts)
13
+
14
+ # `App` implements a resource that maps to a generic app directory.
15
+ class App < FileTree
16
+ def initialize(**props)
17
+ super
18
+ @layouts = {}
19
+ @file_info_loader = spin { run_file_info_loader }
20
+ end
21
+
22
+ # Returns a list of pages found in the given directory (relative to the base
23
+ # directory). Each entry containins the absolute file path, the pretty URL,
24
+ # the possible date parsed from the file name, and any other front matter
25
+ # attributes (for .md files). This method will detect only pages with the
26
+ # extensions .html, .md, .rb. The returned entries are sorted by file path.
27
+ #
28
+ # @param dir [String] relative directory
29
+ # @return [Array<Hash>] array of page entries
30
+ def page_list(dir)
31
+ base = File.join(@directory, dir)
32
+ Dir.glob('*.{html,md}', base: base)
33
+ .map { |fn| get_path_info(File.join(dir, fn)) }# page_entry(fn, dir) }
34
+ .sort_by { |i| i[:path] }
35
+ end
36
+
37
+ private
38
+
39
+ DATE_REGEXP = /(\d{4}\-\d{2}\-\d{2})/.freeze
40
+ FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m.freeze
41
+ MD_EXT_REGEXP = /\.md$/.freeze
42
+ PAGE_EXT_REGEXP = /^(.+)\.(md|html|rb)$/.freeze
43
+ INDEX_PAGE_REGEXP = /^(.+)?\/index$/.freeze
44
+
45
+ YAML_OPTS = {
46
+ permitted_classes: [Date],
47
+ symbolize_names: true
48
+ }.freeze
49
+
50
+ # Runs a file info loader handling incoming requests for file info. This
51
+ # method is run in a fiber setup in #initialize.
52
+ #
53
+ # @return [void]
54
+ def run_file_info_loader
55
+ loop do
56
+ peer, path = receive
57
+ begin
58
+ info = calculate_path_info(path)
59
+ peer << info
60
+ rescue Polyphony::BaseException
61
+ raise
62
+ rescue => e
63
+ peer.raise(e)
64
+ end
65
+ end
66
+ end
67
+
68
+ def safe_calculate_path_info(path)
69
+ @file_info_loader << [Fiber.current, path]
70
+ receive
71
+ end
72
+
73
+ # Returns the path info for the given relative path.
74
+ #
75
+ # @param path [String] relative path
76
+ # @return [Hash] path info
77
+ def get_path_info(path)
78
+ @path_info_cache[path] ||= safe_calculate_path_info(path)
79
+ end
80
+
81
+ # Returns complete file info for Markdown files
82
+ #
83
+ # @param info [Hash] file info
84
+ # @param path [String] file path
85
+ # @return [Hash] file info
86
+ def file_info_md(info, path)
87
+ atts, content = parse_markdown_file(path)
88
+ info = info.merge(atts)
89
+ info[:html_content] = Papercraft.markdown(content)
90
+ info[:kind] = :markdown
91
+ if !info[:date] && (m = path.match(DATE_REGEXP))
92
+ info[:date] = Date.parse(m[1])
93
+ end
94
+ info
95
+ end
96
+
97
+ # Returns complete file info for Ruby files
98
+ #
99
+ # @param info [Hash] file info
100
+ # @param path [String] file path
101
+ # @return [Hash] file info
102
+ def file_info_rb(info, path)
103
+ info.merge(
104
+ kind: :module,
105
+ module: import(path)
106
+ )
107
+ end
108
+
109
+ # Returns the path info for the given file path.
110
+ #
111
+ # @param path [String] file path
112
+ # @return [Hash] path info
113
+ def file_info(path)
114
+ info = super
115
+ case info[:ext]
116
+ when '.md'
117
+ file_info_md(info, path)
118
+ when '.rb'
119
+ file_info_rb(info, path)
120
+ else
121
+ info
122
+ end
123
+ end
124
+
125
+ # Returns the pretty URL for the given relative path. For pages, the
126
+ # extension is removed. For index pages, the index suffix is removed.
127
+ #
128
+ # @param relative_path [String] relative path
129
+ # @return [String] pretty URL
130
+ def pretty_url(relative_path)
131
+ if (m = relative_path.match(PAGE_EXT_REGEXP))
132
+ relative_path = m[1]
133
+ end
134
+ if (m = relative_path.match(INDEX_PAGE_REGEXP))
135
+ relative_path = m[1] || '/'
136
+ end
137
+ relative_path == '/' ? absolute_path : File.join(absolute_path, relative_path)
138
+ end
139
+
140
+ # Renders a response according to the given path info.
141
+ #
142
+ # @param req [Qeweney::Request] request
143
+ # @param path_info [Hash] path info
144
+ # @return [void]
145
+ def render_from_path_info(req, path_info)
146
+ case (kind = path_info[:kind])
147
+ when :not_found
148
+ mod_path_info = up_tree_resource_module_path_info(req, path_info)
149
+ if mod_path_info
150
+ render_module(req, mod_path_info)
151
+ else
152
+ req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
153
+ end
154
+ when :module
155
+ render_module(req, path_info)
156
+ when :markdown
157
+ render_markdown_file(req, path_info)
158
+ when :file
159
+ render_file(req, path_info)
160
+ else
161
+ raise "Invalid path info kind #{kind.inspect}"
162
+ end
163
+ end
164
+
165
+ # Returns the path info for an up-tree resource module, or false if not
166
+ # found. the :up_tree_resource_module_path_info KV can be either:
167
+ # - nil (default): up tree module search has not been performed.
168
+ # - false: no up tree module was found.
169
+ # - module path info: up tree module info (subsequent requests will be
170
+ # directly routed to the module).
171
+ #
172
+ # @param req [Qeweney::Request] request
173
+ # @param path_info [Hash] path info
174
+ # @return [Hash, false] up-tree resource module path info
175
+ def up_tree_resource_module_path_info(req, path_info)
176
+ if path_info[:up_tree_resource_module_path_info].nil?
177
+ if (mod_path_info = find_up_tree_resource_module(req, path_info))
178
+ path_info[:up_tree_resource_module_path_info] = mod_path_info
179
+ return mod_path_info;
180
+ else
181
+ path_info[:up_tree_resource_module_path_info] = false
182
+ return false
183
+ end
184
+ end
185
+ path_info[:up_tree_resource_module_path_info]
186
+ end
187
+
188
+ # Performs a recursive search for an up-tree resource module from the given
189
+ # path info. If a resource module is found up the tree, its path_info is
190
+ # returned, otherwise returns nil.
191
+ #
192
+ # @param req [Qeweney::Request] request
193
+ # @param path_info [Hash] path info
194
+ # @return [Hash, nil] up-tree resource module path info
195
+ def find_up_tree_resource_module(req, path_info)
196
+ relative_path = req.resource_relative_path
197
+
198
+ while relative_path != path
199
+ up_tree_path = File.expand_path('..', relative_path)
200
+ return nil if up_tree_path == relative_path
201
+
202
+ up_tree_path_info = get_path_info(up_tree_path)
203
+ case up_tree_path_info[:kind]
204
+ when :not_found
205
+ relative_path = up_tree_path
206
+ next
207
+ when :module
208
+ return up_tree_path_info
209
+ else
210
+ return nil
211
+ end
212
+ end
213
+ nil
214
+ end
215
+
216
+ # Renders a file response for the given request and the given path info,
217
+ # according to the file type.
218
+ #
219
+ # @param req [Qeweney::Request] request
220
+ # @param path_info [Hash] path info
221
+ # @return [void]
222
+ # def render_file(req, path_info)
223
+ # case path_info[:kind]
224
+ # else
225
+ # req.serve_file(path_info[:path])
226
+ # end
227
+ # end
228
+
229
+ # Renders a module. If the module is a Resource, it is mounted, and then the
230
+ # request is rerouted from the new resource and rendered. If the module is a
231
+ # Proc or a Papercraft::Template, it is rendered as such. Otherwise, an
232
+ # error is raised.
233
+ #
234
+ # @param req [Qeweney::Request] request
235
+ # @param path_info [Hash] path info
236
+ # @return [void]
237
+ def render_module(req, path_info)
238
+ # p render_module: path_info
239
+ case (mod = path_info[:module])
240
+ when Module
241
+ resource = mod.resource
242
+ resource.remount(self, path_info[:url])
243
+ # p path_info_url: path_info[:url], relative_path: req.resource_relative_path
244
+ relative_url = path_info[:url].gsub(/^#{path}/, '')
245
+ # p relative_url: relative_url
246
+ req.recalc_resource_relative_path(relative_url)
247
+ # p resource_relative_path: req.resource_relative_path
248
+ resource.route(req).call(req)
249
+ when Impression::Resource
250
+ mod.remount(self, path_info[:url])
251
+ req.recalc_resource_relative_path(path_info[:url])
252
+ mod.route(req).call(req)
253
+ when Proc, Papercraft::Template
254
+ render_papercraft_module(req, mod)
255
+ else
256
+ raise "Unsupported module type #{mod.class}"
257
+ end
258
+ end
259
+
260
+ # Renders a Papercraft module.
261
+ #
262
+ # @param mod [Module] Papercraft module
263
+ # @param path_info [Hash] path info
264
+ # @return [void]
265
+ def render_papercraft_module(req, mod)
266
+ template = Papercraft.html(mod)
267
+ body = template.render(request: req, resource: self)
268
+ req.respond(body, 'Content-Type' => template.mime_type)
269
+ end
270
+
271
+ # Renders a markdown file using a layout.
272
+ #
273
+ # @param req [Qeweney::Request] reqest
274
+ # @param path_info [Hash] path info
275
+ # @return [void]
276
+ def render_markdown_file(req, path_info)
277
+ layout = get_layout(path_info[:layout])
278
+
279
+ html = layout.render(request: req, resource: self, **path_info) {
280
+ emit path_info[:html_content]
281
+ }
282
+ req.respond(html, 'Content-Type' => layout.mime_type)
283
+ end
284
+
285
+ # Returns a layout component based on the given name. The given name
286
+ # defaults to 'default' if nil.
287
+ #
288
+ # @param layout [String, nil] layout name
289
+ # @return [Papercraft::Template] layout component
290
+ def get_layout(layout)
291
+ layout ||= 'default'
292
+ path = File.join(@directory, "_layouts/#{layout}.rb")
293
+ raise "Layout not found #{path}" unless File.file?(path)
294
+
295
+ import path
296
+ end
297
+
298
+ # Parses the markdown file at the given path.
299
+ #
300
+ # @param path [String] file path
301
+ # @return [Array] an tuple containing properties<Hash>, contents<String>
302
+ def parse_markdown_file(path)
303
+ content = IO.read(path) || ''
304
+ atts = {}
305
+
306
+ # Parse date from file name
307
+ if (m = path.match(DATE_REGEXP))
308
+ atts[:date] ||= Date.parse(m[1])
309
+ end
310
+
311
+ if (m = content.match(FRONT_MATTER_REGEXP))
312
+ front_matter = m[1]
313
+ content = m.post_match
314
+
315
+ yaml = YAML.safe_load(front_matter, **YAML_OPTS)
316
+ atts = atts.merge(yaml)
317
+ end
318
+
319
+ [atts, content]
320
+ end
321
+
322
+ # Returns the supported path extensions used for searching for files based
323
+ # on pretty URLs.
324
+ #
325
+ # @return [Array] list of supported path extensions
326
+ def supported_path_extensions
327
+ [:html, :rb, :md]
7
328
  end
8
329
  end
9
330
  end
@@ -14,8 +14,8 @@ module Impression
14
14
  #
15
15
  # @param directory [String] static directory path
16
16
  # @return [void]
17
- def initialize(directory:, **props)
18
- super(**props)
17
+ def initialize(directory: nil, **props, &block)
18
+ super(**props, &block)
19
19
  @directory = directory
20
20
  @path_info_cache = {}
21
21
  end
@@ -25,6 +25,8 @@ module Impression
25
25
  # @param req [Qeweney::Request] request
26
26
  # @return [void]
27
27
  def call(req)
28
+ return super if @directory.nil?
29
+
28
30
  path_info = get_path_info(req.resource_relative_path)
29
31
  render_from_path_info(req, path_info)
30
32
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tipi'
5
+ require_relative './resource'
6
+
7
+ module Impression
8
+
9
+ # The `RackApp` class represents Rack apps as resources.
10
+ class RackApp < Resource
11
+ def initialize(app: nil, **props, &block)
12
+ raise "No Rack app given" unless app || block
13
+
14
+ # We pass nil as the block, otherwise the block will pass to
15
+ # Resource#initialize, which will cause #call to be overidden.
16
+ super(**props, &nil)
17
+ @handler = Tipi::RackAdapter.run(app || block)
18
+ end
19
+
20
+ def call(req)
21
+ if @path != '/'
22
+ req.rewrite!(@path, '/')
23
+ end
24
+ @handler.(req)
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Impression
4
- VERSION = '0.10'
4
+ VERSION = '0.13'
5
5
  end