sdforge 0.3.1

Multi-protocol SDK framework with unified macro configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
// Copyright (c) 2026 Kirky.X
// SPDX-License-Identifier: MIT
//! SDForge runtime library
//!
//! This crate provides the runtime types and service builders for the SDForge framework.

#![doc(html_root_url = "https://docs.rs/sdforge/0.3.0")]
#![warn(missing_docs)]

// Allow macro-generated code (which references `sdforge::cli::...`,
// `sdforge::prelude::ApiError`, etc.) to resolve when the `#[service_api]`
// macro is expanded inside the sdforge crate itself — e.g. in the in-crate
// `src/cli/tests/macro_integration_tests.rs` suite. Downstream crates resolve
// `sdforge::` naturally via the extern prelude; this alias mirrors that for
// self-references. `pub` visibility still applies, so only `pub` items are
// reachable through `sdforge::`.
extern crate self as sdforge;

/// Re-export macros from sdforge-macros for convenient use
pub use sdforge_macros::{service_api, service_module, test_macro};

/// Macro to implement Default::default() constructor for types
///
/// This macro generates both a `new()` static method and implements `Default` trait.
/// Useful for simple types that can be constructed with no arguments.
///
/// # Example
///
/// ```ignore
/// use sdforge::impl_default_new;
///
/// // Works with unit structs:
/// struct EmptyConfig;
/// impl_default_new!(EmptyConfig);
///
/// let config = EmptyConfig::new();
/// ```
#[macro_export]
macro_rules! impl_default_new {
    ($type:ident) => {
        impl $type {
            /// Create a new instance with default values
            pub fn new() -> Self {
                Self
            }
        }

        impl Default for $type {
            fn default() -> Self {
                Self::new()
            }
        }
    };
}

/// Re-export inventory for use in generated code
#[cfg(any(
    feature = "http",
    feature = "mcp",
    feature = "websocket",
    feature = "grpc",
    feature = "cli"
))]
pub use inventory;

/// Re-export tokio_stream for use in generated code
#[cfg(feature = "streaming")]
pub use tokio_stream;

/// Re-export axum types for use in generated code
#[cfg(feature = "http")]
pub mod axum {
    pub use axum::body::Body;
    pub use axum::response::IntoResponse;

    /// HTTP routing utilities
    pub mod routing {
        pub use axum::routing::{get, post, MethodRouter};
    }

    /// Extractor utilities for HTTP requests
    pub mod extract {
        pub use axum::extract::{Extension, Form, Json, Path, Query};
        pub use axum_extra::TypedHeader;
    }

    /// HTTP types and utilities
    pub mod http {
        pub use axum::http::Request;
        /// HTTP header utilities
        pub mod header {
            pub use axum::http::header::CONTENT_TYPE;
        }
        /// HTTP status codes
        pub mod status {
            pub use axum::http::StatusCode;
        }
    }

    /// Handler utilities
    pub mod handler {
        pub use axum::handler::Handler;
    }

    pub use axum::serve;
}

/// Commonly used types and re-exports
pub mod prelude {
    #[cfg(feature = "http")]
    pub use crate::core::validation::validators::{validate_email, validate_length};
    pub use crate::core::{ApiError, ApiMetadata, ServiceError, ServiceResponse};
    #[cfg(feature = "http")]
    pub use crate::http::{HttpRoute, RouteRegistration};
    #[cfg(feature = "mcp")]
    pub use crate::mcp::McpToolInstance;

    #[cfg(feature = "http")]
    pub use crate::axum::IntoResponse;

    pub use sdforge_macros::{service_api, service_module, test_macro};
}

/// Core types and utilities
pub mod core;

/// HTTP server and routing
#[cfg(feature = "http")]
pub mod http;

/// MCP (Model Context Protocol) support — built on official rmcp SDK
#[cfg(feature = "mcp")]
pub mod mcp;

// Re-export rmcp types for convenience (replaces old mcp_sdk re-exports)
#[cfg(feature = "mcp")]
pub use mcp::{
    build, get_mcp_tools, InputRequiredResult, McpHeaderInfo, McpToolInstance, McpToolRegistration,
    MrtrSession, SdForgeMcpServer, SdForgeTool, StatelessServerHandler,
};

/// Streaming utilities for SSE and streaming responses
#[cfg(feature = "streaming")]
pub mod streaming;

