impression 0.10 → 0.13

Sign up to get free protection for your applications and to get access to all the features.
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