syntropy 0.26 → 0.27

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: 69fb0521a1229556ec52f7d64d9cb94c00a6e4000d947cd1c78b829923938906
4
- data.tar.gz: 50fceb9e5cf0edd0765da05d44950e1d32fd8bd23cbbf5396df0da3cf941b880
3
+ metadata.gz: b84251781cfce3688f58c6f25a754b0699d97fe4a7c51f390b63635b45007960
4
+ data.tar.gz: 69e85137dcee6b399d3198303f9de4227b5276521a6c964eca5ac4954a9fb10f
5
5
  SHA512:
6
- metadata.gz: b0547a29500fed8ffa15d3d81be8cce5af9a9c8b128291edf44c1eb16aab36987db2f2f16226095649609d9fb0a2b1cc18235b496d0d217fb2518e1de84dfd4b
7
- data.tar.gz: 1b08a07b9220a09b199f4c411af861fe58f674a344643908e7fd691f994cfa1aadaa7782794507800d7a6834be91cc8d117b02a77d597da6bd0411f82dae4e0f
6
+ metadata.gz: dd871276c35cd2a9c216f37ec1361794e34f489af71580a1da5cebef244830daf0e2bc4e418abe28623607e8da5693ead8074dd1f397ba9ea61afb0e4bcf7d96
7
+ data.tar.gz: 2b3ae1c3c4691d247357a8ec3d9f7e3da778c0fcdd7a94de84b2d83c94ffe75c7beb7ec7883976c2ca09b5c45225391b9322e28a32be9cc3a55acfc6d8c5ac9e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 0.27 2025-10-21
2
+
3
+ - Use accept header (instead of user-agent) for rendering error page
4
+ - Add `Request#accept?` method
5
+ - Fix import path normalization, module method visibility
6
+ - Fix instantiation of Syntropy::Error
7
+ - Improve default error handler response
8
+ - Fix and enhance `Request#html_response`, `Request#json_response` methods
9
+
1
10
  # 0.26 2025-10-21
2
11
 
3
12
  - Add /.syntropy/req route for testing request headers
@@ -35,21 +35,24 @@ def transform_backtrace(backtrace)
35
35
  end
36
36
  end
37
37
 
38
- def error_response_browser(req, error)
38
+ def error_response_html(req, error)
39
39
  status = Syntropy::Error.http_status(error)
40
40
  backtrace = transform_backtrace(error.backtrace)
41
41
  html = Papercraft.html(ErrorPage, error:, status:, backtrace:)
42
- req.respond(html, ':status' => status, 'Content-Type' => 'text/html')
42
+ req.html_response(html, ':status' => status)
43
43
  end
44
44
 
45
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)
46
+ status = Syntropy::Error.http_status(error)
47
+ response = {
48
+ class: error.class.to_s,
49
+ message: error.message,
50
+ backtrace: error.backtrace
51
+ }
52
+ req.json_pretty_response(response, ':status' => status)
50
53
  end
51
54
 
52
55
  export ->(req, error) {
53
- req.browser? ?
54
- error_response_browser(req, error) : error_response_raw(req, error)
56
+ req.accept?('text/html') ?
57
+ error_response_html(req, error) : error_response_raw(req, error)
55
58
  }
@@ -21,17 +21,17 @@ module Syntropy
21
21
  # Creates an error with status 404 Not Found
22
22
  #
23
23
  # @return [Syntropy::Error]
24
- def self.not_found(msg = '') = new(Status::NOT_FOUND, msg)
24
+ def self.not_found(msg = 'Not found') = new(msg, Status::NOT_FOUND)
25
25
 
26
26
  # Creates an error with status 405 Method Not Allowed
27
27
  #
28
28
  # @return [Syntropy::Error]
29
- def self.method_not_allowed(msg = '') = new(Status::METHOD_NOT_ALLOWED, msg)
29
+ def self.method_not_allowed(msg = 'Method not allowed') = new(msg, Status::METHOD_NOT_ALLOWED)
30
30
 
31
31
  # Creates an error with status 418 I'm a teapot
32
32
  #
33
33
  # @return [Syntropy::Error]
34
- def self.teapot(msg = '') = new(Status::TEAPOT, msg)
34
+ def self.teapot(msg = 'I\'m a teapot') = new(msg, Status::TEAPOT)
35
35
 
36
36
  attr_reader :http_status
37
37
 
@@ -40,7 +40,7 @@ module Syntropy
40
40
  # @param http_status [Integer, String] HTTP status
41
41
  # @param msg [String] error message
42
42
  # @return [void]
43
- def initialize(http_status = DEFAULT_STATUS, msg = '')
43
+ def initialize(msg = 'Internal server error', http_status = DEFAULT_STATUS)
44
44
  super(msg)
45
45
  @http_status = http_status
46
46
  end
@@ -116,7 +116,8 @@ module Syntropy
116
116
  def clean_ref(ref)
117
117
  return '/' if ref =~ /^index(\+)?$/
118
118
 
119
- ref.gsub(/\/index(?:\+)?$/, '')
119
+ clean = ref.gsub(/\/index(?:\+)?$/, '')
120
+ clean == '' ? '/' : clean
120
121
  end
