diff --git a/package-lock.json b/package-lock.json index 64bb37f..b4bf739 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "Apache-2.0", "dependencies": { "@openfga/sdk": "^0.0.2", - "lodash": "^4.17.21", "nearley": "^2.20.1" }, "devDependencies": { @@ -3645,11 +3644,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7625,11 +7619,6 @@ "p-locate": "^5.0.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", diff --git a/package.json b/package.json index a4233ee..f17232c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "author": "OpenFGA", "dependencies": { "@openfga/sdk": "^0.0.2", - "lodash": "^4.17.21", "nearley": "^2.20.1" }, "devDependencies": { diff --git a/src/check-dsl.ts b/src/check-dsl.ts index 850ceec..6f31e74 100644 --- a/src/check-dsl.ts +++ b/src/check-dsl.ts @@ -1,14 +1,30 @@ -import * as _ from "lodash"; - import { Keywords } from "./keywords"; -import { parseDSL } from "./parse-dsl"; +import { parseDSL, RewriteType } from "./parse-dsl"; import { report } from "./reporters"; -const readTypeData = (r: any, lines: any) => { - const typeName = _.trim(_.flattenDeep(r[0][2]).join("")); - const typeDefinition = _.flattenDeep([r[0][1], r[0][2]]).join(""); - const typeDefinitionLine = _.findIndex(lines, (l: string) => _.trim(l) === typeDefinition) + 1; - return { typeName, typeDefinition, typeDefinitionLine }; +// return the line number for the specified relation +const getLineNumber = (relation: string, lines: string[], skipIndex?: number) => { + if (!skipIndex) { + skipIndex = 0; + } + return ( + lines + .slice(skipIndex) + .findIndex((line: string) => line.trim().replace(/ {2,}/g, " ").startsWith(`define ${relation}`)) + skipIndex + ); +}; + +const defaultError = (lines: any) => { + return { + // monaco.MarkerSeverity.Error, + severity: 8, + startColumn: 0, + endColumn: Number.MAX_SAFE_INTEGER, + startLineNumber: 0, + endLineNumber: lines.length, + message: "Invalid syntax", + source: "linter", + }; }; export const checkDSL = (codeInEditor: string) => { @@ -17,164 +33,127 @@ export const checkDSL = (codeInEditor: string) => { const reporter = report({ lines, markers }); try { - const results = parseDSL(codeInEditor); - const relationsPerType: Record = {}; - const globalRelations = [Keywords.SELF as string]; + const parserResults = parseDSL(codeInEditor); + const relationsPerType: Record> = {}; + const globalRelations: Record = { [Keywords.SELF]: true }; - // Reading relations per type - _.forEach(results, (r) => { - const { typeName, typeDefinitionLine } = readTypeData(r, lines); + // Looking at the types + parserResults.forEach((typeDef) => { + const typeName = typeDef.type; // Include keyword - const relations = [Keywords.SELF as string]; + const encounteredRelationsInType: Record = { [Keywords.SELF]: true }; - _.forEach(r[2], (r2, idx: number) => { - const relation = _.trim(_.flattenDeep(r2).join("")).match(/define\s+(.*)\s+as\s/)?.[1]; - if (!relation) { - return; - } - const lineIdx = typeDefinitionLine + idx + 1; + typeDef.relations.forEach((relationDef) => { + const { relation: relationName } = relationDef; - if (relations.includes(relation)) { - reporter.duplicateDefinition({ lineIndex: lineIdx, value: relation }); + // Check if we have any duplicate relations + if (encounteredRelationsInType[relationName]) { + // figure out what is the lineIdx in question + const initialLineIdx = getLineNumber(relationName, lines); + const duplicateLineIdx = getLineNumber(relationName, lines, initialLineIdx + 1); + reporter.duplicateDefinition({ lineIndex: duplicateLineIdx, value: relationName }); } - relations.push(relation); - globalRelations.push(relation); + encounteredRelationsInType[relationName] = true; + globalRelations[relationName] = true; }); - relationsPerType[typeName] = relations; + relationsPerType[typeName] = encounteredRelationsInType; }); - _.forEach(results, (r) => { - const { typeName, typeDefinitionLine } = readTypeData(r, lines); - - _.forEach(r[2], (r2, idx: number) => { - const lineIndex = typeDefinitionLine + idx + 1; - let definition: any[] = _.flatten(r2[0][1]); - - if (!definition[0].includes(Keywords.DEFINE)) { - definition = _.flatten(definition); - } - - const definitionName = _.flattenDeep(definition[2]).join(""); - - if (definition[0].includes(Keywords.DEFINE)) { - const clauses = _.slice(definition, 7, definition.length); - - _.forEach(clauses, (clause) => { - let value = _.trim(_.flattenDeep(clause).join("")); - - if (value.indexOf(Keywords.OR) === 0) { - value = value.replace(`${Keywords.OR} `, ""); - } - - if (value.indexOf(Keywords.AND) === 0) { - value = value.replace(`${Keywords.AND} `, ""); - } - - const hasFrom = value.includes(Keywords.FROM); - const hasButNot = value.includes(Keywords.BUT_NOT); - - if (hasButNot) { - const butNotValue = _.trim(_.last(value.split(Keywords.BUT_NOT))); - - if (definitionName === butNotValue) { - reporter.invalidButNot({ - lineIndex, - value: butNotValue, - clause: value, - }); - } else { - reporter.invalidRelationWithinClause({ - typeName, - value: butNotValue, - validRelations: relationsPerType, - clause: value, - reverse: true, - lineIndex, - }); - } - } else if (hasFrom) { - // Checking: `define share as owner from parent` - const values = value.split(Keywords.FROM).map((v) => _.trim(v)); - - reporter.invalidRelationWithinClause({ - typeName, - reverse: false, - value: values[0], - validRelations: globalRelations, - clause: value, - lineIndex, - }); - - if (definitionName === values[1]) { - // Checking: `define owner as writer from owner` - if (clauses.length < 2) { - reporter.invalidFrom({ - lineIndex, - value: values[1], - clause: value, - }); - } - } else { - reporter.invalidRelationWithinClause({ - typeName, - value: values[1], - validRelations: relationsPerType, - clause: value, - reverse: true, - lineIndex, - }); - } - } else if (definitionName === value) { - // Checking: `define owner as owner` + parserResults.forEach((typeDef) => { + const typeName = typeDef.type; + + // parse through each of the relations to do validation + typeDef.relations.forEach((relationDef) => { + const { relation: relationName } = relationDef; + const validateTargetRelation = (typeName: string, relationName: string, target: any) => { + if (!target) { + // no need to continue to parse if there is no target + return; + } + if (relationName === target.target) { + if (target.rewrite != RewriteType.TupleToUserset) { + // the error case will be relation require self reference (i.e., define owner as owner) + const lineIndex = getLineNumber(relationName, lines); reporter.useSelf({ lineIndex, - value, + value: relationName, }); - } else { - // Checking: `define owner as self` - reporter.invalidRelation({ + } else if (relationDef.definition.targets?.length === 1) { + // define owner as writer from owner + const lineIndex = getLineNumber(relationName, lines); + reporter.invalidFrom({ lineIndex, - value, - validRelations: globalRelations, + value: target.target, + clause: target.target, }); } - }); + } + + if (target.target && !globalRelations[target.target]) { + // the target relation is not defined (i.e., define owner as foo) where foo is not defined + const lineIndex = getLineNumber(relationName, lines); + const value = target.target; + reporter.invalidRelation({ + lineIndex, + value, + validRelations: Object.keys(globalRelations), + }); + } + + if (target.from && !relationsPerType[typeName][target.from]) { + // The "from" is not defined for the current type `define owner as member from writer` + const lineIndex = getLineNumber(relationName, lines); + const value = target.from; + reporter.invalidRelationWithinClause({ + typeName, + value: target.from, + validRelations: relationsPerType, + clause: value, + reverse: true, + lineIndex, + }); + } + }; + + relationDef.definition.targets?.forEach((target) => { + validateTargetRelation(typeName, relationName, target); + }); + + // check the but not + if (relationDef.definition.base) { + validateTargetRelation(typeName, relationName, relationDef.definition.base); + } + if (relationDef.definition.diff) { + validateTargetRelation(typeName, relationName, relationDef.definition.diff); } }); }); } catch (e: any) { - if (!_.isUndefined(e.offset)) { - const line = Number.parseInt(e.message.match(/line\s([0-9]*)\scol\s([0-9]*)/m)[1]); - const column = Number.parseInt(e.message.match(/line\s([0-9]*)\scol\s([0-9]*)/m)[2]); - - const marker = { - // monaco.MarkerSeverity.Error, - severity: 8, - startColumn: column - 1, - endColumn: lines[line - 1].length, - startLineNumber: column === 0 ? line - 1 : line, - endLineNumber: column === 0 ? line - 1 : line, - message: "Invalid syntax", - source: "linter", - }; - - markers.push(marker); + if (typeof e.offset !== "undefined") { + try { + let line = Number.parseInt(e.message.match(/line\s([0-9]*)\scol\s([0-9]*)/m)[1]); + const column = Number.parseInt(e.message.match(/line\s([0-9]*)\scol\s([0-9]*)/m)[2]); + line = line <= lines.length ? line : lines.length; + + const marker = { + // monaco.MarkerSeverity.Error, + severity: 8, + startColumn: column - 1 < 0 ? 0 : column - 1, + endColumn: lines[line - 1].length, + startLineNumber: column === 0 ? line - 1 : line, + endLineNumber: column === 0 ? line - 1 : line, + message: "Invalid syntax", + source: "linter", + }; + markers.push(marker); + } catch (e: any) { + markers.push(defaultError(lines)); + } } else { - const marker = { - // monaco.MarkerSeverity.Error, - severity: 8, - startColumn: 0, - endColumn: Number.MAX_SAFE_INTEGER, - startLineNumber: 0, - endLineNumber: lines.length, - message: "Invalid syntax", - source: "linter", - }; - - markers.push(marker); + markers.push(defaultError(lines)); } } diff --git a/src/friendly-to-api.ts b/src/friendly-to-api.ts index 2c14337..411d749 100644 --- a/src/friendly-to-api.ts +++ b/src/friendly-to-api.ts @@ -5,36 +5,36 @@ import { RelationDefOperator, RelationTargetParserResult, RewriteType, - TypeDefParserResult + TypeDefParserResult, } from "./parse-dsl"; import { assertNever } from "./utils/assert-never"; const resolveRelation = (relation: RelationTargetParserResult): Userset => { switch (relation.rewrite) { - case RewriteType.Direct: - return { this: {} }; - case RewriteType.ComputedUserset: - return { - computedUserset: { - object: "", - relation: relation.target, - }, - }; - case RewriteType.TupleToUserset: - return { - tupleToUserset: { - tupleset: { - object: "", - relation: relation.from, - }, + case RewriteType.Direct: + return { this: {} }; + case RewriteType.ComputedUserset: + return { computedUserset: { object: "", relation: relation.target, }, - }, - }; - default: - assertNever(relation.rewrite); + }; + case RewriteType.TupleToUserset: + return { + tupleToUserset: { + tupleset: { + object: "", + relation: relation.from, + }, + computedUserset: { + object: "", + relation: relation.target, + }, + }, + }; + default: + assertNever(relation.rewrite); } }; @@ -44,37 +44,37 @@ export const friendlySyntaxToApiSyntax = (config: string): Required { const relationsMap: Record = {}; - rawRelations.forEach(rawRelation => { + rawRelations.forEach((rawRelation) => { const { relation: relationName, definition } = rawRelation; switch (definition.type) { - case RelationDefOperator.Single: - relationsMap[relationName] = resolveRelation(definition.targets![0]); - break; - case RelationDefOperator.Exclusion: - relationsMap[relationName] = { - difference: { - base: { - ...resolveRelation(definition.base!), - }, - subtract: { - ...resolveRelation(definition.diff!), + case RelationDefOperator.Single: + relationsMap[relationName] = resolveRelation(definition.targets![0]); + break; + case RelationDefOperator.Exclusion: + relationsMap[relationName] = { + difference: { + base: { + ...resolveRelation(definition.base!), + }, + subtract: { + ...resolveRelation(definition.diff!), + }, }, - }, - }; - break; - case RelationDefOperator.Union: - relationsMap[relationName] = { - union: { child: definition.targets!.map(target => resolveRelation(target)) }, - }; - break; - case RelationDefOperator.Intersection: - relationsMap[relationName] = { - intersection: { child: definition.targets!.map(target => resolveRelation(target)) }, - }; - break; - default: - assertNever(definition.type); + }; + break; + case RelationDefOperator.Union: + relationsMap[relationName] = { + union: { child: definition.targets!.map((target) => resolveRelation(target)) }, + }; + break; + case RelationDefOperator.Intersection: + relationsMap[relationName] = { + intersection: { child: definition.targets!.map((target) => resolveRelation(target)) }, + }; + break; + default: + assertNever(definition.type); } }); diff --git a/src/keywords.ts b/src/keywords.ts index 5cbaa7d..918f0e9 100644 --- a/src/keywords.ts +++ b/src/keywords.ts @@ -12,5 +12,5 @@ export enum Keywords { } export enum ReservedKeywords { - THIS = "this" + THIS = "this", } diff --git a/src/parse-dsl.ts b/src/parse-dsl.ts index 0a79443..7f1de29 100644 --- a/src/parse-dsl.ts +++ b/src/parse-dsl.ts @@ -18,7 +18,7 @@ export enum RelationDefOperator { export interface RelationTargetParserResult { target?: string; from?: string; - rewrite: RewriteType + rewrite: RewriteType; } export interface RelationDefParserResult { @@ -29,7 +29,7 @@ export interface RelationDefParserResult { targets: T extends RelationDefOperator.Exclusion ? undefined : RelationTargetParserResult[]; base: T extends RelationDefOperator.Exclusion ? RelationTargetParserResult : undefined; diff: T extends RelationDefOperator.Exclusion ? RelationTargetParserResult : undefined; - } + }; } export interface TypeDefParserResult { @@ -38,7 +38,7 @@ export interface TypeDefParserResult { relations: RelationDefParserResult[]; } -export const parseDSL = (code: string): any[] => { +export const parseDSL = (code: string): TypeDefParserResult[] => { const parser = new Parser(Grammar.fromCompiled(grammar)); parser.feed(code.trim() + "\n"); return parser.results[0] || []; diff --git a/src/reporters.ts b/src/reporters.ts index e93532d..c17bc13 100644 --- a/src/reporters.ts +++ b/src/reporters.ts @@ -1,5 +1,3 @@ -import * as _ from "lodash"; - import { Keywords } from "./keywords"; interface BaseReporterOpts { @@ -10,7 +8,7 @@ interface BaseReporterOpts { } interface ReporterOpts extends BaseReporterOpts { - validRelations?: string[] | Record; + validRelations?: string[] | Record>; clause?: any; typeName?: string; reverse?: boolean; @@ -21,15 +19,15 @@ interface ErrorReporterOpts extends BaseReporterOpts { customResolver?: (wordIdx: number, rawLine: string, value: string) => number; relatedInformation?: { type: string; - } + }; } -const getValidRelationsArray = (validRelations?: string[] | Record, typeName?: string): string[] => { +const getValidRelationsArray = (validRelations?: ReporterOpts["validRelations"], typeName?: string): string[] => { if (!validRelations) { return []; } - return Array.isArray(validRelations) ? - validRelations : typeName ? validRelations?.[typeName] : []; + + return Array.isArray(validRelations) ? validRelations : typeName ? Object.keys(validRelations?.[typeName]) : []; }; const reportError = ({ @@ -42,12 +40,10 @@ const reportError = ({ relatedInformation = { type: "" }, }: ErrorReporterOpts) => { const rawLine = lines[lineIndex]; + const re = new RegExp("\\b" + value + "\\b"); + let wordIdx = rawLine.search(re) + 1; - const asIdx = rawLine.indexOf("as") + 1; - const definition = _.last(rawLine.split("as"))!; - let wordIdx = asIdx + definition.indexOf(value) + 2; - - if (_.isFunction(customResolver)) { + if (typeof customResolver === "function") { wordIdx = customResolver(wordIdx, rawLine, value); } @@ -66,7 +62,7 @@ const reportError = ({ export const reportUseSelf = ({ markers, lines, lineIndex, value }: ReporterOpts) => { reportError({ - message: "For auto-referencing use `self`.", + message: `For auto-referencing use '${Keywords.SELF}'.`, markers, lines, value, @@ -77,7 +73,7 @@ export const reportUseSelf = ({ markers, lines, lineIndex, value }: ReporterOpts export const reportInvalidFrom = ({ markers, lines, lineIndex, value, clause }: ReporterOpts) => { reportError({ - message: `Cannot self-reference (\`${value}\`) within \`from\` clause.`, + message: `Cannot self-reference (\`${value}\`) within \`${Keywords.FROM}\` clause.`, markers, lines, value, @@ -93,7 +89,7 @@ export const reportInvalidFrom = ({ markers, lines, lineIndex, value, clause }: export const reportInvalidButNot = ({ markers, lines, lineIndex, value, clause }: ReporterOpts) => { reportError({ - message: `Cannot self-reference (\`${value}\`) within \`but not\` clause.`, + message: `Cannot self-reference (\`${value}\`) within \`${Keywords.BUT_NOT}\` clause.`, markers, lineIndex, lines, @@ -197,17 +193,23 @@ export const report = function ({ markers, lines }: Pick) => - reportInvalidRelationWithinClause({ - lineIndex, - value, - typeName, - validRelations, - clause, - reverse, - markers, - lines, - }), + invalidRelationWithinClause: ({ + lineIndex, + value, + typeName, + validRelations, + clause, + reverse, + }: Omit) => + reportInvalidRelationWithinClause({ + lineIndex, + value, + typeName, + validRelations, + clause, + reverse, + markers, + lines, + }), }; }; diff --git a/tests/__snapshots__/dsl.test.ts.snap b/tests/__snapshots__/dsl.test.ts.snap index 752eec8..f806b54 100644 --- a/tests/__snapshots__/dsl.test.ts.snap +++ b/tests/__snapshots__/dsl.test.ts.snap @@ -2,19 +2,7 @@ exports[`DSL checkDSL() invalid code should handle \`no definitions\` 1`] = `[]`; -exports[`DSL checkDSL() invalid code should handle \`no relations\` 1`] = ` -[ - { - "endColumn": 9007199254740991, - "endLineNumber": 1, - "message": "Invalid syntax", - "severity": 8, - "source": "linter", - "startColumn": 0, - "startLineNumber": 0, - }, -] -`; +exports[`DSL checkDSL() invalid code should handle \`no relations\` 1`] = `[]`; exports[`DSL checkDSL() invalid keywords should handle invalid \`and\` 1`] = ` [ @@ -114,44 +102,76 @@ exports[`DSL checkDSL() invalid keywords should handle invalid \`self\` 1`] = ` ] `; -exports[`DSL checkDSL() semantics should allow model with relations starting with as 1`] = ` +exports[`DSL checkDSL() semantics should allow model with relations starting with as 1`] = `[]`; + +exports[`DSL checkDSL() semantics should allow reference from other relation 1`] = `[]`; + +exports[`DSL checkDSL() semantics should allow self reference 1`] = `[]`; + +exports[`DSL checkDSL() semantics should be able to handle more than one error 1`] = ` [ { - "endColumn": 9007199254740991, - "endLineNumber": 15, - "message": "Invalid syntax", + "endColumn": 18, + "endLineNumber": 3, + "message": "For auto-referencing use 'self'.", + "relatedInformation": { + "type": "self-error", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 12, + "startLineNumber": 3, }, -] -`; - -exports[`DSL checkDSL() semantics should allow self reference 1`] = ` -[ { - "endColumn": 9007199254740991, - "endLineNumber": 3, - "message": "Invalid syntax", + "endColumn": 33, + "endLineNumber": 5, + "message": "The relation \`relation2\` does not exist.", + "relatedInformation": { + "relation": "relation2", + "type": "missing-definition", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 24, + "startLineNumber": 5, + }, + { + "endColumn": 33, + "endLineNumber": 5, + "message": "The relation \`relation2\` does not exist in type \`group\`", + "relatedInformation": { + "type": "", + }, + "severity": 8, + "source": "linter", + "startColumn": 24, + "startLineNumber": 5, + }, + { + "endColumn": 18, + "endLineNumber": 6, + "message": "For auto-referencing use 'self'.", + "relatedInformation": { + "type": "self-error", + }, + "severity": 8, + "source": "linter", + "startColumn": 12, + "startLineNumber": 6, }, ] `; -exports[`DSL checkDSL() semantics should be able to handle more than one error 1`] = ` +exports[`DSL checkDSL() semantics should gracefully handle error 1`] = ` [ { - "endColumn": 9007199254740991, - "endLineNumber": 6, + "endColumn": 7, + "endLineNumber": 15, "message": "Invalid syntax", "severity": 8, "source": "linter", "startColumn": 0, - "startLineNumber": 0, + "startLineNumber": 15, }, ] `; @@ -159,13 +179,16 @@ exports[`DSL checkDSL() semantics should be able to handle more than one error 1 exports[`DSL checkDSL() semantics should handle duplicated definition 1`] = ` [ { - "endColumn": 9007199254740991, + "endColumn": 26, "endLineNumber": 4, - "message": "Invalid syntax", + "message": "Duplicate definition \`writer\`.", + "relatedInformation": { + "type": "duplicated-error", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 5, + "startLineNumber": 4, }, ] `; @@ -173,13 +196,17 @@ exports[`DSL checkDSL() semantics should handle duplicated definition 1`] = ` exports[`DSL checkDSL() semantics should handle invalid \`invalid and\` 1`] = ` [ { - "endColumn": 9007199254740991, + "endColumn": 37, "endLineNumber": 3, - "message": "Invalid syntax", + "message": "The relation \`reader\` does not exist.", + "relatedInformation": { + "relation": "reader", + "type": "missing-definition", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 31, + "startLineNumber": 3, }, ] `; @@ -187,13 +214,17 @@ exports[`DSL checkDSL() semantics should handle invalid \`invalid and\` 1`] = ` exports[`DSL checkDSL() semantics should handle invalid \`invalid but not\` 1`] = ` [ { - "endColumn": 9007199254740991, + "endColumn": 41, "endLineNumber": 3, - "message": "Invalid syntax", + "message": "The relation \`reader\` does not exist.", + "relatedInformation": { + "relation": "reader", + "type": "missing-definition", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 35, + "startLineNumber": 3, }, ] `; @@ -201,13 +232,29 @@ exports[`DSL checkDSL() semantics should handle invalid \`invalid but not\` 1`] exports[`DSL checkDSL() semantics should handle invalid \`invalid from\` 1`] = ` [ { - "endColumn": 9007199254740991, + "endColumn": 36, "endLineNumber": 3, - "message": "Invalid syntax", + "message": "The relation \`reader\` does not exist.", + "relatedInformation": { + "relation": "reader", + "type": "missing-definition", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 30, + "startLineNumber": 3, + }, + { + "endColumn": 46, + "endLineNumber": 3, + "message": "The relation \`test\` does not exist in type \`group\`", + "relatedInformation": { + "type": "", + }, + "severity": 8, + "source": "linter", + "startColumn": 42, + "startLineNumber": 3, }, ] `; @@ -215,13 +262,17 @@ exports[`DSL checkDSL() semantics should handle invalid \`invalid from\` 1`] = ` exports[`DSL checkDSL() semantics should handle invalid \`invalid or\` 1`] = ` [ { - "endColumn": 9007199254740991, + "endColumn": 36, "endLineNumber": 3, - "message": "Invalid syntax", + "message": "The relation \`reader\` does not exist.", + "relatedInformation": { + "relation": "reader", + "type": "missing-definition", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 30, + "startLineNumber": 3, }, ] `; @@ -229,13 +280,35 @@ exports[`DSL checkDSL() semantics should handle invalid \`invalid or\` 1`] = ` exports[`DSL checkDSL() semantics should handle invalid \`relation not define\` 1`] = ` [ { - "endColumn": 9007199254740991, + "endColumn": 28, "endLineNumber": 3, - "message": "Invalid syntax", + "message": "The relation \`reader\` does not exist.", + "relatedInformation": { + "relation": "reader", + "type": "missing-definition", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 22, + "startLineNumber": 3, + }, +] +`; + +exports[`DSL checkDSL() semantics should handle invalid \`relation not define\` where name is substring of other word 1`] = ` +[ + { + "endColumn": 25, + "endLineNumber": 3, + "message": "The relation \`def\` does not exist.", + "relatedInformation": { + "relation": "def", + "type": "missing-definition", + }, + "severity": 8, + "source": "linter", + "startColumn": 22, + "startLineNumber": 3, }, ] `; @@ -243,13 +316,16 @@ exports[`DSL checkDSL() semantics should handle invalid \`relation not define\` exports[`DSL checkDSL() semantics should handle invalid \`self-error\` 1`] = ` [ { - "endColumn": 9007199254740991, + "endColumn": 18, "endLineNumber": 3, - "message": "Invalid syntax", + "message": "For auto-referencing use 'self'.", + "relatedInformation": { + "type": "self-error", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 12, + "startLineNumber": 3, }, ] `; @@ -257,13 +333,34 @@ exports[`DSL checkDSL() semantics should handle invalid \`self-error\` 1`] = ` exports[`DSL checkDSL() semantics should handle invalid \`self-ref in but not\` 1`] = ` [ { - "endColumn": 9007199254740991, + "endColumn": 18, "endLineNumber": 3, - "message": "Invalid syntax", + "message": "For auto-referencing use 'self'.", + "relatedInformation": { + "type": "self-error", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 12, + "startLineNumber": 3, + }, +] +`; + +exports[`DSL checkDSL() semantics should identify correct error line number if there are spaces 1`] = ` +[ + { + "endColumn": 28, + "endLineNumber": 5, + "message": "The relation \`reader\` does not exist.", + "relatedInformation": { + "relation": "reader", + "type": "missing-definition", + }, + "severity": 8, + "source": "linter", + "startColumn": 22, + "startLineNumber": 5, }, ] `; @@ -271,17 +368,22 @@ exports[`DSL checkDSL() semantics should handle invalid \`self-ref in but not\` exports[`DSL checkDSL() semantics should not allow impossible self reference 1`] = ` [ { - "endColumn": 9007199254740991, + "endColumn": 18, "endLineNumber": 3, - "message": "Invalid syntax", + "message": "Cannot self-reference (\`member\`) within \`from\` clause.", + "relatedInformation": { + "type": "", + }, "severity": 8, "source": "linter", - "startColumn": 0, - "startLineNumber": 0, + "startColumn": 12, + "startLineNumber": 3, }, ] `; +exports[`DSL checkDSL() should correctly parse a simple sample 1`] = `[]`; + exports[`DSL parseDSL() should correctly parse a complex sample 1`] = ` [ { diff --git a/tests/dsl.test.ts b/tests/dsl.test.ts index 35eab31..6588898 100644 --- a/tests/dsl.test.ts +++ b/tests/dsl.test.ts @@ -50,6 +50,22 @@ type app }); describe("checkDSL()", () => { + it("should correctly parse a simple sample", () => { + const markers = checkDSL(`type group + relations + define member as self + +type document + relations + define writer as self + define reader as writer or self + define commenter as member from writer + define owner as writer + define super as reader but not member from writer`); + + expect(markers).toMatchSnapshot(); + }); + describe("invalid code", () => { it("should handle `no relations`", () => { const markers = checkDSL("type group"); @@ -128,6 +144,23 @@ type app it("should handle invalid `relation not define`", () => { const markers = checkDSL(`type group relations + define writer as reader`); + expect(markers).toMatchSnapshot(); + }); + + it("should handle invalid `relation not define` where name is substring of other word", () => { + const markers = checkDSL(`type group + relations + define writer as def`); + expect(markers).toMatchSnapshot(); + }); + + + it("should identify correct error line number if there are spaces", () => { + const markers = checkDSL(`type group + relations + + define owner as self define writer as reader`); expect(markers).toMatchSnapshot(); }); @@ -185,6 +218,14 @@ type app expect(markers).toMatchSnapshot(); }); + it("should allow reference from other relation", () => { + const markers = checkDSL(`type group + relations + define foo as self + define member as self or member from foo`); + expect(markers).toMatchSnapshot(); + }); + it("should allow self reference", () => { const markers = checkDSL(`type group relations @@ -217,6 +258,27 @@ type permission define associated_feature as self`); expect(markers).toMatchSnapshot(); }); + + it("should gracefully handle error", () => { + const markers = checkDSL(`type group + relations + define member as self + define admin as self + + define can_add as can_manage_group + define can_edit as can_manage_group + define can_delete as can_manage_group + define can_read as can_manage + + define can_manage_group as admin + define can_manage_users as admin + define can_view_group as admin or member + define can_view_users as admin or member + + def`); + expect(markers).toMatchSnapshot(); + }); + }); }); });