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 reference)
1235fn _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
1375/// Generate infrastructure/rate_limiter.rs (includes metrics)
1376pub 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
1486/// Generate infrastructure/cache.rs
1487pub 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
1606/// Generate presentation/mod.rs
1607pub 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
1617/// Generate presentation/grpc.rs
1618pub 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
1835/// Generate .env.example
1836pub 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
1876/// Generate Dockerfile
1877pub 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
1895/// Generate README.md
1896pub 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
1954/// Generate .gitignore
1955pub fn gitignore() -> String {
1956    r#"/target
1957Cargo.lock
1958.env
1959*.log
1960"#
1961    .to_string()
1962}
1963
1964/// Convert snake_case to PascalCase
1965fn 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}