From 9229c58d5306d0a5b119d175ca83780238491575 Mon Sep 17 00:00:00 2001 From: itowlson Date: Tue, 11 Feb 2025 13:19:29 +1300 Subject: [PATCH] Add examples to rustdoc Signed-off-by: itowlson --- .github/workflows/build.yml | 3 + src/http.rs | 418 +++++++++++++++++++++++++++++++++++- src/http/router.rs | 116 +++++++++- src/key_value.rs | 107 ++++++++- src/lib.rs | 14 +- src/llm.rs | 50 ++++- src/sqlite.rs | 249 ++++++++++++++++++++- 7 files changed, 935 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ccccaa..bb65622 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,3 +39,6 @@ jobs: shell: bash run: cargo test --workspace + - name: Validate docs examples + shell: bash + run: cargo test --doc diff --git a/src/http.rs b/src/http.rs index abd2fc0..0dab8fd 100644 --- a/src/http.rs +++ b/src/http.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub use conversions::IntoResponse; #[doc(inline)] pub use types::{ - ErrorCode, Fields, Headers, IncomingRequest, IncomingResponse, Method, OutgoingBody, - OutgoingRequest, OutgoingResponse, Scheme, StatusCode, Trailers, + ErrorCode, Headers, IncomingResponse, Method, OutgoingBody, OutgoingRequest, Scheme, + StatusCode, Trailers, }; use self::conversions::{TryFromIncomingResponse, TryIntoOutgoingRequest}; @@ -16,10 +16,253 @@ use super::wit::wasi::http0_2_0::types; use futures::SinkExt; use spin_executor::bindings::wasi::io::streams::{self, StreamError}; +/// Represents an incoming HTTP request. +/// +/// If you don't need streaming access to the request body, you may find it +/// easier to work with [Request] instead. To make outgoing requests, use +/// [Request] (non-streaming) or [OutgoingRequest]. +/// +/// # Examples +/// +/// Access the request body as a Rust stream: +/// +/// ```no_run +/// # use spin_sdk::http::{IncomingRequest, ResponseOutparam}; +/// async fn handle_request(req: IncomingRequest, response_outparam: ResponseOutparam) { +/// use futures::stream::StreamExt; +/// +/// let mut stream = req.into_body_stream(); +/// loop { +/// let chunk = stream.next().await; +/// match chunk { +/// None => { +/// println!("end of request body"); +/// break; +/// } +/// Some(Ok(chunk)) => { +/// // process the data from the stream in a very realistic way +/// println!("read {} bytes", chunk.len()); +/// } +/// Some(Err(e)) => { +/// println!("error reading body: {e:?}"); +/// break; +/// } +/// } +/// } +/// } +/// ``` +/// +/// Access the body in a non-streaming way. This can be useful where your component +/// must take IncomingRequest because some scenarios need streaming, but you +/// have other scenarios that do not. +/// +/// ```no_run +/// # use spin_sdk::http::{IncomingRequest, ResponseOutparam}; +/// async fn handle_request(req: IncomingRequest, response_outparam: ResponseOutparam) { +/// let body = req.into_body().await.unwrap(); +/// } +/// ``` +#[doc(inline)] +pub use types::IncomingRequest; + +/// Represents an outgoing HTTP response. +/// +/// OutgoingResponse is used in conjunction with [ResponseOutparam] in cases where +/// you want to stream the response body. In cases where you don't need to stream, +/// it is often simpler to use [Response]. +/// +/// # Examples +/// +/// Send a streaming response to an incoming request: +/// +/// ```no_run +/// # use spin_sdk::http::{Fields, IncomingRequest, OutgoingResponse, ResponseOutparam}; +/// async fn handle_request(req: IncomingRequest, response_outparam: ResponseOutparam) { +/// use futures::SinkExt; +/// use std::io::Read; +/// +/// let response_headers = Fields::from_list(&[ +/// ("content-type".to_owned(), "text/plain".into()) +/// ]).unwrap(); +/// +/// let response = OutgoingResponse::new(response_headers); +/// response.set_status_code(200).unwrap(); +/// let mut response_body = response.take_body(); +/// +/// response_outparam.set(response); +/// +/// let mut file = std::fs::File::open("war-and-peace.txt").unwrap(); +/// +/// loop { +/// let mut buf = vec![0; 1024]; +/// let count = file.read(&mut buf).unwrap(); +/// +/// if count == 0 { +/// break; // end of file +/// } +/// +/// buf.truncate(count); +/// response_body.send(buf).await.unwrap(); +/// } +/// } +/// ``` +/// +/// Send a response then continue processing: +/// +/// ```no_run +/// # use spin_sdk::http::{Fields, IncomingRequest, OutgoingResponse, ResponseOutparam}; +/// async fn handle_request(req: IncomingRequest, response_outparam: ResponseOutparam) { +/// use futures::SinkExt; +/// +/// let response_headers = Fields::from_list(&[ +/// ("content-type".to_owned(), "text/plain".into()) +/// ]).unwrap(); +/// +/// let response = OutgoingResponse::new(response_headers); +/// response.set_status_code(200).unwrap(); +/// let mut response_body = response.take_body(); +/// +/// response_outparam.set(response); +/// +/// response_body +/// .send("Request accepted".as_bytes().to_vec()) +/// .await +/// .unwrap(); +/// +/// // End the HTTP response so the client deems it complete. +/// response_body.flush().await.unwrap(); +/// response_body.close().await.unwrap(); +/// drop(response_body); +/// +/// // Perform any additional processing +/// println!("While the cat's away, the mice will play"); +/// } +/// ``` +#[doc(inline)] +pub use types::OutgoingResponse; + +/// A common representation for headers and trailers. +/// +/// # Examples +/// +/// Initialise response headers from a list: +/// +/// ```no_run +/// # use spin_sdk::http::{Fields, IncomingRequest, OutgoingResponse}; +/// # fn handle_request(req: IncomingRequest) { +/// let response_headers = Fields::from_list(&[ +/// ("content-type".to_owned(), "text/plain".into()) +/// ]).unwrap(); +/// +/// let response = OutgoingResponse::new(response_headers); +/// # } +/// ``` +/// +/// Build response headers up dynamically: +/// +/// ```no_run +/// # use spin_sdk::http::{Fields, IncomingRequest, OutgoingResponse}; +/// # fn handle_request(req: IncomingRequest) { +/// let accepts_json = req.headers() +/// .get(&"accept".to_owned()) +/// .iter() +/// .flat_map(|v| String::from_utf8(v.clone()).ok()) +/// .any(|s| s == "application/json"); +/// +/// let response_headers = Fields::new(); +/// if accepts_json { +/// response_headers.set(&"content-type".to_owned(), &["application/json".into()]).unwrap(); +/// }; +/// # } +/// ``` +/// +/// # WASI resource documentation +/// +#[doc(inline)] +pub use types::Fields; + /// A unified request object that can represent both incoming and outgoing requests. /// -/// This should be used in favor of `IncomingRequest` and `OutgoingRequest` when there +/// This should be used in favor of [IncomingRequest] and [OutgoingRequest] when there /// is no need for streaming bodies. +/// +/// # Examples +/// +/// Read the method, a header, and the body an incoming HTTP request, without streaming: +/// +/// ```no_run +/// # use spin_sdk::http::{Method, Request, Response}; +/// +/// fn handle_request(req: Request) -> anyhow::Result { +/// let method = req.method(); +/// let content_type = req.header("content-type"); +/// if *method == Method::Post { +/// let body = String::from_utf8_lossy(req.body()); +/// } +/// todo!() +/// } +/// ``` +/// +/// Send an outgoing GET request (no body) to `example.com`: +/// +/// ```no_run +/// use spin_sdk::http::{Request, Response}; +/// +/// # #[tokio::main] +/// # async fn main() -> anyhow::Result<()> { +/// let request = Request::get("https://example.com"); +/// let response: Response = spin_sdk::http::send(request).await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Send an outgoing POST request with a non-streaming body to `example.com`. +/// +/// ```no_run +/// use spin_sdk::http::{Request, Response}; +/// +/// # #[tokio::main] +/// # async fn main() -> anyhow::Result<()> { +/// let request = Request::post("https://example.com", "it's a-me, Spin") +/// .header("content-type", "text/plain") +/// .build(); +/// let response: Response = spin_sdk::http::send(request).await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Build and send an outgoing request without using the helper shortcut. +/// +/// ```no_run +/// use spin_sdk::http::{Method, Request, Response}; +/// +/// # #[tokio::main] +/// # async fn main() -> anyhow::Result<()> { +/// let mut request = Request::new(Method::Put, "https://example.com/message/safety"); +/// request.set_header("content-type", "text/plain"); +/// *request.body_mut() = "beware the crocodile".as_bytes().to_vec(); +/// let response: Response = spin_sdk::http::send(request).await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Build and send an outgoing request using the fluent builder. +/// +/// ```no_run +/// use spin_sdk::http::{Method, Request, Response}; +/// +/// # #[tokio::main] +/// # async fn main() -> anyhow::Result<()> { +/// let request = Request::builder() +/// .uri("https://example.com/message/motivational") +/// .method(Method::Put) +/// .header("content-type", "text/plain") +/// .body("the capybaras of creativity fly higher than the bluebirds of banality") +/// .build(); +/// let response: Response = spin_sdk::http::send(request).await?; +/// # Ok(()) +/// # } +/// ``` pub struct Request { /// The method of the request method: Method, @@ -179,7 +422,44 @@ impl Request { } } -/// A request builder +/// A builder for non-streaming outgoing HTTP requests. You can obtain +/// a RequestBuilder from the [Request::builder()] method, or from +/// method-specific helpers such as [Request::get()] or [Request::post()]. +/// +/// # Examples +/// +/// Use a method helper to build an outgoing POST request: +/// +/// ```no_run +/// use spin_sdk::http::{Request, Response}; +/// +/// # #[tokio::main] +/// # async fn main() -> anyhow::Result<()> { +/// let request = Request::post("https://example.com", "it's a-me, Spin") +/// .header("content-type", "text/plain") +/// .build(); +/// let response: Response = spin_sdk::http::send(request).await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Build and send an outgoing request using the RequestBuilder. +/// +/// ```no_run +/// use spin_sdk::http::{Method, Request, Response}; +/// +/// # #[tokio::main] +/// # async fn main() -> anyhow::Result<()> { +/// let request = Request::builder() +/// .uri("https://example.com/message/motivational") +/// .method(Method::Put) +/// .header("content-type", "text/plain") +/// .body("the capybaras of creativity fly higher than the bluebirds of banality") +/// .build(); +/// let response: Response = spin_sdk::http::send(request).await?; +/// # Ok(()) +/// # } +/// ``` pub struct RequestBuilder { request: Request, } @@ -234,6 +514,43 @@ impl RequestBuilder { /// /// This should be used in favor of `OutgoingResponse` and `IncomingResponse` when there /// is no need for streaming bodies. +/// +/// # Examples +/// +/// Send a response to an incoming HTTP request: +/// +/// ```no_run +/// use spin_sdk::http::{Request, Response}; +/// +/// fn handle_request(req: Request) -> anyhow::Result { +/// Ok(Response::builder() +/// .status(200) +/// .header("content-type", "text/plain") +/// .body("Hello, world") +/// .build()) +/// } +/// ``` +/// +/// Parse a response from an outgoing HTTP request: +/// +/// ```no_run +/// # use spin_sdk::http::{Request, Response}; +/// #[derive(serde::Deserialize)] +/// struct User { +/// name: String, +/// } +/// +/// # #[tokio::main] +/// # async fn main() -> anyhow::Result<()> { +/// let request = Request::get("https://example.com"); +/// let response: Response = spin_sdk::http::send(request).await?; +/// if *response.status() == 200 { +/// let body = response.body(); +/// let user: User = serde_json::from_slice(body)?; +/// } +/// # Ok(()) +/// # } +/// ``` pub struct Response { /// The status of the response status: StatusCode, @@ -577,7 +894,43 @@ impl OutgoingRequest { } } -/// The out param for setting an `OutgoingResponse` +/// A parameter provided by Spin for setting a streaming [OutgoingResponse]. +/// +/// # Examples +/// +/// Send a streaming response to an incoming request: +/// +/// ```no_run +/// # use spin_sdk::http::{Fields, IncomingRequest, OutgoingResponse, ResponseOutparam}; +/// async fn handle_request(req: IncomingRequest, response_outparam: ResponseOutparam) { +/// use futures::SinkExt; +/// use std::io::Read; +/// +/// let response_headers = Fields::from_list(&[ +/// ("content-type".to_owned(), "text/plain".into()) +/// ]).unwrap(); +/// +/// let response = OutgoingResponse::new(response_headers); +/// response.set_status_code(200).unwrap(); +/// let mut response_body = response.take_body(); +/// +/// response_outparam.set(response); +/// +/// let mut file = std::fs::File::open("war-and-peace.txt").unwrap(); +/// +/// loop { +/// let mut buf = vec![0; 1024]; +/// let count = file.read(&mut buf).unwrap(); +/// +/// if count == 0 { +/// break; // end of file +/// } +/// +/// buf.truncate(count); +/// response_body.send(buf).await.unwrap(); +/// } +/// } +/// ``` pub struct ResponseOutparam(types::ResponseOutparam); impl ResponseOutparam { @@ -613,6 +966,59 @@ impl ResponseOutparam { } /// Send an outgoing request +/// +/// # Examples +/// +/// Get the example.com home page: +/// +/// ```no_run +/// use spin_sdk::http::{Request, Response}; +/// +/// # #[tokio::main] +/// # async fn main() -> anyhow::Result<()> { +/// let request = Request::get("example.com").build(); +/// let response: Response = spin_sdk::http::send(request).await?; +/// println!("{}", response.body().len()); +/// # Ok(()) +/// # } +/// ``` +/// +/// Use the `http` crate Request type to send a data transfer value: +/// +/// ```no_run +/// use hyperium::Request; +/// +/// #[derive(serde::Serialize)] +/// struct User { +/// name: String, +/// } +/// +/// impl spin_sdk::http::conversions::TryIntoBody for User { +/// type Error = serde_json::Error; +/// +/// fn try_into_body(self) -> Result, Self::Error> { +/// serde_json::to_vec(&self) +/// } +/// } +/// +/// # #[tokio::main] +/// # async fn main() -> anyhow::Result<()> { +/// let user = User { +/// name: "Alice".to_owned(), +/// }; +/// +/// let request = hyperium::Request::builder() +/// .method("POST") +/// .uri("https://example.com/users") +/// .header("content-type", "application/json") +/// .body(user)?; +/// +/// let response: hyperium::Response<()> = spin_sdk::http::send(request).await?; +/// +/// println!("{}", response.status().is_success()); +/// # Ok(()) +/// # } +/// ``` pub async fn send(request: I) -> Result where I: TryIntoOutgoingRequest, @@ -692,7 +1098,7 @@ impl std::fmt::Display for NonUtf8BodyError { } mod router; -/// Exports HTTP Router items. +#[doc(inline)] pub use router::*; /// A Body extractor diff --git a/src/http/router.rs b/src/http/router.rs index ca04046..8f43033 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -39,7 +39,121 @@ where /// Route parameters extracted from a URI that match a route pattern. pub type Params = Captures<'static, 'static>; -/// The Spin SDK HTTP router. +/// Routes HTTP requests within a Spin component. +/// +/// Routes may contain wildcards: +/// +/// * `:name` is a single segment wildcard. The handler can retrieve it using +/// [Params::get()]. +/// * `*` is a trailing wildcard (matches anything). The handler can retrieve it +/// using [Params::wildcard()]. +/// +/// If a request matches more than one route, the match is selected according to the follow criteria: +/// +/// * An exact route takes priority over any wildcard. +/// * A single segment wildcard takes priority over a trailing wildcard. +/// +/// (This is the same logic as overlapping routes in the Spin manifest.) +/// +/// # Examples +/// +/// Handle GET requests to a path with a wildcard, falling back to "not found": +/// +/// ```no_run +/// # use spin_sdk::http::{IntoResponse, Params, Request, Response, Router}; +/// fn handle_route(req: Request) -> Response { +/// let mut router = Router::new(); +/// router.get("/hello/:planet", hello_planet); +/// router.any("/*", not_found); +/// router.handle(req) +/// } +/// +/// fn hello_planet(req: Request, params: Params) -> anyhow::Result { +/// let planet = params.get("planet").unwrap_or("world"); +/// Ok(Response::new(200, format!("hello, {planet}"))) +/// } +/// +/// fn not_found(req: Request, params: Params) -> anyhow::Result { +/// Ok(Response::new(404, "not found")) +/// } +/// ``` +/// +/// Handle requests using a mix of synchronous and asynchronous handlers: +/// +/// ```no_run +/// # use spin_sdk::http::{IntoResponse, Params, Request, Response, Router}; +/// fn handle_route(req: Request) -> Response { +/// let mut router = Router::new(); +/// router.get("/hello/:planet", hello_planet); +/// router.get_async("/goodbye/:planet", goodbye_planet); +/// router.handle(req) +/// } +/// +/// fn hello_planet(req: Request, params: Params) -> anyhow::Result { +/// todo!() +/// } +/// +/// async fn goodbye_planet(req: Request, params: Params) -> anyhow::Result { +/// todo!() +/// } +/// ``` +/// +/// Route differently according to HTTP method: +/// +/// ```no_run +/// # use spin_sdk::http::{IntoResponse, Params, Request, Response, Router}; +/// fn handle_route(req: Request) -> Response { +/// let mut router = Router::new(); +/// router.get("/user", list_users); +/// router.post("/user", create_user); +/// router.get("/user/:id", get_user); +/// router.put("/user/:id", update_user); +/// router.delete("/user/:id", delete_user); +/// router.any("/user", method_not_allowed); +/// router.any("/user/:id", method_not_allowed); +/// router.handle(req) +/// } +/// # fn list_users(req: Request, params: Params) -> anyhow::Result { todo!() } +/// # fn create_user(req: Request, params: Params) -> anyhow::Result { todo!() } +/// # fn get_user(req: Request, params: Params) -> anyhow::Result { todo!() } +/// # fn update_user(req: Request, params: Params) -> anyhow::Result { todo!() } +/// # fn delete_user(req: Request, params: Params) -> anyhow::Result { todo!() } +/// # fn method_not_allowed(req: Request, params: Params) -> anyhow::Result { todo!() } +/// ``` +/// +/// Run the handler asynchronously: +/// +/// ```no_run +/// # use spin_sdk::http::{IntoResponse, Params, Request, Response, Router}; +/// async fn handle_route(req: Request) -> Response { +/// let mut router = Router::new(); +/// router.get_async("/user", list_users); +/// router.post_async("/user", create_user); +/// router.handle_async(req).await +/// } +/// # async fn list_users(req: Request, params: Params) -> anyhow::Result { todo!() } +/// # async fn create_user(req: Request, params: Params) -> anyhow::Result { todo!() } +/// ``` +/// +/// Priority when routes overlap: +/// +/// ```no_run +/// # use spin_sdk::http::{IntoResponse, Params, Request, Response, Router}; +/// fn handle_route(req: Request) -> Response { +/// let mut router = Router::new(); +/// router.any("/*", handle_any); +/// router.any("/:seg", handle_single_segment); +/// router.any("/fie", handle_exact); +/// +/// // '/fie' is routed to `handle_exact` +/// // '/zounds' is routed to `handle_single_segment` +/// // '/zounds/fie' is routed to `handle_any` +/// router.handle(req) +/// } +/// # fn handle_any(req: Request, params: Params) -> anyhow::Result { todo!() } +/// # fn handle_single_segment(req: Request, params: Params) -> anyhow::Result { todo!() } +/// # fn handle_exact(req: Request, params: Params) -> anyhow::Result { todo!() } +/// ``` pub struct Router { methods_map: HashMap>>, any_methods: MethodRouter>, diff --git a/src/key_value.rs b/src/key_value.rs index 7a62a8f..a6d8c46 100644 --- a/src/key_value.rs +++ b/src/key_value.rs @@ -3,6 +3,18 @@ //! This module provides a generic interface for key-value storage, which may be implemented by the host various //! ways (e.g. via an in-memory table, a local file, or a remote database). Details such as consistency model and //! durability will depend on the implementation and may vary from one to store to the next. +//! +//! # Examples +//! +//! Open the default store and set the 'message' key: +//! +//! ```no_run +//! # fn main() -> anyhow::Result<()> { +//! let store = spin_sdk::key_value::Store::open_default()?; +//! store.set("message", "Hello world".as_bytes())?; +//! # Ok(()) +//! # } +//! ``` use super::wit::v2::key_value; @@ -10,7 +22,54 @@ use super::wit::v2::key_value; use serde::{de::DeserializeOwned, Serialize}; #[doc(inline)] -pub use key_value::{Error, Store}; +pub use key_value::Error; + +/// An open key-value store. +/// +/// # Examples +/// +/// Open the default store and set the 'message' key: +/// +/// ```no_run +/// # fn main() -> anyhow::Result<()> { +/// let store = spin_sdk::key_value::Store::open_default()?; +/// store.set("message", "Hello world".as_bytes())?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Open the default store and get the 'message' key: +/// +/// ```no_run +/// # fn main() -> anyhow::Result<()> { +/// let store = spin_sdk::key_value::Store::open_default()?; +/// let message = store.get("message")?; +/// let response = message.unwrap_or_else(|| "not found".into()); +/// # Ok(()) +/// # } +/// ``` +/// +/// Open a named store and list all the keys defined in it: +/// +/// ```no_run +/// # fn main() -> anyhow::Result<()> { +/// let store = spin_sdk::key_value::Store::open("finance")?; +/// let keys = store.get_keys()?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Open the default store and delete the 'message' key: +/// +/// ```no_run +/// # fn main() -> anyhow::Result<()> { +/// let store = spin_sdk::key_value::Store::open_default()?; +/// store.delete("message")?; +/// # Ok(()) +/// # } +/// ``` +#[doc(inline)] +pub use key_value::Store; impl Store { /// Open the default store. @@ -24,6 +83,31 @@ impl Store { impl Store { #[cfg(feature = "json")] /// Serialize the given data to JSON, then set it as the value for the specified `key`. + /// + /// # Examples + /// + /// Open the default store and save a customer information document against the customer ID: + /// + /// ```no_run + /// # use serde::{Deserialize, Serialize}; + /// #[derive(Deserialize, Serialize)] + /// struct Customer { + /// name: String, + /// address: Vec, + /// } + /// + /// # fn main() -> anyhow::Result<()> { + /// let customer_id = "CR1234567"; + /// let customer = Customer { + /// name: "Alice".to_owned(), + /// address: vec!["Wonderland Way".to_owned()], + /// }; + /// + /// let store = spin_sdk::key_value::Store::open_default()?; + /// store.set_json(customer_id, &customer)?; + /// # Ok(()) + /// # } + /// ``` pub fn set_json( &self, key: impl AsRef, @@ -34,6 +118,27 @@ impl Store { #[cfg(feature = "json")] /// Deserialize an instance of type `T` from the value of `key`. + /// + /// # Examples + /// + /// Open the default store and retrieve a customer information document by customer ID: + /// + /// ```no_run + /// # use serde::{Deserialize, Serialize}; + /// #[derive(Deserialize, Serialize)] + /// struct Customer { + /// name: String, + /// address: Vec, + /// } + /// + /// # fn main() -> anyhow::Result<()> { + /// let customer_id = "CR1234567"; + /// + /// let store = spin_sdk::key_value::Store::open_default()?; + /// let customer = store.get_json::(customer_id)?; + /// # Ok(()) + /// # } + /// ``` pub fn get_json( &self, key: impl AsRef, diff --git a/src/lib.rs b/src/lib.rs index 53f6b6f..042f613 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ pub mod key_value; /// SQLite storage. pub mod sqlite; -/// Large Language Model APIs +/// Large Language Model (Serverless AI) APIs pub mod llm; /// Exports the procedural macros for writing handlers for Spin components. @@ -57,13 +57,13 @@ extern "C" fn __spin_sdk_hash() {} /// Helpers for building Spin `wasi-http` components. pub mod http; -/// Implementation of the spin mqtt interface. +/// MQTT messaging. #[allow(missing_docs)] pub mod mqtt { pub use super::wit::v2::mqtt::{Connection, Error, Payload, Qos}; } -/// Implementation of the spin redis interface. +/// Redis storage and messaging. #[allow(missing_docs)] pub mod redis { use std::hash::{Hash, Hasher}; @@ -99,16 +99,18 @@ pub mod redis { } } -/// Implementation of the spin postgres db interface. +/// Spin 2 Postgres relational database storage. Applications that do not require +/// Spin 2 support should use the `pg3` module instead. pub mod pg; -/// Implementation of the spin postgres v3 db interface. +/// Postgres relational database storage. pub mod pg3; -/// Implementation of the Spin MySQL database interface. +/// MySQL relational database storage. pub mod mysql; #[doc(inline)] +/// Component configuration variables. pub use wit::v2::variables; #[doc(hidden)] diff --git a/src/llm.rs b/src/llm.rs index adad39a..162fda4 100644 --- a/src/llm.rs +++ b/src/llm.rs @@ -1,7 +1,49 @@ -pub use crate::wit::v2::llm::{ - self, EmbeddingsResult, EmbeddingsUsage, Error, InferencingParams, InferencingResult, - InferencingUsage, -}; +pub use crate::wit::v2::llm::{Error, InferencingParams, InferencingResult, InferencingUsage}; + +/// Provides access to the underlying WIT interface. You should not normally need +/// to use this module: use the re-exports in this module instead. +#[doc(inline)] +pub use crate::wit::v2::llm; + +/// The result of generating embeddings. +/// +/// # Examples +/// +/// Generate embeddings using the all-minilm-l6-v2 LLM. +/// +/// ```no_run +/// use spin_sdk::llm; +/// +/// # fn main() -> anyhow::Result<()> { +/// let text = &[ +/// "I've just broken a priceless turnip".to_owned(), +/// ]; +/// +/// let embed_result = llm::generate_embeddings(llm::EmbeddingModel::AllMiniLmL6V2, text)?; +/// +/// println!("prompt token count: {}", embed_result.usage.prompt_token_count); +/// println!("embedding: {:?}", embed_result.embeddings.first()); +/// # Ok(()) +/// # } +/// ``` +#[doc(inline)] +pub use crate::wit::v2::llm::EmbeddingsResult; + +/// Usage related to an embeddings generation request. +/// +/// # Examples +/// +/// ```no_run +/// use spin_sdk::llm; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let text = &[]; +/// let embed_result = llm::generate_embeddings(llm::EmbeddingModel::AllMiniLmL6V2, text)?; +/// println!("prompt token count: {}", embed_result.usage.prompt_token_count); +/// # Ok(()) +/// # } +/// ``` +pub use crate::wit::v2::llm::EmbeddingsUsage; /// The model use for inferencing #[allow(missing_docs)] diff --git a/src/sqlite.rs b/src/sqlite.rs index be7021a..df82b14 100644 --- a/src/sqlite.rs +++ b/src/sqlite.rs @@ -1,7 +1,186 @@ use super::wit::v2::sqlite; #[doc(inline)] -pub use sqlite::{Connection, Error, QueryResult, RowResult, Value}; +pub use sqlite::{Error, Value}; + +/// An open connection to a SQLite database. +/// +/// # Examples +/// +/// Load a set of rows from the default SQLite database, and iterate over them. +/// +/// ```no_run +/// use spin_sdk::sqlite::{Connection, Value}; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let min_age = 0; +/// let db = Connection::open_default()?; +/// +/// let query_result = db.execute( +/// "SELECT * FROM users WHERE age >= ?", +/// &[Value::Integer(min_age)] +/// )?; +/// +/// let name_index = query_result.columns.iter().position(|c| c == "name").unwrap(); +/// +/// for row in &query_result.rows { +/// let name: &str = row.get(name_index).unwrap(); +/// println!("Found user {name}"); +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// Use the [QueryResult::rows()] wrapper to access a column by name. This is simpler and +/// more readable but incurs a lookup on each access, so is not recommended when +/// iterating a data set. +/// +/// ```no_run +/// # use spin_sdk::sqlite::{Connection, Value}; +/// # fn main() -> anyhow::Result<()> { +/// # let user_id = 0; +/// let db = Connection::open_default()?; +/// let query_result = db.execute( +/// "SELECT * FROM users WHERE id = ?", +/// &[Value::Integer(user_id)] +/// )?; +/// let name = query_result.rows().next().and_then(|r| r.get::<&str>("name")).unwrap(); +/// # Ok(()) +/// # } +/// ``` +/// +/// Perform an aggregate (scalar) operation over a named SQLite database. The result +/// set contains a single column, with a single row. +/// +/// ```no_run +/// use spin_sdk::sqlite::Connection; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let user_id = 0; +/// let db = Connection::open("customer-data")?; +/// let query_result = db.execute("SELECT COUNT(*) FROM users", &[])?; +/// let count = query_result.rows.first().and_then(|r| r.get::(0)).unwrap(); +/// # Ok(()) +/// # } +/// ``` +/// +/// Delete rows from a SQLite database. The usual [Connection::execute()] syntax +/// is used but the query result is always empty. +/// +/// ```no_run +/// use spin_sdk::sqlite::{Connection, Value}; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let min_age = 18; +/// let db = Connection::open("customer-data")?; +/// db.execute("DELETE FROM users WHERE age < ?", &[Value::Integer(min_age)])?; +/// # Ok(()) +/// # } +/// ``` +#[doc(inline)] +pub use sqlite::Connection; + +/// The result of a SQLite query issued with [Connection::execute()]. +/// +/// # Examples +/// +/// Load a set of rows from the default SQLite database, and iterate over them. +/// +/// ```no_run +/// use spin_sdk::sqlite::{Connection, Value}; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let min_age = 0; +/// let db = Connection::open_default()?; +/// +/// let query_result = db.execute( +/// "SELECT * FROM users WHERE age >= ?", +/// &[Value::Integer(min_age)] +/// )?; +/// +/// let name_index = query_result.columns.iter().position(|c| c == "name").unwrap(); +/// +/// for row in &query_result.rows { +/// let name: &str = row.get(name_index).unwrap(); +/// println!("Found user {name}"); +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// Use the [QueryResult::rows()] wrapper to access a column by name. This is simpler and +/// more readable but incurs a lookup on each access, so is not recommended when +/// iterating a data set. +/// +/// ```no_run +/// # use spin_sdk::sqlite::{Connection, Value}; +/// # fn main() -> anyhow::Result<()> { +/// # let user_id = 0; +/// let db = Connection::open_default()?; +/// let query_result = db.execute( +/// "SELECT * FROM users WHERE id = ?", +/// &[Value::Integer(user_id)] +/// )?; +/// let name = query_result.rows().next().and_then(|r| r.get::<&str>("name")).unwrap(); +/// # Ok(()) +/// # } +/// ``` +/// +/// Perform an aggregate (scalar) operation over a named SQLite database. The result +/// set contains a single column, with a single row. +/// +/// ```no_run +/// use spin_sdk::sqlite::Connection; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let user_id = 0; +/// let db = Connection::open("customer-data")?; +/// let query_result = db.execute("SELECT COUNT(*) FROM users", &[])?; +/// let count = query_result.rows.first().and_then(|r| r.get::(0)).unwrap(); +/// # Ok(()) +/// # } +/// ``` +#[doc(inline)] +pub use sqlite::QueryResult; + +/// A database row result. +/// +/// There are two representations of a SQLite row in the SDK. This type is obtained from +/// the [field@QueryResult::rows] field, and provides index-based lookup or low-level access +/// to row values via a vector. The [Row] type is useful for +/// addressing elements by column name, and is obtained from the [QueryResult::rows()] function. +/// +/// # Examples +/// +/// Load a set of rows from the default SQLite database, and iterate over them selecting one +/// field from each. The example caches the index of the desired field to avoid repeated lookup, +/// making this more efficient than the [Row]-based equivalent at the expense of +/// extra code and inferior readability. +/// +/// ```no_run +/// use spin_sdk::sqlite::{Connection, Value}; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let min_age = 0; +/// let db = Connection::open_default()?; +/// +/// let query_result = db.execute( +/// "SELECT * FROM users WHERE age >= ?", +/// &[Value::Integer(min_age)] +/// )?; +/// +/// let name_index = query_result.columns.iter().position(|c| c == "name").unwrap(); +/// +/// for row in &query_result.rows { +/// let name: &str = row.get(name_index).unwrap(); +/// println!("Found user {name}"); +/// } +/// # Ok(()) +/// # } +/// ``` + +#[doc(inline)] +pub use sqlite::RowResult; impl sqlite::Connection { /// Open a connection to the default database @@ -20,14 +199,47 @@ impl sqlite::QueryResult { } } -/// A database row result +/// A database row result. +/// +/// There are two representations of a SQLite row in the SDK. This type is useful for +/// addressing elements by column name, and is obtained from the [QueryResult::rows()] function. +/// The [RowResult] type is obtained from the [field@QueryResult::rows] field, and provides +/// index-based lookup or low-level access to row values via a vector. pub struct Row<'a> { columns: &'a [String], result: &'a sqlite::RowResult, } impl<'a> Row<'a> { - /// Get a value by its column name + /// Get a value by its column name. The value is converted to the target type. + /// + /// * SQLite integers are convertible to Rust integer types (i8, u8, i16, etc. including usize and isize) and bool. + /// * SQLite strings are convertible to Rust &str or &[u8] (encoded as UTF-8). + /// * SQLite reals are convertible to Rust f64. + /// * SQLite blobs are convertible to Rust &[u8] or &str (interpreted as UTF-8). + /// + /// If your code does not know the type in advance, use [RowResult] instead of `Row` to + /// access the underlying [Value] enum. + /// + /// # Examples + /// + /// ```no_run + /// use spin_sdk::sqlite::{Connection, Value}; + /// + /// # fn main() -> anyhow::Result<()> { + /// # let user_id = 0; + /// let db = Connection::open_default()?; + /// let query_result = db.execute( + /// "SELECT * FROM users WHERE id = ?", + /// &[Value::Integer(user_id)] + /// )?; + /// let user_row = query_result.rows().next().unwrap(); + /// + /// let name = user_row.get::<&str>("name").unwrap(); + /// let age = user_row.get::("age").unwrap(); + /// # Ok(()) + /// # } + /// ``` pub fn get>(&self, column: &str) -> Option { let i = self.columns.iter().position(|c| c == column)?; self.result.get(i) @@ -35,7 +247,36 @@ impl<'a> Row<'a> { } impl sqlite::RowResult { - /// Get a value by its index + /// Get a value by its column name. The value is converted to the target type. + /// + /// * SQLite integers are convertible to Rust integer types (i8, u8, i16, etc. including usize and isize) and bool. + /// * SQLite strings are convertible to Rust &str or &[u8] (encoded as UTF-8). + /// * SQLite reals are convertible to Rust f64. + /// * SQLite blobs are convertible to Rust &[u8] or &str (interpreted as UTF-8). + /// + /// To look up by name, you can use `QueryResult::rows()` or obtain the invoice from `QueryResult::columns`. + /// If you do not know the type of a value, access the underlying [Value] enum directly + /// via the [RowResult::values] field + /// + /// # Examples + /// + /// ```no_run + /// use spin_sdk::sqlite::{Connection, Value}; + /// + /// # fn main() -> anyhow::Result<()> { + /// # let user_id = 0; + /// let db = Connection::open_default()?; + /// let query_result = db.execute( + /// "SELECT name, age FROM users WHERE id = ?", + /// &[Value::Integer(user_id)] + /// )?; + /// let user_row = query_result.rows.first().unwrap(); + /// + /// let name = user_row.get::<&str>(0).unwrap(); + /// let age = user_row.get::(1).unwrap(); + /// # Ok(()) + /// # } + /// ``` pub fn get<'a, T: TryFrom<&'a Value>>(&'a self, index: usize) -> Option { self.values.get(index).and_then(|c| c.try_into().ok()) }