#[cfg(feature = "streaming")]
pub use streaming::{create_stream_channel, stream_to_sse, StreamEvent, StreamResponse};

/// Security middleware and authentication utilities.
///
/// Available when either `security` or `ratelimit` is enabled: the
/// `ratelimit` submodule lives under `crate::security::ratelimit` and must be
/// reachable even when the full `security` feature is off. Non-ratelimit
/// submodules are individually gated by `feature = "security"` inside
/// `src/security/mod.rs`.
#[cfg(any(feature = "security", feature = "ratelimit"))]
pub mod security;

#[cfg(feature = "security")]
pub use security::{
    auth_middleware,
    // Trait interfaces (feature layer)
    ApiKeyAuth,
    // Concrete implementations (renamed structs)
    AppApiKeyAuth,
    AppApiKeyAuthBuilder,
    AppAuditLogger,
    AppAuditLoggerBuilder,
    AuditLog,
    AuditLogger,
    // Supporting types
    AuditResult,
    AuthContext,
    AuthError,
    AuthExtractor,
    AuthMetadata,
    AuthResult,
    BearerAuth,
    BearerAuthBuilder,
};

/// Configuration management
#[cfg(feature = "http")]
pub mod config;

#[cfg(feature = "http")]
pub use config::{
    ApiConfig, AppConfig, AuthConfig, ConfigError, CorsConfig, EnvHelper, ServerConfig, TlsConfig,
    TracingConfig,
};

/// 直接透传 oxcache 库(缓存功能由 oxcache 统一提供)
#[cfg(feature = "cache")]
pub use oxcache;

/// 缓存模块(直接透传 oxcache 的缓存接口)
#[cfg(feature = "cache")]
pub mod cache;

#[cfg(feature = "cache")]
pub use cache::{Cache, CacheKey, DashMapCache, OxcacheSyncCache, SharedCache, SyncCache};

/// WebSocket support
#[cfg(feature = "websocket")]
pub mod websocket;

#[cfg(feature = "websocket")]
pub use websocket::{
    parse_websocket_message, websocket_upgrade, BoxFuture, ConnectionManager,
    ValidatedWebSocketUpgrade, WebSocketConfig, WebSocketConnection, WebSocketHandler,
    WebSocketMessage, WebSocketRoute,
};

/// gRPC server support
#[cfg(feature = "grpc")]
pub mod grpc;

#[cfg(feature = "grpc")]
pub use grpc::{
    build_server, build_server_with_config, GrpcRoute, GrpcServerConfig, SdForgeGrpcService,
};

/// Structured logging utilities
#[cfg(feature = "logging")]
pub mod logging;

#[cfg(feature = "logging")]
pub use logging::{
    get_global_logger, init_global_logger, LogEntry, LogLevel, LoggerConfig, StructuredLogger,
};

#[cfg(feature = "grpc")]
pub use grpc::sdforge_v1::{
    sd_forge_service_server::SdForgeServiceServer, CallRequest, CallResponse, InfoRequest,
    InfoResponse,
};

#[cfg(feature = "http")]
pub use http::version_routing::{build_version_router, VersionRouterConfig, VersionedRoute};

/// CLI (clap) integration — feature-gated by `cli`.
///
/// Promoted to `pub mod cli;` in T009 so that macro-generated
/// `sdforge::cli::CliCommandRegistration` paths (emitted by
/// `#[service_api(cli = true)]`) resolve both inside the sdforge crate
/// (via `extern crate self as sdforge;` above) and in downstream crates.
/// T010 wires the inventory iteration into `init_all_plugins`.
#[cfg(feature = "cli")]
pub mod cli;

/// OpenAPI 3.1 specification generation.
///
/// Only available when the `openapi` feature is enabled. See [`openapi`] module
/// docs for usage.
#[cfg(feature = "openapi")]
pub mod openapi;

#[cfg(feature = "openapi")]
pub use openapi::{generate_openapi_spec, OpenApiBuilder, OpenApiPathParam, OpenApiRouteInfo};

/// 统一文档输出模块 — Swagger UI + CLI/MCP Markdown。
///
/// 仅当 `docs` feature 启用时可用。T011-T019 实现逐步填充。
#[cfg(feature = "docs")]
pub mod docs;

#[cfg(feature = "docs")]
pub use docs::{generate_docs, write_docs, DocError, DocFormat};

