syntropy 0.39.0 → 0.41.0

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/TODO.md +44 -142
  4. data/cmd/console.rb +0 -2
  5. data/cmd/new/template/config/production.rb +2 -1
  6. data/cmd/new.rb +1 -1
  7. data/cmd/serve.rb +26 -0
  8. data/examples/agent/Dockerfile +25 -0
  9. data/examples/agent/Gemfile +3 -0
  10. data/examples/agent/README.md +40 -0
  11. data/examples/agent/TODO.md +5 -0
  12. data/examples/agent/app/_layout/default.rb +15 -0
  13. data/examples/agent/app/_lib/storage.rb +13 -0
  14. data/examples/agent/app/_schema/2026-01-01-initial.rb +9 -0
  15. data/examples/agent/app/assets/agent.js +63 -0
  16. data/examples/agent/app/assets/minigfm.js +207 -0
  17. data/examples/agent/app/assets/model.js +45 -0
  18. data/examples/agent/app/assets/style.css +25 -0
  19. data/examples/agent/app/assets/tools.js +74 -0
  20. data/examples/agent/app/assets/ui.js +50 -0
  21. data/examples/agent/app/index.rb +19 -0
  22. data/examples/agent/app/test.rb +7 -0
  23. data/examples/agent/config/Caddyfile +5 -0
  24. data/examples/agent/config/development.rb +5 -0
  25. data/examples/agent/config/production.rb +5 -0
  26. data/examples/agent/config/test.rb +5 -0
  27. data/examples/agent/docker-compose.yml +71 -0
  28. data/examples/agent/response_example.json +108 -0
  29. data/examples/agent/test/test_app.rb +14 -0
  30. data/examples/blog/app/foo.rb +1 -0
  31. data/examples/blog/config/production.rb +2 -1
  32. data/lib/syntropy/app.rb +6 -2
  33. data/lib/syntropy/applets/builtin/auto_refresh/watch.js +1 -1
  34. data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +7 -0
  35. data/lib/syntropy/dev_mode.rb +8 -3
  36. data/lib/syntropy/http/client.rb +17 -1
  37. data/lib/syntropy/http/client_connection.rb +16 -0
  38. data/lib/syntropy/http/io_extensions.rb +32 -4
  39. data/lib/syntropy/http/server.rb +55 -24
  40. data/lib/syntropy/http/server_connection.rb +75 -4
  41. data/lib/syntropy/logger.rb +8 -0
  42. data/lib/syntropy/markdown.rb +44 -20
  43. data/lib/syntropy/module_loader.rb +29 -16
  44. data/lib/syntropy/request/request_info.rb +1 -1
  45. data/lib/syntropy/request/response.rb +75 -6
  46. data/lib/syntropy/request/validation.rb +6 -0
  47. data/lib/syntropy/storage/connection_pool.rb +34 -0
  48. data/lib/syntropy/test.rb +4 -4
  49. data/lib/syntropy/version.rb +1 -1
  50. data/lib/syntropy.rb +7 -1
  51. data/syntropy.gemspec +4 -4
  52. data/test/test_markdown.rb +3 -3
  53. data/test/test_module_loader.rb +4 -1
  54. metadata +34 -11
@@ -12,6 +12,7 @@ module Syntropy
12
12
  symbolize_names: true
13
13
  }.freeze
14
14
 
15
+ # Implements a Markdown file controller.
15
16
  class Controller
16
17
  def initialize(env, atts, md)
17
18
  @env = env
@@ -20,6 +21,9 @@ module Syntropy
20
21
  @module_loader = env[:module_loader]
21
22
  end
22
23
 
24
+ # Returns the controller proc.
25
+ #
26
+ # @return [Proc] controller proc
23
27
  def to_proc
