Skip to content

Commit 624bdac

Browse files
committed
feat(capabilities): trait architecture http draft
1 parent 0411c94 commit 624bdac

10 files changed

Lines changed: 354 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ members = [
2929
"libdd-trace-utils",
3030
"libdd-trace-stats",
3131
"datadog-tracer-flare",
32+
"libdd-capabilities",
3233
"libdd-common",
3334
"libdd-common-ffi",
3435
"libdd-telemetry",

libdd-capabilities/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
[package]
5+
name = "libdd-capabilities"
6+
version = "0.1.0"
7+
edition = "2021"
8+
description = "Portable capability traits for cross-platform libdatadog"
9+
homepage = "https://github.com/DataDog/libdatadog"
10+
repository = "https://github.com/DataDog/libdatadog"
11+
license = "Apache-2.0"
12+
13+
[lib]
14+
crate-type = ["lib"]
15+
16+
[dependencies]
17+

libdd-capabilities/src/http.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! HTTP capability trait and types.
5+
6+
use crate::maybe_send::MaybeSend;
7+
use core::fmt;
8+
use core::future::Future;
9+
10+
#[derive(Debug, Clone)]
11+
pub struct HttpResponse {
12+
pub status: u16,
13+
pub body: Vec<u8>,
14+
}
15+
16+
#[derive(Debug, Clone)]
17+
pub enum HttpError {
18+
Network(String),
19+
Timeout,
20+
InvalidRequest(String),
21+
Other(String),
22+
}
23+
24+
impl fmt::Display for HttpError {
25+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26+
match self {
27+
HttpError::Network(msg) => write!(f, "Network error: {}", msg),
28+
HttpError::Timeout => write!(f, "Request timed out"),
29+
HttpError::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
30+
HttpError::Other(msg) => write!(f, "HTTP error: {}", msg),
31+
}
32+
}
33+
}
34+
35+
impl std::error::Error for HttpError {}
36+
37+
/// Request without body (GET, HEAD, DELETE, OPTIONS).
38+
#[derive(Debug, Clone, Default)]
39+
pub struct RequestHead {
40+
pub url: String,
41+
pub headers: Vec<(String, String)>,
42+
}
43+
44+
impl RequestHead {
45+
pub fn new(url: impl Into<String>) -> Self {
46+
Self {
47+
url: url.into(),
48+
headers: Vec::new(),
49+
}
50+
}
51+
52+
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
53+
self.headers.push((name.into(), value.into()));
54+
self
55+
}
56+
}
57+
58+
/// Request with body (POST, PUT, PATCH).
59+
#[derive(Debug, Clone)]
60+
pub struct RequestWithBody {
61+
pub head: RequestHead,
62+
pub body: Vec<u8>,
63+
}
64+
65+
impl RequestWithBody {
66+
pub fn new(url: impl Into<String>, body: Vec<u8>) -> Self {
67+
Self {
68+
head: RequestHead::new(url),
69+
body,
70+
}
71+
}
72+
73+
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
74+
self.head = self.head.with_header(name, value);
75+
self
76+
}
77+
}
78+
79+
#[derive(Debug, Clone)]
80+
pub enum HttpRequest {
81+
Get(RequestHead),
82+
Head(RequestHead),
83+
Delete(RequestHead),
84+
Options(RequestHead),
85+
Post(RequestWithBody),
86+
Put(RequestWithBody),
87+
Patch(RequestWithBody),
88+
}
89+
90+
impl HttpRequest {
91+
pub fn get(url: impl Into<String>) -> Self {
92+
Self::Get(RequestHead::new(url))
93+
}
94+
95+
pub fn head(url: impl Into<String>) -> Self {
96+
Self::Head(RequestHead::new(url))
97+
}
98+
99+
pub fn delete(url: impl Into<String>) -> Self {
100+
Self::Delete(RequestHead::new(url))
101+
}
102+
103+
pub fn options(url: impl Into<String>) -> Self {
104+
Self::Options(RequestHead::new(url))
105+
}
106+
107+
pub fn post(url: impl Into<String>, body: Vec<u8>) -> Self {
108+
Self::Post(RequestWithBody::new(url, body))
109+
}
110+
111+
pub fn put(url: impl Into<String>, body: Vec<u8>) -> Self {
112+
Self::Put(RequestWithBody::new(url, body))
113+
}
114+
115+
pub fn patch(url: impl Into<String>, body: Vec<u8>) -> Self {
116+
Self::Patch(RequestWithBody::new(url, body))
117+
}
118+
119+
pub fn with_header(self, name: impl Into<String>, value: impl Into<String>) -> Self {
120+
match self {
121+
Self::Get(head) => Self::Get(head.with_header(name, value)),
122+
Self::Head(head) => Self::Head(head.with_header(name, value)),
123+
Self::Delete(head) => Self::Delete(head.with_header(name, value)),
124+
Self::Options(head) => Self::Options(head.with_header(name, value)),
125+
Self::Post(req) => Self::Post(req.with_header(name, value)),
126+
Self::Put(req) => Self::Put(req.with_header(name, value)),
127+
Self::Patch(req) => Self::Patch(req.with_header(name, value)),
128+
}
129+
}
130+
131+
pub fn url(&self) -> &str {
132+
match self {
133+
Self::Get(h) | Self::Head(h) | Self::Delete(h) | Self::Options(h) => &h.url,
134+
Self::Post(r) | Self::Put(r) | Self::Patch(r) => &r.head.url,
135+
}
136+
}
137+
138+
pub fn headers(&self) -> &[(String, String)] {
139+
match self {
140+
Self::Get(h) | Self::Head(h) | Self::Delete(h) | Self::Options(h) => &h.headers,
141+
Self::Post(r) | Self::Put(r) | Self::Patch(r) => &r.head.headers,
142+
}
143+
}
144+
145+
pub fn body(&self) -> &[u8] {
146+
match self {
147+
Self::Get(_) | Self::Head(_) | Self::Delete(_) | Self::Options(_) => &[],
148+
Self::Post(r) | Self::Put(r) | Self::Patch(r) => &r.body,
149+
}
150+
}
151+
152+
pub fn into_body(self) -> Vec<u8> {
153+
match self {
154+
Self::Get(_) | Self::Head(_) | Self::Delete(_) | Self::Options(_) => Vec::new(),
155+
Self::Post(r) | Self::Put(r) | Self::Patch(r) => r.body,
156+
}
157+
}
158+
159+
pub fn method_str(&self) -> &'static str {
160+
match self {
161+
Self::Get(_) => "GET",
162+
Self::Head(_) => "HEAD",
163+
Self::Delete(_) => "DELETE",
164+
Self::Options(_) => "OPTIONS",
165+
Self::Post(_) => "POST",
166+
Self::Put(_) => "PUT",
167+
Self::Patch(_) => "PATCH",
168+
}
169+
}
170+
}
171+
172+
pub trait HttpClientTrait {
173+
fn request(
174+
req: HttpRequest,
175+
) -> impl Future<Output = Result<HttpResponse, HttpError>> + MaybeSend;
176+
}

