Branch data Line data Source code
1 : : // Copyright 2026 HPActor Contributors
2 : : //
3 : : // Licensed under the Apache License, Version 2.0 (the "License");
4 : : // you may not use this file except in compliance with the License.
5 : : // You may obtain a copy of the License at
6 : : //
7 : : // http://www.apache.org/licenses/LICENSE-2.0
8 : : //
9 : : // Unless required by applicable law or agreed to in writing, software
10 : : // distributed under the License is distributed on an "AS IS" BASIS,
11 : : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : : // See the License for the specific language governing permissions and
13 : : // limitations under the License.
14 : :
15 : : #pragma once
16 : :
17 : : #include <hpactor/actor/typed_message.hpp>
18 : : #include <hpactor/net/http_types.hpp>
19 : : #include <hpactor/types/types.hpp>
20 : :
21 : : #include <string>
22 : : #include <utility>
23 : : #include <vector>
24 : :
25 : : namespace hpactor {
26 : : namespace net {
27 : :
28 : : // ---------------------------------------------------------------------------
29 : : // HttpSerializer — content negotiation for HTTP ↔ actor messaging
30 : : // ---------------------------------------------------------------------------
31 : : // Bridges between HTTP wire formats (JSON, protobuf, text) and TypedMessage.
32 : : // Determines serialization format based on Content-Type (ingress) and
33 : : // Accept (egress) headers.
34 : : // ---------------------------------------------------------------------------
35 : : class HttpSerializer {
36 : : public:
37 : 6 : HttpSerializer() = default;
38 : :
39 : : // -----------------------------------------------------------------------
40 : : // Ingress: HTTP request body → TypedMessage
41 : : // -----------------------------------------------------------------------
42 : : // Determines encoding from the request's Content-Type header and converts
43 : : // the body to a TypedMessage with the expected TypeTag. Falls back to
44 : : // treating the body as raw protobuf if Content-Type is absent.
45 : : result<TypedMessage>
46 : : deserialize_request(const HttpRequest& req, TypeTag expected_tag);
47 : :
48 : : // -----------------------------------------------------------------------
49 : : // Egress: TypedMessage → HTTP response body + Content-Type
50 : : // -----------------------------------------------------------------------
51 : : // Serializes the TypedMessage payload according to the client's Accept
52 : : // header. Returns the response body bytes and the Content-Type string
53 : : // to set on the HTTP response.
54 : : std::pair<StreamBuffer, std::string>
55 : : serialize_response(const TypedMessage& msg,
56 : : const std::string& accept_header);
57 : :
58 : : // -----------------------------------------------------------------------
59 : : // Egress: TypedMessage → HTTP request body + Content-Type (for HttpClient)
60 : : // -----------------------------------------------------------------------
61 : : std::pair<StreamBuffer, std::string>
62 : : serialize_request(const TypedMessage& msg);
63 : :
64 : : // -----------------------------------------------------------------------
65 : : // Configuration
66 : : // -----------------------------------------------------------------------
67 : : // Register that a TypeTag should use JSON serialization by default.
68 : : // Without this, the default is protobuf binary.
69 : : void set_default_format(TypeTag tag, const std::string& content_type) {
70 : : default_formats_[static_cast<uint32_t>(tag)] = content_type;
71 : : }
72 : :
73 : : private:
74 : : // Parse Accept header into ordered list of (media_type, quality) pairs
75 : : struct AcceptedType {
76 : : std::string media_type;
77 : : float quality = 1.0f;
78 : : };
79 : : std::vector<AcceptedType> parse_accept_header(const std::string& header) const;
80 : :
81 : : // Select the best Content-Type for the response based on Accept header
82 : : std::string negotiate_response_type(const std::string& accept_header) const;
83 : :
84 : : // Minimal JSON escape/wrap utilities (full JSON↔protobuf mapping is a future phase)
85 : : static StreamBuffer wrap_as_json_bytes(const StreamBuffer& proto_payload);
86 : : static StreamBuffer wrap_as_text_bytes(const StreamBuffer& payload);
87 : :
88 : : std::unordered_map<uint32_t, std::string> default_formats_;
89 : : };
90 : :
91 : : // =============================================================================
92 : : // Inline implementations
93 : : // =============================================================================
94 : :
95 : : inline result<TypedMessage>
96 : 2 : HttpSerializer::deserialize_request(const HttpRequest& req, TypeTag expected_tag) {
97 : 2 : auto ct = req.content_type();
98 : :
99 : : // Determine content type
100 : 2 : std::string content_type = ct.value_or("application/x-protobuf");
101 : :
102 : 2 : if (content_type.find("application/x-protobuf") != std::string::npos) {
103 : : // Raw protobuf — pass bytes directly as TypedMessage payload
104 : : return result<TypedMessage>::make(
105 : 1 : TypedMessage(expected_tag, req.body));
106 : : }
107 : :
108 : 1 : if (content_type.find("application/json") != std::string::npos) {
109 : : // JSON body — store as-is in TypedMessage; the receiving actor
110 : : // is responsible for JSON→protobuf parsing via as<T>().
111 : : return result<TypedMessage>::make(
112 : 1 : TypedMessage(expected_tag, req.body));
113 : : }
114 : :
115 : 0 : if (content_type.find("text/plain") != std::string::npos) {
116 : : // Plain text — wrap as bytes payload
117 : : return result<TypedMessage>::make(
118 : 0 : TypedMessage(expected_tag, req.body));
119 : : }
120 : :
121 : : // Unknown Content-Type — pass through as raw bytes
122 : : return result<TypedMessage>::make(
123 : 0 : TypedMessage(expected_tag, req.body));
124 : 2 : }
125 : :
126 : : inline std::pair<StreamBuffer, std::string>
127 : 4 : HttpSerializer::serialize_response(const TypedMessage& msg,
128 : : const std::string& accept_header) {
129 : 4 : std::string response_type = negotiate_response_type(accept_header);
130 : :
131 : 4 : if (response_type == "application/x-protobuf") {
132 : 0 : return {msg.payload(), "application/x-protobuf"};
133 : : }
134 : :
135 : 4 : if (response_type == "text/plain") {
136 : 0 : return {wrap_as_text_bytes(msg.payload()), "text/plain; charset=utf-8"};
137 : : }
138 : :
139 : : // Default: JSON
140 : 4 : return {wrap_as_json_bytes(msg.payload()), "application/json; charset=utf-8"};
141 : 4 : }
142 : :
143 : : inline std::pair<StreamBuffer, std::string>
144 : : HttpSerializer::serialize_request(const TypedMessage& msg) {
145 : : // HttpClient always sends protobuf binary for efficiency
146 : : return {msg.payload(), "application/x-protobuf"};
147 : : }
148 : :
149 : : inline std::string
150 : 4 : HttpSerializer::negotiate_response_type(const std::string& accept_header) const {
151 : 4 : if (accept_header.empty()) {
152 : 2 : return "application/json";
153 : : }
154 : :
155 : 3 : auto accepted = parse_accept_header(accept_header);
156 : :
157 : 3 : for (const auto& at : accepted) {
158 : 3 : if (at.media_type == "application/json" ||
159 : 0 : at.media_type == "application/x-protobuf" ||
160 : 0 : at.media_type == "text/plain" ||
161 : 3 : at.media_type == "*/*" ||
162 : 0 : at.media_type == "text/*") {
163 : : // Return the first concrete match, or default to JSON for wildcards
164 : 3 : if (at.media_type == "*/*" || at.media_type == "text/*") {
165 : 0 : return "application/json";
166 : : }
167 : 3 : return at.media_type;
168 : : }
169 : : }
170 : :
171 : : // No acceptable match — default to JSON
172 : 0 : return "application/json";
173 : 3 : }
174 : :
175 : : inline std::vector<HttpSerializer::AcceptedType>
176 : 3 : HttpSerializer::parse_accept_header(const std::string& header) const {
177 : 3 : std::vector<AcceptedType> result;
178 : :
179 : 3 : size_t pos = 0;
180 : 7 : while (pos < header.size()) {
181 : : // Skip whitespace
182 : 5 : while (pos < header.size() && (header[pos] == ' ' || header[pos] == '\t')) {
183 : 1 : ++pos;
184 : : }
185 : :
186 : : // Parse media type
187 : 4 : size_t end = header.find_first_of(",;", pos);
188 : 4 : std::string media_type = header.substr(pos, end - pos);
189 : : // Trim trailing whitespace from media type
190 : 4 : while (!media_type.empty() && (media_type.back() == ' ' || media_type.back() == '\t')) {
191 : 0 : media_type.pop_back();
192 : : }
193 : : // Trim leading whitespace
194 : 4 : size_t start = 0;
195 : 4 : while (start < media_type.size() && (media_type[start] == ' ' || media_type[start] == '\t')) {
196 : 0 : ++start;
197 : : }
198 : 4 : media_type = media_type.substr(start);
199 : :
200 : 4 : pos = end;
201 : :
202 : : // Parse optional quality parameter
203 : 4 : float quality = 1.0f;
204 : 4 : if (pos < header.size() && header[pos] == ';') {
205 : 2 : ++pos; // skip ';'
206 : 2 : while (pos < header.size() && (header[pos] == ' ' || header[pos] == '\t')) {
207 : 0 : ++pos;
208 : : }
209 : 2 : if (pos + 2 < header.size() &&
210 : 4 : (header[pos] == 'q' || header[pos] == 'Q') &&
211 : 2 : header[pos + 1] == '=') {
212 : 2 : pos += 2;
213 : : // Parse float
214 : 2 : size_t qend = header.find_first_of(",;", pos);
215 : 2 : std::string qstr = header.substr(pos, qend == std::string::npos ? qend : qend - pos);
216 : 2 : quality = std::stof(qstr);
217 : 2 : pos = qend;
218 : 2 : }
219 : : }
220 : :
221 : 4 : if (pos < header.size() && header[pos] == ',') {
222 : 1 : ++pos; // skip ','
223 : : }
224 : :
225 : 4 : result.push_back({std::move(media_type), quality});
226 : 4 : }
227 : :
228 : : // Sort by quality (descending)
229 : 3 : std::sort(result.begin(), result.end(),
230 : 1 : [](const AcceptedType& a, const AcceptedType& b) {
231 : 1 : return a.quality > b.quality;
232 : : });
233 : :
234 : 3 : return result;
235 : : }
236 : :
237 : 4 : inline StreamBuffer HttpSerializer::wrap_as_json_bytes(const StreamBuffer& proto_payload) {
238 : : // Minimal JSON wrapper for protobuf payload.
239 : : // Full JSON↔protobuf conversion is a future phase.
240 : 4 : StreamBuffer result;
241 : 4 : if (proto_payload.size() == 0) {
242 : 0 : const uint8_t empty_json[] = {'{', '}'};
243 : 0 : result.append(empty_json, 2);
244 : 0 : return result;
245 : : }
246 : :
247 : : static const char* prefix = "{\"data\":\"";
248 : 4 : result.append(reinterpret_cast<const uint8_t*>(prefix), 9);
249 : :
250 : : // Simple hex encoding for now (future: proper base64/JSON)
251 : : static const char hex[] = "0123456789abcdef";
252 : 26 : for (size_t i = 0; i < proto_payload.size() && i < 256; ++i) {
253 : 22 : uint8_t b = proto_payload.data()[i];
254 : 22 : char buf[2] = {hex[b >> 4], hex[b & 0xf]};
255 : 22 : result.append(reinterpret_cast<const uint8_t*>(buf), 2);
256 : : }
257 : 4 : const uint8_t suffix[] = {'"', '}'};
258 : 4 : result.append(suffix, 2);
259 : 4 : return result;
260 : : }
261 : :
262 : 0 : inline StreamBuffer HttpSerializer::wrap_as_text_bytes(const StreamBuffer& payload) {
263 : 0 : if (payload.size() == 0) return {};
264 : 0 : return payload;
265 : : }
266 : :
267 : : } // namespace net
268 : : } // namespace hpactor
|