Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import jsep from 'jsep';

/**
* Sources:
* - Copyright (c) 2013 Stephen Oney, http://jsep.from.so/, MIT License
* - Copyright (c) 2023 Don McCurdy, https://github.com/donmccurdy/expression-eval, MIT License
*/

// Default operator precedence from https://github.com/EricSmekens/jsep/blob/master/src/jsep.js#L55
import jsep from 'jsep';

/** Default operator precedence from https://github.com/EricSmekens/jsep/blob/master/src/jsep.js#L55 */
const DEFAULT_PRECEDENCE = {
'||': 1,
'&&': 2,
Expand Down
26 changes: 15 additions & 11 deletions modules/json/src/helpers/convert-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,37 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import parseExpressionString from './parse-expression-string';
import {parseExpressionString} from './parse-expression-string';

import {FUNCTION_IDENTIFIER} from '../syntactic-sugar';
import {type JSONConfiguration} from '../json-configuration';

function hasFunctionIdentifier(value) {
function hasFunctionIdentifier(value: unknown): value is string {
return typeof value === 'string' && value.startsWith(FUNCTION_IDENTIFIER);
}

function trimFunctionIdentifier(value) {
function trimFunctionIdentifier(value: string): string {
return value.replace(FUNCTION_IDENTIFIER, '');
}

// Try to determine if any props are function valued
// and if so convert their string values to functions
export default function convertFunctions(props, configuration) {
/**
* Tries to determine if any props are "function valued"
* and if so convert their string values to functions
*/
export function convertFunctions(
props: Record<string, unknown>,
configuration: JSONConfiguration
): Record<string, unknown> {
// Use deck.gl prop types if available.
const replacedProps = {};
for (const propName in props) {
let propValue = props[propName];

// Parse string valued expressions
const isFunction = hasFunctionIdentifier(propValue);

if (isFunction) {
if (hasFunctionIdentifier(propValue)) {
// Parse string as "expression", return equivalent JavaScript function
propValue = trimFunctionIdentifier(propValue);
propValue = parseExpressionString(propValue, configuration);
const trimmedFunctionIdentifier = trimFunctionIdentifier(propValue);
propValue = parseExpressionString(trimmedFunctionIdentifier, configuration);
}

replacedProps[propName] = propValue;
Expand Down
16 changes: 12 additions & 4 deletions modules/json/src/helpers/execute-function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

// This attempts to execute a function
export function executeFunction(targetFunction, props, configuration) {
import {type JSONConfiguration} from '../json-configuration';

/**
* Attempt to execute a function
*/
export function executeFunction(
targetFunction: string,
props: Record<string, unknown>,
configuration: JSONConfiguration
) {
// Find the function
const matchedFunction = configuration.functions[targetFunction];
const matchedFunction = configuration.config.functions[targetFunction];

// Check that the function is in the configuration.
if (!matchedFunction) {
const {log} = configuration; // eslint-disable-line
const {log} = configuration.config; // eslint-disable-line
if (log) {
const stringProps = JSON.stringify(props, null, 0).slice(0, 40);
log.warn(`JSON converter: No registered function ${targetFunction}(${stringProps}...) `);
Expand Down
37 changes: 25 additions & 12 deletions modules/json/src/helpers/instantiate-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import convertFunctions from './convert-functions';

// This attempts to instantiate a class, either as a class or as a React component
export function instantiateClass(type, props, configuration) {
import {JSONConfiguration} from '../json-configuration';
import {convertFunctions} from './convert-functions';

type Constructor<T = unknown> = new (props: Record<string, unknown>) => T;

/**
* Attempt to instantiate a class, either as a class or as a React component
*/
export function instantiateClass(
type: string,
props: Record<string, unknown>,
configuration: JSONConfiguration
): unknown {
// Find the class
const Class = configuration.classes[type];
const Component = configuration.reactComponents[type];
const Class = configuration.config.classes[type];
const Component = configuration.config.reactComponents[type];

// Check that the class is in the configuration.
if (!Class && !Component) {
const {log} = configuration; // eslint-disable-line
const {log} = configuration.config; // eslint-disable-line
if (log) {
const stringProps = JSON.stringify(props, null, 0).slice(0, 40);
log.warn(`JSON converter: No registered class of type ${type}(${stringProps}...) `);
Expand All @@ -27,20 +36,24 @@ export function instantiateClass(type, props, configuration) {
return instantiateReactComponent(Component, props, configuration);
}

function instantiateJavaScriptClass(Class, props, configuration) {
function instantiateJavaScriptClass<T = unknown>(
Class: Constructor<T>,
props: Record<string, unknown>,
configuration: JSONConfiguration
): unknown {
if (configuration.preProcessClassProps) {
props = configuration.preProcessClassProps(Class, props, configuration);
props = configuration.preProcessClassProps(Class, props);
}
props = convertFunctions(props, configuration);
return new Class(props);
}

function instantiateReactComponent(Component, props, configuration) {
const {React} = configuration;
function instantiateReactComponent(Component, props, configuration: JSONConfiguration) {
const {React} = configuration.config;
const {children = []} = props;
delete props.children;
if (configuration.preProcessClassProps) {
props = configuration.preProcessClassProps(Component, props, configuration);
props = configuration.preProcessClassProps(Component, props);
}

props = convertFunctions(props, configuration);
Expand Down
22 changes: 15 additions & 7 deletions modules/json/src/helpers/parse-expression-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@
import {get} from '../utils/get';

// expression-eval: Small jsep based expression parser that supports array and object indexing
import {parse, eval as evaluate} from '../utils/expression-eval';
import {parse, eval as evaluate} from '../expression-eval/expression-eval';

const cachedExpressionMap = {
type AccessorFunction = (row: Record<string, unknown>) => unknown;

const cachedExpressionMap: Record<string, AccessorFunction> = {
// Identity function
'-': object => object
};

// Calculates an accessor function from a JSON string
// '-' : x => x
// 'a.b.c': x => x.a.b.c
export default function parseExpressionString(propValue, configuration) {
/**
* Generates an accessor function from a JSON string
* '-' : x => x
* 'a.b.c': x => x.a.b.c
*/
export function parseExpressionString(
propValue: string,
configuration?
): (row: Record<string, unknown>) => unknown {
// NOTE: Can be null which represents invalid function. Return null so that prop can be omitted
if (propValue in cachedExpressionMap) {
return cachedExpressionMap[propValue];
Expand Down Expand Up @@ -47,7 +55,7 @@ export default function parseExpressionString(propValue, configuration) {
return func;
}

// Helper function to search all nodes in AST returned by expressionEval
/** Helper function to search all nodes in AST returned by expressionEval */
// eslint-disable-next-line complexity
function traverse(node, visitor) {
if (Array.isArray(node)) {
Expand Down
2 changes: 1 addition & 1 deletion modules/json/src/helpers/parse-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

// Accept JSON strings by parsing them
// TODO - use a parser that provides meaninful error messages
export default function parseJSON(json) {
export function parseJSON(json) {
return typeof json === 'string' ? JSON.parse(json) : json;
}
10 changes: 5 additions & 5 deletions modules/json/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
// @deck.gl/json: top-level exports

// Generic JSON converter, usable by other wrapper modules
export {default as JSONConverter} from './json-converter';
export {default as JSONConfiguration} from './json-configuration';
export {JSONConverter, type JSONConverterProps} from './json-converter';
export {JSONConfiguration, type JSONConfigurationProps} from './json-configuration';

// Transports
export {default as Transport} from './transports/transport';
export {Transport} from './transports/transport';

// Helpers
export {default as _convertFunctions} from './helpers/convert-functions';
export {default as _parseExpressionString} from './helpers/parse-expression-string';
export {convertFunctions as _convertFunctions} from './helpers/convert-functions';
export {parseExpressionString as _parseExpressionString} from './helpers/parse-expression-string';
export {shallowEqualObjects as _shallowEqualObjects} from './utils/shallow-equal-objects';
75 changes: 38 additions & 37 deletions modules/json/src/json-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,57 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

// TODO - default parsing code should not be part of the configuration.
import parseExpressionString from './helpers/parse-expression-string';
import assert from './utils/assert';

import {TYPE_KEY, FUNCTION_KEY} from './syntactic-sugar';
// TODO - default parsing code should not be part of the configuration.
import {parseExpressionString} from './helpers/parse-expression-string';

const isObject = value => value && typeof value === 'object';

export default class JSONConfiguration {
typeKey = TYPE_KEY;
functionKey = FUNCTION_KEY;
log = console; // eslint-disable-line
classes = {};
reactComponents = {};
enumerations = {};
constants = {};
functions = {};
React = null;
export type JSONConfigurationProps = {
log?; // eslint-disable-line
typeKey?: string;
functionKey?: string;
classes?: Record<string, new (props: Record<string, unknown>) => unknown>;
enumerations?: Record<string, any>;
constants: Record<string, unknown>;
functions: Record<string, Function>;
React?: {createElement: (Component, props, children) => any};
reactComponents?: Record<string, Function>;
};

export class JSONConfiguration {
static defaultProps: Required<JSONConfigurationProps> = {
log: console, // eslint-disable-lin,
typeKey: TYPE_KEY,
functionKey: FUNCTION_KEY,
classes: {},
reactComponents: {},
enumerations: {},
constants: {},
functions: {},
React: undefined!
};

config: Required<JSONConfigurationProps> = {...JSONConfiguration.defaultProps};

// TODO - this needs to be simpler, function conversion should be built in
convertFunction = parseExpressionString;
preProcessClassProps = (Class, props) => props;
postProcessConvertedJson = json => json;

constructor(...configurations) {
for (const configuration of configurations) {
this.merge(configuration);
}
constructor(configuration: JSONConfigurationProps) {
this.merge(configuration);
}

merge(configuration) {
merge(configuration: JSONConfigurationProps) {
for (const key in configuration) {
switch (key) {
// DEPRECATED = For backwards compatibility, add views and layers to classes;
case 'layers':
case 'views':
Object.assign(this.classes, configuration[key]);
break;
default:
// Store configuration as root fields (this.classes, ...)
if (key in this) {
const value = configuration[key];
this[key] = isObject(this[key]) ? Object.assign(this[key], value) : value;
}
// Store configuration as root fields (this.classes, ...)
if (key in this.config) {
const value = configuration[key];
this.config[key] = isObject(this.config[key])
? Object.assign(this.config[key], value)
: value;
}
}
}

validate(configuration) {
assert(!this.typeKey || typeof this.typeKey === 'string');
assert(isObject(this.classes));
return true;
}
}
Loading
Loading