File: README.md

package info (click to toggle)
ruby-protocol-http 0.23.12-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 472 kB
  • sloc: ruby: 2,794; makefile: 4
file content (191 lines) | stat: -rw-r--r-- 5,008 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# Design Overview

The interfaces provided by {ruby Protocol::HTTP} underpin all downstream implementations. Therefore, we provide some justification for the design choices.

## Request/Response Model

The main model we support is the request/response model. A client sends a request to a server which return response. The protocol is responsible for serializing the request and response objects.

```mermaid
sequenceDiagram
	participant CA as Application
	participant Client
	participant Server
	participant SA as Application
	CA->>+Client: Request
	Client->>+Server: Request
	Server->>+SA: Request
	SA->>+Server: Response
	Server->>+Client: Response
	Client->>+CA: Response
```

We provide an interface for request and response objects. This provides performance, predictability and robustness. This model has proven itself over several years, handling a variety of different use cases.

~~~ ruby
class Request
	attr :verb
	attr :target
	attr :headers
	attr :body
end

class Response
	attr :status
	attr :headers
	attr :body
end
~~~

One other advantage is that it's symmetrical between clients and servers with a clear mapping, i.e. the protocol is responsible for transiting requests from the client to the server, and responses from the server back to the client. This helps us separate and define request/response interfaces independently from protocol implementation.

### Client Design

A request/response model implies that you create a request and receive a response back. This maps to a normal function call where the request is the argument and the response is the returned value.

~~~ ruby
request = Request.new("GET", url)
response = client.call(request)

response.headers
response.read
~~~

## Stream Model

An alternative model is the stream model. This model is more suitable for WebSockets and other persistent bi-directional channels.

```mermaid
sequenceDiagram
	participant CA as Application
	participant Client
	participant Server
	participant SA as Application
	CA->>+Client: Stream
	Client->>+Server: Stream
	Server->>+SA: Stream
```

The interfaces for streaming can be implemented a bit differently, since a response is not returned but rather assigned to the stream, and the streaming occurs in the same execution context as the client or server handling the request.

~~~ ruby
class Stream
	# Request details.
	attr :verb
	attr :target
	attr :headers
	
	attr :response
	
	# Write the response and start streaming the output body.
	def respond(status, headers)
		response.status = status
		response.headers = headers
	end
	
	# Request body.
	attr_accessor :input
	
	# Response body.
	attr_accessor :output
	
	# Write to the response body.
	def write(...)
		@output.write(...)
	end
	
	# Read from the request body.
	def read
		@input.read
	end
end

class Response
	def initialize(verb, target)
		@input = Body::Writable.new
		@output = Body::Writable.new
	end
	
	attr_accessor :status
	attr_accessor :headers
	
	# Prepare a stream for making a request.
	def request(verb, target, headers)
		# Create a request stream suitable for writing into the buffered response:
		Stream.new(verb, target, headers, self, @input, @output)
	end
	
	# Write to the request body.
	def write(...)
		@input.write(...)
	end
	
	# Read from the response body.
	def read
		@output.read
	end
end
~~~

### Client Design

A stream model implies that you create a stream which contains both the request and response bodies. This maps to a normal function call where the argument is the stream and the returned value is ignored.

~~~ ruby
response = Response.new
stream = response.request("GET", url)

client.call(stream)

response.headers
response.read
~~~

## Differences

The request/response model has a symmetrical design which naturally uses the return value for the result of executing the request. The result encapsulates the behaviour of how to read the response status, headers and body. Because of that, streaming input and output becomes a function of the result object itself. As in:

~~~ ruby
def call(request)
	body = Body::Writable.new
	
	Fiber.schedule do
		while chunk = request.input.read
			body.write(chunk.reverse)
		end
	end
	
	return Response[200, headers, body]
end

input = Body::Writable.new
response = call(... body ...)

input.write("Hello World")
input.close
response.read -> "dlroW olleH"
~~~

The streaming model does not have the same symmetry, and instead opts for a uni-directional flow of information.

~~~ruby
def call(stream)
	stream.respond(200, headers)
	
	Fiber.schedule do
		while chunk = stream.read
			stream.write(chunk.reverse)
		end
	end
end

input = Body::Writable.new
response = Response.new(...input...)
call(response.stream)

input.write("Hello World")
input.close
response.read -> "dlroW olleH"
~~~

The value of this uni-directional flow is that it is natural for the stream to be taken out of the scope imposed by the nested `call(request)` model. However, the user must explicitly close the stream, since it's no longer scoped to the client and/or server.