libdd-capabilities/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Portable capability traits for cross-platform libdatadog.
5+
6+
pub mod http;
7+
pub mod maybe_send;
8+
9+
pub use http::{
10+
HttpClientTrait, HttpError, HttpRequest, HttpResponse, RequestHead, RequestWithBody,
11+
};
12+
pub use maybe_send::MaybeSend;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Conditional `Send` bound for cross-platform compatibility.
5+
//!
6+
//! On native targets, `MaybeSend` is equivalent to `Send`.
7+
//! On wasm32, `MaybeSend` is auto-implemented for all types.
8+
//!
9+
//! This allows traits to require `Send` on native (for multi-threaded runtimes)
10+
//! while remaining compatible with wasm's single-threaded execution model.
11+
//!
12+
//! # Why This Exists
13+
//!
14+
//! JavaScript interop types (like `JsFuture`, `JsValue`) are **not `Send`**
15+
//! because wasm is single-threaded. But on native, tokio's multi-threaded
16+
//! runtime requires `Send` futures. `MaybeSend` bridges this gap:
17+
//!
18+
//! ```rust,ignore
19+
//! // Instead of:
20+
//! fn request() -> impl Future<Output = Response> + Send; // Won't compile on wasm!
21+
//!
22+
//! // Use:
23+
//! fn request() -> impl Future<Output = Response> + MaybeSend; // Works everywhere!
24+
//! ```
25+
//!
26+
//! # Critical Rule
27+
//!
28+
//! **Never use `+ Send` directly in trait bounds for async functions in
29+
//! wasm-compatible code.** Always use `+ MaybeSend` instead.
30+
31+
/// A trait that is `Send` on native targets, but auto-implemented on wasm.
32+
///
33+
/// Use this instead of `Send` in all capability trait bounds.
34+
#[cfg(not(target_arch = "wasm32"))]
35+
pub trait MaybeSend: Send {}
36+
37+
#[cfg(not(target_arch = "wasm32"))]
38+
impl<T: Send> MaybeSend for T {}
39+
40+
/// On wasm, `MaybeSend` is implemented for all types (no `Send` requirement).
41+
#[cfg(target_arch = "wasm32")]
42+
pub trait MaybeSend {}
43+
44+
#[cfg(target_arch = "wasm32")]
45+
impl<T> MaybeSend for T {}

