libmoo

Zig library for encoding and decoding Roon WebSocket message

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
  119. 119
  120. 120
  121. 121
  122. 122
  123. 123
  124. 124
  125. 125
  126. 126
  127. 127
  128. 128
  129. 129
  130. 130
  131. 131
  132. 132
  133. 133
  134. 134
  135. 135
  136. 136
  137. 137
  138. 138
  139. 139
  140. 140
  141. 141
  142. 142
  143. 143
  144. 144
  145. 145
  146. 146
  147. 147
  148. 148
// Copyright 2025 Shota FUJI
//
// Licensed under the Zero-Clause BSD License or the Apache License, Version 2.0, at your option.
// You may not use, copy, modify, or distribute this file except according to those terms. You can
// find a copy of the Zero-Clause BSD License at LICENSES/0BSD.txt, and a copy of the Apache License,
// Version 2.0 at LICENSES/Apache-2.0.txt. You may also obtain a copy of the Zero-Clause BSD License
// at <https://opensource.org/license/0bsd> and a copy of the Apache License, Version 2.0 at
// <https://www.apache.org/licenses/LICENSE-2.0>
//
// SPDX-License-Identifier: 0BSD OR Apache-2.0
//
// ===
//
// Zig API.

//! MOO message is a message format used for communicating with Roon Server.
//! Encodes to and decodes from newline-delimited UTF-8 encoded plain text, like HTTP.
//! Newline character is LF (0xa) without CR character.
//!
//! MOO message consists of metadata, header and body: header is a list of key-values
//! that both key and value contains UTF-8 string value, and body is single UTF-8 string.
//! Typical MOO message have "Content-Type" header that describes how the message's body
//! is encoded. (e.g. "application/json")
//! The first line of MOO message is metadata line, which is in "MOO/VERSION VERB SERVICE"
//! format. SERVICE is delimited by slash.

const std = @import("std");

pub const Parser = struct {
    state: State = .before_metadata,
    position: usize = 0,
    buffer: []const u8,
    content_type: ?[]const u8 = null,
    content_length: ?usize = null,

    const State = enum {
        before_metadata,
        before_header,
        in_header,
        before_body,
        done,
    };
};

/// Metadata contains a MOO message's version and semantics.
/// Fields point to the source message bytes: freeing the source message bytes then
/// accessing fields results in use-after-free.
pub const Metadata = struct {
    /// Message schema version described in metadata line.
    version: u32 = 1,

    /// Verb described in metadata line. Verb consists of uppercase alphabets.
    /// @example: "REQUEST", "COMPLETE"
    verb: []const u8,

    /// Service name described in metadata line.
    service: []const u8,

    pub const ParseError = error{
        BufferTooShort,
    };

    pub fn init(parser: Parser) ParseError!@This() {
        _ = parser;
        @panic("Not implemented");
    }
};

/// WellKnownFieldsHeader contains header fields appears in the official roon-node-api
/// source code.
/// Fields point to the source message bytes: freeing the source message bytes then
/// accessing fields results in use-after-free.
pub const WellKnownFieldsHeader = struct {
    /// MIME type.
    content_type: []const u8,

    /// Byte size of the body.
    content_length: usize,

    /// An ID unique to a connection, which is used for identificating which
    /// response is for which request.
    request_id: []const u8,

    pub const ParseError = error{};

    pub fn init(parser: Parser) ParseError!@This() {
        _ = parser;
        @panic("Not implemented");
    }
};

/// HashMapHeader stores header fields as a string hash map.
/// Fields point to the source message bytes: freeing the source message bytes then
/// accessing fields results in use-after-free.
pub const HashMapHeader = struct {
    allocator: std.mem.Allocator,

    /// Header key-values.
    map: std.hash_map.StringHashMap([]const u8),

    pub const ParseError = error{};

    pub fn init(allocator: std.mem.Allocator, parser: Parser) ParseError!@This() {
        var map = std.hash_map.StringHashMap([]const u8).init(allocator);
        errdefer map.deinit();

        _ = parser;
        @panic("Not implemented");
    }

    pub fn deinit(self: *@This()) void {
        self.map.deinit();
        self.* = undefined;
    }
};

/// RawBody contains a bytes slice of body section in a MOO message.
/// The bytes field points to the source message bytes: freeing the source message bytes
/// then accessing the bytes field results in use-after-free.
pub const RawBody = struct {
    bytes: []const u8,

    pub fn init(parser: Parser) @This() {
        _ = parser;
        @panic("Not implemented");
    }
};

/// JsonBody stores MOO message body as JSON value. User has to provided an expected
/// type (schema) as "T".
pub fn JsonBody(comptime T: type) type {
    return struct {
        allocator: std.mem.Allocator,
        value: *const T,
        parsed: std.json.Parsed(T),

        pub fn init(allocator: std.mem.Allocator, parser: Parser) @This() {
            _ = allocator;
            _ = parser;
            @panic("Not implemented");
        }

        pub fn deinit(self: @This()) void {
            self.parsed.deinit();
            self.* = undefined;
        }
    };
}