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