Skip to content

Commit c6446ac

Browse files
100% documentation coverage.
1 parent cee96bd commit c6446ac

13 files changed

Lines changed: 129 additions & 0 deletions

File tree

‎gems.rb‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
gem "bake-modernize"
1515
gem "bake-releases"
1616

17+
gem "agent-context"
18+
1719
gem "utopia-project"
1820
end
1921

‎lib/protocol/websocket/coder.rb‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
module Protocol
99
module WebSocket
10+
# @namespace
1011
module Coder
1112
# The default coder for WebSocket messages.
1213
DEFAULT = JSON::DEFAULT

‎lib/protocol/websocket/coder/json.rb‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ module WebSocket
1010
module Coder
1111
# A JSON coder that uses the standard JSON library.
1212
class JSON
13+
# Initialize a new JSON coder.
14+
# @parameter options [Hash] Options to pass to the JSON library when parsing or generating.
1315
def initialize(**options)
1416
@options = options
1517
end

‎lib/protocol/websocket/error.rb‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ class Error < HTTP::Error
4141

4242
# Raised by stream or connection handlers, results in GOAWAY frame which signals termination of the current connection. You *cannot* recover from this exception, or any exceptions subclassed from it.
4343
class ProtocolError < Error
44+
# Initialize a protocol error with an optional status code.
45+
# @parameter message [String] The error message.
46+
# @parameter code [Integer] The WebSocket status code associated with the error.
4447
def initialize(message, code = PROTOCOL_ERROR)
4548
super(message)
4649

‎lib/protocol/websocket/extension/compression.rb‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
module Protocol
1111
module WebSocket
12+
# @namespace
1213
module Extension
1314
# Provides support for the permessage-deflate extension.
1415
module Compression

‎lib/protocol/websocket/extension/compression/deflate.rb‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module Protocol
99
module WebSocket
1010
module Extension
1111
module Compression
12+
# Compresses outgoing WebSocket frames using the DEFLATE algorithm.
1213
class Deflate
1314
# Client writing to server.
1415
def self.client(parent, client_max_window_bits: 15, client_no_context_takeover: false, **options)
@@ -28,6 +29,13 @@ def self.server(parent, server_max_window_bits: 15, server_no_context_takeover:
2829
)
2930
end
3031

32+
# Initialize a new deflate compressor.
33+
# @parameter parent [Object] The parent framer to wrap.
34+
# @parameter level [Integer] The compression level. Defaults to `Zlib::DEFAULT_COMPRESSION`.
35+
# @parameter memory_level [Integer] The memory level for compression. Defaults to `Zlib::DEF_MEM_LEVEL`.
36+
# @parameter strategy [Integer] The compression strategy. Defaults to `Zlib::DEFAULT_STRATEGY`.
37+
# @parameter window_bits [Integer] The window size in bits for the DEFLATE algorithm.
38+
# @parameter context_takeover [Boolean] Whether to reuse the compression context across messages.
3139
def initialize(parent, level: Zlib::DEFAULT_COMPRESSION, memory_level: Zlib::DEF_MEM_LEVEL, strategy: Zlib::DEFAULT_STRATEGY, window_bits: 15, context_takeover: true, **options)
3240
@parent = parent
3341

