impression 0.2 → 0.3

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: aaea11fcf3fcaab1902290a620d9cdb0fa5dfb11ae38c90335f80471629d652f
4
- data.tar.gz: 9eaebd5c2a28856a568e583399c3ea6db8332e6c2aae6d570639505788203dc1
3
+ metadata.gz: 5644ebb122c166a8a5579a950b3e64fde5d3d332eea8299aa4c4023af8b85cb6
4
+ data.tar.gz: b89065d1cd8c14d5ddad28f1fc3ff90cc48d43354698e339715f3b6d172af671
5
5
  SHA512:
6
- metadata.gz: 145722a3828e039ed1a021c002cc0a551e66f78a300d869203f15d8c3df8c2d532d71d37674a7c11b1d995c39cb138a06040665a785e22581473843d28581a49
7
- data.tar.gz: 9d179f895f2d819f1c63762099137dda5511614176c9e25df4aef7d735975fbda25167b5f3c3a94788f96c8b37ece7c2f42acc7b54dcaa38054863976f039ccf
6
+ metadata.gz: 45ab7c1f4e3c9f920175acc8bce2417932571ceca8946cf1fd0b784a6280413514587bc998d467808023c56e054a0edc2c821e6dc700289a4a02f4e2372dfe05
7
+ data.tar.gz: d3b57e4089e0473e508f5a67c52db12be7a62850897bc79a27871b4ea7a3d2b3b3062f214980a98d5e842033b62496ae9024416c37b00ca05b527b01c088b53e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # 0.3 2022-01-19
2
+
3
+ - Implement basic Jamstack resource (#5)
4
+ - Add support for stock callable responses (#4)
5
+ - Make resource callable, rename `#respond` to `#callable` (#3)
6
+
1
7
  ## 0.2 2022-01-13
2
8
 
3
9
  - Implement `FileTree` (#2)
data/Gemfile CHANGED
@@ -2,7 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- # %w{polyphony tipi qeweney rubyoshka}.each do |dep|
5
+ # %w{polyphony tipi qeweney papercraft}.each do |dep|
6
6
  # dir = "../#{dep}"
7
7
  # gem(dep, path: dir) if File.directory?(dir)
8
8
  # end
data/Gemfile.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- impression (0.2)
4
+ impression (0.3)
5
5
  kramdown (~> 2.3.0)
6
6
  kramdown-parser-gfm (~> 1.1.0)
7
- papercraft (~> 0.12)
7
+ modulation (~> 1.1)
8
+ papercraft (~> 0.14)
8
9
  polyphony (~> 0.73.1)
9
10
  qeweney (~> 0.15)
10
11
  rouge (~> 3.26.0)
@@ -51,9 +52,10 @@ GEM
51
52
  kramdown (~> 2.0)
52
53
  localhost (1.1.9)
53
54
  minitest (5.11.3)
55
+ modulation (1.1)
54
56
  msgpack (1.4.2)
55
57
  multipart-post (2.1.1)
56
- papercraft (0.12)
58
+ papercraft (0.14)
57
59
  escape_utils (= 1.2.1)
58
60
  kramdown (~> 2.3.0)
59
61
  kramdown-parser-gfm (~> 1.1.0)
data/README.md CHANGED
@@ -1,2 +1,44 @@
1
- # impression
2
- A modern web framework for Ruby
1
+ <h1 align="center">
2
+ Impression
3
+ </h1>
4
+
5
+ <h4 align="center">A modern web framework for Ruby</h4>
6
+
7
+ <p align="center">
8
+ <a href="http://rubygems.org/gems/impression">
9
+ <img src="https://badge.fury.io/rb/impression.svg" alt="Ruby gem">
10
+ </a>
11
+ <a href="https://github.com/digital-fabric/impression/actions?query=workflow%3ATests">
12
+ <img src="https://github.com/digital-fabric/impression/workflows/Tests/badge.svg" alt="Tests">
13
+ </a>
14
+ <a href="https://github.com/digital-fabric/impression/blob/master/LICENSE">
15
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
16
+ </a>
17
+ </p>
18
+
19
+ ## What is Impression
20
+
21
+ > Impression is still in a very early stage of development. Things might not
22
+ > correctly, or not at all.
23
+
24
+ Impression is a modern web framework for Ruby. Unlike other web framework,
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.
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.
32
+
33
+ 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.
36
+
37
+ ## I want to know more
38
+
39
+ Further documentation is coming real soon (TM)...
40
+
41
+ ## Contributing
42
+
43
+ Contributions in the form of issues, PRs or comments will be greatly
44
+ appreciated!
data/impression.gemspec CHANGED
@@ -28,7 +28,8 @@ Gem::Specification.new do |s|
28
28
  s.add_runtime_dependency 'kramdown-parser-gfm', '~>1.1.0'
29
29
 
30
30
  # s.add_runtime_dependency 'rb-inotify', '~>0.10.1'
31
- s.add_runtime_dependency 'papercraft', '~>0.12'
31
+ s.add_runtime_dependency 'papercraft', '~>0.14'
32
+ s.add_runtime_dependency 'modulation', '~>1.1'
32
33
 
33
34
 
34
35
  s.add_development_dependency 'rake', '~>12.3.3'
@@ -22,9 +22,9 @@ module Impression
22
22
  #
23
23
  # @param req [Qeweney::Request] request
24
24
  # @return [void]
25
- def respond(req)
25
+ def call(req)
26
26
  path_info = get_path_info(req.resource_relative_path)
27
- render_from_path_info(req, *path_info)
27
+ render_from_path_info(req, path_info)
28
28
  end
29
29
 
30
30
  private
@@ -34,56 +34,91 @@ module Impression
34
34
  # @param req [Qeweney::Request] request
35
35
  # @param kind [Symbol] path kind (`:not_found` or `:file`)
36
36
  # @param path [String, nil] file path
37
- def render_from_path_info(req, kind, path = nil)
38
- case kind
37
+ def render_from_path_info(req, path_info)
38
+ case path_info[:kind]
39
39
  when :not_found
40
40
  req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
41
41
  when :file
42
- req.serve_file(path)
42
+ render_file(req, path_info)
43
43
  else
44
44
  raise "Invalid path info kind #{kind.inspect}"
45
45
  end
46
46
  end
47
47
 
48
+ private
49
+
50
+ # Renders a file response for the given request and the given path info.
51
+ #
52
+ # @param req [Qeweney::Request] request
53
+ # @param path_info [Hash] path info
54
+ # @return [void]
55
+ def render_file(req, path_info)
56
+ req.serve_file(path_info[:path])
57
+ end
58
+
48
59
  # Returns the path info for the given relative path.
49
60
  #
50
61
  # @param path [String] relative path
51
- # @return [Array] path info (a tuple comprising kind and file path)
62
+ # @return [Hash] path info
52
63
  def get_path_info(path)
53
- @path_info_cache[path] || calculate_path_info(path)
64
+ @path_info_cache[path] ||= calculate_path_info(path)
54
65
  end
55
66
 
56
67
  # Calculates the path info for the given relative path.
57
68
  #
58
69
  # @param path [String] relative path
59
- # @param add_html_ext [bool] whether to add .html extension if not found
60
- # @return [Array] path info
61
- def calculate_path_info(path, add_html_ext = true)
70
+ # @param add_ext [bool] whether to add .html extension if not found
71
+ # @return [Hash] path info
72
+ def calculate_path_info(path)
62
73
  full_path = File.join(@directory, path)
63
74
 
64
- stat = File.stat(full_path) rescue nil
75
+ path_info(full_path) || search_path_info_with_extension(full_path) || { kind: :not_found }
76
+ end
77
+
78
+ # Returns the path info for the given path. If the path refers to a file,
79
+ # returns a hash containing the file information. If the path refers to a
80
+ # directory, performs a search for an index file using #directory_path_info.
81
+ # Otherwise, returns nil.
82
+ #
83
+ # @param path [String] path
84
+ # @return [Hash, nil] path info
85
+ def path_info(path)
86
+ stat = File.stat(path) rescue nil
65
87
  if !stat
66
- return add_html_ext ?
67
- calculate_path_info("#{path}.html", false) : [:not_found]
88
+ nil
68
89
  elsif stat.directory?
69
- return calculate_directory_path_info(full_path)
90
+ return directory_path_info(path)
70
91
  else
71
- return [:file, full_path]
92
+ return { kind: :file, path: path, ext: File.extname(path) }
72
93
  end
73
94
  end
74
95
 
75
- # Calculates the path info for a directory. If an `index.html` file exists,
76
- # its path info is returned, otherwise a `:not_found` path info is returned.
96
+ # Calculates the path info for a directory. If an index file exists, its
97
+ # path info is returned, otherwise, returns nil.
77
98
  #
78
99
  # @param path [String] directory path
79
- # @return [Array] path info
80
- def calculate_directory_path_info(path)
81
- index_path = File.join(path, 'index.html')
82
- if File.file?(index_path)
83
- [:file, index_path]
84
- else
85
- [:not_found]
100
+ # @return [Hash, nil] path info
101
+ def directory_path_info(path)
102
+ search_path_info_with_extension(File.join(path, 'index'))
103
+ end
104
+
105
+ # Returns the supported path extensions for paths without extension.
106
+ #
107
+ # @return [Array] supported extensions
108
+ def supported_path_extensions
109
+ [:html]
110
+ end
111
+
112
+ # Searches for files with extensions for the given path.
113
+ #
114
+ # @param path [String] path
115
+ # @return [Hash, nil] path info
116
+ def search_path_info_with_extension(path)
117
+ supported_path_extensions.each do |ext|
118
+ info = path_info("#{path}.#{ext}")
119
+ return info if info
86
120
  end
121
+ nil
87
122
  end
88
123
  end
89
124
  end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'yaml'
5
+ require 'modulation'
6
+ require 'papercraft'
7
+
8
+ require_relative './resource'
9
+ require_relative './file_tree'
10
+
11
+ module Impression
12
+
13
+ # `Jamstack` implements a resource that maps to a Jamstack app directory.
14
+ class Jamstack < FileTree
15
+ def initialize(**props)
16
+ super
17
+ @layouts = {}
18
+ end
19
+
20
+ private
21
+
22
+ # Renders a file response for the given request and the given path info.
23
+ #
24
+ # @param req [Qeweney::Request] request
25
+ # @param path_info [Hash] path info
26
+ # @return [void]
27
+ def render_file(req, path_info)
28
+ case path_info[:ext]
29
+ when '.rb'
30
+ render_papercraft_module(req, path_info[:path])
31
+ when '.md'
32
+ render_markdown_file(req, path_info[:path])
33
+ else
34
+ req.serve_file(path_info[:path])
35
+ end
36
+ end
37
+
38
+ # Renders a Papercraft module. The module is loaded using Modulation.
39
+ #
40
+ # @param req [Qeweney::Request] reqest
41
+ # @param path [String] file path
42
+ # @return [void]
43
+ def render_papercraft_module(req, path)
44
+ mod = import path
45
+
46
+ html = H(mod).render
47
+ req.respond(html, 'Content-Type' => Qeweney::MimeTypes[:html])
48
+ end
49
+
50
+ # Renders a markdown file using a layout.
51
+ #
52
+ # @param req [Qeweney::Request] reqest
53
+ # @param path [String] file path
54
+ # @return [void]
55
+ def render_markdown_file(req, path)
56
+ attributes, markdown = parse_markdown_file(path)
57
+
58
+ layout = get_layout(attributes[:layout])
59
+
60
+ html = layout.render(**attributes) { emit_markdown markdown }
61
+ req.respond(html, 'Content-Type' => Qeweney::MimeTypes[:html])
62
+ end
63
+
64
+ # Returns a layout component based on the given name. The given name
65
+ # defaults to 'default' if nil.
66
+ #
67
+ # @param layout [String, nil] layout name
68
+ # @return [Papercraft::Component] layout component
69
+ def get_layout(layout)
70
+ layout ||= 'default'
71
+ path = File.join(@directory, "_layouts/#{layout}.rb")
72
+ raise "Layout not found #{path}" unless File.file?(path)
73
+
74
+ import path
75
+ end
76
+
77
+ MARKDOWN_PAGE_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m.freeze
78
+
79
+ # Parses the markdown file at the given path.
80
+ #
81
+ # @param path [String] file path
82
+ # @return [Array] an tuple containing properties<Hash>, contents<String>
83
+ def parse_markdown_file(path)
84
+ data = IO.read(path) || ''
85
+ if (m = data.match(MARKDOWN_PAGE_REGEXP))
86
+ front_matter = m[1]
87
+
88
+ [symbolize_keys(YAML.load(front_matter)), m.post_match]
89
+ else
90
+ [{}, data]
91
+ end
92
+ end
93
+
94
+ # Converts a hash with string keys to one with symbol keys.
95
+ #
96
+ # @param hash [Hash] input hash
97
+ # @return [Hash] output hash
98
+ def symbolize_keys(hash)
99
+ hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
100
+ end
101
+
102
+ # Returns the supported path extensions used for searching for files based
103
+ # on pretty URLs.
104
+ #
105
+ # @return [Array] list of supported path extensions
106
+ def supported_path_extensions
107
+ [:html, :rb, :md]
108
+ end
109
+ end
110
+ 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
@@ -3,11 +3,13 @@
3
3
  require 'qeweney'
4
4
 
5
5
  # require_relative './pages'
6
- require_relative './request_routing'
6
+ require_relative './request_extensions/routing'
7
+ require_relative './request_extensions/responses'
7
8
 
8
9
  # Extensions to `Qeweney::Request`
9
10
  class Qeweney::Request
10
11
 
11
12
  # include Impression::Pages::RequestMethods
12
- include Impression::RequestRouting
13
+ include Impression::RequestExtensions::Routing
14
+ include Impression::RequestExtensions::Responses
13
15
  end
@@ -71,7 +71,7 @@ module Impression
71
71
  #
72
72
  # @param req [Qeweney::Request] request
73
73
  # @return [void]
74
- def respond(req)
74
+ def call(req)
75
75
  req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
76
76
  end
77
77
 
@@ -122,10 +122,37 @@ module Impression
122
122
  def to_proc
123
123
  ->(req) do
124
124
  resource = route(req) || self
125
- resource.respond(req)
125
+ resource.call(req)
126
126
  end
127
127
  end
128
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
+
129
156
  private
130
157
 
131
158
  SLASH_PREFIXED_PATH_REGEXP = /^\//.freeze
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Impression
4
- VERSION = '0.2'
4
+ VERSION = '0.3'
5
5
  end
data/lib/impression.rb CHANGED
@@ -7,4 +7,5 @@ require_relative './impression/request_extensions'
7
7
 
8
8
  require_relative './impression/resource'
9
9
  require_relative './impression/file_tree'
10
+ require_relative './impression/jamstack'
10
11
  require_relative './impression/app'
data/test/helper.rb CHANGED
@@ -41,33 +41,33 @@ module Minitest::Assertions
41
41
  end
42
42
 
43
43
  def assert_response exp_body, exp_content_type, req
44
- msg = message(msg) { "Expected response body #{mu_pp(act)} to equal #{mu_pp(exp_body)}" }
45
- assert_equal exp_body, req.response_body, msg
44
+ actual = req.response_body
45
+ assert_equal exp_body.gsub("\n", ''), actual&.gsub("\n", '')
46
46
 
47
47
  return unless exp_content_type
48
48
 
49
49
  if Symbol === exp_content_type
50
50
  exp_content_type = Qeweney::MimeTypes[exp_content_type]
51
51
  end
52
- msg = message(msg) { "Expected response content type #{mu_pp(act)} to equal #{mu_pp(exp_body)}" }
53
- assert_equal exp_content_type, req.response_content_type, msg
52
+ actual = req.response_content_type
53
+ assert_equal exp_content_type, actual
54
54
  end
55
55
  end
56
56
 
57
57
  class Impression::Resource
58
- def route_and_respond(req)
59
- route(req).respond(req)
58
+ def route_and_call(req)
59
+ route(req).call(req)
60
60
  end
61
61
  end
62
62
 
63
63
  class PathRenderingResource < Impression::Resource
64
- def respond(req)
64
+ def call(req)
65
65
  req.respond(absolute_path)
66
66
  end
67
67
  end
68
68
 
69
69
  class CompletePathInfoRenderingResource < Impression::Resource
70
- def respond(req)
70
+ def call(req)
71
71
  req.respond("#{absolute_path} #{req.resource_relative_path}")
72
72
  end
73
73
  end
@@ -0,0 +1,9 @@
1
+ require 'papercraft'
2
+
3
+ default = import('./default')
4
+
5
+ export_default default.apply { |**props|
6
+ article {
7
+ emit_yield
8
+ }
9
+ }
@@ -0,0 +1,12 @@
1
+ require 'papercraft'
2
+
3
+ export_default H { |**props|
4
+ html5 {
5
+ head {
6
+ title props[:title]
7
+ }
8
+ body {
9
+ emit_yield **props
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: AAA
3
+ layout: article
4
+ ---
5
+
6
+ ## BBB
@@ -0,0 +1 @@
1
+ console.log(42);
@@ -0,0 +1 @@
1
+ <h1>Bar</h1>
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: BarBar
3
+ ---
4
+
5
+ <h1>BarIndex</h1>
6
+
7
+
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ layout = import('./_layouts/default')
4
+
5
+ export_default layout.apply(title: 'Foo title') {
6
+ h1 'foo'
7
+ }
@@ -0,0 +1,4 @@
1
+ ---
2
+ title: Hello
3
+ ---
4
+ <h1>Index</h1>
data/test/test_app.rb CHANGED
@@ -7,7 +7,7 @@ class AppTest < MiniTest::Test
7
7
  app = Impression::App.new(path: '/')
8
8
  req = mock_req(':method' => 'GET', ':path' => '/')
9
9
 
10
- app.respond(req)
10
+ app.call(req)
11
11
  assert_equal Qeweney::Status::NOT_FOUND, req.adapter.status
12
12
  end
13
13
 
@@ -30,43 +30,43 @@ class FileTreeTest < MiniTest::Test
30
30
 
31
31
  def test_file_tree_response
32
32
  req = mock_req(':method' => 'GET', ':path' => '/roo')
33
- @file_tree.route_and_respond(req)
33
+ @file_tree.route_and_call(req)
34
34
  assert_equal Qeweney::Status::NOT_FOUND, req.response_status
35
35
 
36
36
  req = mock_req(':method' => 'GET', ':path' => '/foo2')
37
- @file_tree.route_and_respond(req)
37
+ @file_tree.route_and_call(req)
38
38
  assert_equal Qeweney::Status::NOT_FOUND, req.response_status
39
39
 
40
40
  req = mock_req(':method' => 'GET', ':path' => '/bar2')
41
- @file_tree.route_and_respond(req)
41
+ @file_tree.route_and_call(req)
42
42
  assert_equal Qeweney::Status::NOT_FOUND, req.response_status
43
43
 
44
44
  req = mock_req(':method' => 'GET', ':path' => '/js/a.js')
45
- @file_tree.route_and_respond(req)
45
+ @file_tree.route_and_call(req)
46
46
  assert_response static('js/a.js'), :js, req
47
47
 
48
48
  req = mock_req(':method' => 'GET', ':path' => '/foo.html')
49
- @file_tree.route_and_respond(req)
49
+ @file_tree.route_and_call(req)
50
50
  assert_response static('foo.html'), :html, req
51
51
 
52
52
  req = mock_req(':method' => 'GET', ':path' => '/foo')
53
- @file_tree.route_and_respond(req)
53
+ @file_tree.route_and_call(req)
54
54
  assert_response static('foo.html'), :html, req
55
55
 
56
56
  req = mock_req(':method' => 'GET', ':path' => '/index.html')
57
- @file_tree.route_and_respond(req)
57
+ @file_tree.route_and_call(req)
58
58
  assert_response static('index.html'), :html, req
59
59
 
60
60
  req = mock_req(':method' => 'GET', ':path' => '/')
61
- @file_tree.route_and_respond(req)
61
+ @file_tree.route_and_call(req)
62
62
  assert_response static('index.html'), :html, req
63
63
 
64
64
  req = mock_req(':method' => 'GET', ':path' => '/bar/index.html')
65
- @file_tree.route_and_respond(req)
65
+ @file_tree.route_and_call(req)
66
66
  assert_response static('bar/index.html'), :html, req
67
67
 
68
68
  req = mock_req(':method' => 'GET', ':path' => '/bar')
69
- @file_tree.route_and_respond(req)
69
+ @file_tree.route_and_call(req)
70
70
  assert_response static('bar/index.html'), :html, req
71
71
  end
72
72
 
@@ -74,43 +74,43 @@ class FileTreeTest < MiniTest::Test
74
74
  @file_tree = Impression::FileTree.new(path: '/app', directory: STATIC_PATH)
75
75
 
76
76
  req = mock_req(':method' => 'GET', ':path' => '/app/roo')
77
- @file_tree.route_and_respond(req)
77
+ @file_tree.route_and_call(req)
78
78
  assert_equal Qeweney::Status::NOT_FOUND, req.response_status
79
79
 
80
80
  req = mock_req(':method' => 'GET', ':path' => '/app/foo2')
81
- @file_tree.route_and_respond(req)
81
+ @file_tree.route_and_call(req)
82
82
  assert_equal Qeweney::Status::NOT_FOUND, req.response_status
83
83
 
84
84
  req = mock_req(':method' => 'GET', ':path' => '/app/bar2')
85
- @file_tree.route_and_respond(req)
85
+ @file_tree.route_and_call(req)
86
86
  assert_equal Qeweney::Status::NOT_FOUND, req.response_status
87
87
 
88
88
  req = mock_req(':method' => 'GET', ':path' => '/app/js/a.js')
89
- @file_tree.route_and_respond(req)
89
+ @file_tree.route_and_call(req)
90
90
  assert_response static('js/a.js'), :js, req
91
91
 
92
92
  req = mock_req(':method' => 'GET', ':path' => '/app/foo.html')
93
- @file_tree.route_and_respond(req)
93
+ @file_tree.route_and_call(req)
94
94
  assert_response static('foo.html'), :html, req
95
95
 
96
96
  req = mock_req(':method' => 'GET', ':path' => '/app/foo')
97
- @file_tree.route_and_respond(req)
97
+ @file_tree.route_and_call(req)
98
98
  assert_response static('foo.html'), :html, req
99
99
 
100
100
  req = mock_req(':method' => 'GET', ':path' => '/app/index.html')
101
- @file_tree.route_and_respond(req)
101
+ @file_tree.route_and_call(req)
102
102
  assert_response static('index.html'), :html, req
103
103
 
104
104
  req = mock_req(':method' => 'GET', ':path' => '/app/')
105
- @file_tree.route_and_respond(req)
105
+ @file_tree.route_and_call(req)
106
106
  assert_response static('index.html'), :html, req
107
107
 
108
108
  req = mock_req(':method' => 'GET', ':path' => '/app/bar/index.html')
109
- @file_tree.route_and_respond(req)
109
+ @file_tree.route_and_call(req)
110
110
  assert_response static('bar/index.html'), :html, req
111
111
 
112
112
  req = mock_req(':method' => 'GET', ':path' => '/app/bar')
113
- @file_tree.route_and_respond(req)
113
+ @file_tree.route_and_call(req)
114
114
  assert_response static('bar/index.html'), :html, req
115
115
  end
116
116
  end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'qeweney/test_adapter'
5
+
6
+ class JamstackTest < MiniTest::Test
7
+ JAMSTACK_PATH = File.join(__dir__, 'jamstack')
8
+
9
+ def setup
10
+ @jamstack = Impression::Jamstack.new(path: '/', directory: JAMSTACK_PATH)
11
+ end
12
+
13
+ def test_jamstack_routing
14
+ req = mock_req(':method' => 'GET', ':path' => '/')
15
+ assert_equal @jamstack, @jamstack.route(req)
16
+
17
+ req = mock_req(':method' => 'GET', ':path' => '/nonexistent')
18
+ assert_equal @jamstack, @jamstack.route(req)
19
+
20
+ req = mock_req(':method' => 'GET', ':path' => '/index.html')
21
+ assert_equal @jamstack, @jamstack.route(req)
22
+
23
+ req = mock_req(':method' => 'GET', ':path' => '/foo')
24
+ assert_equal @jamstack, @jamstack.route(req)
25
+ end
26
+
27
+ def static(path)
28
+ IO.read(File.join(JAMSTACK_PATH, path))
29
+ end
30
+
31
+ def test_jamstack_response
32
+ req = mock_req(':method' => 'GET', ':path' => '/roo')
33
+ @jamstack.route_and_call(req)
34
+ assert_equal Qeweney::Status::NOT_FOUND, req.response_status
35
+
36
+ req = mock_req(':method' => 'GET', ':path' => '/foo2')
37
+ @jamstack.route_and_call(req)
38
+ assert_equal Qeweney::Status::NOT_FOUND, req.response_status
39
+
40
+ req = mock_req(':method' => 'GET', ':path' => '/bar2')
41
+ @jamstack.route_and_call(req)
42
+ assert_equal Qeweney::Status::NOT_FOUND, req.response_status
43
+
44
+ req = mock_req(':method' => 'GET', ':path' => '/assets/js/a.js')
45
+ @jamstack.route_and_call(req)
46
+ assert_response static('assets/js/a.js'), :js, req
47
+
48
+ req = mock_req(':method' => 'GET', ':path' => '/foo')
49
+ @jamstack.route_and_call(req)
50
+
51
+ foo = H {
52
+ html5 {
53
+ head {
54
+ title 'Foo title'
55
+ }
56
+ body {
57
+ h1 'foo'
58
+ }
59
+ }
60
+ }
61
+ assert_response foo.render, :html, req
62
+
63
+ req = mock_req(':method' => 'GET', ':path' => '/index')
64
+ @jamstack.route_and_call(req)
65
+
66
+ index = H {
67
+ html5 {
68
+ head {
69
+ title 'Hello'
70
+ }
71
+ body {
72
+ h1 'Index'
73
+ }
74
+ }
75
+ }
76
+ assert_response index.render, :html, req
77
+
78
+ req = mock_req(':method' => 'GET', ':path' => '/')
79
+ @jamstack.route_and_call(req)
80
+ assert_response index.render, :html, req
81
+
82
+ req = mock_req(':method' => 'GET', ':path' => '/bar')
83
+ @jamstack.route_and_call(req)
84
+ assert_response static('bar.html'), :html, req
85
+
86
+ req = mock_req(':method' => 'GET', ':path' => '/baz')
87
+ @jamstack.route_and_call(req)
88
+
89
+ baz_index = H {
90
+ html5 {
91
+ head {
92
+ title 'BarBar'
93
+ }
94
+ body {
95
+ h1 'BarIndex'
96
+ }
97
+ }
98
+ }
99
+ assert_response baz_index.render, :html, req
100
+
101
+ req = mock_req(':method' => 'GET', ':path' => '/articles/a')
102
+ @jamstack.route_and_call(req)
103
+
104
+ a = H {
105
+ html5 {
106
+ head {
107
+ title 'AAA'
108
+ }
109
+ body {
110
+ article {
111
+ h2 'BBB', id: 'bbb'
112
+ }
113
+ }
114
+ }
115
+ }
116
+ assert_response a.render, :html, req
117
+ end
118
+
119
+ def test_non_root_file_tree_response
120
+ @jamstack = Impression::Jamstack.new(path: '/app', directory: JAMSTACK_PATH)
121
+
122
+ req = mock_req(':method' => 'GET', ':path' => '/app/roo')
123
+ @jamstack.route_and_call(req)
124
+ assert_equal Qeweney::Status::NOT_FOUND, req.response_status
125
+
126
+ req = mock_req(':method' => 'GET', ':path' => '/app/foo2')
127
+ @jamstack.route_and_call(req)
128
+ assert_equal Qeweney::Status::NOT_FOUND, req.response_status
129
+
130
+ req = mock_req(':method' => 'GET', ':path' => '/app/bar2')
131
+ @jamstack.route_and_call(req)
132
+ assert_equal Qeweney::Status::NOT_FOUND, req.response_status
133
+
134
+ req = mock_req(':method' => 'GET', ':path' => '/app/assets/js/a.js')
135
+ @jamstack.route_and_call(req)
136
+ assert_response static('assets/js/a.js'), :js, req
137
+
138
+ req = mock_req(':method' => 'GET', ':path' => '/app/foo')
139
+ @jamstack.route_and_call(req)
140
+
141
+ foo = H {
142
+ html5 {
143
+ head {
144
+ title 'Foo title'
145
+ }
146
+ body {
147
+ h1 'foo'
148
+ }
149
+ }
150
+ }
151
+ assert_response foo.render, :html, req
152
+
153
+ req = mock_req(':method' => 'GET', ':path' => '/app/index')
154
+ @jamstack.route_and_call(req)
155
+
156
+ index = H {
157
+ html5 {
158
+ head {
159
+ title 'Hello'
160
+ }
161
+ body {
162
+ h1 'Index'
163
+ }
164
+ }
165
+ }
166
+ assert_response index.render, :html, req
167
+
168
+ req = mock_req(':method' => 'GET', ':path' => '/app/')
169
+ @jamstack.route_and_call(req)
170
+ assert_response index.render, :html, req
171
+
172
+ req = mock_req(':method' => 'GET', ':path' => '/app/bar')
173
+ @jamstack.route_and_call(req)
174
+ assert_response static('bar.html'), :html, req
175
+
176
+ req = mock_req(':method' => 'GET', ':path' => '/app/baz')
177
+ @jamstack.route_and_call(req)
178
+
179
+ baz_index = H {
180
+ html5 {
181
+ head {
182
+ title 'BarBar'
183
+ }
184
+ body {
185
+ h1 'BarIndex'
186
+ }
187
+ }
188
+ }
189
+ assert_response baz_index.render, :html, req
190
+
191
+ req = mock_req(':method' => 'GET', ':path' => '/app/articles/a')
192
+ @jamstack.route_and_call(req)
193
+
194
+ a = H {
195
+ html5 {
196
+ head {
197
+ title 'AAA'
198
+ }
199
+ body {
200
+ article {
201
+ h2 'BBB', id: 'bbb'
202
+ }
203
+ }
204
+ }
205
+ }
206
+ assert_response a.render, :html, req
207
+ end
208
+ end
@@ -78,16 +78,16 @@ class ResourceTest < MiniTest::Test
78
78
  assert_nil r1.route(req)
79
79
 
80
80
  req = mock_req(':method' => 'GET', ':path' => '/foo')
81
- r1.route(req).respond(req)
81
+ r1.route_and_call(req)
82
82
  # default reply
83
83
  assert_equal Qeweney::Status::NOT_FOUND, req.response_status
84
84
 
85
85
  req = mock_req(':method' => 'GET', ':path' => '/foo/bar')
86
- r1.route(req).respond(req)
86
+ r1.route_and_call(req)
87
87
  assert_equal '/foo/bar', req.response_body
88
88
 
89
89
  req = mock_req(':method' => 'GET', ':path' => '/foo/baz')
90
- r1.route(req).respond(req)
90
+ r1.route_and_call(req)
91
91
  assert_equal '/foo/baz', req.response_body
92
92
 
93
93
  req = mock_req(':method' => 'GET', ':path' => '/foo/bbb')
@@ -103,27 +103,99 @@ class ResourceTest < MiniTest::Test
103
103
  assert_nil r1.route(req)
104
104
 
105
105
  req = mock_req(':method' => 'GET', ':path' => '/foo')
106
- r1.route(req).respond(req)
106
+ r1.route_and_call(req)
107
107
  assert_equal '/foo /', req.response_body
108
108
 
109
109
  req = mock_req(':method' => 'GET', ':path' => '/foo/zzz')
110
- r1.route(req).respond(req)
110
+ r1.route_and_call(req)
111
111
  assert_equal '/foo /zzz', req.response_body
112
112
 
113
113
  req = mock_req(':method' => 'GET', ':path' => '/foo/bar')
114
- r1.route(req).respond(req)
114
+ r1.route_and_call(req)
115
115
  assert_equal '/foo/bar /', req.response_body
116
116
 
117
117
  req = mock_req(':method' => 'GET', ':path' => '/foo/bar/zzz')
118
- r1.route(req).respond(req)
118
+ r1.route_and_call(req)
119
119
  assert_equal '/foo/bar /zzz', req.response_body
120
120
 
121
121
  req = mock_req(':method' => 'GET', ':path' => '/foo/baz')
122
- r1.route(req).respond(req)
122
+ r1.route_and_call(req)
123
123
  assert_equal '/foo/baz /', req.response_body
124
124
 
125
125
  req = mock_req(':method' => 'GET', ':path' => '/foo/baz/xxx/yyy')
126
- r1.route(req).respond(req)
126
+ r1.route_and_call(req)
127
127
  assert_equal '/foo/baz /xxx/yyy', req.response_body
128
128
  end
129
+
130
+ class CallableResource < Impression::Resource
131
+ def initialize(**props, &block)
132
+ super(**props)
133
+ @block = block
134
+ end
135
+
136
+ def call(req)
137
+ @block.call(req)
138
+ end
139
+ end
140
+
141
+ def test_callable_resource
142
+ r1 = CompletePathInfoRenderingResource.new(path: 'foo')
143
+ r2 = CallableResource.new(parent: r1, path: 'bar') { |req| req.respond('hi') }
144
+
145
+ req = mock_req(':method' => 'GET', ':path' => '/foo/bar')
146
+ r1.route_and_call(req)
147
+ assert_equal 'hi', req.response_body
148
+ end
149
+
150
+ class CallableRouteResource < Impression::Resource
151
+ def initialize(**props, &block)
152
+ super(**props)
153
+ @block = block
154
+ end
155
+
156
+ def route(req)
157
+ @block
158
+ end
159
+ end
160
+
161
+ def test_callable_from_route_method
162
+ r1 = CompletePathInfoRenderingResource.new(path: 'foo')
163
+ r2 = CallableRouteResource.new(parent: r1, path: 'bar') { |req| req.respond('bye') }
164
+
165
+ req = mock_req(':method' => 'GET', ':path' => '/foo/bar')
166
+ r1.route_and_call(req)
167
+ assert_equal 'bye', req.response_body
168
+ end
169
+
170
+ def test_text_response
171
+ c = Class.new(Impression::Resource) do
172
+ def route(req)
173
+ case req.path
174
+ when '/text'
175
+ text_response('foo')
176
+ when '/html'
177
+ html_response('bar')
178
+ when '/json'
179
+ json_response({ :baz => 123 })
180
+ end
181
+ end
182
+ end
183
+
184
+ r = c.new(path: '/')
185
+
186
+ req = mock_req(':method' => 'GET', ':path' => '/text')
187
+ r.route_and_call(req)
188
+ assert_equal 'foo', req.response_body
189
+ assert_equal 'text/plain', req.response_content_type
190
+
191
+ req = mock_req(':method' => 'GET', ':path' => '/html')
192
+ r.route_and_call(req)
193
+ assert_equal 'bar', req.response_body
194
+ assert_equal 'text/html', req.response_content_type
195
+
196
+ req = mock_req(':method' => 'GET', ':path' => '/json')
197
+ r.route_and_call(req)
198
+ assert_equal '{"baz":123}', req.response_body
199
+ assert_equal 'application/json', req.response_content_type
200
+ end
129
201
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: impression
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: '0.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-13 00:00:00.000000000 Z
11
+ date: 2022-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: polyphony
@@ -100,14 +100,28 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0.12'
103
+ version: '0.14'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0.12'
110
+ version: '0.14'
111
+ - !ruby/object:Gem::Dependency
112
+ name: modulation
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.1'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.1'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: rake
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -177,12 +191,22 @@ files:
177
191
  - lib/impression/errors.rb
178
192
  - lib/impression/file_tree.rb
179
193
  - lib/impression/file_watcher.rb
194
+ - lib/impression/jamstack.rb
180
195
  - lib/impression/pages.rb
181
196
  - lib/impression/request_extensions.rb
182
- - lib/impression/request_routing.rb
197
+ - lib/impression/request_extensions/responses.rb
198
+ - lib/impression/request_extensions/routing.rb
183
199
  - lib/impression/resource.rb
184
200
  - lib/impression/version.rb
185
201
  - test/helper.rb
202
+ - test/jamstack/_layouts/article.rb
203
+ - test/jamstack/_layouts/default.rb
204
+ - test/jamstack/articles/a.md
205
+ - test/jamstack/assets/js/a.js
206
+ - test/jamstack/bar.html
207
+ - test/jamstack/baz/index.md
208
+ - test/jamstack/foo.rb
209
+ - test/jamstack/index.md
186
210
  - test/run.rb
187
211
  - test/static/bar/index.html
188
212
  - test/static/foo.html
@@ -191,6 +215,7 @@ files:
191
215
  - test/test_app.rb
192
216
  - test/test_file_tree.rb
193
217
  - test/test_file_watcher.rb
218
+ - test/test_jamstack.rb
194
219
  - test/test_resource.rb
195
220
  homepage: http://github.com/digital-fabric/impression
196
221
  licenses:
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fileutils'
4
-
5
- module Impression
6
-
7
- # Request routing extensions for `Qeweney::Request`
8
- module RequestRouting
9
-
10
- # Matches a route regexp against the relative request path. The relative
11
- # request path is a separate string (stored in `@resource_relative_path`)
12
- # that is updated as routes are matched against it. The `route_regexp`
13
- # should be either `nil` for root routes (`/`) or a Regexp of the form
14
- # `/^#{route}(\/.*)?$/`. See also `Resource#initialize`.
15
- #
16
- # @param route_regexp [Regexp, nil] Route regexp to match against
17
- # @return [String, nil] The remainder of the path (relative to the route)
18
- def match_resource_path?(route_regexp)
19
- @resource_relative_path ||= path.dup
20
-
21
- return @resource_relative_path unless route_regexp
22
-
23
- # Simplified logic: no match returns nil, otherwise we set the relative path for
24
- @resource_relative_path = match_resource_relative_path(
25
- @resource_relative_path, route_regexp
26
- )
27
- end
28
-
29
- # Returns the relative_path for the latest matched resource
30
- #
31
- # @return [String]
32
- def resource_relative_path
33
- @resource_relative_path ||= path.dup
34
- end
35
-
36
- private
37
-
38
- # Matches the given path against the given route regexp. If the path matches
39
- # the regexp, the relative path for the given route is returned. Otherwise,
40
- # this method returns `nil`.
41
- #
42
- # @param path [String] path to match
43
- # @param route_regexp [Regexp] route regexp to match against
44
- # @return [String, nil] the relative path for the given route, or nil if no match.
45
- def match_resource_relative_path(path, route_regexp)
46
- match = path.match(route_regexp)
47
- match && (match[1] || '/')
48
- end
49
- end
50
- end