From 95f82dd2b1796baca613ae712a6ca1acb65fb28a Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:55:50 -0700 Subject: [PATCH] fix: support string literal keys for object expressions in ZModel --- packages/language/src/generated/ast.ts | 2 +- packages/language/src/generated/grammar.ts | 22 +++++++--- packages/language/src/zmodel.langium | 2 +- packages/schema/package.json | 1 + .../tests/generator/expression-writer.test.ts | 2 +- .../tests/generator/prisma-generator.test.ts | 2 +- .../tests/generator/zmodel-generator.test.ts | 2 +- packages/schema/tests/schema/abstract.test.ts | 2 +- packages/schema/tests/schema/cal-com.test.ts | 2 +- packages/schema/tests/schema/parser.test.ts | 2 +- .../schema/tests/schema/sample-todo.test.ts | 2 +- packages/schema/tests/schema/stdlib.test.ts | 2 +- .../schema/tests/schema/trigger-dev.test.ts | 2 +- .../validation/attribute-validation.test.ts | 2 +- .../validation/datamodel-validation.test.ts | 2 +- .../validation/datasource-validation.test.ts | 2 +- .../schema/validation/enum-validation.test.ts | 2 +- .../validation/schema-validation.test.ts | 2 +- packages/sdk/src/utils.ts | 13 ++++-- packages/testtools/package.json | 2 + packages/testtools/src/index.ts | 3 +- .../tests/utils.ts => testtools/src/model.ts} | 12 +++--- pnpm-lock.yaml | 9 +++++ .../tests/regression/issue-744.test.ts | 40 +++++++++++++++++++ 24 files changed, 104 insertions(+), 30 deletions(-) rename packages/{schema/tests/utils.ts => testtools/src/model.ts} (86%) create mode 100644 tests/integration/tests/regression/issue-744.test.ts diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index d5f462277..1f4b25b84 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -386,7 +386,7 @@ export function isEnumField(item: unknown): item is EnumField { export interface FieldInitializer extends AstNode { readonly $container: ObjectExpr; readonly $type: 'FieldInitializer'; - name: RegularID + name: RegularID | string value: Expression } diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 7bdf53b6a..8cad3d2cf 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -1171,11 +1171,23 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "feature": "name", "operator": "=", "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@47" - }, - "arguments": [] + "$type": "Alternatives", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@47" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@67" + }, + "arguments": [] + } + ] } }, { diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 5f1aa5008..47dfa1866 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -90,7 +90,7 @@ ObjectExpr: '}'; FieldInitializer: - name=RegularID ':' value=(Expression); + name=(RegularID | STRING) ':' value=(Expression); InvocationExpr: function=[FunctionDecl] '(' ArgumentList? ')'; diff --git a/packages/schema/package.json b/packages/schema/package.json index 3b35633c5..486c158cd 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -126,6 +126,7 @@ "@typescript-eslint/parser": "^5.42.0", "@vscode/vsce": "^2.19.0", "@zenstackhq/runtime": "workspace:*", + "@zenstackhq/testtools": "workspace:*", "concurrently": "^7.4.0", "copyfiles": "^2.4.1", "dotenv": "^16.0.3", diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts index d4a5fe5db..5c905d904 100644 --- a/packages/schema/tests/generator/expression-writer.test.ts +++ b/packages/schema/tests/generator/expression-writer.test.ts @@ -4,7 +4,7 @@ import { DataModel, Enum, Expression, isDataModel, isEnum } from '@zenstackhq/la import * as tmp from 'tmp'; import { Project, VariableDeclarationKind } from 'ts-morph'; import { ExpressionWriter } from '../../src/plugins/access-policy/expression-writer'; -import { loadModel } from '../utils'; +import { loadModel } from '@zenstackhq/testtools'; describe('Expression Writer Tests', () => { it('boolean literal', async () => { diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 8ba127842..c1d835245 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -6,7 +6,7 @@ import path from 'path'; import tmp from 'tmp'; import { loadDocument } from '../../src/cli/cli-util'; import PrismaSchemaGenerator from '../../src/plugins/prisma/schema-generator'; -import { loadModel } from '../utils'; +import { loadModel } from '@zenstackhq/testtools'; describe('Prisma generator test', () => { it('datasource coverage', async () => { diff --git a/packages/schema/tests/generator/zmodel-generator.test.ts b/packages/schema/tests/generator/zmodel-generator.test.ts index 91ddacca2..9240ae741 100644 --- a/packages/schema/tests/generator/zmodel-generator.test.ts +++ b/packages/schema/tests/generator/zmodel-generator.test.ts @@ -1,4 +1,4 @@ -import { loadModel } from '../utils'; +import { loadModel } from '@zenstackhq/testtools'; import ZModelCodeGenerator from '../../src/plugins/prisma/zmodel-code-generator'; import { DataModel, DataModelAttribute, DataModelFieldAttribute } from '@zenstackhq/language/ast'; diff --git a/packages/schema/tests/schema/abstract.test.ts b/packages/schema/tests/schema/abstract.test.ts index 621e7998a..6681a1e58 100644 --- a/packages/schema/tests/schema/abstract.test.ts +++ b/packages/schema/tests/schema/abstract.test.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import path from 'path'; -import { loadModel } from '../utils'; +import { loadModel } from '@zenstackhq/testtools'; describe('Abstract Schema Tests', () => { it('model loading', async () => { diff --git a/packages/schema/tests/schema/cal-com.test.ts b/packages/schema/tests/schema/cal-com.test.ts index 05da241b9..9bf58e64b 100644 --- a/packages/schema/tests/schema/cal-com.test.ts +++ b/packages/schema/tests/schema/cal-com.test.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import path from 'path'; -import { loadModel } from '../utils'; +import { loadModel } from '@zenstackhq/testtools'; describe('Cal.com Schema Tests', () => { it('model loading', async () => { diff --git a/packages/schema/tests/schema/parser.test.ts b/packages/schema/tests/schema/parser.test.ts index 9b4150cd5..99a1753f3 100644 --- a/packages/schema/tests/schema/parser.test.ts +++ b/packages/schema/tests/schema/parser.test.ts @@ -20,7 +20,7 @@ import { StringLiteral, UnaryExpr, } from '@zenstackhq/language/ast'; -import { loadModel } from '../utils'; +import { loadModel } from '@zenstackhq/testtools'; describe('Parsing Tests', () => { it('data source', async () => { diff --git a/packages/schema/tests/schema/sample-todo.test.ts b/packages/schema/tests/schema/sample-todo.test.ts index 40387604c..1f4eaefbe 100644 --- a/packages/schema/tests/schema/sample-todo.test.ts +++ b/packages/schema/tests/schema/sample-todo.test.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import path from 'path'; -import { loadModel } from '../utils'; +import { loadModel } from '@zenstackhq/testtools'; describe('Sample Todo Schema Tests', () => { it('model loading', async () => { diff --git a/packages/schema/tests/schema/stdlib.test.ts b/packages/schema/tests/schema/stdlib.test.ts index e4fa5b966..c34accd11 100644 --- a/packages/schema/tests/schema/stdlib.test.ts +++ b/packages/schema/tests/schema/stdlib.test.ts @@ -1,8 +1,8 @@ +import { SchemaLoadingError } from '@zenstackhq/testtools'; import { NodeFileSystem } from 'langium/node'; import path from 'path'; import { URI } from 'vscode-uri'; import { createZModelServices } from '../../src/language-server/zmodel-module'; -import { SchemaLoadingError } from '../utils'; describe('Stdlib Tests', () => { it('stdlib', async () => { diff --git a/packages/schema/tests/schema/trigger-dev.test.ts b/packages/schema/tests/schema/trigger-dev.test.ts index c712ad25d..599ec7a4f 100644 --- a/packages/schema/tests/schema/trigger-dev.test.ts +++ b/packages/schema/tests/schema/trigger-dev.test.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import path from 'path'; -import { loadModel } from '../utils'; +import { loadModel } from '@zenstackhq/testtools'; describe('Trigger.dev Schema Tests', () => { it('model loading', async () => { diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 5c638a841..51af8f460 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -1,6 +1,6 @@ /// -import { loadModel, loadModelWithError } from '../../utils'; +import { loadModel, loadModelWithError } from '@zenstackhq/testtools'; describe('Attribute tests', () => { const prelude = ` diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index a2d68f2c0..bcfc4f54a 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -1,4 +1,4 @@ -import { loadModel, loadModelWithError } from '../../utils'; +import { loadModel, loadModelWithError } from '@zenstackhq/testtools'; describe('Data Model Validation Tests', () => { const prelude = ` diff --git a/packages/schema/tests/schema/validation/datasource-validation.test.ts b/packages/schema/tests/schema/validation/datasource-validation.test.ts index 19be1f076..a6596b7cd 100644 --- a/packages/schema/tests/schema/validation/datasource-validation.test.ts +++ b/packages/schema/tests/schema/validation/datasource-validation.test.ts @@ -1,4 +1,4 @@ -import { loadModel, loadModelWithError } from '../../utils'; +import { loadModel, loadModelWithError } from '@zenstackhq/testtools'; describe('Datasource Validation Tests', () => { it('missing fields', async () => { diff --git a/packages/schema/tests/schema/validation/enum-validation.test.ts b/packages/schema/tests/schema/validation/enum-validation.test.ts index fc31a0c92..c20d386d5 100644 --- a/packages/schema/tests/schema/validation/enum-validation.test.ts +++ b/packages/schema/tests/schema/validation/enum-validation.test.ts @@ -1,4 +1,4 @@ -import { loadModelWithError } from '../../utils'; +import { loadModelWithError } from '@zenstackhq/testtools'; describe('Enum Validation Tests', () => { const prelude = ` diff --git a/packages/schema/tests/schema/validation/schema-validation.test.ts b/packages/schema/tests/schema/validation/schema-validation.test.ts index c7b000338..9e90b28ec 100644 --- a/packages/schema/tests/schema/validation/schema-validation.test.ts +++ b/packages/schema/tests/schema/validation/schema-validation.test.ts @@ -1,4 +1,4 @@ -import { loadModelWithError } from '../../utils'; +import { loadModelWithError } from '@zenstackhq/testtools'; describe('Toplevel Schema Validation Tests', () => { it('too many datasources', async () => { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 80366480a..f10ae7b1e 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -49,10 +49,17 @@ export function resolved(ref: Reference): T { export function getLiteral( expr: Expression | ConfigExpr | undefined ): T | undefined { - if (!isLiteralExpr(expr)) { - return getObjectLiteral(expr); + switch (expr?.$type) { + case 'ObjectExpr': + return getObjectLiteral(expr); + case 'StringLiteral': + case 'BooleanLiteral': + return expr.value as T; + case 'NumberLiteral': + return parseFloat(expr.value) as T; + default: + return undefined; } - return expr.value as T; } export function getArray(expr: Expression | ConfigExpr | undefined) { diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 4e9e5118f..9624ef461 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -24,8 +24,10 @@ "@zenstackhq/runtime": "workspace:*", "@zenstackhq/sdk": "workspace:*", "json5": "^2.2.3", + "langium": "1.2.0", "pg": "^8.11.1", "tmp": "^0.2.1", + "vscode-uri": "^3.0.6", "zenstack": "workspace:*" }, "devDependencies": { diff --git a/packages/testtools/src/index.ts b/packages/testtools/src/index.ts index 3c1996686..488498b2f 100644 --- a/packages/testtools/src/index.ts +++ b/packages/testtools/src/index.ts @@ -1,2 +1,3 @@ -export * from './schema'; export * from './db'; +export * from './model'; +export * from './schema'; diff --git a/packages/schema/tests/utils.ts b/packages/testtools/src/model.ts similarity index 86% rename from packages/schema/tests/utils.ts rename to packages/testtools/src/model.ts index f362b4019..4be8a1613 100644 --- a/packages/schema/tests/utils.ts +++ b/packages/testtools/src/model.ts @@ -1,11 +1,11 @@ -import { Model } from '@zenstackhq/language/ast'; +import { Model } from '@zenstackhq/sdk/ast'; import * as fs from 'fs'; import { NodeFileSystem } from 'langium/node'; import * as path from 'path'; import * as tmp from 'tmp'; import { URI } from 'vscode-uri'; -import { createZModelServices } from '../src/language-server/zmodel-module'; -import { mergeBaseModel } from '../src/utils/ast-utils'; +import { createZModelServices } from 'zenstack/language-server/zmodel-module'; +import { mergeBaseModel } from 'zenstack/utils/ast-utils'; export class SchemaLoadingError extends Error { constructor(public readonly errors: string[]) { @@ -18,7 +18,7 @@ export async function loadModel(content: string, validate = true, verbose = true fs.writeFileSync(docPath, content); const { shared } = createZModelServices(NodeFileSystem); const stdLib = shared.workspace.LangiumDocuments.getOrCreateDocument( - URI.file(path.resolve('src/res/stdlib.zmodel')) + URI.file(path.resolve(__dirname, '../../schema/src/res/stdlib.zmodel')) ); const doc = shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(docPath)); @@ -60,7 +60,9 @@ export async function loadModelWithError(content: string, verbose = false) { try { await loadModel(content, true, verbose); } catch (err) { - expect(err).toBeInstanceOf(SchemaLoadingError); + if (!(err instanceof SchemaLoadingError)) { + throw err; + } return (err as SchemaLoadingError).errors; } throw new Error('No error is thrown'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09952fa02..ad62571c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -567,6 +567,9 @@ importers: '@zenstackhq/runtime': specifier: workspace:* version: link:../runtime/dist + '@zenstackhq/testtools': + specifier: workspace:* + version: link:../testtools/dist concurrently: specifier: ^7.4.0 version: 7.4.0 @@ -779,12 +782,18 @@ importers: json5: specifier: ^2.2.3 version: 2.2.3 + langium: + specifier: 1.2.0 + version: 1.2.0 pg: specifier: ^8.11.1 version: 8.11.1 tmp: specifier: ^0.2.1 version: 0.2.1 + vscode-uri: + specifier: ^3.0.6 + version: 3.0.7 zenstack: specifier: workspace:* version: link:../schema/dist diff --git a/tests/integration/tests/regression/issue-744.test.ts b/tests/integration/tests/regression/issue-744.test.ts new file mode 100644 index 000000000..d46d110ec --- /dev/null +++ b/tests/integration/tests/regression/issue-744.test.ts @@ -0,0 +1,40 @@ +import { getObjectLiteral } from '@zenstackhq/sdk'; +import { Plugin, PluginField, isPlugin } from '@zenstackhq/sdk/ast'; +import { loadModel } from '@zenstackhq/testtools'; + +describe('Regression: issue 744', () => { + it('regression', async () => { + const model = await loadModel( + ` + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + + plugin zod { + provider = '@core/zod' + settings = { + '200': { status: 'ok' }, + 'x-y-z': 200, + foo: 'bar' + } + } + + model Foo { + id String @id @default(cuid()) + } + ` + ); + + const plugin = model.declarations.find((d): d is Plugin => isPlugin(d)); + const settings = plugin?.fields.find((f): f is PluginField => f.name === 'settings'); + const value: any = getObjectLiteral(settings?.value); + expect(value['200']).toMatchObject({ status: 'ok' }); + expect(value['x-y-z']).toBe(200); + expect(value.foo).toBe('bar'); + }); +});