Skip to main content

allframe_forge/templates/
gateway.rs

1//! Gateway archetype templates
2//!
3//! Templates for generating exchange gateway services with gRPC,
4//! resilience patterns, caching, and observability.
5
6use crate::config::{AuthMethod, CacheBackend, ProjectConfig};
7
8/// Generate Cargo.toml for gateway project
9pub 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
86/// Generate build.rs for proto compilation
87pub 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
102/// Generate the proto file
103pub 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
236/// Generate main.rs
237pub 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
339/// Generate lib.rs
340pub 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
355/// Generate config.rs
356pub 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
480/// Generate error.rs
481pub 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
547/// Generate domain/mod.rs
548pub 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
560/// Generate domain/entities.rs
561pub 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
701/// Generate domain/repository.rs
702pub 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
764/// Generate application/mod.rs
765pub 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
775/// Generate application/service.rs
776pub 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
904/// Generate infrastructure/mod.rs
905pub 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
926/// Generate infrastructure/http_client.rs
927pub 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!("{{}}&timestamp={{}}", 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
1170/// Generate infrastructure/auth.rs
1171pub 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
1234/// Generate infrastructure/{service}_repository.rs (not used directly, kept for
1235/// reference)
1236fn _infrastructure_repository(config: &ProjectConfig) -> String {
1237    let gateway = config.gateway.as_ref().unwrap();
1238    let pascal_name = to_pascal_case(&gateway.service_name);
1239
1240    format!(
1241        r#"//! Repository implementation
1242
1243use std::sync::Arc;
1244use async_trait::async_trait;
1245use rust_decimal::Decimal;
1246use tracing::instrument;
1247
1248use crate::domain::{{entities::*, repository::{pascal_name}Repository}};
1249use crate::error::Result;
1250use crate::infrastructure::{{
1251    {pascal_name}Client,
1252    GatewayRateLimiter,
1253    GatewayMetrics,
1254}};
1255
1256/// Repository implementation using HTTP client
1257pub struct {pascal_name}RestRepository {{
1258    client: {pascal_name}Client,
1259    rate_limiter: Arc<GatewayRateLimiter>,
1260    metrics: Arc<GatewayMetrics>,
1261}}
1262
1263impl {pascal_name}RestRepository {{
1264    pub fn new(
1265        client: {pascal_name}Client,
1266        rate_limiter: Arc<GatewayRateLimiter>,
1267        metrics: Arc<GatewayMetrics>,
1268    ) -> Self {{
1269        Self {{
1270            client,
1271            rate_limiter,
1272            metrics,
1273        }}
1274    }}
1275}}
1276
1277#[async_trait]
1278impl {pascal_name}Repository for {pascal_name}RestRepository {{
1279    #[instrument(skip(self))]
1280    async fn get_server_time(&self) -> Result<i64> {{
1281        self.rate_limiter.wait_public().await;
1282        self.metrics.requests_total.increment(1);
1283
1284        // TODO: Implement actual API call
1285        // let response: ServerTimeResponse = self.client.query_public("/api/time", &[]).await?;
1286        // Ok(response.server_time)
1287
1288        Ok(chrono::Utc::now().timestamp())
1289    }}
1290
1291    #[instrument(skip(self))]
1292    async fn get_assets(&self) -> Result<Vec<AssetInfo>> {{
1293        self.rate_limiter.wait_public().await;
1294        self.metrics.requests_total.increment(1);
1295
1296        // TODO: Implement actual API call
1297        Ok(vec![])
1298    }}
1299
1300    #[instrument(skip(self))]
1301    async fn get_ticker(&self, pairs: &[String]) -> Result<Vec<TickerInfo>> {{
1302        self.rate_limiter.wait_public().await;
1303        self.metrics.requests_total.increment(1);
1304
1305        // TODO: Implement actual API call
1306        let _ = pairs;
1307        Ok(vec![])
1308    }}
1309
1310    #[instrument(skip(self, creds))]
1311    async fn get_account_balance(&self, creds: &Credentials) -> Result<Vec<Balance>> {{
1312        self.rate_limiter.wait_private().await;
1313        self.metrics.requests_total.increment(1);
1314
1315        // TODO: Implement actual API call
1316        let _ = creds;
1317        Ok(vec![])
1318    }}
1319
1320    #[instrument(skip(self, creds))]
1321    async fn get_trades_history(
1322        &self,
1323        creds: &Credentials,
1324        start: Option<i64>,
1325        end: Option<i64>,
1326        limit: Option<i32>,
1327    ) -> Result<Vec<TradeInfo>> {{
1328        self.rate_limiter.wait_private().await;
1329        self.metrics.requests_total.increment(1);
1330
1331        // TODO: Implement actual API call
1332        let _ = (creds, start, end, limit);
1333        Ok(vec![])
1334    }}
1335
1336    #[instrument(skip(self, creds))]
1337    async fn add_order(
1338        &self,
1339        creds: &Credentials,
1340        pair: &str,
1341        side: OrderSide,
1342        order_type: OrderType,
1343        volume: Decimal,
1344        price: Option<Decimal>,
1345    ) -> Result<OrderInfo> {{
1346        self.rate_limiter.wait_private().await;
1347        self.metrics.requests_total.increment(1);
1348
1349        // TODO: Implement actual API call
1350        Ok(OrderInfo {{
1351            id: "placeholder".to_string(),
1352            pair: pair.to_string(),
1353            side,
1354            order_type,
1355            price,
1356            volume,
1357            status: OrderStatus::Open,
1358        }})
1359    }}
1360
1361    #[instrument(skip(self, creds))]
1362    async fn cancel_order(&self, creds: &Credentials, order_id: &str) -> Result<bool> {{
1363        self.rate_limiter.wait_private().await;
1364        self.metrics.requests_total.increment(1);
1365
1366        // TODO: Implement actual API call
1367        let _ = (creds, order_id);
1368        Ok(true)
1369    }}
1370}}
1371"#,
1372        pascal_name = pascal_name,
1373    )
1374}
1375
1376/// Generate infrastructure/rate_limiter.rs (includes metrics)
1377pub fn infrastructure_rate_limiter(_config: &ProjectConfig) -> String {
1378    r#"//! Rate limiting and metrics for API calls
1379
1380use std::sync::atomic::{AtomicU64, Ordering};
1381use std::time::{Duration, Instant};
1382use tokio::sync::Semaphore;
1383
1384/// Rate limiter for gateway API calls
1385pub struct GatewayRateLimiter {
1386    public_semaphore: Semaphore,
1387    private_semaphore: Semaphore,
1388    public_interval: Duration,
1389    private_interval: Duration,
1390    last_public: AtomicU64,
1391    last_private: AtomicU64,
1392}
1393
1394impl GatewayRateLimiter {
1395    pub fn new(public_rps: u32, private_rps: u32, burst: u32) -> Self {
1396        Self {
1397            public_semaphore: Semaphore::new(burst as usize),
1398            private_semaphore: Semaphore::new(burst as usize),
1399            public_interval: Duration::from_millis(1000 / public_rps.max(1) as u64),
1400            private_interval: Duration::from_millis(1000 / private_rps.max(1) as u64),
1401            last_public: AtomicU64::new(0),
1402            last_private: AtomicU64::new(0),
1403        }
1404    }
1405
1406    /// Wait for public rate limit
1407    pub async fn wait_public(&self) {
1408        let _permit = self.public_semaphore.acquire().await.unwrap();
1409        self.wait_interval(&self.last_public, self.public_interval).await;
1410    }
1411
1412    /// Wait for private rate limit
1413    pub async fn wait_private(&self) {
1414        let _permit = self.private_semaphore.acquire().await.unwrap();
1415        self.wait_interval(&self.last_private, self.private_interval).await;
1416    }
1417
1418    async fn wait_interval(&self, last: &AtomicU64, interval: Duration) {
1419        let now = Instant::now().elapsed().as_millis() as u64;
1420        let last_time = last.load(Ordering::Relaxed);
1421        let elapsed = now.saturating_sub(last_time);
1422
1423        if elapsed < interval.as_millis() as u64 {
1424            let wait_time = interval.as_millis() as u64 - elapsed;
1425            tokio::time::sleep(Duration::from_millis(wait_time)).await;
1426        }
1427
1428        last.store(Instant::now().elapsed().as_millis() as u64, Ordering::Relaxed);
1429    }
1430}
1431
1432/// Metrics collector for gateway operations
1433pub struct GatewayMetrics {
1434    pub requests_total: Counter,
1435    pub cache_hits: Counter,
1436    pub cache_misses: Counter,
1437    pub errors_total: Counter,
1438}
1439
1440impl GatewayMetrics {
1441    pub fn new() -> Self {
1442        Self {
1443            requests_total: Counter::new(),
1444            cache_hits: Counter::new(),
1445            cache_misses: Counter::new(),
1446            errors_total: Counter::new(),
1447        }
1448    }
1449}
1450
1451impl Default for GatewayMetrics {
1452    fn default() -> Self {
1453        Self::new()
1454    }
1455}
1456
1457/// Simple atomic counter
1458pub struct Counter {
1459    value: AtomicU64,
1460}
1461
1462impl Counter {
1463    pub fn new() -> Self {
1464        Self {
1465            value: AtomicU64::new(0),
1466        }
1467    }
1468
1469    pub fn increment(&self, n: u64) {
1470        self.value.fetch_add(n, Ordering::Relaxed);
1471    }
1472
1473    pub fn get(&self) -> u64 {
1474        self.value.load(Ordering::Relaxed)
1475    }
1476}
1477
1478impl Default for Counter {
1479    fn default() -> Self {
1480        Self::new()
1481    }
1482}
1483"#
1484    .to_string()
1485}
1486
1487/// Generate infrastructure/cache.rs
1488pub fn infrastructure_cache(config: &ProjectConfig) -> String {
1489    let gateway = config.gateway.as_ref().unwrap();
1490    let pascal_name = to_pascal_case(&gateway.service_name);
1491
1492    format!(
1493        r#"//! Caching decorator for repository
1494
1495use std::sync::Arc;
1496use std::time::Duration;
1497use async_trait::async_trait;
1498use moka::future::Cache;
1499use rust_decimal::Decimal;
1500use tracing::debug;
1501
1502use crate::domain::{{entities::*, repository::{pascal_name}Repository}};
1503use crate::error::Result;
1504
1505/// Caching decorator for repository
1506pub struct CachedRepository {{
1507    inner: Arc<dyn {pascal_name}Repository>,
1508    asset_cache: Cache<String, Vec<AssetInfo>>,
1509    ticker_cache: Cache<String, Vec<TickerInfo>>,
1510    public_ttl: Duration,
1511    private_ttl: Duration,
1512}}
1513
1514impl CachedRepository {{
1515    pub fn new(
1516        inner: Arc<dyn {pascal_name}Repository>,
1517        public_ttl: Duration,
1518        private_ttl: Duration,
1519    ) -> Self {{
1520        Self {{
1521            inner,
1522            asset_cache: Cache::builder()
1523                .time_to_live(public_ttl)
1524                .max_capacity(100)
1525                .build(),
1526            ticker_cache: Cache::builder()
1527                .time_to_live(public_ttl)
1528                .max_capacity(1000)
1529                .build(),
1530            public_ttl,
1531            private_ttl,
1532        }}
1533    }}
1534}}
1535
1536#[async_trait]
1537impl {pascal_name}Repository for CachedRepository {{
1538    async fn get_server_time(&self) -> Result<i64> {{
1539        // Server time should not be cached
1540        self.inner.get_server_time().await
1541    }}
1542
1543    async fn get_assets(&self) -> Result<Vec<AssetInfo>> {{
1544        let cache_key = "assets".to_string();
1545
1546        if let Some(cached) = self.asset_cache.get(&cache_key).await {{
1547            debug!("Cache hit for assets");
1548            return Ok(cached);
1549        }}
1550
1551        debug!("Cache miss for assets");
1552        let result = self.inner.get_assets().await?;
1553        self.asset_cache.insert(cache_key, result.clone()).await;
1554        Ok(result)
1555    }}
1556
1557    async fn get_ticker(&self, pairs: &[String]) -> Result<Vec<TickerInfo>> {{
1558        let cache_key = pairs.join(",");
1559
1560        if let Some(cached) = self.ticker_cache.get(&cache_key).await {{
1561            debug!("Cache hit for ticker");
1562            return Ok(cached);
1563        }}
1564
1565        debug!("Cache miss for ticker");
1566        let result = self.inner.get_ticker(pairs).await?;
1567        self.ticker_cache.insert(cache_key, result.clone()).await;
1568        Ok(result)
1569    }}
1570
1571    // Private methods are not cached by default
1572    async fn get_account_balance(&self, creds: &Credentials) -> Result<Vec<Balance>> {{
1573        self.inner.get_account_balance(creds).await
1574    }}
1575
1576    async fn get_trades_history(
1577        &self,
1578        creds: &Credentials,
1579        start: Option<i64>,
1580        end: Option<i64>,
1581        limit: Option<i32>,
1582    ) -> Result<Vec<TradeInfo>> {{
1583        self.inner.get_trades_history(creds, start, end, limit).await
1584    }}
1585
1586    async fn add_order(
1587        &self,
1588        creds: &Credentials,
1589        pair: &str,
1590        side: OrderSide,
1591        order_type: OrderType,
1592        volume: Decimal,
1593        price: Option<Decimal>,
1594    ) -> Result<OrderInfo> {{
1595        self.inner.add_order(creds, pair, side, order_type, volume, price).await
1596    }}
1597
1598    async fn cancel_order(&self, creds: &Credentials, order_id: &str) -> Result<bool> {{
1599        self.inner.cancel_order(creds, order_id).await
1600    }}
1601}}
1602"#,
1603        pascal_name = pascal_name,
1604    )
1605}
1606
1607/// Generate presentation/mod.rs
1608pub fn presentation_mod(_config: &ProjectConfig) -> String {
1609    r#"//! Presentation layer - gRPC service implementation
1610
1611pub mod grpc;
1612
1613pub use grpc::*;
1614"#
1615    .to_string()
1616}
1617
1618/// Generate presentation/grpc.rs
1619pub fn presentation_grpc(config: &ProjectConfig) -> String {
1620    let gateway = config.gateway.as_ref().unwrap();
1621    let service_name = &gateway.service_name;
1622    let pascal_name = to_pascal_case(service_name);
1623
1624    format!(
1625        r#"//! gRPC service implementation
1626
1627use std::sync::Arc;
1628use tonic::{{Request, Response, Status}};
1629use tracing::instrument;
1630
1631use crate::application::{pascal_name}ServiceTrait;
1632use crate::domain::entities::{{Credentials, OrderSide, OrderType}};
1633use crate::generated::{{
1634    {service_name}_service_server::{pascal_name}Service as GrpcServiceTrait,
1635    *,
1636}};
1637
1638/// gRPC service implementation
1639pub struct {pascal_name}GrpcService {{
1640    service: Arc<dyn {pascal_name}ServiceTrait>,
1641}}
1642
1643impl {pascal_name}GrpcService {{
1644    pub fn new(service: Arc<dyn {pascal_name}ServiceTrait>) -> Self {{
1645        Self {{ service }}
1646    }}
1647
1648    fn extract_credentials(creds: Option<crate::generated::Credentials>) -> Result<Credentials, Status> {{
1649        let c = creds.ok_or_else(|| Status::unauthenticated("Missing credentials"))?;
1650        Ok(Credentials {{
1651            api_key: c.api_key,
1652            api_secret: c.api_secret,
1653        }})
1654    }}
1655}}
1656
1657#[tonic::async_trait]
1658impl GrpcServiceTrait for {pascal_name}GrpcService {{
1659    #[instrument(skip(self))]
1660    async fn get_server_time(
1661        &self,
1662        _request: Request<GetServerTimeRequest>,
1663    ) -> Result<Response<GetServerTimeResponse>, Status> {{
1664        let time = self.service.get_server_time().await.map_err(Status::from)?;
1665        Ok(Response::new(GetServerTimeResponse {{ server_time: time }}))
1666    }}
1667
1668    #[instrument(skip(self))]
1669    async fn get_assets(
1670        &self,
1671        _request: Request<GetAssetsRequest>,
1672    ) -> Result<Response<GetAssetsResponse>, Status> {{
1673        let assets = self.service.get_assets().await.map_err(Status::from)?;
1674
1675        let assets_map = assets
1676            .into_iter()
1677            .map(|a| {{
1678                (
1679                    a.symbol.clone(),
1680                    AssetInfo {{
1681                        symbol: a.symbol,
1682                        name: a.name,
1683                        decimals: a.decimals,
1684                    }},
1685                )
1686            }})
1687            .collect();
1688
1689        Ok(Response::new(GetAssetsResponse {{ assets: assets_map }}))
1690    }}
1691
1692    #[instrument(skip(self))]
1693    async fn get_ticker(
1694        &self,
1695        request: Request<GetTickerRequest>,
1696    ) -> Result<Response<GetTickerResponse>, Status> {{
1697        let pairs = request.into_inner().pairs;
1698        let tickers = self.service.get_ticker(&pairs).await.map_err(Status::from)?;
1699
1700        let tickers_map = tickers
1701            .into_iter()
1702            .map(|t| {{
1703                (
1704                    t.pair.clone(),
1705                    crate::generated::TickerInfo {{
1706                        pair: t.pair,
1707                        last_price: t.last_price.to_string(),
1708                        bid: t.bid.to_string(),
1709                        ask: t.ask.to_string(),
1710                        volume_24h: t.volume_24h.to_string(),
1711                    }},
1712                )
1713            }})
1714            .collect();
1715
1716        Ok(Response::new(GetTickerResponse {{ tickers: tickers_map }}))
1717    }}
1718
1719    #[instrument(skip(self))]
1720    async fn get_account_balance(
1721        &self,
1722        request: Request<GetAccountBalanceRequest>,
1723    ) -> Result<Response<GetAccountBalanceResponse>, Status> {{
1724        let creds = Self::extract_credentials(request.into_inner().credentials)?;
1725        let balances = self.service.get_account_balance(&creds).await.map_err(Status::from)?;
1726
1727        let balances_map = balances
1728            .into_iter()
1729            .map(|b| (b.asset, b.free.to_string()))
1730            .collect();
1731
1732        Ok(Response::new(GetAccountBalanceResponse {{ balances: balances_map }}))
1733    }}
1734
1735    #[instrument(skip(self))]
1736    async fn get_trades_history(
1737        &self,
1738        request: Request<GetTradesHistoryRequest>,
1739    ) -> Result<Response<GetTradesHistoryResponse>, Status> {{
1740        let req = request.into_inner();
1741        let creds = Self::extract_credentials(req.credentials)?;
1742
1743        let start = req.start_time.and_then(|s| s.parse().ok());
1744        let end = req.end_time.and_then(|s| s.parse().ok());
1745        let limit = req.limit;
1746
1747        let trades = self.service
1748            .get_trades_history(&creds, start, end, limit)
1749            .await
1750            .map_err(Status::from)?;
1751
1752        let trades_proto = trades
1753            .into_iter()
1754            .map(|t| crate::generated::TradeInfo {{
1755                id: t.id,
1756                pair: t.pair,
1757                side: t.side.to_string(),
1758                price: t.price.to_string(),
1759                volume: t.volume.to_string(),
1760                fee: t.fee.to_string(),
1761                timestamp: t.timestamp,
1762            }})
1763            .collect();
1764
1765        Ok(Response::new(GetTradesHistoryResponse {{ trades: trades_proto }}))
1766    }}
1767
1768    #[instrument(skip(self))]
1769    async fn add_order(
1770        &self,
1771        request: Request<AddOrderRequest>,
1772    ) -> Result<Response<AddOrderResponse>, Status> {{
1773        let req = request.into_inner();
1774        let creds = Self::extract_credentials(req.credentials)?;
1775
1776        let side: OrderSide = req.side.parse()
1777            .map_err(|_| Status::invalid_argument("Invalid order side"))?;
1778        let order_type: OrderType = req.order_type.parse()
1779            .map_err(|_| Status::invalid_argument("Invalid order type"))?;
1780        let volume = req.volume.parse()
1781            .map_err(|_| Status::invalid_argument("Invalid volume"))?;
1782        let price = req.price.map(|p| p.parse()).transpose()
1783            .map_err(|_| Status::invalid_argument("Invalid price"))?;
1784
1785        let order = self.service
1786            .add_order(&creds, &req.pair, side, order_type, volume, price)
1787            .await
1788            .map_err(Status::from)?;
1789
1790        Ok(Response::new(AddOrderResponse {{
1791            order_id: order.id,
1792            status: format!("{{:?}}", order.status),
1793        }}))
1794    }}
1795
1796    #[instrument(skip(self))]
1797    async fn cancel_order(
1798        &self,
1799        request: Request<CancelOrderRequest>,
1800    ) -> Result<Response<CancelOrderResponse>, Status> {{
1801        let req = request.into_inner();
1802        let creds = Self::extract_credentials(req.credentials)?;
1803
1804        let success = self.service
1805            .cancel_order(&creds, &req.order_id)
1806            .await
1807            .map_err(Status::from)?;
1808
1809        Ok(Response::new(CancelOrderResponse {{ success }}))
1810    }}
1811
1812    #[instrument(skip(self))]
1813    async fn health_check(
1814        &self,
1815        _request: Request<HealthCheckRequest>,
1816    ) -> Result<Response<HealthCheckResponse>, Status> {{
1817        // Simple health check - try to get server time
1818        match self.service.get_server_time().await {{
1819            Ok(_) => Ok(Response::new(HealthCheckResponse {{
1820                healthy: true,
1821                status: "healthy".to_string(),
1822            }})),
1823            Err(e) => Ok(Response::new(HealthCheckResponse {{
1824                healthy: false,
1825                status: format!("unhealthy: {{}}", e),
1826            }})),
1827        }}
1828    }}
1829}}
1830"#,
1831        service_name = service_name,
1832        pascal_name = pascal_name,
1833    )
1834}
1835
1836/// Generate .env.example
1837pub fn env_example(config: &ProjectConfig) -> String {
1838    let gateway = config.gateway.as_ref().unwrap();
1839    let upper_name = gateway.service_name.to_uppercase();
1840
1841    format!(
1842        r#"# Server Configuration
1843{upper_name}_GATEWAY_PORT={grpc_port}
1844{upper_name}_HEALTH_PORT={health_port}
1845{upper_name}_METRICS_PORT={metrics_port}
1846
1847# API Configuration
1848{upper_name}_API_URL={api_base_url}
1849{upper_name}_API_TIMEOUT_SECONDS=30
1850
1851# Rate Limiting
1852{upper_name}_RATE_LIMIT_PUBLIC_RPS={public_rps}
1853{upper_name}_RATE_LIMIT_PRIVATE_RPS={private_rps}
1854{upper_name}_RATE_LIMIT_BURST={burst}
1855
1856# Cache Configuration
1857CACHE_ENABLED=true
1858CACHE_PUBLIC_TTL_SECONDS={public_ttl}
1859CACHE_PRIVATE_TTL_SECONDS={private_ttl}
1860
1861# Observability
1862RUST_LOG=info
1863"#,
1864        upper_name = upper_name,
1865        grpc_port = gateway.server.grpc_port,
1866        health_port = gateway.server.health_port,
1867        metrics_port = gateway.server.metrics_port,
1868        api_base_url = gateway.api_base_url,
1869        public_rps = gateway.rate_limit.public_rps,
1870        private_rps = gateway.rate_limit.private_rps,
1871        burst = gateway.rate_limit.burst,
1872        public_ttl = gateway.cache.public_ttl_secs,
1873        private_ttl = gateway.cache.private_ttl_secs,
1874    )
1875}
1876
1877/// Generate Dockerfile
1878pub fn dockerfile(config: &ProjectConfig) -> String {
1879    format!(
1880        r#"FROM rust:1.86 AS builder
1881WORKDIR /app
1882COPY . .
1883RUN apt-get update && apt-get install -y protobuf-compiler
1884RUN cargo build --release
1885
1886FROM debian:bookworm-slim
1887RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
1888COPY --from=builder /app/target/release/{name} /usr/local/bin/
1889EXPOSE 8080 8081 9090
1890CMD ["{name}"]
1891"#,
1892        name = config.name,
1893    )
1894}
1895
1896/// Generate README.md
1897pub fn readme(config: &ProjectConfig) -> String {
1898    let gateway = config.gateway.as_ref().unwrap();
1899
1900    format!(
1901        r#"# {display_name}
1902
1903A gRPC gateway service wrapping the {display_name} API with built-in resilience, caching, and observability.
1904
1905## Features
1906
1907- **gRPC API**: Full gRPC service with proto definitions
1908- **Rate Limiting**: Configurable rate limits for public and private endpoints
1909- **Caching**: In-memory caching with configurable TTLs
1910- **Resilience**: Built-in retry, circuit breaker patterns
1911- **Observability**: Tracing and metrics support
1912
1913## Quick Start
1914
1915```bash
1916# Copy environment template
1917cp .env.example .env
1918
1919# Build and run
1920cargo run
1921
1922# Or with Docker
1923docker build -t {name} .
1924docker run -p 8080:8080 -p 8081:8081 -p 9090:9090 {name}
1925```
1926
1927## Configuration
1928
1929See `.env.example` for all available configuration options.
1930
1931## Ports
1932
1933| Port | Purpose |
1934|------|---------|
1935| {grpc_port} | gRPC server |
1936| {health_port} | Health check |
1937| {metrics_port} | Prometheus metrics |
1938
1939## Generated with AllFrame
1940
1941This project was generated using [AllFrame](https://github.com/all-source-os/all-frame).
1942
1943```bash
1944allframe ignite {name} --archetype gateway
1945```
1946"#,
1947        name = config.name,
1948        display_name = gateway.display_name,
1949        grpc_port = gateway.server.grpc_port,
1950        health_port = gateway.server.health_port,
1951        metrics_port = gateway.server.metrics_port,
1952    )
1953}
1954
1955/// Generate .gitignore
1956pub fn gitignore() -> String {
1957    r#"/target
1958Cargo.lock
1959.env
1960*.log
1961"#
1962    .to_string()
1963}
1964
1965/// Convert snake_case to PascalCase
1966fn to_pascal_case(s: &str) -> String {
1967    s.split('_')
1968        .map(|word| {
1969            let mut chars = word.chars();
1970            match chars.next() {
1971                Some(c) => c.to_uppercase().chain(chars).collect::<String>(),
1972                None => String::new(),
1973            }
1974        })
1975        .collect()
1976}
1977
1978#[cfg(test)]
1979mod tests {
1980    use super::*;
1981
1982    #[test]
1983    fn test_to_pascal_case() {
1984        assert_eq!(to_pascal_case("kraken"), "Kraken");
1985        assert_eq!(to_pascal_case("my_exchange"), "MyExchange");
1986        assert_eq!(to_pascal_case("api_gateway_service"), "ApiGatewayService");
1987    }
1988}