fastedge/lib.rs
1/*
2* Copyright 2025 G-Core Innovations SARL
3*/
4//! # FastEdge Rust SDK
5//!
6//! A comprehensive toolkit for building high-performance edge computing applications using WebAssembly.
7//!
8//! ## Overview
9//!
10//! The `fastedge` crate provides two runtime models:
11//!
12//! * **WebAssembly Component Model** (default): Modern WebAssembly component model using [WIT] (WebAssembly Interface Types)
13//! for type-safe interfaces. This API resides in the root of the `fastedge` crate's namespace.
14//!
15//! * **ProxyWasm API**: Compatibility layer for [ProxyWasm] environments (Envoy, etc.).
16//! Available via the [`fastedge::proxywasm`](`proxywasm`) module when the `proxywasm` feature is enabled.
17//!
18//! * **WASI-HTTP interface**: An alternative to the FastEdge SDK using the standard [WASI-HTTP]
19//! interface via the [`wstd`] crate.
20//!
21//! [WIT]: https://component-model.bytecodealliance.org/design/wit.html
22//! [WebAssembly components]: https://component-model.bytecodealliance.org
23//! [ProxyWasm]: https://github.com/proxy-wasm/spec
24//! [WASI-HTTP]: https://github.com/WebAssembly/wasi-http
25//! [`wstd`]: https://crates.io/crates/wstd
26//!
27//! ## Features
28//!
29//! - **HTTP Request Handling**: Process incoming HTTP requests at the edge
30//! - **Outbound HTTP Client**: Make HTTP requests to backend services via [`send_request`]
31//! - **Key-Value Storage**: Persistent storage with advanced operations (scan, sorted sets, bloom filters)
32//! - **Secret Management**: Secure access to encrypted secrets with time-based retrieval
33//! - **Dictionary**: Fast read-only key-value lookups for configuration
34//! - **WASI-NN**: Machine learning inference support via [`wasi_nn`]
35//!
36//! ## Quick Start
37//!
38//! Add to your `Cargo.toml`:
39//!
40//! ```toml
41//! [dependencies]
42//! fastedge = "0.3"
43//! anyhow = "1.0"
44//!
45//! [lib]
46//! crate-type = ["cdylib"]
47//! ```
48//!
49//! Create a simple HTTP handler:
50//!
51//! ```no_run
52//! use anyhow::Result;
53//! use fastedge::body::Body;
54//! use fastedge::http::{Request, Response, StatusCode};
55//!
56//! #[fastedge::http]
57//! fn main(_req: Request<Body>) -> Result<Response<Body>> {
58//! Response::builder()
59//! .status(StatusCode::OK)
60//! .body(Body::from("Hello, FastEdge!"))
61//! .map_err(Into::into)
62//! }
63//! ```
64//!
65//! Build for WebAssembly:
66//!
67//! ```bash
68//! rustup target add wasm32-wasip1
69//! cargo build --target wasm32-wasip1 --release
70//! ```
71//!
72//! ### Using the WASI-HTTP Interface (async, `wstd`)
73//!
74//! As an alternative to the synchronous FastEdge SDK, you can use the standard [WASI-HTTP]
75//! interface via the [`wstd`] crate. This enables an async handler and a proper HTTP client:
76//!
77//! ```no_run
78//! use wstd::http::body::Body;
79//! use wstd::http::{Client, Request, Response};
80//!
81//! #[wstd::http_server]
82//! async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> {
83//! let upstream_req = Request::get("https://api.example.com/data")
84//! .header("accept", "application/json")
85//! .body(Body::empty())?;
86//!
87//! let response = Client::new().send(upstream_req).await?;
88//! Ok(response)
89//! }
90//! ```
91//!
92//! Build with [`cargo-component`] instead of `cargo build`:
93//!
94//! ```bash
95//! cargo install cargo-component
96//! cargo component build --release
97//! ```
98//!
99//! See the [fetch example] for a complete working app and a side-by-side comparison
100//! with the FastEdge SDK approach.
101//!
102//! [`cargo-component`]: https://github.com/bytecodealliance/cargo-component
103//! [fetch example]: https://github.com/G-Core/FastEdge-sdk-rust/tree/main/examples/fetch
104//!
105//! ## Feature Flags
106//!
107//! - `proxywasm` (default): Enable ProxyWasm compatibility layer
108//! - `json`: Enable JSON body support via `serde_json`
109//!
110//! ## Examples
111//!
112//! ### Making HTTP Requests
113//!
114//! ```no_run
115//! use anyhow::Result;
116//! use fastedge::body::Body;
117//! use fastedge::http::{Method, Request, Response, StatusCode};
118//!
119//! #[fastedge::http]
120//! fn main(_req: Request<Body>) -> Result<Response<Body>> {
121//! // Create a request to a backend service
122//! let backend_request = Request::builder()
123//! .method(Method::GET)
124//! .uri("https://api.example.com/data")
125//! .header("User-Agent", "FastEdge/1.0")
126//! .body(Body::empty())?;
127//!
128//! // Send the request
129//! let backend_response = fastedge::send_request(backend_request)?;
130//!
131//! // Return the response
132//! Response::builder()
133//! .status(StatusCode::OK)
134//! .body(backend_response.into_body())
135//! .map_err(Into::into)
136//! }
137//! ```
138//!
139//! ### Using Key-Value Storage
140//!
141//! ```no_run
142//! use anyhow::Result;
143//! use fastedge::body::Body;
144//! use fastedge::http::{Request, Response, StatusCode};
145//! use fastedge::key_value::Store;
146//!
147//! #[fastedge::http]
148//! fn main(_req: Request<Body>) -> Result<Response<Body>> {
149//! // Open a key-value store
150//! let store = Store::open("my-store")?;
151//!
152//! // Get a value
153//! if let Some(value) = store.get("user:123")? {
154//! return Response::builder()
155//! .status(StatusCode::OK)
156//! .body(Body::from(value))
157//! .map_err(Into::into);
158//! }
159//!
160//! Response::builder()
161//! .status(StatusCode::NOT_FOUND)
162//! .body(Body::empty())
163//! .map_err(Into::into)
164//! }
165//! ```
166//!
167//! ### Accessing Secrets
168//!
169//! ```no_run
170//! use anyhow::Result;
171//! use fastedge::body::Body;
172//! use fastedge::http::{Request, Response, StatusCode};
173//! use fastedge::secret;
174//!
175//! #[fastedge::http]
176//! fn main(_req: Request<Body>) -> Result<Response<Body>> {
177//! // Get a secret value
178//! match secret::get("API_KEY")? {
179//! Some(api_key) => {
180//! // Use the API key
181//! let key = String::from_utf8_lossy(&api_key);
182//! Response::builder()
183//! .status(StatusCode::OK)
184//! .body(Body::from("Secret retrieved"))
185//! .map_err(Into::into)
186//! }
187//! None => {
188//! Response::builder()
189//! .status(StatusCode::NOT_FOUND)
190//! .body(Body::from("Secret not found"))
191//! .map_err(Into::into)
192//! }
193//! }
194//! }
195//! ```
196pub extern crate http;
197
198pub use fastedge_derive::http;
199pub use http_client::send_request;
200
201#[doc(hidden)]
202pub use crate::exports::gcore::fastedge::http_handler;
203use crate::gcore::fastedge::http::{Error as HttpError, Method, Request, Response};
204
205mod helper;
206
207/// Implementation of Outbound HTTP component
208mod http_client;
209
210/// FastEdge ProxyWasm module extension
211#[cfg(feature = "proxywasm")]
212pub mod proxywasm;
213
214pub mod wasi_nn {
215 #![allow(missing_docs)]
216 wit_bindgen::generate!({
217 world: "ml",
218 path: "wasi-nn/wit"
219 });
220}
221
222wit_bindgen::generate!({
223 world: "reactor",
224 path: "wit",
225 pub_export_macro: true,
226});
227
228/// Fast read-only key-value dictionary for configuration.
229///
230/// The dictionary provides efficient access to read-only configuration values.
231/// It's optimized for fast lookups and is ideal for static configuration that
232/// doesn't change during request processing.
233///
234/// # Examples
235///
236/// ```no_run
237/// use fastedge::dictionary;
238///
239/// // Get a configuration value
240/// if let Some(config) = dictionary::get("api_endpoint")? {
241/// let endpoint = String::from_utf8_lossy(&config);
242/// println!("API endpoint: {}", endpoint);
243/// }
244/// # Ok::<(), Box<dyn std::error::Error>>(())
245/// ```
246pub mod dictionary {
247 #[doc(inline)]
248 pub use crate::gcore::fastedge::dictionary::get;
249}
250
251/// Secure access to encrypted secrets and credentials.
252///
253/// The secret module provides secure storage and retrieval of sensitive data such as
254/// API keys, passwords, and certificates. Secrets are encrypted at rest and support
255/// versioning with time-based retrieval.
256///
257/// # Security
258///
259/// - Secrets are encrypted and can only be accessed by authorized applications
260/// - Access is controlled via platform permissions
261/// - Never log or expose secret values in responses
262///
263/// # Examples
264///
265/// ```no_run
266/// use fastedge::secret;
267///
268/// // Get current secret value
269/// match secret::get("DATABASE_PASSWORD")? {
270/// Some(password) => {
271/// let pwd = String::from_utf8_lossy(&password);
272/// // Use the password to connect to database
273/// }
274/// None => {
275/// eprintln!("Secret not found");
276/// }
277/// }
278///
279/// // Get secret value at a specific time (for rotation scenarios)
280/// use std::time::{SystemTime, UNIX_EPOCH};
281/// let timestamp = SystemTime::now()
282/// .duration_since(UNIX_EPOCH)?
283/// .as_secs() as u32;
284/// let historical = secret::get_effective_at("API_KEY", timestamp)?;
285/// # Ok::<(), Box<dyn std::error::Error>>(())
286/// ```
287pub mod secret {
288 #[doc(inline)]
289 pub use crate::gcore::fastedge::secret::get;
290 #[doc(inline)]
291 pub use crate::gcore::fastedge::secret::get_effective_at;
292 pub use crate::gcore::fastedge::secret::Error;
293}
294
295/// Persistent key-value storage with advanced data structures.
296///
297/// The key-value module provides a persistent storage interface with support for:
298/// - Simple key-value pairs
299/// - Pattern-based scanning with glob-style patterns
300/// - Sorted sets with score-based range queries
301/// - Bloom filters for probabilistic set membership testing
302///
303/// # Storage Model
304///
305/// Data is organized into named stores. Applications must be granted access to specific
306/// stores via platform configuration.
307///
308/// # Examples
309///
310/// ## Basic Operations
311///
312/// ```no_run
313/// use fastedge::key_value::Store;
314///
315/// // Open a store
316/// let store = Store::open("user-data")?;
317///
318/// // Get a value
319/// if let Some(data) = store.get("user:123:profile")? {
320/// let profile = String::from_utf8_lossy(&data);
321/// println!("Profile: {}", profile);
322/// }
323/// # Ok::<(), Box<dyn std::error::Error>>(())
324/// ```
325///
326/// ## Pattern Scanning
327///
328/// ```no_run
329/// use fastedge::key_value::Store;
330///
331/// let store = Store::open("user-data")?;
332///
333/// // Find all keys matching a pattern
334/// let user_keys = store.scan("user:123:*")?;
335/// for key in user_keys {
336/// println!("Found key: {}", key);
337/// }
338/// # Ok::<(), Box<dyn std::error::Error>>(())
339/// ```
340///
341/// ## Sorted Sets (Leaderboards)
342///
343/// ```no_run
344/// use fastedge::key_value::Store;
345///
346/// let store = Store::open("game-data")?;
347///
348/// // Get top players by score (score between 1000 and infinity)
349/// let top_players = store.zrange_by_score("leaderboard", 1000.0, f64::INFINITY)?;
350/// for (player_id, score) in top_players {
351/// println!("Player: {:?}, Score: {}", player_id, score);
352/// }
353/// # Ok::<(), Box<dyn std::error::Error>>(())
354/// ```
355///
356/// ## Bloom Filters
357///
358/// ```no_run
359/// use fastedge::key_value::Store;
360///
361/// let store = Store::open("cache")?;
362///
363/// // Check if an item is in a bloom filter
364/// if store.bf_exists("seen_urls", "https://example.com")? {
365/// println!("URL was probably seen before");
366/// }
367/// # Ok::<(), Box<dyn std::error::Error>>(())
368/// ```
369pub mod key_value {
370 #[doc(inline)]
371 pub use crate::gcore::fastedge::key_value::Store;
372 #[doc(inline)]
373 pub use crate::gcore::fastedge::key_value::Error;
374}
375
376/// FastEdge-specific utility functions for diagnostics and statistics.
377///
378/// This module provides utilities for debugging and monitoring your FastEdge applications.
379///
380/// # Examples
381///
382/// ```no_run
383/// use fastedge::utils::set_user_diag;
384///
385/// // Set diagnostic information for debugging
386/// set_user_diag("Processing user request: 12345");
387/// ```
388///
389/// Diagnostic messages can be viewed in the FastEdge platform logs.
390pub mod utils {
391 #[doc(inline)]
392 pub use crate::gcore::fastedge::utils::set_user_diag;
393}
394
395/// Errors that can occur when using the FastEdge SDK.
396///
397/// This error type is returned by [`send_request`] and other SDK functions.
398///
399/// # Examples
400///
401/// ```
402/// use fastedge::Error;
403/// use fastedge::http::Method;
404///
405/// // Handling errors
406/// fn handle_error(err: Error) {
407/// match err {
408/// Error::UnsupportedMethod(method) => {
409/// eprintln!("Method {} is not supported", method);
410/// }
411/// Error::InvalidStatusCode(code) => {
412/// eprintln!("Invalid status code: {}", code);
413/// }
414/// _ => {
415/// eprintln!("An error occurred: {}", err);
416/// }
417/// }
418/// }
419/// ```
420#[derive(thiserror::Error, Debug)]
421pub enum Error {
422 /// The HTTP method is not supported by the FastEdge runtime.
423 ///
424 /// Only the following methods are supported: GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS.
425 #[error("method `{0}` is not supported")]
426 UnsupportedMethod(::http::Method),
427 /// An error occurred in the underlying HTTP client component.
428 #[error("http error: {0}")]
429 BindgenHttpError(#[from] HttpError),
430 /// An error occurred while building or parsing an HTTP message.
431 #[error("http error: {0}")]
432 HttpError(#[from] ::http::Error),
433 /// The HTTP body is invalid or could not be processed.
434 #[error("invalid http body")]
435 InvalidBody,
436 /// The HTTP status code is invalid (not in range 100-599).
437 #[error("invalid status code {0}")]
438 InvalidStatusCode(u16),
439}
440
441/// HTTP request and response body types.
442///
443/// This module provides the [`Body`] type which wraps [`Bytes`] and tracks content-type information.
444/// The body type automatically handles content-type detection based on the input data.
445///
446/// # Examples
447///
448/// ```
449/// use fastedge::body::Body;
450///
451/// // Create from string - automatically sets content-type to text/plain
452/// let text_body = Body::from("Hello, world!");
453/// assert_eq!(text_body.content_type(), "text/plain; charset=utf-8");
454///
455/// // Create from bytes - automatically sets content-type to application/octet-stream
456/// let bytes_body = Body::from(vec![1, 2, 3, 4]);
457/// assert_eq!(bytes_body.content_type(), "application/octet-stream");
458///
459/// // Create empty body
460/// let empty_body = Body::empty();
461/// assert_eq!(empty_body.len(), 0);
462/// ```
463pub mod body {
464 use std::ops::Deref;
465
466 use bytes::Bytes;
467
468 /// HTTP request/response body with content-type tracking.
469 ///
470 /// The `Body` type wraps [`Bytes`] and maintains content-type information.
471 /// It automatically detects and sets appropriate MIME types based on the input data.
472 ///
473 /// # Examples
474 ///
475 /// ```
476 /// use fastedge::body::Body;
477 ///
478 /// // From string
479 /// let body = Body::from("Hello");
480 ///
481 /// // From bytes
482 /// let body = Body::from(vec![1, 2, 3]);
483 ///
484 /// // Empty body
485 /// let body = Body::empty();
486 /// ```
487 #[derive(Debug)]
488 pub struct Body {
489 pub(crate) content_type: String,
490 pub(crate) inner: Bytes,
491 }
492
493 impl Deref for Body {
494 type Target = Bytes;
495
496 fn deref(&self) -> &Self::Target {
497 &self.inner
498 }
499 }
500
501 impl From<String> for Body {
502 fn from(value: String) -> Self {
503 Body {
504 content_type: mime::TEXT_PLAIN_UTF_8.to_string(),
505 inner: Bytes::from(value),
506 }
507 }
508 }
509
510 impl From<&'static str> for Body {
511 fn from(value: &'static str) -> Self {
512 Body {
513 content_type: mime::TEXT_PLAIN_UTF_8.to_string(),
514 inner: Bytes::from(value),
515 }
516 }
517 }
518
519 impl From<Vec<u8>> for Body {
520 fn from(value: Vec<u8>) -> Self {
521 Body {
522 content_type: mime::APPLICATION_OCTET_STREAM.to_string(),
523 inner: Bytes::from(value),
524 }
525 }
526 }
527
528 impl From<&'static [u8]> for Body {
529 fn from(value: &'static [u8]) -> Self {
530 Body {
531 content_type: mime::APPLICATION_OCTET_STREAM.to_string(),
532 inner: Bytes::from(value),
533 }
534 }
535 }
536
537 #[cfg(feature = "json")]
538 impl TryFrom<serde_json::Value> for Body {
539 type Error = serde_json::Error;
540 fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
541 Ok(Body {
542 content_type: mime::APPLICATION_JSON.to_string(),
543 inner: Bytes::from(serde_json::to_vec(&value)?),
544 })
545 }
546 }
547
548 impl Default for Body {
549 fn default() -> Self {
550 Self {
551 content_type: mime::TEXT_PLAIN_UTF_8.to_string(),
552 inner: Bytes::default(),
553 }
554 }
555 }
556
557 impl Body {
558 /// Creates an empty body with default content-type.
559 ///
560 /// # Examples
561 ///
562 /// ```
563 /// use fastedge::body::Body;
564 ///
565 /// let body = Body::empty();
566 /// assert_eq!(body.len(), 0);
567 /// ```
568 pub fn empty() -> Self {
569 Body::default()
570 }
571
572 /// Returns the MIME content-type of this body.
573 ///
574 /// The content-type is automatically determined when the body is created:
575 /// - Text strings: `text/plain; charset=utf-8`
576 /// - Byte arrays: `application/octet-stream`
577 /// - JSON (with `json` feature): `application/json`
578 ///
579 /// # Examples
580 ///
581 /// ```
582 /// use fastedge::body::Body;
583 ///
584 /// let body = Body::from("Hello");
585 /// assert_eq!(body.content_type(), "text/plain; charset=utf-8");
586 /// ```
587 pub fn content_type(&self) -> String {
588 self.content_type.to_owned()
589 }
590 }
591}
592
593impl From<Method> for ::http::Method {
594 fn from(method: Method) -> Self {
595 match method {
596 Method::Get => ::http::Method::GET,
597 Method::Post => ::http::Method::POST,
598 Method::Put => ::http::Method::PUT,
599 Method::Delete => ::http::Method::DELETE,
600 Method::Head => ::http::Method::HEAD,
601 Method::Patch => ::http::Method::PATCH,
602 Method::Options => ::http::Method::OPTIONS,
603 }
604 }
605}
606
607impl TryFrom<Request> for ::http::Request<body::Body> {
608 type Error = Error;
609
610 fn try_from(req: Request) -> Result<Self, Self::Error> {
611 let builder = ::http::Request::builder()
612 .method(::http::Method::from(req.method))
613 .uri(req.uri.to_string());
614 let builder = req
615 .headers
616 .iter()
617 .fold(builder, |builder, (k, v)| builder.header(k, v));
618
619 let body = req.body.map_or_else(body::Body::empty, body::Body::from);
620 builder.body(body).map_err(|_| Error::InvalidBody)
621 }
622}
623
624impl From<::http::Response<body::Body>> for Response {
625 fn from(res: ::http::Response<body::Body>) -> Self {
626 let status = res.status().as_u16();
627 let headers = if !res.headers().is_empty() {
628 Some(
629 res.headers()
630 .iter()
631 .map(|(name, value)| (name.to_string(), value.to_str().unwrap().to_string()))
632 .collect::<Vec<(String, String)>>(),
633 )
634 } else {
635 None
636 };
637
638 let body = Some(res.into_body().to_vec());
639
640 Response {
641 status,
642 headers,
643 body,
644 }
645 }
646}
647
648impl TryFrom<Response> for ::http::Response<body::Body> {
649 type Error = Error;
650
651 fn try_from(res: Response) -> Result<Self, Self::Error> {
652 let builder = ::http::Response::builder().status(
653 ::http::StatusCode::try_from(res.status)
654 .map_err(|_| Error::InvalidStatusCode(res.status))?,
655 );
656 let builder = if let Some(headers) = res.headers {
657 headers
658 .iter()
659 .fold(builder, |builder, (k, v)| builder.header(k, v))
660 } else {
661 builder
662 };
663
664 let body = res.body.map_or_else(body::Body::empty, body::Body::from);
665 builder.body(body).map_err(|_| Error::InvalidBody)
666 }
667}