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}