syntropy 0.25 → 0.26

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: d8e03dfbaf126674e740e0ef3819967a181499962a2940dbcbb075c005d91f87
4
- data.tar.gz: aa26e63ce7f99be78e8a70b879a27ac039674947b833122533afb23a34bbdda2
3
+ metadata.gz: 69fb0521a1229556ec52f7d64d9cb94c00a6e4000d947cd1c78b829923938906
4
+ data.tar.gz: 50fceb9e5cf0edd0765da05d44950e1d32fd8bd23cbbf5396df0da3cf941b880
5
5
  SHA512:
6
- metadata.gz: d2eac9d510c3d449fc507a7bcd457ac448d857121915b030730714a6fcaeadecc36f52c4b5b77becc1bbe6760d71ab59bb8d10e3ff0edcece136f55e4fe503f8
7
- data.tar.gz: a05435e23baa3b6d0ba34c7bc24ae0fc6652931e53313c63652ad84be955b1b15467e3d0abb0b4ce4eb152a72b5f20b2664392a7b4ea8bd4c61478f48d7e6903
6
+ metadata.gz: b0547a29500fed8ffa15d3d81be8cce5af9a9c8b128291edf44c1eb16aab36987db2f2f16226095649609d9fb0a2b1cc18235b496d0d217fb2518e1de84dfd4b
7
+ data.tar.gz: 1b08a07b9220a09b199f4c411af861fe58f674a344643908e7fd691f994cfa1aadaa7782794507800d7a6834be91cc8d117b02a77d597da6bd0411f82dae4e0f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 0.26 2025-10-21
2
+
3
+ - Add /.syntropy/req route for testing request headers
4
+ - Add default builtin error page / response
5
+ - Add `Request#browser?` method for detecting browser
6
+ - Change behaviour of import to support relative paths
7
+ - Update Papercraft
8
+
1
9
  # 0.25 2025-10-19
2
10
 
3
11
  - Upgrade Papercraft to version 3.0
data/examples/index.md CHANGED
@@ -5,4 +5,4 @@ title: Syntropy Examples
5
5
  # Syntropy Examples
6
6
 
7
7
  - [Template composition](/templates)
8
- - [JSON API](/counter)
8
+ - [JSON API](/counter)
data/lib/syntropy/app.rb CHANGED
@@ -168,7 +168,7 @@ module Syntropy
168
168
  end
169
169
 
170
170
  # Serves a static file from the given target hash with cache validation.
171
- #
171
+ #
172
172
  # @param req [Qeweney::Request] request
173
173
  # @param target [Hash] route target hash
174
174
  # @return [void]
@@ -190,7 +190,7 @@ module Syntropy
190
190
 
191
191
  # Validates and conditionally updates the file information for the given
192
192
  # target.
193
- #
193
+ #
194
194
  # @param target [Hash] route target hash
195
195
  # @return [void]
196
196
  def validate_static_file_info(target)
@@ -203,7 +203,7 @@ module Syntropy
203
203
  STATX_MASK = UM::STATX_MTIME | UM::STATX_SIZE
204
204
 
205
205
  # Updates the static file information for the given target
206
- #
206
+ #
207
207
  # @param target [Hash] route target hash
208
208
  # @param now [Time] current time
209
209
  # @return [void]
@@ -373,12 +373,6 @@ module Syntropy
373
373
  @module_loader.load(ref)
374
374
  end
375
375
 
376
- DEFAULT_ERROR_HANDLER = ->(req, err) {
377
- msg = err.message
378
- msg = nil if msg.empty? || (req.method == 'head')
379
- req.respond(msg, ':status' => Syntropy::Error.http_status(err)) rescue nil
380
- }
381
-
382
376
  # Returns an error handler for the given route. If route is nil, looks up
383
377
  # the error handler for the routing tree root. If no handler is found,
384
378
  # returns the default error handler.
@@ -386,7 +380,7 @@ module Syntropy
386
380
  # @param route [Hash] route entry
387
381
  # @return [Proc] error handler proc
388
382
  def get_error_handler(route)
389
- route_error_handler(route || @routing_tree.root) || DEFAULT_ERROR_HANDLER
383
+ route_error_handler(route || @routing_tree.root) || default_error_handler
390
384
  end
391
385
 
392
386
  # Returns the given route's error handler, caching the result.
