Module: Gienah::Protocol

Defined in:
lib/gienah/protocol.rb,
sig/gienah.rbs

Constant Summary collapse

MAX_MESSAGE =

Returns:

  • (Integer)
4 * 1024 * 1024
MAX_HEADER =

Returns:

  • (Integer)
16 * 1024

Class Method Summary collapse

Class Method Details

.frame(message, max_size: MAX_MESSAGE) ⇒ String

Parameters:

  • message (Hash[String, untyped])
  • max_size: (Integer) (defaults to: MAX_MESSAGE)

Returns:

  • (String)


9
10
11
12
13
14
15
16
17
# File 'lib/gienah/protocol.rb', line 9

def frame(message, max_size: MAX_MESSAGE)
  validate_message(message)
  body = JSON.generate(message).b
  raise ProtocolError, "message exceeds #{max_size} bytes" unless body.bytesize.between?(1, max_size)

  "Content-Length: #{body.bytesize}\r\n\r\n".b + body
rescue JSON::GeneratorError => error
  raise ProtocolError, "invalid JSON: #{error.message}"
end

.notification(method, params = {}) ⇒ Hash[String, untyped]

Parameters:

  • method (String)
  • params (json) (defaults to: {})

Returns:

  • (Hash[String, untyped])


82
83
84
# File 'lib/gienah/protocol.rb', line 82

def notification(method, params = {})
  {"jsonrpc" => "2.0", "method" => method, "params" => params}
end

.read(io, max_size: MAX_MESSAGE) ⇒ Hash[String, untyped]?

Parameters:

  • io (IO)
  • max_size: (Integer) (defaults to: MAX_MESSAGE)

Returns:

  • (Hash[String, untyped], nil)


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
# File 'lib/gienah/protocol.rb', line 19

def read(io, max_size: MAX_MESSAGE)
  headers = {}
  bytes = 0
  loop do
    line = io.gets("\r\n", MAX_HEADER + 1)
    return nil if line.nil? && headers.empty?
    raise ProtocolError, "truncated header" unless line&.end_with?("\r\n")
    raise ProtocolError, "oversized header" if line.bytesize > MAX_HEADER
    break if line == "\r\n"

    bytes += line.bytesize
    raise ProtocolError, "oversized headers" if bytes > MAX_HEADER
    raise ProtocolError, "non-ASCII header" unless line.ascii_only?

    key, value = line.delete_suffix("\r\n").split(":", 2)
    raise ProtocolError, "invalid header" unless key&.match?(/\A[A-Za-z][A-Za-z0-9-]*\z/) && value
    key = key.downcase
    raise ProtocolError, "duplicate header" if headers.key?(key)
    headers[key] = value.strip
  end

  raw_length = headers["content-length"]
  raise ProtocolError, "missing or invalid Content-Length" unless raw_length&.match?(/\A\d+\z/)
  length = Integer(raw_length, 10)
  raise ProtocolError, "message exceeds #{max_size} bytes" unless length.between?(1, max_size)
  body = read_exact(io, length)
  raise ProtocolError, "truncated body" unless body&.bytesize == length
  body.force_encoding(Encoding::UTF_8)
  raise ProtocolError, "invalid UTF-8 body" unless body.valid_encoding?
  validate_message(JSON.parse(body))
rescue JSON::ParserError => error
  raise ProtocolError, "invalid JSON: #{error.message}"
end

.request(id, method, params = {}) ⇒ Hash[String, untyped]

Parameters:

  • id (Integer)
  • method (String)
  • params (json) (defaults to: {})

Returns:

  • (Hash[String, untyped])


78
79
80
# File 'lib/gienah/protocol.rb', line 78

def request(id, method, params = {})
  {"jsonrpc" => "2.0", "id" => id, "method" => method, "params" => params}
end

.response(id, result: nil, error: nil) ⇒ Hash[String, untyped]

Parameters:

  • id (Integer)
  • result: (json) (defaults to: nil)
  • error: (Hash[String, json]) (defaults to: nil)

Returns:

  • (Hash[String, untyped])

Raises:

  • (ArgumentError)


86
87
88
89
90
91
# File 'lib/gienah/protocol.rb', line 86

def response(id, result: nil, error: nil)
  raise ArgumentError, "result and error are exclusive" if !error.nil? && !result.nil?
  value = {"jsonrpc" => "2.0", "id" => id}
  error.nil? ? value["result"] = result : value["error"] = error
  value
end

.validate_message(message) ⇒ Hash[String, untyped]

Parameters:

  • message (Hash[String, untyped])

Returns:

  • (Hash[String, untyped])

Raises:



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/gienah/protocol.rb', line 53

def validate_message(message)
  raise ProtocolError, "message must be an object" unless message.is_a?(Hash)
  version = message["jsonrpc"] || message[:jsonrpc]
  raise ProtocolError, "jsonrpc must be \"2.0\"" unless version == "2.0"
  method = message["method"] || message[:method]
  id_present = message.key?("id") || message.key?(:id)
  has_result = message.key?("result") || message.key?(:result)
  has_error = message.key?("error") || message.key?(:error)
  raise ProtocolError, "method must be a nonempty String" if method && (!method.is_a?(String) || method.empty?)
  if method
    raise ProtocolError, "request cannot contain result or error" if has_result || has_error
    params = message["params"] || message[:params]
    raise ProtocolError, "params must be an object or array" if params && !params.is_a?(Hash) && !params.is_a?(Array)
  elsif id_present
    id = message["id"] || message[:id]
    raise ProtocolError, "invalid response id" unless id.is_a?(Integer) || id.is_a?(String)
    raise ProtocolError, "response must contain exactly one result or error" unless has_result ^ has_error
    error = message["error"] || message[:error]
    raise ProtocolError, "invalid response error" if has_error && (!error.is_a?(Hash) || !error["message"].is_a?(String))
  else
    raise ProtocolError, "message must contain method or id"
  end
  message
end