impression 0.1 → 0.5
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 +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/test.yml +32 -0
- data/CHANGELOG.md +23 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +63 -32
- data/README.md +44 -2
- data/Rakefile +10 -0
- data/examples/markdown/_assets/style.css +98 -0
- data/examples/markdown/app.rb +11 -5
- data/examples/markdown/docs/tutorial.md +15 -0
- data/impression.gemspec +11 -7
- data/lib/impression/app.rb +9 -0
- data/lib/impression/file_tree.rb +124 -0
- data/lib/impression/file_watcher.rb +50 -0
- data/lib/impression/jamstack.rb +179 -0
- data/lib/impression/request_extensions/responses.rb +48 -0
- data/lib/impression/request_extensions/routing.rb +52 -0
- data/lib/impression/request_extensions.rb +8 -2
- data/lib/impression/resource.rb +168 -0
- data/lib/impression/version.rb +1 -1
- data/lib/impression.rb +8 -0
- data/test/helper.rb +94 -0
- data/test/jamstack/_layouts/article.rb +9 -0
- data/test/jamstack/_layouts/default.rb +12 -0
- data/test/jamstack/articles/2008-06-14-manu.md +6 -0
- data/test/jamstack/articles/2009-06-12-noatche.md +6 -0
- data/test/jamstack/articles/a.md +6 -0
- data/test/jamstack/assets/js/a.js +1 -0
- data/test/jamstack/bar.html +1 -0
- data/test/jamstack/baz/index.md +7 -0
- data/test/jamstack/foo.rb +7 -0
- data/test/jamstack/foobar.rb +10 -0
- data/test/jamstack/index.md +4 -0
- data/test/run.rb +5 -0
- data/test/static/bar/index.html +1 -0
- data/test/static/foo.html +1 -0
- data/test/static/index.html +1 -0
- data/test/static/js/a.js +1 -0
- data/test/test_app.rb +52 -0
- data/test/test_file_tree.rb +116 -0
- data/test/test_file_watcher.rb +57 -0
- data/test/test_jamstack.rb +263 -0
- data/test/test_resource.rb +201 -0
- metadata +57 -35
- data/lib/impression/pages.rb +0 -120
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'date'
|
|
6
|
+
require 'modulation'
|
|
7
|
+
require 'papercraft'
|
|
8
|
+
|
|
9
|
+
require_relative './resource'
|
|
10
|
+
require_relative './file_tree'
|
|
11
|
+
|
|
12
|
+
module Impression
|
|
13
|
+
|
|
14
|
+
# `Jamstack` implements a resource that maps to a Jamstack app directory.
|
|
15
|
+
class Jamstack < FileTree
|
|
16
|
+
def initialize(**props)
|
|
17
|
+
super
|
|
18
|
+
@layouts = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns a list of pages found in the given directory (relative to the base
|
|
22
|
+
# directory). Each entry containins the absolute file path, the pretty URL,
|
|
23
|
+
# the possible date parsed from the file name, and any other front matter
|
|
24
|
+
# attributes (for .md files). This method will detect only pages with the
|
|
25
|
+
# extensions .html, .md, .rb. The returned entries are sorted by file path.
|
|
26
|
+
#
|
|
27
|
+
# @param dir [String] relative directory
|
|
28
|
+
# @return [Array<Hash>] array of page entries
|
|
29
|
+
def page_list(dir)
|
|
30
|
+
base = File.join(@directory, dir)
|
|
31
|
+
Dir.glob('*.{html,md}', base: base)
|
|
32
|
+
.map { |fn| page_entry(fn, dir) }
|
|
33
|
+
.sort_by { |i| i[:path] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
DATE_REGEXP = /^(\d{4}\-\d{2}\-\d{2})/.freeze
|
|
39
|
+
MARKDOWN_PAGE_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m.freeze
|
|
40
|
+
MD_EXT_REGEXP = /\.md$/.freeze
|
|
41
|
+
PAGE_EXT_REGEXP = /^(.+)\.(md|html|rb)$/.freeze
|
|
42
|
+
INDEX_PAGE_REGEXP = /^(.+)\/index$/.freeze
|
|
43
|
+
|
|
44
|
+
# Returns a page entry for the given file.
|
|
45
|
+
#
|
|
46
|
+
# @param fn [String] file name
|
|
47
|
+
# @param dir [String] relative directory
|
|
48
|
+
# @return [Hash] page entry
|
|
49
|
+
def page_entry(fn, dir)
|
|
50
|
+
relative_path = File.join(dir, fn)
|
|
51
|
+
absolute_path = File.join(@directory, relative_path)
|
|
52
|
+
info = {
|
|
53
|
+
path: absolute_path,
|
|
54
|
+
url: pretty_url(relative_path)
|
|
55
|
+
}
|
|
56
|
+
if fn =~ MD_EXT_REGEXP
|
|
57
|
+
atts, _ = parse_markdown_file(absolute_path)
|
|
58
|
+
info.merge!(atts)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if (m = fn.match(DATE_REGEXP))
|
|
62
|
+
info[:date] ||= Date.parse(m[1])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
info
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the pretty URL for the given relative path. For pages, the
|
|
69
|
+
# extension is removed. For index pages, the index suffix is removed.
|
|
70
|
+
#
|
|
71
|
+
# @param relative_path [String] relative path
|
|
72
|
+
# @return [String] pretty URL
|
|
73
|
+
def pretty_url(relative_path)
|
|
74
|
+
if (m = relative_path.match(PAGE_EXT_REGEXP))
|
|
75
|
+
relative_path = m[1]
|
|
76
|
+
end
|
|
77
|
+
if (m = relative_path.match(INDEX_PAGE_REGEXP))
|
|
78
|
+
relative_path = m[1]
|
|
79
|
+
end
|
|
80
|
+
File.join(absolute_path, relative_path)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Renders a file response for the given request and the given path info.
|
|
84
|
+
#
|
|
85
|
+
# @param req [Qeweney::Request] request
|
|
86
|
+
# @param path_info [Hash] path info
|
|
87
|
+
# @return [void]
|
|
88
|
+
def render_file(req, path_info)
|
|
89
|
+
case path_info[:ext]
|
|
90
|
+
when '.rb'
|
|
91
|
+
render_papercraft_module(req, path_info[:path])
|
|
92
|
+
when '.md'
|
|
93
|
+
render_markdown_file(req, path_info[:path])
|
|
94
|
+
else
|
|
95
|
+
req.serve_file(path_info[:path])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Renders a Papercraft module. The module is loaded using Modulation.
|
|
100
|
+
#
|
|
101
|
+
# @param req [Qeweney::Request] reqest
|
|
102
|
+
# @param path [String] file path
|
|
103
|
+
# @return [void]
|
|
104
|
+
def render_papercraft_module(req, path)
|
|
105
|
+
mod = import path
|
|
106
|
+
|
|
107
|
+
html = H(mod).render(request: req, resource: self)
|
|
108
|
+
req.respond(html, 'Content-Type' => Qeweney::MimeTypes[:html])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Renders a markdown file using a layout.
|
|
112
|
+
#
|
|
113
|
+
# @param req [Qeweney::Request] reqest
|
|
114
|
+
# @param path [String] file path
|
|
115
|
+
# @return [void]
|
|
116
|
+
def render_markdown_file(req, path)
|
|
117
|
+
attributes, markdown = parse_markdown_file(path)
|
|
118
|
+
|
|
119
|
+
layout = get_layout(attributes[:layout])
|
|
120
|
+
|
|
121
|
+
html = layout.render(request: req, resource: self, **attributes) { emit_markdown markdown }
|
|
122
|
+
req.respond(html, 'Content-Type' => Qeweney::MimeTypes[:html])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Returns a layout component based on the given name. The given name
|
|
126
|
+
# defaults to 'default' if nil.
|
|
127
|
+
#
|
|
128
|
+
# @param layout [String, nil] layout name
|
|
129
|
+
# @return [Papercraft::Component] layout component
|
|
130
|
+
def get_layout(layout)
|
|
131
|
+
layout ||= 'default'
|
|
132
|
+
path = File.join(@directory, "_layouts/#{layout}.rb")
|
|
133
|
+
raise "Layout not found #{path}" unless File.file?(path)
|
|
134
|
+
|
|
135
|
+
import path
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Parses the markdown file at the given path.
|
|
139
|
+
#
|
|
140
|
+
# @param path [String] file path
|
|
141
|
+
# @return [Array] an tuple containing properties<Hash>, contents<String>
|
|
142
|
+
def parse_markdown_file(path)
|
|
143
|
+
data = IO.read(path) || ''
|
|
144
|
+
atts = {}
|
|
145
|
+
|
|
146
|
+
# Parse date from file name
|
|
147
|
+
if (m = path.match(DATE_REGEXP))
|
|
148
|
+
atts[:date] ||= Date.parse(m[1])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if (m = data.match(MARKDOWN_PAGE_REGEXP))
|
|
152
|
+
front_matter = m[1]
|
|
153
|
+
data = m.post_match
|
|
154
|
+
|
|
155
|
+
YAML.load(front_matter).each_with_object(atts) do |(k, v), h|
|
|
156
|
+
h[k.to_sym] = v
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
[atts, data]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Converts a hash with string keys to one with symbol keys.
|
|
164
|
+
#
|
|
165
|
+
# @param hash [Hash] input hash
|
|
166
|
+
# @return [Hash] output hash
|
|
167
|
+
def symbolize_keys(hash)
|
|
168
|
+
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Returns the supported path extensions used for searching for files based
|
|
172
|
+
# on pretty URLs.
|
|
173
|
+
#
|
|
174
|
+
# @return [Array] list of supported path extensions
|
|
175
|
+
def supported_path_extensions
|
|
176
|
+
[:html, :rb, :md]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Impression
|
|
6
|
+
|
|
7
|
+
module RequestExtensions
|
|
8
|
+
|
|
9
|
+
# Response extensions for `Qeweney::Request`
|
|
10
|
+
module Responses
|
|
11
|
+
|
|
12
|
+
TEXT_HEADERS = { 'Content-Type' => Qeweney::MimeTypes[:txt] }.freeze
|
|
13
|
+
HTML_HEADERS = { 'Content-Type' => Qeweney::MimeTypes[:html] }.freeze
|
|
14
|
+
JSON_HEADERS = { 'Content-Type' => Qeweney::MimeTypes[:json] }.freeze
|
|
15
|
+
|
|
16
|
+
# Send an HTTP response with plain text content. The content type is set
|
|
17
|
+
# to `text/plain`.
|
|
18
|
+
#
|
|
19
|
+
# @param text [String] response body
|
|
20
|
+
# @param **headers [Hash] additional response headers
|
|
21
|
+
def respond_text(text, **headers)
|
|
22
|
+
headers = headers.empty? ? TEXT_HEADERS : headers.merge(TEXT_HEADERS)
|
|
23
|
+
respond(text, headers)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Send an HTTP response with HTML content. The content type is set to
|
|
27
|
+
# `text/html`.
|
|
28
|
+
#
|
|
29
|
+
# @param html [String] response body
|
|
30
|
+
# @param **headers [Hash] additional response headers
|
|
31
|
+
def respond_html(html, **headers)
|
|
32
|
+
headers = headers.empty? ? HTML_HEADERS : headers.merge(HTML_HEADERS)
|
|
33
|
+
respond(html, headers)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Send an JSON response. The given object is converted to JSON. The
|
|
37
|
+
# content type is set to `application/json`.
|
|
38
|
+
#
|
|
39
|
+
# @param object [any] object to convert to JSON
|
|
40
|
+
# @param **headers [Hash] additional response headers
|
|
41
|
+
def respond_json(object, **headers)
|
|
42
|
+
headers = headers.empty? ? JSON_HEADERS : headers.merge(JSON_HEADERS)
|
|
43
|
+
respond(object.to_json, headers)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Impression
|
|
4
|
+
|
|
5
|
+
# Extensions for `Qeweney::Request`
|
|
6
|
+
module RequestExtensions
|
|
7
|
+
|
|
8
|
+
# Routing extensions for `Qeweney::Request`
|
|
9
|
+
module Routing
|
|
10
|
+
|
|
11
|
+
# Matches a route regexp against the relative request path. The relative
|
|
12
|
+
# request path is a separate string (stored in `@resource_relative_path`)
|
|
13
|
+
# that is updated as routes are matched against it. The `route_regexp`
|
|
14
|
+
# should be either `nil` for root routes (`/`) or a Regexp of the form
|
|
15
|
+
# `/^#{route}(\/.*)?$/`. See also `Resource#initialize`.
|
|
16
|
+
#
|
|
17
|
+
# @param route_regexp [Regexp, nil] Route regexp to match against
|
|
18
|
+
# @return [String, nil] The remainder of the path (relative to the route)
|
|
19
|
+
def match_resource_path?(route_regexp)
|
|
20
|
+
@resource_relative_path ||= path.dup
|
|
21
|
+
|
|
22
|
+
return @resource_relative_path unless route_regexp
|
|
23
|
+
|
|
24
|
+
# Simplified logic: no match returns nil, otherwise we set the relative path for
|
|
25
|
+
@resource_relative_path = match_resource_relative_path(
|
|
26
|
+
@resource_relative_path, route_regexp
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the relative_path for the latest matched resource
|
|
31
|
+
#
|
|
32
|
+
# @return [String]
|
|
33
|
+
def resource_relative_path
|
|
34
|
+
@resource_relative_path ||= path.dup
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Matches the given path against the given route regexp. If the path matches
|
|
40
|
+
# the regexp, the relative path for the given route is returned. Otherwise,
|
|
41
|
+
# this method returns `nil`.
|
|
42
|
+
#
|
|
43
|
+
# @param path [String] path to match
|
|
44
|
+
# @param route_regexp [Regexp] route regexp to match against
|
|
45
|
+
# @return [String, nil] the relative path for the given route, or nil if no match.
|
|
46
|
+
def match_resource_relative_path(path, route_regexp)
|
|
47
|
+
match = path.match(route_regexp)
|
|
48
|
+
match && (match[1] || '/')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require 'qeweney'
|
|
4
4
|
|
|
5
|
-
require_relative './pages'
|
|
5
|
+
# require_relative './pages'
|
|
6
|
+
require_relative './request_extensions/routing'
|
|
7
|
+
require_relative './request_extensions/responses'
|
|
6
8
|
|
|
9
|
+
# Extensions to `Qeweney::Request`
|
|
7
10
|
class Qeweney::Request
|
|
8
|
-
|
|
11
|
+
|
|
12
|
+
# include Impression::Pages::RequestMethods
|
|
13
|
+
include Impression::RequestExtensions::Routing
|
|
14
|
+
include Impression::RequestExtensions::Responses
|
|
9
15
|
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'qeweney'
|
|
5
|
+
|
|
6
|
+
module Impression
|
|
7
|
+
|
|
8
|
+
# The `Resource` class represents an abstract web resource. Resources are
|
|
9
|
+
# organized in tree like structure, with the structure normally corresponding
|
|
10
|
+
# to the URL path hierarchy. Other ways of organising resources, according to
|
|
11
|
+
# other taxonomies, can be implemented as long as the resources implement the
|
|
12
|
+
# same interface as the `Resource` class, which includes the following
|
|
13
|
+
# methods:
|
|
14
|
+
#
|
|
15
|
+
# - `Resource#route` - returns the resource which should respond to the
|
|
16
|
+
# request.
|
|
17
|
+
# - `Resource#respond` - responds to the request.
|
|
18
|
+
#
|
|
19
|
+
class Resource
|
|
20
|
+
# Reference to the parent resource
|
|
21
|
+
attr_reader :parent
|
|
22
|
+
|
|
23
|
+
# The resource's path relative to its parent
|
|
24
|
+
attr_reader :path
|
|
25
|
+
|
|
26
|
+
# A hash mapping relative paths to child resources
|
|
27
|
+
attr_reader :children
|
|
28
|
+
|
|
29
|
+
# Initalizes a new resource instance.
|
|
30
|
+
#
|
|
31
|
+
# @param parent [Impression::Resource, nil] the parent resource (or nil)
|
|
32
|
+
# @param path [String] the resource's relative path
|
|
33
|
+
# @return [void]
|
|
34
|
+
def initialize(parent: nil, path:)
|
|
35
|
+
@parent = parent
|
|
36
|
+
@path = normalize_route_path(path)
|
|
37
|
+
@route_regexp = @path == '/' ? nil : /^#{@path}(\/.*)?$/.freeze
|
|
38
|
+
@children = {}
|
|
39
|
+
|
|
40
|
+
@parent&.add_child(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the resource's absolute path, according to its location in the
|
|
44
|
+
# resource hierarchy.
|
|
45
|
+
#
|
|
46
|
+
# @return [String] absolute path
|
|
47
|
+
def absolute_path
|
|
48
|
+
@absoulte_path ||= File.join(@parent ? @parent.absolute_path : '/', @path)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Iterates over the resource and any of its sub-resources, passing each to
|
|
52
|
+
# the given block.
|
|
53
|
+
#
|
|
54
|
+
# @return [Impression::Resource] self
|
|
55
|
+
def each(&block)
|
|
56
|
+
block.(self)
|
|
57
|
+
@children.values.each { |c| c.each(&block) }
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Adds a child reference to the children map.
|
|
62
|
+
#
|
|
63
|
+
# @param child [Impression::Resource] child resource
|
|
64
|
+
# @return [Impression::Resource] self
|
|
65
|
+
def add_child(child)
|
|
66
|
+
@children[child.path] = child
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Responds to the given request by rendering a 404 Not found response.
|
|
71
|
+
#
|
|
72
|
+
# @param req [Qeweney::Request] request
|
|
73
|
+
# @return [void]
|
|
74
|
+
def call(req)
|
|
75
|
+
req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
FIRST_PATH_SEGMENT_REGEXP = /^(\/[^\/]+)\//.freeze
|
|
79
|
+
|
|
80
|
+
# Routes the request by matching self and any children against the request
|
|
81
|
+
# path, returning the target resource, or nil if there's no match.
|
|
82
|
+
#
|
|
83
|
+
# @param req [Qeweney::Request] request
|
|
84
|
+
# @return [Impression::Resource, nil] target resource
|
|
85
|
+
def route(req)
|
|
86
|
+
case (relative_path = req.match_resource_path?(@route_regexp))
|
|
87
|
+
when nil
|
|
88
|
+
return nil
|
|
89
|
+
when '/'
|
|
90
|
+
return self
|
|
91
|
+
else
|
|
92
|
+
# naive case
|
|
93
|
+
child = @children[relative_path]
|
|
94
|
+
return child.route(req) if child
|
|
95
|
+
|
|
96
|
+
if (m = relative_path.match(FIRST_PATH_SEGMENT_REGEXP))
|
|
97
|
+
child = @children[m[1]]
|
|
98
|
+
return child.route(req) if child
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
return self
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Renders the resource and all of its sub-resources to static files.
|
|
106
|
+
#
|
|
107
|
+
# @param base_path [String] base path of target directory
|
|
108
|
+
# @return [Impression::Resource] self
|
|
109
|
+
def render_tree_to_static_files(base_path)
|
|
110
|
+
each do |r|
|
|
111
|
+
path = File.join(base_path, r.relative_static_file_path)
|
|
112
|
+
dir = File.dirname(path)
|
|
113
|
+
FileUtils.mkdir_p(dir) if !File.directory?(dir)
|
|
114
|
+
File.open(path, 'w') { |f| r.render_to_file(f) }
|
|
115
|
+
end
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Converts the resource to a Proc, for use as a Qeweney app.
|
|
120
|
+
#
|
|
121
|
+
# @return [Proc] web app proc
|
|
122
|
+
def to_proc
|
|
123
|
+
->(req) do
|
|
124
|
+
resource = route(req) || self
|
|
125
|
+
resource.call(req)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns a callable that responds with plain text using the given
|
|
130
|
+
# parameters.
|
|
131
|
+
#
|
|
132
|
+
# @param text [String] response body
|
|
133
|
+
# @param **headers [Hash] additional response headers
|
|
134
|
+
def text_response(text, **headers)
|
|
135
|
+
->(req) { req.respond_text(text, **headers) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns a callable that responds with HTML using the given
|
|
139
|
+
# parameters.
|
|
140
|
+
#
|
|
141
|
+
# @param html [String] response body
|
|
142
|
+
# @param **headers [Hash] additional response headers
|
|
143
|
+
def html_response(html, **headers)
|
|
144
|
+
->(req) { req.respond_html(html, **headers) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Returns a callable that responds with JSON using the given
|
|
148
|
+
# parameters.
|
|
149
|
+
#
|
|
150
|
+
# @param object [any] object to be converted to JSON
|
|
151
|
+
# @param **headers [Hash] additional response headers
|
|
152
|
+
def json_response(object, **headers)
|
|
153
|
+
->(req) { req.respond_json(object, **headers) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
SLASH_PREFIXED_PATH_REGEXP = /^\//.freeze
|
|
159
|
+
|
|
160
|
+
# Normalizes the given path by ensuring it starts with a slash.
|
|
161
|
+
#
|
|
162
|
+
# @param path [String] path to normalize
|
|
163
|
+
# @return [String] normalized path
|
|
164
|
+
def normalize_route_path(path)
|
|
165
|
+
path =~ SLASH_PREFIXED_PATH_REGEXP ? path : "/#{path}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
data/lib/impression/version.rb
CHANGED
data/lib/impression.rb
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'polyphony'
|
|
4
|
+
|
|
3
5
|
require_relative './impression/request_extensions'
|
|
6
|
+
# require_relative './impression/file_watcher'
|
|
7
|
+
|
|
8
|
+
require_relative './impression/resource'
|
|
9
|
+
require_relative './impression/file_tree'
|
|
10
|
+
require_relative './impression/jamstack'
|
|
11
|
+
require_relative './impression/app'
|
data/test/helper.rb
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require_relative './coverage' if ENV['COVERAGE']
|
|
5
|
+
require 'minitest/autorun'
|
|
6
|
+
require 'impression'
|
|
7
|
+
require 'qeweney/test_adapter'
|
|
8
|
+
|
|
9
|
+
module Kernel
|
|
10
|
+
def mock_req(**args)
|
|
11
|
+
Qeweney::TestAdapter.mock(**args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def capture_exception
|
|
15
|
+
yield
|
|
16
|
+
rescue Exception => e
|
|
17
|
+
e
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def trace(*args)
|
|
21
|
+
STDOUT.orig_write(format_trace(args))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def format_trace(args)
|
|
25
|
+
if args.first.is_a?(String)
|
|
26
|
+
if args.size > 1
|
|
27
|
+
format("%s: %p\n", args.shift, args)
|
|
28
|
+
else
|
|
29
|
+
format("%s\n", args.first)
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
format("%p\n", args.size == 1 ? args.first : args)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module Minitest::Assertions
|
|
38
|
+
def assert_in_range exp_range, act
|
|
39
|
+
msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
|
|
40
|
+
assert exp_range.include?(act), msg
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def assert_response exp_body, exp_content_type, req
|
|
44
|
+
actual = req.response_body
|
|
45
|
+
assert_equal exp_body.gsub("\n", ''), actual&.gsub("\n", '')
|
|
46
|
+
|
|
47
|
+
return unless exp_content_type
|
|
48
|
+
|
|
49
|
+
if Symbol === exp_content_type
|
|
50
|
+
exp_content_type = Qeweney::MimeTypes[exp_content_type]
|
|
51
|
+
end
|
|
52
|
+
actual = req.response_content_type
|
|
53
|
+
assert_equal exp_content_type, actual
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Impression::Resource
|
|
58
|
+
def route_and_call(req)
|
|
59
|
+
route(req).call(req)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class PathRenderingResource < Impression::Resource
|
|
64
|
+
def call(req)
|
|
65
|
+
req.respond(absolute_path)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class CompletePathInfoRenderingResource < Impression::Resource
|
|
70
|
+
def call(req)
|
|
71
|
+
req.respond("#{absolute_path} #{req.resource_relative_path}")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Extensions to be used in conjunction with `Qeweney::TestAdapter`
|
|
76
|
+
class Qeweney::Request
|
|
77
|
+
def response_headers
|
|
78
|
+
adapter.headers
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def response_body
|
|
82
|
+
adapter.body
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def response_status
|
|
86
|
+
adapter.status
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def response_content_type
|
|
90
|
+
response_headers['Content-Type']
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
puts "Polyphony backend: #{Thread.current.backend.kind}"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log(42);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>Bar</h1>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
layout = import('./_layouts/default')
|
|
4
|
+
|
|
5
|
+
export_default layout.apply(title: 'Foobar') { |resource:, request:, **props|
|
|
6
|
+
h1 request.query[:q]
|
|
7
|
+
resource.page_list('/articles').each do |i|
|
|
8
|
+
a i[:title], href: i[:url]
|
|
9
|
+
end
|
|
10
|
+
}
|
data/test/run.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>BarIndex</h1>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>Foo</h1>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>Index</h1>
|
data/test/static/js/a.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log(42);
|