blob: c95cc6de3255fd707f62d11810872e4227bdfe68 [file]
/*
* 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';
import { Buffer } from 'buffer';
const GO_PACKAGE_NAME = 'gremlingo.';
const STRATEGY_WITH_MAP_OPTS = new Set([
'OptionsStrategy',
'ReferenceElementStrategy', 'ComputerFinalizationStrategy', 'ProfileStrategy',
'ComputerVerificationStrategy', 'StandardVerificationStrategy', 'VertexProgramRestrictionStrategy',
'MessagePassingReductionStrategy',
]);
const STRATEGY_WITH_STRING_SLICE = new Set([
'ReservedKeysVerificationStrategy', 'ProductiveByStrategy',
]);
/**
* Converts a Gremlin traversal string into a Go source code representation.
* Assumes use of the gremlingo, math, time, and uuid packages.
*/
export default class GoTranslateVisitor extends TranslateVisitor {
constructor(graphTraversalSourceName: string = 'g') {
super(graphTraversalSourceName);
}
protected override processGremlinSymbol(step: string): string {
return SymbolHelper.toGo(step);
}
protected override getCardinalityFunctionClass(): string {
return 'CardinalityValue';
}
protected override appendExplicitNaming(txt: string, prefix: string): void {
this.sb.push(GO_PACKAGE_NAME);
super.appendExplicitNaming(txt, prefix);
}
protected override appendAnonymousSpawn(): void {
this.sb.push(GO_PACKAGE_NAME + 'T__.');
}
protected override visitP(ctx: any, pClass: string, methodName: string): void {
this.sb.push(GO_PACKAGE_NAME);
super.visitP(ctx, pClass, methodName);
}
visitTraversalGType(ctx: any): void {
const parts: string[] = ctx.getText().split('.');
this.sb.push(GO_PACKAGE_NAME);
this.sb.push(this.processGremlinSymbol(parts[0]));
this.sb.push('.');
this.sb.push(this.processGremlinSymbol(parts[1].toLowerCase()));
}
visitNanLiteral(_ctx: any): void {
this.sb.push('math.NaN()');
}
visitInfLiteral(ctx: any): void {
if (ctx.SignedInfLiteral() != null && ctx.SignedInfLiteral().getText() === '-Infinity') {
this.sb.push('math.Inf(-1)');
} else {
this.sb.push('math.Inf(1)');
}
}
visitNullLiteral(_ctx: any): void {
this.sb.push('nil');
}
visitStringNullableLiteral(ctx: any): void {
if (ctx.getText() === 'null') {
this.sb.push('nil');
} else {
const text = TranslateVisitor.removeFirstAndLastCharacters(ctx.getText());
this.handleStringLiteralText(text);
}
}
visitIntegerLiteral(ctx: any): void {
const integerLiteral: string = ctx.getText().toLowerCase();
const lastChar = integerLiteral[integerLiteral.length - 1];
const num = integerLiteral.slice(0, -1);
if (!/[a-z]/.test(lastChar)) {
this.sb.push(integerLiteral);
return;
}
switch (lastChar) {
case 'b':
this.sb.push('int8(');
this.sb.push(num);
this.sb.push(')');
break;
case 's':
this.sb.push('int16(');
this.sb.push(num);
this.sb.push(')');
break;
case 'i':
this.sb.push('int32(');
this.sb.push(num);
this.sb.push(')');
break;
case 'l':
this.sb.push('int64(');
this.sb.push(num);
this.sb.push(')');
break;
case 'n':
this.sb.push(GO_PACKAGE_NAME + 'ParseBigInt("');
this.sb.push(num);
this.sb.push('")');
break;
default:
this.sb.push(integerLiteral);
}
}
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);
if (!/[a-z]/.test(lastChar)) {
this.sb.push(floatLiteral);
return;
}
switch (lastChar) {
case 'f':
this.sb.push('float32(');
this.sb.push(num);
this.sb.push(')');
break;
case 'd':
this.sb.push(num);
break;
case 'm':
this.sb.push(GO_PACKAGE_NAME + 'ParseBigDecimal("');
this.sb.push(num);
this.sb.push('")');
break;
default:
this.sb.push(floatLiteral);
}
}
visitDateLiteral(ctx: any): void {
const dtString: string = ctx.getChild(2).getText();
const inner = TranslateVisitor.removeFirstAndLastCharacters(dtString);
const d = new Date(inner);
if (isNaN(d.getTime())) {
throw new TranslatorException(`Invalid datetime: ${inner}`);
}
const year = d.getUTCFullYear();
const month = d.getUTCMonth() + 1;
const day = d.getUTCDate();
const hours = d.getUTCHours();
const minutes = d.getUTCMinutes();
const seconds = d.getUTCSeconds();
const nanos = d.getUTCMilliseconds() * 1_000_000;
const totalSeconds = 0; // assume UTC when no offset specified
const zoneInfo = 'UTC+00:00';
this.sb.push(`time.Date(${year}, ${month}, ${day}, ${hours}, ${minutes}, ${seconds}, ${nanos}, time.FixedZone("${zoneInfo}", ${totalSeconds}))`);
}
visitGenericRangeLiteral(_ctx: any): void {
throw new TranslatorException('Go does not support range literals');
}
visitGenericSetLiteral(ctx: any): void {
this.sb.push(GO_PACKAGE_NAME + 'NewSimpleSet(');
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(')');
}
visitGenericCollectionLiteral(ctx: any): void {
this.sb.push('[]interface{}{');
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('map[interface{}]interface{}{');
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.New()');
return;
}
this.sb.push('uuid.MustParse(');
this.visitStringLiteral(ctx.stringLiteral());
this.sb.push(')');
}
visitCharacterLiteral(_ctx: any): void {
throw new TranslatorException('Character literals are not supported in Go');
}
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';
// Use BigInt to avoid precision loss for durations over ~104 days
const totalNanos = BigInt(seconds) * 1_000_000_000n + BigInt(nanos);
this.sb.push(`time.Duration(${isPositive ? totalNanos : -totalNanos})`);
}
visitBinaryLiteral(ctx: any): void {
const base64Str = TranslateVisitor.removeFirstAndLastCharacters(ctx.stringLiteral().getText());
const bytes = base64ToBytes(base64Str);
this.sb.push(GO_PACKAGE_NAME + 'ByteBuffer{Data: []byte{');
for (let i = 0; i < bytes.length; i++) {
if (i > 0) this.sb.push(',');
this.sb.push(String(bytes[i]));
}
this.sb.push('}}');
}
visitTraversalStrategy(ctx: any): void {
if (ctx.getChildCount() === 1) {
this.sb.push(GO_PACKAGE_NAME);
this.sb.push(ctx.getText());
this.sb.push('()');
return;
}
const firstText = ctx.getChild(0).getText();
const strategyName = firstText === 'new' ? ctx.getChild(1).getText() : firstText;
this.sb.push(GO_PACKAGE_NAME);
this.sb.push(strategyName);
this.sb.push('(');
if (!STRATEGY_WITH_MAP_OPTS.has(strategyName)) {
this.sb.push(GO_PACKAGE_NAME);
this.sb.push(strategyName);
this.sb.push('Config{');
}
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(', ');
}
}
if (strategyName !== 'OptionsStrategy') {
this.sb.push('}');
}
this.sb.push(')');
}
visitConfiguration(ctx: any): void {
const parent: string = ctx.parent.getText();
const parenIdx = parent.indexOf('(');
const rawName = parenIdx > -1 ? parent.substring(0, parenIdx) : parent;
const parentName = rawName.startsWith('new') ? rawName.substring(3) : rawName;
if (STRATEGY_WITH_MAP_OPTS.has(parentName)) {
this.sb.push('map[string]interface{}{"');
this.sb.push(ctx.getChild(0).getText());
this.sb.push('": ');
this.visit(ctx.getChild(2));
this.sb.push('}');
return;
}
const key: string = ctx.getChild(0).getText();
this.sb.push(SymbolHelper.toGo(key));
this.sb.push(': ');
const startIdx = this.sb.length;
this.visit(ctx.genericArgument());
if (STRATEGY_WITH_STRING_SLICE.has(parentName)) {
// Replace []interface{} with []string if present
const idx = this.sb.indexOf('[]interface{}', startIdx);
if (idx > -1) {
this.sb[idx] = '[]string';
}
}
if (key === 'readPartitions') {
// Replace []interface{}{items} with gremlingo.NewSimpleSet(items)
const added = this.sb.splice(startIdx);
const joined = added.join('');
const transformed = joined.replace(/^\[\]interface\{\}\{([\s\S]*)\}$/, GO_PACKAGE_NAME + 'NewSimpleSet($1)');
this.sb.push(transformed);
}
}
visitTraversalSourceSelfMethod_withoutStrategies(ctx: any): void {
this.sb.push('WithoutStrategies(');
this.sb.push(GO_PACKAGE_NAME);
this.sb.push(ctx.classType().getText());
this.sb.push('()');
if (ctx.classTypeList() != null && ctx.classTypeList().classTypeExpr() != null) {
for (const classTypeCtx of ctx.classTypeList().classTypeExpr().classType()) {
this.sb.push(', ');
this.sb.push(GO_PACKAGE_NAME);
this.sb.push(classTypeCtx.getText());
this.sb.push('()');
}
}
this.sb.push(')');
}
}
/**
* Decodes a base64 string into an array of unsigned byte values.
*/
function base64ToBytes(base64: string): number[] {
if (base64 === '') return [];
const buf = Buffer.from(base64, 'base64');
const bytes: number[] = new Array(buf.length);
for (let i = 0; i < buf.length; i++) {
bytes[i] = buf[i];
}
return bytes;
}
class SymbolHelper {
private static readonly TO_GO_MAP: Record<string, string> = {
'OUT': 'Out',
'IN': 'In',
'BOTH': 'Both',
'bigdecimal': 'BigDecimal',
'bigint': 'BigInt',
'datetime': 'DateTime',
'uuid': 'UUID',
'vproperty': 'VProperty',
'WithOptions': GO_PACKAGE_NAME + 'WithOptions',
'IO': GO_PACKAGE_NAME + 'IO',
'__': GO_PACKAGE_NAME + 'T__',
};
static toGo(symbol: string): string {
if (symbol in SymbolHelper.TO_GO_MAP) {
return SymbolHelper.TO_GO_MAP[symbol];
}
// Capitalize first letter (mirrors Java StringUtils.capitalize)
if (!symbol) return symbol;
return symbol.charAt(0).toUpperCase() + symbol.slice(1);
}
}