@@ -46,13 +54,20 @@ def initialize(parent, level: Zlib::DEFAULT_COMPRESSION, memory_level: Zlib::DEF
4654
@context_takeover = context_takeover
4755
end
4856

57+
# @returns [String] A string representation including window bits and context takeover settings.
4958
def to_s
5059
"#<#{self.class} window_bits=#{@window_bits} context_takeover=#{@context_takeover}>"
5160
end
5261

62+
# @attribute [Integer] The window size in bits used for compression.
5363
attr :window_bits
64+
# @attribute [Boolean] Whether the compression context is reused across messages.
5465
attr :context_takeover
5566

67+
# Pack a text frame, optionally compressing the buffer.
68+
# @parameter buffer [String] The text payload to pack.
69+
# @parameter compress [Boolean] Whether to compress the buffer. Defaults to `true`.
70+
# @returns [Frame] The packed (and optionally compressed) text frame.
5671
def pack_text_frame(buffer, compress: true, **options)
5772
if compress
5873
buffer = self.deflate(buffer)
@@ -67,6 +82,10 @@ def pack_text_frame(buffer, compress: true, **options)
6782
return frame
6883
end
6984

85+
# Pack a binary frame, optionally compressing the buffer.
86+
# @parameter buffer [String] The binary payload to pack.
87+
# @parameter compress [Boolean] Whether to compress the buffer. Defaults to `false`.
88+
# @returns [Frame] The packed (and optionally compressed) binary frame.
7089
def pack_binary_frame(buffer, compress: false, **options)
7190
if compress
7291
buffer = self.deflate(buffer)

‎lib/protocol/websocket/extension/compression/inflate.rb‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module Protocol
99
module WebSocket
1010
module Extension
1111
module Compression
12+
# Decompresses incoming WebSocket frames using the DEFLATE algorithm.
1213
class Inflate
1314
# Client reading from server.
1415
def self.client(parent, server_max_window_bits: 15, server_no_context_takeover: false, **options)
@@ -28,6 +29,10 @@ def self.server(parent, client_max_window_bits: 15, client_no_context_takeover:
2829

2930
TRAILER = [0x00, 0x00, 0xff, 0xff].pack("C*")
3031

32+
# Initialize a new inflate decompressor.
33+
# @parameter parent [Object] The parent framer to wrap.
34+
# @parameter context_takeover [Boolean] Whether to reuse the decompression context across messages.
35+
# @parameter window_bits [Integer] The window size in bits for the DEFLATE algorithm.
3136
def initialize(parent, context_takeover: true, window_bits: 15)
3237
@parent = parent
3338

@@ -42,13 +47,19 @@ def initialize(parent, context_takeover: true, window_bits: 15)
4247
@context_takeover = context_takeover
4348
end
4449

50+
# @returns [String] A string representation including window bits and context takeover settings.
4551
def to_s
4652
"#<#{self.class} window_bits=#{@window_bits} context_takeover=#{@context_takeover}>"
4753
end
4854

55+
# @attribute [Integer] The window size in bits used for decompression.
4956
attr :window_bits
57+
# @attribute [Boolean] Whether the decompression context is reused across messages.
5058
attr :context_takeover
5159

60+
# Unpack and decompress frames into a buffer.
61+
# @parameter frames [Array(Frame)] The frames to unpack.
62+
# @returns [String] The decompressed payload buffer.
5263
def unpack_frames(frames, **options)
5364
buffer = @parent.unpack_frames(frames, **options)
5465

‎lib/protocol/websocket/extensions.rb‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88

99
module Protocol
1010
module WebSocket
11+
# Manages WebSocket extensions negotiated during the handshake.
1112
module Extensions
13+
# Parse a list of extension header values into name and argument pairs.
14+
# @parameter headers [Array(String)] The raw extension header values.
15+
# @yields {|name, arguments| ...} Each parsed extension.
16+
# @parameter name [String] The name of the extension.
17+
# @parameter arguments [Array] The key-value argument pairs.
1218
def self.parse(headers)
1319
return to_enum(:parse, headers) unless block_given?
1420

@@ -23,27 +29,39 @@ def self.parse(headers)
2329
end
2430
end
2531

32+
# Manages extensions on the client side, offering and accepting server responses.
2633
class Client
34+
# Create a default client with permessage-deflate compression enabled.
35+
# @returns [Client] A new client with the default compression extension.
2736
def self.default
2837
self.new([
2938
[Extension::Compression, {}]
3039
])
3140
end
3241

42+
# Initialize a new client extension manager.
43+
# @parameter extensions [Array] The list of extensions to offer, each as `[klass, options]`.
3344
def initialize(extensions = [])
3445
@extensions = extensions
3546
@accepted = []
3647
end
3748

49+
# @attribute [Array] The list of extensions to offer.
3850
attr :extensions
51+
# @attribute [Array] The extensions accepted after negotiation.
3952
attr :accepted
4053

54+
# Build a lookup table of extensions keyed by their name.
55+
# @returns [Hash] A hash mapping extension names to their `[klass, options]` pairs.
4156
def named
4257
@extensions.map do |extension|
4358
[extension.first::NAME, extension]
4459
end.to_h
4560
end
4661

62+
# Yield extension offer headers for each registered extension.
63+
# @yields {|header| ...} Each offer header string.
64+
# @parameter header [Array(String)] The extension offer header tokens.
4765
def offer
4866
@extensions.each do |extension, options|
4967
if header = extension.offer(**options)
@@ -52,6 +70,9 @@ def offer
5270
end
5371
end
5472

73+
# Accept server extension responses and record the negotiated extensions.
74+
# @parameter headers [Array(String)] The `Sec-WebSocket-Extensions` response header values.
75+
# @returns [Array] The accepted extensions as `[klass, options]` pairs.
5576
def accept(headers)
5677
named = self.named
5778

@@ -69,34 +90,50 @@ def accept(headers)
6990
return @accepted
7091
end
7192

93+
# Apply all accepted extensions to the given connection as a client.
94+
# @parameter connection [Connection] The WebSocket connection to configure.
7295
def apply(connection)
7396
@accepted.each do |(klass, options)|
7497
klass.client(connection, **options)
7598
end
7699
end
77100
end
78101

102+
# Manages extensions on the server side, negotiating client offers and applying the agreed extensions.
79103
class Server
104+
# Create a default server with permessage-deflate compression enabled.
105+
# @returns [Server] A new server with the default compression extension.
80106
def self.default
81107
self.new([
82108
[Extension::Compression, {}]
83109
])
84110
end
85111

112+
# Initialize a new server extension manager.
113+
# @parameter extensions [Array] The list of supported extensions, each as `[klass, options]`.
86114
def initialize(extensions)
87115
@extensions = extensions
88116
@accepted = []
89117
end
90118

119+
# @attribute [Array] The list of supported extensions.
91120
attr :extensions
121+
# @attribute [Array] The extensions accepted after negotiation.
92122
attr :accepted
93123

124+
# Build a lookup table of extensions keyed by their name.
125+
# @returns [Hash] A hash mapping extension names to their `[klass, options]` pairs.
94126
def named
95127
@extensions.map do |extension|
96128
[extension.first::NAME, extension]
97129
end.to_h
98130
end
99131

132+
# Negotiate client extension offers and yield accepted response headers.
133+
# @parameter headers [Array(String)] The `Sec-WebSocket-Extensions` request header values.
134+
# @yields {|header| ...} Each accepted extension header to include in the response.
135+
# @parameter header [Array(String)] The negotiated extension header tokens.
136+
# @returns [Array] The accepted extensions as `[klass, options]` pairs.
100137
def accept(headers)
101138
extensions = []
102139

@@ -124,6 +161,8 @@ def accept(headers)
124161
return @accepted
125162
end
126163

164+
# Apply all accepted extensions to the given connection as a server.
165+
# @parameter connection [Connection] The WebSocket connection to configure.
127166
def apply(connection)
128167
@accepted.reverse_each do |(klass, options)|
129168
klass.server(connection, **options)

‎lib/protocol/websocket/frame.rb‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
module Protocol
1212
module WebSocket
13+
# Represents a single WebSocket frame as defined by RFC 6455.
1314
class Frame
1415
include Comparable
1516

@@ -35,18 +36,28 @@ def initialize(finished = true, payload = nil, flags: 0, opcode: self.class::OPC
3536
@payload = payload
3637
end
3738

39+
# Check whether the specified RSV flag bit is set on this frame.
40+
# @parameter value [Integer] The flag bitmask to test (e.g. `RSV1`).
41+
# @returns [Boolean] `true` if the flag bit is set.
3842
def flag?(value)
3943
@flags & value != 0
4044
end
4145

46+
# Compare this frame to another frame by their array representation.
47+
# @parameter other [Frame] The frame to compare against.
48+
# @returns [Integer] A comparison result (-1, 0, or 1).
4249
def <=> other
4350
to_ary <=> other.to_ary
4451
end
4552

53+
# Convert this frame to an array of its fields for comparison or inspection.
54+
# @returns [Array] An array of `[finished, flags, opcode, mask, length, payload]`.
4655
def to_ary
4756
[@finished, @flags, @opcode, @mask, @length, @payload]
4857
end
4958

59+
# Check whether this is a control frame (opcode has bit 3 set).
60+
# @returns [Boolean] `true` if this is a control frame.
5061
def control?
5162
@opcode & 0x8 != 0
5263
end
@@ -56,10 +67,14 @@ def data?
5667
false
5768
end
5869

70+
# Check whether this is the final frame in a message.
71+
# @returns [Boolean] `true` if the FIN bit is set.
5972
def finished?
6073
@finished == true
6174
end
6275

76+
# Check whether this frame is a continuation fragment (FIN bit not set).
77+
# @returns [Boolean] `true` if the FIN bit is not set.
6378
def continued?
6479
@finished == false
6580
end
@@ -116,6 +131,9 @@ def continued?
116131
end
117132
end
118133

134+
# Pack the given data into this frame's payload, applying masking if configured.
135+
# @parameter data [String] The payload data to pack.
136+
# @returns [Frame] Returns `self`.
119137
def pack(data = "")
120138
length = data.bytesize
121139

@@ -134,6 +152,8 @@ def pack(data = "")
134152
return self
135153
end
136154

155+
# Unpack the raw payload, removing masking if present.
156+
# @returns [String] The unmasked payload data.
137157
def unpack
138158
if @mask and !@payload.empty?
139159
return mask_xor(@payload, @mask)
@@ -142,10 +162,16 @@ def unpack
142162
end
143163
end
144164

165+
# Apply this frame to the connection by dispatching it to the appropriate handler.
166+
# @parameter connection [Connection] The WebSocket connection to receive this frame.
145167
def apply(connection)
146168
connection.receive_frame(self)
147169
end
148170

171+
# Parse the first byte of a frame header to extract FIN, RSV flags, and opcode.
172+
# @parameter buffer [String] A 1-byte binary string.
173+
# @returns [Array] A tuple of `[finished, flags, opcode]`.
174+
# @raises [ProtocolError] If the opcode is a reserved non-control or control opcode.
149175
def self.parse_header(buffer)
150176
byte = buffer.unpack("C").first
151177

@@ -162,6 +188,15 @@ def self.parse_header(buffer)
162188
return finished, flags, opcode
163189
end
164190

191+
# Read a full frame from the stream given pre-parsed header fields.
192+
# @parameter finished [Boolean] Whether the FIN bit was set.
193+
# @parameter flags [Integer] The RSV flag bits.
194+
# @parameter opcode [Integer] The frame opcode.
195+
# @parameter stream [IO] The stream to read from.
196+
# @parameter maximum_frame_size [Integer] The maximum allowed payload size in bytes.
197+
# @returns [Frame] The fully read and populated frame.
198+
# @raises [ProtocolError] If the frame violates protocol constraints.
199+
# @raises [EOFError] If the stream ends unexpectedly.
165200
def self.read(finished, flags, opcode, stream, maximum_frame_size)
166201
buffer = stream.read(1) or raise EOFError, "Could not read header!"
167202
byte = buffer.unpack("C").first
@@ -202,6 +237,9 @@ def self.read(finished, flags, opcode, stream, maximum_frame_size)
202237
return self.new(finished, payload, flags: flags, opcode: opcode, mask: mask)
203238
end
204239

240+
# Write this frame to the given stream.
241+
# @parameter stream [IO] The stream to write the serialized frame to.
242+
# @raises [ProtocolError] If the frame has invalid length or mask.
205243
def write(stream)
206244
buffer = String.new(encoding: Encoding::BINARY)
207245

‎lib/protocol/websocket/framer.rb‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ module WebSocket
2929

3030
# Wraps an underlying {Async::IO::Stream} for reading and writing binary data into structured frames.
3131
class Framer
32+
# Initialize a new framer wrapping the given stream.
33+
# @parameter stream [IO] The underlying stream to read from and write to.
34+
# @parameter frames [Hash] A mapping of opcodes to frame classes.
3235
def initialize(stream, frames = FRAMES)
3336
@stream = stream
3437
@frames = frames

0 commit comments

Comments
 (0)