diff --git a/eslint.config.mts b/eslint.config.mts index ae2c2ac..a20727f 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -37,6 +37,19 @@ export default tseslint.config( "@typescript-eslint/no-empty-object-type": "off", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/only-throw-error": "off", + "@typescript-eslint/consistent-type-definitions": ["error", "type"], + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], "@typescript-eslint/restrict-template-expressions": [ "error", { allowNumber: true }, diff --git a/package.json b/package.json index d3dc418..84b9586 100644 --- a/package.json +++ b/package.json @@ -57,13 +57,13 @@ "dts": true }, "dependencies": { - "@types/parsimmon": "^1.10.9", "parsimmon": "^1.18.1" }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.2", "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", + "@types/parsimmon": "^1.10.9", "@typescript-eslint/parser": "^8.48.1", "eslint": "^9.39.1", "eslint-import-resolver-typescript": "^4.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2648efd..6981f72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@types/parsimmon': - specifier: ^1.10.9 - version: 1.10.9 parsimmon: specifier: ^1.18.1 version: 1.18.1 @@ -24,6 +21,9 @@ importers: '@types/node': specifier: ^24.10.1 version: 24.10.1 + '@types/parsimmon': + specifier: ^1.10.9 + version: 1.10.9 '@typescript-eslint/parser': specifier: ^8.48.1 version: 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) diff --git a/src/dsl.test.ts b/src/dsl.test.ts index 92504d3..2e2a50d 100644 --- a/src/dsl.test.ts +++ b/src/dsl.test.ts @@ -1,15 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { - ParsedArrowExpression, - ParsedBinaryExpression, - ParsedCaveatDefinition, - ParsedNamedArrowExpression, - ParsedNilExpression, - ParsedObjectDefinition, - ParsedRelationRefExpression, - ParsedUseFlag, -} from "./dsl"; -import { parseSchema } from "./dsl"; +import { parseSchema, stringLiteral } from "./dsl"; // Takes an expression as an argument and throws if that assertion fails. // This primarily exists to provide typescript narrowing in a statement, @@ -26,428 +16,553 @@ describe("parsing", () => { expect(parsed?.definitions.length).toEqual(0); }); - it("parses use flag", () => { - const schema = `use expiration`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - expect((parsed?.definitions[0] as ParsedUseFlag).featureName).toEqual( - "expiration", - ); - }); - - it("parses use flag and definition", () => { - const schema = `use expiration - - definition foo {} - `; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(2); - }); - - it("parses empty definition", () => { - const schema = `definition foo {}`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - expect((parsed?.definitions[0] as ParsedObjectDefinition).name).toEqual( - "foo", - ); - }); - - it("parses empty definition with multiple path segements", () => { - const schema = `definition foo/bar/baz {}`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - expect((parsed?.definitions[0] as ParsedObjectDefinition).name).toEqual( - "foo/bar/baz", - ); - }); - - it("parses basic caveat", () => { - const schema = `caveat foo (someParam string, anotherParam int) { someParam == 42 }`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - expect((parsed?.definitions[0] as ParsedCaveatDefinition).name).toEqual( - "foo", - ); - - const definition = parsed?.definitions[0] as ParsedCaveatDefinition; - expect(definition.parameters.map((p) => p.name)).toEqual([ - "someParam", - "anotherParam", - ]); - }); - - it("parses caveat with generic parameter type", () => { - const schema = `caveat foo (someParam string, anotherParam map) { + describe("use flags", () => { + it("parses use flag", () => { + const schema = `use expiration`; + const parsed = parseSchema(schema); + + expect(parsed?.definitions).toHaveLength(1); + const useFlag = parsed?.definitions[0]; + assert(useFlag); + assert(useFlag.kind === "use"); + expect(useFlag.featureName).toEqual("expiration"); + }); + + it("parses use flag and definition", () => { + const schema = `use expiration + +definition foo {} +`; + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(2); + }); + }); + + describe("imports", () => { + it("parses basic import", () => { + const schema = `import "foo/bar/baz.zed"`; + const parsed = parseSchema(schema); + + expect(parsed?.definitions).toHaveLength(1); + const imPort = parsed?.definitions[0]; + assert(imPort); + assert(imPort.kind === "import"); + expect(imPort.path).toEqual("foo/bar/baz.zed"); + }); + it("parses import and definition", () => { + const schema = `import "foo/bar/baz.zed" +definition user {} +`; + const parsed = parseSchema(schema); + + expect(parsed?.definitions).toHaveLength(2); + const imPort = parsed?.definitions[0]; + assert(imPort); + assert(imPort.kind === "import"); + expect(imPort.path).toEqual("foo/bar/baz.zed"); + + const definition = parsed.definitions[1]; + assert(definition); + assert(definition.kind === "objectDef"); + }); + it("rejects too many things on line", () => { + const schema = `import "foo/bar/baz.zed" yolo`; + const parsed = parseSchema(schema); + expect(parsed).toBeUndefined(); + }); + it("rejects malformed import string", () => { + const schema = `import "foo/bar/baz.zed"yolo`; + const parsed = parseSchema(schema); + expect(parsed).toBeUndefined(); + }); + }); + + describe("definitions", () => { + it("parses empty definition", () => { + const schema = `definition foo {}`; + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + const definition = parsed?.definitions[0]; + assert(definition); + assert(definition.kind === "objectDef"); + expect(definition.name).toEqual("foo"); + }); + + it("parses empty definition with multiple path segements", () => { + const schema = `definition foo/bar/baz {}`; + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + const definition = parsed?.definitions[0]; + assert(definition); + assert(definition.kind === "objectDef"); + expect(definition.name).toEqual("foo/bar/baz"); + }); + + it("parses basic caveat", () => { + const schema = `caveat foo (someParam string, anotherParam int) { someParam == 42 }`; + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + const caveat = parsed?.definitions[0]; + assert(caveat); + assert(caveat.kind === "caveatDef"); + expect(caveat.name).toEqual("foo"); + + expect(caveat.parameters.map((p) => p.name)).toEqual([ + "someParam", + "anotherParam", + ]); + }); + + it("parses caveat with generic parameter type", () => { + const schema = `caveat foo (someParam string, anotherParam map) { someParam == 'hi' }`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - expect((parsed?.definitions[0] as ParsedCaveatDefinition).name).toEqual( - "foo", - ); - const definition = parsed?.definitions[0] as ParsedCaveatDefinition; - expect(definition.parameters.map((p) => p.name)).toEqual([ - "someParam", - "anotherParam", - ]); - }); - - it("parses empty definition with path", () => { - const schema = `definition foo/bar {}`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - expect((parsed?.definitions[0] as ParsedObjectDefinition).name).toEqual( - "foo/bar", - ); - }); - - it("parses multiple definitions", () => { - const schema = `definition foo {} + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + const caveat = parsed?.definitions[0]; + assert(caveat); + assert(caveat.kind === "caveatDef"); + expect(caveat.name).toEqual("foo"); + expect(caveat.parameters.map((p) => p.name)).toEqual([ + "someParam", + "anotherParam", + ]); + }); + + it("parses empty definition with path", () => { + const schema = `definition foo/bar {}`; + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + const definition = parsed?.definitions[0]; + assert(definition); + assert(definition.kind === "objectDef"); + expect(definition.name).toEqual("foo/bar"); + }); + + it("parses multiple definitions", () => { + const schema = `definition foo {} definition bar {}`; - const parsed = parseSchema(schema); + const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); + expect(parsed?.definitions).toHaveLength(2); + const definitionOne = parsed?.definitions[0]; + assert(definitionOne); + assert(definitionOne.kind === "objectDef"); - expect((parsed?.definitions[0] as ParsedCaveatDefinition).name).toEqual( - "foo", - ); - expect((parsed?.definitions[1] as ParsedCaveatDefinition).name).toEqual( - "bar", - ); - }); + expect(definitionOne.name).toEqual("foo"); - it("parses relation with expiration", () => { - const schema = ` + const definitionTwo = parsed.definitions[1]; + assert(definitionTwo); + assert(definitionTwo.kind === "objectDef"); + expect(definitionTwo.name).toEqual("bar"); + }); + + it("parses relation with expiration", () => { + const schema = ` use expiration definition foo { relation viewer: user with expiration }`; - const parsed = parseSchema(schema); + const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); - }); + expect(parsed?.definitions.length).toEqual(2); + }); - it("parses relation with caveat and expiration", () => { - const schema = ` + it("parses relation with caveat and expiration", () => { + const schema = ` use expiration definition foo { relation viewer: user with somecaveat and expiration }`; - const parsed = parseSchema(schema); + const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); - }); + expect(parsed?.definitions.length).toEqual(2); + }); - it("parses definition with relation", () => { - const schema = `definition foo { + it("parses definition with relation", () => { + const schema = `definition foo { relation barrel: something; }`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(1); - - const relation = definition.relations[0]; - assert(relation); - expect(relation.name).toEqual("barrel"); - expect(relation.allowedTypes.types.length).toEqual(1); - assert(relation.allowedTypes.types[0]); - expect(relation.allowedTypes.types[0].path).toEqual("something"); - }); - - it("parses definition with caveated relation", () => { - const schema = `definition foo { + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + + const definition = parsed?.definitions[0]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("foo"); + expect(definition.relations.length).toEqual(1); + + const relation = definition.relations[0]; + assert(relation); + expect(relation.name).toEqual("barrel"); + expect(relation.allowedTypes.types.length).toEqual(1); + assert(relation.allowedTypes.types[0]); + expect(relation.allowedTypes.types[0].path).toEqual("something"); + }); + + it("parses definition with caveated relation", () => { + const schema = `definition foo { relation barrel: something with somecaveat; }`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(1); - - const relation = definition.relations[0]; - assert(relation); - expect(relation.name).toEqual("barrel"); - expect(relation.allowedTypes.types.length).toEqual(1); - assert(relation.allowedTypes.types[0]); - expect(relation.allowedTypes.types[0].path).toEqual("something"); - - expect(relation.allowedTypes.types[0].withCaveat?.path).toEqual( - "somecaveat", - ); - }); - - it("parses definition with prefixed caveated relation", () => { - const schema = `definition foo { + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + + const definition = parsed?.definitions[0]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("foo"); + expect(definition.relations.length).toEqual(1); + + const relation = definition.relations[0]; + assert(relation); + expect(relation.name).toEqual("barrel"); + expect(relation.allowedTypes.types.length).toEqual(1); + assert(relation.allowedTypes.types[0]); + expect(relation.allowedTypes.types[0].path).toEqual("something"); + + expect(relation.allowedTypes.types[0].withCaveat?.path).toEqual( + "somecaveat", + ); + }); + + it("parses definition with prefixed caveated relation", () => { + const schema = `definition foo { relation barrel: something with test/somecaveat; }`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(1); - - const relation = definition.relations[0]; - assert(relation); - expect(relation.name).toEqual("barrel"); - expect(relation.allowedTypes.types.length).toEqual(1); - assert(relation.allowedTypes.types[0]); - expect(relation.allowedTypes.types[0].path).toEqual("something"); - - expect(relation.allowedTypes.types[0].withCaveat?.path).toEqual( - "test/somecaveat", - ); - }); - - it("parses definition with subject wildcard type", () => { - const schema = `definition foo { + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + + const definition = parsed?.definitions[0]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("foo"); + expect(definition.relations.length).toEqual(1); + + const relation = definition.relations[0]; + assert(relation); + expect(relation.name).toEqual("barrel"); + expect(relation.allowedTypes.types.length).toEqual(1); + assert(relation.allowedTypes.types[0]); + expect(relation.allowedTypes.types[0].path).toEqual("something"); + + expect(relation.allowedTypes.types[0].withCaveat?.path).toEqual( + "test/somecaveat", + ); + }); + + it("parses definition with subject wildcard type", () => { + const schema = `definition foo { relation barrel: something:*; }`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(1); - - const relation = definition.relations[0]; - assert(relation); - expect(relation.name).toEqual("barrel"); - expect(relation.allowedTypes.types.length).toEqual(1); - assert(relation.allowedTypes.types[0]); - expect(relation.allowedTypes.types[0].path).toEqual("something"); - expect(relation.allowedTypes.types[0].relationName).toEqual(undefined); - expect(relation.allowedTypes.types[0].wildcard).toEqual(true); - }); - - it("parses definition with subject rel type", () => { - const schema = `definition foo { + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + + const definition = parsed?.definitions[0]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("foo"); + expect(definition.relations.length).toEqual(1); + + const relation = definition.relations[0]; + assert(relation); + expect(relation.name).toEqual("barrel"); + expect(relation.allowedTypes.types.length).toEqual(1); + assert(relation.allowedTypes.types[0]); + expect(relation.allowedTypes.types[0].path).toEqual("something"); + expect(relation.allowedTypes.types[0].relationName).toEqual(undefined); + expect(relation.allowedTypes.types[0].wildcard).toEqual(true); + }); + + it("parses definition with subject rel type", () => { + const schema = `definition foo { relation barrel: something#foo; }`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(1); - - const relation = definition.relations[0]; - assert(relation); - expect(relation.name).toEqual("barrel"); - expect(relation.allowedTypes.types.length).toEqual(1); - assert(relation.allowedTypes.types[0]); - expect(relation.allowedTypes.types[0].path).toEqual("something"); - expect(relation.allowedTypes.types[0].relationName).toEqual("foo"); - }); - - it("parses definition with relation with multiple types", () => { - const schema = `definition foo { + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + + const definition = parsed?.definitions[0]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("foo"); + expect(definition.relations.length).toEqual(1); + + const relation = definition.relations[0]; + assert(relation); + expect(relation.name).toEqual("barrel"); + expect(relation.allowedTypes.types.length).toEqual(1); + assert(relation.allowedTypes.types[0]); + expect(relation.allowedTypes.types[0].path).toEqual("something"); + expect(relation.allowedTypes.types[0].relationName).toEqual("foo"); + }); + + it("parses definition with relation with multiple types", () => { + const schema = `definition foo { relation barrel: something | somethingelse | thirdtype; }`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(1); - - const relation = definition.relations[0]; - assert(relation); - expect(relation.name).toEqual("barrel"); - expect(relation.allowedTypes.types.length).toEqual(3); - assert(relation.allowedTypes.types[0]); - expect(relation.allowedTypes.types[0].path).toEqual("something"); - assert(relation.allowedTypes.types[1]); - expect(relation.allowedTypes.types[1].path).toEqual("somethingelse"); - assert(relation.allowedTypes.types[2]); - expect(relation.allowedTypes.types[2].path).toEqual("thirdtype"); - }); - - it("parses definition with multiple relations", () => { - const schema = `definition foo { + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + + const definition = parsed?.definitions[0]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("foo"); + expect(definition.relations.length).toEqual(1); + + const relation = definition.relations[0]; + assert(relation); + expect(relation.name).toEqual("barrel"); + expect(relation.allowedTypes.types.length).toEqual(3); + assert(relation.allowedTypes.types[0]); + expect(relation.allowedTypes.types[0].path).toEqual("something"); + assert(relation.allowedTypes.types[1]); + expect(relation.allowedTypes.types[1].path).toEqual("somethingelse"); + assert(relation.allowedTypes.types[2]); + expect(relation.allowedTypes.types[2].path).toEqual("thirdtype"); + }); + + it("parses definition with multiple relations", () => { + const schema = `definition foo { relation first: something relation second: somethingelse }`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(2); - - const first = definition.relations[0]; - assert(first); - expect(first.name).toEqual("first"); - expect(first.allowedTypes.types.length).toEqual(1); - assert(first.allowedTypes.types[0]); - expect(first.allowedTypes.types[0].path).toEqual("something"); - - const second = definition.relations[1]; - assert(second); - expect(second.name).toEqual("second"); - expect(second.allowedTypes.types.length).toEqual(1); - assert(second.allowedTypes.types[0]); - expect(second.allowedTypes.types[0].path).toEqual("somethingelse"); - }); - - it("parses definition with a permission", () => { - const schema = `definition foo { + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + + const definition = parsed?.definitions[0]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("foo"); + expect(definition.relations.length).toEqual(2); + + const first = definition.relations[0]; + assert(first); + expect(first.name).toEqual("first"); + expect(first.allowedTypes.types.length).toEqual(1); + assert(first.allowedTypes.types[0]); + expect(first.allowedTypes.types[0].path).toEqual("something"); + + const second = definition.relations[1]; + assert(second); + expect(second.name).toEqual("second"); + expect(second.allowedTypes.types.length).toEqual(1); + assert(second.allowedTypes.types[0]); + expect(second.allowedTypes.types[0].path).toEqual("somethingelse"); + }); + + it("parses definition with a permission", () => { + const schema = `definition foo { permission first = someexpr; }`; - const parsed = parseSchema(schema); + const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(1); + expect(parsed?.definitions.length).toEqual(1); - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(0); - expect(definition.permissions.length).toEqual(1); + const definition = parsed?.definitions[0]; + assert(definition); + assert(definition.kind === "objectDef"); + expect(definition.name).toEqual("foo"); + expect(definition.relations.length).toEqual(0); + expect(definition.permissions.length).toEqual(1); - const permission = definition.permissions[0]; - assert(permission); - expect(permission.name).toEqual("first"); - }); + const permission = definition.permissions[0]; + assert(permission); + expect(permission.name).toEqual("first"); + }); - it("parses definition with associativity matching the schema parser in Go", () => { - const schema = `definition foo { + it("parses definition with associativity matching the schema parser in Go", () => { + const schema = `definition foo { permission first = a - b + c }`; - const parsed = parseSchema(schema); + const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(1); + expect(parsed?.definitions.length).toEqual(1); - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(0); - expect(definition.permissions.length).toEqual(1); + const definition = parsed?.definitions[0]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("foo"); + expect(definition.relations.length).toEqual(0); + expect(definition.permissions.length).toEqual(1); - const permission = definition.permissions[0]; - assert(permission); - expect(permission.name).toEqual("first"); + const permission = definition.permissions[0]; + assert(permission); + expect(permission.name).toEqual("first"); - const binExpr = permission.expr; - assert(binExpr.kind === "binary"); - expect(binExpr.operator).toEqual("exclusion"); + const binExpr = permission.expr; + assert(binExpr.kind === "binary"); + expect(binExpr.operator).toEqual("exclusion"); - const leftExpr = binExpr.left; - assert(leftExpr.kind === "relationref"); - expect(leftExpr.relationName).toEqual("a"); - }); + const leftExpr = binExpr.left; + assert(leftExpr.kind === "relationref"); + expect(leftExpr.relationName).toEqual("a"); + }); - it("parses definition with a complex permission", () => { - const schema = `definition foo { + it("parses definition with a complex permission", () => { + const schema = `definition foo { permission first = ((a - b) + nil) & d; }`; - const parsed = parseSchema(schema); + const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(1); + expect(parsed?.definitions.length).toEqual(1); - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(0); - expect(definition.permissions.length).toEqual(1); + const definition = parsed?.definitions[0]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("foo"); + expect(definition.relations.length).toEqual(0); + expect(definition.permissions.length).toEqual(1); - const permission = definition.permissions[0]; - assert(permission); - expect(permission.name).toEqual("first"); + const permission = definition.permissions[0]; + assert(permission); + expect(permission.name).toEqual("first"); - // TODO: refactor these to use assertions.. - const binExpr = permission.expr as ParsedBinaryExpression; - expect(binExpr.operator).toEqual("intersection"); + // TODO: refactor these to use assertions.. + const binExpr = permission.expr; + assert(binExpr.kind === "binary"); + expect(binExpr.operator).toEqual("intersection"); - const leftExpr = binExpr.left as ParsedBinaryExpression; - expect(leftExpr.operator).toEqual("union"); + const leftExpr = binExpr.left; + assert(leftExpr.kind === "binary"); + expect(leftExpr.operator).toEqual("union"); - const leftLeftExpr = leftExpr.left as ParsedBinaryExpression; + const leftLeftExpr = leftExpr.left; + assert(leftLeftExpr.kind === "binary"); - const leftLeftLeftExpr = leftLeftExpr.left as ParsedRelationRefExpression; - expect(leftLeftLeftExpr.relationName).toEqual("a"); + const leftLeftLeftExpr = leftLeftExpr.left; + assert(leftLeftLeftExpr.kind === "relationref"); + expect(leftLeftLeftExpr.relationName).toEqual("a"); - const rightLeftLeftExpr = leftLeftExpr.right as ParsedRelationRefExpression; - expect(rightLeftLeftExpr.relationName).toEqual("b"); + const rightLeftLeftExpr = leftLeftExpr.right; + assert(rightLeftLeftExpr.kind === "relationref"); + expect(rightLeftLeftExpr.relationName).toEqual("b"); - const rightLeftExpr = leftExpr.right as ParsedNilExpression; - expect(rightLeftExpr.isNil).toEqual(true); + const rightLeftExpr = leftExpr.right; + assert(rightLeftExpr.kind === "nil"); + expect(rightLeftExpr.isNil).toEqual(true); - const rightExpr = binExpr.right as ParsedRelationRefExpression; - expect(rightExpr.relationName).toEqual("d"); - }); + const rightExpr = binExpr.right; + assert(rightExpr.kind === "relationref"); + expect(rightExpr.relationName).toEqual("d"); + }); - it("parses definition with multiple permissions", () => { - const schema = `definition foo { + it("parses definition with multiple permissions", () => { + const schema = `definition foo { permission first = firstrel permission second = secondrel }`; - const parsed = parseSchema(schema); - - expect(parsed?.definitions.length).toEqual(1); - - const definition = parsed?.definitions[0]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("foo"); - expect(definition.relations.length).toEqual(0); - expect(definition.permissions.length).toEqual(2); - - const first = definition.permissions[0]; - assert(first); - expect(first.name).toEqual("first"); - - const firstExpr = first.expr as ParsedRelationRefExpression; - expect(firstExpr.relationName).toEqual("firstrel"); - - const second = definition.permissions[1]; - assert(second); - expect(second.name).toEqual("second"); - - const secondExpr = second.expr as ParsedRelationRefExpression; - expect(secondExpr.relationName).toEqual("secondrel"); - }); - - it("full", () => { - const schema = `use expiration + const parsed = parseSchema(schema); + + expect(parsed?.definitions.length).toEqual(1); + + const definition = parsed?.definitions[0]; + assert(definition); + assert(definition.kind === "objectDef"); + expect(definition.name).toEqual("foo"); + expect(definition.relations).toHaveLength(0); + expect(definition.permissions).toHaveLength(2); + + const first = definition.permissions[0]; + assert(first); + expect(first.name).toEqual("first"); + + const firstExpr = first.expr; + assert(firstExpr.kind === "relationref"); + expect(firstExpr.relationName).toEqual("firstrel"); + + const second = definition.permissions[1]; + assert(second); + expect(second.name).toEqual("second"); + + const secondExpr = second.expr; + assert(secondExpr.kind === "relationref"); + expect(secondExpr.relationName).toEqual("secondrel"); + }); + }); + + describe("partial syntax", () => { + it("parses a basic partial", () => { + const schema = `partial thing { +relation user: user +permission view = user +}`; + const parsed = parseSchema(schema); + expect(parsed?.definitions).toHaveLength(1); + const partial = parsed?.definitions[0]; + assert(partial); + assert(partial.kind === "partial"); + expect(partial.name).toEqual("thing"); + expect(partial.relations).toHaveLength(1); + expect(partial.permissions).toHaveLength(1); + + const relation = partial.relations[0]; + assert(relation); + expect(relation.name).toEqual("user"); + + const permission = partial.permissions[0]; + assert(permission); + expect(permission.name).toEqual("view"); + }); + it("parses a basic partial reference", () => { + const schema = `definition thing { +...some_partial +}`; + const parsed = parseSchema(schema); + expect(parsed?.definitions).toHaveLength(1); + const definition = parsed?.definitions[0]; + assert(definition); + assert(definition.kind === "objectDef"); + expect(definition.partialReferences).toHaveLength(1); + assert(definition.partialReferences[0]); + expect(definition.partialReferences[0].name).toEqual("some_partial"); + }); + it("fails on a basic partial reference with extra stuff", () => { + const schema = `definition thing { +...some_partial yolo +}`; + const parsed = parseSchema(schema); + expect(parsed).toBeUndefined(); + }); + it("fails on a basic partial reference with disallowed chars", () => { + const schema = `definition thing { +...some'schtuff +}`; + const parsed = parseSchema(schema); + expect(parsed).toBeUndefined(); + }); + it("passes on a partial with a partial reference inside", () => { + const schema = `partial thing { +...some_partial +}`; + const parsed = parseSchema(schema); + expect(parsed?.definitions).toHaveLength(1); + const partial = parsed?.definitions[0]; + assert(partial); + assert(partial.kind === "partial"); + expect(partial.partialReferences).toHaveLength(1); + assert(partial.partialReferences[0]); + expect(partial.partialReferences[0].name).toEqual("some_partial"); + }); + }); + + describe("full schemas", () => { + it("full", () => { + const schema = `use expiration definition user {} caveat somecaveat(somecondition int) { @@ -465,12 +580,12 @@ describe("parsing", () => { permission read = reader + writer // has both }`; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(4); - }); + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(4); + }); - it("full with wildcard", () => { - const schema = `definition user {} + it("full with wildcard", () => { + const schema = `definition user {} /** * a document @@ -483,17 +598,19 @@ describe("parsing", () => { permission read = reader + writer // has both }`; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); - const documentDef = parsed?.definitions.find( - (def) => (def as ParsedObjectDefinition).name === "document", - ) as ParsedObjectDefinition; - expect(documentDef.relations.length).toEqual(2); - expect(documentDef.permissions.length).toEqual(2); - }); - - it("full with more comments", () => { - const schema = `definition user {} + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(2); + const documentDef = parsed?.definitions.find( + (def) => def.kind === "objectDef" && def.name === "document", + ); + assert(documentDef); + assert(documentDef.kind === "objectDef"); + expect(documentDef.relations.length).toEqual(2); + expect(documentDef.permissions.length).toEqual(2); + }); + + it("full with more comments", () => { + const schema = `definition user {} /** * a document @@ -508,12 +625,12 @@ describe("parsing", () => { permission read = reader + writer // has both }`; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); - }); + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(2); + }); - it("parses a real example", () => { - const schema = `definition user {} + it("parses a real example", () => { + const schema = `definition user {} definition collection { relation curator: user @@ -529,12 +646,12 @@ describe("parsing", () => { permission share = curator }`; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); - }); + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(2); + }); - it("parses an arrow expression", () => { - const schema = `definition user {} + it("parses an arrow expression", () => { + const schema = `definition user {} definition organization { relation admin: user; @@ -547,31 +664,34 @@ describe("parsing", () => { permission read = reader + org->admin }`; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(3); - - const definition = parsed?.definitions[2]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("document"); - expect(definition.relations.length).toEqual(2); - expect(definition.permissions.length).toEqual(1); - - const read = definition.permissions[0]; - assert(read); - expect(read.name).toEqual("read"); - - const expr = read.expr as ParsedBinaryExpression; - const leftExpr = expr.left as ParsedRelationRefExpression; - expect(leftExpr.relationName).toEqual("reader"); - - const rightExpr = expr.right as ParsedArrowExpression; - expect(rightExpr.sourceRelation.relationName).toEqual("org"); - expect(rightExpr.targetRelationOrPermission).toEqual("admin"); - }); + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(3); + + const definition = parsed?.definitions[2]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("document"); + expect(definition.relations.length).toEqual(2); + expect(definition.permissions.length).toEqual(1); + + const read = definition.permissions[0]; + assert(read); + expect(read.name).toEqual("read"); + + const expr = read.expr; + assert(expr.kind === "binary"); + const leftExpr = expr.left; + assert(leftExpr.kind === "relationref"); + expect(leftExpr.relationName).toEqual("reader"); - it("parses a named arrow expression", () => { - const schema = `definition user {} + const rightExpr = expr.right; + assert(rightExpr.kind === "arrow"); + expect(rightExpr.sourceRelation.relationName).toEqual("org"); + expect(rightExpr.targetRelationOrPermission).toEqual("admin"); + }); + + it("parses a named arrow expression", () => { + const schema = `definition user {} definition organization { relation admin: user; @@ -584,32 +704,35 @@ describe("parsing", () => { permission read = reader + org.any(admin) }`; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(3); - - const definition = parsed?.definitions[2]; - assert(definition); - assert("relations" in definition); - expect(definition.name).toEqual("document"); - expect(definition.relations.length).toEqual(2); - expect(definition.permissions.length).toEqual(1); - - const read = definition.permissions[0]; - assert(read); - expect(read.name).toEqual("read"); - - const expr = read.expr as ParsedBinaryExpression; - const leftExpr = expr.left as ParsedRelationRefExpression; - expect(leftExpr.relationName).toEqual("reader"); - - const rightExpr = expr.right as ParsedNamedArrowExpression; - expect(rightExpr.sourceRelation.relationName).toEqual("org"); - expect(rightExpr.functionName).toEqual("any"); - expect(rightExpr.targetRelationOrPermission).toEqual("admin"); - }); - - it("parses an example with multiple comments", () => { - const schema = ` + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(3); + + const definition = parsed?.definitions[2]; + assert(definition); + assert("relations" in definition); + expect(definition.name).toEqual("document"); + expect(definition.relations.length).toEqual(2); + expect(definition.permissions.length).toEqual(1); + + const read = definition.permissions[0]; + assert(read); + expect(read.name).toEqual("read"); + + const expr = read.expr; + assert(expr.kind === "binary"); + const leftExpr = expr.left; + assert(leftExpr.kind === "relationref"); + expect(leftExpr.relationName).toEqual("reader"); + + const rightExpr = expr.right; + assert(rightExpr.kind === "namedarrow"); + expect(rightExpr.sourceRelation.relationName).toEqual("org"); + expect(rightExpr.functionName).toEqual("any"); + expect(rightExpr.targetRelationOrPermission).toEqual("admin"); + }); + + it("parses an example with multiple comments", () => { + const schema = ` /** * This is a user definition */ @@ -618,12 +741,12 @@ describe("parsing", () => { /** doc */ definition document {}`; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); - }); + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(2); + }); - it("parses correctly with synthetic semicolon", () => { - const schema = ` + it("parses correctly with synthetic semicolon", () => { + const schema = ` definition document { permission foo = (first + second) permission bar = third @@ -632,15 +755,16 @@ describe("parsing", () => { definition user {} `; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); - expect( - (parsed?.definitions[0] as ParsedObjectDefinition).permissions.length, - ).toEqual(2); - }); + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(2); + const definition = parsed?.definitions[0]; + assert(definition); + assert(definition.kind === "objectDef"); + expect(definition.permissions.length).toEqual(2); + }); - it("parses correctly with synthetic semicolon and comment after", () => { - const schema = ` + it("parses correctly with synthetic semicolon and comment after", () => { + const schema = ` definition document { relation foo: user permission resolve = foo + (bar) @@ -650,18 +774,17 @@ describe("parsing", () => { definition user {} `; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); - expect( - (parsed?.definitions[0] as ParsedObjectDefinition).permissions.length, - ).toEqual(1); - expect( - (parsed?.definitions[0] as ParsedObjectDefinition).relations.length, - ).toEqual(1); - }); - - it("parses wildcard relation correctly with synthetic semicolon and comment after", () => { - const schema = ` + const parsed = parseSchema(schema); + expect(parsed?.definitions).toHaveLength(2); + const definition = parsed?.definitions[0]; + assert(definition); + assert(definition.kind === "objectDef"); + expect(definition.permissions).toHaveLength(1); + expect(definition.relations).toHaveLength(1); + }); + + it("parses wildcard relation correctly with synthetic semicolon and comment after", () => { + const schema = ` definition document { relation foo: user:* permission resolve = foo + (bar) @@ -671,18 +794,17 @@ describe("parsing", () => { definition user {} `; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); - expect( - (parsed?.definitions[0] as ParsedObjectDefinition).permissions.length, - ).toEqual(1); - expect( - (parsed?.definitions[0] as ParsedObjectDefinition).relations.length, - ).toEqual(1); - }); - - it("parses correctly with synthetic semicolon and comment before", () => { - const schema = ` + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(2); + const definition = parsed?.definitions[0]; + assert(definition); + assert(definition.kind === "objectDef"); + expect(definition.permissions.length).toEqual(1); + expect(definition.relations.length).toEqual(1); + }); + + it("parses correctly with synthetic semicolon and comment before", () => { + const schema = ` definition document { permission resolve = foo + (bar) // a comment @@ -692,22 +814,24 @@ describe("parsing", () => { definition user {} `; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); - expect( - (parsed?.definitions[0] as ParsedObjectDefinition).permissions.length, - ).toEqual(2); - }); + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(2); + const definition = parsed?.definitions[0]; + assert(definition); + assert(definition.kind === "objectDef"); + expect(definition.permissions.length).toEqual(2); + }); - it("succeeds parsing a valid schema with caveat", () => { - const schema = `caveat somecaveat(somearg any) { + it("succeeds parsing a valid schema with caveat", () => { + const schema = `caveat somecaveat(somearg any) { somearg.foo().bar } definition user {}`; - const parsed = parseSchema(schema); - expect(parsed?.definitions.length).toEqual(2); + const parsed = parseSchema(schema); + expect(parsed?.definitions.length).toEqual(2); + }); }); }); @@ -782,3 +906,34 @@ describe("parsing fails when", () => { expect(parsed).toBeUndefined(); }); }); + +describe("utils", () => { + describe("stringLiteral", () => { + it("consumes a normal string literal", () => { + const parsed = stringLiteral.parse("'/foo/bar/baz.zed'"); + expect(parsed.status).toBeTruthy(); + assert(parsed.status); + expect(parsed.value).toEqual("/foo/bar/baz.zed"); + }); + it("consumes a quoted singlequote", () => { + const parsed = stringLiteral.parse(`"'"`); + expect(parsed.status).toBeTruthy(); + assert(parsed.status); + expect(parsed.value).toEqual("'"); + }); + it("consumes a quoted doublequote", () => { + const parsed = stringLiteral.parse(`'"'`); + expect(parsed.status).toBeTruthy(); + assert(parsed.status); + expect(parsed.value).toEqual('"'); + }); + it("disallows backticks as delimiter", () => { + const parsed = stringLiteral.parse("`something in backticks`"); + expect(parsed.status).toBeFalsy(); + }); + it("fails when contents continue", () => { + const parsed = stringLiteral.parse("'something'thatcontinues"); + expect(parsed.status).toBeFalsy(); + }); + }); +}); diff --git a/src/dsl.ts b/src/dsl.ts index 8cbb4c8..82f153c 100644 --- a/src/dsl.ts +++ b/src/dsl.ts @@ -1,4 +1,4 @@ -import type { Parser, Success } from "parsimmon"; +import type { Parser } from "parsimmon"; import { formatError, index, @@ -9,14 +9,14 @@ import { seqMap, newline, seq, - alt, + takeWhile, } from "parsimmon"; import { celExpression } from "./cel"; /** * ParseResult is the result of a direct parse. */ -export interface ParseResult { +export type ParseResult = { /** * error is the parsing error found, if any. */ @@ -26,12 +26,12 @@ export interface ParseResult { * schema is the fully parsed schema, if no error. */ schema: ParsedSchema | undefined; -} +}; /** * ParseError represents an error raised by the parser. */ -export interface ParseError { +export type ParseError = { /** * message is the human-readable error message. */ @@ -46,7 +46,7 @@ export interface ParseError { * expected is the set of expected regular expression(s) at the index. */ expected: Array; -} +}; /** * parseSchema parses a DSL schema, returning relevant semantic information @@ -67,6 +67,8 @@ export const parseSchema = (value: string): ParsedSchema | undefined => { export type TopLevelDefinition = | ParsedObjectDefinition | ParsedCaveatDefinition + | ParsedPartialDefinition + | ParsedImportExpression | ParsedUseFlag; /** @@ -86,7 +88,7 @@ export function parse(input: string): ParseResult { ? { kind: "schema", stringValue: input, - definitions: (result as Success>).value, + definitions: result.value, } : undefined, }; @@ -148,10 +150,10 @@ export function flatMapExpression( * ReferenceNode is the node returned by findReferenceNode, along with its parent definition, * if any. */ -export interface ReferenceNode { +export type ReferenceNode = { node: ParsedRelationRefExpression | TypeRef | undefined; def: TopLevelDefinition; -} +}; /** * findReferenceNode walks the parse tree to find the node matching the given line number and @@ -189,68 +191,89 @@ export function mapParsedSchema( return; } - schema.definitions.forEach((def: TopLevelDefinition) => { - mapParseNodes(def, mapper); - }); + schema.definitions.forEach(mapParseNodes(mapper)); } //////////////////////////////////////////////////////////////////////////////////////////////////// // Parser node types //////////////////////////////////////////////////////////////////////////////////////////////////// -export interface ParsedSchema { +export type ParsedSchema = { kind: "schema"; stringValue: string; + // TODO: it may make sense to split uses and imports out. definitions: Array; -} +}; -export interface ParsedUseFlag { +export type ParsedUseFlag = { kind: "use"; featureName: string; range: TextRange; -} +}; -export interface ParsedCaveatDefinition { +export type ParsedImportExpression = { + kind: "import"; + path: string; + range: TextRange; +}; + +export type ParsedCaveatDefinition = { kind: "caveatDef"; name: string; parameters: Array; expression: ParsedCaveatExpression; range: TextRange; -} +}; -export interface ParsedCaveatExpression { +export type ParsedCaveatExpression = { kind: "caveatExpr"; range: TextRange; -} +}; -export interface ParsedCaveatParameter { +export type ParsedCaveatParameter = { kind: "caveatParameter"; name: string; type: ParsedCaveatParameterTypeRef; range: TextRange; -} +}; -export interface ParsedCaveatParameterTypeRef { +export type ParsedCaveatParameterTypeRef = { kind: "caveatParameterTypeExpr"; name: string; generics: Array; range: TextRange; -} +}; -export interface ParsedObjectDefinition { +export type ParsedObjectDefinition = { kind: "objectDef"; name: string; relations: Array; permissions: Array; + partialReferences: Array; range: TextRange; -} +}; + +export type ParsedPartialDefinition = { + kind: "partial"; + name: string; + relations: Array; + permissions: Array; + partialReferences: Array; + range: TextRange; +}; -export interface ParsedRelation { +type PartialReference = { + kind: "partialreference"; + name: string; + range: TextRange; +}; + +export type ParsedRelation = { kind: "relation"; name: string; allowedTypes: TypeExpr; range: TextRange; -} +}; export type ParsedExpression = | ParsedBinaryExpression @@ -259,51 +282,54 @@ export type ParsedExpression = | ParsedNamedArrowExpression | ParsedNilExpression; -export interface ParsedArrowExpression { +export type ParsedArrowExpression = { kind: "arrow"; sourceRelation: ParsedRelationRefExpression; targetRelationOrPermission: string; range: TextRange; -} +}; -export interface ParsedNamedArrowExpression { +export type ParsedNamedArrowExpression = { kind: "namedarrow"; sourceRelation: ParsedRelationRefExpression; functionName: string; targetRelationOrPermission: string; range: TextRange; -} +}; -export interface ParsedRelationRefExpression { +export type ParsedRelationRefExpression = { kind: "relationref"; relationName: string; range: TextRange; -} +}; -export interface ParsedNilExpression { +export type ParsedNilExpression = { kind: "nil"; isNil: true; range: TextRange; -} +}; -export interface ParsedBinaryExpression { +export type ParsedBinaryExpression = { kind: "binary"; operator: "union" | "intersection" | "exclusion"; left: ParsedExpression; right: ParsedExpression; range: TextRange; -} +}; -export interface ParsedPermission { +export type ParsedPermission = { kind: "permission"; name: string; expr: ParsedExpression; range: TextRange; -} +}; -export type RelationOrPermission = ParsedRelation | ParsedPermission; +export type DefinitionMember = + | ParsedRelation + | ParsedPermission + | PartialReference; -export interface TypeRef { +export type TypeRef = { kind: "typeref"; path: string; relationName: string | undefined; @@ -311,32 +337,34 @@ export interface TypeRef { withCaveat: WithCaveat | undefined; withExpiration: WithExpiration | undefined; range: TextRange; -} +}; -export interface WithExpiration { +export type WithExpiration = { kind: "withexpiration"; range: TextRange; -} +}; -export interface WithCaveat { +export type WithCaveat = { kind: "withcaveat"; path: string; range: TextRange; -} +}; -export interface TypeExpr { +export type TypeExpr = { kind: "typeexpr"; types: Array; range: TextRange; -} +}; export type ParsedNode = | ParsedUseFlag + | ParsedImportExpression | ParsedCaveatDefinition | ParsedCaveatParameter | ParsedCaveatParameterTypeRef | ParsedCaveatExpression | ParsedObjectDefinition + | ParsedPartialDefinition | ParsedRelation | ParsedPermission | ParsedExpression @@ -344,16 +372,16 @@ export type ParsedNode = | TypeExpr | WithCaveat; -export interface Index { +export type Index = { offset: number; line: number; column: number; -} +}; -export interface TextRange { +export type TextRange = { startIndex: Index; endIndex: Index; -} +}; export type ExprWalker = (expr: ParsedExpression) => T | undefined; @@ -376,10 +404,10 @@ const identifier = lexeme(regex(/[a-zA-Z_][0-9a-zA-Z_+]*/)); const path = lexeme( regex(/([a-zA-Z_][0-9a-zA-Z_+-]*\/)*[a-zA-Z_][0-9a-zA-Z_+-]*/), ); -const colon = lexeme(regex(/:/)); -const equal = lexeme(regex(/=/)); -const semicolon = lexeme(regex(/;/)); -const pipe = lexeme(regex(/\|/)); +const colon = lexeme(string(":")); +const equal = lexeme(string("=")); +const semicolon = lexeme(string(";")); +const pipe = lexeme(string("|")); const lbrace = lexeme(string("{")); const rbrace = lexeme(string("}")); @@ -391,9 +419,19 @@ const arrow = lexeme(string("->")); const hash = lexeme(string("#")); const comma = lexeme(string(",")); const dot = lexeme(string(".")); +const ellipsis = lexeme(string("...")); const terminator = newline.or(semicolon); +// Take a single or a doublequote, +// consume while the next character isn't that first delimiter, +// then skip that last delimiter +export const stringLiteral = string("'") + .or(string('"')) + .chain((delimiter) => + takeWhile((c) => c !== delimiter).skip(string(delimiter)), + ); + // Type reference and expression. const andExpiration = seqMap( @@ -408,7 +446,7 @@ const andExpiration = seqMap( }, ); -const withCaveat = seqMap( +const withCaveat: Parser = seqMap( index, seq(lexeme(string("with")), path, andExpiration.atMost(1)), index, @@ -422,7 +460,7 @@ const withCaveat = seqMap( }, ); -const withExpiration = seqMap( +const withExpiration: Parser = seqMap( index, seq(lexeme(string("with")), lexeme(string("expiration"))), index, @@ -434,7 +472,7 @@ const withExpiration = seqMap( }, ); -const typeRef = seqMap( +const typeRef: Parser = seqMap( index, seq( seq(path, colon, lexeme(string("*"))).or( @@ -445,18 +483,23 @@ const typeRef = seqMap( index, function (startIndex, data, endIndex) { const isWildcard = data[0][2] === "*"; + const withCaveat = data[1].find((trait) => trait.kind === "withcaveat"); + const withExpiration = data[1].find( + (trait) => trait.kind === "withexpiration", + ); return { kind: "typeref", path: data[0][0], relationName: isWildcard ? undefined : data[0][1][0], wildcard: isWildcard, - withCaveat: data[1].length > 0 ? data[1][0] : undefined, + withCaveat, + withExpiration, range: { startIndex: startIndex, endIndex: endIndex }, }; }, ); -const typeExpr = lazy(() => { +const typeExpr: Parser = lazy(() => { return seqMap( index, seq(typeRef, pipedTypeExpr.atLeast(0)), @@ -476,7 +519,7 @@ const pipedTypeExpr = pipe.then(typeRef); // Permission expression. // Based on: https://github.com/jneen/parsimmon/blob/93648e20f40c5c0335ac6506b39b0ca58b87b1d9/examples/math.js#L29 -const relationReference = lazy(() => { +const relationReference: Parser = lazy(() => { return seqMap( index, seq(identifier), @@ -491,7 +534,7 @@ const relationReference = lazy(() => { ); }); -const arrowExpr = lazy(() => { +const arrowExpr: Parser = lazy(() => { return seqMap( index, seq(relationReference, arrow, identifier), @@ -507,7 +550,7 @@ const arrowExpr = lazy(() => { ); }); -const namedArrowExpr = lazy(() => { +const namedArrowExpr: Parser = lazy(() => { return seqMap( index, seq(relationReference, dot, identifier, lparen, identifier, rparen), @@ -524,7 +567,7 @@ const namedArrowExpr = lazy(() => { ); }); -const nilExpr = lazy(() => { +const nilExpr: Parser = lazy(() => { return seqMap( index, string("nil"), @@ -550,76 +593,51 @@ const parensExpr = lazy(() => ); function BINARY_LEFT( - operatorsParser: Parser, - nextParser: Parser, + operatorsParser: Parser, + nextParser: Parser, ) { return seqMap( nextParser, seq(operatorsParser, nextParser).many(), - ( - first: ParsedBinaryExpression, - rest: Array<[string, ParsedBinaryExpression]>, - ) => { - return rest.reduce( - ( - acc: ParsedBinaryExpression, - ch: [string, ParsedBinaryExpression], - ): ParsedBinaryExpression => { - const [op, another] = ch; - return { - kind: "binary", - // NOTE: this as is necessary because the table below where - // these parsers are defined defines them statically, but - // typescript doesn't know that they're limited to this union. - operator: op as "union" | "intersection" | "exclusion", - left: acc, - right: another, - range: { - startIndex: acc.range.startIndex, - endIndex: another.range.endIndex, - }, - }; - }, - first, - ); + (first, rest) => { + return rest.reduce((acc, ch): ParsedBinaryExpression => { + const [operator, another] = ch; + return { + kind: "binary", + operator, + left: acc, + right: another, + range: { + startIndex: acc.range.startIndex, + endIndex: another.range.endIndex, + }, + }; + }, first); }, ); } -// TODO: ensure this is right. -function operators(ops: Record) { - const ps = Object.entries(ops) - .sort(([firstKey], [secondKey]) => firstKey.localeCompare(secondKey)) - .map(([key, value]) => string(value).trim(optWhitespace).result(key)); - return alt(...ps); +type OperatorType = "union" | "intersection" | "exclusion"; + +function operator(operation: OperatorType, symbol: string) { + return string(symbol).trim(optWhitespace).result(operation); } -const table: Array<{ - type: typeof BINARY_LEFT; - ops: Parser; -}> = [ - { type: BINARY_LEFT, ops: operators({ union: "+" }) }, - { type: BINARY_LEFT, ops: operators({ intersection: "&" }) }, - { type: BINARY_LEFT, ops: operators({ exclusion: "-" }) }, -]; - -const tableParser: Parser = table.reduce( - ( - acc: Parser, - level: (typeof table)[0], - ): Parser => level.type(level.ops, acc), - // TODO: there's probably a better way to type this. - // BINARY_LEFT returns a Parser, and the types - // are compatible as seen in the parsing tests passing, but we have to - // cast here because there isn't a broader type that works well - // in this context. - parensExpr as unknown as Parser, +const table = [ + { type: BINARY_LEFT, ops: operator("union", "+") }, + { type: BINARY_LEFT, ops: operator("intersection", "&") }, + { type: BINARY_LEFT, ops: operator("exclusion", "-") }, +] as const; + +const tableParser: Parser = table.reduce( + (acc, level) => level.type(level.ops, acc), + parensExpr, ); const expr = tableParser.trim(whitespace); // Definitions members. -const permission = seqMap( +const permission: Parser = seqMap( index, seq( lexeme(string("permission")), @@ -637,7 +655,7 @@ const permission = seqMap( }, ); -const relation = seqMap( +const relation: Parser = seqMap( index, seq( lexeme(string("relation")), @@ -645,78 +663,144 @@ const relation = seqMap( colon.then(typeExpr).skip(terminator.atMost(1)), ), index, - function (startIndex, data, endIndex) { + function (startIndex, [_, name, allowedTypes], endIndex) { return { kind: "relation", - name: data[1], - allowedTypes: data[2], - range: { startIndex: startIndex, endIndex: endIndex }, + name, + allowedTypes, + range: { startIndex, endIndex }, }; }, ); -const relationOrPermission = relation.or(permission); +const partialReference: Parser = seqMap( + index, + seq(ellipsis, identifier, terminator.atMost(1)), + index, + function (startIndex, [_, name, __], endIndex) { + return { + kind: "partialreference", + name, + range: { startIndex, endIndex }, + }; + }, +); + +const definitionMember: Parser = relation + .or(permission) + .or(partialReference); // Use flags -const useFlag = seqMap( +const useFlag: Parser = seqMap( index, seq(lexeme(string("use")), identifier, terminator.atMost(1)), index, - function (startIndex, data, endIndex) { + function (startIndex, [_, featureName, __], endIndex) { return { kind: "use", - featureName: data[1], - range: { startIndex: startIndex, endIndex: endIndex }, + featureName, + range: { startIndex, endIndex }, }; }, ); +// Import expressions +const importExpression: Parser = seqMap( + index, + seq(lexeme(string("import")), stringLiteral, terminator.atMost(1)), + index, + function (startIndex, [_, path, __], endIndex) { + return { + kind: "import", + path, + range: { startIndex, endIndex }, + }; + }, +); + +function isRelation(member: DefinitionMember): member is ParsedRelation { + return member.kind === "relation"; +} + +function isPermission(member: DefinitionMember): member is ParsedPermission { + return member.kind === "permission"; +} + +function isPartialReference( + member: DefinitionMember, +): member is PartialReference { + return member.kind === "partialreference"; +} + // Object Definitions. -const definition = seqMap( +const definition: Parser = seqMap( index, seq( lexeme(string("definition")), path, - lbrace.then(relationOrPermission.atLeast(0)).skip(rbrace), + lbrace.then(definitionMember.atLeast(0)).skip(rbrace), ), index, function (startIndex, data, endIndex) { - const rp = data[2] as Array; + const members = data[2]; return { kind: "objectDef", name: data[1], - relations: rp.filter( - (relOrPerm: RelationOrPermission) => "allowedTypes" in relOrPerm, - ), - permissions: rp.filter( - (relOrPerm: RelationOrPermission) => !("allowedTypes" in relOrPerm), - ), + relations: members.filter(isRelation), + permissions: members.filter(isPermission), + partialReferences: members.filter(isPartialReference), + range: { startIndex: startIndex, endIndex: endIndex }, + }; + }, +); + +// Partial Definitions. +// NOTE: these are parsed the same as definitions. +// TODO: see if this can be combined with objectDef in a sane way +const partial: Parser = seqMap( + index, + seq( + lexeme(string("partial")), + path, + lbrace.then(definitionMember.atLeast(0)).skip(rbrace), + ), + index, + function (startIndex, data, endIndex) { + const members = data[2]; + return { + kind: "partial", + name: data[1], + relations: members.filter(isRelation), + permissions: members.filter(isPermission), + partialReferences: members.filter(isPartialReference), range: { startIndex: startIndex, endIndex: endIndex }, }; }, ); // Caveats. -const caveatParameterTypeExpr: Parser = lazy(() => { - return seqMap( - index, - seq( - identifier, - lcaret.then(caveatParameterTypeExpr).skip(rcaret).atMost(1), - ), - index, - function (startIndex, data, endIndex) { - return { - kind: "caveatParameterTypeExpr", - name: data[0], - generics: data[1], - range: { startIndex: startIndex, endIndex: endIndex }, - }; - }, - ); -}); +const caveatParameterTypeExpr: Parser = lazy( + () => { + return seqMap( + index, + seq( + identifier, + lcaret.then(caveatParameterTypeExpr).skip(rcaret).atMost(1), + ), + index, + function (startIndex, data, endIndex) { + return { + kind: "caveatParameterTypeExpr", + name: data[0], + generics: data[1], + range: { startIndex: startIndex, endIndex: endIndex }, + }; + }, + ); + }, +); -const caveatParameter = lazy(() => { +const caveatParameter: Parser = lazy(() => { return seqMap( index, seq(identifier, caveatParameterTypeExpr), @@ -750,7 +834,7 @@ const caveatParameters = lazy(() => { const commaedParameter = comma.then(caveatParameter); -const caveatExpression = seqMap( +const caveatExpression: Parser = seqMap( index, seq(celExpression), index, @@ -762,7 +846,7 @@ const caveatExpression = seqMap( }, ); -const caveat = seqMap( +const caveat: Parser = seqMap( index, seq( lexeme(string("caveat")), @@ -784,7 +868,11 @@ const caveat = seqMap( }, ); -const topLevel = definition.or(caveat).or(useFlag); +const topLevel = definition + .or(partial) + .or(caveat) + .or(useFlag) + .or(importExpression); function findReferenceNodeInDef( def: ParsedObjectDefinition, @@ -864,62 +952,55 @@ function findReferenceNodeInRelation( return found.length > 0 ? found[0] : undefined; } -function mapParseNodes( - node: ParsedNode | undefined, - mapper: (node: ParsedNode) => void, -) { - if (node === undefined) { - return; - } +const mapParseNodes = + (mapper: (node: ParsedNode) => void) => (node: ParsedNode | undefined) => { + if (node === undefined) { + return; + } - mapper(node); - - switch (node.kind) { - case "objectDef": - node.relations.forEach((rel: ParsedRelation) => { - mapParseNodes(rel, mapper); - }); - node.permissions.forEach((perm: ParsedPermission) => { - mapParseNodes(perm, mapper); - }); - break; - - case "caveatDef": - node.parameters.forEach((param: ParsedCaveatParameter) => { - mapParseNodes(param, mapper); - }); - mapParseNodes(node.expression, mapper); - break; - - case "caveatParameter": - mapParseNodes(node.type, mapper); - break; - - case "caveatParameterTypeExpr": - node.generics.forEach((n) => { - mapParseNodes(n, mapper); - }); - break; - - case "relation": - mapParseNodes(node.allowedTypes, mapper); - break; - - case "permission": - flatMapExpression(node.expr, mapper); - break; - - case "typeexpr": - node.types.forEach((n) => { - mapParseNodes(n, mapper); - }); - break; - - case "typeref": - mapParseNodes(node.withCaveat, mapper); - break; - } -} + mapper(node); + + switch (node.kind) { + case "objectDef": + node.relations.forEach(mapParseNodes(mapper)); + node.permissions.forEach(mapParseNodes(mapper)); + break; + + case "partial": + node.relations.forEach(mapParseNodes(mapper)); + node.permissions.forEach(mapParseNodes(mapper)); + break; + + case "caveatDef": + node.parameters.forEach(mapParseNodes(mapper)); + mapParseNodes(mapper)(node.expression); + break; + + case "caveatParameter": + mapParseNodes(mapper)(node.type); + break; + + case "caveatParameterTypeExpr": + node.generics.forEach(mapParseNodes(mapper)); + break; + + case "relation": + mapParseNodes(mapper)(node.allowedTypes); + break; + + case "permission": + flatMapExpression(node.expr, mapper); + break; + + case "typeexpr": + node.types.forEach(mapParseNodes(mapper)); + break; + + case "typeref": + mapParseNodes(mapper)(node.withCaveat); + break; + } + }; function rangeContains( withRange: { range: TextRange }, diff --git a/src/resolution.ts b/src/resolution.ts index 9909fe2..72620a7 100644 --- a/src/resolution.ts +++ b/src/resolution.ts @@ -16,7 +16,7 @@ import { flatMapExpression } from "./dsl"; /** * TypeRefResolution is the result of resolving a type reference. */ -export interface TypeRefResolution { +export type TypeRefResolution = { /** * definition is the definition which is referred by this type reference. */ @@ -35,7 +35,7 @@ export interface TypeRefResolution { * refs, so make sure to check the expression to see if it has a relation reference. */ permission: ParsedPermission | undefined; -} +}; /** * ExpressionResolution is the resolution of a reference in a permission expression. @@ -45,20 +45,20 @@ export type ExpressionResolution = ParsedRelation | ParsedPermission; /** * ResolvedTypeReference is a type reference found in the schema, with resolution attempted. */ -export interface ResolvedTypeReference { +export type ResolvedTypeReference = { kind: "type"; reference: TypeRef; referencedTypeAndRelation: TypeRefResolution | undefined; -} +}; /** * ResolvedExprReference is a relation reference expression found in the schema, with resolution attempted. */ -export interface ResolvedExprReference { +export type ResolvedExprReference = { kind: "expression"; reference: ParsedRelationRefExpression; resolvedRelationOrPermission: ExpressionResolution | undefined; -} +}; /** * ResolvedReference is a found and resolution performed type reference or expression. @@ -187,7 +187,7 @@ export class Resolver { def: TopLevelDefinition, ): ExpressionResolution | undefined { this.populate(); - if (def.kind === "use") { + if (def.kind === "use" || def.kind === "import") { return undefined; } @@ -255,6 +255,7 @@ export class ResolvedDefinition { name: "", relations: [], permissions: [], + partialReferences: [], range: emptyRange, };