@@ -2447,6 +2447,8 @@ class RustTranspiler {
24472447 ] . join ( '\n' ) ;
24482448 overwriteFile ( libFile , libBody ) ;
24492449
2450+ this . transpileExamples ( force ) ;
2451+
24502452 log . bright . green ( 'Rust transpilation complete.' ) ;
24512453 }
24522454
@@ -2457,6 +2459,221 @@ class RustTranspiler {
24572459 const { allModules } = await this . transpileDerivedExchangeFiles ( './js/src/pro' , rustPro , force ) ;
24582460 this . exportRustModules ( './rust/src/pro/mod.rs' , allModules ) ;
24592461 }
2462+
2463+ getRustTraitNameFromExchangeId ( exchangeId : string ) {
2464+ return exchangeId . charAt ( 0 ) . toUpperCase ( ) + exchangeId . slice ( 1 ) ;
2465+ }
2466+
2467+ transpileExamples ( force = false ) {
2468+ const tsExamplesFolder = './examples/ts' ;
2469+ const rustExamplesFolder = './examples/rust' ;
2470+ createFolderRecursively ( rustExamplesFolder ) ;
2471+
2472+ if ( ! fs . existsSync ( tsExamplesFolder ) ) {
2473+ return ;
2474+ }
2475+
2476+ const generated : string [ ] = [ ] ;
2477+
2478+ const files = fs
2479+ . readdirSync ( tsExamplesFolder )
2480+ . filter ( ( f ) => f . endsWith ( '.ts' ) )
2481+ . sort ( ) ;
2482+ for ( const file of files ) {
2483+ const inputPath = path . join ( tsExamplesFolder , file ) ;
2484+ const outputName = path
2485+ . basename ( file , '.ts' )
2486+ . replace ( / [ ^ a - z A - Z 0 - 9 _ ] + / g, '_' )
2487+ . replace ( / ^ ( \d ) / , '_$1' )
2488+ . toLowerCase ( ) ;
2489+ const outputPath = path . join ( rustExamplesFolder , `${ outputName } .rs` ) ;
2490+
2491+ const inMtime = fs . statSync ( inputPath ) . mtime . getTime ( ) ;
2492+ const outMtime = fs . existsSync ( outputPath ) ? fs . statSync ( outputPath ) . mtime . getTime ( ) : 0 ;
2493+ if ( ! force && inMtime <= outMtime ) {
2494+ continue ;
2495+ }
2496+
2497+ const tsCode = fs . readFileSync ( inputPath , 'utf8' ) ;
2498+
2499+ const isPro = / n e w \s + c c x t \. p r o \. [ a - z A - Z 0 - 9 _ ] + \s * \( / . test ( tsCode ) ;
2500+ const exchangeMatch = / n e w \s + c c x t (?: \. p r o ) ? \. ( [ a - z A - Z 0 - 9 _ ] + ) \s * \( / . exec ( tsCode ) ;
2501+ if ( ! exchangeMatch ) {
2502+ const placeholder = [
2503+ '// AUTO-GENERATED: transpiled from TypeScript examples/' ,
2504+ `// Source: examples/ts/${ file } ` ,
2505+ '' ,
2506+ '#[tokio::main]' ,
2507+ 'async fn main() {' ,
2508+ ` println!("No exchange constructor detected in ${ file } ; generated placeholder.");` ,
2509+ '}' ,
2510+ '' ,
2511+ ] . join ( '\n' ) ;
2512+ overwriteFile ( outputPath , placeholder ) ;
2513+ fs . utimesSync ( outputPath , new Date ( ) , new Date ( inMtime ) ) ;
2514+ generated . push ( outputName ) ;
2515+ continue ;
2516+ }
2517+ const exchangeId = exchangeMatch [ 1 ] ;
2518+ const exchangeModulePath = isPro ? `./rust/src/pro/${ exchangeId } .rs` : `./rust/src/exchanges/${ exchangeId } .rs` ;
2519+ if ( ! fs . existsSync ( exchangeModulePath ) ) {
2520+ const placeholder = [
2521+ '// AUTO-GENERATED: transpiled from TypeScript examples/' ,
2522+ `// Source: examples/ts/${ file } ` ,
2523+ '' ,
2524+ '#[tokio::main]' ,
2525+ 'async fn main() {' ,
2526+ ` println!("No transpiled Rust module for exchange '${ exchangeId } ' (${ isPro ? 'pro' : 'rest' } ); generated placeholder.");` ,
2527+ '}' ,
2528+ '' ,
2529+ ] . join ( '\n' ) ;
2530+ overwriteFile ( outputPath , placeholder ) ;
2531+ fs . utimesSync ( outputPath , new Date ( ) , new Date ( inMtime ) ) ;
2532+ generated . push ( outputName ) ;
2533+ continue ;
2534+ }
2535+ let classNameKey = exchangeId ;
2536+ try {
2537+ const sourcePath = isPro ? `./js/src/pro/${ exchangeId } .js` : `./js/src/${ exchangeId } .js` ;
2538+ if ( fs . existsSync ( sourcePath ) ) {
2539+ const src = fs . readFileSync ( sourcePath , 'utf8' ) ;
2540+ const classNode = getClassNode ( src ) ;
2541+ const className = classNode . id ?. name ;
2542+ if ( className ) {
2543+ classNameKey = className ;
2544+ analyzeClassFromAst ( className , classNode ) ;
2545+ }
2546+ }
2547+ } catch ( _ ) {
2548+ // Best-effort extraction only; keep fallback.
2549+ }
2550+
2551+ const methodNames = new Set < string > ( ) ;
2552+ const exchangeVars = new Set < string > ( ) ;
2553+ let m : RegExpExecArray | null = null ;
2554+ const ctorVarRegex = / \b (?: c o n s t | l e t | v a r ) \s + ( [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * ) \s * = \s * n e w \s + c c x t (?: \. p r o ) ? \. [ a - z A - Z 0 - 9 _ ] + \s * \( / g;
2555+ while ( ( m = ctorVarRegex . exec ( tsCode ) ) !== null ) {
2556+ exchangeVars . add ( m [ 1 ] ) ;
2557+ }
2558+ if ( exchangeVars . size === 0 ) {
2559+ exchangeVars . add ( 'exchange' ) ;
2560+ }
2561+ const varsAlternation = Array . from ( exchangeVars ) . join ( '|' ) ;
2562+ const methodRegex = new RegExp ( `\\b(?:${ varsAlternation } )\\.([a-zA-Z][A-Za-z0-9_]*)\\s*\\(` , 'g' ) ;
2563+ while ( ( m = methodRegex . exec ( tsCode ) ) !== null ) {
2564+ methodNames . add ( m [ 1 ] ) ;
2565+ }
2566+
2567+ if ( methodNames . size === 0 ) {
2568+ const placeholder = [
2569+ '// AUTO-GENERATED: transpiled from TypeScript examples/' ,
2570+ `// Source: examples/ts/${ file } ` ,
2571+ '' ,
2572+ `use ${ isPro ? `ccxt::pro::${ exchangeId } ` : `ccxt::exchanges::${ exchangeId } ` } ::{${ this . getRustTraitNameFromExchangeId ( exchangeId ) } Impl};` ,
2573+ 'use ccxt::exchange::Value;' ,
2574+ 'use serde_json::json;' ,
2575+ '' ,
2576+ '#[tokio::main]' ,
2577+ 'async fn main() {' ,
2578+ ` let _exchange = ${ this . getRustTraitNameFromExchangeId ( exchangeId ) } Impl::new(Value::Json(json!({})));` ,
2579+ ` println!("No exchange method calls detected in ${ file } ; generated placeholder.");` ,
2580+ '}' ,
2581+ '' ,
2582+ ] . join ( '\n' ) ;
2583+ overwriteFile ( outputPath , placeholder ) ;
2584+ fs . utimesSync ( outputPath , new Date ( ) , new Date ( inMtime ) ) ;
2585+ generated . push ( outputName ) ;
2586+ continue ;
2587+ }
2588+
2589+ const symbolMatch = / c o n s t \s + s y m b o l \s * = \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / . exec ( tsCode ) ;
2590+ const symbol = symbolMatch ?. [ 1 ] || 'BTC/USDT' ;
2591+
2592+ const traitName = this . getRustTraitNameFromExchangeId ( exchangeId ) ;
2593+ const importPath = isPro ? `ccxt::pro::${ exchangeId } ` : `ccxt::exchanges::${ exchangeId } ` ;
2594+ const classFns = {
2595+ ...( FUNCTION_INFO [ 'Exchange' ] || { } ) ,
2596+ ...( FUNCTION_INFO [ classNameKey ] || { } ) ,
2597+ ...( FUNCTION_INFO [ traitName ] || { } ) ,
2598+ ...( FUNCTION_INFO [ exchangeId ] || { } ) ,
2599+ } ;
2600+ const exampleBody : string [ ] = [ ] ;
2601+ exampleBody . push ( '// AUTO-GENERATED: transpiled from TypeScript examples/' ) ;
2602+ exampleBody . push ( `// Source: examples/ts/${ file } ` ) ;
2603+ exampleBody . push ( '' ) ;
2604+ exampleBody . push ( 'use ccxt::exchange::{normalize, Value};' ) ;
2605+ exampleBody . push ( `use ${ importPath } ::{${ traitName } , ${ traitName } Impl};` ) ;
2606+ exampleBody . push ( 'use serde_json::json;' ) ;
2607+ exampleBody . push ( '' ) ;
2608+ exampleBody . push ( '#[tokio::main]' ) ;
2609+ exampleBody . push ( 'async fn main() {' ) ;
2610+ exampleBody . push ( ` let mut exchange = ${ traitName } Impl::new(Value::Json(json!({})));` ) ;
2611+ exampleBody . push ( ` let symbol: Value = "${ symbol } ".into();` ) ;
2612+ exampleBody . push ( '' ) ;
2613+
2614+ const orderedMethods = Array . from ( methodNames ) . sort ( ) ;
2615+ for ( const method of orderedMethods ) {
2616+ const fnInfo = classFns [ method ] ;
2617+ if ( ! fnInfo ) {
2618+ exampleBody . push ( ` // skipped: ${ method } (not found in transpiled trait)` ) ;
2619+ continue ;
2620+ }
2621+ const rustName = unCamelCase ( method ) ;
2622+ const args : string [ ] = [ ] ;
2623+ const needsSymbol =
2624+ / ^ ( f e t c h | w a t c h | c r e a t e | c a n c e l | e d i t | p a r s e ) / . test ( method ) ||
2625+ method . toLowerCase ( ) . includes ( 'ticker' ) ||
2626+ method . toLowerCase ( ) . includes ( 'orderbook' ) ||
2627+ method . toLowerCase ( ) . includes ( 'ohlcv' ) ||
2628+ method . toLowerCase ( ) . includes ( 'trade' ) ;
2629+ for ( let i = 0 ; i < fnInfo . paramsCount ; i ++ ) {
2630+ if ( i === 0 && needsSymbol ) {
2631+ args . push ( 'symbol.clone()' ) ;
2632+ } else {
2633+ args . push ( 'Value::Undefined' ) ;
2634+ }
2635+ }
2636+ const argsExpr = args . length > 0 ? `, ${ args . join ( ', ' ) } ` : '' ;
2637+ if ( fnInfo . async ) {
2638+ exampleBody . push ( ` let rv = ${ traitName } ::${ rustName } (&mut exchange${ argsExpr } ).await;` ) ;
2639+ } else {
2640+ exampleBody . push ( ` let rv = ${ traitName } ::${ rustName } (&mut exchange${ argsExpr } );` ) ;
2641+ }
2642+ exampleBody . push (
2643+ ` println!("${ method } : {}", normalize(&rv).map(|v| v.to_string()).unwrap_or_else(|| "undefined".into()));`
2644+ ) ;
2645+ }
2646+
2647+ exampleBody . push ( '}' ) ;
2648+ exampleBody . push ( '' ) ;
2649+ overwriteFile ( outputPath , exampleBody . join ( '\n' ) ) ;
2650+ fs . utimesSync ( outputPath , new Date ( ) , new Date ( inMtime ) ) ;
2651+ generated . push ( outputName ) ;
2652+ }
2653+
2654+ if ( generated . length > 0 ) {
2655+ this . updateCargoExamples ( generated ) ;
2656+ log . cyan ( `Transpiled Rust examples: ${ generated . length } ` ) ;
2657+ }
2658+ }
2659+
2660+ updateCargoExamples ( exampleNames : string [ ] ) {
2661+ const cargoPath = './rust/Cargo.toml' ;
2662+ if ( ! fs . existsSync ( cargoPath ) ) return ;
2663+ const cargo = fs . readFileSync ( cargoPath , 'utf8' ) ;
2664+ const startMarker = '# AUTO-GENERATED RUST EXAMPLES START' ;
2665+ const endMarker = '# AUTO-GENERATED RUST EXAMPLES END' ;
2666+ const block = [
2667+ startMarker ,
2668+ ...exampleNames
2669+ . sort ( )
2670+ . map ( ( name ) => `[[example]]\nname = "${ name } "\npath = "../examples/rust/${ name } .rs"` ) ,
2671+ endMarker ,
2672+ ] . join ( '\n\n' ) ;
2673+ const markerRegex = new RegExp ( `${ startMarker } [\\s\\S]*${ endMarker } ` , 'm' ) ;
2674+ const next = markerRegex . test ( cargo ) ? cargo . replace ( markerRegex , block ) : `${ cargo . trimEnd ( ) } \n\n${ block } \n` ;
2675+ overwriteFile ( cargoPath , next ) ;
2676+ }
24602677}
24612678
24622679if ( process . argv [ 1 ] && process . argv [ 1 ] . includes ( 'rustTranspiler' ) ) {
0 commit comments