syntropy 0.6 → 0.7
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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +135 -4
- data/lib/syntropy/app.rb +5 -5
- data/lib/syntropy/connection_pool.rb +6 -13
- data/lib/syntropy/module.rb +23 -1
- data/lib/syntropy/version.rb +1 -1
- data/test/app/_hook.rb +0 -1
- data/test/app/_layout/default.rb +1 -1
- data/test/app/_lib/self.rb +5 -0
- data/test/app/api+.rb +4 -0
- data/test/app_multi_site/_site.rb +1 -0
- data/test/app_multi_site/bar.baz/index.html +1 -0
- data/test/app_multi_site/foo.bar/index.html +1 -0
- data/test/test_app.rb +33 -0
- data/test/test_module.rb +6 -0
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b86a65bfeb990aa38b76b5bb6487210887e7103bab652e40c3790d68c1ab48df
|
4
|
+
data.tar.gz: 89397a9faf63d2a07c69ab69f93048cb92c82014e30d7ca6ed9399671c17bb48
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f536c81be4baa5c8e8733705a9a41042b322eb3960ee8a092523bfdfff886893a4183a6b00c69c2960daefe6443a6f06ee0e07596eeb5ffc3b45ceceb86c342
|
7
|
+
data.tar.gz: 05c3e6c5d3641847a0c962811e290313f7d08a4233504466035e94cfc03284cf652f990f5fd4681efd059673050fda5d021bba5a982c0e539b8c3351bd316913
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -59,6 +59,7 @@ site/
|
|
59
59
|
├ _articles/
|
60
60
|
| └ 2025-01-01-hello_world.md
|
61
61
|
├ api/
|
62
|
+
| ├ _hook.rb
|
62
63
|
| └ v1.rb
|
63
64
|
├ assets/
|
64
65
|
| ├ css/
|
@@ -73,10 +74,30 @@ site/
|
|
73
74
|
Syntropy knows how to serve static asset files (CSS, JS, images...) as well as
|
74
75
|
render markdown files and run modules written in Ruby.
|
75
76
|
|
77
|
+
Some conventions employed in Syntropy-based web apps:
|
78
|
+
|
79
|
+
- Files and directories starting with an underscore, e.g. `/_layout` are
|
80
|
+
considered private, and are not exposed to HTTP clients.
|
81
|
+
- Normally, a module route only responds to its exact path. To respond to any
|
82
|
+
subtree path, add a plus sign to the end of the module name, e.g. `/api+.rb`.
|
83
|
+
- A `_hook.rb` module is invoked on each request routed to anywhere in the
|
84
|
+
corresponding subtree. For example, a hook defined in `/api/_hook.rb` will be
|
85
|
+
used on requests to `/api`, `/api/foo`, `/api/bar` etc.
|
86
|
+
- As a corollary, each route "inherits" all hooks defined up the tree. For
|
87
|
+
example, a request to `/api/foo` will invoke hooks defined in `/api/_hook.rb`
|
88
|
+
and `/_hook.rb`.
|
89
|
+
- In a similar fashion to hooks, error handlers can be defined for different
|
90
|
+
subtrees in a `_error.rb` module. For each route, in case of an exception,
|
91
|
+
Syntropy will invoke the closest-found error handler module up the tree. For
|
92
|
+
example, an error raised while responding to a request to `/api/foo` will
|
93
|
+
prefer the error handler in `/api/_error.rb`, rather than `/_error.rb`.
|
94
|
+
- The Syntrpy router accepts clean URLs for Ruby modules and Markdown files. It
|
95
|
+
also accepts clean URLs for `index.html` files.
|
96
|
+
|
76
97
|
## What does a Syntropic Ruby module look like?
|
77
98
|
|
78
|
-
Consider `archive.rb` in the
|
79
|
-
and render it
|
99
|
+
Consider `site/archive.rb` in the file tree above. We want to get a list of
|
100
|
+
articles and render it using the given layout:
|
80
101
|
|
81
102
|
```ruby
|
82
103
|
# archive.rb
|
@@ -97,7 +118,7 @@ export @@layout.apply(title: 'archive') {
|
|
97
118
|
}
|
98
119
|
```
|
99
120
|
|
100
|
-
But a module can be something completely different:
|
121
|
+
But a module can also be something completely different:
|
101
122
|
|
102
123
|
```ruby
|
103
124
|
# api/v1.rb
|
@@ -121,4 +142,114 @@ export APIV1
|
|
121
142
|
```
|
122
143
|
|
123
144
|
Basically, the exported value can be a template, a callable or a class that
|
124
|
-
responds to the request.
|
145
|
+
responds to the request. Here's a minimal module that responds with a hello
|
146
|
+
world:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
export ->(req) { req.respond('Hello, world!') }
|
150
|
+
```
|
151
|
+
|
152
|
+
## Module Export / Import
|
153
|
+
|
154
|
+
Modules communicate with the Syntropy framework and with other modules using
|
155
|
+
`export` and `import`. Each module must export a single object, which can be a
|
156
|
+
controller class, a callable (a proc/closure) or a template. The exported object
|
157
|
+
is used by Syntropy as the entrypoint for the route.
|
158
|
+
|
159
|
+
But modules can also import other modules. This permits the use of layouts:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
# site/_layout/default.rb
|
163
|
+
export template { |**props|
|
164
|
+
header {
|
165
|
+
h1 'Foo'
|
166
|
+
}
|
167
|
+
content {
|
168
|
+
emit_yield(**props)
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
# site/index.rb
|
173
|
+
layout = import '_layout/default'
|
174
|
+
|
175
|
+
export layout.apply { |**props|
|
176
|
+
p 'o hi!'
|
177
|
+
}
|
178
|
+
```
|
179
|
+
|
180
|
+
A module can also be written as a set of methods without any explicit class
|
181
|
+
definition. This allows writing modules in a more functional style:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
# site/_lib/utils.rb
|
185
|
+
|
186
|
+
def foo
|
187
|
+
42
|
188
|
+
end
|
189
|
+
|
190
|
+
export self
|
191
|
+
|
192
|
+
# site/index.rb
|
193
|
+
Utils = import '_lib/utils'
|
194
|
+
|
195
|
+
export template {
|
196
|
+
h1 "foo = #{Utils.foo}"
|
197
|
+
}
|
198
|
+
```
|
199
|
+
|
200
|
+
## Hooks (a.k.a. Middleware)
|
201
|
+
|
202
|
+
A hook is a piece of code that can intercept HTTP requests before they are
|
203
|
+
passed off to the correspending route. Hooks are applied to the subtree of the
|
204
|
+
directory in which they reside.
|
205
|
+
|
206
|
+
Hooks can be used for a variety of purposes:
|
207
|
+
|
208
|
+
- Parameter validation
|
209
|
+
- Authentication, authorization & session management
|
210
|
+
- Logging
|
211
|
+
- Request rewriting / redirecting
|
212
|
+
|
213
|
+
When multiple hooks are defined up the tree for a particular route, they are
|
214
|
+
chained together such that each hook is invoked starting from the file tree root
|
215
|
+
and down to the route path.
|
216
|
+
|
217
|
+
Hooks are implemented as modules named `_hook.rb`, that export procs (or
|
218
|
+
callables) with the following signature:
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
# **/_hook.rb
|
222
|
+
export ->(req, app) { ... }
|
223
|
+
```
|
224
|
+
|
225
|
+
... where req is the request object, and app is the callable that code. Here's
|
226
|
+
an example of an authorization hook:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
export ->(req, app) {
|
230
|
+
if (!req.cookies[:session_id])
|
231
|
+
req.redirect('/signin')
|
232
|
+
else
|
233
|
+
app.(req)
|
234
|
+
end
|
235
|
+
}
|
236
|
+
```
|
237
|
+
|
238
|
+
## Error handlers
|
239
|
+
|
240
|
+
An error handler can be defined separately for each subtree. When an exception
|
241
|
+
is raised that is not rescued by the application code, Syntropy will look for an
|
242
|
+
error handler up the file tree, and will invoke the first error handler found.
|
243
|
+
|
244
|
+
Error handlers are implemented as modules named `_error.rb`, that export procs (or
|
245
|
+
callables) with the following signature:
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
# **/_error.rb
|
249
|
+
->(req, err) { ... }
|
250
|
+
```
|
251
|
+
|
252
|
+
Using different error handlers for parts of the route tree allows different
|
253
|
+
error responses for each route. For example, the error response for an API route
|
254
|
+
can be a JSON object, while the error response for a browser page route can be a
|
255
|
+
custom HTML page.
|
data/lib/syntropy/app.rb
CHANGED
@@ -32,20 +32,20 @@ module Syntropy
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
def initialize(machine,
|
35
|
+
def initialize(machine, location, mount_path, opts = {})
|
36
36
|
@machine = machine
|
37
|
-
@
|
37
|
+
@location = File.expand_path(location)
|
38
38
|
@mount_path = mount_path
|
39
39
|
@opts = opts
|
40
40
|
|
41
|
-
@module_loader = Syntropy::ModuleLoader.new(@
|
41
|
+
@module_loader = Syntropy::ModuleLoader.new(@location, @opts)
|
42
42
|
@router = Syntropy::Router.new(@opts, @module_loader)
|
43
43
|
|
44
44
|
@machine.spin do
|
45
45
|
# we do startup stuff asynchronously, in order to first let TP2 do its
|
46
46
|
# setup tasks
|
47
47
|
@machine.sleep 0.15
|
48
|
-
@opts[:logger]&.call("Serving from #{File.expand_path(@
|
48
|
+
@opts[:logger]&.call("Serving from #{File.expand_path(@location)}")
|
49
49
|
@router.start_file_watcher if opts[:watch_files]
|
50
50
|
end
|
51
51
|
end
|
@@ -135,7 +135,7 @@ module Syntropy
|
|
135
135
|
end
|
136
136
|
|
137
137
|
def load_module(entry)
|
138
|
-
ref = entry[:fn].gsub(%r{^#{@
|
138
|
+
ref = entry[:fn].gsub(%r{^#{@location}/}, '').gsub(/\.rb$/, '')
|
139
139
|
o = @module_loader.load(ref)
|
140
140
|
o.is_a?(Papercraft::Template) ? wrap_template(o) : o
|
141
141
|
rescue Exception => e
|
@@ -34,9 +34,7 @@ module Syntropy
|
|
34
34
|
private
|
35
35
|
|
36
36
|
def checkout
|
37
|
-
if @queue.count == 0 && @count < @max_conn
|
38
|
-
return create_db
|
39
|
-
end
|
37
|
+
return make_db_instance if @queue.count == 0 && @count < @max_conn
|
40
38
|
|
41
39
|
@machine.shift(@queue)
|
42
40
|
end
|
@@ -45,16 +43,11 @@ module Syntropy
|
|
45
43
|
@machine.push(@queue, db)
|
46
44
|
end
|
47
45
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
54
|
-
|
55
|
-
def setup_db(db)
|
56
|
-
# setup WAL, sync
|
57
|
-
# setup concurrency stuff
|
46
|
+
def make_db_instance
|
47
|
+
Extralite::Database.new(@fn, wal: true).tap do
|
48
|
+
@count += 1
|
49
|
+
it.on_progress(mode: :at_least_once, period: 320, tick: 10) { @machine.snooze }
|
50
|
+
end
|
58
51
|
end
|
59
52
|
end
|
60
53
|
end
|
data/lib/syntropy/module.rb
CHANGED
@@ -33,6 +33,7 @@ module Syntropy
|
|
33
33
|
mod_body = IO.read(fn)
|
34
34
|
mod_ctx = Class.new(Syntropy::Module)
|
35
35
|
mod_ctx.loader = self
|
36
|
+
mod_ctx.env = @env
|
36
37
|
mod_ctx.module_eval(mod_body, fn, 1)
|
37
38
|
|
38
39
|
export_value = mod_ctx.__export_value__
|
@@ -66,6 +67,10 @@ module Syntropy
|
|
66
67
|
@loader = loader
|
67
68
|
end
|
68
69
|
|
70
|
+
def self.env=(env)
|
71
|
+
@env = env
|
72
|
+
end
|
73
|
+
|
69
74
|
def self.import(ref)
|
70
75
|
@loader.load(ref)
|
71
76
|
end
|
@@ -74,12 +79,29 @@ module Syntropy
|
|
74
79
|
@__export_value__ = ref
|
75
80
|
end
|
76
81
|
|
77
|
-
def self.
|
82
|
+
def self.template(&block)
|
78
83
|
Papercraft.html(&block)
|
79
84
|
end
|
80
85
|
|
86
|
+
def self.route_by_host
|
87
|
+
root = @env[:location]
|
88
|
+
sites = Dir[File.join(root, '*')]
|
89
|
+
.select { File.directory?(it) }
|
90
|
+
.inject({}) { |h, fn|
|
91
|
+
name = File.basename(fn)
|
92
|
+
opts = @env.merge(location: fn)
|
93
|
+
h[name] = Syntropy::App.new(opts[:machine], opts[:location], opts[:mount_path], opts)
|
94
|
+
h
|
95
|
+
}
|
96
|
+
->(req) {
|
97
|
+
site = sites[req.host]
|
98
|
+
site ? site.call(req) : req.respond(nil, ':status' => Status::BAD_REQUEST)
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
81
102
|
def self.__export_value__
|
82
103
|
@__export_value__
|
83
104
|
end
|
105
|
+
|
84
106
|
end
|
85
107
|
end
|
data/lib/syntropy/version.rb
CHANGED
data/test/app/_hook.rb
CHANGED
data/test/app/_layout/default.rb
CHANGED
data/test/app/api+.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
export route_by_host
|
@@ -0,0 +1 @@
|
|
1
|
+
<h1>bar.baz</h1>
|
@@ -0,0 +1 @@
|
|
1
|
+
<h1>foo.bar</h1>
|
data/test/test_app.rb
CHANGED
@@ -180,3 +180,36 @@ class CustomAppTest < Minitest::Test
|
|
180
180
|
assert_equal Status::TEAPOT, req.response_status
|
181
181
|
end
|
182
182
|
end
|
183
|
+
|
184
|
+
class MultiSiteAppTest < Minitest::Test
|
185
|
+
Status = Qeweney::Status
|
186
|
+
|
187
|
+
APP_ROOT = File.join(__dir__, 'app_multi_site')
|
188
|
+
|
189
|
+
def setup
|
190
|
+
@machine = UM.new
|
191
|
+
@app = Syntropy::App.load(
|
192
|
+
machine: @machine,
|
193
|
+
location: APP_ROOT,
|
194
|
+
mount_path: '/'
|
195
|
+
)
|
196
|
+
end
|
197
|
+
|
198
|
+
def make_request(*, **)
|
199
|
+
req = mock_req(*, **)
|
200
|
+
@app.call(req)
|
201
|
+
req
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_route_by_host
|
205
|
+
req = make_request(':method' => 'GET', ':path' => '/', 'host' => 'blah')
|
206
|
+
assert_nil req.response_body
|
207
|
+
assert_equal Status::BAD_REQUEST, req.response_status
|
208
|
+
|
209
|
+
req = make_request(':method' => 'GET', ':path' => '/', 'host' => 'foo.bar')
|
210
|
+
assert_equal '<h1>foo.bar</h1>', req.response_body.chomp
|
211
|
+
|
212
|
+
req = make_request(':method' => 'GET', ':path' => '/', 'host' => 'bar.baz')
|
213
|
+
assert_equal '<h1>bar.baz</h1>', req.response_body.chomp
|
214
|
+
end
|
215
|
+
end
|
data/test/test_module.rb
CHANGED
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.
|
4
|
+
version: '0.7'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
@@ -207,6 +207,7 @@ files:
|
|
207
207
|
- test/app/_lib/callable.rb
|
208
208
|
- test/app/_lib/klass.rb
|
209
209
|
- test/app/_lib/missing-export.rb
|
210
|
+
- test/app/_lib/self.rb
|
210
211
|
- test/app/about/_error.rb
|
211
212
|
- test/app/about/foo.md
|
212
213
|
- test/app/about/index.rb
|
@@ -218,6 +219,9 @@ files:
|
|
218
219
|
- test/app/index.html
|
219
220
|
- test/app/tmp.rb
|
220
221
|
- test/app_custom/_site.rb
|
222
|
+
- test/app_multi_site/_site.rb
|
223
|
+
- test/app_multi_site/bar.baz/index.html
|
224
|
+
- test/app_multi_site/foo.bar/index.html
|
221
225
|
- test/helper.rb
|
222
226
|
- test/run.rb
|
223
227
|
- test/test_app.rb
|