Skip to main content

socle/
lib.rs

1//! # socle
2//!
3//! Opinionated axum service bootstrap: telemetry, database, rate limiting, and
4//! shutdown in one builder. Public open-source facade extracted from an internal
5//! service kit.
6//!
7//! ## Recommended middleware ordering
8//!
9//! ```text
10//! RequestIdLayer → auth (placeholder) → OrgIsolationLayer → rate-limit → audit → handler
11//! ```
12//!
13//! ```rust,no_run
14//! use socle::{ServiceBootstrap, BootstrapCtx, Result};
15//! use axum::{Router, routing::get};
16//!
17//! # #[tokio::main] async fn main() -> Result<()> {
18//! ServiceBootstrap::new("billing-service")
19//!     .with_telemetry()
20//!     .with_database("postgres://localhost/billing")
21//!     .with_router(|_ctx: &BootstrapCtx| Router::new().route("/health", get(|| async { "ok" })))
22//!     .serve("0.0.0.0:8080")
23//!     .await
24//! # }
25//! ```
26
27#![allow(clippy::result_large_err)]
28
29// ── Internal modules ──────────────────────────────────────────────────────────
30
31pub(crate) mod adapters;
32pub(crate) mod bootstrap;
33pub mod ports;
34
35mod config;
36mod error;
37mod handler_error;
38mod request_id;
39
40pub mod org_isolation;
41pub mod org_policy;
42
43pub mod audit;
44pub mod etag;
45pub mod extract;
46pub mod pagination;
47
48#[cfg(feature = "testing")]
49pub mod testing;
50
51#[cfg(feature = "http-client")]
52pub mod http_client;
53
54#[cfg(feature = "metrics")]
55pub mod metrics;
56
57// ── Public surface ────────────────────────────────────────────────────────────
58
59pub use audit::{
60    AuditAnnotation, AuditAnnotationSlot, AuditEvent, AuditFilter, AuditLayer, AuditService,
61    AuditSink, AuditSinkError, TracingAuditSink,
62};
63pub use bootstrap::{BootstrapCtx, ServiceBootstrap, ShutdownHookFn};
64pub use config::{BootstrapConfig, CorsConfig, LogFormat, RateLimitConfig, RateLimitKind};
65pub use error::{Error, Result};
66pub use etag::{ETag, IfMatch, IfNoneMatch, check_if_match, etag_from_updated_at};
67pub use handler_error::{
68    ApiError, CreatedAtResponse, CreatedResponse, ErrorCode, EtaggedHandlerResponse, HandlerError,
69    HandlerListResponse, HandlerResponse, ProblemJson, UnconstrainedResponse, ValidationError,
70    created, created_at, created_under, etagged, listed, listed_page, ok,
71};
72
73#[cfg(feature = "rfc-types")]
74pub use handler_error::RfcOk;
75
76#[cfg(feature = "test-util")]
77pub use audit::ChannelAuditSink;
78
79#[cfg(feature = "nats")]
80pub use audit::NatsJetStreamAuditSink;
81
82pub use org_isolation::{
83    OrgContextExtractor, OrgContextSource, OrgIsolationLayer, OrgIsolationService,
84};
85pub use org_policy::{AncestryOrgPolicy, OrgPolicy};
86
87pub use pagination::{
88    CursorPaginatedResponse, CursorPagination, CursorPaginationParams, KeysetPaginatedResponse,
89    KeysetPaginationParams, PaginatedResponse, PaginationParams, SortDirection, SortParams,
90};
91
92#[cfg(feature = "cursor")]
93pub use pagination::{Cursor, CursorError};
94
95#[cfg(feature = "ratelimit")]
96pub use adapters::security::rate_limit::{RateLimitBackend, RateLimitExtractor};
97
98#[cfg(feature = "validation")]
99pub use extract::Valid;
100
101pub use ports::auth::AuthProvider;
102pub use ports::health::ReadinessCheckFn;
103pub use ports::rate_limit::RateLimitProvider;
104#[cfg(feature = "telemetry")]
105pub use ports::telemetry::{BasicTelemetryProvider, TelemetryProvider};
106
107// ── api-bones re-exports ──────────────────────────────────────────────────────
108
109pub use api_bones::org_context::OrganizationContext;
110pub use api_bones::org_id::{OrgId, OrgPath};
111
112pub use api_bones::common::{ResourceId, Timestamp};
113pub use api_bones::ratelimit::RateLimitInfo;
114
115pub use api_bones::AuditInfo;
116pub use api_bones::{ApiResponse, ApiResponseBuilder, ResponseMeta};
117pub use api_bones::{BulkItemResult, BulkRequest, BulkResponse};
118pub use api_bones::{CorrelationId, CorrelationIdError};
119pub use api_bones::{IdempotencyKey, IdempotencyKeyError};
120pub use api_bones::{Link, Links};
121pub use api_bones::{RequestId, RequestIdParseError};
122pub use api_bones::{Slug, SlugError};
123
124/// Generate a `fn main()` for a `generate-openapi` binary in one line.
125///
126/// # Usage
127///
128/// ```rust,no_run
129/// # #[cfg(feature = "openapi")] {
130/// use utoipa::OpenApi;
131///
132/// #[derive(OpenApi)]
133/// #[openapi()]
134/// struct ApiDoc;
135///
136/// socle::generate_openapi_binary!(ApiDoc);
137/// # }
138/// ```
139#[cfg(feature = "openapi")]
140#[macro_export]
141macro_rules! generate_openapi_binary {
142    ($api_doc:ty) => {
143        $crate::generate_openapi_binary!($api_doc, "/health");
144    };
145    ($api_doc:ty, $health_path:expr) => {
146        fn main() {
147            use utoipa::OpenApi as _;
148            use $crate::openapi::{merge_health_paths, to_3_0_pretty_json};
149            let mut doc = <$api_doc>::openapi();
150            merge_health_paths(&mut doc, $health_path);
151            match to_3_0_pretty_json(&doc) {
152                Ok(json) => println!("{json}"),
153                Err(e) => {
154                    eprintln!("generate-openapi: serialize failed: {e}");
155                    std::process::exit(1);
156                }
157            }
158        }
159    };
160}
161
162/// OpenAPI 3.0.3 helpers for `axum` + `utoipa` + `progenitor` consumers.
163#[cfg(feature = "openapi")]
164pub mod openapi;
165
166/// Re-exports of the underlying crates.
167pub mod reexports {
168    pub use api_bones;
169}
170
171#[cfg(test)]
172mod tests {
173    use crate::error::Error;
174
175    #[test]
176    fn error_display_covers_all_variants() {
177        assert!(Error::Config("x".into()).to_string().contains("x"));
178        assert!(Error::Telemetry("x".into()).to_string().contains("x"));
179        assert!(Error::Database("x".into()).to_string().contains("x"));
180        assert!(Error::Bind("x".into()).to_string().contains("x"));
181        assert!(Error::Serve("x".into()).to_string().contains("x"));
182    }
183
184    #[cfg(feature = "telemetry")]
185    #[test]
186    fn init_basic_tracing_is_idempotent() {
187        crate::adapters::observability::telemetry::init_basic_tracing();
188        crate::adapters::observability::telemetry::init_basic_tracing();
189    }
190}