Skip to content

Commit

Permalink
Merge pull request #345 from datavisyn/thinkh/331_string-filter-compa…
Browse files Browse the repository at this point in the history
…tibility

Make LineUp v4 RegExp filter serialization backwards compatible
  • Loading branch information
thinkh authored Apr 6, 2020
2 parents 1f62bb7 + 304e51f commit f027d02
Show file tree
Hide file tree
Showing 3 changed files with 327 additions and 57 deletions.
70 changes: 13 additions & 57 deletions src/lineup/internal/cmds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {IObjectRef, action, meta, cat, op, ProvenanceGraph, ICmdResult} from 'ph
import {NumberColumn, LocalDataProvider, StackColumn, ScriptColumn, OrdinalColumn, CompositeColumn, Ranking, ISortCriteria, Column, isMapAbleColumn, mappingFunctions} from 'lineupjs';
import {resolveImmediately} from 'phovea_core/src';
import i18n from 'phovea_core/src/i18n';
import {isSerializedFilter, restoreLineUpFilter, serializeLineUpFilter, isLineUpStringFilter} from './cmds/filter';


// used for function calls in the context of tracking or untracking actions in the provenance graph in order to get a consistent defintion of the used strings
Expand Down Expand Up @@ -220,9 +221,15 @@ export async function setColumnImpl(inputs: IObjectRef<any>[], parameter: any) {
bak = source[`getRenderer`]();
source[`setRenderer`].call(source, parameter.value);
break;
case LineUpTrackAndUntrackActions.filter:
bak = source[`get${prop}`]();
// restore serialized regular expression before passing to LineUp
const value = isSerializedFilter(parameter.value) ? restoreLineUpFilter(parameter.value) : parameter.value;
source[`set${prop}`].call(source, value);
break;
default:
bak = source[`get${prop}`]();
source[`set${prop}`].call(source, restoreRegExp(parameter.value)); // restore serialized regular expression before passing to LineUp
source[`set${prop}`].call(source, parameter.value);
break;
}
}
Expand Down Expand Up @@ -349,80 +356,29 @@ function recordPropertyChange(source: Column | Ranking, provider: LocalDataProvi
return;
}

const newSerializedValue = serializeRegExp(newValue); // serialize possible RegExp object to be properly stored as provenance graph
if (property === LineUpTrackAndUntrackActions.filter) {
newValue = isLineUpStringFilter(newValue) ? serializeLineUpFilter(newValue) : newValue; // serialize possible RegExp object to be properly stored as provenance graph
}

