UNPKG

jinaga

Version:

Data management for web and mobile applications.

449 lines 16 kB
"use strict"; 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