24
28
  ->(req) {
25
29
  case req.method
@@ -37,6 +41,10 @@ module Syntropy
37
41
 
38
42
  private
39
43
 
44
+ # Process any Papercraft embeds in the markdown file by rendering them
45
+ # into the markdown code.
46
+ #
47
+ # @return [String] Markdown with rendered embeds
40
48
  def process_md_embeds
41
49
  return @md if @embedded_templates&.empty?
42
50
 
@@ -48,23 +56,40 @@ module Syntropy
48
56
  }
49
57
  end
50
58
 
51
- def prepare_snippet_template(snippet, location = nil)
59
+ # Prepares a snippet template by writing it to a temporary file and
60
+ # instance_eval'ing the code, returning a Papercraft template.
61
+ #
62
+ # @param snippet [String] Papercraft snippet
63
+ # @param _location [String] currently not used
64
+ # @return [Proc] Papercraft template
65
+ def prepare_snippet_template(snippet, _location = nil)
52
66
  fn = "/tmp/snippet-#{SecureRandom.hex(8)}.rb"
53
67
  src = "->() do\n#{snippet}\nend"
54
68
  IO.write(fn, src)
55
69
  instance_eval src, fn
56
70
  end
57
71
 
72
+ # Renders the given markdown to HTML.
73
+ #
74
+ # @param md [String] markdown
75
+ # @return [String] HTML
58
76
  def render(md)
59
77
  @template ||= make_template
60
78
  Papercraft.html(@template, md: md, **@atts)
61
79
  end
62
80
 
81
+ # Makes a Markdown rendering template based on the corresponding layout.
82
+ #
83
+ # @return [Proc] Papercraft template
63
84
  def make_template
64
85
  layout = make_layout
65
86
  Papercraft.apply(layout) { |md:, **| markdown(md) }
66
87
  end
67
88
 
89
+ # Makes a layout template by loading the layout module. If no layout
90
+ # module was specified, returns the default layout.
91
+ #
92
+ # @return [Proc] Layout template
68
93
  def make_layout
69
94
  return default_layout if !@atts[:layout]
70
95
  raise Error, 'Missing module loader' if !@module_loader
@@ -72,6 +97,9 @@ module Syntropy
72
97
  @module_loader.load("_layout/#{@atts[:layout]}")
73
98
  end
74
99
 
100
+ # Creates a default layout template.
101
+ #
102
+ # @return [Proc] Default layout template
75
103
  def default_layout
