657 lines
21 KiB
Rust
657 lines
21 KiB
Rust
//! NAC CNNL HTTP 服务
|
||
//!
|
||
//! 将 CNNL 编译器封装为 RESTful HTTP API,供立法 IDE、宪法沙箱等工具调用。
|
||
//!
|
||
//! # 接口列表
|
||
//! - POST /api/v1/compile - 编译 CNNL 源代码
|
||
//! - POST /api/v1/parse - 解析 CNNL 源代码(仅返回 AST)
|
||
//! - POST /api/v1/validate - 验证 CNNL 语法(不生成字节码)
|
||
//! - GET /api/v1/health - 健康检查
|
||
//! - GET /api/v1/version - 版本信息
|
||
|
||
use actix_web::{web, App, HttpServer, HttpResponse, Responder};
|
||
use serde::{Deserialize, Serialize};
|
||
use std::time::Instant;
|
||
use chrono::Utc;
|
||
use log::{info, warn};
|
||
|
||
// ============================================================
|
||
// 请求/响应数据结构
|
||
// ============================================================
|
||
|
||
/// 编译请求
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct CompileRequest {
|
||
/// CNNL 源代码
|
||
pub source: String,
|
||
/// 是否启用形式化验证
|
||
#[serde(default)]
|
||
pub enable_verification: bool,
|
||
/// 是否生成调试信息
|
||
#[serde(default)]
|
||
pub debug_info: bool,
|
||
/// 是否生成宪法状态文件
|
||
#[serde(default = "default_true")]
|
||
pub generate_state: bool,
|
||
}
|
||
|
||
fn default_true() -> bool {
|
||
true
|
||
}
|
||
|
||
/// 解析请求
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct ParseRequest {
|
||
/// CNNL 源代码
|
||
pub source: String,
|
||
}
|
||
|
||
/// 验证请求
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct ValidateRequest {
|
||
/// CNNL 源代码
|
||
pub source: String,
|
||
}
|
||
|
||
/// 通用 API 响应
|
||
#[derive(Debug, Serialize)]
|
||
pub struct ApiResponse<T: Serialize> {
|
||
pub success: bool,
|
||
pub data: Option<T>,
|
||
pub error: Option<String>,
|
||
pub timestamp: String,
|
||
pub duration_ms: u64,
|
||
}
|
||
|
||
impl<T: Serialize> ApiResponse<T> {
|
||
pub fn ok(data: T, duration_ms: u64) -> Self {
|
||
Self {
|
||
success: true,
|
||
data: Some(data),
|
||
error: None,
|
||
timestamp: Utc::now().to_rfc3339(),
|
||
duration_ms,
|
||
}
|
||
}
|
||
|
||
pub fn err(msg: String, duration_ms: u64) -> ApiResponse<serde_json::Value> {
|
||
ApiResponse {
|
||
success: false,
|
||
data: None,
|
||
error: Some(msg),
|
||
timestamp: Utc::now().to_rfc3339(),
|
||
duration_ms,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 编译结果
|
||
#[derive(Debug, Serialize)]
|
||
pub struct CompileResult {
|
||
/// 字节码(hex 编码)
|
||
pub bytecode_hex: String,
|
||
/// 字节码长度(字节)
|
||
pub bytecode_size: usize,
|
||
/// 宪法状态 JSON(可选)
|
||
pub state_json: Option<serde_json::Value>,
|
||
/// 条款数量
|
||
pub clause_count: usize,
|
||
/// 条款摘要
|
||
pub clauses: Vec<ClauseSummary>,
|
||
}
|
||
|
||
/// 解析结果
|
||
#[derive(Debug, Serialize)]
|
||
pub struct ParseResult {
|
||
/// 条款数量
|
||
pub clause_count: usize,
|
||
/// 测试块数量
|
||
pub test_count: usize,
|
||
/// 条款详情
|
||
pub clauses: Vec<ClauseDetail>,
|
||
}
|
||
|
||
/// 验证结果
|
||
#[derive(Debug, Serialize)]
|
||
pub struct ValidateResult {
|
||
/// 是否有效
|
||
pub valid: bool,
|
||
/// 错误列表
|
||
pub errors: Vec<String>,
|
||
/// 警告列表
|
||
pub warnings: Vec<String>,
|
||
/// 条款数量
|
||
pub clause_count: usize,
|
||
}
|
||
|
||
/// 条款摘要
|
||
#[derive(Debug, Serialize)]
|
||
pub struct ClauseSummary {
|
||
pub id: String,
|
||
pub level: String,
|
||
pub title: String,
|
||
pub parameter_count: usize,
|
||
pub predicate_count: usize,
|
||
pub obligation_count: usize,
|
||
}
|
||
|
||
/// 条款详情
|
||
#[derive(Debug, Serialize)]
|
||
pub struct ClauseDetail {
|
||
pub id: String,
|
||
pub level: String,
|
||
pub title: String,
|
||
pub name: Option<String>,
|
||
pub version: Option<String>,
|
||
pub description: Option<String>,
|
||
pub depends_on: Vec<String>,
|
||
pub parameters: Vec<ParameterDetail>,
|
||
pub predicates: Vec<PredicateDetail>,
|
||
pub obligations: Vec<ObligationDetail>,
|
||
}
|
||
|
||
/// 参数详情
|
||
#[derive(Debug, Serialize)]
|
||
pub struct ParameterDetail {
|
||
pub name: String,
|
||
pub type_name: String,
|
||
pub value: String,
|
||
pub description: Option<String>,
|
||
}
|
||
|
||
/// 谓词详情
|
||
#[derive(Debug, Serialize)]
|
||
pub struct PredicateDetail {
|
||
pub name: String,
|
||
pub params: Vec<(String, String)>,
|
||
pub return_type: String,
|
||
}
|
||
|
||
/// 义务详情
|
||
#[derive(Debug, Serialize)]
|
||
pub struct ObligationDetail {
|
||
pub name: String,
|
||
pub frequency: String,
|
||
pub enforcer: String,
|
||
pub penalty: String,
|
||
pub description: Option<String>,
|
||
}
|
||
|
||
/// 版本信息
|
||
#[derive(Debug, Serialize)]
|
||
pub struct VersionInfo {
|
||
pub service: String,
|
||
pub version: String,
|
||
pub compiler_version: String,
|
||
pub build_time: String,
|
||
}
|
||
|
||
/// 健康状态
|
||
#[derive(Debug, Serialize)]
|
||
pub struct HealthStatus {
|
||
pub status: String,
|
||
pub uptime_seconds: u64,
|
||
}
|
||
|
||
// ============================================================
|
||
// 全局状态
|
||
// ============================================================
|
||
|
||
pub struct AppState {
|
||
pub start_time: Instant,
|
||
}
|
||
|
||
// ============================================================
|
||
// API 处理函数
|
||
// ============================================================
|
||
|
||
/// POST /api/v1/compile - 编译 CNNL 源代码
|
||
async fn handle_compile(
|
||
_state: web::Data<AppState>,
|
||
req: web::Json<CompileRequest>,
|
||
) -> impl Responder {
|
||
let start = Instant::now();
|
||
info!("收到编译请求,源代码长度: {} 字节", req.source.len());
|
||
|
||
let options = cnnl_compiler::CompilerOptions {
|
||
enable_verification: req.enable_verification,
|
||
debug_info: req.debug_info,
|
||
output_dir: None,
|
||
generate_state_file: req.generate_state,
|
||
};
|
||
|
||
match cnnl_compiler::compile(&req.source, options) {
|
||
Ok(result) => {
|
||
let bytecode_hex = hex::encode(&result.bytecode);
|
||
let state_json = result.state_json.as_ref().and_then(|s| {
|
||
serde_json::from_str(s).ok()
|
||
});
|
||
let clauses: Vec<ClauseSummary> = result.ast.clauses.iter().map(|c| ClauseSummary {
|
||
id: c.id.clone(),
|
||
level: c.level.to_string(),
|
||
title: c.title.clone(),
|
||
parameter_count: c.parameters.len(),
|
||
predicate_count: c.predicates.len(),
|
||
obligation_count: c.obligations.len(),
|
||
}).collect();
|
||
let clause_count = clauses.len();
|
||
let compile_result = CompileResult {
|
||
bytecode_hex,
|
||
bytecode_size: result.bytecode.len(),
|
||
state_json,
|
||
clause_count,
|
||
clauses,
|
||
};
|
||
let duration_ms = start.elapsed().as_millis() as u64;
|
||
info!("编译成功,字节码大小: {} 字节,耗时: {}ms", compile_result.bytecode_size, duration_ms);
|
||
HttpResponse::Ok().json(ApiResponse::ok(compile_result, duration_ms))
|
||
}
|
||
Err(e) => {
|
||
let duration_ms = start.elapsed().as_millis() as u64;
|
||
warn!("编译失败: {}", e);
|
||
HttpResponse::BadRequest().json(ApiResponse::<serde_json::Value>::err(
|
||
format!("{}", e),
|
||
duration_ms,
|
||
))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// POST /api/v1/parse - 解析 CNNL 源代码
|
||
async fn handle_parse(
|
||
_state: web::Data<AppState>,
|
||
req: web::Json<ParseRequest>,
|
||
) -> impl Responder {
|
||
let start = Instant::now();
|
||
info!("收到解析请求,源代码长度: {} 字节", req.source.len());
|
||
|
||
match cnnl_compiler::Parser::new(&req.source) {
|
||
Ok(mut parser) => {
|
||
match parser.parse() {
|
||
Ok(ast) => {
|
||
let clauses: Vec<ClauseDetail> = ast.clauses.iter().map(|c| ClauseDetail {
|
||
id: c.id.clone(),
|
||
level: c.level.to_string(),
|
||
title: c.title.clone(),
|
||
name: c.name.clone(),
|
||
version: c.version.clone(),
|
||
description: c.description.clone(),
|
||
depends_on: c.depends_on.clone(),
|
||
parameters: c.parameters.iter().map(|p| ParameterDetail {
|
||
name: p.name.clone(),
|
||
type_name: p.ty.to_string(),
|
||
value: format!("{}", p.value),
|
||
description: p.description.clone(),
|
||
}).collect(),
|
||
predicates: c.predicates.iter().map(|pred| PredicateDetail {
|
||
name: pred.name.clone(),
|
||
params: pred.params.iter().map(|(n, t)| (n.clone(), t.to_string())).collect(),
|
||
return_type: pred.return_type.to_string(),
|
||
}).collect(),
|
||
obligations: c.obligations.iter().map(|o| ObligationDetail {
|
||
name: o.name.clone(),
|
||
frequency: format!("{:?}", o.frequency),
|
||
enforcer: o.enforcer.clone(),
|
||
penalty: o.penalty.clone(),
|
||
description: o.description.clone(),
|
||
}).collect(),
|
||
}).collect();
|
||
let test_count = ast.tests.len();
|
||
let clause_count = clauses.len();
|
||
let parse_result = ParseResult {
|
||
clause_count,
|
||
test_count,
|
||
clauses,
|
||
};
|
||
let duration_ms = start.elapsed().as_millis() as u64;
|
||
info!("解析成功,条款数: {},测试块数: {},耗时: {}ms", clause_count, test_count, duration_ms);
|
||
HttpResponse::Ok().json(ApiResponse::ok(parse_result, duration_ms))
|
||
}
|
||
Err(e) => {
|
||
let duration_ms = start.elapsed().as_millis() as u64;
|
||
warn!("解析失败: {}", e);
|
||
HttpResponse::BadRequest().json(ApiResponse::<serde_json::Value>::err(
|
||
format!("{}", e),
|
||
duration_ms,
|
||
))
|
||
}
|
||
}
|
||
}
|
||
Err(e) => {
|
||
let duration_ms = start.elapsed().as_millis() as u64;
|
||
warn!("词法分析失败: {}", e);
|
||
HttpResponse::BadRequest().json(ApiResponse::<serde_json::Value>::err(
|
||
format!("{}", e),
|
||
duration_ms,
|
||
))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// POST /api/v1/validate - 验证 CNNL 语法
|
||
async fn handle_validate(
|
||
_state: web::Data<AppState>,
|
||
req: web::Json<ValidateRequest>,
|
||
) -> impl Responder {
|
||
let start = Instant::now();
|
||
info!("收到验证请求,源代码长度: {} 字节", req.source.len());
|
||
|
||
let mut errors = Vec::new();
|
||
let warnings = Vec::new();
|
||
let mut clause_count = 0;
|
||
|
||
match cnnl_compiler::Parser::new(&req.source) {
|
||
Ok(mut parser) => {
|
||
match parser.parse() {
|
||
Ok(ast) => {
|
||
clause_count = ast.clauses.len();
|
||
// 语义分析
|
||
let mut analyzer = cnnl_compiler::semantic::SemanticAnalyzer::new();
|
||
if let Err(e) = analyzer.analyze(&ast) {
|
||
errors.push(format!("语义错误: {}", e));
|
||
}
|
||
}
|
||
Err(e) => {
|
||
errors.push(format!("语法错误: {}", e));
|
||
}
|
||
}
|
||
}
|
||
Err(e) => {
|
||
errors.push(format!("词法错误: {}", e));
|
||
}
|
||
}
|
||
|
||
let valid = errors.is_empty();
|
||
let validate_result = ValidateResult {
|
||
valid,
|
||
errors,
|
||
warnings,
|
||
clause_count,
|
||
};
|
||
let duration_ms = start.elapsed().as_millis() as u64;
|
||
info!("验证完成,有效: {},耗时: {}ms", valid, duration_ms);
|
||
HttpResponse::Ok().json(ApiResponse::ok(validate_result, duration_ms))
|
||
}
|
||
|
||
/// GET /api/v1/health - 健康检查
|
||
async fn handle_health(state: web::Data<AppState>) -> impl Responder {
|
||
let uptime_seconds = state.start_time.elapsed().as_secs();
|
||
let health = HealthStatus {
|
||
status: "ok".to_string(),
|
||
uptime_seconds,
|
||
};
|
||
HttpResponse::Ok().json(ApiResponse::ok(health, 0))
|
||
}
|
||
|
||
/// GET /api/v1/version - 版本信息
|
||
async fn handle_version(_state: web::Data<AppState>) -> impl Responder {
|
||
let version = VersionInfo {
|
||
service: "nac-cnnl-service".to_string(),
|
||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||
compiler_version: "0.1.0".to_string(),
|
||
build_time: env!("CARGO_PKG_VERSION").to_string(),
|
||
};
|
||
HttpResponse::Ok().json(ApiResponse::ok(version, 0))
|
||
}
|
||
|
||
|
||
// ============================================================
|
||
// 根路径 - API 文档页面
|
||
// ============================================================
|
||
async fn handle_root() -> impl Responder {
|
||
let html = r#"<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>NAC CNNL 编译服务</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
background: #0a0e1a;
|
||
color: #c9d1d9;
|
||
min-height: 100vh;
|
||
padding: 40px 20px;
|
||
}
|
||
.container { max-width: 900px; margin: 0 auto; }
|
||
.header {
|
||
text-align: center;
|
||
padding: 60px 0 40px;
|
||
border-bottom: 1px solid #21262d;
|
||
margin-bottom: 40px;
|
||
}
|
||
.logo {
|
||
font-size: 48px;
|
||
font-weight: 700;
|
||
background: linear-gradient(135deg, #58a6ff, #3fb950, #f78166);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
letter-spacing: -1px;
|
||
}
|
||
.subtitle {
|
||
color: #8b949e;
|
||
font-size: 16px;
|
||
margin-top: 12px;
|
||
}
|
||
.badge {
|
||
display: inline-block;
|
||
background: #1f6feb;
|
||
color: #fff;
|
||
font-size: 12px;
|
||
padding: 3px 10px;
|
||
border-radius: 20px;
|
||
margin-top: 10px;
|
||
font-weight: 500;
|
||
}
|
||
.section { margin-bottom: 40px; }
|
||
h2 {
|
||
font-size: 20px;
|
||
color: #e6edf3;
|
||
margin-bottom: 16px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #21262d;
|
||
}
|
||
.endpoint-grid { display: grid; gap: 12px; }
|
||
.endpoint {
|
||
background: #161b22;
|
||
border: 1px solid #30363d;
|
||
border-radius: 8px;
|
||
padding: 16px 20px;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
}
|
||
.method {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
padding: 4px 10px;
|
||
border-radius: 4px;
|
||
min-width: 52px;
|
||
text-align: center;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
.method.get { background: #1a7f37; color: #3fb950; border: 1px solid #238636; }
|
||
.method.post { background: #1a4a7f; color: #58a6ff; border: 1px solid #1f6feb; }
|
||
.endpoint-info { flex: 1; }
|
||
.path {
|
||
font-family: "SFMono-Regular", Consolas, monospace;
|
||
font-size: 14px;
|
||
color: #e6edf3;
|
||
font-weight: 600;
|
||
}
|
||
.desc { color: #8b949e; font-size: 13px; margin-top: 4px; }
|
||
.code-block {
|
||
background: #161b22;
|
||
border: 1px solid #30363d;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
font-family: "SFMono-Regular", Consolas, monospace;
|
||
font-size: 13px;
|
||
overflow-x: auto;
|
||
line-height: 1.6;
|
||
}
|
||
.code-block .key { color: #79c0ff; }
|
||
.code-block .str { color: #a5d6ff; }
|
||
.code-block .num { color: #f2cc60; }
|
||
.code-block .bool { color: #ff7b72; }
|
||
.info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
.info-card {
|
||
background: #161b22;
|
||
border: 1px solid #30363d;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
}
|
||
.info-label { color: #8b949e; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.info-value { color: #e6edf3; font-size: 15px; font-weight: 600; margin-top: 4px; }
|
||
.footer {
|
||
text-align: center;
|
||
color: #484f58;
|
||
font-size: 13px;
|
||
padding-top: 40px;
|
||
border-top: 1px solid #21262d;
|
||
margin-top: 40px;
|
||
}
|
||
a { color: #58a6ff; text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<div class="logo">NAC CNNL</div>
|
||
<div class="subtitle">宪政神经网络语言编译服务</div>
|
||
<div class="badge">v0.1.0 · 运行中</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>API 端点</h2>
|
||
<div class="endpoint-grid">
|
||
<div class="endpoint">
|
||
<span class="method post">POST</span>
|
||
<div class="endpoint-info">
|
||
<div class="path">/api/v1/compile</div>
|
||
<div class="desc">编译 CNNL 源代码,生成 NVM 字节码和宪法状态文件</div>
|
||
</div>
|
||
</div>
|
||
<div class="endpoint">
|
||
<span class="method post">POST</span>
|
||
<div class="endpoint-info">
|
||
<div class="path">/api/v1/parse</div>
|
||
<div class="desc">解析 CNNL 源代码,返回抽象语法树(AST)</div>
|
||
</div>
|
||
</div>
|
||
<div class="endpoint">
|
||
<span class="method post">POST</span>
|
||
<div class="endpoint-info">
|
||
<div class="path">/api/v1/validate</div>
|
||
<div class="desc">验证 CNNL 语法正确性,不生成字节码</div>
|
||
</div>
|
||
</div>
|
||
<div class="endpoint">
|
||
<span class="method get">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="path">/api/v1/health</div>
|
||
<div class="desc">服务健康检查,返回运行状态和运行时长</div>
|
||
</div>
|
||
</div>
|
||
<div class="endpoint">
|
||
<span class="method get">GET</span>
|
||
<div class="endpoint-info">
|
||
<div class="path">/api/v1/version</div>
|
||
<div class="desc">服务版本信息</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>快速示例</h2>
|
||
<div class="code-block">
|
||
<span style="color:#8b949e"># 编译一个 CNNL 条款</span>
|
||
curl -X POST https://cnnl.newassetchain.io/api/v1/compile \
|
||
-H <span class="str">"Content-Type: application/json"</span> \
|
||
-d <span class="str">'{
|
||
"source": "clause XTZH_GOLD_COVERAGE {\n level: eternal\n title: \"黄金储备覆盖率底线\"\n parameter XTZH_GOLD_COVERAGE_MIN: f64 = 1.25\n}",
|
||
"generate_state": true
|
||
}'</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>服务信息</h2>
|
||
<div class="info-grid">
|
||
<div class="info-card">
|
||
<div class="info-label">服务名称</div>
|
||
<div class="info-value">nac-cnnl-service</div>
|
||
</div>
|
||
<div class="info-card">
|
||
<div class="info-label">编译器版本</div>
|
||
<div class="info-value">CNNL v0.1.0</div>
|
||
</div>
|
||
<div class="info-card">
|
||
<div class="info-label">目标虚拟机</div>
|
||
<div class="info-value">NVM (NAC VM)</div>
|
||
</div>
|
||
<div class="info-card">
|
||
<div class="info-label">协议</div>
|
||
<div class="info-value">HTTPS / TLS 1.3</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<p>NAC NewAssetChain · CNNL 宪政神经网络语言 · <a href="https://git.newassetchain.io/nacadmin/NAC_Blockchain">源代码仓库</a></p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>"#;
|
||
HttpResponse::Ok()
|
||
.content_type("text/html; charset=utf-8")
|
||
.body(html)
|
||
}
|
||
|
||
// ============================================================
|
||
// 主函数
|
||
// ============================================================
|
||
|
||
#[actix_web::main]
|
||
async fn main() -> std::io::Result<()> {
|
||
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
|
||
|
||
let host = std::env::var("CNNL_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
|
||
let port: u16 = std::env::var("CNNL_PORT")
|
||
.unwrap_or_else(|_| "8080".to_string())
|
||
.parse()
|
||
.unwrap_or(8080);
|
||
|
||
info!("NAC CNNL HTTP 服务启动中...");
|
||
info!("监听地址: {}:{}", host, port);
|
||
|
||
let app_state = web::Data::new(AppState {
|
||
start_time: Instant::now(),
|
||
});
|
||
|
||
HttpServer::new(move || {
|
||
App::new()
|
||
.app_data(app_state.clone())
|
||
.app_data(web::JsonConfig::default().limit(1024 * 1024)) // 1MB 请求体限制
|
||
// API 路由
|
||
.route("/", web::get().to(handle_root))
|
||
.route("/api/v1/compile", web::post().to(handle_compile))
|
||
.route("/api/v1/parse", web::post().to(handle_parse))
|
||
.route("/api/v1/validate", web::post().to(handle_validate))
|
||
.route("/api/v1/health", web::get().to(handle_health))
|
||
.route("/api/v1/version", web::get().to(handle_version))
|
||
})
|
||
.bind(format!("{}:{}", host, port))?
|
||
.run()
|
||
.await
|
||
}
|