121
122
 
122
123
  # Transforms the given export value. If the value is nil, an exception is
@@ -180,10 +181,21 @@ module Syntropy
180
181
  @app = env[:app]
181
182
  @ref = env[:ref]
182
183
  @logger = env[:logger]
184
+ @__dependencies__ = []
183
185
  singleton_class.const_set(:MODULE, self)
184
186
  end
185
187
 
186
- attr_reader :__export_value__
188
+ attr_reader :__export_value__, :__dependencies__
189
+
190
+ # Returns a list of pages found at the given ref.
191
+ #
192
+ # @param ref [String] directory reference
193
+ # @return [Array] array of pages found in directory
194
+ def page_list(ref)
195
+ Syntropy.page_list(@env, ref)
196
+ end
197
+
198
+ private
187
199
 
188
200
  # Exports the given value. This value will be used as the module's
189
201
  # entrypoint. It can be any Ruby value, but for a route module would
@@ -195,23 +207,23 @@ module Syntropy
195
207
  @__export_value__ = v
196
208
  end
197
209
 
198
- # Returns the list of module references imported by the module.
199
- #
200
- # @return [Array] array of module references
201
- def __dependencies__
202
- @__dependencies__ ||= []
203
- end
204
-
205
210
  # Imports the module corresponding to the given reference. The return value
206
211
  # is the module's export value.
207
212
  #
208
213
  # @param ref [String] module reference
209
214
  # @return [any] loaded dependency's export value
210
215
  def import(ref)
211
- path = ref =~ /^\// ? ref : File.expand_path(File.join(File.dirname(@ref), ref))
212
- @module_loader.load(path).tap {
213
- __dependencies__ << path
214
- }
216
+ ref = normalize_import_ref(ref)
217
+ @module_loader.load(ref).tap { __dependencies__ << ref }
218
+ end
219
+
220
+ def normalize_import_ref(ref)
221
+ base = @ref == '' ? '/' : @ref
222
+ if ref =~ /^\//
223
+ ref
224
+ else
225
+ File.expand_path(File.join(File.dirname(base), ref))
226
+ end
215
227
  end
216
228
 
217
229
  # Creates and returns a Papercraft template created with the given block.
@@ -242,14 +254,6 @@ module Syntropy
242
254
  raise
243
255
  end
244
256
 
245
- # Returns a list of pages found at the given ref.
246
- #
247
- # @param ref [String] directory reference
248
- # @return [Array] array of pages found in directory
249
- def page_list(ref)
250
- Syntropy.page_list(@env, ref)
251
- end
252
-
253
257
  # Creates and returns a Syntropy app for the given environment. The app's
254
258
  # environment is based on the module's env merged with the given parameters.
255
259
  #
@@ -185,12 +185,28 @@ module Syntropy
185
185
  raise Syntropy::Error.new(Qeweney::Status::BAD_REQUEST, 'Invalid form data')
186
186
  end
187
187
 
188
- def html_repsonse(html)
189
- respond(html, 'Content-Type' => 'text/html; charset=utf-8')
188
+ def html_response(html, **headers)
189
+ respond(
190
+ html,
191
+ 'Content-Type' => 'text/html; charset=utf-8',
192
+ **headers
193
+ )
190
194
  end
191
195
 
192
- def json_response(obj)
193
- respond(JSON.dump(obj), 'Content-Type' => 'application/json; charset=utf-8')
196
+ def json_response(obj, **headers)
197
+ respond(
198
+ JSON.dump(obj),
199
+ 'Content-Type' => 'application/json; charset=utf-8',
200
+ **headers
201
+ )
202
+ end
203
+
204
+ def json_pretty_response(obj, **headers)
205
+ respond(
206
+ JSON.pretty_generate(obj),
207
+ 'Content-Type' => 'application/json; charset=utf-8',
208
+ **headers
209
+ )
194
210
  end
195
211
 
196
212
  def browser?
@@ -198,8 +214,24 @@ module Syntropy
198
214
  user_agent && user_agent =~ /^Mozilla\//
199
215
  end
200
216
 
217
+ # Returns true if the accept header includes the given MIME type
218
+ #
219
+ # @param mime_type [String] MIME type
220
+ # @return [bool]
221
+ def accept?(mime_type)
222
+ accept = headers['accept']
223
+ return nil if !accept
224
+
225
+ @accept_parts ||= parse_accept_parts(accept)
226
+ @accept_parts.include?(mime_type)
227
+ end
228
+
201
229
  private
202
230
 
231
+ def parse_accept_parts(accept)
232
+ accept.split(',').map { it.match(/^\s*([^\s;]+)/)[1] }
233
+ end
234
+
203
235
  BOOL_REGEXP = /^(t|f|true|false|on|off|1|0|yes|no)$/
204
236
  BOOL_TRUE_REGEXP = /^(t|true|on|1|yes)$/
205
237
  INTEGER_REGEXP = /^[+-]?[0-9]+$/
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.26'
4
+ VERSION = '0.27'
5
5
  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.26'
4
+ version: '0.27'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner