blob: 6d8c09287b7db5a000e619fe62fefbae3c6ebb29 [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.
*/
import { P, TextP, EnumValue } from './traversal.js';
import { OptionsStrategy, TraversalStrategy } from './traversal-strategy.js';
import { Long } from '../utils.js';
import { Vertex } from '../structure/graph.js';
import { Buffer } from 'buffer';
export default class GremlinLang {
private gremlin: string = '';
private optionsStrategies: OptionsStrategy[] = [];
private parameters: Map<string, any> = new Map();
constructor(toClone?: GremlinLang) {
if (toClone) {
this.gremlin = toClone.gremlin;
this.optionsStrategies = [...toClone.optionsStrategies];
this.parameters = new Map(toClone.parameters);
}
}
getOptionsStrategies(): OptionsStrategy[] {
return this.optionsStrategies;
}
addG(g: string): void {
this.parameters.set('g', g);
}
getParameters(): Map<string, any> {
return this.parameters;
}
private _predicateAsString(p: P | TextP): string {
if (p.operator === 'and' || p.operator === 'or') {
return `${this._predicateAsString(p.value)}.${p.operator}(${this._predicateAsString(p.other)})`;
}
let result = p.operator + '(';
if (Array.isArray(p.value)) {
result += '[' + p.value.map(v => this._argAsString(v)).join(',') + ']';
} else {
result += this._argAsString(p.value);
if (p.other !== undefined && p.other !== null) {
result += ',' + this._argAsString(p.other);
}
}
result += ')';
return result;
}
private _argAsString(arg: any): string {
if (arg === null || arg === undefined) return 'null';
if (typeof arg === 'boolean') return arg ? 'true' : 'false';
if (arg instanceof Long) {
return String(arg.value) + 'L';
}
if (typeof arg === 'bigint') {
return String(arg) + 'N';
}
if (typeof arg === 'number') {
if (Number.isNaN(arg)) return 'NaN';
if (arg === Infinity) return '+Infinity';
if (arg === -Infinity) return '-Infinity';
if (!Number.isInteger(arg)) return String(arg) + 'D';
if (arg >= -2147483648 && arg <= 2147483647) return String(arg);
// Outside safe integer range, values have lost precision and may exceed Java Long — emit as Double.
if (arg > Number.MAX_SAFE_INTEGER || arg < -Number.MAX_SAFE_INTEGER) return String(arg) + 'D';
return String(arg) + 'L';
}
if (arg instanceof Date) {
const iso = arg.toISOString();
return `datetime("${iso}")`;
}
if (typeof arg === 'string') {
// JSON.stringify handles all special character escaping in one call.
// Strip the outer double quotes and re-wrap in single quotes for Gremlin.
const escaped = JSON.stringify(arg).slice(1, -1).replace(/'/g, "\\'");
return `'${escaped}'`;
}
if (arg instanceof P || arg instanceof TextP) {
return this._predicateAsString(arg);
}
if (arg instanceof EnumValue) {
return arg.toString();
}
if (arg instanceof TraversalStrategy && !(arg instanceof OptionsStrategy)) {
const simpleName = arg.strategyName;
const configEntries = Object.entries(arg.configuration);
if (configEntries.length === 0) {
return simpleName;
}
const configStr = configEntries.map(([k, v]) => `${k}:${this._argAsString(v)}`).join(',');
return `new ${simpleName}(${configStr})`;
}
if (typeof arg === 'function' && arg.prototype instanceof TraversalStrategy) {
return arg.name;
}
if (arg instanceof Vertex) {
return this._argAsString(arg.id);
}
if (arg instanceof GremlinLang) {
return arg.getGremlin('__');
}
if (typeof arg.getGremlinLang === 'function') {
// This is a Traversal - render as anonymous traversal
if (arg.graph != null) {
throw new Error('Child traversal must be anonymous - use __ not g');
}
return arg.getGremlinLang().getGremlin('__');
}
if (arg instanceof Uint8Array) {
return `Binary("${Buffer.from(arg.buffer, arg.byteOffset, arg.byteLength).toString('base64')}")`;
}
if (arg instanceof Set) {
const items = [...arg];
if (items.length === 0) return '{}';
return '{' + items.map(a => this._argAsString(a)).join(',') + '}';
}
if (arg instanceof Map) {
if (arg.size === 0) return '[:]';
const entries: string[] = [];
arg.forEach((v, k) => {
const ks = this._argAsString(k);
const vs = this._argAsString(v);
entries.push(typeof k === 'string' ? `${ks}:${vs}` : `(${ks}):${vs}`);
});
return '[' + entries.join(',') + ']';
}
if (Array.isArray(arg)) {
return '[' + arg.map(a => this._argAsString(a)).join(',') + ']';
}
if (typeof arg === 'object' && arg.constructor === Object) {
const entries = Object.entries(arg);
if (entries.length === 0) return '[:]';
return '[' + entries.map(([k, v]) => `${this._argAsString(k)}:${this._argAsString(v)}`).join(',') + ']';
}
throw new TypeError(`GremlinLang contains at least one type [${arg?.constructor?.name ?? typeof arg}] that cannot be represented as text.`);
}
addStep(name: string, args?: any[]): GremlinLang {
const argsStr = args?.length ? args.map(a => this._argAsString(a)).join(',') : '';
this.gremlin += `.${name}(${argsStr})`;
return this;
}
addSource(name: string, args?: any[]): GremlinLang {
if (name === 'CardinalityValueTraversal' && args) {
const card = args[0] as EnumValue;
this.gremlin += `Cardinality.${card.elementName}(${this._argAsString(args[1])})`;
return this;
}
if (name === 'withStrategies' && args) {
const nonOptionsStrategies: any[] = [];
for (const strategy of args) {
if (strategy instanceof OptionsStrategy) {
this.optionsStrategies.push(strategy);
} else {
nonOptionsStrategies.push(strategy);
}
}
if (nonOptionsStrategies.length > 0) {
const argsStr = nonOptionsStrategies.map(a => this._argAsString(a)).join(',');
this.gremlin += `.withStrategies(${argsStr})`;
}
return this;
}
const argsStr = args?.length ? args.map(a => this._argAsString(a)).join(',') : '';
this.gremlin += `.${name}(${argsStr})`;
return this;
}
getGremlin(prefix: string = 'g'): string {
if (this.gremlin.length > 0 && this.gremlin[0] !== '.') {
return this.gremlin;
}
return prefix + this.gremlin;
}
getParametersAsString(): string {
return GremlinLang.convertParametersToString(this.parameters);
}
static convertParametersToString(params: Map<string, any> | null): string {
if (params == null || params.size === 0) return '[:]';
const helper = new GremlinLang();
const parts: string[] = [];
params.forEach((v, k) => {
parts.push(`${helper._argAsString(k)}:${helper._argAsString(v)}`);
});
return '[' + parts.join(',') + ']';
}
}