libdd-common/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ crate-type = ["lib"]
1616
bench = false
1717

1818
[dependencies]
19+
libdd-capabilities = { path = "../libdd-capabilities" }
1920
anyhow = "1.0"
2021
futures = "0.3"
2122
futures-core = { version = "0.3.0", default-features = false }
@@ -37,7 +38,8 @@ regex = "1.5"
3738
reqwest = { version = "0.13", features = ["rustls"], default-features = false, optional = true }
3839
rustls-native-certs = { version = "0.8.1", optional = true }
3940
thiserror = "1.0"
40-
tokio = { version = "1.23", features = ["rt", "macros", "net", "io-util", "fs"] }
41+
tokio = { version = "1.23", features = ["rt", "rt-multi-thread", "macros", "net", "io-util", "fs", "time"] }
42+
bytes = "1"
4143
tokio-rustls = { version = "0.26", default-features = false, optional = true }
4244
serde = { version = "1.0", features = ["derive"] }
4345
static_assertions = "1.1.0"
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! HTTP capability implementation using hyper.
5+
6+
#[cfg(not(target_arch = "wasm32"))]
7+
mod hyper_client {
8+
use crate::{connector::Connector, hyper_migration};
9+
use http_body_util::BodyExt;
10+
use libdd_capabilities::http::{HttpClientTrait, HttpError, HttpRequest, HttpResponse};
11+
use libdd_capabilities::maybe_send::MaybeSend;
12+
use std::sync::OnceLock;
13+
14+
static HTTP_CLIENT: OnceLock<hyper_migration::GenericHttpClient<Connector>> = OnceLock::new();
15+
16+
fn get_client() -> &'static hyper_migration::GenericHttpClient<Connector> {
17+
HTTP_CLIENT.get_or_init(hyper_migration::new_default_client)
18+
}
19+
20+
pub struct HyperHttpClient;
21+
22+
impl HttpClientTrait for HyperHttpClient {
23+
#[allow(clippy::manual_async_fn)]
24+
fn request(
25+
req: HttpRequest,
26+
) -> impl std::future::Future<Output = Result<HttpResponse, HttpError>> + MaybeSend
27+
{
28+
async move {
29+
let client = get_client();
30+
31+
let uri: hyper::Uri = req
32+
.url()
33+
.parse()
34+
.map_err(|e| HttpError::InvalidRequest(format!("Invalid URL: {}", e)))?;
35+
36+
let method = match &req {
37+
HttpRequest::Get(_) => hyper::Method::GET,
38+
HttpRequest::Head(_) => hyper::Method::HEAD,
39+
HttpRequest::Delete(_) => hyper::Method::DELETE,
40+
HttpRequest::Options(_) => hyper::Method::OPTIONS,
41+
HttpRequest::Post(_) => hyper::Method::POST,
42+
HttpRequest::Put(_) => hyper::Method::PUT,
43+
HttpRequest::Patch(_) => hyper::Method::PATCH,
44+
};
45+
46+
let mut builder = hyper::Request::builder().method(method).uri(uri);
47+
48+
for (key, value) in req.headers() {
49+
builder = builder.header(key.as_str(), value.as_str());
50+
}
51+
52+
let body = hyper_migration::Body::from(req.into_body());
53+
let hyper_req = builder.body(body).map_err(|e| {
54+
HttpError::InvalidRequest(format!("Failed to build request: {}", e))
55+
})?;
56+
57+
let response = client
58+
.request(hyper_req)
59+
.await
60+
.map_err(|e| HttpError::Network(format!("Request failed: {}", e)))?;
61+
62+
let status = response.status().as_u16();
63+
64+
let body_collected = response.into_body().collect().await.map_err(|e| {
65+
HttpError::Network(format!("Failed to read response body: {}", e))
66+
})?;
67+
let body_bytes = body_collected.to_bytes();
68+
69+
Ok(HttpResponse {
70+
status,
71+
body: body_bytes.to_vec(),
72+
})
73+
}
74+
}
75+
}
76+
}
77+
78+
#[cfg(not(target_arch = "wasm32"))]
79+
pub use hyper_client::HyperHttpClient;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Capability trait implementations.
5+
6+
#[cfg(not(target_arch = "wasm32"))]
7+
pub mod http;
8+
9+
#[cfg(not(target_arch = "wasm32"))]
10+
pub use http::HyperHttpClient;
11+
12+
pub use libdd_capabilities::{
13+
HttpClientTrait, HttpError, HttpRequest, HttpResponse, RequestHead, RequestWithBody,
14+
};

0 commit comments

Comments
 (0)