ratalada 3.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '079378d0ea4867145471fa730a8d2eb6ec20513d50e7bfcb52b643cd99bbf6e2'
4
- data.tar.gz: 228276a9a88b32c152a4e84eb12f267f62b8cde567d7d9dab9fae4734e207ab9
3
+ metadata.gz: 1da8d4c6eff98bad680c6e3da80c2d452b54711c1d2e2fe5b9de11500640f0a9
4
+ data.tar.gz: 9fae4c8b7aa84cc4ccf69017779f24fb011491a7143c3abb9332483db25489e5
5
5
  SHA512:
6
- metadata.gz: 5fa8af50cdf2da52fcc5e35d4a8f9acdd97307a6b1c5a7afd09d2971889331ec7af9f16a9472991db768fc71bb195174bf7c77e45fec50dded0b7696eef37c03
7
- data.tar.gz: 939a4d0b6d7f9b1434561607ba7d02e44ab93a471abb8d7565e223a2b103434177648d648901fa65a75b001ced4a926aef14f95478cffeb1c1c1c885dbcfbb84
6
+ metadata.gz: 6af7b845bd465ede4788967478ad9e67be55e3a2708000f2678a3a4ded46d3b024778d2828bb546e01f9e618fd8a43c1566bf46bdac2b1dff760d65105f3892a
7
+ data.tar.gz: '08bc22a824af6bacdfadc7b6546d0b8c9f116b22c8efc358f4cd7bc97cbe7652a59f23a9beb44616e87b5a3ae8f90a22628d02a1fd6dbf9b7460533336b137c1'
@@ -2,15 +2,107 @@
2
2
 
3
3
  require "async/http/server"
4
4
  require "async/http/endpoint"
5
+ require "protocol/http"
6
+ require "protocol/url"
5
7
  require "protocol/rack/adapter"
8
+ require_relative "protocol_http_ext"
6
9
  require_relative "../ratalada"
7
10
 
8
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
+
9
96
  module Backends
10
97
  # Falcon without falcon: one Async::HTTP::Server in this process, no
11
98
  # container, no supervision, no restarts. Use ratalada/falcon when you
12
99
  # want those; this is the fiber-based server on its own.
13
- module Async
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
14
106
  module_function
15
107
 
16
108
  def run(app, host:, port:, count: 1)
@@ -19,7 +111,7 @@ module Ratalada
19
111
  end
20
112
 
21
113
  endpoint = ::Async::HTTP::Endpoint.parse("http://#{host}:#{port}")
22
- server = ::Async::HTTP::Server.new(::Protocol::Rack::Adapter.new(app), endpoint)
114
+ server = ::Async::HTTP::Server.new(adapt(app), endpoint)
23
115
 
24
116
  warn "ratalada: async listening on http://#{host}:#{port}"
25
117
  # #run wraps itself in Async{}, which outside a reactor starts one and
@@ -28,8 +120,29 @@ module Ratalada
28
120
  rescue Interrupt
29
121
  # clean shutdown
30
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
31
137
  end
32
138
  end
33
139
 
34
- config.backend = Backends::Async
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
35
148
  end
@@ -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(configuration, container_class: Async::Container.best_container_class)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ratalada
4
- VERSION = "3.1.0"
4
+ VERSION = "4.0.0"
5
5
  end
data/lib/ratalada.rb CHANGED
@@ -36,6 +36,15 @@ module Ratalada
36
36
  end
37
37
 
38
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
39
48
  end
40
49
 
41
50
  # Rack::Request plus just enough sugar to pattern match on, so everything
@@ -103,19 +112,25 @@ module Ratalada
103
112
 
104
113
  private
105
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.
106
119
  def respond(handler, request)
107
120
  case handler
108
121
  when nil
109
122
  [404, { "content-type" => "text/plain" }, ["not found"]]
110
- when Proc, Method
111
- respond(handler.call(request), request)
112
123
  when String
113
124
  [200, { "content-type" => "text/plain" }, [handler]]
114
125
  when Array
115
126
  status, headers, body = handler
116
127
  [status, headers, body.is_a?(String) ? [body] : body]
117
128
  else
118
- handler
129
+ if handler.respond_to?(:call)
130
+ respond(handler.call(request), request)
131
+ else
132
+ handler
133
+ end
119
134
  end
120
135
  end
121
136
  end
@@ -179,6 +194,12 @@ module Ratalada
179
194
  end
180
195
 
181
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.
182
203
  def to_app(block)
183
204
  @middleware.reverse.inject(Ratalada.frontend.build(block)) do |inner, (klass, args, blk)|
184
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: 3.1.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan K
@@ -152,6 +152,7 @@ files:
152
152
  - lib/ratalada/async.rb
153
153
  - lib/ratalada/builder.rb
154
154
  - lib/ratalada/falcon.rb
155
+ - lib/ratalada/protocol_http_ext.rb
155
156
  - lib/ratalada/puma.rb
156
157
  - lib/ratalada/version.rb
157
158
  - lib/ratalada/webrick.rb