@@ -420,6 +414,23 @@ module Syntropy
420
414
  route[:parent] && find_error_handler(route[:parent])
421
415
  end
422
416
 
417
+ RAW_DEFAULT_ERROR_HANDLER = ->(req, err) {
418
+ msg = err.message
419
+ msg = nil if msg.empty? || (req.method == 'head')
420
+ req.respond(msg, ':status' => Syntropy::Error.http_status(err)) rescue nil
421
+ }
422
+
423
+ def default_error_handler
424
+
425
+ @default_error_handler ||= begin
426
+ if @builtin_applet
427
+ @builtin_applet.module_loader.load('/default_error_handler')
428
+ else
429
+ RAW_DEFAULT_ERROR_HANDLER
430
+ end
431
+ end
432
+ end
433
+
423
434
  # Performs app start up, creating a log message and starting the file
424
435
  # watcher according to app options.
425
436
  #
@@ -0,0 +1,18 @@
1
+ body {
2
+ max-width: 800px;
3
+ margin: 4em auto;
4
+
5
+ font-family: Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans', source-sans-pro, sans-serif;
6
+
7
+ big {
8
+ display: block;
9
+ font-weight: bold;
10
+ font-size: 10em;
11
+ }
12
+ }
13
+
14
+ @media (max-width: 768px) {
15
+ body {
16
+ margin-inline: 1em;
17
+ }
18
+ }
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ ErrorPage = ->(error:, status:, backtrace:) {
4
+ html {
5
+ head {
6
+ title "Syntropy error: #{error.message}"
7
+ meta charset: 'utf-8'
8
+ meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
9
+ link rel: 'stylesheet', type: 'text/css', href: '/.syntropy/default_error_handler/style.css'
10
+ }
11
+ body {
12
+ div {
13
+ big status
14
+ h2 error.message
15
+ if backtrace
16
+ p "Backtrace:"
17
+ ul {
18
+ backtrace.each {
19
+ li {
20
+ a(it[:entry], href: it[:url])
21
+ }
22
+ }
23
+ }
24
+ end
25
+ }
26
+ auto_refresh_watch!
27
+ }
28
+ }
29
+ }
30
+
31
+ def transform_backtrace(backtrace)
32
+ backtrace.map do
33
+ location = it.match(/^(.+:\d+):/)[1]
34
+ { entry: it, url: "vscode://file/#{location}" }
35
+ end
36
+ end
37
+
38
+ def error_response_browser(req, error)
39
+ status = Syntropy::Error.http_status(error)
40
+ backtrace = transform_backtrace(error.backtrace)
41
+ html = Papercraft.html(ErrorPage, error:, status:, backtrace:)
42
+ req.respond(html, ':status' => status, 'Content-Type' => 'text/html')
43
+ end
44
+
45
+ def error_response_raw(req, error)
46
+ status = error_status_code(error)
47
+ msg = err.message
48
+ msg = nil if msg.empty? || (req.method == 'head')
49
+ req.respond(msg, ':status' => status)
50
+ end
51
+
52
+ export ->(req, error) {
53
+ req.browser? ?
54
+ error_response_browser(req, error) : error_response_raw(req, error)
55
+ }
@@ -25,4 +25,4 @@ class JSONAPI {
25
25
  }
26
26
  }
27
27
 
28
- export default JSONAPI;
28
+ export default JSONAPI;
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ export ->(req) {
4
+ req.json_response({
5
+ headers: req.headers
6
+ })
7
+ }
@@ -38,6 +38,8 @@ module Syntropy
38
38
  # @param ref [String] module reference
39
39
  # @return [any] export value
40
40
  def load(ref)
41
+ ref = "/#{ref}" if ref !~ /^\//
42
+
41
43
  entry = (@modules[ref] ||= load_module(ref))
42
44
  entry[:export_value]
43
45
  end
@@ -93,6 +95,7 @@ module Syntropy
93
95
  # @param ref [String] module reference
94
96
  # @return [Hash] module entry
95
97
  def load_module(ref)
98
+ ref = "/#{ref}" if ref !~ /^\//
96
99
  fn = File.expand_path(File.join(@root_dir, "#{ref}.rb"))
97
100
  raise Syntropy::Error, "File not found #{fn}" if !File.file?(fn)
98
101
 
@@ -112,7 +115,7 @@ module Syntropy
112
115
 