#[cfg(feature = "docs")]
pub use docs::swagger_ui_router;

/// 初始化所有已注册的插件,确保它们不会被链接器优化掉。
///
/// This function must be called at least once to ensure that all inventory-based
/// registrations (HTTP routes, MCP tools, WebSocket routes, gRPC routes) are linked
/// into the final binary. Call this at the start of your application.
///
/// Returns the count of registered items for each type for debugging purposes.
///
/// # Example
///
/// ```ignore
/// use sdforge::init_all_plugins;
///
/// fn main() {
///     let counts = sdforge::init_all_plugins();
/// }
/// ```
#[cfg(any(
    feature = "http",
    feature = "mcp",
    feature = "websocket",
    feature = "grpc",
    feature = "cli"
))]
pub fn init_all_plugins() -> PluginCounts {
    use std::sync::Mutex;
    use std::sync::OnceLock;

    // Store in global static to prevent linker optimization.
    // Poison-aware: 若任一 inventory 收集期间 panic 导致 Mutex 中毒,
    // 降级返回 0 而非连锁 panic,避免 init_all_plugins 永久失效。
    #[cfg(feature = "http")]
    let routes = {
        use crate::http::RouteRegistration;

        static ROUTES: OnceLock<Mutex<Vec<&'static RouteRegistration>>> = OnceLock::new();
        let routes =
            ROUTES.get_or_init(|| Mutex::new(inventory::iter::<RouteRegistration>().collect()));
        routes.lock().map(|g| g.len()).unwrap_or_else(|e| {
            log::error!("inventory Mutex poisoned: {}", e);
            0
        })
    };
    #[cfg(not(feature = "http"))]
    let routes = 0;

    #[cfg(feature = "mcp")]
    let mcp_tools = {
        use crate::mcp::McpToolRegistration;

        static MCP_TOOLS: OnceLock<Mutex<Vec<&'static McpToolRegistration>>> = OnceLock::new();
        let tools = MCP_TOOLS
            .get_or_init(|| Mutex::new(inventory::iter::<McpToolRegistration>().collect()));
        tools.lock().map(|g| g.len()).unwrap_or_else(|e| {
            log::error!("inventory Mutex poisoned: {}", e);
            0
        })
    };
    #[cfg(feature = "websocket")]
    let ws_routes = {
        use crate::websocket::WebSocketRoute;

        static WS_ROUTES: OnceLock<Mutex<Vec<&'static WebSocketRoute>>> = OnceLock::new();
        let routes =
            WS_ROUTES.get_or_init(|| Mutex::new(inventory::iter::<WebSocketRoute>().collect()));
        routes.lock().map(|g| g.len()).unwrap_or_else(|e| {
            log::error!("inventory Mutex poisoned: {}", e);
            0
        })
    };
    #[cfg(feature = "grpc")]
    let grpc_routes = {
        use crate::grpc::GrpcRouteRegistration;

        static GRPC_ROUTES: OnceLock<Mutex<Vec<&'static GrpcRouteRegistration>>> = OnceLock::new();
        let routes = GRPC_ROUTES
            .get_or_init(|| Mutex::new(inventory::iter::<GrpcRouteRegistration>().collect()));
        routes.lock().map(|g| g.len()).unwrap_or_else(|e| {
            log::error!("inventory Mutex poisoned: {}", e);
            0
        })
    };

    // T010: touch CLI inventory so the linker keeps `inventory::submit!`
    // blocks emitted by `#[service_api(cli = true)]`. Mirrors the http/mcp/
    // websocket/grpc blocks above. Both `CliCommandRegistration` and
    // `CliHandlerRegistration` are collected; the returned count reflects
    // command registrations (handler registrations are paired 1:1).
    #[cfg(feature = "cli")]
    let cli_commands = {
        use crate::cli::{CliCommandRegistration, CliHandlerRegistration};

        static CLI_CMDS: OnceLock<Mutex<Vec<&'static CliCommandRegistration>>> = OnceLock::new();
        let cmds = CLI_CMDS
            .get_or_init(|| Mutex::new(inventory::iter::<CliCommandRegistration>().collect()));

        // Also iterate handler registrations to prevent the linker from
        // stripping the paired `CliHandlerRegistration` submit blocks.
        static CLI_HANDLERS: OnceLock<Mutex<Vec<&'static CliHandlerRegistration>>> =
            OnceLock::new();
        let _handlers = CLI_HANDLERS
            .get_or_init(|| Mutex::new(inventory::iter::<CliHandlerRegistration>().collect()));

        cmds.lock().map(|g| g.len()).unwrap_or_else(|e| {
            log::error!("inventory Mutex poisoned: {}", e);
            0
        })
    };

    PluginCounts {
        routes,
        #[cfg(feature = "mcp")]
        mcp_tools,
        #[cfg(feature = "websocket")]
        ws_routes,
        #[cfg(feature = "grpc")]
        grpc_routes,
        #[cfg(feature = "cli")]
        cli_commands,
    }
}

