diff --git a/src/lib.rs b/src/lib.rs index 042f613..8d5e088 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,61 +57,19 @@ extern "C" fn __spin_sdk_hash() {} /// Helpers for building Spin `wasi-http` components. pub mod http; -/// MQTT messaging. #[allow(missing_docs)] -pub mod mqtt { - pub use super::wit::v2::mqtt::{Connection, Error, Payload, Qos}; -} +pub mod mqtt; -/// Redis storage and messaging. #[allow(missing_docs)] -pub mod redis { - use std::hash::{Hash, Hasher}; - - pub use super::wit::v2::redis::{Connection, Error, Payload, RedisParameter, RedisResult}; - - impl PartialEq for RedisResult { - fn eq(&self, other: &Self) -> bool { - use RedisResult::*; - match (self, other) { - (Nil, Nil) => true, - (Status(a), Status(b)) => a == b, - (Int64(a), Int64(b)) => a == b, - (Binary(a), Binary(b)) => a == b, - _ => false, - } - } - } - - impl Eq for RedisResult {} - - impl Hash for RedisResult { - fn hash(&self, state: &mut H) { - use RedisResult::*; - - match self { - Nil => (), - Status(s) => s.hash(state), - Int64(v) => v.hash(state), - Binary(v) => v.hash(state), - } - } - } -} +pub mod redis; -/// Spin 2 Postgres relational database storage. Applications that do not require -/// Spin 2 support should use the `pg3` module instead. pub mod pg; -/// Postgres relational database storage. pub mod pg3; -/// MySQL relational database storage. pub mod mysql; -#[doc(inline)] -/// Component configuration variables. -pub use wit::v2::variables; +pub mod variables; #[doc(hidden)] pub use wit_bindgen; diff --git a/src/mqtt.rs b/src/mqtt.rs new file mode 100644 index 0000000..3fef26a --- /dev/null +++ b/src/mqtt.rs @@ -0,0 +1,65 @@ +//! MQTT message publishing. +//! +//! To receive MQTT messages, use the MQTT trigger. +//! +//! # Examples +//! +//! Send an MQTT message. +//! +//! ```no_run +//! use spin_sdk::mqtt::{Connection, Qos}; +//! +//! # fn ensure_pet_picture(_: &[u8]) -> anyhow::Result<()> { Ok(()) } +//! # fn use_mqtt(request: spin_sdk::http::Request) -> anyhow::Result<()> { +//! let user = spin_sdk::variables::get("mqtt_username")?; +//! let password = spin_sdk::variables::get("mqtt_password")?; +//! +//! let conn = Connection::open( +//! "mqtt://localhost:1883?client_id=123", +//! &user, +//! &password, +//! 30 /* seconds */ +//! )?; +//! +//! let payload = request.body().to_vec(); +//! ensure_pet_picture(&payload)?; +//! +//! conn.publish("pet-pictures", &payload, Qos::AtLeastOnce)?; +//! # Ok(()) +//! # } +//! ``` + +/// An open connection to an MQTT queue. +/// +/// The address must be in URL form, and must include a `client_id`: +/// `mqtt://hostname?client_id=...` +/// +/// # Examples +/// +/// Send an MQTT message. +/// +/// ```no_run +/// use spin_sdk::mqtt::{Connection, Qos}; +/// +/// # fn ensure_pet_picture(_: &[u8]) -> anyhow::Result<()> { Ok(()) } +/// # fn use_mqtt(request: spin_sdk::http::Request) -> anyhow::Result<()> { +/// let user = spin_sdk::variables::get("mqtt_username")?; +/// let password = spin_sdk::variables::get("mqtt_password")?; +/// +/// let conn = Connection::open( +/// "mqtt://localhost:1883?client_id=123", +/// &user, +/// &password, +/// 30 /* seconds */ +/// )?; +/// +/// let payload = request.body().to_vec(); +/// ensure_pet_picture(&payload)?; +/// +/// conn.publish("pet-pictures", &payload, Qos::AtLeastOnce)?; +/// # Ok(()) +/// # } +/// ``` +pub use super::wit::v2::mqtt::Connection; + +pub use super::wit::v2::mqtt::{Error, Payload, Qos}; diff --git a/src/mysql.rs b/src/mysql.rs index 104ded4..3bca54e 100644 --- a/src/mysql.rs +++ b/src/mysql.rs @@ -1,4 +1,7 @@ -//! Conversions between Rust, WIT and **MySQL** types. +//! MySQL relational database storage. +//! +//! You can use the [`Decode`] trait to convert a [`DbValue`] to a +//! suitable Rust type. The following table shows available conversions. //! //! # Types //! @@ -18,8 +21,109 @@ //! | `String` | str(string) | VARCHAR, CHAR, TEXT | //! | `Vec` | binary(list\) | VARBINARY, BINARY, BLOB | +/// An open connection to a MySQL database. +/// +/// # Examples +/// +/// Load a set of rows from a local PostgreSQL database, and iterate over them. +/// +/// ```no_run +/// use spin_sdk::mysql::{Connection, Decode, ParameterValue}; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let min_age = 0; +/// let db = Connection::open("mysql://root:my_password@localhost/mydb")?; +/// +/// let query_result = db.query( +/// "SELECT * FROM users WHERE age < ?", +/// &[ParameterValue::Int32(20)] +/// )?; +/// +/// let name_index = query_result.columns.iter().position(|c| c.name == "name").unwrap(); +/// +/// for row in &query_result.rows { +/// let name = String::decode(&row[name_index])?; +/// println!("Found user {name}"); +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// Perform an aggregate (scalar) operation over a table. The result set +/// contains a single column, with a single row. +/// +/// ```no_run +/// use spin_sdk::mysql::{Connection, Decode}; +/// +/// # fn main() -> anyhow::Result<()> { +/// let db = Connection::open("mysql://root:my_password@localhost/mydb")?; +/// +/// let query_result = db.query("SELECT COUNT(*) FROM users", &[])?; +/// +/// assert_eq!(1, query_result.columns.len()); +/// assert_eq!("COUNT(*)", query_result.columns[0].name); +/// assert_eq!(1, query_result.rows.len()); +/// +/// let count = i64::decode(&query_result.rows[0][0])?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Delete rows from a MySQL table. This uses [Connection::execute()] +/// instead of the `query` method. +/// +/// ```no_run +/// use spin_sdk::mysql::{Connection, ParameterValue}; +/// +/// # fn main() -> anyhow::Result<()> { +/// let db = Connection::open("mysql://root:my_password@localhost/mydb")?; +/// +/// let rows_affected = db.execute( +/// "DELETE FROM users WHERE name = ?", +/// &[ParameterValue::Str("Baldrick".to_owned())] +/// )?; +/// # Ok(()) +/// # } +/// ``` +#[doc(inline)] +pub use super::wit::v2::mysql::Connection; + +/// The result of a database query. +/// +/// # Examples +/// +/// Load a set of rows from a local PostgreSQL database, and iterate over them +/// selecting one field from each. The columns collection allows you to find +/// column indexes for column names; you can bypass this lookup if you name +/// specific columns in the query. +/// +/// ```no_run +/// use spin_sdk::mysql::{Connection, Decode, ParameterValue}; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let min_age = 0; +/// let db = Connection::open("mysql://root:my_password@localhost/mydb")?; +/// +/// let query_result = db.query( +/// "SELECT * FROM users WHERE age >= ?", +/// &[ParameterValue::Int32(min_age)] +/// )?; +/// +/// let name_index = query_result.columns.iter().position(|c| c.name == "name").unwrap(); +/// +/// for row in &query_result.rows { +/// let name = String::decode(&row[name_index])?; +/// println!("Found user {name}"); +/// } +/// # Ok(()) +/// # } +/// ``` #[doc(inline)] -pub use super::wit::v2::mysql::{Connection, Error as MysqlError}; +pub use super::wit::v2::mysql::RowSet; + +#[doc(inline)] +pub use super::wit::v2::mysql::Error as MysqlError; + #[doc(inline)] pub use super::wit::v2::rdbms_types::*; diff --git a/src/pg.rs b/src/pg.rs index 07e120f..2c4df09 100644 --- a/src/pg.rs +++ b/src/pg.rs @@ -1,3 +1,6 @@ +//! Spin 2 Postgres relational database storage. Applications that do not require +//! Spin 2 support should use the [`pg3`](crate::pg3) module instead. +//! //! Conversions between Rust, WIT and **Postgres** types. //! //! # Types diff --git a/src/pg3.rs b/src/pg3.rs index 3787a08..4a88dd6 100644 --- a/src/pg3.rs +++ b/src/pg3.rs @@ -1,4 +1,9 @@ -//! Conversions between Rust, WIT and **Postgres** types. +//! Postgres relational database storage. +//! +//! You can use the [`into()`](std::convert::Into) method to convert +//! a Rust value into a [`ParameterValue`]. You can use the +//! [`Decode`] trait to convert a [`DbValue`] to a suitable Rust type. +//! The following table shows available conversions. //! //! # Types //! @@ -17,18 +22,117 @@ //! | `chrono::NaiveDateTime` | datetime(tuple) | TIMESTAMP | //! | `chrono::Duration` | timestamp(s64) | BIGINT | +/// An open connection to a PostgreSQL database. +/// +/// # Examples +/// +/// Load a set of rows from a local PostgreSQL database, and iterate over them. +/// +/// ```no_run +/// use spin_sdk::pg3::{Connection, Decode}; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let min_age = 0; +/// let db = Connection::open("host=localhost user=postgres password=my_password dbname=mydb")?; +/// +/// let query_result = db.query( +/// "SELECT * FROM users WHERE age >= $1", +/// &[min_age.into()] +/// )?; +/// +/// let name_index = query_result.columns.iter().position(|c| c.name == "name").unwrap(); +/// +/// for row in &query_result.rows { +/// let name = String::decode(&row[name_index])?; +/// println!("Found user {name}"); +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// Perform an aggregate (scalar) operation over a table. The result set +/// contains a single column, with a single row. +/// +/// ```no_run +/// use spin_sdk::pg3::{Connection, Decode}; +/// +/// # fn main() -> anyhow::Result<()> { +/// let db = Connection::open("host=localhost user=postgres password=my_password dbname=mydb")?; +/// +/// let query_result = db.query("SELECT COUNT(*) FROM users", &[])?; +/// +/// assert_eq!(1, query_result.columns.len()); +/// assert_eq!("count", query_result.columns[0].name); +/// assert_eq!(1, query_result.rows.len()); +/// +/// let count = i64::decode(&query_result.rows[0][0])?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Delete rows from a PostgreSQL table. This uses [Connection::execute()] +/// instead of the `query` method. +/// +/// ```no_run +/// use spin_sdk::pg3::Connection; +/// +/// # fn main() -> anyhow::Result<()> { +/// let db = Connection::open("host=localhost user=postgres password=my_password dbname=mydb")?; +/// +/// let rows_affected = db.execute( +/// "DELETE FROM users WHERE name = $1", +/// &["Baldrick".to_owned().into()] +/// )?; +/// # Ok(()) +/// # } +/// ``` +#[doc(inline)] +pub use super::wit::pg3::Connection; + +/// The result of a database query. +/// +/// # Examples +/// +/// Load a set of rows from a local PostgreSQL database, and iterate over them +/// selecting one field from each. The columns collection allows you to find +/// column indexes for column names; you can bypass this lookup if you name +/// specific columns in the query. +/// +/// ```no_run +/// use spin_sdk::pg3::{Connection, Decode}; +/// +/// # fn main() -> anyhow::Result<()> { +/// # let min_age = 0; +/// let db = Connection::open("host=localhost user=postgres password=my_password dbname=mydb")?; +/// +/// let query_result = db.query( +/// "SELECT * FROM users WHERE age >= $1", +/// &[min_age.into()] +/// )?; +/// +/// let name_index = query_result.columns.iter().position(|c| c.name == "name").unwrap(); +/// +/// for row in &query_result.rows { +/// let name = String::decode(&row[name_index])?; +/// println!("Found user {name}"); +/// } +/// # Ok(()) +/// # } +/// ``` +pub use super::wit::pg3::RowSet; + #[doc(inline)] pub use super::wit::pg3::{Error as PgError, *}; use chrono::{Datelike, Timelike}; -/// A pg error +/// A Postgres error #[derive(Debug, thiserror::Error)] pub enum Error { /// Failed to deserialize [`DbValue`] #[error("error value decoding: {0}")] Decode(String), - /// Pg query failed with an error + /// Postgres query failed with an error #[error(transparent)] PgError(#[from] PgError), } diff --git a/src/redis.rs b/src/redis.rs new file mode 100644 index 0000000..622af58 --- /dev/null +++ b/src/redis.rs @@ -0,0 +1,116 @@ +//! Redis storage and message publishing. +//! +//! To receive Redis messages, use the Redis trigger. +//! +//! # Examples +//! +//! Get a value from the Redis database. +//! +//! ```no_run +//! use spin_sdk::redis::Connection; +//! +//! # fn main() -> anyhow::Result<()> { +//! let conn = Connection::open("redis://127.0.0.1:6379")?; +//! let payload = conn.get("archimedes-data")?; +//! if let Some(data) = payload { +//! println!("{}", String::from_utf8_lossy(&data)); +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! See the [`Connection`] type for further examples. + +use std::hash::{Hash, Hasher}; + +/// An open connection to a Redis server. +/// +/// # Examples +/// +/// Get a value from the Redis database. +/// +/// ```no_run +/// use spin_sdk::redis::Connection; +/// +/// # fn main() -> anyhow::Result<()> { +/// let conn = Connection::open("redis://127.0.0.1:6379")?; +/// let payload = conn.get("archimedes-data")?; +/// if let Some(data) = payload { +/// println!("{}", String::from_utf8_lossy(&data)); +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// Set a value in the Redis database. +/// +/// ```no_run +/// use spin_sdk::redis::Connection; +/// +/// # fn main() -> anyhow::Result<()> { +/// let conn = Connection::open("redis://127.0.0.1:6379")?; +/// let payload = "Eureka!".to_owned().into_bytes(); +/// conn.set("archimedes-data", &payload)?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Delete a value from the Redis database. +/// +/// ```no_run +/// use spin_sdk::redis::Connection; +/// +/// # fn main() -> anyhow::Result<()> { +/// let conn = Connection::open("redis://127.0.0.1:6379")?; +/// conn.del(&["archimedes-data".to_owned()])?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Publish a message to a Redis channel. +/// +/// ```no_run +/// use spin_sdk::redis::Connection; +/// +/// # fn ensure_pet_picture(_: &[u8]) -> anyhow::Result<()> { Ok(()) } +/// # fn use_redis(request: spin_sdk::http::Request) -> anyhow::Result<()> { +/// let conn = Connection::open("redis://127.0.0.1:6379")?; +/// +/// let payload = request.body().to_vec(); +/// ensure_pet_picture(&payload)?; +/// +/// conn.publish("pet-pictures", &payload)?; +/// # Ok(()) +/// # } +/// ``` +pub use super::wit::v2::redis::Connection; + +pub use super::wit::v2::redis::{Error, Payload, RedisParameter, RedisResult}; + +impl PartialEq for RedisResult { + fn eq(&self, other: &Self) -> bool { + use RedisResult::*; + match (self, other) { + (Nil, Nil) => true, + (Status(a), Status(b)) => a == b, + (Int64(a), Int64(b)) => a == b, + (Binary(a), Binary(b)) => a == b, + _ => false, + } + } +} + +impl Eq for RedisResult {} + +impl Hash for RedisResult { + fn hash(&self, state: &mut H) { + use RedisResult::*; + + match self { + Nil => (), + Status(s) => s.hash(state), + Int64(v) => v.hash(state), + Binary(v) => v.hash(state), + } + } +} diff --git a/src/sqlite.rs b/src/sqlite.rs index df82b14..3cce529 100644 --- a/src/sqlite.rs +++ b/src/sqlite.rs @@ -178,7 +178,6 @@ pub use sqlite::QueryResult; /// # Ok(()) /// # } /// ``` - #[doc(inline)] pub use sqlite::RowResult; diff --git a/src/variables.rs b/src/variables.rs new file mode 100644 index 0000000..373422f --- /dev/null +++ b/src/variables.rs @@ -0,0 +1,69 @@ +//! Component configuration variables. +//! +//! Component variables must be defined in the application +//! manifest, in the `[component..variables]` section. +//! Component variables typically use template syntax to +//! derive values from application variables, which are +//! the only variables that may be overridden directly (for +//! example, on the Spin command line). +//! +//! # Examples +//! +//! Get the value of a component variable. +//! +//! ```no_run +//! # fn main() -> anyhow::Result<()> { +//! let region = spin_sdk::variables::get("region_id")?; +//! let regional_url = format!("https://{region}.db.example.com"); +//! # Ok(()) +//! # } +//! ``` +//! +//! Fail gracefully if a variable is not set. +//! +//! ```no_run +//! use spin_sdk::variables::Error; +//! +//! # fn main() -> anyhow::Result<()> { +//! let favourite = match spin_sdk::variables::get("favourite") { +//! Ok(value) => value, +//! Err(Error::Undefined(_)) => "not playing favourites".to_owned(), +//! Err(e) => anyhow::bail!(e), +//! }; +//! # Ok(()) +//! # } +//! ``` + +/// Get the value of a component variable. +/// +/// # Examples +/// +/// Get the value of a component variable. +/// +/// ```no_run +/// # fn main() -> anyhow::Result<()> { +/// let region = spin_sdk::variables::get("region_id")?; +/// let regional_url = format!("https://{region}.db.example.com"); +/// # Ok(()) +/// # } +/// ``` +/// +/// Fail gracefully if a variable is not set. +/// +/// ```no_run +/// use spin_sdk::variables::Error; +/// +/// # fn main() -> anyhow::Result<()> { +/// let favourite = match spin_sdk::variables::get("favourite") { +/// Ok(value) => value, +/// Err(Error::Undefined(_)) => "not playing favourites".to_owned(), +/// Err(e) => anyhow::bail!(e), +/// }; +/// # Ok(()) +/// # } +/// ``` +#[doc(inline)] +pub use super::wit::v2::variables::get; + +#[doc(inline)] +pub use super::wit::v2::variables::Error;