113
116
  def clean_ref(ref)
114
117
  return '/' if ref =~ /^index(\+)?$/
115
-
118
+
116
119
  ref.gsub(/\/index(?:\+)?$/, '')
117
120
  end
118
121
 
@@ -205,8 +208,9 @@ module Syntropy
205
208
  # @param ref [String] module reference
206
209
  # @return [any] loaded dependency's export value
207
210
  def import(ref)
208
- @module_loader.load(ref).tap {
209
- __dependencies__ << ref
211
+ path = ref =~ /^\// ? ref : File.expand_path(File.join(File.dirname(@ref), ref))
212
+ @module_loader.load(path).tap {
213
+ __dependencies__ << path
210
214
  }
211
215
  end
212
216
 
@@ -246,11 +250,13 @@ module Syntropy
246
250
  Syntropy.page_list(@env, ref)
247
251
  end
248
252
 
249
- # Creates and returns a Syntropy app for the given environment.
253
+ # Creates and returns a Syntropy app for the given environment. The app's
254
+ # environment is based on the module's env merged with the given parameters.
250
255
  #
251
256
  # @param env [Hash] environment
252
257
  def app(**env)
253
- Syntropy::App.new(**(@env.merge(env)))
258
+ env = @env.merge(env)
259
+ Syntropy::App.new(**env)
254
260
  end
255
261
  end
256
262
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'qeweney'
4
+ require 'json'
4
5
 
5
6
  module Syntropy
6
7
  # Extensions for the Qeweney::Request class
@@ -144,7 +145,7 @@ module Syntropy
144
145
  # matches the given etag or last_modified values, responds with a 304 Not
145
146
  # Modified status. Otherwise, yields to the given block for a normal
146
147
  # response, and sets cache control headers according to the given arguments.
147
- #
148
+ #
148
149
  # @param cache_control [String] value for Cache-Control header
149
150
  # @param etag [String, nil] Etag header value
150
151
  # @param last_modified [String, nil] Last-Modified header value
@@ -184,6 +185,19 @@ module Syntropy
184
185
  raise Syntropy::Error.new(Qeweney::Status::BAD_REQUEST, 'Invalid form data')
185
186
  end
186
187
 
188
+ def html_repsonse(html)
189
+ respond(html, 'Content-Type' => 'text/html; charset=utf-8')
190
+ end
191
+
192
+ def json_response(obj)
193
+ respond(JSON.dump(obj), 'Content-Type' => 'application/json; charset=utf-8')
194
+ end
195
+
196
+ def browser?
197
+ user_agent = headers['user-agent']
198
+ user_agent && user_agent =~ /^Mozilla\//
199
+ end
200
+
187
201
  private
188
202
 
189
203
  BOOL_REGEXP = /^(t|f|true|false|on|off|1|0|yes|no)$/
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.25'
4
+ VERSION = '0.26'
5
5
  end
data/syntropy.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |s|
23
23
 
24
24
  s.add_dependency 'extralite', '2.13'
25
25
  s.add_dependency 'json', '2.13.2'
26
- s.add_dependency 'papercraft', '3.0'
26
+ s.add_dependency 'papercraft', '3.0.1'
27
27
  s.add_dependency 'qeweney', '0.24'
28
28
  s.add_dependency 'tp2', '0.19'
29
29
  s.add_dependency 'uringmachine', '0.18'
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Klass = import '_lib/klass'
3
+ Klass = import './klass'
4
4
 
5
5
  def call(x)
6
6
  Klass.foo.to_s * x
data/test/app/_lib/dep.rb CHANGED
@@ -1,4 +1,4 @@
1
- Foo = import '_lib/self'
1
+ Foo = import '/_lib/self'
2
2
 
3
3
  def bar
4
4
  Foo.foo
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Klass = import '_lib/klass'
3
+ Klass = import '/_lib/klass'
4
4
 
5
5
  def call
6
6
  Klass.foo
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ export :foo
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ a1 = import './a'
4
+ a2 = import 'a'
5
+ foo = import '../foo/index'
6
+ callable = import '/_lib/callable'
7
+
8
+ export(a1:, a2:, foo:, callable:)
data/test/test_module.rb CHANGED
@@ -29,6 +29,17 @@ class ModuleTest < Minitest::Test
29
29
  assert_equal 43, mod.bar
30
30
  end
31
31
 
32
+ def test_import_paths
33
+ mod = @loader.load('/mod/path/b')
34
+ assert_kind_of Hash, mod
35
+ assert_equal [:a1, :a2, :foo, :callable], mod.keys
36
+
37
+ assert_equal :foo, mod[:a1]
38
+ assert_equal :foo, mod[:a2]
39
+ assert_kind_of Syntropy::Module, mod[:foo]
40
+ assert_equal 'barbarbar', mod[:callable].(3)
41
+ end
42
+
32
43
  def test_export_self
33
44
  mod = @loader.load('_lib/self')
34
45
  assert_kind_of Syntropy::Module, mod
@@ -39,23 +50,23 @@ class ModuleTest < Minitest::Test
39
50
  mod = @loader.load('_lib/env')
40
51
 
41
52
  assert_equal mod, mod.module_const
42
- assert_equal @env.merge(module_loader: @loader, ref: '_lib/env'), mod.env
53
+ assert_equal @env.merge(module_loader: @loader, ref: '/_lib/env'), mod.env
43
54
  assert_equal @machine, mod.machine
44
55
  assert_equal @loader, mod.module_loader
45
56
  assert_equal 42, mod.app
46
57
 
47
58
  assert_equal mod, mod.module_const
48
- assert_equal @env.merge(module_loader: @loader, ref: '_lib/env'), mod.env
59
+ assert_equal @env.merge(module_loader: @loader, ref: '/_lib/env'), mod.env
49
60
  assert_equal @machine, mod.machine
50
61
  assert_equal @loader, mod.module_loader
51
62
  assert_equal 42, mod.app
52
63
  end
53
64
 
54
65
  def test_dependency_invalidation
55
- mod = @loader.load('_lib/dep')
56
- assert_equal ['_lib/self', '_lib/dep'], @loader.modules.keys
66
+ _mod = @loader.load('_lib/dep')
67
+ assert_equal ['/_lib/self', '/_lib/dep'], @loader.modules.keys
57
68
 
58
- self_fn = @loader.modules['_lib/self'][:fn]
69
+ self_fn = @loader.modules['/_lib/self'][:fn]
59
70
  @loader.invalidate_fn(self_fn)
60
71
 
61
72
  assert_equal [], @loader.modules.keys
@@ -63,9 +74,9 @@ class ModuleTest < Minitest::Test
63
74
 
64
75
  def test_index_module_env
65
76
  mod = @loader.load('mod/bar/index+')
66
- assert_equal 'mod/bar', mod.env[:ref]
77
+ assert_equal '/mod/bar', mod.env[:ref]
67
78
 
68
79
  mod = @loader.load('mod/foo/index')
69
- assert_equal 'mod/foo', mod.env[:ref]
80
+ assert_equal '/mod/foo', mod.env[:ref]
70
81
  end
71
82
  end
@@ -570,5 +570,5 @@ class RoutingTreeWildcardIndexTest < Minitest::Test
570
570
  assert_nil route
571
571
  end
572
572
 
573
-
573
+
574
574
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syntropy
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.25'
4
+ version: '0.26'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: '3.0'
46
+ version: 3.0.1
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: '3.0'
53
+ version: 3.0.1
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: qeweney
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -203,8 +203,11 @@ files:
203
203
  - lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb
204
204
  - lib/syntropy/applets/builtin/debug/debug.css
205
205
  - lib/syntropy/applets/builtin/debug/debug.js
206
+ - lib/syntropy/applets/builtin/default_error_handler.rb
207
+ - lib/syntropy/applets/builtin/default_error_handler/style.css
206
208
  - lib/syntropy/applets/builtin/json_api.js
207
209
  - lib/syntropy/applets/builtin/ping.rb
210
+ - lib/syntropy/applets/builtin/req.rb
208
211
  - lib/syntropy/connection_pool.rb
209
212
  - lib/syntropy/dev_mode.rb
210
213
  - lib/syntropy/errors.rb
@@ -238,6 +241,8 @@ files:
238
241
  - test/app/index.html
239
242
  - test/app/mod/bar/index+.rb
240
243
  - test/app/mod/foo/index.rb
244
+ - test/app/mod/path/a.rb
245
+ - test/app/mod/path/b.rb
241
246
  - test/app/params/[foo].rb
242
247
  - test/app/rss.rb
243
248
  - test/app/tmp.rb