Skip to main content

fandhe_backend_core/
lib.rs

1//! fandhe-backend の最小コア。
2//!
3//! # このクレートの役割
4//!
5//! `crates/core` は HTTP/1.1 パーサ・keep-alive・3 種拡張点
6//! (`Middleware` / `UpgradeHandler` / `RequestGate`、[`extension`] モジュール)を
7//! 実装する最小コアの置き場所。TASK-1.4-1(#69)で 3 拡張点の trait 定義を、
8//! TASK-1.4-2(#70)でコアループ(接続受理・リクエストループ、[`server`]
9//! モジュール)による実接続を提供する。3 拡張点で表現できないリダイレクト・
10//! レスポンス改変([`interceptor::Interceptor`]、イシュー #420)は feature
11//! ゲート不要の追加拡張点として同モジュールに置く(詳細は [`interceptor`] を参照)。
12//!
13//! # workspace 内での依存方向
14//!
15//! `docs/spec/04-requirements.md` REQ-1 / `docs/spec/05-tasks.md` TASK-1.5 の方針に従い、
16//! workspace 全体の依存方向は次の一方向を維持する:
17//!
18//! ```text
19//! server → routes → http::*
20//! ```
21//!
22//! `core`(本クレート)はこの依存グラフの末端に位置し、`plugin-*` の固有シンボルには
23//! 一切依存しない。プラグインは feature 経由でコアの拡張点(`Middleware` /
24//! `UpgradeHandler` / `RequestGate`)を実装する側であり、コアからプラグインへの
25//! 依存は発生しない(pay-for-what-you-use、.claude/rules/pay-for-what-you-use.md)。
26//! `fandhe-backend-http`(sans-IO な HTTP/1.1 パーサ、TASK-1.3)は workspace 内の下位層クレート
27//! であり、外部 crates.io 依存はここでも増やさない。
28//!
29//! `crates/routes`(`fandhe-backend-routes`)は TASK-1.5(#14)で新設し、`server → routes`
30//! エッジを `Cargo.toml` の依存宣言として実体化した。`fandhe_backend_routes::Router` は
31//! `impl` [`server::Handler`] `for` `fandhe_backend_routes::Router` により本クレートの
32//! 既定ハンドラとしてそのまま登録できる(`crates/core/examples/minimal.rs`
33//! 参照)。依存方向の機械検証は `scripts/dep-direction-check.sh`(TASK-1.5)が
34//! `cargo metadata` の依存エッジホワイトリスト照合で行う。
35//!
36//! # feature 一覧
37//!
38//! - `webrtc-proxy`(TASK-2.1 / #18): `crates/plugin-webrtc-proxy` を
39//!   `optional = true` + `dep:` 構文で組み込み、[`server::Server::webrtc_proxy`]
40//!   で登録した `fandhe_backend_plugin_webrtc_proxy::ProxyConfig` に基づき `POST
41//!   /rtc/offer` をパスインターセプトする。無効時(既定)は依存・コード・
42//!   `unsafe` を一切含まない(クレート非公開の `plugin` モジュールの doc・
43//!   `docs/design/plugin-boundary.md` を参照)。
44//! - `webrtc`(TASK-8.1 / #26): `crates/plugin-webrtc`(in-process 型、
45//!   `webrtc-rs` 直接依存)を同じくプラグイン境界パターンで組み込み、
46//!   [`server::Server::webrtc`] で登録した `fandhe_backend_plugin_webrtc::WebRtcConfig` に
47//!   基づき `POST /rtc/offer` をパスインターセプトする。`webrtc-proxy` と
48//!   同時有効時は `webrtc-proxy` が優先される(`plugin::try_intercept` の
49//!   doc)。無効時(既定)は依存・コード・`unsafe` を一切含まない。
50//! - `websocket`(TASK-4.1 / #22): `crates/plugin-websocket` を
51//!   `optional = true` + `dep:` 構文で組み込み、[`server::Server::websocket`]
52//!   で登録した `fandhe_backend_plugin_websocket::WebSocketConfig` の指すパス(既定
53//!   `/ws`)への `GET` + `Upgrade: websocket` を `UpgradeHandler` 拡張点経由で
54//!   検知し、`fandhe_backend_plugin_websocket::handle_upgrade` へ完全委譲する(RFC 6455
55//!   ハンドシェイク・フレーミングは `crates/plugin-websocket` 側の責務)。設定型
56//!   `WebSocketConfig` は [`plugin_websocket`] モジュールとして再エクスポートし
57//!   (イシュー #435)、`fandhe-backend-plugin-websocket` へ直接依存せずに構築できる。
58//!   無効時(既定)は依存・コード・`unsafe` を一切含まない。
59//! - `graphql`(TASK-2.4 / #21): `crates/plugin-graphql` を組み込み、
60//!   [`server::Server::graphql`] へ渡す設定型 `GraphQlConfig` を
61//!   [`plugin_graphql`] モジュールとして再エクスポートする(イシュー #435)。
62//!   利用者は `fandhe-backend-plugin-graphql` へ直接依存せずに
63//!   `fandhe_backend_core::plugin_graphql::GraphQlConfig` を構築できる。
64//!   無効時(既定)は依存・コード・`unsafe` を一切含まない。
65//! - `cors`(イシュー #305): `crates/plugin-cors` を組み込み、
66//!   [`server::Server::cors`] へ渡す設定型 `CorsConfig` を
67//!   [`plugin_cors`] モジュールとして再エクスポートする(イシュー #435)。
68//!   利用者は `fandhe-backend-plugin-cors` へ直接依存せずに
69//!   `fandhe_backend_core::plugin_cors::CorsConfig` を構築できる。
70//!   無効時(既定)は依存・コード・`unsafe` を一切含まない。
71//! - `tracing`(TASK-10.1 / #56): `crates/plugin-tracing` を組み込み、
72//!   [`server::Server::tracing`] へ渡す設定型 `TracingConfig` を
73//!   [`plugin_tracing`] モジュールとして再エクスポートする(イシュー #435)。
74//!   利用者は `fandhe-backend-plugin-tracing` へ直接依存せずに
75//!   `fandhe_backend_core::plugin_tracing::TracingConfig` を構築できる。
76//!   無効時(既定)は依存・コード・`unsafe` を一切含まない。
77//! - `openapi`(TASK-2.1 / #256): `crates/plugin-openapi` を組み込み、
78//!   [`server::Server::openapi_with`] へ渡す設定型 `OpenApiDoc` を
79//!   [`plugin_openapi`] モジュールとして再エクスポートする(イシュー #435)。
80//!   利用者は `fandhe-backend-plugin-openapi` へ直接依存せずに
81//!   `fandhe_backend_core::plugin_openapi::OpenApiDoc` を構築できる。
82//!   無効時(既定)は依存・コード・`unsafe` を一切含まない。
83//! - `static`(イシュー #318): `crates/plugin-static` を組み込み、
84//!   [`server::Server::static_files`] へ渡す設定型 `StaticFilesConfig` を
85//!   [`plugin_static`] モジュールとして再エクスポートする(イシュー #421)。
86//!   利用者は `fandhe-backend-plugin-static` へ直接依存せずに
87//!   `fandhe_backend_core::plugin_static::StaticFilesConfig` を構築できる。
88//!   無効時(既定)は依存・コード・`unsafe` を一切含まない。
89//! - `compression`(イシュー #321): `crates/plugin-compression` を組み込み、
90//!   [`server::Server::compression`] へ渡す設定型 `CompressionConfig` を
91//!   [`plugin_compression`] モジュールとして再エクスポートする(イシュー #421)。
92//!   利用者は `fandhe-backend-plugin-compression` へ直接依存せずに
93//!   `fandhe_backend_core::plugin_compression::CompressionConfig` を構築できる。
94//!   無効時(既定)は依存・コード・`unsafe` を一切含まない。
95//!
96//! `webrtc` / `webrtc-proxy` の設定型 `WebRtcConfig` / `ProxyConfig` も同様に
97//! それぞれ [`plugin_webrtc`] / [`plugin_webrtc_proxy`] モジュールとして
98//! 再エクスポートする(イシュー #435)。
99//!
100//! # 今後のタスクとの対応
101//!
102//! - TASK-2.1(#18)で確立したプラグイン境界パターン(feature flag + `dep:`
103//!   構文、cfg-free なコアループ + 固定シグネチャシーム)は非公開 `plugin`
104//!   モジュールの `try_intercept`(パスインターセプト型)・
105//!   `try_handle_upgrade`(Upgrade 型、TASK-4.1 / #22 で実装)の 2 種として
106//!   実証済みであり、後続プラグインは `docs/design/plugin-boundary.md` の
107//!   適用手順に従って同パターンを踏襲する
108//!
109//! # クイックスタート(TASK-11.5 / #95、`docs/guide/getting-started.md` の裏取り)
110//!
111//! [`Server`] に [`fandhe_backend_routes::Router`] を 1 件登録するだけで最小サーバが
112//! 組み立てられる。`bind` → `run` の流れは `crates/core/examples/minimal.rs`
113//! (`cargo run --example minimal -p fandhe-backend-core` で実行可能)と同一。
114//! 本 doc test は `no_run`(実際に listen はしない)だが `cargo test --doc` で
115//! コンパイル可能性を機械検証する(AI ファースト保守性、
116//! `.claude/rules/feature-modification.md` の「実装にはテスト追加を伴う」)。
117//!
118//! ```no_run
119//! use fandhe_backend_core::Server;
120//! use fandhe_backend_http::response::Response;
121//! use fandhe_backend_routes::Router;
122//!
123//! # async fn quickstart() -> std::io::Result<()> {
124//! let router = Router::new().route("GET", "/", |_head, _body| {
125//!     Response::new(200, b"hello\n".to_vec())
126//! });
127//!
128//! let server = Server::new().handler(router);
129//! // 外部公開する場合は呼び出し側の責任でバインドアドレスを明示する
130//! // (.claude/rules/security.md の攻撃表面最小化)。ここではループバック限定。
131//! let bound = server.bind("127.0.0.1:3000").await?;
132//! bound.run().await
133//! # }
134//! ```
135//!
136//! プロセス終了を `run()` の kill に頼らず、accept 停止 → in-flight
137//! 完了待ち(上限時間・超過時強制クローズ)を伴って安全に停止したい場合は
138//! [`server::BoundServer::run_until`](イシュー #313)を使う。`run()` は
139//! `run_until` への薄い委譲であり後方互換を維持する。利用例は
140//! `crates/core/examples/graceful_shutdown.rs`、設計判断は
141//! `docs/design/graceful-shutdown.md` を参照。
142
143pub mod diagnostics;
144pub mod extension;
145pub mod interceptor;
146pub(crate) mod plugin;
147pub(crate) mod send_buffer;
148pub mod server;
149pub mod streaming;
150
151// 3 拡張点はクレート直下からも参照できるよう re-export する。プラグイン側
152// (`crates/plugin-*`)はこの再エクスポート経由で `fandhe_backend_core::Middleware`
153// のように参照でき、`extension` モジュールの存在を意識せずに実装できる。
154pub use extension::{GateContext, GateOutcome, Middleware, RequestGate, UpgradeHandler};
155
156// ユーザー向けインターセプト・レスポンス改変拡張点(イシュー #420)。
157// 既存 3 拡張点(Middleware/RequestGate/UpgradeHandler)で表現できないリダイレクト・
158// レスポンス改変ユースケースの受け皿。詳細契約は `interceptor` モジュール doc を参照。
159pub use interceptor::Interceptor;
160
161// ライブラリ内部の実行時診断(accept 失敗・grace 超過強制クローズ等)を
162// 利用側で差し替え可能にする診断シンク(イシュー #720)。`Server::diagnostics`
163// で登録する。詳細契約は `diagnostics` モジュール doc を参照。
164pub use diagnostics::{DiagnosticEvent, Diagnostics, StderrDiagnostics};
165
166// コアループの主要 API もクレート直下から参照できるよう re-export する。
167// `handle_connection_with_peer_addr` は `RequestGate::check` へ実 peer address
168// を伝搬させたいカスタム accept ループ向けの公開 API(イシュー #486)。
169// `RebindHandle`(イシュー #485)は稼働中の `BoundServer::run_until` へ
170// listener 差し替えを指示するハンドル(`BoundServer::rebind_handle` で取得)。
171pub use server::{
172    BoundServer, Handler, RebindHandle, Server, handle_connection, handle_connection_with_peer_addr,
173};
174
175// レスポンス側 chunked ストリーミング送信(イシュー #319)の opt-in API。
176pub use streaming::{BodyWriter, StreamClosed, StreamingResponse};
177
178/// 静的ファイル配信プラグイン(`crates/plugin-static`)の再エクスポート
179/// (`static` feature 限定、イシュー #421)。
180///
181/// [`server::Server::static_files`] へ渡す [`StaticFilesConfig`](および
182/// `StaticFilesConfigBuilder` / `StaticConfigError` 等の付随型)を、
183/// `fandhe-backend-plugin-static` への直接依存を追加せずに構築できるようにする
184/// 唯一の目的で存在する薄い再エクスポート(whole-crate 形式。プラグイン側に
185/// 型が増えても本モジュールの追随は不要)。ハンドラ本体
186/// (`try_handle_static` 等)はコア内部の `crate::plugin` シームから
187/// 呼ばれる実装詳細であり、本モジュール経由での利用は想定しない。
188///
189/// [`StaticFilesConfig`]: fandhe_backend_plugin_static::StaticFilesConfig
190///
191/// # Examples
192///
193/// ```
194/// use fandhe_backend_core::plugin_static::StaticFilesConfig;
195///
196/// let config = StaticFilesConfig::builder("/static", std::env::temp_dir()).build();
197/// assert!(config.is_ok());
198/// ```
199#[cfg(feature = "static")]
200pub use fandhe_backend_plugin_static as plugin_static;
201
202/// レスポンス圧縮プラグイン(`crates/plugin-compression`)の再エクスポート
203/// (`compression` feature 限定、イシュー #421)。
204///
205/// [`server::Server::compression`] へ渡す [`CompressionConfig`](および
206/// `CompressionConfigBuilder` 等の付随型)を、
207/// `fandhe-backend-plugin-compression` への直接依存を追加せずに構築できる
208/// ようにする唯一の目的で存在する薄い再エクスポート(`plugin_static` と同一の
209/// whole-crate パターン)。圧縮適用本体(`apply_compression` 等)はコア内部の
210/// `crate::plugin::finalize_response` シームから呼ばれる実装詳細であり、
211/// 本モジュール経由での利用は想定しない。
212///
213/// [`CompressionConfig`]: fandhe_backend_plugin_compression::CompressionConfig
214///
215/// # Examples
216///
217/// ```
218/// use fandhe_backend_core::plugin_compression::CompressionConfig;
219///
220/// let config = CompressionConfig::builder().build();
221/// let _ = config;
222/// ```
223#[cfg(feature = "compression")]
224pub use fandhe_backend_plugin_compression as plugin_compression;
225
226/// WebSocket プラグイン(`crates/plugin-websocket`)の再エクスポート
227/// (`websocket` feature 限定、イシュー #435。#421 で `static` /
228/// `compression` に導入したパターンの水平展開)。
229///
230/// [`server::Server::websocket`] へ渡す [`WebSocketConfig`](および
231/// `WsMessageHandler` 等の付随型)を、`fandhe-backend-plugin-websocket` への
232/// 直接依存を追加せずに構築できるようにする唯一の目的で存在する薄い
233/// 再エクスポート(`plugin_static` と同一の whole-crate パターン)。
234/// アップグレードハンドラ本体(`handle_upgrade` 等)はコア内部の
235/// `crate::server` の `UpgradeHandler` アダプタから呼ばれる実装詳細であり、
236/// 本モジュール経由での利用は想定しない。
237///
238/// [`WebSocketConfig`]: fandhe_backend_plugin_websocket::WebSocketConfig
239///
240/// # Examples
241///
242/// ```
243/// use fandhe_backend_core::plugin_websocket::WebSocketConfig;
244///
245/// let config = WebSocketConfig::default();
246/// assert_eq!(config.path, "/ws");
247/// ```
248#[cfg(feature = "websocket")]
249pub use fandhe_backend_plugin_websocket as plugin_websocket;
250
251/// GraphQL プラグイン(`crates/plugin-graphql`)の再エクスポート
252/// (`graphql` feature 限定、イシュー #435)。
253///
254/// [`server::Server::graphql`] へ渡す [`GraphQlConfig`] を、
255/// `fandhe-backend-plugin-graphql` への直接依存を追加せずに構築できるように
256/// する唯一の目的で存在する薄い再エクスポート(`plugin_static` と同一の
257/// whole-crate パターン)。クエリ実行本体(`try_handle_graphql` 等)はコア
258/// 内部の `crate::plugin::try_intercept` シームから呼ばれる実装詳細であり、
259/// 本モジュール経由での利用は想定しない。クエリ深さ・複雑度制限は
260/// [`GraphQlConfig::new`] の doc のとおりスキーマ登録者(呼び出し元)の責務
261/// のまま変わらない。
262///
263/// [`GraphQlConfig`]: fandhe_backend_plugin_graphql::GraphQlConfig
264/// [`GraphQlConfig::new`]: fandhe_backend_plugin_graphql::GraphQlConfig::new
265///
266/// # Examples
267///
268/// ```
269/// use async_graphql::Value;
270/// use async_graphql::dynamic::{Field, FieldFuture, Object, Schema, TypeRef};
271/// use fandhe_backend_core::plugin_graphql::GraphQlConfig;
272///
273/// let query = Object::new("Query").field(Field::new(
274///     "hello",
275///     TypeRef::named_nn(TypeRef::STRING),
276///     |_ctx| FieldFuture::new(async move { Ok(Some(Value::from("world"))) }),
277/// ));
278/// let schema = Schema::build(query.type_name(), None, None)
279///     .register(query)
280///     .finish()
281///     .unwrap();
282/// let _config = GraphQlConfig::new(schema);
283/// ```
284#[cfg(feature = "graphql")]
285pub use fandhe_backend_plugin_graphql as plugin_graphql;
286
287/// CORS プラグイン(`crates/plugin-cors`)の再エクスポート
288/// (`cors` feature 限定、イシュー #435)。
289///
290/// [`server::Server::cors`] へ渡す [`CorsConfig`](および
291/// `CorsConfigBuilder` 等の付随型)を、`fandhe-backend-plugin-cors` への
292/// 直接依存を追加せずに構築できるようにする唯一の目的で存在する薄い
293/// 再エクスポート(`plugin_static` と同一の whole-crate パターン)。ヘッダ
294/// 付与本体(`apply_cors_headers` 等)はコア内部の
295/// `crate::plugin::finalize_response` シームから呼ばれる実装詳細であり、
296/// 本モジュール経由での利用は想定しない。プリフライト用 `preflight_response`
297/// は利用者が `Router::options_fallback` へ直接配線する契約(`plugin-cors`
298/// crate doc 参照)も本再エクスポート経由でそのまま利用できる。
299///
300/// [`CorsConfig`]: fandhe_backend_plugin_cors::CorsConfig
301///
302/// # Examples
303///
304/// ```
305/// use fandhe_backend_core::plugin_cors::CorsConfig;
306///
307/// let config = CorsConfig::builder()
308///     .allow_origin("https://app.example.com")
309///     .build();
310/// assert!(config.is_ok());
311/// ```
312#[cfg(feature = "cors")]
313pub use fandhe_backend_plugin_cors as plugin_cors;
314
315/// 可観測性(トレーシング)プラグイン(`crates/plugin-tracing`)の再エクスポート
316/// (`tracing` feature 限定、イシュー #435)。
317///
318/// [`server::Server::tracing`] へ渡す [`TracingConfig`] を、
319/// `fandhe-backend-plugin-tracing` への直接依存を追加せずに構築できるように
320/// する唯一の目的で存在する薄い再エクスポート(`plugin_static` と同一の
321/// whole-crate パターン)。サンプリング・記録本体(`TracingLayer` 等)は
322/// コア内部の `Middleware` アダプタから呼ばれる実装詳細であり、本モジュール
323/// 経由での利用は想定しない。本再エクスポートはグローバルサブスクライバの
324/// 初期化を一切行わない設定構築のみを提供する。
325///
326/// [`TracingConfig`]: fandhe_backend_plugin_tracing::TracingConfig
327///
328/// # Examples
329///
330/// ```
331/// use fandhe_backend_core::plugin_tracing::TracingConfig;
332/// use std::num::NonZeroU64;
333///
334/// let config = TracingConfig::default();
335/// assert_eq!(config.sample_interval, NonZeroU64::new(100).unwrap());
336/// ```
337#[cfg(feature = "tracing")]
338pub use fandhe_backend_plugin_tracing as plugin_tracing;
339
340/// OpenAPI ドキュメント配信プラグイン(`crates/plugin-openapi`)の
341/// 再エクスポート(`openapi` feature 限定、イシュー #435)。
342///
343/// [`server::Server::openapi_with`] へ渡す [`OpenApiDoc`] を、
344/// `fandhe-backend-plugin-openapi` への直接依存を追加せずに構築できるように
345/// する唯一の目的で存在する薄い再エクスポート(`plugin_static` と同一の
346/// whole-crate パターン)。`GET /openapi.json` / `GET /openapi.yaml` の
347/// 配信本体はコア内部の `crate::plugin::try_intercept` シームから呼ばれる
348/// 実装詳細であり、本モジュール経由での利用は想定しない。
349///
350/// [`OpenApiDoc`]: fandhe_backend_plugin_openapi::OpenApiDoc
351/// [`server::Server::openapi_with`]: server::Server::openapi_with
352///
353/// # Examples
354///
355/// ```
356/// use fandhe_backend_core::plugin_openapi::OpenApiDoc;
357///
358/// let doc = OpenApiDoc::from_json(r#"{"openapi":"3.0.0"}"#);
359/// assert!(doc.is_ok());
360/// ```
361#[cfg(feature = "openapi")]
362pub use fandhe_backend_plugin_openapi as plugin_openapi;
363
364/// in-process WebRTC プラグイン(`crates/plugin-webrtc`)の再エクスポート
365/// (`webrtc` feature 限定、イシュー #435)。
366///
367/// [`server::Server::webrtc`] へ渡す [`WebRtcConfig`] を、
368/// `fandhe-backend-plugin-webrtc` への直接依存を追加せずに構築できるように
369/// する唯一の目的で存在する薄い再エクスポート(`plugin_static` と同一の
370/// whole-crate パターン)。シグナリング処理本体(`try_handle_rtc_offer` 等)
371/// はコア内部の `crate::plugin::try_intercept` シームから呼ばれる実装詳細
372/// であり、本モジュール経由での利用は想定しない。`webrtc-proxy` feature も
373/// 同時有効な場合は `webrtc-proxy` が優先される契約(`plugin::try_intercept`
374/// の doc)は本再エクスポートの追加では変わらない。
375///
376/// [`WebRtcConfig`]: fandhe_backend_plugin_webrtc::WebRtcConfig
377///
378/// # Examples
379///
380/// ```
381/// use fandhe_backend_core::plugin_webrtc::WebRtcConfig;
382///
383/// let config = WebRtcConfig::new();
384/// assert_eq!(config.max_offer_bytes(), 64 * 1024);
385/// ```
386#[cfg(feature = "webrtc")]
387pub use fandhe_backend_plugin_webrtc as plugin_webrtc;
388
389/// WebRTC シグナリングプロキシプラグイン(`crates/plugin-webrtc-proxy`)の
390/// 再エクスポート(`webrtc-proxy` feature 限定、イシュー #435)。
391///
392/// [`server::Server::webrtc_proxy`] へ渡す [`ProxyConfig`] を、
393/// `fandhe-backend-plugin-webrtc-proxy` への直接依存を追加せずに構築できる
394/// ようにする唯一の目的で存在する薄い再エクスポート(`plugin_static` と同一
395/// の whole-crate パターン)。転送処理本体(`try_handle_rtc_offer` /
396/// `forward_offer` 等)はコア内部の `crate::plugin::try_intercept` シームから
397/// 呼ばれる実装詳細であり、本モジュール経由での利用は想定しない。上流
398/// アドレスをリクエスト由来の値で決めない SSRF 対策(`ProxyConfig` の doc)
399/// も本再エクスポート経由でそのまま維持される。
400///
401/// [`ProxyConfig`]: fandhe_backend_plugin_webrtc_proxy::ProxyConfig
402///
403/// # Examples
404///
405/// ```
406/// use fandhe_backend_core::plugin_webrtc_proxy::ProxyConfig;
407///
408/// let config = ProxyConfig::new("127.0.0.1:9000");
409/// assert_eq!(config.upstream_addr(), "127.0.0.1:9000");
410/// ```
411#[cfg(feature = "webrtc-proxy")]
412pub use fandhe_backend_plugin_webrtc_proxy as plugin_webrtc_proxy;
413
414/// このクレートのバージョン文字列を返す。
415///
416/// TASK-1.1 時点では workspace のビルド・doc test が機能する状態を確認するための
417/// 最小公開 API として存在する。以降のタスクで実体実装(HTTP コア)に置き換わる過程でも、
418/// `cargo test` が本クレートに対して何かを検証し続けられるようにするための足場。
419///
420/// # Examples
421///
422/// ```
423/// let version = fandhe_backend_core::version();
424/// assert!(!version.is_empty());
425/// ```
426#[must_use]
427pub fn version() -> &'static str {
428    env!("CARGO_PKG_VERSION")
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn version_is_not_empty() {
437        assert!(!version().is_empty());
438    }
439}