/// Counts of registered plugins after initialization
///
/// This struct provides visibility into which protocol implementations
/// have been registered via inventory and are available at runtime.
///
/// # Usage
///
/// ```ignore
/// use sdforge::init_all_plugins;
///
/// fn main() {
///     let counts = init_all_plugins();
///     
///     println!("Registered:");
///     println!("  HTTP routes: {}", counts.routes);
///     #[cfg(feature = "mcp")]
///     println!("  MCP tools: {}", counts.mcp_tools);
///     #[cfg(feature = "websocket")]
///     println!("  WebSocket routes: {}", counts.ws_routes);
///     #[cfg(feature = "grpc")]
///     println!("  gRPC routes: {}", counts.grpc_routes);
/// }
/// ```
///
/// # Feature Flags
///
/// Fields are conditionally compiled based on features:
/// - `routes`: Always present when any protocol feature is enabled
/// - `mcp_tools`: Only with `mcp` feature
/// - `ws_routes`: Only with `websocket` feature
/// - `grpc_routes`: Only with `grpc` feature
/// - `cli_commands`: Only with `cli` feature
#[cfg(any(
    feature = "http",
    feature = "mcp",
    feature = "websocket",
    feature = "grpc",
    feature = "cli"
))]
pub struct PluginCounts {
    /// Number of registered HTTP routes
    pub routes: usize,
    /// Number of registered MCP tools
    #[cfg(feature = "mcp")]
    pub mcp_tools: usize,
    /// Number of registered WebSocket routes
    #[cfg(feature = "websocket")]
    pub ws_routes: usize,
    /// Number of registered gRPC routes
    #[cfg(feature = "grpc")]
    pub grpc_routes: usize,
    /// Number of registered CLI commands (emitted by `#[service_api(cli = true)]`)
    #[cfg(feature = "cli")]
    pub cli_commands: usize,
}

