impression 0.11 → 0.12

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: c7a6bc0c67e0c2488971f3277d47a40c44b482dd76e2456c4a1769d7cfd176a0
4
- data.tar.gz: aef9442c29716d60f20bb41e0fe7f518599352bacebff8c489d13b8e21046908
3
+ metadata.gz: e9b79a3bb3823fad2c5c7ad2ed7f187c2c398dea84c62d9d0fdbd481238f499a
4
+ data.tar.gz: 6cd1d41db73b992ee6f2027220440871d6512811f23b810ba064733a0538e7d4
5
5
  SHA512:
6
- metadata.gz: 8e26204c6147d8f10ba10e0d84c89d2d13b92795eee2aec72650dca37a6e069c2806a004491a5507f6fd32d540213b2dfae353b45e827febc71d3e19f2d5fb94
7
- data.tar.gz: 808dc2e16f4b58f74a82570b84e589da05649f9e78d4997e3c90e68c11537c636b3f93d9b32bd1b01add73617bfa921fba240f3e774978a9d898fdab834b1bfa
6
+ metadata.gz: 734ab33bb5056e06076403fc774a13851953edec042b915826577aaa810c8e9d39c1a242b95836b5b6dcb61404d0fd1e8ab3afaf0cd095105a14d5ccd1bab9ea
7
+ data.tar.gz: df49438b49021b4d42de855f72b2342ecfe9416a2ea118feb8d09580d8ac4c0bb3f10b87d25edc020cdf37be8a91a5d0d4f85119b3a7bf3e3d3186b917477594
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.12 2022-02-16
2
+
3
+ - Implement `RackApp` resource class (#16)
4
+
1
5
  ## 0.11 2022-02-10
2
6
 
3
7
  - Update Tipi
data/Gemfile CHANGED
@@ -1,8 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
-
5
- # %w{polyphony tipi qeweney papercraft}.each do |dep|
6
- # dir = "../#{dep}"
7
- # gem(dep, path: dir) if File.directory?(dir)
8
- # end
data/Gemfile.lock CHANGED
@@ -1,11 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- impression (0.11)
4
+ impression (0.12)
5
5
  modulation (~> 1.1)
6
- papercraft (~> 0.19)
6
+ papercraft (~> 0.23)
7
7
  polyphony (~> 0.77)
8
- qeweney (~> 0.17)
8
+ qeweney (~> 0.18)
9
9
  tipi (~> 0.50)
10
10
 
11
11
  GEM
@@ -16,7 +16,7 @@ GEM
16
16
  docile (1.4.0)
17
17
  escape_utils (1.2.1)
18
18
  ever (0.1)
19
- extralite (1.11)
19
+ extralite (1.12)
20
20
  faraday (1.9.3)
21
21
  faraday-em_http (~> 1.0)
22
22
  faraday-em_synchrony (~> 1.0)
@@ -50,15 +50,15 @@ GEM
50
50
  localhost (1.1.9)
51
51
  minitest (5.11.3)
52
52
  modulation (1.1)
53
- msgpack (1.4.4)
53
+ msgpack (1.4.5)
54
54
  multipart-post (2.1.1)
55
- papercraft (0.19)
55
+ papercraft (0.23)
56
56
  escape_utils (~> 1.2.1)
57
57
  kramdown (~> 2.3.1)
58
58
  kramdown-parser-gfm (~> 1.1.0)
59
59
  rouge (~> 3.27.0)
60
60
  polyphony (0.77)
61
- qeweney (0.17)
61
+ qeweney (0.18)
62
62
  escape_utils (~> 1.2.1)
63
63
  rack (2.2.3)
64
64
  rake (12.3.3)
data/README.md CHANGED
@@ -19,20 +19,90 @@
19
19
  ## What is Impression
20
20
 
21
21
  > Impression is still in a very early stage of development. Things might not
22
- > correctly, or not at all.
22
+ > work correctly.
23
23
 
24
24
  Impression is a modern web framework for Ruby. Unlike other web framework,
25
25
  Impression does not impose any rigid structure or paradigm, but instead provides
26
- a set of tools letting you build any kind of web app, by freely mixing different
27
- kinds of web resources, be they static files, structured templates, Jamstack
28
- sites, or dynamic APIs.
26
+ a minimalistic set of tools, letting you build any kind of web app, by freely
27
+ mixing different kinds of web resources, be they static files, structured
28
+ templates, Jamstack sites, or dynamic APIs.
29
29
 
30
- In Impression, resources can be composed into a hierarchical tree, allowing
31
- combining of multiple web apps, or web subsystems, into a single URL hierarchy.
30
+ ## Resources
31
+
32
+ The main abstraction in Impression is the resource - which represents an web
33
+ endpoint that is mounted at a specific location in the URL namespace, and
34
+ responds to requests. Resources can be nested in order to create arbitrarily
35
+ complex routing trees. Impression provides multiple resource types, each
36
+ customized for a specific use case, be it a JSON API, a set of MVC-style
37
+ controllers, or a Markdown-based blog with static content.
38
+
39
+ Finally, any kind of resource can be used as an Impression app. Routing is
40
+ performed automatically according to the resource tree, starting from the root
41
+ resource.
42
+
43
+ ## The request-response cycle
44
+
45
+ The handling of incoming HTTP requests is done in two stages. First the request
46
+ is routed to the corresponding resource, which then handles the request by
47
+ generating a response.
48
+
49
+ HTTP requests and responses use the
50
+ [Qeweney](https://github.com/digital-fabric/qeweney) API.
51
+
52
+ ## Resource types
53
+
54
+ Impression provides the following resources:
55
+
56
+ - `Resource` - a generic resource.
57
+ - `FileTree` - a resource serving static files from the given directory.
58
+ - `App` - a resource serving static files, markdown files with layouts and Ruby
59
+ modules from the given directory.
60
+ - `RackApp` - a resource serving the given Rack app.
61
+
62
+ ## Setting up a basic resource
63
+
64
+ To setup a generic resource, call `Impression.resource` and provide a request
65
+ handler:
66
+
67
+ ```
68
+ app = Impression.resource { |req| req.respond('Hello, world!') }
69
+ ```
70
+
71
+ ## Running your app with Tipi
32
72
 
33
73
  Impression is made for running on top of
34
- [Tipi](https://github.com/digital-fabric/tipi), a new all-in-one web server for
35
- Ruby.
74
+ [Tipi](https://github.com/digital-fabric/tipi). Your Tipi app file would like
75
+ something like the following:
76
+
77
+ ```ruby
78
+ # app.rb
79
+ app = Impression.resource { |req| req.respond('Hello, world!') }
80
+ Tipi.run(&app)
81
+ ```
82
+
83
+ You can then start Tipi by running `tipi run app.rb`.
84
+
85
+ ## Running your app with a Rack app server
86
+
87
+ You can also run your app on any Rack app server, using something like the
88
+ following:
89
+
90
+ ```ruby
91
+ app = Impression.resource { |req| req.respond('Hello, world!') }
92
+ run Qeweney.rack(&app)
93
+ ```
94
+
95
+ ## Creating a routing map with resources
96
+
97
+ A resource can be mounted at any point in the app's URL space. Resources can be
98
+ nested within other resources by passing a `parent:` argument when creating a
99
+ resource:
100
+
101
+ ```ruby
102
+ app = Impression.app { |req| req.respond('Homepage') }
103
+ greeter = Impression.resource(parent: app, path: 'greeter')
104
+ static = Impression.file_tree(parent: app, path: 'static', directory: __dir__)
105
+ ```
36
106
 
37
107
  ## I want to know more
38
108
 
data/impression.gemspec CHANGED
@@ -22,9 +22,9 @@ Gem::Specification.new do |s|
22
22
 
23
23
  s.add_runtime_dependency 'polyphony', '~>0.77'
24
24
  s.add_runtime_dependency 'tipi', '~>0.50'
25
- s.add_runtime_dependency 'qeweney', '~>0.17'
25
+ s.add_runtime_dependency 'qeweney', '~>0.18'
26
26
 
27
- s.add_runtime_dependency 'papercraft', '~>0.19'
27
+ s.add_runtime_dependency 'papercraft', '~>0.23'
28
28
  s.add_runtime_dependency 'modulation', '~>1.1'
29
29
 
30
30
 
@@ -1,9 +1,277 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
4
+ require 'date'
5
+ require 'yaml'
6
+ require 'modulation'
7
+ require 'papercraft'
8
+
9
+ require_relative './resource'
10
+ require_relative './file_tree'
11
+
3
12
  module Impression
4
- class App < Resource
5
- def initialize(path: '/', **opts)
6
- super(path: path, **opts)
13
+
14
+ # `App` implements a resource that maps to a generic app directory.
15
+ class App < FileTree
16
+ def initialize(**props)
17
+ super
18
+ @layouts = {}
19
+ 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| get_path_info(File.join(dir, 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
+ FRONT_MATTER_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
+ YAML_OPTS = {
45
+ permitted_classes: [Date],
46
+ symbolize_names: true
47
+ }.freeze
48
+
49
+ # Returns the path info for the given file path.
50
+ #
51
+ # @param path [String] file path
52
+ # @return [Hash] path info
53
+ def file_info(path)
54
+ info = super
55
+ case info[:ext]
56
+ when '.md'
57
+ atts, content = parse_markdown_file(path)
58
+ info = info.merge(atts)
59
+ info[:html_content] = Papercraft.markdown(content)
60
+ info[:kind] = :markdown
61
+ when '.rb'
62
+ info[:module] = import(path)
63
+ info[:kind] = :module
64
+ end
65
+ if (m = path.match(DATE_REGEXP))
66
+ info[:date] ||= Date.parse(m[1])
67
+ end
68
+
69
+ info
70
+ end
71
+
72
+ # Returns the pretty URL for the given relative path. For pages, the
73
+ # extension is removed. For index pages, the index suffix is removed.
74
+ #
75
+ # @param relative_path [String] relative path
76
+ # @return [String] pretty URL
77
+ def pretty_url(relative_path)
78
+ if (m = relative_path.match(PAGE_EXT_REGEXP))
79
+ relative_path = m[1]
80
+ end
81
+ if (m = relative_path.match(INDEX_PAGE_REGEXP))
82
+ relative_path = m[1] || '/'
83
+ end
84
+ relative_path == '/' ? absolute_path : File.join(absolute_path, relative_path)
85
+ end
86
+
87
+ # Renders a response according to the given path info.
88
+ #
89
+ # @param req [Qeweney::Request] request
90
+ # @param path_info [Hash] path info
91
+ # @return [void]
92
+ def render_from_path_info(req, path_info)
93
+ case (kind = path_info[:kind])
94
+ when :not_found
95
+ mod_path_info = up_tree_resource_module_path_info(req, path_info)
96
+ if mod_path_info
97
+ render_module(req, mod_path_info)
98
+ else
99
+ req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
100
+ end
101
+ when :module
102
+ render_module(req, path_info)
103
+ when :markdown
104
+ render_markdown_file(req, path_info)
105
+ when :file
106
+ render_file(req, path_info)
107
+ else
108
+ raise "Invalid path info kind #{kind.inspect}"
109
+ end
110
+ end
111
+
112
+ # Returns the path info for an up-tree resource module, or false if not
113
+ # found. the :up_tree_resource_module_path_info KV can be either:
114
+ # - nil (default): up tree module search has not been performed.
115
+ # - false: no up tree module was found.
116
+ # - module path info: up tree module info (subsequent requests will be
117
+ # directly routed to the module).
118
+ #
119
+ # @param req [Qeweney::Request] request
120
+ # @param path_info [Hash] path info
121
+ # @return [Hash, false] up-tree resource module path info
122
+ def up_tree_resource_module_path_info(req, path_info)
123
+ if path_info[:up_tree_resource_module_path_info].nil?
124
+ if (mod_path_info = find_up_tree_resource_module(req, path_info))
125
+ path_info[:up_tree_resource_module_path_info] = mod_path_info
126
+ return mod_path_info;
127
+ else
128
+ path_info[:up_tree_resource_module_path_info] = false
129
+ return false
130
+ end
131
+ end
132
+ path_info[:up_tree_resource_module_path_info]
133
+ end
134
+
135
+ # Performs a recursive search for an up-tree resource module from the given
136
+ # path info. If a resource module is found up the tree, its path_info is
137
+ # returned, otherwise returns nil.
138
+ #
139
+ # @param req [Qeweney::Request] request
140
+ # @param path_info [Hash] path info
141
+ # @return [Hash, nil] up-tree resource module path info
142
+ def find_up_tree_resource_module(req, path_info)
143
+ relative_path = req.resource_relative_path
144
+
145
+ while relative_path != path
146
+ up_tree_path = File.expand_path('..', relative_path)
147
+ return nil if up_tree_path == relative_path
148
+
149
+ up_tree_path_info = get_path_info(up_tree_path)
150
+ case up_tree_path_info[:kind]
151
+ when :not_found
152
+ relative_path = up_tree_path
153
+ next
154
+ when :module
155
+ return up_tree_path_info
156
+ else
157
+ return nil
158
+ end
159
+ end
160
+ nil
161
+ end
162
+
163
+ # Renders a file response for the given request and the given path info,
164
+ # according to the file type.
165
+ #
166
+ # @param req [Qeweney::Request] request
167
+ # @param path_info [Hash] path info
168
+ # @return [void]
169
+ # def render_file(req, path_info)
170
+ # case path_info[:kind]
171
+ # else
172
+ # req.serve_file(path_info[:path])
173
+ # end
174
+ # end
175
+
176
+ # Renders a module. If the module is a Resource, it is mounted, and then the
177
+ # request is rerouted from the new resource and rendered. If the module is a
178
+ # Proc or a Papercraft::Template, it is rendered as such. Otherwise, an
179
+ # error is raised.
180
+ #
181
+ # @param req [Qeweney::Request] request
182
+ # @param path_info [Hash] path info
183
+ # @return [void]
184
+ def render_module(req, path_info)
185
+ # p render_module: path_info
186
+ case (mod = path_info[:module])
187
+ when Module
188
+ resource = mod.resource
189
+ resource.remount(self, path_info[:url])
190
+ # p path_info_url: path_info[:url], relative_path: req.resource_relative_path
191
+ relative_url = path_info[:url].gsub(/^#{path}/, '')
192
+ # p relative_url: relative_url
193
+ req.recalc_resource_relative_path(relative_url)
194
+ # p resource_relative_path: req.resource_relative_path
195
+ resource.route(req).call(req)
196
+ when Impression::Resource
197
+ mod.remount(self, path_info[:url])
198
+ req.recalc_resource_relative_path(path_info[:url])
199
+ mod.route(req).call(req)
200
+ when Proc, Papercraft::Template
201
+ render_papercraft_module(req, mod)
202
+ else
203
+ raise "Unsupported module type #{mod.class}"
204
+ end
205
+ end
206
+
207
+ # Renders a Papercraft module.
208
+ #
209
+ # @param mod [Module] Papercraft module
210
+ # @param path_info [Hash] path info
211
+ # @return [void]
212
+ def render_papercraft_module(req, mod)
213
+ template = Papercraft.html(mod)
214
+ body = template.render(request: req, resource: self)
215
+ req.respond(body, 'Content-Type' => template.mime_type)
216
+ end
217
+
218
+ # Renders a markdown file using a layout.
219
+ #
220
+ # @param req [Qeweney::Request] reqest
221
+ # @param path_info [Hash] path info
222
+ # @return [void]
223
+ def render_markdown_file(req, path_info)
224
+ layout = get_layout(path_info[:layout])
225
+
226
+ html = layout.render(request: req, resource: self, **path_info) {
227
+ emit path_info[:html_content]
228
+ }
229
+ req.respond(html, 'Content-Type' => layout.mime_type)
230
+ end
231
+
232
+ # Returns a layout component based on the given name. The given name
233
+ # defaults to 'default' if nil.
234
+ #
235
+ # @param layout [String, nil] layout name
236
+ # @return [Papercraft::Template] layout component
237
+ def get_layout(layout)
238
+ layout ||= 'default'
239
+ path = File.join(@directory, "_layouts/#{layout}.rb")
240
+ raise "Layout not found #{path}" unless File.file?(path)
241
+
242
+ import path
243
+ end
244
+
245
+ # Parses the markdown file at the given path.
246
+ #
247
+ # @param path [String] file path
248
+ # @return [Array] an tuple containing properties<Hash>, contents<String>
249
+ def parse_markdown_file(path)
250
+ content = IO.read(path) || ''
251
+ atts = {}
252
+
253
+ # Parse date from file name
254
+ if (m = path.match(DATE_REGEXP))
255
+ atts[:date] ||= Date.parse(m[1])
256
+ end
257
+
258
+ if (m = content.match(FRONT_MATTER_REGEXP))
259
+ front_matter = m[1]
260
+ content = m.post_match
261
+
262
+ yaml = YAML.safe_load(front_matter, **YAML_OPTS)
263
+ atts = atts.merge(yaml)
264
+ end
265
+
266
+ [atts, content]
267
+ end
268
+
269
+ # Returns the supported path extensions used for searching for files based
270
+ # on pretty URLs.
271
+ #
272
+ # @return [Array] list of supported path extensions
273
+ def supported_path_extensions
274
+ [:html, :rb, :md]
7
275
  end
8
276
  end
9
277
  end
@@ -14,8 +14,8 @@ module Impression
14
14
  #
15
15
  # @param directory [String] static directory path
16
16
  # @return [void]
17
- def initialize(directory:, **props)
18
- super(**props)
17
+ def initialize(directory: nil, **props, &block)
18
+ super(**props, &block)
19
19
  @directory = directory
20
20
  @path_info_cache = {}
21
21
  end
@@ -25,6 +25,8 @@ module Impression
25
25
  # @param req [Qeweney::Request] request
26
26
  # @return [void]
27
27
  def call(req)
28
+ return super if @directory.nil?
29
+
28
30
  path_info = get_path_info(req.resource_relative_path)
29
31
  render_from_path_info(req, path_info)
30
32
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tipi'
5
+ require_relative './resource'
6
+
7
+ module Impression
8
+
9
+ # The `RackApp` class represents Rack apps as resources.
10
+ class RackApp < Resource
11
+ def initialize(app: nil, **props, &block)
12
+ raise "No Rack app given" unless app || block
13
+
14
+ # We pass nil as the block, otherwise the block will pass to
15
+ # Resource#initialize, which will cause #call to be overidden.
16
+ super(**props, &nil)
17
+ @handler = Tipi::RackAdapter.run(app || block)
18
+ end
19
+
20
+ def call(req)
21
+ if @path != '/'
22
+ req.rewrite!(@path, '/')
23
+ end
24
+ @handler.(req)
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Impression
4
- VERSION = '0.11'
4
+ VERSION = '0.12'
5
5
  end
data/lib/impression.rb CHANGED
@@ -3,12 +3,10 @@
3
3
  require 'polyphony'
4
4
 
5
5
  require_relative './impression/request_extensions'
6
- # require_relative './impression/file_watcher'
7
-
8
6
  require_relative './impression/resource'
9
7
  require_relative './impression/file_tree'
10
- require_relative './impression/jamstack'
11
8
  require_relative './impression/app'
9
+ require_relative './impression/rack_app'
12
10
 
13
11
  # The Impression module contains convenience methods for creating resources.
14
12
  module Impression
@@ -26,17 +24,31 @@ module Impression
26
24
 
27
25
  # Creates a new `Impression::FileTree` instance with the given parameters.
28
26
  #
29
- # @param **props [Hash] properties
27
+ # @param path [String] resource path (defaults to `"/"`)
28
+ # @param **props [Hash] other resource properties
29
+ # @param &block [Proc] optional block for overriding default request handler
30
30
  # @return [Impression::FileTree] new resource
31
- def self.file_tree(path: '/', **props)
32
- FileTree.new(path: path, **props)
31
+ def self.file_tree(path: '/', **props, &block)
32
+ FileTree.new(path: path, **props, &block)
33
+ end
34
+
35
+ # Creates a new `Impression::App` instance with the given parameters.
36
+ #
37
+ # @param path [String] resource path (defaults to `"/"`)
38
+ # @param **props [Hash] other resource properties
39
+ # @param &block [Proc] optional block for overriding default request handler
40
+ # @return [Impression::App] new resource
41
+ def self.app(path: '/', **props, &block)
42
+ App.new(path: path, **props, &block)
33
43
  end
34
44
 
35
- # Creates a new `Impression::Jamstack` instance with the given parameters.
45
+ # Creates a new `Impression::RackApp` instance with the given parameters.
36
46
  #
37
- # @param **props [Hash] properties
38
- # @return [Impression::Jamstack] new resource
39
- def self.jamstack(path: '/', **props)
40
- Jamstack.new(path: path, **props)
47
+ # @param path [String] resource path (defaults to `"/"`)
48
+ # @param **props [Hash] other resource properties
49
+ # @param &block [Proc] Rack app proc (can also be passed using the `app:` parameter)
50
+ # @return [Impression::RackApp] new resource
51
+ def self.rack_app(path: '/', **props, &block)
52
+ RackApp.new(path: path, **props, &block)
41
53
  end
42
54
  end
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -3,7 +3,7 @@
3
3
  export :resource
4
4
 
5
5
  def resource
6
- Impression.jamstack(
6
+ Impression.app(
7
7
  directory: File.expand_path('..', __dir__)
8
8
  )
9
9
  end