jinaga
Version:
Data management for web and mobile applications.
449 lines • 16 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpecificationParser = exports.Invalid = void 0;
const authorizationRules_1 = require("../authorization/authorizationRules");
const distribution_rules_1 = require("../distribution/distribution-rules");
const hash_1 = require("../fact/hash");
const purgeConditions_1 = require("../purge/purgeConditions");
class Invalid extends Error {
constructor(message) {
const trueProto = new.target.prototype;
super(message);
this.__proto__ = trueProto;
}
}
exports.Invalid = Invalid;
class SpecificationParser {
constructor(input) {
this.input = input;
this.offset = 0;
}
skipWhitespace() {
const whitespace = /\s/;
while (whitespace.test(this.input[this.offset])) {
this.offset++;
}
}
atEnd() {
return this.offset >= this.input.length;
}
expectEnd() {
if (this.offset < this.input.length) {
throw new Invalid(`Expected end of input but found '${this.previewText()}'`);
}
}
previewText() {
return this.input.substring(this.offset, this.offset + 100);
}
continues(symbol) {
return this.input.substring(this.offset, this.offset + symbol.length) === symbol;
}
consume(symbol) {
if (this.continues(symbol)) {
this.offset += symbol.length;
this.skipWhitespace();
return true;
}
return false;
}
expect(symbol) {
if (!this.consume(symbol)) {
throw new Invalid(`Expected '${symbol}' but found '${this.previewText()}'`);
}
}
match(expression) {
const match = expression.exec(this.input.substring(this.offset));
if (match) {
this.offset += match[0].length;
this.skipWhitespace();
return match[0];
}
else {
return null;
}
}
parseIdentifier() {
const result = this.match(/^[a-zA-Z_][a-zA-Z0-9_]*/);
if (result !== null) {
return result;
}
throw new Invalid("Expected identifier but found '" + this.previewText() + "'");
}
parseType() {
const type = /^[a-zA-Z_][a-zA-Z0-9_.]*/;
const result = this.match(type);
if (result !== null) {
return result;
}
throw new Invalid("Expected type but found '" + this.previewText() + "'");
}
parseLabel() {
const name = this.parseIdentifier();
this.expect(":");
const type = this.parseType();
return { name, type };
}
parseRole() {
const name = this.parseIdentifier();
this.expect(":");
const predecessorType = this.parseType();
return { name, predecessorType };
}
parseGiven() {
this.expect("(");
if (this.continues(")")) {
throw new Invalid("The specification must contain at least one given label");
}
const labels = [];
labels.push(this.parseLabel());
while (this.consume(",")) {
labels.push(this.parseLabel());
}
this.expect(")");
return labels;
}
parseRoles() {
const roles = [];
while (this.consume("->")) {
roles.push(this.parseRole());
}
return roles;
}
parsePathCondition(unknown, labels) {
const labelLeft = this.parseIdentifier();
if (labelLeft !== unknown.name) {
throw new Invalid(`The unknown '${unknown.name}' must appear on the left side of the path`);
}
const rolesLeft = this.parseRoles();
this.expect("=");
const labelRight = this.parseIdentifier();
if (!labels.some(label => label.name === labelRight)) {
throw new Invalid(`The label '${labelRight}' has not been defined`);
}
const rolesRight = this.parseRoles();
return {
type: "path",
rolesLeft,
labelRight,
rolesRight
};
}
parseExistentialCondition(labels, unknown, exists) {
const { matches } = this.parseMatches([...labels, unknown]);
if (!matches.some(match => match.conditions.some(condition => condition.type === "path" &&
condition.labelRight === unknown.name))) {
throw new Invalid(`The existential condition must be based on the unknown '${unknown.name}'`);
}
return {
type: "existential",
exists,
matches
};
}
parseCondition(unknown, labels) {
if (this.consume("!")) {
this.expect("E");
return this.parseExistentialCondition(labels, unknown, false);
}
else if (this.consume("E")) {
return this.parseExistentialCondition(labels, unknown, true);
}
else {
return this.parsePathCondition(unknown, labels);
}
}
parseMatch(labels) {
const unknown = this.parseLabel();
if (labels.some(label => label.name === unknown.name)) {
throw new Invalid(`The name '${unknown.name}' has already been used`);
}
this.expect("[");
if (this.continues("]")) {
throw new Invalid(`The match for '${unknown.name}' has no conditions`);
}
const conditions = [];
while (!this.consume("]")) {
conditions.push(this.parseCondition(unknown, labels));
}
return { unknown, conditions };
}
parseMatches(labels) {
const matches = [];
this.expect("{");
while (!this.consume("}")) {
const match = this.parseMatch(labels);
labels = [...labels, match.unknown];
matches.push(match);
}
return { matches, labels };
}
parseComponent(labels) {
const name = this.parseIdentifier();
this.expect("=");
if (this.continues("{")) {
const { matches, labels: allLabels } = this.parseMatches(labels);
const projection = this.parseProjection(allLabels);
return { type: "specification", name, matches, projection: projection };
}
else if (this.consume("#")) {
const label = this.parseIdentifier();
return { type: "hash", name, label };
}
else {
const label = this.parseIdentifier();
if (this.consume(".")) {
const field = this.parseIdentifier();
return { type: "field", name, label, field };
}
else {
return { type: "fact", name, label };
}
}
}
parseProjection(labels) {
if (!this.consume("=>")) {
return {
type: "composite",
components: []
};
}
if (this.continues("{")) {
this.consume("{");
const components = [];
while (!this.consume("}")) {
const component = this.parseComponent(labels);
components.push(component);
}
return {
type: "composite",
components
};
}
else if (this.consume("#")) {
const label = this.parseIdentifier();
return {
type: "hash",
label
};
}
else {
const label = this.parseIdentifier();
if (this.consume(".")) {
const field = this.parseIdentifier();
return {
type: "field",
label,
field
};
}
else {
return {
type: "fact",
label
};
}
}
}
parseValue(knownFacts) {
const jsonValue = /^true|^false|^null|^"(?:[^"\\]|\\.)*"|^[+-]?\d+(\.\d+)?/;
const value = this.match(jsonValue);
if (value) {
// The string matches a JSON literal value, so this is a field.
return JSON.parse(value);
}
else if (this.continues("[")) {
// This is an array of facts.
const facts = [];
this.consume("[");
if (!this.continues("]")) {
facts.push(this.parseFactReference(knownFacts));
while (this.consume(",")) {
facts.push(this.parseFactReference(knownFacts));
}
}
this.expect("]");
return facts;
}
else {
// The string does not match a JSON literal value, so this is a fact reference.
return this.parseFactReference(knownFacts);
}
}
parseFactReference(knownFacts) {
const reference = this.parseIdentifier();
const fact = knownFacts.find(fact => fact.name === reference);
if (!fact) {
throw new Invalid(`The fact '${reference}' has not been defined`);
}
return fact.declared;
}
parseField(fields, predecessors, knownFacts) {
const name = this.parseIdentifier();
if (!this.continues(":")) {
// This is an auto-named element, which must be a predecessor
const fact = knownFacts.find(fact => fact.name === name);
if (!fact) {
throw new Invalid(`The fact '${name}' has not been defined`);
}
return {
fields,
predecessors: Object.assign(Object.assign({}, predecessors), { [name]: fact.declared.reference })
};
}
else {
// This is a named element, which could be a field or a predecessor
this.consume(":");
const value = this.parseValue(knownFacts);
if (typeof value === "object") {
if (Array.isArray(value)) {
// The value is an array of predecessors
return {
fields,
predecessors: Object.assign(Object.assign({}, predecessors), { [name]: value.map(predecessor => predecessor.reference) })
};
}
else {
// The value is a single predecessor
return {
fields,
predecessors: Object.assign(Object.assign({}, predecessors), { [name]: value.reference })
};
}
}
else {
// The value is a field
return {
fields: Object.assign(Object.assign({}, fields), { [name]: value }),
predecessors
};
}
}
}
parseFact(type, knownFacts) {
if (this.consume("{")) {
let fields = {};
let predecessors = {};
if (!this.continues("}")) {
({ fields, predecessors } = this.parseField(fields, predecessors, knownFacts));
while (this.consume(",")) {
({ fields, predecessors } = this.parseField(fields, predecessors, knownFacts));
}
}
this.expect("}");
const hash = (0, hash_1.computeHash)(fields, predecessors);
return {
fact: {
type,
hash,
fields,
predecessors
},
reference: {
type,
hash,
}
};
}
else if (this.consume("#")) {
const hash = this.match(/[A-Za-z0-9+/]+={0,2}/);
if (!hash) {
throw new Invalid("The hash must be a base64-encoded string");
}
return {
fact: null,
reference: {
type,
hash,
}
};
}
else {
const reference = this.parseIdentifier();
const fact = knownFacts.find(fact => fact.name === reference);
if (!fact) {
throw new Invalid(`The fact '${reference}' has not been defined`);
}
const knownFact = fact.declared;
if (knownFact.reference.type !== type) {
throw new Invalid(`Cannot assign a '${knownFact.reference.type}' to a '${type}'`);
}
return knownFact;
}
}
parseSpecification() {
const given = this.parseGiven();
const { matches, labels } = this.parseMatches(given);
const projection = this.parseProjection(labels);
return { given, matches, projection };
}
parseDeclaration(knownFacts) {
let result = knownFacts;
while (this.consume("let")) {
const name = this.parseIdentifier();
if (result.some(r => r.name === name)) {
throw new Invalid(`The name '${name}' has already been used`);
}
this.expect(":");
const type = this.parseType();
this.expect("=");
const value = this.parseFact(type, result);
result = [...result, { name, declared: value }];
}
return result;
}
parseAuthorizationRules() {
let authorizationRules = new authorizationRules_1.AuthorizationRules(undefined);
this.expect("authorization");
this.expect("{");
while (!this.consume("}")) {
if (this.consume("any")) {
const type = this.parseType();
authorizationRules = authorizationRules_1.AuthorizationRules.combine(authorizationRules, type, new authorizationRules_1.AuthorizationRuleAny());
}
else if (this.consume("no")) {
const type = this.parseType();
authorizationRules = authorizationRules_1.AuthorizationRules.combine(authorizationRules, type, new authorizationRules_1.AuthorizationRuleNone());
}
else {
const specification = this.parseSpecification();
if (specification.given.length !== 1) {
throw new Invalid("A specification in an authorization rule must have exactly one given label");
}
const type = specification.given[0].type;
authorizationRules = authorizationRules_1.AuthorizationRules.combine(authorizationRules, type, new authorizationRules_1.AuthorizationRuleSpecification(specification));
}
}
return authorizationRules;
}
parseDistributionRules() {
let distributionRules = new distribution_rules_1.DistributionRules([]);
this.expect("distribution");
this.expect("{");
while (!this.consume("}")) {
this.expect("share");
const specification = this.parseSpecification();
this.expect("with");
let user = null;
if (!this.consume("everyone")) {
user = this.parseSpecification();
}
distributionRules = distribution_rules_1.DistributionRules.combine(distributionRules, specification, user);
}
return distributionRules;
}
parsePurgeConditions() {
let purgeConditions = new purgeConditions_1.PurgeConditions([]);
this.expect("purge");
this.expect("{");
while (!this.consume("}")) {
const specification = this.parseSpecification();
purgeConditions = new purgeConditions_1.PurgeConditions([
...purgeConditions.specifications,
specification
]);
}
return purgeConditions;
}
}
exports.SpecificationParser = SpecificationParser;
//# sourceMappingURL=specification-parser.js.map