Skip to content

Commit

Permalink
AG-11705 Add $ref to $ref and catch circular references
Browse files Browse the repository at this point in the history
  • Loading branch information
lsjroberts committed Jan 30, 2025
1 parent 1b54891 commit 2116b95
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ export class ChartTheme {
fontWeight: 400,
gridLineColor: '#e0eaf1',
padding: 20,
subtleTextColor: { $mix: [{ $ref: 'foregroundColor' }, { $ref: 'backgroundColor' }, 0.38] },
subtleTextColor: { $mix: [{ $ref: 'textColor' }, { $ref: 'backgroundColor' }, 0.38] },
textColor: { $ref: 'foregroundColor' },
};
}
Expand Down
19 changes: 19 additions & 0 deletions packages/ag-charts-community/src/util/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,25 @@ describe('json module', () => {
]);
});

it('should resolve `$ref` operation on second `$ref` operation', () => {
const source = { a: { $ref: 'first' } };
jsonResolveOperations(source, { first: { $ref: 'second' }, second: 'hello' });
expect(source).toEqual({ a: 'hello' });
});

it('should catch circular references', () => {
const source = { a: { $ref: 'first' } };
jsonResolveOperations(source, {
first: { $ref: 'second' },
second: { $ref: 'third' },
third: { $ref: 'first' },
});
expect(source).toEqual({ a: undefined });
expectWarningMessages([
'AG Charts - `$ref` json operation failed on [first] at [a], circular reference detected with [second, third, first].',
]);
});

it('should resolve `$path` operation', () => {
const source = {
a: 'parent',
Expand Down
75 changes: 55 additions & 20 deletions packages/ag-charts-community/src/util/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,25 +353,13 @@ function jsonResolveVisitorValue<T, P>(
params: P,
source: T,
path: string[],
modifiedPaths?: Record<string, any>
modifiedPaths: Record<string, any>
) {
if (!isPlainObject(value)) {
return value;
}

const [key, ...otherKeys] = Object.keys(value) as Array<Operation>;
if (otherKeys.length !== 0 || !operationKeys.has(key)) {
return value;
}
const { operation, values } = getOperation(value);
if (!operation) return value;
modifiedPaths[path.join('.')] = value;

let values = value[key];
if (isArray(values)) {
values = values.map((v: any) => jsonResolveVisitorValue(v, params, source, path));
}

if (modifiedPaths) modifiedPaths[path.join('.')] = value;

return operations[key](values, params, source, path);
return resolveOperation(operation, values, params, source, path, new Set());
}

enum Operation {
Expand All @@ -388,11 +376,58 @@ enum Operation {
Mix = '$mix',
}
const operationKeys = new Set(Object.values(Operation));
type OperationFn<T, C> = (value: string | Array<unknown>, params: C, source: T, path: string[]) => any;
type OperationFn<T, P> = (
value: string | Array<unknown>,
params: P,
source: T,
path: string[],
referencedParams?: Set<keyof P>
) => any;

function getOperation(value: unknown) {
if (!isPlainObject(value)) return {};
const [operation, ...otherKeys] = Object.keys(value) as Array<Operation>;
if (otherKeys.length !== 0 || !operationKeys.has(operation)) return {};
return { operation, values: value[operation] };
}

function resolveOperation<T, P>(
operation: Operation,
value: string | Array<unknown>,
params: P,
source: T,
path: string[],
referencedParams?: Set<keyof P>
): any {
if (isArray(value)) {
value = value.map((v) => {
const { operation: nestedOperation, values } = getOperation(v);
if (!nestedOperation) return v;
return resolveOperation(nestedOperation, values, params, source, path, referencedParams);
});
}

return operations[operation](value, params, source, path, referencedParams);
}

const operations: Record<Operation, OperationFn<any, any>> = {
$ref: (key, params, _source, path) => {
if (isString(key) && key in params) return params[key];
$ref: (key, params, source, path, referencedParams) => {
if (isString(key) && key in params) {
const { operation, values } = getOperation(params[key]);
if (operation !== Operation.Ref) {
return params[key];
}

if (referencedParams?.has(values)) {
Logger.warnOnce(
`\`$ref\` json operation failed on [${String(key)}] at [${path.join('.')}], circular reference detected with [${[...referencedParams].join(', ')}].`
);
return;
}

referencedParams?.add(values);
return operations.$ref(values, params, source, path, referencedParams);
}
Logger.warnOnce(
`\`$ref\` json operation failed on [${String(key)}] at [${path.join('.')}], expecting one of [${Object.keys(params).join(', ')}].`
);
Expand Down

0 comments on commit 2116b95

Please sign in to comment.