Skip to content

Commit 6bc90e3

Browse files
committed
Transpile examples
1 parent 0ed46e8 commit 6bc90e3

2 files changed

Lines changed: 233 additions & 0 deletions

File tree

build/rustTranspiler.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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-zA-Z0-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 = /new\s+ccxt\.pro\.[a-zA-Z0-9_]+\s*\(/.test(tsCode);
2500+
const exchangeMatch = /new\s+ccxt(?:\.pro)?\.([a-zA-Z0-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(?:const|let|var)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*new\s+ccxt(?:\.pro)?\.[a-zA-Z0-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 = /const\s+symbol\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+
/^(fetch|watch|create|cancel|edit|parse)/.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

24622679
if (process.argv[1] && process.argv[1].includes('rustTranspiler')) {

rust/tests/public_data_smoke.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,22 @@ async fn public_data_smoke_multi_exchange() {
186186
"BTC/USDT"
187187
),
188188
);
189+
assert_suite(
190+
"dydx",
191+
run_suite!(
192+
ccxt::exchanges::dydx::Dydx,
193+
ccxt::exchanges::dydx::DydxImpl,
194+
"BTC/USDC:USDC"
195+
),
196+
);
197+
assert_suite(
198+
"hyperliquid",
199+
run_suite!(
200+
ccxt::exchanges::hyperliquid::Hyperliquid,
201+
ccxt::exchanges::hyperliquid::HyperliquidImpl,
202+
"BTC/USDC:USDC"
203+
),
204+
);
189205
}
190206

191207
#[tokio::test]

0 commit comments

Comments
 (0)