impression 0.2 → 0.3

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: 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