Skip to main content

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}