| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| // Context parameters are typed as `any` because the generated grammar files |
| // require accepting arbitrary context types at runtime. |
| |
| import TranslateVisitor from './TranslateVisitor.js'; |
| import { TranslatorException } from './TranslatorException.js'; |
| |
| /** |
| * Converts a Gremlin traversal string into a Python source code representation. |
| * Assumes use of the `uuid` and `datetime` standard library modules. |
| */ |
| export default class PythonTranslateVisitor extends TranslateVisitor { |
| |
| constructor(graphTraversalSourceName: string = 'g') { |
| super(graphTraversalSourceName); |
| } |
| |
| protected override processGremlinSymbol(step: string): string { |
| return SymbolHelper.toPython(step); |
| } |
| |
| protected override getCardinalityFunctionClass(): string { |
| return 'CardinalityValue'; |
| } |
| |
| protected override handleStringLiteralText(text: string): void { |
| this.sb.push("'"); |
| this.sb.push(text); |
| this.sb.push("'"); |
| } |
| |
| visitBooleanLiteral(ctx: any): void { |
| const text: string = ctx.getText(); |
| this.sb.push(text.charAt(0).toUpperCase() + text.slice(1)); |
| } |
| |
| visitNullLiteral(_ctx: any): void { |
| this.sb.push('None'); |
| } |
| |
| visitStringNullableLiteral(ctx: any): void { |
| if (ctx.getText() === 'null') { |
| this.sb.push('None'); |
| } else { |
| const text = TranslateVisitor.removeFirstAndLastCharacters(ctx.getText()); |
| this.handleStringLiteralText(text); |
| } |
| } |
| |
| visitNanLiteral(_ctx: any): void { |
| this.sb.push("float('nan')"); |
| } |
| |
| visitInfLiteral(ctx: any): void { |
| if (ctx.SignedInfLiteral() != null && ctx.SignedInfLiteral().getText() === '-Infinity') { |
| this.sb.push("float('-inf')"); |
| } else { |
| this.sb.push("float('inf')"); |
| } |
| } |
| |
| visitIntegerLiteral(ctx: any): void { |
| const integerLiteral: string = ctx.getText().toLowerCase(); |
| const lastChar = integerLiteral[integerLiteral.length - 1]; |
| |
| if (!/[a-z]/.test(lastChar)) { |
| // No type suffix — check if the value exceeds 32-bit signed integer range |
| const value = BigInt(integerLiteral); |
| if (value > BigInt(2147483647) || value < BigInt(-2147483648)) { |
| this.sb.push('long('); |
| this.sb.push(integerLiteral); |
| this.sb.push(')'); |
| } else { |
| this.sb.push(integerLiteral); |
| } |
| return; |
| } |
| |
| const num = integerLiteral.slice(0, -1); |
| switch (lastChar) { |
| case 'b': |
| case 's': |
| case 'i': |
| this.sb.push(num); |
| break; |
| case 'n': |
| this.sb.push('bigint('); |
| this.sb.push(num); |
| this.sb.push(')'); |
| break; |
| case 'l': |
| this.sb.push('long('); |
| this.sb.push(num); |
| this.sb.push(')'); |
| break; |
| default: |
| this.sb.push(integerLiteral); |
| break; |
| } |
| } |
| |
| visitFloatLiteral(ctx: any): void { |
| if (ctx.infLiteral() != null) { this.visit(ctx.infLiteral()); return; } |
| if (ctx.nanLiteral() != null) { this.visit(ctx.nanLiteral()); return; } |
| |
| const floatLiteral: string = ctx.getText().toLowerCase(); |
| const lastChar = floatLiteral[floatLiteral.length - 1]; |
| const num = floatLiteral.slice(0, -1); |
| |
| switch (lastChar) { |
| case 'f': |
| case 'd': |
| this.sb.push(num); |
| break; |
| case 'm': |
| this.sb.push('bigdecimal('); |
| this.sb.push(num); |
| this.sb.push(')'); |
| break; |
| default: |
| this.sb.push(floatLiteral); |
| break; |
| } |
| } |
| |
| visitDateLiteral(ctx: any): void { |
| const dtString: string = ctx.getChild(2).getText(); |
| const inner = TranslateVisitor.removeFirstAndLastCharacters(dtString); |
| this.sb.push("datetime.datetime.fromisoformat('"); |
| this.sb.push(formatDatetimePythonStyle(inner)); |
| this.sb.push("')"); |
| } |
| |
| visitGenericRangeLiteral(_ctx: any): void { |
| throw new TranslatorException('Python does not support range literals'); |
| } |
| |
| visitGenericSetLiteral(ctx: any): void { |
| const literals: any[] = ctx.genericLiteral(); |
| if (literals.length === 0) { |
| this.sb.push('set()'); |
| return; |
| } |
| this.sb.push('{'); |
| for (let i = 0; i < literals.length; i++) { |
| this.visit(literals[i]); |
| if (i < literals.length - 1) { |
| this.sb.push(', '); |
| } |
| } |
| this.sb.push('}'); |
| } |
| |
| visitGenericCollectionLiteral(ctx: any): void { |
| this.sb.push('['); |
| const literals: any[] = ctx.genericLiteral(); |
| for (let i = 0; i < literals.length; i++) { |
| this.visit(literals[i]); |
| if (i < literals.length - 1) { |
| this.sb.push(', '); |
| } |
| } |
| this.sb.push(']'); |
| } |
| |
| visitGenericMapLiteral(ctx: any): void { |
| this.sb.push('{ '); |
| const entries: any[] = ctx.mapEntry(); |
| for (let i = 0; i < entries.length; i++) { |
| this.visit(entries[i]); |
| if (i < entries.length - 1) { |
| this.sb.push(', '); |
| } |
| } |
| this.sb.push(' }'); |
| } |
| |
| visitMapEntry(ctx: any): void { |
| this.visit(ctx.mapKey()); |
| this.sb.push(': '); |
| this.visit(ctx.genericLiteral()); |
| } |
| |
| visitMapKey(ctx: any): void { |
| const keyIndex = (ctx.LPAREN() != null && ctx.RPAREN() != null) ? 1 : 0; |
| this.visit(ctx.getChild(keyIndex)); |
| } |
| |
| visitUuidLiteral(ctx: any): void { |
| if (ctx.stringLiteral() == null) { |
| this.sb.push('uuid.uuid4()'); |
| return; |
| } |
| this.sb.push('uuid.UUID('); |
| this.visitStringLiteral(ctx.stringLiteral()); |
| this.sb.push(')'); |
| } |
| |
| visitCharacterLiteral(ctx: any): void { |
| const text: string = ctx.getText(); |
| const withoutSuffix = text.substring(0, text.length - 1); |
| const inner = TranslateVisitor.removeFirstAndLastCharacters(withoutSuffix); |
| this.sb.push("SingleChar('"); |
| this.sb.push(inner); |
| this.sb.push("')"); |
| } |
| |
| visitDurationLiteral(ctx: any): void { |
| const seconds = parseInt(ctx.integerLiteral(0).getText(), 10); |
| const nanos = parseInt(ctx.integerLiteral(1).getText(), 10); |
| const isPositive = ctx.booleanLiteral() === null || |
| ctx.booleanLiteral().getText() === 'true'; |
| const totalSeconds = isPositive ? seconds : -seconds; |
| // Python's timedelta has microsecond resolution; sub-microsecond nanos are truncated |
| const micros = Math.floor(nanos / 1000); |
| if (micros === 0) { |
| this.sb.push(`timedelta(seconds=${totalSeconds})`); |
| } else { |
| const totalMicros = isPositive ? micros : -micros; |
| this.sb.push(`timedelta(seconds=${totalSeconds},microseconds=${totalMicros})`); |
| } |
| } |
| |
| visitBinaryLiteral(ctx: any): void { |
| this.sb.push('base64.b64decode('); |
| this.visitStringLiteral(ctx.stringLiteral()); |
| this.sb.push(')'); |
| } |
| |
| visitTraversalStrategy(ctx: any): void { |
| if (ctx.getChildCount() === 1) { |
| this.sb.push(ctx.getText()); |
| this.sb.push('()'); |
| } else { |
| const firstText = ctx.getChild(0).getText(); |
| const className = firstText === 'new' ? ctx.getChild(1).getText() : firstText; |
| this.sb.push(className); |
| this.sb.push('('); |
| |
| const configs: any[] = ctx.configuration(); |
| for (let ix = 0; ix < configs.length; ix++) { |
| this.visit(configs[ix]); |
| if (ix < configs.length - 1) { |
| this.sb.push(', '); |
| } |
| } |
| |
| this.sb.push(')'); |
| } |
| } |
| |
| visitConfiguration(ctx: any): void { |
| const keyText: string = ctx.keyword() != null ? ctx.keyword().getText() : ctx.nakedKey().getText(); |
| this.sb.push(SymbolHelper.toPython(keyText)); |
| this.sb.push('='); |
| this.visit(ctx.genericArgument()); |
| } |
| } |
| |
| /** |
| * Formats a datetime string for Python's datetime.fromisoformat(): |
| * - Truncates seconds if both seconds and milliseconds are 0 |
| * - Uses '+00:00' for UTC (Python < 3.11 does not support 'Z') |
| * - Assumes UTC when no offset is specified |
| */ |
| function formatDatetimePythonStyle(s: string): string { |
| const d = new Date(s); |
| if (isNaN(d.getTime())) { |
| throw new TranslatorException(`Invalid datetime: ${s}`); |
| } |
| const year = d.getUTCFullYear().toString().padStart(4, '0'); |
| const month = (d.getUTCMonth() + 1).toString().padStart(2, '0'); |
| const day = d.getUTCDate().toString().padStart(2, '0'); |
| const hours = d.getUTCHours().toString().padStart(2, '0'); |
| const minutes = d.getUTCMinutes().toString().padStart(2, '0'); |
| const seconds = d.getUTCSeconds(); |
| const millis = d.getUTCMilliseconds(); |
| |
| let result = `${year}-${month}-${day}T${hours}:${minutes}`; |
| if (seconds !== 0 || millis !== 0) { |
| result += ':' + seconds.toString().padStart(2, '0'); |
| if (millis !== 0) { |
| result += '.' + millis.toString().padStart(3, '0').replace(/0+$/, ''); |
| } |
| } |
| result += '+00:00'; |
| return result; |
| } |
| |
| class SymbolHelper { |
| private static readonly TO_PYTHON_MAP: Record<string, string> = { |
| 'global': 'global_', |
| 'all': 'all_', |
| 'and': 'and_', |
| 'any': 'any_', |
| 'as': 'as_', |
| 'filter': 'filter_', |
| 'format': 'format_', |
| 'from': 'from_', |
| 'id': 'id_', |
| 'in': 'in_', |
| 'is': 'is_', |
| 'list': 'list_', |
| 'max': 'max_', |
| 'min': 'min_', |
| 'or': 'or_', |
| 'not': 'not_', |
| 'range': 'range_', |
| 'set': 'set_', |
| 'sum': 'sum_', |
| 'with': 'with_', |
| }; |
| |
| static toPython(symbol: string): string { |
| return SymbolHelper.TO_PYTHON_MAP[symbol] ?? convertCamelCaseToSnakeCase(symbol); |
| } |
| } |
| |
| function convertCamelCaseToSnakeCase(camelCase: string): string { |
| if (!camelCase) return camelCase; |
| // skip if this is a class/enum indicated by the first letter being upper case |
| if (camelCase.charAt(0) === camelCase.charAt(0).toUpperCase() && /[A-Z]/.test(camelCase.charAt(0))) { |
| return camelCase; |
| } |
| let result = ''; |
| for (const c of camelCase) { |
| if (c >= 'A' && c <= 'Z') { |
| result += '_' + c.toLowerCase(); |
| } else { |
| result += c; |
| } |
| } |
| return result; |
| } |
| |
| |