76
104
  ->(**atts) {
77
105
  html5 {
@@ -101,26 +129,26 @@ module Syntropy
101
129
  parse_md(atts, md)
102
130
  end
103
131
 
132
+ # Parses the markdown content, returning the attributes and cleaned up
133
+ # markdown.
134
+ #
135
+ # @param atts [Hash] attributes hash
136
+ # @param md [String] markdown file
137
+ # @return [Array<atts, md>] Array containing attributes and clean markdown
104
138
  def parse_md(atts, md)
105
- html = parse_content(atts, md)
106
- [atts, html]
139
+ md = parse_content(atts, md)
140
+ [atts, md]
107
141
  end
108
142
 
143
+ # Creates a Markdown file contoroller for the given environment,
144
+ # attributes and markdown.
145
+ #
146
+ # @param env [Hash] app environment
147
+ # @param atts [Hash] markdown file attributes
148
+ # @param md [String] markdown
149
+ # @return [Markdown::Controller] controller instance
109
150
  def make_controller(env, atts, md)
110
151
  Controller.new(env, atts, md).to_proc
111
- # layout = setup_layout_template(env, atts)
112
-
113
- # ->(req) {
114
- # case req.method
115
- # when 'head'
116
- # req.respond_html(nil)
117
- # when 'get'
118
- # html = render_md(env, atts, md)
119
- # req.respond_html(html)
120
- # else
121
- # req.respond(nil, ':status' => HTTP::METHOD_NOT_ALLOWED)
122
- # end
123
- # }
124
152
  end
125
153
 
126
154
  private
@@ -165,10 +193,6 @@ module Syntropy
165
193
  path.gsub(/#{app_root}/, '').gsub(/\.md$/, '')
166
194
  end
167
195
  end
168
-
169
- def render_md(env, atts, md)
170
-
171
- end
172
196
  end
173
197
  end
174
198
  end
@@ -44,11 +44,11 @@ module Syntropy
44
44
  #
45
45
  # @param ref [String] module reference
46
46
  # @return [any] export value
47
- def load(ref, raise_on_missing: true)
47
+ def load(ref, raise_on_missing_export: true)
48
48
  lock do
49
49
  ref = "/#{ref}" if ref !~ /^\//
50
50
  if !(entry = @modules[ref])
51
- entry = load_module(ref, raise_on_missing:)
51
+ entry = load_module(ref, raise_on_missing_export:)
52
52
  return if !entry
53
53
 
54
54
  @modules[ref] = entry
@@ -86,6 +86,8 @@ module Syntropy
86
86
  private
87
87
 
88
88
  # Synchronizes access to the module loader state
89
+ #
90
+ # @return [any] block return value
89
91
  def lock
90
92
  return yield if @lock_holder == Fiber.current
91
93
 
@@ -142,20 +144,25 @@ module Syntropy
142
144
  #
143
145
  # @param ref [String] module reference
144
146
  # @return [Hash] module entry
145
- def load_module(ref, raise_on_missing: true)
147
+ def load_module(ref, raise_on_missing_export: true)
146
148
  ref = "/#{ref}" if ref !~ /^\//
147
149
  fn = File.expand_path(File.join(@app_root, "#{ref}.rb"))
148
150
  if !File.file?(fn)
149
- raise Syntropy::Error, "File not found #{fn}" if raise_on_missing
151
+ raise Syntropy::Error, "File not found #{fn}" if raise_on_missing_export
150
152
 
151
153
  return
152
154
  end
153
155
 
154
156
  raise Syntropy::Error, "Circular dependency detected" if @loading.include?(ref)
155
- do_load_module(ref, fn, raise_on_missing:)
157
+ do_load_module(ref, fn, raise_on_missing_export:)
156
158
  end
157
159
 
158
- def do_load_module(ref, fn, raise_on_missing:)
160
+ # Loads a module.
161
+ #
162
+ # @param ref [String] module reference
163
+ # @param fn [String] module filename
164
+ # @param raise_on_missing_export [bool] whether to raise on missing export
165
+ def do_load_module(ref, fn, raise_on_missing_export:)
159
166
  @loading << ref
160
167
  @fn_map[fn] = ref
161
168
  code = read_file(fn)
@@ -163,7 +170,7 @@ module Syntropy
163
170
  mod = Syntropy::ModuleContext.new(env, code, fn, @extensions)
164
171
  add_dependencies(ref, mod.__dependencies__)
165
172
  export_value = transform_module_export_value(
166
- mod.__export_value__, fn, raise_on_missing:
173
+ mod, fn, raise_on_missing_export:
167
174
  )
168
175
  @env[:logger]&.info(message: "Loaded module at #{fn}")
169
176
 
@@ -180,6 +187,10 @@ module Syntropy
180
187
  @loading.delete(ref)
181
188
  end
182
189
 
190
+ # Returns a file's content.
191
+ #
192
+ # @param fn [String] filename
193
+ # @return [String] file content
183
194
  def read_file(fn)
184
195
  @machine.open(fn, UM::O_RDONLY) { |fd|
185
196
  buf = +''
@@ -199,22 +210,24 @@ module Syntropy
199
210
  (clean == '') ? '/' : clean
200
211
  end
201
212
 
202
- # Transforms the given export value. If the value is nil, an exception is
203
- # raised.
213
+ # Transforms the given module's export value. If the value is nil, an
214
+ # exception is raised.
204
215
  #
205
- # @param export_value [any] module's export value
216
+ # @param mod [Syntropy::ModuleContext] module context
206
217
  # @return [any] transformed value
207
- def transform_module_export_value(export_value, fn, raise_on_missing:)
208
- case export_value
218
+ def transform_module_export_value(mod, fn, raise_on_missing_export:)
219
+ case (value = mod.__export_value__)
209
220
  when nil
210
- raise Syntropy::Error, "No export found in #{fn}" if raise_on_missing
221
+ mod
211
222
  when String
212
- ->(req) { req.respond(export_value) }
223
+ ->(req) { req.respond(value) }
213
224
  else
214
- export_value
225
+ value
215
226
  end
216
227
  end
217
228
 
229
+ # Handles a syntax error encountered while loading a module by generating a
230
+ # new exception with a backtrace including the syntax error location.
218
231
  def handle_syntax_error(fn, e)
219
232
  $stderr.puts("\n#{e.message}") if !Syntropy.test_mode
220
233
  m = e.message.match(/^(.+): syntax/)
@@ -249,7 +262,7 @@ module Syntropy
249
262
  # @param env [Hash] environment hash
250
263
  # @param code [String] module source code
251
264
  # @param fn [String] module filename
252
- # @param extensions [Array<Module>, Module]
265
+ # @param extensions [Array<Module>, Module]
253
266
  # @return [void]
254
267
  def initialize(env, code, fn, extensions)
255
268
  @env = env
@@ -234,7 +234,7 @@ module Syntropy
234
234
  # @return [String]
235
235
  def rel(rel_path)
236
236
  File.expand_path(File.join(path, rel_path))
237
- end
237
+ end
238
238
 
239
239
  private
240
240
 
@@ -11,6 +11,10 @@ module Syntropy
11
11
  module ResponseMethods
12
12
  WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
13
13
 
14
+ # Upgrades a request to the WebSocket protocol.
15
+ #
16
+ # @param custom_headers [Hash, nil] additional response headers
17
+ # @return [void]
14
18
  def upgrade_to_websocket(custom_headers = nil)
15
19
  key = "#{headers['sec-websocket-key']}#{WEBSOCKET_GUID}"
16
20
  upgrade_headers = {
@@ -22,21 +26,42 @@ module Syntropy
22
26
  adapter.websocket_connection(self)
23
27
  end
24
28
 
29
+ # Responds with a redirect to the given URL. If a relative path is provided
30
+ # as the URL, the absolute URL path will be calculated relative to the
31
+ # current request's path.
32
+ #
33
+ # @param url [String] redirect URL
34
+ # @param status [Integer] HTTP status code
35
+ # @return [void]
25
36
  def redirect(url, status = HTTP::FOUND)
26
37
  url = File.expand_path(File.join(path, url)) if url =~ /^\./
27
38
  respond(nil, ':status' => status, 'Location' => url)
28
39
  end
29
40
 
41
+ # Redirects to the same URL, except with HTTPS.
42
+ #
43
+ # @param status [Integer] HTTP status code
44
+ # @return [void]
30
45
  def redirect_to_https(status = HTTP::MOVED_PERMANENTLY)
31
46
  secure_uri = "https://#{host}#{uri}"
32
47
  redirect(secure_uri, status)
33
48
  end
34
49
 
50
+ # Redirect to the same path and query, on a different host.
51
+ #
52
+ # @param new_host [String] new host name
53
+ # @param status [Integer] HTTP status code
54
+ # @return [void]
35
55
  def redirect_to_host(new_host, status = HTTP::FOUND)
36
56
  secure_uri = "//#{new_host}#{uri}"
37
57
  redirect(secure_uri, status)
38
58
  end
39
59
 
60
+ # Serves a static file as a response, with cache headers.
61
+ #
62
+ # @param path [String] file path
63
+ # @param opts [Hash] options
64
+ # @return [void]
40
65
  def serve_file(path, opts = {})
41
66
  full_path = file_full_path(path, opts)
42
67
  stat = File.stat(full_path)
@@ -59,6 +84,12 @@ module Syntropy
59
84
  respond(nil, ':status' => HTTP::NOT_FOUND)
60
85
  end
61
86
 
87
+ # Validates a static file cache against the request cache headers. Returns
88
+ # true if cache headers are valid.
89
+ #
90
+ # @param etag [String] ETag
91
+ # @param last_modified [String] last-modified value
92
+ # @return [bool] are cache headers valid
62
93
  def validate_static_file_cache(etag, last_modified)
63
94
  if (none_match = headers['if-none-match'])
64
95
  return true if none_match == etag
@@ -70,6 +101,11 @@ module Syntropy
70
101
  false
71
102
  end
72
103
 
104
+ # Returns a fully qualified file path based on the given options.
105
+ #
106
+ # @param path [String] file path
107
+ # @param opts [Hash] options hash
108
+ # @return [String] qualified file path
73
109
  def file_full_path(path, opts)
74
110
  if (base_path = opts[:base_path])
75
111
  File.join(opts[:base_path], path)
@@ -78,10 +114,13 @@ module Syntropy
78
114
  end
79
115
  end
80
116
 
81
- def serve_io(io, opts)
82
- respond(io.read, opts[:headers] || {})
83
- end
84
-
117
+ # Renders a static file response.
118
+ #
119
+ # @param path [String] file path
120
+ # @param etag [String] ETag
121
+ # @param last_modified [String] last-modified value
122
+ # @param opts [Hash] options
123
+ # @return [void]
85
124
  def respond_with_static_file(path, etag, last_modified, opts)
86
125
  cache_headers = (etag || last_modified) ? {
87
126
  'etag' => etag,
@@ -91,14 +130,29 @@ module Syntropy
91
130
  adapter.respond_with_static_file(self, path, opts, cache_headers)
92
131
  end
93
132
 
133
+ # Set additional response headers.
134
+ #
135
+ # @param headers [Hash] response headers
136
+ # @return [void]
94
137
  def set_response_headers(headers)
95
138
  adapter.set_response_headers(headers)
96
139
  end
97
140
 
98
- def set_cookie(k, v)
99
- adapter.set_cookie(k, v)
141
+ # Sets a response cookie.
142
+ #
143
+ # @param key [String] cookie name
144
+ # @param value [String] cookie value
145
+ # @return [void]
146
+ def set_cookie(key, value)
147
+ adapter.set_cookie(key, value)
100
148
  end
101
149
 
150
+ # Upgrades the connection to the given protocol, yielding the connection to
151
+ # the given block.
152
+ #
153
+ # @param protocol [String] upgraded protocol
154
+ # @param custom_headers [Hash, nil] additional response headers
155
+ # @return [void]
102
156
  def upgrade(protocol, custom_headers = nil, &block)
103
157
  upgrade_headers = {
104
158
  ':status' => HTTP::SWITCHING_PROTOCOLS,
@@ -169,6 +223,11 @@ module Syntropy
169
223
  end
170
224
  end
171
225
 
226
+ # Renders an HTML repsonse with HTML content type.
227
+ #
228
+ # @param html [String] HTML response body
229
+ # @param headers [Hash] additional response headers
230
+ # @return [void]
172
231
  def respond_html(html, **headers)
173
232
  respond(
174
233
  html,
@@ -177,6 +236,11 @@ module Syntropy
177
236
  )
178
237
  end
179
238
 
239
+ # Renders a JSON repsonse with JSON content type.
240
+ #
241
+ # @param obj [any] obj to render into JSON
242
+ # @param headers [Hash] additional response headers
243
+ # @return [void]
180
244
  def respond_json(obj, **headers)
181
245
  respond(
182
246
  JSON.dump(obj),
@@ -185,6 +249,11 @@ module Syntropy
185
249
  )
186
250
  end
187
251
 
252
+ # Renders a nicely-formatted JSON repsonse with JSON content type.
253
+ #
254
+ # @param obj [any] obj to render into JSON
255
+ # @param headers [Hash] additional response headers
256
+ # @return [void]
188
257
  def json_pretty_response(obj, **headers)
189
258
  respond(
190
259
  JSON.pretty_generate(obj),
@@ -18,6 +18,12 @@ module Syntropy
18
18
  raise Syntropy::Error.method_not_allowed
19
19
  end
20
20
 
21
+ # Validates the request's content-type against the given accepted content
22
+ # types. If the content type does not match the given accepted list, raises
23
+ # an error.
24
+ #
25
+ # @param accepted [Array<String>] array of accepted content types
26
+ # @return [String] accepted content type
21
27
  def validate_content_type(*accepted)
22
28
  ct = content_type
23
29
  return ct if accepted.include?(ct)
@@ -8,6 +8,12 @@ module Syntropy
8
8
  class ConnectionPool
9
9
  attr_reader :count
10
10
 
11
+ # Initializes the connection pool.
12
+ #
13
+ # @param machine [UringMachine] machine instance
14
+ # @param fn [String] database filename
15
+ # @param max_conn [Integer] maximum number of connections
16
+ # @return [void]
11
17
  def initialize(machine, fn, max_conn)
12
18
  @machine = machine
13
19
  @fn = fn
@@ -17,6 +23,11 @@ module Syntropy
17
23
  @key = :"connection_pool_#{object_id}"
18
24
  end
19
25
 
26
+ # Checks out a database connection, passing it to the given block. This
27
+ # method is reentrant - called from the same fiber, it will yield the same
28
+ # database connection.
29
+ #
30
+ # @return [Extralite::Database] database connection
20
31
  def with_db
21
32
  if (db = Thread.current[@key])
22
33
  @machine.snooze
@@ -33,14 +44,27 @@ module Syntropy
33
44
  end
34
45
  end
35
46
 
47
+ # Performs the given query by acquiring a database connection and running
48
+ # the query.
49
+ #
50
+ # @param sql [String] SQL query
51
+ # @return [Array<Hash>] result rows
36
52
  def query(sql, *, **, &)
37
53
  with_db { it.query(sql, *, **, &) }
38
54
  end
39
55
 
56
+ # Executes the given query by acquiring a database connection and running
57
+ # the query.
58
+ #
59
+ # @param sql [String] SQL query
60
+ # @return [Integer] number of changed rows
40
61
  def execute(sql, *, **)
41
62
  with_db { it.execute(sql, *, **) }
42
63
  end
43
64
 
65
+ # Closes the connection pool by removing all connections.
66
+ #
67
+ # @return [void]
44
68
  def close
45
69
  while @queue.count > 0
46
70
  db = @machine.shift(@queue)
@@ -51,16 +75,26 @@ module Syntropy
51
75
 
52
76
  private
53
77
 
78
+ # Checks out a connection from the pool.
79
+ #
80
+ # @return [Extralite::Database] database connection
54
81
  def checkout
55
82
  return make_db_instance if @queue.count == 0 && @count < @max_conn
56
83
 
57
84
  @machine.shift(@queue)
58
85
  end
59
86
 
87
+ # Checks in a connection to the pool.
88
+ #
89
+ # @param db [Extralite::Database] database connection
90
+ # @return [void]
60
91
  def checkin(db)
61
92
  @machine.push(@queue, db)
62
93
  end
63
94
 
95
+ # Creates a database connection.
96
+ #
97
+ # @return [Extralite::Database] database connection
64
98
  def make_db_instance
65
99
  Extralite::Database.new(@fn, wal: true).tap do
66
100
  @count += 1
data/lib/syntropy/test.rb CHANGED
@@ -19,7 +19,7 @@ module Syntropy
19
19
  def global_env
20
20
  @@global_env
21
21
  end
22
-
22
+
23
23
  # Gets/sets app environment for tests
24
24
  attr_accessor :env
25
25
  end
@@ -38,8 +38,8 @@ module Syntropy
38
38
  #
39
39
  # @param ref [String] module reference
40
40
  # @return [any] module
41
- def load_module(ref, raise_on_missing: true)
42
- app.module_loader.load(ref, raise_on_missing:)
41
+ def load_module(ref, raise_on_missing_export: true)
42
+ app.module_loader.load(ref, raise_on_missing_export:)
43
43
  end
44
44
 
45
45
  # Makes an HTTP request to the test app.
@@ -145,7 +145,7 @@ module Syntropy
145
145
  )
146
146
  @test_harness = Syntropy::TestHarness.new(@app)
147
147
 
148
- @db = load_module('/_lib/storage', raise_on_missing: false)
148
+ @db = load_module('/_lib/storage', raise_on_missing_export: false)
149
149
  @db&.migrate! if @db.respond_to?(:migrate!)
150
150
  end
151
151
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.39.0'
4
+ VERSION = '0.41.0'
5
5
  end
data/lib/syntropy.rb CHANGED
@@ -72,6 +72,12 @@ module Syntropy
72
72
  end
73
73
  end
74
74
 
75
+ # Loads the app config module based on the environment's config root and
76
+ # mode: (test, dev, production). The module's exported value is assigned to
77
+ # env[:config].
78
+ #
79
+ # @param env [Hash] app environment
80
+ # @return [void]
75
81
  def load_config(env)
76
82
  return if !env[:config_root]
77
83
 
@@ -81,7 +87,7 @@ module Syntropy
81
87
  machine: UM.new(size: 8)
82
88
  )
83
89
  loader = ModuleLoader.new(loader_env)
84
- if (config = loader.load(env[:mode], raise_on_missing: false))
90
+ if (config = loader.load(env[:mode], raise_on_missing_export: false))
85
91
  env[:config] = config
86
92
  end
87
93
  end
data/syntropy.gemspec CHANGED
@@ -21,9 +21,9 @@ Gem::Specification.new do |s|
21
21
  s.required_ruby_version = '>= 3.4'
22
22
  s.executables = ['syntropy']
23
23
 
24
- s.add_dependency 'extralite', '~>2.14'
25
- s.add_dependency 'papercraft', '~>3.2.0'
26
- s.add_dependency 'uringmachine', '~>1.0.2'
24
+ s.add_dependency 'extralite', '~>3.0.1'
25
+ s.add_dependency 'papercraft', '~>3.2.2'
26
+ s.add_dependency 'uringmachine', '~>1.0.4'
27
27
 
28
28
  s.add_dependency 'json'
29
29
  s.add_dependency 'base64'
@@ -31,6 +31,6 @@ Gem::Specification.new do |s|
31
31
  s.add_dependency 'irb'
32
32
  s.add_dependency 'minitest'
33
33
 
34
- s.add_development_dependency 'rake', '~>13.3.1'
34
+ s.add_development_dependency 'rake'
35
35
  s.add_development_dependency 'solargraph'
36
36
  end
@@ -60,7 +60,7 @@ class MarkdownControllerTest < Minitest::Test
60
60
  @controller = nil
61
61
  @test_harness = Syntropy::TestHarness.new(->(req) { @controller.(req) })
62
62
  end
63
-
63
+
64
64
  def test_markdown_renderer_no_layout
65
65
  md = <<~MD
66
66
  foo *bar*
@@ -230,8 +230,8 @@ class MarkdownControllerTest < Minitest::Test
230
230
  md = <<~MD
231
231
  ```ruby
232
232
  # render: true
233
- @env[:count] ||= 0
234
- h3 (@env[:count] += 1)
233
+ @count ||= 0
234
+ h3 (@count += 1)
235
235
  ```
236
236
  bar
237
237
  MD
@@ -16,7 +16,10 @@ class ModuleLoaderTest < Minitest::Test
16
16
  assert_equal 42, mod.bar
17
17
 
18
18
  assert_raises(Syntropy::Error) { @loader.load('_lib/missing-module') }
19
- assert_raises(Syntropy::Error) { @loader.load('_lib/missing-export') }
19
+
20
+ mod = @loader.load('_lib/missing-export')
21
+ assert_kind_of Syntropy::ModuleContext, mod
22
+ assert_equal :bar, mod.call
20
23
 
21
24
  mod = @loader.load('_lib/callable')
22
25
  assert_kind_of Syntropy::ModuleContext, mod