mik_sdk/
lib.rs

1// =============================================================================
2// CRATE-LEVEL QUALITY LINTS
3// =============================================================================
4#![forbid(unsafe_code)]
5#![deny(unused_must_use)]
6#![warn(missing_docs)]
7#![warn(missing_debug_implementations)]
8#![warn(rust_2018_idioms)]
9#![warn(unreachable_pub)]
10#![warn(rustdoc::missing_crate_level_docs)]
11#![warn(rustdoc::broken_intra_doc_links)]
12// =============================================================================
13// CLIPPY CONFIGURATION
14// =============================================================================
15// Pedantic lints - allow stylistic ones that don't affect correctness
16#![allow(clippy::doc_markdown)] // Code in docs - extensive changes needed
17#![allow(clippy::must_use_candidate)] // Not all returned values need must_use
18#![allow(clippy::return_self_not_must_use)] // Builder pattern returns Self by design
19#![allow(clippy::cast_possible_truncation)] // Intentional in WASM context
20#![allow(clippy::cast_sign_loss)] // Intentional in WASM context
21#![allow(clippy::cast_possible_wrap)] // Intentional in WASM context
22#![allow(clippy::unreadable_literal)] // Bit patterns don't need separators
23#![allow(clippy::items_after_statements)] // Const in functions for locality
24#![allow(clippy::missing_errors_doc)] // # Errors sections - doc-heavy
25#![allow(clippy::missing_panics_doc)] // # Panics sections - doc-heavy
26#![allow(clippy::match_same_arms)] // Intentional for clarity
27#![allow(clippy::format_push_string)] // String building style
28#![allow(clippy::format_collect)]
29// Iterator to string style
30// Internal implementation where bounds/values are known at compile time or checked
31#![allow(clippy::indexing_slicing)] // Fixed-size buffers and checked lengths
32#![allow(clippy::unwrap_used)] // Used after explicit checks or with known values
33#![allow(clippy::expect_used)] // Used for system-level guarantees (RNG, etc.)
34#![allow(clippy::double_must_use)] // Builder methods can have their own docs
35
36//! mik-sdk - Ergonomic SDK for WASI HTTP handlers
37//!
38//! # Overview
39//!
40//! mik-sdk provides a simple, ergonomic way to build portable WASI HTTP handlers.
41//! Write your handler once, run it on Spin, wasmCloud, wasmtime, or any WASI-compliant runtime.
42//!
43//! Available on [crates.io](https://crates.io/crates/mik-sdk).
44//!
45//! # Architecture
46//!
47//! ```text
48//! ┌─────────────────────────────────────────────────────────┐
49//! │  Your Handler                                           │
50//! │  ┌───────────────────────────────────────────────────┐  │
51//! │  │  use mik_sdk::prelude::*;                         │  │
52//! │  │                                                   │  │
53//! │  │  routes! {                                        │  │
54//! │  │      "/" => home,                                 │  │
55//! │  │      "/users/{id}" => get_user,                   │  │
56//! │  │  }                                                │  │
57//! │  │                                                   │  │
58//! │  │  fn get_user(req: &Request) -> Response {         │  │
59//! │  │      let id = req.param("id").unwrap();           │  │
60//! │  │      ok!({ "id": str(id) })                       │  │
61//! │  │  }                                                │  │
62//! │  └───────────────────────────────────────────────────┘  │
63//! └─────────────────────────────────────────────────────────┘
64//!                           ↓ compose with
65//! ┌─────────────────────────────────────────────────────────┐
66//! │  Router Component (provides JSON/HTTP utilities)        │
67//! └─────────────────────────────────────────────────────────┘
68//!                           ↓ compose with
69//! ┌─────────────────────────────────────────────────────────┐
70//! │  Bridge Component (WASI HTTP adapter)                   │
71//! └─────────────────────────────────────────────────────────┘
72//!                           ↓ runs on
73//! ┌─────────────────────────────────────────────────────────┐
74//! │  Any WASI HTTP Runtime (Spin, wasmCloud, wasmtime)      │
75//! └─────────────────────────────────────────────────────────┘
76//! ```
77//!
78//! # Quick Start
79//!
80//! ```ignore
81//! use bindings::exports::mik::core::handler::Guest;
82//! use bindings::mik::core::{http, json};
83//! use mik_sdk::prelude::*;
84//!
85//! routes! {
86//!     GET "/" => home,
87//!     GET "/hello/{name}" => hello(path: HelloPath),
88//! }
89//!
90//! fn home(_req: &Request) -> http::Response {
91//!     ok!({
92//!         "message": "Welcome!",
93//!         "version": "0.1.0"
94//!     })
95//! }
96//!
97//! fn hello(req: &Request) -> http::Response {
98//!     let name = req.param("name").unwrap_or("world");
99//!     ok!({
100//!         "greeting": str(format!("Hello, {}!", name))
101//!     })
102//! }
103//! ```
104//!
105//! # Configuration
106//!
107//! The SDK and bridge can be configured via environment variables:
108//!
109//! | Variable             | Default | Description                          |
110//! |----------------------|---------|--------------------------------------|
111//! | `MIK_MAX_JSON_SIZE`  | 1 MB    | Maximum JSON input size for parsing  |
112//! | `MIK_MAX_BODY_SIZE`  | 10 MB   | Maximum request body size (bridge)   |
113//!
114//! ```bash
115//! # Allow 5MB JSON payloads
116//! MIK_MAX_JSON_SIZE=5000000
117//!
118//! # Allow 50MB request bodies
119//! MIK_MAX_BODY_SIZE=52428800
120//! ```
121//!
122//! # Core Macros
123//!
124//! - [`ok!`] - Return 200 OK with JSON body
125//! - [`error!`] - Return RFC 7807 error response
126//! - [`json!`] - Create a JSON value with type hints
127//!
128//! # DX Macros
129//!
130//! - [`guard!`] - Early return validation
131//! - [`created!`] - 201 Created response with Location header
132//! - [`no_content!`] - 204 No Content response
133//! - [`redirect!`] - Redirect responses (301, 302, 307, etc.)
134//!
135//! # Request Helpers
136//!
137//! ```ignore
138//! // Path parameters (from route pattern)
139//! let id = req.param("id");              // Option<&str>
140//!
141//! // Query parameters
142//! let page = req.query("page");          // Option<&str> - first value
143//! let tags = req.query_all("tag");       // &[String] - all values
144//!
145//! // Example: /search?tag=rust&tag=wasm&tag=http
146//! req.query("tag")      // → Some("rust")
147//! req.query_all("tag")  // → &["rust", "wasm", "http"]
148//!
149//! // Headers (case-insensitive)
150//! let auth = req.header("Authorization");    // Option<&str>
151//! let cookies = req.header_all("Set-Cookie"); // &[String]
152//!
153//! // Body
154//! let bytes = req.body();                // Option<&[u8]>
155//! let text = req.text();                 // Option<&str>
156//! let json = req.json_with(json::try_parse); // Option<JsonValue>
157//! ```
158//!
159//! # DX Macro Examples
160//!
161//! ```ignore
162//! // Early return validation
163//! fn create_user(req: &Request) -> http::Response {
164//!     let name = body.get("name").str_or("");
165//!     guard!(!name.is_empty(), 400, "Name is required");
166//!     guard!(name.len() <= 100, 400, "Name too long");
167//!     created!("/users/123", { "id": "123", "name": str(name) })
168//! }
169//!
170//! // Response shortcuts
171//! fn delete_user(req: &Request) -> http::Response {
172//!     no_content!()
173//! }
174//!
175//! fn legacy_endpoint(req: &Request) -> http::Response {
176//!     redirect!("/api/v2/users")  // 302 Found
177//! }
178//! ```
179//!
180//! # Type Hints
181//!
182//! Use type hints inside `ok!`, `json!`, and `error!` macros:
183//! - `str(expr)` - Convert to JSON string
184//! - `int(expr)` - Convert to JSON integer
185//! - `float(expr)` - Convert to JSON float
186//! - `bool(expr)` - Convert to JSON boolean
187//!
188//! # RFC 7807 Problem Details
189//!
190//! Error responses follow [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807.html):
191//!
192//! ```ignore
193//! // Basic usage (only status is required)
194//! error! { status: 400, title: "Bad Request", detail: "Missing field" }
195//!
196//! // Full RFC 7807 with extensions
197//! error! {
198//!     status: status::UNPROCESSABLE_ENTITY,
199//!     title: "Validation Error",
200//!     detail: "Invalid input",
201//!     problem_type: "urn:problem:validation",
202//!     instance: "/users/123",
203//!     meta: { "field": "email" }
204//! }
205//! ```
206
207pub mod constants;
208mod request;
209pub mod typed;
210
211pub mod env;
212pub mod http_client;
213pub mod json;
214pub mod log;
215pub mod random;
216pub mod time;
217
218// WASI bindings (HTTP, random, clocks)
219// Always included for WASM target, uses http-client feature for HTTP client on native
220#[cfg(any(target_arch = "wasm32", feature = "http-client"))]
221pub(crate) mod wasi_http;
222
223// Query module - re-export from mik-sql when the sql feature is enabled
224#[cfg(feature = "sql")]
225pub use mik_sql as query;
226
227pub use mik_sdk_macros::{
228    // Derive macros for typed inputs
229    Path,
230    Query,
231    Type,
232    // Response macros
233    accepted,
234    bad_request,
235    conflict,
236    created,
237    // DX macros
238    ensure,
239    // Core macros
240    error,
241    // HTTP client macro
242    fetch,
243    forbidden,
244    guard,
245    // Batched loading helper
246    ids,
247    json,
248    no_content,
249    not_found,
250    ok,
251    redirect,
252    // Routing macros
253    routes,
254};
255
256// SQL CRUD macros - re-exported from mik-sql-macros when sql feature is enabled
257#[cfg(feature = "sql")]
258pub use mik_sql_macros::{sql_create, sql_delete, sql_read, sql_update};
259
260/// Helper trait for the `ensure!` macro to work with both Option and Result.
261/// This is an implementation detail and should not be used directly.
262#[doc(hidden)]
263pub trait EnsureHelper<T> {
264    fn into_option(self) -> Option<T>;
265}
266
267impl<T> EnsureHelper<T> for Option<T> {
268    #[inline]
269    fn into_option(self) -> Self {
270        self
271    }
272}
273
274impl<T, E> EnsureHelper<T> for Result<T, E> {
275    #[inline]
276    fn into_option(self) -> Option<T> {
277        self.ok()
278    }
279}
280
281/// Helper function for the `ensure!` macro.
282/// This is an implementation detail and should not be used directly.
283#[doc(hidden)]
284#[inline]
285pub fn __ensure_helper<T, H: EnsureHelper<T>>(value: H) -> Option<T> {
286    value.into_option()
287}
288
289pub use request::{DecodeError, Method, Request, url_decode};
290
291/// HTTP status code constants.
292///
293/// Use these instead of hardcoding status codes:
294/// ```ignore
295/// error! { status: status::NOT_FOUND, title: "Not Found", detail: "Resource not found" }
296/// ```
297pub mod status {
298    // 2xx Success
299    /// 200 OK - Request succeeded.
300    pub const OK: u16 = 200;
301    /// 201 Created - Resource created successfully.
302    pub const CREATED: u16 = 201;
303    /// 202 Accepted - Request accepted for processing.
304    pub const ACCEPTED: u16 = 202;
305    /// 204 No Content - Success with no response body.
306    pub const NO_CONTENT: u16 = 204;
307
308    // 3xx Redirection
309    /// 301 Moved Permanently - Resource moved permanently.
310    pub const MOVED_PERMANENTLY: u16 = 301;
311    /// 302 Found - Resource temporarily at different URI.
312    pub const FOUND: u16 = 302;
313    /// 304 Not Modified - Resource not modified since last request.
314    pub const NOT_MODIFIED: u16 = 304;
315    /// 307 Temporary Redirect - Temporary redirect preserving method.
316    pub const TEMPORARY_REDIRECT: u16 = 307;
317    /// 308 Permanent Redirect - Permanent redirect preserving method.
318    pub const PERMANENT_REDIRECT: u16 = 308;
319
320    // 4xx Client Errors
321    /// 400 Bad Request - Invalid request syntax or parameters.
322    pub const BAD_REQUEST: u16 = 400;
323    /// 401 Unauthorized - Authentication required.
324    pub const UNAUTHORIZED: u16 = 401;
325    /// 403 Forbidden - Access denied.
326    pub const FORBIDDEN: u16 = 403;
327    /// 404 Not Found - Resource not found.
328    pub const NOT_FOUND: u16 = 404;
329    /// 405 Method Not Allowed - HTTP method not supported.
330    pub const METHOD_NOT_ALLOWED: u16 = 405;
331    /// 406 Not Acceptable - Cannot produce acceptable response.
332    pub const NOT_ACCEPTABLE: u16 = 406;
333    /// 409 Conflict - Request conflicts with current state.
334    pub const CONFLICT: u16 = 409;
335    /// 410 Gone - Resource permanently removed.
336    pub const GONE: u16 = 410;
337    /// 422 Unprocessable Entity - Validation failed.
338    pub const UNPROCESSABLE_ENTITY: u16 = 422;
339    /// 429 Too Many Requests - Rate limit exceeded.
340    pub const TOO_MANY_REQUESTS: u16 = 429;
341
342    // 5xx Server Errors
343    /// 500 Internal Server Error - Unexpected server error.
344    pub const INTERNAL_SERVER_ERROR: u16 = 500;
345    /// 501 Not Implemented - Feature not implemented.
346    pub const NOT_IMPLEMENTED: u16 = 501;
347    /// 502 Bad Gateway - Invalid upstream response.
348    pub const BAD_GATEWAY: u16 = 502;
349    /// 503 Service Unavailable - Server temporarily unavailable.
350    pub const SERVICE_UNAVAILABLE: u16 = 503;
351    /// 504 Gateway Timeout - Upstream server timeout.
352    pub const GATEWAY_TIMEOUT: u16 = 504;
353}
354
355/// Prelude module for convenient imports.
356///
357/// # Usage
358///
359/// ```ignore
360/// use mik_sdk::prelude::*;
361/// ```
362///
363/// This imports:
364/// - [`Request`] - HTTP request wrapper with convenient accessors
365/// - [`Method`] - HTTP method enum (Get, Post, Put, etc.)
366/// - [`status`] - HTTP status code constants
367/// - [`mod@env`] - Environment variable access helpers
368/// - [`http_client`] - HTTP client for outbound requests
369/// - Core macros: [`ok!`], [`error!`], [`json!`], [`routes!`], [`log!`]
370/// - DX macros: [`guard!`],
371///   [`created!`], [`no_content!`], [`redirect!`], [`not_found!`],
372///   [`conflict!`], [`forbidden!`], [`ensure!`], [`fetch!`]
373pub mod prelude {
374    pub use crate::env;
375    pub use crate::http_client;
376    pub use crate::json;
377    pub use crate::json::ToJson;
378    pub use crate::log;
379    pub use crate::random;
380    pub use crate::request::{DecodeError, Method, Request};
381    pub use crate::status;
382    pub use crate::time;
383    // Typed input types
384    pub use crate::typed::{
385        FromJson, FromPath, FromQuery, Id, OpenApiSchema, ParseError, Validate, ValidationError,
386    };
387    // Core macros (json module already exported above)
388    pub use crate::{error, ok, routes};
389    // Derive macros for typed inputs
390    pub use crate::{Path, Query, Type};
391    // DX macros
392    pub use crate::{
393        accepted, bad_request, conflict, created, ensure, fetch, forbidden, guard, no_content,
394        not_found, redirect,
395    };
396
397    // SQL macros and types - only when sql feature is enabled
398    #[cfg(feature = "sql")]
399    pub use crate::query::{Cursor, PageInfo, Value};
400    #[cfg(feature = "sql")]
401    pub use crate::{sql_create, sql_delete, sql_read, sql_update};
402}
403
404// ============================================================================
405// API Contract Tests (compile-time assertions)
406// ============================================================================
407
408#[cfg(test)]
409mod api_contracts {
410    use static_assertions::{assert_impl_all, assert_not_impl_any};
411
412    // ========================================================================
413    // Request types
414    // ========================================================================
415
416    // Request is Debug but not Clone (body shouldn't be cloned)
417    assert_impl_all!(crate::Request: std::fmt::Debug);
418    assert_not_impl_any!(crate::Request: Clone);
419
420    // Method is Copy, Clone, Debug, PartialEq, Eq, Hash
421    assert_impl_all!(crate::Method: Copy, Clone, std::fmt::Debug, PartialEq, Eq, std::hash::Hash);
422
423    // Id is Clone, Debug, PartialEq, Eq, Hash (can be map key)
424    assert_impl_all!(crate::typed::Id: Clone, std::fmt::Debug, PartialEq, Eq, std::hash::Hash);
425
426    // ========================================================================
427    // JSON types
428    // ========================================================================
429
430    // JsonValue is Clone and Debug
431    assert_impl_all!(crate::json::JsonValue: Clone, std::fmt::Debug);
432
433    // JsonValue is NOT Send/Sync (uses Rc internally for WASM optimization)
434    assert_not_impl_any!(crate::json::JsonValue: Send, Sync);
435
436    // ========================================================================
437    // Error types
438    // ========================================================================
439
440    // ParseError is Clone, Debug, PartialEq, Eq
441    assert_impl_all!(crate::typed::ParseError: Clone, std::fmt::Debug, PartialEq, Eq);
442
443    // ValidationError is Clone, Debug, PartialEq, Eq
444    assert_impl_all!(crate::typed::ValidationError: Clone, std::fmt::Debug, PartialEq, Eq);
445
446    // DecodeError is Copy, Clone, Debug, PartialEq, Eq
447    assert_impl_all!(crate::DecodeError: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
448
449    // ========================================================================
450    // HTTP Client types (when http-client feature is enabled)
451    // ========================================================================
452
453    #[cfg(feature = "http-client")]
454    mod http_client_contracts {
455        use static_assertions::assert_impl_all;
456
457        // ClientRequest is Debug and Clone
458        assert_impl_all!(crate::http_client::ClientRequest: Clone, std::fmt::Debug);
459
460        // Response is Debug and Clone
461        assert_impl_all!(crate::http_client::Response: Clone, std::fmt::Debug);
462
463        // Error is Clone, Debug, PartialEq, Eq
464        assert_impl_all!(crate::http_client::Error: Clone, std::fmt::Debug, PartialEq, Eq);
465    }
466}