if (source instanceof Column) {
// assert ALineUpView and update the stats
lineupViewWrapper.value.getInstance().updateLineUpStats();

const rid = rankingId(provider, source.findMyRanker());
const path = source.fqpath;
graph.pushWithResult(setColumn(lineupViewWrapper, rid, path, property, newSerializedValue), {
graph.pushWithResult(setColumn(lineupViewWrapper, rid, path, property, newValue), {
inverse: setColumn(lineupViewWrapper, rid, path, property, old)
});
} else if (source instanceof Ranking) {
const rid = rankingId(provider, source);
graph.pushWithResult(setColumn(lineupViewWrapper, rid, null, property, newSerializedValue), {
graph.pushWithResult(setColumn(lineupViewWrapper, rid, null, property, newValue), {
inverse: setColumn(lineupViewWrapper, rid, null, property, old)
});
}
};
source.on(`${property}Changed.track`, delayed > 0 ? delayedCall(f, delayed) : f);
}

/**
* Serialize RegExp objects from LineUp string columns as plain object
* that can be stored in the provenance graph
*/
interface IRegExpFilter {
/**
* RegExp as string
*/
value: string;
/**
* Flag to indicate the value should be restored as RegExp
*/
isRegExp: boolean;
}

/**
* Serializes RegExp objects to an IRegexFilter object, which can be stored in the provenance graph.
* In case a string is passed to this function no serialization is applied.
*
* Background information:
* The serialization step is necessary, because RegExp objects are converted into an empty object `{}` on `JSON.stringify`.
* ```
* JSON.stringify(/^123$/gm); // result: {}
* ```
*
* @param value Input string or RegExp object
* @returns {string | IRegExpFilter} Returns the input string or a plain `IRegExpFilter` object
*/
function serializeRegExp(value: string | RegExp): string | IRegExpFilter {
if (!(value instanceof RegExp)) {
return value;
}
return {value: value.toString(), isRegExp: true};
}

/**
* Restores a RegExp object from a given IRegExpFilter object.
* In case a string is passed to this function no deserialization is applied.
*
* @param filter Filter as string or plain object matching the IRegExpFilter
* @returns {string | RegExp| null} Returns the input string or the restored RegExp object
*/
function restoreRegExp(filter: string | IRegExpFilter): string | RegExp {
if (filter === null || !(<IRegExpFilter>filter).isRegExp) {
return <string | null>filter;
}

const serializedRegexParser = /^\/(.+)\/(\w+)?$/; // from https://gist.github.com/tenbits/ec7f0155b57b2d61a6cc90ef3d5f8b49
const matches = serializedRegexParser.exec((<IRegExpFilter>filter).value);
const [_full, regexString, regexFlags] = matches;
return new RegExp(regexString, regexFlags);
}

function trackColumn(provider: LocalDataProvider, lineup: IObjectRef<IViewProvider>, graph: ProvenanceGraph, col: Column) {
recordPropertyChange(col, provider, lineup, graph, LineUpTrackAndUntrackActions.metaData);
recordPropertyChange(col, provider, lineup, graph, LineUpTrackAndUntrackActions.filter);
Expand Down
175 changes: 175 additions & 0 deletions src/lineup/internal/cmds/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* Basic LineUp string filter values
*/
type LineUpStringFilterValue = string[] | string | null;

/**
* This type guard checks if `filter` parameter matches the `LineUpStringFilterValue` type
* @param filter Any filterable value that should be checked
* @returns Returns true if filter applies to the `LineUpStringFilterValue`
*/
function isLineUpStringFilterValue(filter: any): filter is LineUpStringFilterValue {
return filter === null || typeof filter === 'string' || Array.isArray(filter);
}

/**
* Serialize RegExp objects from LineUp string columns as plain object
* that can be stored in the provenance graph
*/
interface IRegExpFilter {
/**
* RegExp as string
*/
value: LineUpStringFilterValue;
/**
* Flag to indicate the value should be restored as RegExp
*/
isRegExp: boolean;
}

/**
* This type guard checks if `filter` parameter matches the `IRegExpFilter` type.
* @param filter Any filterable value that should be checked
* @returns Returns true if filter applies to the `IRegExpFilter`
*/
function isIRegExpFilter(filter: any): filter is IRegExpFilter {
return filter && filter.hasOwnProperty('value') && isLineUpStringFilterValue(filter.value) && filter.hasOwnProperty('isRegExp');
}

/**
* This interface combines the `IStringFilter` from `StringColumn`
* and `ICategoricalFilter` from `ICategoricalColumn`.
*/
interface ILineUpStringFilter {
/**
* Filter value
*/
filter: LineUpStringFilterValue | RegExp;

/**
* Filter for missing values
*/
filterMissing: boolean;
}

/**
* This type guard checks if the `filter` parameter matches the `isLineUpStringFilter` type.
*
* @internal
* @param filter Any value that could be a filter
* @returns Returns true if filter should be serialized/restored or false if not.
*/
export function isLineUpStringFilter(filter: any): filter is ILineUpStringFilter {
return filter && filter.hasOwnProperty('filter') && (isLineUpStringFilterValue(filter.filter) || filter.filter instanceof RegExp) && filter.hasOwnProperty('filterMissing');
}


/**
* Similar to the `ILineUpStringFilter`, but the RegExp is replaced with `IRegExpFilter`
*/
interface ISerializableLineUpFilter {
/**
* Filter value
* Note that the RegExp is replaced with IRegExpFilter (compared to the `ILineUpStringFilter` interface)
*/
filter: LineUpStringFilterValue | IRegExpFilter;

/**
* Filter for missing values
*/
filterMissing: boolean;
}

/**
* This type guard checks if the `filter` parameter matches the `ISerializableLineUpFilter` type.
* Necessary since number columns filter has properties `min`, `max` and no filter property.
*
* @internal
* @param filter Any value that could be a filter
* @returns Returns true if filter should be serialized/restored or false if not.
*/
export function isSerializedFilter(filter: any): filter is ISerializableLineUpFilter {
return filter && filter.hasOwnProperty('filter') && (isLineUpStringFilterValue(filter.filter) || isIRegExpFilter(filter.filter)) && filter.hasOwnProperty('filterMissing');
}

/**
* Serializes LineUp string filter, which can contain RegExp objects to an IRegexFilter object.
* The return value of this function can be passed to `JSON.stringify()` and stored in the provenance graph.
*
* Background information:
* The serialization step is necessary, because RegExp objects are converted into an empty object `{}` on `JSON.stringify`.
* ```
* JSON.stringify(/^123$/gm); // result: {}
* ```
*
* @internal
*
* @param filter LineUp filter object
* @returns Returns the `ISerializableLineUpFilter` object
*/
export function serializeLineUpFilter(filter: ILineUpStringFilter): ISerializableLineUpFilter {
const value = filter.filter;
const isRegExp = value instanceof RegExp;
return {
filter: {
value: isRegExp ? value.toString() : value as LineUpStringFilterValue,
isRegExp
},
filterMissing: filter.filterMissing
};
}

/**
* Coverts a RegExp string to a RegExp instance
*
* @internal
* @param value RegExp string
* @returns The RegExp instance
*/
export function restoreRegExp(value: string): RegExp {
const serializedRegexParser = /^\/(.+)\/(\w+)?$/; // from https://gist.github.com/tenbits/ec7f0155b57b2d61a6cc90ef3d5f8b49
const matches = serializedRegexParser.exec(value);

if(matches === null) {
throw new Error('Unable to parse regular expression from string. The string does not seem to be a valid RegExp.');
}

const [_full, regexString, regexFlags] = matches;
return new RegExp(regexString, regexFlags);
}

/**
* Restores filter values from the provenance graph and returns an `ILineUpStringFilter`
* which can be passed to the LineUp instance (using LineUp > 4.0.0).
*
* Valid seralized filter values are:
* - `LineUpStringFilterValue` used with LineUp < 4.0.0 and tdp_core < 9.0.0
* - `IRegExpFilter` used with LineUp < 4.0.0 and tdp_core >= 9.0.0
* - `ISerializableLineUpFilter` used with LineUp > 4.0.0 and tdp_core > 9.1.0
*
* @interal
* @param filter Filter with one of the types described above
* @param filterMissing The flag indicates if missing values should be filtered (default = `false`)
* @returns Returns an `ILineUpStringFilter` which can be passed to LineUp
*/
export function restoreLineUpFilter(filter: LineUpStringFilterValue | IRegExpFilter | ISerializableLineUpFilter, filterMissing = false): ILineUpStringFilter {
if (isLineUpStringFilterValue(filter)) {
return {filter, filterMissing};

} else if (isIRegExpFilter(filter) && filter.isRegExp === true) {
if(typeof filter.value === 'string') {
return {filter: restoreRegExp(filter.value), filterMissing};
}

throw new Error('Wrong type of filter value. Unable to restore RegExp instance from the given filter value.');

} else if (isIRegExpFilter(filter) && filter.isRegExp === false) {
return restoreLineUpFilter(filter.value, filterMissing);

} else if (isSerializedFilter(filter)) {
return restoreLineUpFilter(filter.filter, filter.filterMissing);

}

throw new Error('Unknown LineUp filter format. Unable to restore the given filter.');
}
Loading

0 comments on commit f027d02

Please sign in to comment.