/// Get all registered WebSocket routes
#[cfg(feature = "websocket")]
pub fn get_websocket_routes() -> Vec<&'static crate::websocket::WebSocketRoute> {
    inventory::iter::<crate::websocket::WebSocketRoute>().collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    // ============================================================================
    // init_all_plugins tests
    //
    // init_all_plugins uses process-wide OnceLocks to cache inventory iterations,
    // so the function is idempotent: the first call initializes the locks and
    // subsequent calls return the same counts. We use #[serial] to ensure these
    // tests don't run concurrently with any other test that may touch the same
    // globals.
    // ============================================================================

    #[test]
    #[serial_test::serial]
    fn test_init_all_plugins_returns_counts() {
        let counts = init_all_plugins();
        // With the `full` feature, all protocol features are enabled. The exact
        // counts depend on how many inventory items are registered in the test
        // binary, so we only assert structural correctness here.
        let _ = counts.routes;
        #[cfg(feature = "mcp")]
        let _ = counts.mcp_tools;
        #[cfg(feature = "websocket")]
        let _ = counts.ws_routes;
        #[cfg(feature = "grpc")]
        let _ = counts.grpc_routes;
        #[cfg(feature = "cli")]
        let _ = counts.cli_commands;
    }

    #[test]
    #[serial_test::serial]
    fn test_init_all_plugins_is_idempotent() {
        // Calling init_all_plugins twice should return consistent counts
        // because the OnceLocks cache the inventory iteration results.
        let first = init_all_plugins();
        let second = init_all_plugins();
        assert_eq!(first.routes, second.routes);
        #[cfg(feature = "mcp")]
        assert_eq!(first.mcp_tools, second.mcp_tools);
        #[cfg(feature = "websocket")]
        assert_eq!(first.ws_routes, second.ws_routes);
        #[cfg(feature = "grpc")]
        assert_eq!(first.grpc_routes, second.grpc_routes);
        #[cfg(feature = "cli")]
        assert_eq!(first.cli_commands, second.cli_commands);
    }

    // ============================================================================
    // get_mcp_tools tests
    // ============================================================================

    #[cfg(feature = "mcp")]
    #[test]
    fn test_get_mcp_tools_returns_vec() {
        // get_mcp_tools collects inventory::iter::<McpToolRegistration> into
        // a Vec<McpToolInstance>. The exact count depends on how many tools
        // are registered in the test binary, so we only assert it returns
        // without panicking.
        let tools = get_mcp_tools();
        let _ = tools.len();
    }

    // ============================================================================
    // get_websocket_routes tests
    // ============================================================================

    #[cfg(feature = "websocket")]
    #[test]
    fn test_get_websocket_routes_returns_vec() {
        // get_websocket_routes collects inventory::iter::<WebSocketRoute>
        // into a Vec. The exact count depends on registrations in the test
        // binary, so we only assert it returns without panicking.
        let routes = get_websocket_routes();
        let _ = routes.len();
    }

    // ============================================================================
    // impl_default_new macro test
    // ============================================================================

    #[test]
    fn test_impl_default_new_macro() {
        struct EmptyConfig;
        impl_default_new!(EmptyConfig);

        let config = EmptyConfig::new();
        let _default = EmptyConfig;
        // Ensure both construction paths produce the same type
        let _: EmptyConfig = config;
    }

    // ============================================================================
    // impl_default_new! macro: Default trait integration
    //
    // The macro generates both `new()` and `Default::default()`. The existing
    // test only calls `new()`. These tests verify the generated `Default`
    // impl produces an equivalent value and that the macro can be applied to
    // multiple distinct unit structs in the same scope.
    // ============================================================================

    /// Test the impl_default_new! macro generates a working Default impl
    /// whose `default()` equals `new()`.
    #[test]
    #[allow(clippy::default_constructed_unit_structs)] // test asserts Default impl exists
    fn test_impl_default_new_macro_generates_default_trait() {
        struct ConfigA;
        impl_default_new!(ConfigA);

        let from_new = ConfigA::new();
        let from_default = ConfigA::default();
        // Unit structs have a single value, so both must be equal in type.
        let _: ConfigA = from_new;
        let _: ConfigA = from_default;
    }

    /// Test the impl_default_new! macro can be applied to multiple distinct
    /// unit structs without name collisions.
    #[test]
    #[allow(clippy::default_constructed_unit_structs)] // test asserts Default impl exists
    fn test_impl_default_new_macro_multiple_structs() {
        struct FirstConfig;
        struct SecondConfig;
        impl_default_new!(FirstConfig);
        impl_default_new!(SecondConfig);

        let _a = FirstConfig::new();
        let _b = SecondConfig::new();
        let _a2 = FirstConfig::default();
        let _b2 = SecondConfig::default();
    }

    // ============================================================================
    // PluginCounts struct: field access under the http,cache feature set
    //
    // When only `http` and `cache` features are enabled, PluginCounts has a
    // single `routes` field (the mcp_tools/ws_routes/grpc_routes fields are
    // cfg-gated out). These tests verify the struct can be constructed and
    // its field accessed without panicking.
    // ============================================================================

    /// Test PluginCounts can be constructed via init_all_plugins and the
    /// `routes` field is a non-negative usize.
    #[test]
    #[serial_test::serial]
    fn test_plugin_counts_routes_field_is_usize() {
        let counts = init_all_plugins();
        // routes is always present when any protocol feature is enabled.
        let routes: usize = counts.routes;
        // usize is always >= 0 by definition; this just ensures the field
        // is accessible and has a sane value type.
        let _ = routes;
    }

    /// Test calling init_all_plugins multiple times returns consistent counts
    /// (idempotency via the internal OnceLocks), verifying the cached
    /// inventory iteration does not change between calls.
    #[test]
    #[serial_test::serial]
    fn test_init_all_plugins_counts_are_stable_across_calls() {
        let a = init_all_plugins();
        let b = init_all_plugins();
        let c = init_all_plugins();
        assert_eq!(a.routes, b.routes);
        assert_eq!(b.routes, c.routes);
    }
}