Skip to content

Commit

Permalink
Compress paths into tree (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
Skn0tt authored Aug 11, 2020
1 parent fc932c9 commit f168d2e
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 151 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ json = {
// note that `normal` is not included here; `meta` only has special cases
meta = {
timestamp: 'date',
test: 'regexp',
timestamp: ['date'],
test: ['regexp'],
};
*/
```
Expand Down
2 changes: 1 addition & 1 deletion src/annotator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('annotator', () => {

expect(getAnnotations()).toEqual({
values: {
'a.1.b': 'undefined',
'a.1.b': ['undefined'],
},
});
});
Expand Down
206 changes: 109 additions & 97 deletions src/annotator.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,38 @@
import { getDeep, setDeep } from './accessDeep';
import { isPrimitive } from './is';
import { isPrimitive, isString } from './is';
import * as IteratorUtils from './iteratorutils';
import {
StringifiedPath,
isStringifiedPath,
parsePath,
stringifyPath,
} from './pathstringifier';
import { Walker } from './plainer';
import {
TypeAnnotation,
isTypeAnnotation,
transformValue,
untransformValue,
} from './transformer';
import { PathTree } from './pathtree';

export interface Annotations {
root?: TypeAnnotation;
values?: Record<StringifiedPath, TypeAnnotation>;
referentialEqualities?: Record<StringifiedPath, StringifiedPath[]>;
referentialEqualitiesRoot?: StringifiedPath[];
values?: PathTree.CollapsedRootTree<TypeAnnotation>;
referentialEqualities?: PathTree.CollapsedRootTree<
PathTree.CollapsedRootTree<string>
>;
}

export function isAnnotations(object: any): object is Annotations {
try {
if (!!object.root && !isTypeAnnotation(object.root)) {
return false;
}

if (!!object.values) {
return Object.entries(object.values).every(
([key, value]) => isStringifiedPath(key) && isTypeAnnotation(value)
);
if (object.values) {
if (!PathTree.isMinimizedTree(object.values, isTypeAnnotation)) {
return false;
}
}

if (!!object.referentialEqualities) {
return Object.entries(object.referentialEqualities).every(
([key, value]) =>
isStringifiedPath(key) && (value as string[]).every(isStringifiedPath)
);
if (object.referentialEqualities) {
if (
!PathTree.isMinimizedTree(object.referentialEqualities, tree =>
PathTree.isMinimizedTree(tree, isString)
)
) {
return false;
}
}

return true;
Expand All @@ -47,105 +41,123 @@ export function isAnnotations(object: any): object is Annotations {
}
}

export const makeAnnotator = () => {
const annotations: Annotations = {};
class ValueAnnotationFactory {
private tree = PathTree.create<TypeAnnotation | null>(null);

add(path: any[], annotation: TypeAnnotation) {
this.tree = PathTree.append(this.tree, path.map(String), annotation);
}

const objectIdentities = new Map<any, any[][]>();
function registerObjectPath(object: any, path: any[]) {
const paths = objectIdentities.get(object) ?? [];
create() {
return PathTree.collapseRoot(this.tree);
}
}

class ReferentialEqualityAnnotationFactory {
private readonly objectIdentities = new Map<any, any[][]>();

register(object: any, path: any[]) {
const paths = this.objectIdentities.get(object) ?? [];
paths.push(path);
objectIdentities.set(object, paths);
this.objectIdentities.set(object, paths);
}

create() {
let tree = PathTree.create<PathTree.CollapsedRootTree<string> | null>(null);

IteratorUtils.forEach(this.objectIdentities.values(), paths => {
if (paths.length <= 1) {
return;
}

const [shortestPath, ...identicalPaths] = paths
.map(path => path.map(String))
.sort((a, b) => a.length - b.length);

let identities = PathTree.create<string | null>(null);
for (const identicalPath of identicalPaths) {
identities = PathTree.appendPath(identities, identicalPath);
}

const minimizedIdentities = PathTree.collapseRoot(identities);
if (!minimizedIdentities) {
throw new Error('Illegal State');
}

tree = PathTree.append(tree, shortestPath, minimizedIdentities);
});

return PathTree.collapseRoot(tree);
}
}

class AnnotationFactory {
public readonly valueAnnotations = new ValueAnnotationFactory();
public readonly objectIdentities = new ReferentialEqualityAnnotationFactory();

create(): Annotations {
const annotations: Annotations = {};

const values = this.valueAnnotations.create();
if (values) {
annotations.values = values;
}

const referentialEqualities = this.objectIdentities.create();
if (referentialEqualities) {
annotations.referentialEqualities = referentialEqualities;
}

return annotations;
}
}

export const makeAnnotator = () => {
const annotationFactory = new AnnotationFactory();
const { valueAnnotations, objectIdentities } = annotationFactory;

const annotator: Walker = ({ path, node }) => {
if (!isPrimitive(node)) {
registerObjectPath(node, path);
objectIdentities.register(node, path);
}

const transformed = transformValue(node);

if (transformed) {
if (path.length === 0) {
annotations.root = transformed.type;
} else {
if (!annotations.values) {
annotations.values = {};
}

annotations.values[stringifyPath(path)] = transformed.type;
}

valueAnnotations.add(path, transformed.type);
return transformed.value;
} else {
return node;
}
};

function getAnnotations(): Annotations {
IteratorUtils.forEach(objectIdentities.values(), paths => {
if (paths.length > 1) {
const [shortestPath, ...identityPaths] = paths
.sort((a, b) => a.length - b.length)
.map(stringifyPath);

const isRoot = shortestPath.length === 0;
if (isRoot) {
annotations.referentialEqualitiesRoot = identityPaths;
} else {
if (!annotations.referentialEqualities) {
annotations.referentialEqualities = {};
}

annotations.referentialEqualities[shortestPath] = identityPaths;
}
}
});

return annotations;
}

return { getAnnotations, annotator };
return { getAnnotations: () => annotationFactory.create(), annotator };
};

export const applyAnnotations = (plain: any, annotations: Annotations): any => {
if (annotations.values) {
const annotationsWithPaths = Object.entries(annotations.values).map(
([key, type]) => [parsePath(key), type] as [string[], TypeAnnotation]
);

const annotationsWithPathsLeavesToRoot = annotationsWithPaths.sort(
([pathA], [pathB]) => pathB.length - pathA.length
PathTree.traverseWhileIgnoringNullRoot(
PathTree.expandRoot(annotations.values),
(type, path) => {
plain = setDeep(plain, path, v => untransformValue(v, type));
}
);

for (const [path, type] of annotationsWithPathsLeavesToRoot) {
plain = setDeep(plain, path, v =>
untransformValue(v, type as TypeAnnotation)
);
}
}

if (annotations.root) {
plain = untransformValue(plain, annotations.root);
}

if (annotations.referentialEqualities) {
for (const [objectPath, identicalObjectsPaths] of Object.entries(
annotations.referentialEqualities
)) {
const object = getDeep(plain, parsePath(objectPath));

for (const identicalObjectPath of identicalObjectsPaths.map(parsePath)) {
setDeep(plain, identicalObjectPath, () => object);
PathTree.traverseWhileIgnoringNullRoot(
PathTree.expandRoot(annotations.referentialEqualities),
(identicalObjects, path) => {
const object = getDeep(plain, path);

PathTree.traversePaths(
PathTree.expandRoot(identicalObjects),
identicalObjectPath => {
plain = setDeep(plain, identicalObjectPath, () => object);
}
);
}
}
}

if (annotations.referentialEqualitiesRoot) {
for (const identicalObjectPath of annotations.referentialEqualitiesRoot.map(
parsePath
)) {
setDeep(plain, identicalObjectPath, () => plain);
}
);
}

return plain;
Expand Down
Loading

0 comments on commit f168d2e

Please sign in to comment.