ratalada 3.0.0 → 4.0.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.
- checksums.yaml +4 -4
- data/README.md +5 -0
- data/lib/ratalada/async.rb +148 -0
- data/lib/ratalada/falcon.rb +12 -3
- data/lib/ratalada/protocol_http_ext.rb +44 -0
- data/lib/ratalada/version.rb +1 -1
- data/lib/ratalada/webrick.rb +28 -0
- data/lib/ratalada.rb +28 -5
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1da8d4c6eff98bad680c6e3da80c2d452b54711c1d2e2fe5b9de11500640f0a9
|
|
4
|
+
data.tar.gz: 9fae4c8b7aa84cc4ccf69017779f24fb011491a7143c3abb9332483db25489e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6af7b845bd465ede4788967478ad9e67be55e3a2708000f2678a3a4ded46d3b024778d2828bb546e01f9e618fd8a43c1566bf46bdac2b1dff760d65105f3892a
|
|
7
|
+
data.tar.gz: '08bc22a824af6bacdfadc7b6546d0b8c9f116b22c8efc358f4cd7bc97cbe7652a59f23a9beb44616e87b5a3ae8f90a22628d02a1fd6dbf9b7460533336b137c1'
|
data/README.md
CHANGED
|
@@ -253,6 +253,11 @@ gem kit changelog --write --gem ratalada-roda
|
|
|
253
253
|
gem kit release --gem ratalada-roda # gate, build, push, tag
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
+
## Why
|
|
257
|
+
|
|
258
|
+
Spurred on by things like [this post](https://blainsmith.com/articles/just-fucking-use-go/), and just how easy it is to create servers with [hono](https://hono.dev/) or agents with [volt](https://github.com/voltagent/voltagent), which makes me irrationally angry that ruby hasn't more libraries like this considering it's WELL beyond javascript in it's flexibility as a language.
|
|
259
|
+
[This post is great](https://www.toptal.com/developers/ruby/api-with-sinatra-and-sequel-ruby-tutorial), but it shuold be even simpler.
|
|
260
|
+
|
|
256
261
|
## License
|
|
257
262
|
|
|
258
263
|
[MIT](LICENSE)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/http/server"
|
|
4
|
+
require "async/http/endpoint"
|
|
5
|
+
require "protocol/http"
|
|
6
|
+
require "protocol/url"
|
|
7
|
+
require "protocol/rack/adapter"
|
|
8
|
+
require_relative "protocol_http_ext"
|
|
9
|
+
require_relative "../ratalada"
|
|
10
|
+
|
|
11
|
+
module Ratalada
|
|
12
|
+
module Frontends
|
|
13
|
+
# Frontends::Routes, in Protocol::HTTP terms: the same Server.run block,
|
|
14
|
+
# the same handler kinds, no rack in the middle. Requiring ratalada/async
|
|
15
|
+
# installs it in place of the rack router, because Async::HTTP::Server
|
|
16
|
+
# already speaks this protocol and the adapter between them exists only to
|
|
17
|
+
# translate for a frontend that does not.
|
|
18
|
+
#
|
|
19
|
+
# The block is handed the server's own Protocol::HTTP::Request. It sees
|
|
20
|
+
# the same [verb, path] and {verb:, path:, query:} it sees on any other
|
|
21
|
+
# backend (ratalada/protocol_http_ext teaches that class the patterns),
|
|
22
|
+
# and #body is the body as it arrived rather than a String read out of it.
|
|
23
|
+
# What it does not see is Rack::Request: no cookies, no session, no
|
|
24
|
+
# form-body params, no Rack::Utils helpers in scope. Query params are
|
|
25
|
+
# there as #params; the rest is rack's, so require a rack frontend --
|
|
26
|
+
# ratalada/sinatra, or Frontends::Routes back again -- if you want them.
|
|
27
|
+
module ProtocolRoutes
|
|
28
|
+
# Tells the async backend to hand the app to Async::HTTP::Server whole
|
|
29
|
+
# rather than wrapping it in Protocol::Rack::Adapter.
|
|
30
|
+
def self.rack? = false
|
|
31
|
+
|
|
32
|
+
def self.build(block) = App.new(block)
|
|
33
|
+
|
|
34
|
+
# Server.use needs nothing special here: it chains
|
|
35
|
+
# `klass.new(inner, *args)`, which is exactly
|
|
36
|
+
# Protocol::HTTP::Middleware's interface -- `new(delegate)` answering
|
|
37
|
+
# `call(request)`. So middleware on this frontend is Protocol::HTTP
|
|
38
|
+
# middleware, chained with no adapter anywhere. Rack middleware belongs
|
|
39
|
+
# to a rack frontend, where the backend adapts the lot in one go.
|
|
40
|
+
|
|
41
|
+
class App
|
|
42
|
+
def initialize(router)
|
|
43
|
+
@router = router
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The request is the server's own Protocol::HTTP::Request, handed to
|
|
47
|
+
# the router as it arrived -- it patterns on [verb, path] and carries
|
|
48
|
+
# #params, #headers and a #body that streams, so there is nothing for
|
|
49
|
+
# a wrapper to add.
|
|
50
|
+
def call(request)
|
|
51
|
+
begin
|
|
52
|
+
handler = @router.call(request)
|
|
53
|
+
rescue NoMatchingPatternError
|
|
54
|
+
handler = nil
|
|
55
|
+
end
|
|
56
|
+
respond(handler, request)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Middleware#close delegates down the chain on shutdown, and this is
|
|
60
|
+
# the bottom of it.
|
|
61
|
+
def close = nil
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Callable is #call, not Proc -- a handler object with a #call is as
|
|
66
|
+
# callable as a lambda. It is tested last for a reason that bites
|
|
67
|
+
# here specifically: Body::Readable#call(stream) is how a body
|
|
68
|
+
# writes itself out, so a Readable answers #call. Tested any earlier
|
|
69
|
+
# and `->(req) { req.body }` would have that body called with the
|
|
70
|
+
# request instead of becoming the response.
|
|
71
|
+
def respond(handler, request)
|
|
72
|
+
case handler
|
|
73
|
+
when nil
|
|
74
|
+
::Protocol::HTTP::Response[404, { "content-type" => "text/plain" }, ["not found"]]
|
|
75
|
+
when String
|
|
76
|
+
::Protocol::HTTP::Response[200, { "content-type" => "text/plain" }, [handler]]
|
|
77
|
+
# A body handed back as-is -- most usefully the request's own,
|
|
78
|
+
# which makes an echo or a proxy a pass-through, nothing buffered.
|
|
79
|
+
when ::Protocol::HTTP::Body::Readable
|
|
80
|
+
::Protocol::HTTP::Response[200, {}, handler]
|
|
81
|
+
when Array
|
|
82
|
+
status, headers, body = handler
|
|
83
|
+
::Protocol::HTTP::Response[status, headers, body.is_a?(String) ? [body] : body]
|
|
84
|
+
else
|
|
85
|
+
if handler.respond_to?(:call)
|
|
86
|
+
respond(handler.call(request), request)
|
|
87
|
+
else
|
|
88
|
+
handler
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
module Backends
|
|
97
|
+
# Falcon without falcon: one Async::HTTP::Server in this process, no
|
|
98
|
+
# container, no supervision, no restarts. Use ratalada/falcon when you
|
|
99
|
+
# want those; this is the fiber-based server on its own.
|
|
100
|
+
#
|
|
101
|
+
# Named for the server it runs rather than the gem, because `Async` here
|
|
102
|
+
# would shadow the async gem for every sibling in this namespace: an
|
|
103
|
+
# unqualified `Async::Service` inside Backends::Falcon resolved to
|
|
104
|
+
# `Ratalada::Backends::Async::Service` and broke that backend outright.
|
|
105
|
+
module AsyncHTTP
|
|
106
|
+
module_function
|
|
107
|
+
|
|
108
|
+
def run(app, host:, port:, count: 1)
|
|
109
|
+
if count > 1
|
|
110
|
+
warn "ratalada: async backend ignores count: (single process — use ratalada/falcon)"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
endpoint = ::Async::HTTP::Endpoint.parse("http://#{host}:#{port}")
|
|
114
|
+
server = ::Async::HTTP::Server.new(adapt(app), endpoint)
|
|
115
|
+
|
|
116
|
+
warn "ratalada: async listening on http://#{host}:#{port}"
|
|
117
|
+
# #run wraps itself in Async{}, which outside a reactor starts one and
|
|
118
|
+
# blocks until the accept loop ends.
|
|
119
|
+
server.run
|
|
120
|
+
rescue Interrupt
|
|
121
|
+
# clean shutdown
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Async::HTTP::Server speaks Protocol::HTTP natively, so the adapter is
|
|
125
|
+
# only in the way when the frontend does too -- which ProtocolRoutes,
|
|
126
|
+
# installed below, does. Every other frontend (Routes, Builder, Sinatra,
|
|
127
|
+
# Grape, Hanami, Roda) is rack and gets wrapped, without having to
|
|
128
|
+
# declare anything: Ratalada.rack_frontend? assumes rack unless a
|
|
129
|
+
# frontend says `def self.rack? = false`.
|
|
130
|
+
def adapt(app)
|
|
131
|
+
if Ratalada.rack_frontend?
|
|
132
|
+
::Protocol::Rack::Adapter.new(app)
|
|
133
|
+
else
|
|
134
|
+
app
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
config.backend = Backends::AsyncHTTP
|
|
141
|
+
|
|
142
|
+
# Only the default is replaced. A frontend require sets config.frontend
|
|
143
|
+
# itself, so `require "ratalada/sinatra"` on either side of this one keeps
|
|
144
|
+
# Sinatra (and its rack adapter) rather than racing with it.
|
|
145
|
+
if config.frontend == Frontends::Routes
|
|
146
|
+
config.frontend = Frontends::ProtocolRoutes
|
|
147
|
+
end
|
|
148
|
+
end
|
data/lib/ratalada/falcon.rb
CHANGED
|
@@ -9,6 +9,12 @@ require_relative "../ratalada"
|
|
|
9
9
|
|
|
10
10
|
module Ratalada
|
|
11
11
|
module Backends
|
|
12
|
+
# Every Async:: below is rooted at ::. The async backend used to be
|
|
13
|
+
# Backends::Async, and an unqualified lookup found that sibling first:
|
|
14
|
+
# `Async::Service` resolved to `Ratalada::Backends::Async::Service` and
|
|
15
|
+
# took this backend down with it in any process that had required both.
|
|
16
|
+
# It is Backends::AsyncHTTP now, so nothing shadows the gem -- the `::`
|
|
17
|
+
# stays so the next sibling cannot do it again.
|
|
12
18
|
module Falcon
|
|
13
19
|
module_function
|
|
14
20
|
|
|
@@ -26,7 +32,7 @@ module Ratalada
|
|
|
26
32
|
def run(app, host:, port:, count: 1, cache: false)
|
|
27
33
|
middleware = ::Falcon::Server.rack_middleware(app, cache: cache)
|
|
28
34
|
|
|
29
|
-
environment = Async::Service::Environment.new(::Falcon::Environment::Server).with(
|
|
35
|
+
environment = ::Async::Service::Environment.new(::Falcon::Environment::Server).with(
|
|
30
36
|
name: "ratalada",
|
|
31
37
|
url: "http://#{host}:#{port}",
|
|
32
38
|
middleware: -> { middleware },
|
|
@@ -37,7 +43,7 @@ module Ratalada
|
|
|
37
43
|
root: Dir.pwd,
|
|
38
44
|
)
|
|
39
45
|
|
|
40
|
-
configuration = Async::Service::Configuration.new
|
|
46
|
+
configuration = ::Async::Service::Configuration.new
|
|
41
47
|
configuration.add(environment)
|
|
42
48
|
|
|
43
49
|
if count > 1
|
|
@@ -47,7 +53,10 @@ module Ratalada
|
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
warn "ratalada: falcon listening on http://#{host}:#{port}#{workers}"
|
|
50
|
-
Async::Service::Controller.run(
|
|
56
|
+
::Async::Service::Controller.run(
|
|
57
|
+
configuration,
|
|
58
|
+
container_class: ::Async::Container.best_container_class,
|
|
59
|
+
)
|
|
51
60
|
rescue Interrupt
|
|
52
61
|
# clean shutdown
|
|
53
62
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "protocol/http"
|
|
4
|
+
require "protocol/url"
|
|
5
|
+
|
|
6
|
+
# Pattern matching for Protocol::HTTP::Request, so the router matches the
|
|
7
|
+
# request the server actually built instead of a wrapper standing in for it.
|
|
8
|
+
# `case request in ["GET", "/"]` needs #deconstruct and the class has none --
|
|
9
|
+
# unpatched, every pattern raises NoMatchingPatternError and every route 404s.
|
|
10
|
+
#
|
|
11
|
+
# Additions only: #path still returns the raw target ("/users/1?a=b"), which
|
|
12
|
+
# is what protocol-http1 writes and what anything else reading a request
|
|
13
|
+
# expects. The split path is what the patterns see, because a route is a path
|
|
14
|
+
# and "/hi" should match a request for "/hi?name=x".
|
|
15
|
+
module Protocol
|
|
16
|
+
module HTTP
|
|
17
|
+
class Request
|
|
18
|
+
# The class overrides #method as a reader for the verb, so there is no
|
|
19
|
+
# Object#method here to collide with -- and `in {verb:}` reads better
|
|
20
|
+
# than `in {method:}` would.
|
|
21
|
+
def verb = self.method
|
|
22
|
+
|
|
23
|
+
def reference = @reference ||= ::Protocol::URL::Reference.parse(path)
|
|
24
|
+
|
|
25
|
+
# "" rather than nil with no query string, so `in {query: ""}` means the
|
|
26
|
+
# same here as on the rack router.
|
|
27
|
+
def query = reference.query || ""
|
|
28
|
+
|
|
29
|
+
def params
|
|
30
|
+
if query.empty?
|
|
31
|
+
@params ||= {}
|
|
32
|
+
else
|
|
33
|
+
@params ||= ::Protocol::URL::Encoding.decode(query)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Enables `in ["GET", "/"]`
|
|
38
|
+
def deconstruct = [verb, reference.path.to_s]
|
|
39
|
+
|
|
40
|
+
# Enables `in {verb: "GET", path: "/users"}`
|
|
41
|
+
def deconstruct_keys(_keys) = { verb:, path: reference.path.to_s, query:, params:, headers:, body: }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/ratalada/version.rb
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rackup/handler/webrick"
|
|
4
|
+
require_relative "../ratalada"
|
|
5
|
+
|
|
6
|
+
module Ratalada
|
|
7
|
+
module Backends
|
|
8
|
+
# Stdlib WEBrick via rackup's handler, which already does the rack env
|
|
9
|
+
# translation. Single process, one thread per connection — fine for
|
|
10
|
+
# development and tests, not what you ship.
|
|
11
|
+
module Webrick
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def run(app, host:, port:, count: 1)
|
|
15
|
+
if count > 1
|
|
16
|
+
warn "ratalada: webrick backend ignores count: (single process)"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
warn "ratalada: webrick listening on http://#{host}:#{port}"
|
|
20
|
+
::Rackup::Handler::WEBrick.run(app, Host: host, Port: port)
|
|
21
|
+
rescue Interrupt
|
|
22
|
+
::Rackup::Handler::WEBrick.shutdown
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
config.backend = Backends::Webrick
|
|
28
|
+
end
|
data/lib/ratalada.rb
CHANGED
|
@@ -18,8 +18,10 @@ module Ratalada
|
|
|
18
18
|
super(msg || <<~MSG)
|
|
19
19
|
No server backend selected. Require one before calling Server.run:
|
|
20
20
|
|
|
21
|
-
require "ratalada/puma"
|
|
22
|
-
require "ratalada/falcon"
|
|
21
|
+
require "ratalada/puma" # or
|
|
22
|
+
require "ratalada/falcon" # or
|
|
23
|
+
require "ratalada/async" # or
|
|
24
|
+
require "ratalada/webrick"
|
|
23
25
|
MSG
|
|
24
26
|
end
|
|
25
27
|
end
|
|
@@ -34,6 +36,15 @@ module Ratalada
|
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def frontend = config.frontend
|
|
39
|
+
|
|
40
|
+
# A frontend is rack unless it says otherwise (`def self.rack? = false`),
|
|
41
|
+
# so every existing one counts without declaring anything. The async
|
|
42
|
+
# backend reads this to decide whether its app needs Protocol::Rack::Adapter.
|
|
43
|
+
def rack_frontend?
|
|
44
|
+
frontend = config.frontend
|
|
45
|
+
|
|
46
|
+
!(frontend.respond_to?(:rack?) && !frontend.rack?)
|
|
47
|
+
end
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
# Rack::Request plus just enough sugar to pattern match on, so everything
|
|
@@ -101,19 +112,25 @@ module Ratalada
|
|
|
101
112
|
|
|
102
113
|
private
|
|
103
114
|
|
|
115
|
+
# Callable is #call, not Proc -- a handler object with a #call is as
|
|
116
|
+
# callable as a lambda, and was being handed back as the response.
|
|
117
|
+
# It is tested last, after every shape that is already a response,
|
|
118
|
+
# because some of those answer #call too for reasons of their own.
|
|
104
119
|
def respond(handler, request)
|
|
105
120
|
case handler
|
|
106
121
|
when nil
|
|
107
122
|
[404, { "content-type" => "text/plain" }, ["not found"]]
|
|
108
|
-
when Proc, Method
|
|
109
|
-
respond(handler.call(request), request)
|
|
110
123
|
when String
|
|
111
124
|
[200, { "content-type" => "text/plain" }, [handler]]
|
|
112
125
|
when Array
|
|
113
126
|
status, headers, body = handler
|
|
114
127
|
[status, headers, body.is_a?(String) ? [body] : body]
|
|
115
128
|
else
|
|
116
|
-
handler
|
|
129
|
+
if handler.respond_to?(:call)
|
|
130
|
+
respond(handler.call(request), request)
|
|
131
|
+
else
|
|
132
|
+
handler
|
|
133
|
+
end
|
|
117
134
|
end
|
|
118
135
|
end
|
|
119
136
|
end
|
|
@@ -177,6 +194,12 @@ module Ratalada
|
|
|
177
194
|
end
|
|
178
195
|
|
|
179
196
|
# First `use` in the chain is the outermost, as in Rack::Builder.
|
|
197
|
+
#
|
|
198
|
+
# `klass.new(inner, *args)` is rack's middleware contract and
|
|
199
|
+
# Protocol::HTTP::Middleware's alike -- `new(delegate)`, then `call`
|
|
200
|
+
# with whatever that frontend passes -- so one chain serves both: rack
|
|
201
|
+
# middleware on a rack frontend, Protocol::HTTP middleware on
|
|
202
|
+
# Frontends::ProtocolRoutes.
|
|
180
203
|
def to_app(block)
|
|
181
204
|
@middleware.reverse.inject(Ratalada.frontend.build(block)) do |inner, (klass, args, blk)|
|
|
182
205
|
klass.new(inner, *args, &blk)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ratalada
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan K
|
|
@@ -107,6 +107,20 @@ dependencies:
|
|
|
107
107
|
- - ">="
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: webrick
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
110
124
|
- !ruby/object:Gem::Dependency
|
|
111
125
|
name: lefthook
|
|
112
126
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -135,10 +149,13 @@ files:
|
|
|
135
149
|
- README.md
|
|
136
150
|
- exe/ratalada
|
|
137
151
|
- lib/ratalada.rb
|
|
152
|
+
- lib/ratalada/async.rb
|
|
138
153
|
- lib/ratalada/builder.rb
|
|
139
154
|
- lib/ratalada/falcon.rb
|
|
155
|
+
- lib/ratalada/protocol_http_ext.rb
|
|
140
156
|
- lib/ratalada/puma.rb
|
|
141
157
|
- lib/ratalada/version.rb
|
|
158
|
+
- lib/ratalada/webrick.rb
|
|
142
159
|
homepage: https://github.com/n-at-han-k/ratalada
|
|
143
160
|
licenses:
|
|
144
161
|
- MIT
|