1use crate::config::{AuthMethod, CacheBackend, ProjectConfig};
7
8pub fn cargo_toml(config: &ProjectConfig) -> String {
10 let gateway = config.gateway.as_ref().unwrap();
11 let cache_deps = match gateway.cache.backend {
12 CacheBackend::Redis => r#"redis = { version = "0.27", features = ["tokio-comp"] }"#,
13 CacheBackend::Memory => r#"moka = { version = "0.12", features = ["future"] }"#,
14 CacheBackend::None => "",
15 };
16
17 format!(
18 r#"[package]
19name = "{name}"
20version = "0.1.0"
21edition = "2024"
22rust-version = "1.86"
23description = "{display_name}"
24
25[dependencies]
26# AllFrame
27allframe-core = {{ version = "0.1", features = ["resilience", "security", "otel"] }}
28
29# gRPC
30tonic = "0.12"
31prost = "0.13"
32
33# Async
34tokio = {{ version = "1", features = ["full"] }}
35async-trait = "0.1"
36
37# HTTP Client
38reqwest = {{ version = "0.12", features = ["json", "rustls-tls"] }}
39
40# Caching
41{cache_deps}
42
43# Crypto (for API authentication)
44hmac = "0.12"
45sha2 = "0.10"
46base64 = "0.22"
47hex = "0.4"
48
49# Data
50rust_decimal = {{ version = "1.36", features = ["serde"] }}
51serde = {{ version = "1.0", features = ["derive"] }}
52serde_json = "1.0"
53
54# Errors
55thiserror = "2.0"
56anyhow = "1.0"
57
58# Config
59dotenvy = "0.15"
60
61# Tracing
62tracing = "0.1"
63tracing-subscriber = {{ version = "0.3", features = ["env-filter"] }}
64
65# Metrics
66opentelemetry = {{ version = "0.27", features = ["metrics"] }}
67opentelemetry-otlp = "0.27"
68
69[dev-dependencies]
70mockall = "0.13"
71tokio-test = "0.4"
72
73[build-dependencies]
74tonic-build = "0.12"
75
76[[bin]]
77name = "{name}"
78path = "src/main.rs"
79"#,
80 name = config.name,
81 display_name = gateway.display_name,
82 cache_deps = cache_deps,
83 )
84}
85
86pub fn build_rs(config: &ProjectConfig) -> String {
88 let gateway = config.gateway.as_ref().unwrap();
89 format!(
90 r#"fn main() -> Result<(), Box<dyn std::error::Error>> {{
91 tonic_build::configure()
92 .build_server(true)
93 .build_client(false)
94 .compile_protos(&["proto/{service_name}.proto"], &["proto/"])?;
95 Ok(())
96}}
97"#,
98 service_name = gateway.service_name
99 )
100}
101
102pub fn proto_file(config: &ProjectConfig) -> String {
104 let gateway = config.gateway.as_ref().unwrap();
105 let service_name = &gateway.service_name;
106 let pascal_name = to_pascal_case(service_name);
107
108 format!(
109 r#"syntax = "proto3";
110package {service_name};
111
112// Authentication credentials
113message Credentials {{
114 string api_key = 1;
115 string api_secret = 2;
116}}
117
118// ============ PUBLIC ENDPOINTS ============
119
120message GetServerTimeRequest {{}}
121message GetServerTimeResponse {{
122 int64 server_time = 1;
123}}
124
125message GetAssetsRequest {{}}
126message GetAssetsResponse {{
127 map<string, AssetInfo> assets = 1;
128}}
129
130message AssetInfo {{
131 string symbol = 1;
132 string name = 2;
133 int32 decimals = 3;
134}}
135
136message GetTickerRequest {{
137 repeated string pairs = 1;
138}}
139message GetTickerResponse {{
140 map<string, TickerInfo> tickers = 1;
141}}
142
143message TickerInfo {{
144 string pair = 1;
145 string last_price = 2;
146 string bid = 3;
147 string ask = 4;
148 string volume_24h = 5;
149}}
150
151// ============ PRIVATE ENDPOINTS ============
152
153message GetAccountBalanceRequest {{
154 Credentials credentials = 1;
155}}
156message GetAccountBalanceResponse {{
157 map<string, string> balances = 1;
158}}
159
160message GetTradesHistoryRequest {{
161 Credentials credentials = 1;
162 optional string start_time = 2;
163 optional string end_time = 3;
164 optional int32 limit = 4;
165}}
166message GetTradesHistoryResponse {{
167 repeated TradeInfo trades = 1;
168}}
169
170message TradeInfo {{
171 string id = 1;
172 string pair = 2;
173 string side = 3;
174 string price = 4;
175 string volume = 5;
176 string fee = 6;
177 int64 timestamp = 7;
178}}
179
180// ============ ORDER MANAGEMENT ============
181
182message AddOrderRequest {{
183 Credentials credentials = 1;
184 string pair = 2;
185 string side = 3;
186 string order_type = 4;
187 string volume = 5;
188 optional string price = 6;
189}}
190message AddOrderResponse {{
191 string order_id = 1;
192 string status = 2;
193}}
194
195message CancelOrderRequest {{
196 Credentials credentials = 1;
197 string order_id = 2;
198}}
199message CancelOrderResponse {{
200 bool success = 1;
201}}
202
203// ============ HEALTH ============
204
205message HealthCheckRequest {{}}
206message HealthCheckResponse {{
207 bool healthy = 1;
208 string status = 2;
209}}
210
211// ============ SERVICE DEFINITION ============
212
213service {pascal_name}Service {{
214 // Public
215 rpc GetServerTime(GetServerTimeRequest) returns (GetServerTimeResponse);
216 rpc GetAssets(GetAssetsRequest) returns (GetAssetsResponse);
217 rpc GetTicker(GetTickerRequest) returns (GetTickerResponse);
218
219 // Private
220 rpc GetAccountBalance(GetAccountBalanceRequest) returns (GetAccountBalanceResponse);
221 rpc GetTradesHistory(GetTradesHistoryRequest) returns (GetTradesHistoryResponse);
222
223 // Orders
224 rpc AddOrder(AddOrderRequest) returns (AddOrderResponse);
225 rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse);
226
227 // Health
228 rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
229}}
230"#,
231 service_name = service_name,
232 pascal_name = pascal_name,
233 )
234}
235
236pub fn main_rs(config: &ProjectConfig) -> String {
238 let gateway = config.gateway.as_ref().unwrap();
239 let service_name = &gateway.service_name;
240 let pascal_name = to_pascal_case(service_name);
241
242 format!(
243 r#"//! {display_name}
244//!
245//! A gRPC gateway service wrapping the {display_name} API with
246//! built-in resilience, caching, and observability.
247
248use std::sync::Arc;
249use tonic::transport::Server;
250use tracing::info;
251
252mod config;
253mod error;
254mod domain;
255mod application;
256mod infrastructure;
257mod presentation;
258
259pub mod generated {{
260 tonic::include_proto!("{service_name}");
261}}
262
263use config::Config;
264use application::{pascal_name}Service;
265use infrastructure::{{
266 {pascal_name}Client,
267 GatewayRateLimiter,
268 GatewayMetrics,
269}};
270use presentation::{pascal_name}GrpcService;
271use generated::{service_name}_service_server::{pascal_name}ServiceServer;
272
273#[tokio::main]
274async fn main() -> anyhow::Result<()> {{
275 // Load environment variables
276 dotenvy::dotenv().ok();
277
278 // Initialize tracing
279 tracing_subscriber::fmt()
280 .with_env_filter(
281 tracing_subscriber::EnvFilter::from_default_env()
282 .add_directive(tracing::Level::INFO.into()),
283 )
284 .init();
285
286 // Load configuration
287 let config = Config::from_env();
288 info!("Starting {display_name} on port {{}}", config.server.grpc_port);
289
290 // Initialize metrics
291 let _metrics = Arc::new(GatewayMetrics::new());
292
293 // Initialize rate limiter
294 let _rate_limiter = Arc::new(GatewayRateLimiter::new(
295 config.rate_limit.public_rps,
296 config.rate_limit.private_rps,
297 config.rate_limit.burst,
298 ));
299
300 // Create HTTP client
301 let client = Arc::new({pascal_name}Client::new(
302 &config.{service_name}.base_url,
303 config.{service_name}.timeout,
304 ));
305
306 // Create service
307 let service = Arc::new({pascal_name}Service::new(client));
308
309 // Create gRPC service
310 let grpc_service = {pascal_name}GrpcService::new(service);
311
312 // Start gRPC server
313 let addr = format!("0.0.0.0:{{}}", config.server.grpc_port).parse()?;
314
315 info!("gRPC server listening on {{}}", addr);
316
317 Server::builder()
318 .add_service({pascal_name}ServiceServer::new(grpc_service))
319 .serve_with_shutdown(addr, shutdown_signal())
320 .await?;
321
322 info!("Server shutdown complete");
323 Ok(())
324}}
325
326async fn shutdown_signal() {{
327 tokio::signal::ctrl_c()
328 .await
329 .expect("Failed to listen for ctrl+c");
330 info!("Shutdown signal received");
331}}
332"#,
333 display_name = gateway.display_name,
334 service_name = service_name,
335 pascal_name = pascal_name,
336 )
337}
338
339pub fn lib_rs() -> String {
341 r#"//! Gateway service library
342//!
343//! This module exports all the components of the gateway service.
344
345pub mod config;
346pub mod error;
347pub mod domain;
348pub mod application;
349pub mod infrastructure;
350pub mod presentation;
351"#
352 .to_string()
353}
354
355pub fn config_rs(config: &ProjectConfig) -> String {
357 let gateway = config.gateway.as_ref().unwrap();
358 let service_name = &gateway.service_name;
359 let upper_name = service_name.to_uppercase();
360
361 format!(
362 r#"//! Configuration module
363//!
364//! Loads configuration from environment variables.
365
366use std::time::Duration;
367
368#[derive(Debug, Clone)]
369pub struct Config {{
370 pub server: ServerConfig,
371 pub {service_name}: {pascal_name}Config,
372 pub rate_limit: RateLimitConfig,
373 pub cache: CacheConfig,
374}}
375
376#[derive(Debug, Clone)]
377pub struct ServerConfig {{
378 pub grpc_port: u16,
379 pub health_port: u16,
380 pub metrics_port: u16,
381}}
382
383#[derive(Debug, Clone)]
384pub struct {pascal_name}Config {{
385 pub base_url: String,
386 pub timeout: Duration,
387}}
388
389#[derive(Debug, Clone)]
390pub struct RateLimitConfig {{
391 pub public_rps: u32,
392 pub private_rps: u32,
393 pub burst: u32,
394}}
395
396#[derive(Debug, Clone)]
397pub struct CacheConfig {{
398 pub enabled: bool,
399 pub public_ttl: Duration,
400 pub private_ttl: Duration,
401}}
402
403impl Config {{
404 pub fn from_env() -> Self {{
405 Self {{
406 server: ServerConfig {{
407 grpc_port: std::env::var("{upper_name}_GATEWAY_PORT")
408 .ok()
409 .and_then(|s| s.parse().ok())
410 .unwrap_or({grpc_port}),
411 health_port: std::env::var("{upper_name}_HEALTH_PORT")
412 .ok()
413 .and_then(|s| s.parse().ok())
414 .unwrap_or({health_port}),
415 metrics_port: std::env::var("{upper_name}_METRICS_PORT")
416 .ok()
417 .and_then(|s| s.parse().ok())
418 .unwrap_or({metrics_port}),
419 }},
420 {service_name}: {pascal_name}Config {{
421 base_url: std::env::var("{upper_name}_API_URL")
422 .unwrap_or_else(|_| "{api_base_url}".to_string()),
423 timeout: Duration::from_secs(
424 std::env::var("{upper_name}_API_TIMEOUT_SECONDS")
425 .ok()
426 .and_then(|s| s.parse().ok())
427 .unwrap_or(30),
428 ),
429 }},
430 rate_limit: RateLimitConfig {{
431 public_rps: std::env::var("{upper_name}_RATE_LIMIT_PUBLIC_RPS")
432 .ok()
433 .and_then(|s| s.parse().ok())
434 .unwrap_or({public_rps}),
435 private_rps: std::env::var("{upper_name}_RATE_LIMIT_PRIVATE_RPS")
436 .ok()
437 .and_then(|s| s.parse().ok())
438 .unwrap_or({private_rps}),
439 burst: std::env::var("{upper_name}_RATE_LIMIT_BURST")
440 .ok()
441 .and_then(|s| s.parse().ok())
442 .unwrap_or({burst}),
443 }},
444 cache: CacheConfig {{
445 enabled: std::env::var("CACHE_ENABLED")
446 .map(|s| s.to_lowercase() == "true")
447 .unwrap_or(true),
448 public_ttl: Duration::from_secs(
449 std::env::var("CACHE_PUBLIC_TTL_SECONDS")
450 .ok()
451 .and_then(|s| s.parse().ok())
452 .unwrap_or({public_ttl}),
453 ),
454 private_ttl: Duration::from_secs(
455 std::env::var("CACHE_PRIVATE_TTL_SECONDS")
456 .ok()
457 .and_then(|s| s.parse().ok())
458 .unwrap_or({private_ttl}),
459 ),
460 }},
461 }}
462 }}
463}}
464"#,
465 service_name = service_name,
466 pascal_name = to_pascal_case(service_name),
467 upper_name = upper_name,
468 grpc_port = gateway.server.grpc_port,
469 health_port = gateway.server.health_port,
470 metrics_port = gateway.server.metrics_port,
471 api_base_url = gateway.api_base_url,
472 public_rps = gateway.rate_limit.public_rps,
473 private_rps = gateway.rate_limit.private_rps,
474 burst = gateway.rate_limit.burst,
475 public_ttl = gateway.cache.public_ttl_secs,
476 private_ttl = gateway.cache.private_ttl_secs,
477 )
478}
479
480pub fn error_rs(config: &ProjectConfig) -> String {
482 let gateway = config.gateway.as_ref().unwrap();
483 let pascal_name = to_pascal_case(&gateway.service_name);
484
485 format!(
486 r#"//! Error types for the gateway service
487
488use thiserror::Error;
489use tonic::Status;
490
491#[derive(Debug, Error)]
492pub enum {pascal_name}Error {{
493 #[error("HTTP request failed: {{0}}")]
494 HttpError(String),
495
496 #[error("Missing credentials")]
497 MissingCredentials,
498
499 #[error("Invalid credentials")]
500 InvalidCredentials,
501
502 #[error("Rate limit exceeded")]
503 RateLimitExceeded,
504
505 #[error("{pascal_name} API error: {{0}}")]
506 ApiError(String),
507
508 #[error("Invalid request: {{0}}")]
509 InvalidRequest(String),
510
511 #[error("Asset not found: {{0}}")]
512 AssetNotFound(String),
513
514 #[error("Insufficient balance")]
515 InsufficientBalance,
516
517 #[error("Service unavailable")]
518 ServiceUnavailable,
519
520 #[error("Internal error: {{0}}")]
521 Internal(String),
522}}
523
524impl From<{pascal_name}Error> for Status {{
525 fn from(err: {pascal_name}Error) -> Self {{
526 match err {{
527 {pascal_name}Error::HttpError(msg) => Status::internal(msg),
528 {pascal_name}Error::MissingCredentials => Status::unauthenticated("Missing credentials"),
529 {pascal_name}Error::InvalidCredentials => Status::unauthenticated("Invalid credentials"),
530 {pascal_name}Error::RateLimitExceeded => Status::resource_exhausted("Rate limit exceeded"),
531 {pascal_name}Error::ApiError(msg) => Status::internal(msg),
532 {pascal_name}Error::InvalidRequest(msg) => Status::invalid_argument(msg),
533 {pascal_name}Error::AssetNotFound(asset) => Status::not_found(format!("Asset not found: {{}}", asset)),
534 {pascal_name}Error::InsufficientBalance => Status::failed_precondition("Insufficient balance"),
535 {pascal_name}Error::ServiceUnavailable => Status::unavailable("Service unavailable"),
536 {pascal_name}Error::Internal(msg) => Status::internal(msg),
537 }}
538 }}
539}}
540
541pub type Result<T> = std::result::Result<T, {pascal_name}Error>;
542"#,
543 pascal_name = pascal_name,
544 )
545}
546
547pub fn domain_mod(_config: &ProjectConfig) -> String {
549 r#"//! Domain layer - Business entities and repository traits
550
551pub mod entities;
552pub mod repository;
553
554pub use entities::*;
555pub use repository::*;
556"#
557 .to_string()
558}
559
560pub fn domain_entities(_config: &ProjectConfig) -> String {
562 r#"//! Domain entities
563
564use rust_decimal::Decimal;
565use serde::{Deserialize, Serialize};
566
567/// Asset information
568#[derive(Debug, Clone, Serialize, Deserialize)]
569pub struct AssetInfo {
570 pub symbol: String,
571 pub name: String,
572 pub decimals: i32,
573}
574
575/// Ticker information for a trading pair
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct TickerInfo {
578 pub pair: String,
579 pub last_price: Decimal,
580 pub bid: Decimal,
581 pub ask: Decimal,
582 pub volume_24h: Decimal,
583}
584
585/// Account balance for an asset
586#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct Balance {
588 pub asset: String,
589 pub free: Decimal,
590 pub locked: Decimal,
591}
592
593impl Balance {
594 pub fn total(&self) -> Decimal {
595 self.free + self.locked
596 }
597}
598
599/// Trade history entry
600#[derive(Debug, Clone, Serialize, Deserialize)]
601pub struct TradeInfo {
602 pub id: String,
603 pub pair: String,
604 pub side: OrderSide,
605 pub price: Decimal,
606 pub volume: Decimal,
607 pub fee: Decimal,
608 pub timestamp: i64,
609}
610
611/// Order information
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct OrderInfo {
614 pub id: String,
615 pub pair: String,
616 pub side: OrderSide,
617 pub order_type: OrderType,
618 pub price: Option<Decimal>,
619 pub volume: Decimal,
620 pub status: OrderStatus,
621}
622
623/// Order side (buy or sell)
624#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
625#[serde(rename_all = "lowercase")]
626pub enum OrderSide {
627 Buy,
628 Sell,
629}
630
631impl std::fmt::Display for OrderSide {
632 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
633 match self {
634 Self::Buy => write!(f, "buy"),
635 Self::Sell => write!(f, "sell"),
636 }
637 }
638}
639
640impl std::str::FromStr for OrderSide {
641 type Err = String;
642
643 fn from_str(s: &str) -> Result<Self, Self::Err> {
644 match s.to_lowercase().as_str() {
645 "buy" => Ok(Self::Buy),
646 "sell" => Ok(Self::Sell),
647 _ => Err(format!("Invalid order side: {}", s)),
648 }
649 }
650}
651
652/// Order type
653#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
654#[serde(rename_all = "lowercase")]
655pub enum OrderType {
656 Market,
657 Limit,
658}
659
660impl std::fmt::Display for OrderType {
661 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
662 match self {
663 Self::Market => write!(f, "market"),
664 Self::Limit => write!(f, "limit"),
665 }
666 }
667}
668
669impl std::str::FromStr for OrderType {
670 type Err = String;
671
672 fn from_str(s: &str) -> Result<Self, Self::Err> {
673 match s.to_lowercase().as_str() {
674 "market" => Ok(Self::Market),
675 "limit" => Ok(Self::Limit),
676 _ => Err(format!("Invalid order type: {}", s)),
677 }
678 }
679}
680
681/// Order status
682#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
683#[serde(rename_all = "lowercase")]
684pub enum OrderStatus {
685 Open,
686 Filled,
687 Cancelled,
688 PartiallyFilled,
689}
690
691/// Credentials for authenticated requests
692#[derive(Debug, Clone)]
693pub struct Credentials {
694 pub api_key: String,
695 pub api_secret: String,
696}
697"#
698 .to_string()
699}
700
701pub fn domain_repository(config: &ProjectConfig) -> String {
703 let gateway = config.gateway.as_ref().unwrap();
704 let pascal_name = to_pascal_case(&gateway.service_name);
705
706 format!(
707 r#"//! Repository trait definitions
708
709use async_trait::async_trait;
710use rust_decimal::Decimal;
711
712use crate::domain::entities::*;
713use crate::error::Result;
714
715/// Repository trait for {pascal_name} operations
716#[async_trait]
717pub trait {pascal_name}Repository: Send + Sync {{
718 // ============ PUBLIC ENDPOINTS ============
719
720 /// Get server time
721 async fn get_server_time(&self) -> Result<i64>;
722
723 /// Get all available assets
724 async fn get_assets(&self) -> Result<Vec<AssetInfo>>;
725
726 /// Get ticker information for pairs
727 async fn get_ticker(&self, pairs: &[String]) -> Result<Vec<TickerInfo>>;
728
729 // ============ PRIVATE ENDPOINTS ============
730
731 /// Get account balance
732 async fn get_account_balance(&self, creds: &Credentials) -> Result<Vec<Balance>>;
733
734 /// Get trade history
735 async fn get_trades_history(
736 &self,
737 creds: &Credentials,
738 start: Option<i64>,
739 end: Option<i64>,
740 limit: Option<i32>,
741 ) -> Result<Vec<TradeInfo>>;
742
743 // ============ ORDER MANAGEMENT ============
744
745 /// Place a new order
746 async fn add_order(
747 &self,
748 creds: &Credentials,
749 pair: &str,
750 side: OrderSide,
751 order_type: OrderType,
752 volume: Decimal,
753 price: Option<Decimal>,
754 ) -> Result<OrderInfo>;
755
756 /// Cancel an order
757 async fn cancel_order(&self, creds: &Credentials, order_id: &str) -> Result<bool>;
758}}
759"#,
760 pascal_name = pascal_name,
761 )
762}
763
764pub fn application_mod(_config: &ProjectConfig) -> String {
766 r#"//! Application layer - Business logic orchestration
767
768pub mod service;
769
770pub use service::*;
771"#
772 .to_string()
773}
774
775pub fn application_service(config: &ProjectConfig) -> String {
777 let gateway = config.gateway.as_ref().unwrap();
778 let pascal_name = to_pascal_case(&gateway.service_name);
779
780 format!(
781 r#"//! Application services
782
783use std::sync::Arc;
784use async_trait::async_trait;
785use rust_decimal::Decimal;
786
787use crate::domain::entities::*;
788use crate::infrastructure::{pascal_name}Client;
789use crate::error::Result;
790
791/// Service trait for {pascal_name} operations
792#[async_trait]
793pub trait {pascal_name}ServiceTrait: Send + Sync {{
794 async fn get_server_time(&self) -> Result<i64>;
795 async fn get_assets(&self) -> Result<Vec<AssetInfo>>;
796 async fn get_ticker(&self, pairs: &[String]) -> Result<Vec<TickerInfo>>;
797 async fn get_account_balance(&self, creds: &Credentials) -> Result<Vec<Balance>>;
798 async fn get_trades_history(
799 &self,
800 creds: &Credentials,
801 start: Option<i64>,
802 end: Option<i64>,
803 limit: Option<i32>,
804 ) -> Result<Vec<TradeInfo>>;
805 async fn add_order(
806 &self,
807 creds: &Credentials,
808 pair: &str,
809 side: OrderSide,
810 order_type: OrderType,
811 volume: Decimal,
812 price: Option<Decimal>,
813 ) -> Result<OrderInfo>;
814 async fn cancel_order(&self, creds: &Credentials, order_id: &str) -> Result<bool>;
815}}
816
817/// {pascal_name} service implementation
818pub struct {pascal_name}Service {{
819 client: Arc<{pascal_name}Client>,
820}}
821
822impl {pascal_name}Service {{
823 /// Create a new service with the given HTTP client
824 pub fn new(client: Arc<{pascal_name}Client>) -> Self {{
825 Self {{ client }}
826 }}
827}}
828
829#[async_trait]
830impl {pascal_name}ServiceTrait for {pascal_name}Service {{
831 async fn get_server_time(&self) -> Result<i64> {{
832 // TODO: Implement actual API call
833 // let response: ServerTimeResponse = self.client.query_public("/api/time", &[]).await?;
834 // Ok(response.server_time)
835 Ok(std::time::SystemTime::now()
836 .duration_since(std::time::UNIX_EPOCH)
837 .unwrap()
838 .as_secs() as i64)
839 }}
840
841 async fn get_assets(&self) -> Result<Vec<AssetInfo>> {{
842 // TODO: Implement actual API call
843 let _ = &self.client;
844 Ok(vec![])
845 }}
846
847 async fn get_ticker(&self, pairs: &[String]) -> Result<Vec<TickerInfo>> {{
848 // TODO: Implement actual API call
849 let _ = (&self.client, pairs);
850 Ok(vec![])
851 }}
852
853 async fn get_account_balance(&self, creds: &Credentials) -> Result<Vec<Balance>> {{
854 // TODO: Implement actual API call
855 let _ = (&self.client, creds);
856 Ok(vec![])
857 }}
858
859 async fn get_trades_history(
860 &self,
861 creds: &Credentials,
862 start: Option<i64>,
863 end: Option<i64>,
864 limit: Option<i32>,
865 ) -> Result<Vec<TradeInfo>> {{
866 // TODO: Implement actual API call
867 let _ = (&self.client, creds, start, end, limit);
868 Ok(vec![])
869 }}
870
871 async fn add_order(
872 &self,
873 creds: &Credentials,
874 pair: &str,
875 side: OrderSide,
876 order_type: OrderType,
877 volume: Decimal,
878 price: Option<Decimal>,
879 ) -> Result<OrderInfo> {{
880 // TODO: Implement actual API call
881 let _ = &self.client;
882 Ok(OrderInfo {{
883 id: "placeholder".to_string(),
884 pair: pair.to_string(),
885 side,
886 order_type,
887 price,
888 volume,
889 status: OrderStatus::Open,
890 }})
891 }}
892
893 async fn cancel_order(&self, creds: &Credentials, order_id: &str) -> Result<bool> {{
894 // TODO: Implement actual API call
895 let _ = (&self.client, creds, order_id);
896 Ok(true)
897 }}
898}}
899"#,
900 pascal_name = pascal_name,
901 )
902}
903
904pub fn infrastructure_mod(config: &ProjectConfig) -> String {
906 let gateway = config.gateway.as_ref().unwrap();
907 let pascal_name = to_pascal_case(&gateway.service_name);
908
909 format!(
910 r#"//! Infrastructure layer - External implementations
911
912mod http_client;
913mod auth;
914mod cache;
915mod rate_limiter;
916
917pub use http_client::{pascal_name}Client;
918pub use auth::*;
919pub use cache::CachedRepository;
920pub use rate_limiter::{{GatewayRateLimiter, GatewayMetrics}};
921"#,
922 pascal_name = pascal_name,
923 )
924}
925
926pub fn infrastructure_http_client(config: &ProjectConfig) -> String {
928 let gateway = config.gateway.as_ref().unwrap();
929 let pascal_name = to_pascal_case(&gateway.service_name);
930
931 let auth_impl = match gateway.auth_method {
932 AuthMethod::HmacSha256 => hmac_sha256_auth(&pascal_name),
933 AuthMethod::HmacSha512Base64 => hmac_sha512_base64_auth(&pascal_name),
934 AuthMethod::ApiKey => api_key_auth(&pascal_name),
935 _ => no_auth(&pascal_name),
936 };
937
938 format!(
939 r#"//! HTTP client for {pascal_name} API
940
941use reqwest::Client;
942use serde::de::DeserializeOwned;
943use std::time::Duration;
944use tracing::{{debug, instrument}};
945
946use crate::error::{{Result, {pascal_name}Error}};
947
948/// HTTP client for {pascal_name} API
949pub struct {pascal_name}Client {{
950 client: Client,
951 base_url: String,
952}}
953
954impl {pascal_name}Client {{
955 /// Create a new client
956 pub fn new(base_url: &str, timeout: Duration) -> Self {{
957 let client = Client::builder()
958 .timeout(timeout)
959 .build()
960 .expect("Failed to create HTTP client");
961
962 Self {{
963 client,
964 base_url: base_url.to_string(),
965 }}
966 }}
967
968 /// Make a public API request (no authentication)
969 #[instrument(skip(self))]
970 pub async fn query_public<T: DeserializeOwned>(
971 &self,
972 endpoint: &str,
973 params: &[(&str, &str)],
974 ) -> Result<T> {{
975 let url = format!("{{}}{{}}", self.base_url, endpoint);
976 debug!("Public request to {{}}", url);
977
978 let response = self.client
979 .get(&url)
980 .query(params)
981 .send()
982 .await
983 .map_err(|e| {pascal_name}Error::HttpError(e.to_string()))?;
984
985 if !response.status().is_success() {{
986 let status = response.status();
987 let text = response.text().await.unwrap_or_default();
988 return Err({pascal_name}Error::ApiError(format!("{{}} - {{}}", status, text)));
989 }}
990
991 response
992 .json()
993 .await
994 .map_err(|e| {pascal_name}Error::HttpError(e.to_string()))
995 }}
996
997 /// Make a private API request (with authentication)
998 #[instrument(skip(self, api_secret))]
999 pub async fn query_private<T: DeserializeOwned>(
1000 &self,
1001 endpoint: &str,
1002 api_key: &str,
1003 api_secret: &str,
1004 params: &[(&str, &str)],
1005 ) -> Result<T> {{
1006 let url = format!("{{}}{{}}", self.base_url, endpoint);
1007 debug!("Private request to {{}}", url);
1008
1009 {auth_impl}
1010 }}
1011}}
1012"#,
1013 pascal_name = pascal_name,
1014 auth_impl = auth_impl,
1015 )
1016}
1017
1018fn hmac_sha256_auth(pascal_name: &str) -> String {
1019 format!(
1020 r#"use hmac::{{Hmac, Mac}};
1021 use sha2::Sha256;
1022
1023 let timestamp = std::time::SystemTime::now()
1024 .duration_since(std::time::UNIX_EPOCH)
1025 .unwrap()
1026 .as_millis()
1027 .to_string();
1028
1029 let query_string = params
1030 .iter()
1031 .map(|(k, v)| format!("{{}}={{}}", k, v))
1032 .collect::<Vec<_>>()
1033 .join("&");
1034
1035 let sign_payload = format!("{{}}×tamp={{}}", query_string, timestamp);
1036
1037 let mut mac = Hmac::<Sha256>::new_from_slice(api_secret.as_bytes())
1038 .expect("HMAC can take key of any size");
1039 mac.update(sign_payload.as_bytes());
1040 let signature = hex::encode(mac.finalize().into_bytes());
1041
1042 let response = self.client
1043 .get(&url)
1044 .query(params)
1045 .query(&[("timestamp", timestamp.as_str()), ("signature", signature.as_str())])
1046 .header("X-API-KEY", api_key)
1047 .send()
1048 .await
1049 .map_err(|e| {pascal_name}Error::HttpError(e.to_string()))?;
1050
1051 if !response.status().is_success() {{
1052 let status = response.status();
1053 let text = response.text().await.unwrap_or_default();
1054 return Err({pascal_name}Error::ApiError(format!("{{}} - {{}}", status, text)));
1055 }}
1056
1057 response
1058 .json()
1059 .await
1060 .map_err(|e| {pascal_name}Error::HttpError(e.to_string()))"#,
1061 pascal_name = pascal_name
1062 )
1063}
1064
1065fn hmac_sha512_base64_auth(pascal_name: &str) -> String {
1066 format!(
1067 r#"use hmac::{{Hmac, Mac}};
1068 use sha2::{{Sha256, Sha512, Digest}};
1069 use base64::{{Engine, engine::general_purpose}};
1070
1071 let nonce = std::time::SystemTime::now()
1072 .duration_since(std::time::UNIX_EPOCH)
1073 .unwrap()
1074 .as_millis()
1075 .to_string();
1076
1077 let data = params
1078 .iter()
1079 .map(|(k, v)| format!("{{}}={{}}", k, v))
1080 .chain(std::iter::once(format!("nonce={{}}", nonce)))
1081 .collect::<Vec<_>>()
1082 .join("&");
1083
1084 // SHA256 hash of nonce + data
1085 let sha256_hash = Sha256::digest(format!("{{}}{{}}", nonce, data).as_bytes());
1086
1087 // Concatenate path + sha256 hash
1088 let hmac_input = [endpoint.as_bytes(), &sha256_hash[..]].concat();
1089
1090 // HMAC-SHA512 with base64-decoded secret
1091 let secret_decoded = general_purpose::STANDARD
1092 .decode(api_secret)
1093 .map_err(|_| {pascal_name}Error::InvalidCredentials)?;
1094
1095 let mut mac = Hmac::<Sha512>::new_from_slice(&secret_decoded)
1096 .expect("HMAC can take key of any size");
1097 mac.update(&hmac_input);
1098 let signature = general_purpose::STANDARD.encode(mac.finalize().into_bytes());
1099
1100 let response = self.client
1101 .post(&url)
1102 .header("API-Key", api_key)
1103 .header("API-Sign", signature)
1104 .form(&[("nonce", nonce.as_str())].into_iter().chain(params.iter().copied()).collect::<Vec<_>>())
1105 .send()
1106 .await
1107 .map_err(|e| {pascal_name}Error::HttpError(e.to_string()))?;
1108
1109 if !response.status().is_success() {{
1110 let status = response.status();
1111 let text = response.text().await.unwrap_or_default();
1112 return Err({pascal_name}Error::ApiError(format!("{{}} - {{}}", status, text)));
1113 }}
1114
1115 response
1116 .json()
1117 .await
1118 .map_err(|e| {pascal_name}Error::HttpError(e.to_string()))"#,
1119 pascal_name = pascal_name
1120 )
1121}
1122
1123fn api_key_auth(pascal_name: &str) -> String {
1124 format!(
1125 r#"let response = self.client
1126 .get(&url)
1127 .query(params)
1128 .header("X-API-KEY", api_key)
1129 .send()
1130 .await
1131 .map_err(|e| {pascal_name}Error::HttpError(e.to_string()))?;
1132
1133 if !response.status().is_success() {{
1134 let status = response.status();
1135 let text = response.text().await.unwrap_or_default();
1136 return Err({pascal_name}Error::ApiError(format!("{{}} - {{}}", status, text)));
1137 }}
1138
1139 response
1140 .json()
1141 .await
1142 .map_err(|e| {pascal_name}Error::HttpError(e.to_string()))"#,
1143 pascal_name = pascal_name
1144 )
1145}
1146
1147fn no_auth(pascal_name: &str) -> String {
1148 format!(
1149 r#"let response = self.client
1150 .get(&url)
1151 .query(params)
1152 .send()
1153 .await
1154 .map_err(|e| {pascal_name}Error::HttpError(e.to_string()))?;
1155
1156 if !response.status().is_success() {{
1157 let status = response.status();
1158 let text = response.text().await.unwrap_or_default();
1159 return Err({pascal_name}Error::ApiError(format!("{{}} - {{}}", status, text)));
1160 }}
1161
1162 response
1163 .json()
1164 .await
1165 .map_err(|e| {pascal_name}Error::HttpError(e.to_string()))"#,
1166 pascal_name = pascal_name
1167 )
1168}
1169
1170pub fn infrastructure_auth(config: &ProjectConfig) -> String {
1172 let gateway = config.gateway.as_ref().unwrap();
1173 let pascal_name = to_pascal_case(&gateway.service_name);
1174
1175 format!(
1176 r#"//! Authentication utilities for {pascal_name} API
1177
1178use hmac::{{Hmac, Mac}};
1179use sha2::{{Sha256, Sha512, Digest}};
1180use base64::{{Engine, engine::general_purpose}};
1181
1182use crate::error::{pascal_name}Error;
1183
1184/// Sign a request using HMAC-SHA256
1185pub fn sign_hmac_sha256(
1186 api_secret: &str,
1187 message: &str,
1188) -> String {{
1189 let mut mac = Hmac::<Sha256>::new_from_slice(api_secret.as_bytes())
1190 .expect("HMAC can take key of any size");
1191 mac.update(message.as_bytes());
1192 hex::encode(mac.finalize().into_bytes())
1193}}
1194
1195/// Sign a request using HMAC-SHA512 with Base64 encoding
1196pub fn sign_hmac_sha512_base64(
1197 api_secret: &str,
1198 path: &str,
1199 nonce: &str,
1200 post_data: &str,
1201) -> Result<String, {pascal_name}Error> {{
1202 // SHA256 hash of nonce + post_data
1203 let sha256_hash = Sha256::digest(format!("{{}}{{}}", nonce, post_data).as_bytes());
1204
1205 // Concatenate path + sha256 hash
1206 let hmac_input = [path.as_bytes(), &sha256_hash[..]].concat();
1207
1208 // Decode base64 secret
1209 let secret_decoded = general_purpose::STANDARD
1210 .decode(api_secret)
1211 .map_err(|_| {pascal_name}Error::InvalidCredentials)?;
1212
1213 // HMAC-SHA512
1214 let mut mac = Hmac::<Sha512>::new_from_slice(&secret_decoded)
1215 .expect("HMAC can take key of any size");
1216 mac.update(&hmac_input);
1217
1218 Ok(general_purpose::STANDARD.encode(mac.finalize().into_bytes()))
1219}}
1220
1221/// Generate a nonce (timestamp in milliseconds)
1222pub fn generate_nonce() -> String {{
1223 std::time::SystemTime::now()
1224 .duration_since(std::time::UNIX_EPOCH)
1225 .unwrap()
1226 .as_millis()
1227 .to_string()
1228}}
1229"#,
1230 pascal_name = pascal_name,
1231 )
1232}
1233
1234fn _infrastructure_repository(config: &ProjectConfig) -> String {
1236 let gateway = config.gateway.as_ref().unwrap();
1237 let pascal_name = to_pascal_case(&gateway.service_name);
1238
1239 format!(
1240 r#"//! Repository implementation
1241
1242use std::sync::Arc;
1243use async_trait::async_trait;
1244use rust_decimal::Decimal;
1245use tracing::instrument;
1246
1247use crate::domain::{{entities::*, repository::{pascal_name}Repository}};
1248use crate::error::Result;
1249use crate::infrastructure::{{
1250 {pascal_name}Client,
1251 GatewayRateLimiter,
1252 GatewayMetrics,
1253}};
1254
1255/// Repository implementation using HTTP client
1256pub struct {pascal_name}RestRepository {{
1257 client: {pascal_name}Client,
1258 rate_limiter: Arc<GatewayRateLimiter>,
1259 metrics: Arc<GatewayMetrics>,
1260}}
1261
1262impl {pascal_name}RestRepository {{
1263 pub fn new(
1264 client: {pascal_name}Client,
1265 rate_limiter: Arc<GatewayRateLimiter>,
1266 metrics: Arc<GatewayMetrics>,
1267 ) -> Self {{
1268 Self {{
1269 client,
1270 rate_limiter,
1271 metrics,
1272 }}
1273 }}
1274}}
1275
1276#[async_trait]
1277impl {pascal_name}Repository for {pascal_name}RestRepository {{
1278 #[instrument(skip(self))]
1279 async fn get_server_time(&self) -> Result<i64> {{
1280 self.rate_limiter.wait_public().await;
1281 self.metrics.requests_total.increment(1);
1282
1283 // TODO: Implement actual API call
1284 // let response: ServerTimeResponse = self.client.query_public("/api/time", &[]).await?;
1285 // Ok(response.server_time)
1286
1287 Ok(chrono::Utc::now().timestamp())
1288 }}
1289
1290 #[instrument(skip(self))]
1291 async fn get_assets(&self) -> Result<Vec<AssetInfo>> {{
1292 self.rate_limiter.wait_public().await;
1293 self.metrics.requests_total.increment(1);
1294
1295 // TODO: Implement actual API call
1296 Ok(vec![])
1297 }}
1298
1299 #[instrument(skip(self))]
1300 async fn get_ticker(&self, pairs: &[String]) -> Result<Vec<TickerInfo>> {{
1301 self.rate_limiter.wait_public().await;
1302 self.metrics.requests_total.increment(1);
1303
1304 // TODO: Implement actual API call
1305 let _ = pairs;
1306 Ok(vec![])
1307 }}
1308
1309 #[instrument(skip(self, creds))]
1310 async fn get_account_balance(&self, creds: &Credentials) -> Result<Vec<Balance>> {{
1311 self.rate_limiter.wait_private().await;
1312 self.metrics.requests_total.increment(1);
1313
1314 // TODO: Implement actual API call
1315 let _ = creds;
1316 Ok(vec![])
1317 }}
1318
1319 #[instrument(skip(self, creds))]
1320 async fn get_trades_history(
1321 &self,
1322 creds: &Credentials,
1323 start: Option<i64>,
1324 end: Option<i64>,
1325 limit: Option<i32>,
1326 ) -> Result<Vec<TradeInfo>> {{
1327 self.rate_limiter.wait_private().await;
1328 self.metrics.requests_total.increment(1);
1329
1330 // TODO: Implement actual API call
1331 let _ = (creds, start, end, limit);
1332 Ok(vec![])
1333 }}
1334
1335 #[instrument(skip(self, creds))]
1336 async fn add_order(
1337 &self,
1338 creds: &Credentials,
1339 pair: &str,
1340 side: OrderSide,
1341 order_type: OrderType,
1342 volume: Decimal,
1343 price: Option<Decimal>,
1344 ) -> Result<OrderInfo> {{
1345 self.rate_limiter.wait_private().await;
1346 self.metrics.requests_total.increment(1);
1347
1348 // TODO: Implement actual API call
1349 Ok(OrderInfo {{
1350 id: "placeholder".to_string(),
1351 pair: pair.to_string(),
1352 side,
1353 order_type,
1354 price,
1355 volume,
1356 status: OrderStatus::Open,
1357 }})
1358 }}
1359
1360 #[instrument(skip(self, creds))]
1361 async fn cancel_order(&self, creds: &Credentials, order_id: &str) -> Result<bool> {{
1362 self.rate_limiter.wait_private().await;
1363 self.metrics.requests_total.increment(1);
1364
1365 // TODO: Implement actual API call
1366 let _ = (creds, order_id);
1367 Ok(true)
1368 }}
1369}}
1370"#,
1371 pascal_name = pascal_name,
1372 )
1373}
1374
1375pub fn infrastructure_rate_limiter(_config: &ProjectConfig) -> String {
1377 r#"//! Rate limiting and metrics for API calls
1378
1379use std::sync::atomic::{AtomicU64, Ordering};
1380use std::time::{Duration, Instant};
1381use tokio::sync::Semaphore;
1382
1383/// Rate limiter for gateway API calls
1384pub struct GatewayRateLimiter {
1385 public_semaphore: Semaphore,
1386 private_semaphore: Semaphore,
1387 public_interval: Duration,
1388 private_interval: Duration,
1389 last_public: AtomicU64,
1390 last_private: AtomicU64,
1391}
1392
1393impl GatewayRateLimiter {
1394 pub fn new(public_rps: u32, private_rps: u32, burst: u32) -> Self {
1395 Self {
1396 public_semaphore: Semaphore::new(burst as usize),
1397 private_semaphore: Semaphore::new(burst as usize),
1398 public_interval: Duration::from_millis(1000 / public_rps.max(1) as u64),
1399 private_interval: Duration::from_millis(1000 / private_rps.max(1) as u64),
1400 last_public: AtomicU64::new(0),
1401 last_private: AtomicU64::new(0),
1402 }
1403 }
1404
1405 /// Wait for public rate limit
1406 pub async fn wait_public(&self) {
1407 let _permit = self.public_semaphore.acquire().await.unwrap();
1408 self.wait_interval(&self.last_public, self.public_interval).await;
1409 }
1410
1411 /// Wait for private rate limit
1412 pub async fn wait_private(&self) {
1413 let _permit = self.private_semaphore.acquire().await.unwrap();
1414 self.wait_interval(&self.last_private, self.private_interval).await;
1415 }
1416
1417 async fn wait_interval(&self, last: &AtomicU64, interval: Duration) {
1418 let now = Instant::now().elapsed().as_millis() as u64;
1419 let last_time = last.load(Ordering::Relaxed);
1420 let elapsed = now.saturating_sub(last_time);
1421
1422 if elapsed < interval.as_millis() as u64 {
1423 let wait_time = interval.as_millis() as u64 - elapsed;
1424 tokio::time::sleep(Duration::from_millis(wait_time)).await;
1425 }
1426
1427 last.store(Instant::now().elapsed().as_millis() as u64, Ordering::Relaxed);
1428 }
1429}
1430
1431/// Metrics collector for gateway operations
1432pub struct GatewayMetrics {
1433 pub requests_total: Counter,
1434 pub cache_hits: Counter,
1435 pub cache_misses: Counter,
1436 pub errors_total: Counter,
1437}
1438
1439impl GatewayMetrics {
1440 pub fn new() -> Self {
1441 Self {
1442 requests_total: Counter::new(),
1443 cache_hits: Counter::new(),
1444 cache_misses: Counter::new(),
1445 errors_total: Counter::new(),
1446 }
1447 }
1448}
1449
1450impl Default for GatewayMetrics {
1451 fn default() -> Self {
1452 Self::new()
1453 }
1454}
1455
1456/// Simple atomic counter
1457pub struct Counter {
1458 value: AtomicU64,
1459}
1460
1461impl Counter {
1462 pub fn new() -> Self {
1463 Self {
1464 value: AtomicU64::new(0),
1465 }
1466 }
1467
1468 pub fn increment(&self, n: u64) {
1469 self.value.fetch_add(n, Ordering::Relaxed);
1470 }
1471
1472 pub fn get(&self) -> u64 {
1473 self.value.load(Ordering::Relaxed)
1474 }
1475}
1476
1477impl Default for Counter {
1478 fn default() -> Self {
1479 Self::new()
1480 }
1481}
1482"#
1483 .to_string()
1484}
1485
1486pub fn infrastructure_cache(config: &ProjectConfig) -> String {
1488 let gateway = config.gateway.as_ref().unwrap();
1489 let pascal_name = to_pascal_case(&gateway.service_name);
1490
1491 format!(
1492 r#"//! Caching decorator for repository
1493
1494use std::sync::Arc;
1495use std::time::Duration;
1496use async_trait::async_trait;
1497use moka::future::Cache;
1498use rust_decimal::Decimal;
1499use tracing::debug;
1500
1501use crate::domain::{{entities::*, repository::{pascal_name}Repository}};
1502use crate::error::Result;
1503
1504/// Caching decorator for repository
1505pub struct CachedRepository {{
1506 inner: Arc<dyn {pascal_name}Repository>,
1507 asset_cache: Cache<String, Vec<AssetInfo>>,
1508 ticker_cache: Cache<String, Vec<TickerInfo>>,
1509 public_ttl: Duration,
1510 private_ttl: Duration,
1511}}
1512
1513impl CachedRepository {{
1514 pub fn new(
1515 inner: Arc<dyn {pascal_name}Repository>,
1516 public_ttl: Duration,
1517 private_ttl: Duration,
1518 ) -> Self {{
1519 Self {{
1520 inner,
1521 asset_cache: Cache::builder()
1522 .time_to_live(public_ttl)
1523 .max_capacity(100)
1524 .build(),
1525 ticker_cache: Cache::builder()
1526 .time_to_live(public_ttl)
1527 .max_capacity(1000)
1528 .build(),
1529 public_ttl,
1530 private_ttl,
1531 }}
1532 }}
1533}}
1534
1535#[async_trait]
1536impl {pascal_name}Repository for CachedRepository {{
1537 async fn get_server_time(&self) -> Result<i64> {{
1538 // Server time should not be cached
1539 self.inner.get_server_time().await
1540 }}
1541
1542 async fn get_assets(&self) -> Result<Vec<AssetInfo>> {{
1543 let cache_key = "assets".to_string();
1544
1545 if let Some(cached) = self.asset_cache.get(&cache_key).await {{
1546 debug!("Cache hit for assets");
1547 return Ok(cached);
1548 }}
1549
1550 debug!("Cache miss for assets");
1551 let result = self.inner.get_assets().await?;
1552 self.asset_cache.insert(cache_key, result.clone()).await;
1553 Ok(result)
1554 }}
1555
1556 async fn get_ticker(&self, pairs: &[String]) -> Result<Vec<TickerInfo>> {{
1557 let cache_key = pairs.join(",");
1558
1559 if let Some(cached) = self.ticker_cache.get(&cache_key).await {{
1560 debug!("Cache hit for ticker");
1561 return Ok(cached);
1562 }}
1563
1564 debug!("Cache miss for ticker");
1565 let result = self.inner.get_ticker(pairs).await?;
1566 self.ticker_cache.insert(cache_key, result.clone()).await;
1567 Ok(result)
1568 }}
1569
1570 // Private methods are not cached by default
1571 async fn get_account_balance(&self, creds: &Credentials) -> Result<Vec<Balance>> {{
1572 self.inner.get_account_balance(creds).await
1573 }}
1574
1575 async fn get_trades_history(
1576 &self,
1577 creds: &Credentials,
1578 start: Option<i64>,
1579 end: Option<i64>,
1580 limit: Option<i32>,
1581 ) -> Result<Vec<TradeInfo>> {{
1582 self.inner.get_trades_history(creds, start, end, limit).await
1583 }}
1584
1585 async fn add_order(
1586 &self,
1587 creds: &Credentials,
1588 pair: &str,
1589 side: OrderSide,
1590 order_type: OrderType,
1591 volume: Decimal,
1592 price: Option<Decimal>,
1593 ) -> Result<OrderInfo> {{
1594 self.inner.add_order(creds, pair, side, order_type, volume, price).await
1595 }}
1596
1597 async fn cancel_order(&self, creds: &Credentials, order_id: &str) -> Result<bool> {{
1598 self.inner.cancel_order(creds, order_id).await
1599 }}
1600}}
1601"#,
1602 pascal_name = pascal_name,
1603 )
1604}
1605
1606pub fn presentation_mod(_config: &ProjectConfig) -> String {
1608 r#"//! Presentation layer - gRPC service implementation
1609
1610pub mod grpc;
1611
1612pub use grpc::*;
1613"#
1614 .to_string()
1615}
1616
1617pub fn presentation_grpc(config: &ProjectConfig) -> String {
1619 let gateway = config.gateway.as_ref().unwrap();
1620 let service_name = &gateway.service_name;
1621 let pascal_name = to_pascal_case(service_name);
1622
1623 format!(
1624 r#"//! gRPC service implementation
1625
1626use std::sync::Arc;
1627use tonic::{{Request, Response, Status}};
1628use tracing::instrument;
1629
1630use crate::application::{pascal_name}ServiceTrait;
1631use crate::domain::entities::{{Credentials, OrderSide, OrderType}};
1632use crate::generated::{{
1633 {service_name}_service_server::{pascal_name}Service as GrpcServiceTrait,
1634 *,
1635}};
1636
1637/// gRPC service implementation
1638pub struct {pascal_name}GrpcService {{
1639 service: Arc<dyn {pascal_name}ServiceTrait>,
1640}}
1641
1642impl {pascal_name}GrpcService {{
1643 pub fn new(service: Arc<dyn {pascal_name}ServiceTrait>) -> Self {{
1644 Self {{ service }}
1645 }}
1646
1647 fn extract_credentials(creds: Option<crate::generated::Credentials>) -> Result<Credentials, Status> {{
1648 let c = creds.ok_or_else(|| Status::unauthenticated("Missing credentials"))?;
1649 Ok(Credentials {{
1650 api_key: c.api_key,
1651 api_secret: c.api_secret,
1652 }})
1653 }}
1654}}
1655
1656#[tonic::async_trait]
1657impl GrpcServiceTrait for {pascal_name}GrpcService {{
1658 #[instrument(skip(self))]
1659 async fn get_server_time(
1660 &self,
1661 _request: Request<GetServerTimeRequest>,
1662 ) -> Result<Response<GetServerTimeResponse>, Status> {{
1663 let time = self.service.get_server_time().await.map_err(Status::from)?;
1664 Ok(Response::new(GetServerTimeResponse {{ server_time: time }}))
1665 }}
1666
1667 #[instrument(skip(self))]
1668 async fn get_assets(
1669 &self,
1670 _request: Request<GetAssetsRequest>,
1671 ) -> Result<Response<GetAssetsResponse>, Status> {{
1672 let assets = self.service.get_assets().await.map_err(Status::from)?;
1673
1674 let assets_map = assets
1675 .into_iter()
1676 .map(|a| {{
1677 (
1678 a.symbol.clone(),
1679 AssetInfo {{
1680 symbol: a.symbol,
1681 name: a.name,
1682 decimals: a.decimals,
1683 }},
1684 )
1685 }})
1686 .collect();
1687
1688 Ok(Response::new(GetAssetsResponse {{ assets: assets_map }}))
1689 }}
1690
1691 #[instrument(skip(self))]
1692 async fn get_ticker(
1693 &self,
1694 request: Request<GetTickerRequest>,
1695 ) -> Result<Response<GetTickerResponse>, Status> {{
1696 let pairs = request.into_inner().pairs;
1697 let tickers = self.service.get_ticker(&pairs).await.map_err(Status::from)?;
1698
1699 let tickers_map = tickers
1700 .into_iter()
1701 .map(|t| {{
1702 (
1703 t.pair.clone(),
1704 crate::generated::TickerInfo {{
1705 pair: t.pair,
1706 last_price: t.last_price.to_string(),
1707 bid: t.bid.to_string(),
1708 ask: t.ask.to_string(),
1709 volume_24h: t.volume_24h.to_string(),
1710 }},
1711 )
1712 }})
1713 .collect();
1714
1715 Ok(Response::new(GetTickerResponse {{ tickers: tickers_map }}))
1716 }}
1717
1718 #[instrument(skip(self))]
1719 async fn get_account_balance(
1720 &self,
1721 request: Request<GetAccountBalanceRequest>,
1722 ) -> Result<Response<GetAccountBalanceResponse>, Status> {{
1723 let creds = Self::extract_credentials(request.into_inner().credentials)?;
1724 let balances = self.service.get_account_balance(&creds).await.map_err(Status::from)?;
1725
1726 let balances_map = balances
1727 .into_iter()
1728 .map(|b| (b.asset, b.free.to_string()))
1729 .collect();
1730
1731 Ok(Response::new(GetAccountBalanceResponse {{ balances: balances_map }}))
1732 }}
1733
1734 #[instrument(skip(self))]
1735 async fn get_trades_history(
1736 &self,
1737 request: Request<GetTradesHistoryRequest>,
1738 ) -> Result<Response<GetTradesHistoryResponse>, Status> {{
1739 let req = request.into_inner();
1740 let creds = Self::extract_credentials(req.credentials)?;
1741
1742 let start = req.start_time.and_then(|s| s.parse().ok());
1743 let end = req.end_time.and_then(|s| s.parse().ok());
1744 let limit = req.limit;
1745
1746 let trades = self.service
1747 .get_trades_history(&creds, start, end, limit)
1748 .await
1749 .map_err(Status::from)?;
1750
1751 let trades_proto = trades
1752 .into_iter()
1753 .map(|t| crate::generated::TradeInfo {{
1754 id: t.id,
1755 pair: t.pair,
1756 side: t.side.to_string(),
1757 price: t.price.to_string(),
1758 volume: t.volume.to_string(),
1759 fee: t.fee.to_string(),
1760 timestamp: t.timestamp,
1761 }})
1762 .collect();
1763
1764 Ok(Response::new(GetTradesHistoryResponse {{ trades: trades_proto }}))
1765 }}
1766
1767 #[instrument(skip(self))]
1768 async fn add_order(
1769 &self,
1770 request: Request<AddOrderRequest>,
1771 ) -> Result<Response<AddOrderResponse>, Status> {{
1772 let req = request.into_inner();
1773 let creds = Self::extract_credentials(req.credentials)?;
1774
1775 let side: OrderSide = req.side.parse()
1776 .map_err(|_| Status::invalid_argument("Invalid order side"))?;
1777 let order_type: OrderType = req.order_type.parse()
1778 .map_err(|_| Status::invalid_argument("Invalid order type"))?;
1779 let volume = req.volume.parse()
1780 .map_err(|_| Status::invalid_argument("Invalid volume"))?;
1781 let price = req.price.map(|p| p.parse()).transpose()
1782 .map_err(|_| Status::invalid_argument("Invalid price"))?;
1783
1784 let order = self.service
1785 .add_order(&creds, &req.pair, side, order_type, volume, price)
1786 .await
1787 .map_err(Status::from)?;
1788
1789 Ok(Response::new(AddOrderResponse {{
1790 order_id: order.id,
1791 status: format!("{{:?}}", order.status),
1792 }}))
1793 }}
1794
1795 #[instrument(skip(self))]
1796 async fn cancel_order(
1797 &self,
1798 request: Request<CancelOrderRequest>,
1799 ) -> Result<Response<CancelOrderResponse>, Status> {{
1800 let req = request.into_inner();
1801 let creds = Self::extract_credentials(req.credentials)?;
1802
1803 let success = self.service
1804 .cancel_order(&creds, &req.order_id)
1805 .await
1806 .map_err(Status::from)?;
1807
1808 Ok(Response::new(CancelOrderResponse {{ success }}))
1809 }}
1810
1811 #[instrument(skip(self))]
1812 async fn health_check(
1813 &self,
1814 _request: Request<HealthCheckRequest>,
1815 ) -> Result<Response<HealthCheckResponse>, Status> {{
1816 // Simple health check - try to get server time
1817 match self.service.get_server_time().await {{
1818 Ok(_) => Ok(Response::new(HealthCheckResponse {{
1819 healthy: true,
1820 status: "healthy".to_string(),
1821 }})),
1822 Err(e) => Ok(Response::new(HealthCheckResponse {{
1823 healthy: false,
1824 status: format!("unhealthy: {{}}", e),
1825 }})),
1826 }}
1827 }}
1828}}
1829"#,
1830 service_name = service_name,
1831 pascal_name = pascal_name,
1832 )
1833}
1834
1835pub fn env_example(config: &ProjectConfig) -> String {
1837 let gateway = config.gateway.as_ref().unwrap();
1838 let upper_name = gateway.service_name.to_uppercase();
1839
1840 format!(
1841 r#"# Server Configuration
1842{upper_name}_GATEWAY_PORT={grpc_port}
1843{upper_name}_HEALTH_PORT={health_port}
1844{upper_name}_METRICS_PORT={metrics_port}
1845
1846# API Configuration
1847{upper_name}_API_URL={api_base_url}
1848{upper_name}_API_TIMEOUT_SECONDS=30
1849
1850# Rate Limiting
1851{upper_name}_RATE_LIMIT_PUBLIC_RPS={public_rps}
1852{upper_name}_RATE_LIMIT_PRIVATE_RPS={private_rps}
1853{upper_name}_RATE_LIMIT_BURST={burst}
1854
1855# Cache Configuration
1856CACHE_ENABLED=true
1857CACHE_PUBLIC_TTL_SECONDS={public_ttl}
1858CACHE_PRIVATE_TTL_SECONDS={private_ttl}
1859
1860# Observability
1861RUST_LOG=info
1862"#,
1863 upper_name = upper_name,
1864 grpc_port = gateway.server.grpc_port,
1865 health_port = gateway.server.health_port,
1866 metrics_port = gateway.server.metrics_port,
1867 api_base_url = gateway.api_base_url,
1868 public_rps = gateway.rate_limit.public_rps,
1869 private_rps = gateway.rate_limit.private_rps,
1870 burst = gateway.rate_limit.burst,
1871 public_ttl = gateway.cache.public_ttl_secs,
1872 private_ttl = gateway.cache.private_ttl_secs,
1873 )
1874}
1875
1876pub fn dockerfile(config: &ProjectConfig) -> String {
1878 format!(
1879 r#"FROM rust:1.86 AS builder
1880WORKDIR /app
1881COPY . .
1882RUN apt-get update && apt-get install -y protobuf-compiler
1883RUN cargo build --release
1884
1885FROM debian:bookworm-slim
1886RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
1887COPY --from=builder /app/target/release/{name} /usr/local/bin/
1888EXPOSE 8080 8081 9090
1889CMD ["{name}"]
1890"#,
1891 name = config.name,
1892 )
1893}
1894
1895pub fn readme(config: &ProjectConfig) -> String {
1897 let gateway = config.gateway.as_ref().unwrap();
1898
1899 format!(
1900 r#"# {display_name}
1901
1902A gRPC gateway service wrapping the {display_name} API with built-in resilience, caching, and observability.
1903
1904## Features
1905
1906- **gRPC API**: Full gRPC service with proto definitions
1907- **Rate Limiting**: Configurable rate limits for public and private endpoints
1908- **Caching**: In-memory caching with configurable TTLs
1909- **Resilience**: Built-in retry, circuit breaker patterns
1910- **Observability**: Tracing and metrics support
1911
1912## Quick Start
1913
1914```bash
1915# Copy environment template
1916cp .env.example .env
1917
1918# Build and run
1919cargo run
1920
1921# Or with Docker
1922docker build -t {name} .
1923docker run -p 8080:8080 -p 8081:8081 -p 9090:9090 {name}
1924```
1925
1926## Configuration
1927
1928See `.env.example` for all available configuration options.
1929
1930## Ports
1931
1932| Port | Purpose |
1933|------|---------|
1934| {grpc_port} | gRPC server |
1935| {health_port} | Health check |
1936| {metrics_port} | Prometheus metrics |
1937
1938## Generated with AllFrame
1939
1940This project was generated using [AllFrame](https://github.com/all-source-os/all-frame).
1941
1942```bash
1943allframe ignite {name} --archetype gateway
1944```
1945"#,
1946 name = config.name,
1947 display_name = gateway.display_name,
1948 grpc_port = gateway.server.grpc_port,
1949 health_port = gateway.server.health_port,
1950 metrics_port = gateway.server.metrics_port,
1951 )
1952}
1953
1954pub fn gitignore() -> String {
1956 r#"/target
1957Cargo.lock
1958.env
1959*.log
1960"#
1961 .to_string()
1962}
1963
1964fn to_pascal_case(s: &str) -> String {
1966 s.split('_')
1967 .map(|word| {
1968 let mut chars = word.chars();
1969 match chars.next() {
1970 Some(c) => c.to_uppercase().chain(chars).collect::<String>(),
1971 None => String::new(),
1972 }
1973 })
1974 .collect()
1975}
1976
1977#[cfg(test)]
1978mod tests {
1979 use super::*;
1980
1981 #[test]
1982 fn test_to_pascal_case() {
1983 assert_eq!(to_pascal_case("kraken"), "Kraken");
1984 assert_eq!(to_pascal_case("my_exchange"), "MyExchange");
1985 assert_eq!(to_pascal_case("api_gateway_service"), "ApiGatewayService");
1986 }
1987}