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/tests/schema/stdlib.test.ts b/packages/schema/tests/schema/stdlib.test.ts index e4fa5b966..f4b1cc1fe 100644 --- a/packages/schema/tests/schema/stdlib.test.ts +++ b/packages/schema/tests/schema/stdlib.test.ts @@ -1,8 +1,8 @@ +import { SchemaLoadingError } from '../utils'; 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/utils.ts b/packages/schema/tests/utils.ts index f362b4019..fade65628 100644 --- a/packages/schema/tests/utils.ts +++ b/packages/schema/tests/utils.ts @@ -1,4 +1,4 @@ -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'; @@ -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/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/testtools/src/model.ts b/packages/testtools/src/model.ts new file mode 100644 index 000000000..4be8a1613 --- /dev/null +++ b/packages/testtools/src/model.ts @@ -0,0 +1,69 @@ +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 'zenstack/language-server/zmodel-module'; +import { mergeBaseModel } from 'zenstack/utils/ast-utils'; + +export class SchemaLoadingError extends Error { + constructor(public readonly errors: string[]) { + super('Schema error:\n' + errors.join('\n')); + } +} + +export async function loadModel(content: string, validate = true, verbose = true) { + const { name: docPath } = tmp.fileSync({ postfix: '.zmodel' }); + fs.writeFileSync(docPath, content); + const { shared } = createZModelServices(NodeFileSystem); + const stdLib = shared.workspace.LangiumDocuments.getOrCreateDocument( + URI.file(path.resolve(__dirname, '../../schema/src/res/stdlib.zmodel')) + ); + const doc = shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(docPath)); + + if (doc.parseResult.lexerErrors.length > 0) { + throw new SchemaLoadingError(doc.parseResult.lexerErrors.map((e) => e.message)); + } + + if (doc.parseResult.parserErrors.length > 0) { + throw new SchemaLoadingError(doc.parseResult.parserErrors.map((e) => e.message)); + } + + await shared.workspace.DocumentBuilder.build([stdLib, doc], { + validationChecks: validate ? 'all' : 'none', + }); + + const validationErrors = (doc.diagnostics ?? []).filter((e) => e.severity === 1); + if (validationErrors.length > 0) { + for (const validationError of validationErrors) { + if (verbose) { + const range = doc.textDocument.getText(validationError.range); + console.error( + `line ${validationError.range.start.line + 1}: ${validationError.message}${ + range ? ' [' + range + ']' : '' + }` + ); + } + } + throw new SchemaLoadingError(validationErrors.map((e) => e.message)); + } + + const model = (await doc.parseResult.value) as Model; + + mergeBaseModel(model); + + return model; +} + +export async function loadModelWithError(content: string, verbose = false) { + try { + await loadModel(content, true, verbose); + } catch (err) { + 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..ef4f595d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) + version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -201,7 +201,7 @@ importers: version: 2.0.3(react@18.2.0) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4) + version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4) typescript: specifier: ^4.9.4 version: 4.9.4 @@ -779,12 +779,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 @@ -10625,7 +10631,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10656,7 +10662,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.5 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true @@ -10695,6 +10701,41 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.22.9 + bs-logger: 0.2.6 + esbuild: 0.18.13 + fast-json-stable-stringify: 2.1.0 + jest: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) + jest-util: 29.5.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.3 + typescript: 4.9.5 + yargs-parser: 21.1.1 + dev: true + /ts-morph@16.0.0: resolution: {integrity: sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw==} dependencies: 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'); + }); +});