jinaga
Version:
Data management for web and mobile applications.
299 lines (255 loc) • 11.7 kB
text/typescript
import { Condition, ExistentialCondition, Label, Match, PathCondition, Projection, Specification } from "./specification";
type InverseOperation = "add" | "remove";
export interface SpecificationInverse {
inverseSpecification: Specification;
operation: InverseOperation;
givenSubset: string[];
parentSubset: string[];
path: string;
resultSubset: string[];
};
interface InverterContext {
givenSubset: string[];
parentSubset: string[];
path: string;
resultSubset: string[];
projection: Projection;
}
export function invertSpecification(specification: Specification): SpecificationInverse[] {
// Turn each given into a match.
const emptyMatches: Match[] = specification.given.map(g => ({
unknown: g,
conditions: []
}));
const matches: Match[] = [...emptyMatches, ...specification.matches];
const labels: Label[] = specification.matches.map(m => m.unknown);
const givenSubset: string[] = specification.given.map(g => g.name);
const resultSubset: string[] = [ ...givenSubset, ...labels.map(l => l.name) ];
const context: InverterContext = {
path: "",
givenSubset,
parentSubset: givenSubset,
resultSubset,
projection: specification.projection
};
const inverses: SpecificationInverse[] = invertMatches(matches, labels, context);
const projectionInverses: SpecificationInverse[] = invertProjection(matches, context);
return [ ...inverses, ...projectionInverses ];
}
function invertMatches(matches: Match[], labels: Label[], context: InverterContext): SpecificationInverse[] {
const inverses: SpecificationInverse[] = [];
// Produce an inverse for each unknown in the original specification.
for (const label of labels) {
matches = shakeTree(matches, label.name);
// The given will not have any successors.
// Simplify the matches by removing any conditions that cannot be satisfied.
const simplified: Match[] | null = simplifyMatches(matches, label.name);
if (simplified !== null) {
const inverseSpecification: Specification = {
given: [label],
matches: simplified.slice(1),
projection: context.projection
};
const inverse: SpecificationInverse = {
inverseSpecification,
operation: "add",
givenSubset: context.givenSubset,
parentSubset: context.parentSubset,
path: context.path,
resultSubset: context.resultSubset
};
inverses.push(inverse);
}
const existentialInverses: SpecificationInverse[] = invertExistentialConditions(matches, matches[0].conditions, "add", context);
inverses.push(...existentialInverses);
}
return inverses;
}
function shakeTree(matches: Match[], label: string): Match[] {
// Find the match for the given label.
const match: Match = findMatch(matches, label);
// Move the match to the beginning of the list.
matches = [ match, ...matches.filter(m => m !== match) ];
// Invert all path conditions in the match and move them to the tagged match.
for (const condition of match.conditions) {
if (condition.type === "path") {
matches = invertAndMovePathCondition(matches, label, condition);
}
}
// Move any other matches with no paths down.
for (let i = 1; i < matches.length; i++) {
let otherMatch: Match = matches[i];
while (!otherMatch.conditions.some(c => c.type === "path")) {
// Find all matches beyond this point that tag this one.
for (let j = i + 1; j < matches.length; j++) {
const taggedMatch: Match = matches[j];
// Move their path conditions to the other match.
for (const taggedCondition of taggedMatch.conditions) {
if (taggedCondition.type === "path" &&
taggedCondition.labelRight === otherMatch.unknown.name) {
matches = invertAndMovePathCondition(matches, taggedMatch.unknown.name, taggedCondition);
}
}
}
// Move the other match to the bottom of the list.
matches = [ ...matches.slice(0, i), ...matches.slice(i + 1), matches[i] ];
otherMatch = matches[i];
}
}
return matches;
}
function invertAndMovePathCondition(matches: Match[], label: string, pathCondition: PathCondition): Match[] {
// Find the match for the given label.
const match: Match = findMatch(matches, label);
// Find the match for the target label.
const targetMatch: Match = findMatch(matches, pathCondition.labelRight);
// Invert the path condition.
const invertedPathCondition: PathCondition = {
type: "path",
labelRight: match.unknown.name,
rolesRight: pathCondition.rolesLeft,
rolesLeft: pathCondition.rolesRight
};
// Remove the path condition from the match.
const newMatch: Match = {
unknown: match.unknown,
conditions: match.conditions.filter(c => c !== pathCondition)
};
const matchIndex = matches.indexOf(match);
matches = [ ...matches.slice(0, matchIndex), newMatch, ...matches.slice(matchIndex + 1) ];
// Add the inverted path condition to the target match.
const newTargetMatch: Match = {
unknown: targetMatch.unknown,
conditions: [ invertedPathCondition, ...targetMatch.conditions ]
};
const targetMatchIndex = matches.indexOf(targetMatch);
matches = [ ...matches.slice(0, targetMatchIndex), newTargetMatch, ...matches.slice(targetMatchIndex + 1) ];
return matches;
}
function findMatch(matches: Match[], label: string): Match {
for (const match of matches) {
if (match.unknown.name === label) {
return match;
}
}
throw new Error(`Label ${label} not found`);
}
function invertExistentialConditions(outerMatches: Match[], conditions: Condition[], parentOperation: InverseOperation, context: InverterContext): SpecificationInverse[] {
const inverses: SpecificationInverse[] = [];
// Produce inverses for each existential condition in the match.
for (const condition of conditions) {
if (condition.type === "existential") {
let matches = [ ...outerMatches, ...condition.matches ];
for (const match of condition.matches) {
matches = shakeTree(matches, match.unknown.name);
const matchesWithoutCondition: Match[] = removeCondition(matches.slice(1), condition);
const inverseSpecification: Specification = {
given: [match.unknown],
matches: matchesWithoutCondition,
projection: context.projection
};
const operation = inferOperation(parentOperation, condition.exists);
const inverse: SpecificationInverse = {
inverseSpecification,
operation,
givenSubset: context.givenSubset,
parentSubset: context.parentSubset,
path: context.path,
resultSubset: context.resultSubset
};
inverses.push(inverse);
const existentialInverses: SpecificationInverse[] = invertExistentialConditions(matches, match.conditions, operation, context);
inverses.push(...existentialInverses);
}
}
}
return inverses;
}
function removeCondition(matches: Match[], condition: ExistentialCondition): Match[] {
return matches.map(match =>
match.conditions.includes(condition) ?
{
unknown: match.unknown,
conditions: match.conditions.filter(c => c !== condition)
} :
match
);
}
function inferOperation(parentOperation: InverseOperation, exists: boolean): InverseOperation {
if (parentOperation === "add") {
return exists ? "add" : "remove";
}
else if (parentOperation === "remove") {
return exists ? "remove" : "add";
}
else {
const _exhaustiveCheck: never = parentOperation;
throw new Error(`Cannot infer operation from ${_exhaustiveCheck}, ${exists ? "exists" : "not exists"}`);
}
}
function invertProjection(matches: Match[], context: InverterContext): SpecificationInverse[] {
const inverses: SpecificationInverse[] = [];
// Produce inverses for all collections in the projection.
if (context.projection.type === "composite") {
for (const component of context.projection.components) {
if (component.type === "specification") {
const componentMatches = [ ...matches, ...component.matches ];
const componentLabels = component.matches.map(m => m.unknown);
const childContext: InverterContext = {
...context,
path: context.path + "." + component.name,
parentSubset: context.resultSubset,
resultSubset: [ ...context.resultSubset, ...componentLabels.map(l => l.name) ],
projection: component.projection
};
const matchInverses = invertMatches(componentMatches, componentLabels, childContext);
const projectionInverses = invertProjection(componentMatches, childContext);
inverses.push(...matchInverses, ...projectionInverses);
}
}
}
return inverses;
}
function simplifyMatches(matches: Match[], given: string): Match[] | null {
const simplifiedMatches: Match[] = [];
for (const match of matches) {
const simplifiedMatch: Match | null = simplifyMatch(match, given);
if (simplifiedMatch === null) {
return null;
}
else {
simplifiedMatches.push(simplifiedMatch);
}
}
return simplifiedMatches;
}
function simplifyMatch(match: Match, given: string): Match | null {
const simplifiedConditions: Condition[] = [];
for (const condition of match.conditions) {
if (expectsSuccessor(condition, given)) {
// This path condition matches successors of the given.
// There are no successors yet, so the condition is unsatisfiable.
return null;
}
if (condition.type === "existential") {
const anyExpectsSuccessor = condition.matches.some(m =>
m.conditions.some(c => expectsSuccessor(c, given)));
if (anyExpectsSuccessor && condition.exists) {
// This existential condition expects successors of the given.
// There are no successors yet, so the condition is unsatisfiable.
return null;
}
}
simplifiedConditions.push(condition);
}
return {
unknown: match.unknown,
conditions: simplifiedConditions
};
}
function expectsSuccessor(condition: Condition, given: string) {
return condition.type === "path" &&
condition.labelRight === given &&
condition.rolesRight.length === 0 &&
condition.rolesLeft.length > 0;
}