@prisma-next/psl-parser 0.14.0-dev.6 → 0.14.0-dev.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{declarations-D9h_ihD3.mjs → declarations-DR6To8_k.mjs} +295 -55
- package/dist/declarations-DR6To8_k.mjs.map +1 -0
- package/dist/format.d.mts +1 -1
- package/dist/format.mjs +2 -2
- package/dist/index.d.mts +95 -112
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +373 -60
- package/dist/index.mjs.map +1 -1
- package/dist/interpret.d.mts +29 -0
- package/dist/interpret.d.mts.map +1 -0
- package/dist/interpret.mjs +9 -0
- package/dist/interpret.mjs.map +1 -0
- package/dist/{parse-DhEV6av6.mjs → parse-3-vr14ej.mjs} +26 -5
- package/dist/parse-3-vr14ej.mjs.map +1 -0
- package/dist/{parse-BjZ1LPe6.d.mts → parse-BazJr7Ye.d.mts} +128 -51
- package/dist/parse-BazJr7Ye.d.mts.map +1 -0
- package/dist/symbol-table-C-AH04Ug.d.mts +127 -0
- package/dist/symbol-table-C-AH04Ug.d.mts.map +1 -0
- package/dist/syntax.d.mts +20 -2
- package/dist/syntax.d.mts.map +1 -1
- package/dist/syntax.mjs +43 -3
- package/dist/syntax.mjs.map +1 -0
- package/package.json +7 -5
- package/src/attribute-spec/combinators/bool.ts +19 -0
- package/src/attribute-spec/combinators/diagnostic.ts +15 -0
- package/src/attribute-spec/combinators/entity-ref.ts +24 -0
- package/src/attribute-spec/combinators/field-ref.ts +36 -0
- package/src/attribute-spec/combinators/identifier.ts +16 -0
- package/src/attribute-spec/combinators/int.ts +19 -0
- package/src/attribute-spec/combinators/list.ts +43 -0
- package/src/attribute-spec/combinators/one-of.ts +29 -0
- package/src/attribute-spec/combinators/record.ts +43 -0
- package/src/attribute-spec/combinators/str.ts +19 -0
- package/src/attribute-spec/field-attribute.ts +27 -0
- package/src/attribute-spec/interpret.ts +154 -0
- package/src/attribute-spec/model-attribute.ts +27 -0
- package/src/attribute-spec/optional.ts +8 -0
- package/src/attribute-spec/types.ts +72 -0
- package/src/exports/index.ts +30 -0
- package/src/exports/interpret.ts +2 -0
- package/src/exports/syntax.ts +25 -5
- package/src/interpret.ts +40 -0
- package/src/parse.ts +23 -5
- package/src/resolve.ts +4 -1
- package/src/source-file.ts +25 -0
- package/src/syntax/ast/attributes.ts +5 -6
- package/src/syntax/ast/declarations.ts +51 -26
- package/src/syntax/ast/expressions.ts +12 -13
- package/src/syntax/ast/identifier.ts +2 -3
- package/src/syntax/ast/qualified-name.ts +28 -19
- package/src/syntax/ast/type-annotation.ts +4 -5
- package/src/syntax/ast-helpers.ts +27 -3
- package/src/syntax/navigation.ts +55 -0
- package/src/syntax/red.ts +317 -42
- package/dist/declarations-D9h_ihD3.mjs.map +0 -1
- package/dist/parse-BjZ1LPe6.d.mts.map +0 -1
- package/dist/parse-DhEV6av6.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/psl-parser",
|
|
3
|
-
"version": "0.14.0-dev.
|
|
3
|
+
"version": "0.14.0-dev.60",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "Reusable parser for Prisma Schema Language (PSL)",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/
|
|
10
|
-
"@prisma-next/
|
|
9
|
+
"@prisma-next/config": "0.14.0-dev.60",
|
|
10
|
+
"@prisma-next/framework-components": "0.14.0-dev.60",
|
|
11
|
+
"@prisma-next/utils": "0.14.0-dev.60"
|
|
11
12
|
},
|
|
12
13
|
"devDependencies": {
|
|
13
|
-
"@prisma-next/tsconfig": "0.14.0-dev.
|
|
14
|
-
"@prisma-next/tsdown": "0.14.0-dev.
|
|
14
|
+
"@prisma-next/tsconfig": "0.14.0-dev.60",
|
|
15
|
+
"@prisma-next/tsdown": "0.14.0-dev.60",
|
|
15
16
|
"tsdown": "0.22.1",
|
|
16
17
|
"typescript": "5.9.3",
|
|
17
18
|
"vitest": "4.1.8"
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
"exports": {
|
|
33
34
|
".": "./dist/index.mjs",
|
|
34
35
|
"./format": "./dist/format.mjs",
|
|
36
|
+
"./interpret": "./dist/interpret.mjs",
|
|
35
37
|
"./syntax": "./dist/syntax.mjs",
|
|
36
38
|
"./tokenizer": "./dist/tokenizer.mjs",
|
|
37
39
|
"./package.json": "./package.json"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { BooleanLiteralExprAst } from '../../syntax/ast/expressions';
|
|
4
|
+
import type { ArgType } from '../types';
|
|
5
|
+
import { leafDiagnostic } from './diagnostic';
|
|
6
|
+
|
|
7
|
+
export function bool(): ArgType<boolean> {
|
|
8
|
+
return {
|
|
9
|
+
kind: 'bool',
|
|
10
|
+
label: 'boolean',
|
|
11
|
+
parse: (arg, ctx): Result<boolean, readonly PslDiagnostic[]> => {
|
|
12
|
+
if (arg instanceof BooleanLiteralExprAst) {
|
|
13
|
+
const value = arg.value();
|
|
14
|
+
if (value !== undefined) return ok(value);
|
|
15
|
+
}
|
|
16
|
+
return notOk([leafDiagnostic(ctx, arg, 'Expected a boolean literal')]);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PslDiagnostic, PslDiagnosticCode } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { nodePslSpan } from '../../resolve';
|
|
3
|
+
import type { AstNode } from '../../syntax/ast-helpers';
|
|
4
|
+
import type { InterpretCtx } from '../types';
|
|
5
|
+
|
|
6
|
+
export const ATTRIBUTE_DIAGNOSTIC_CODE: PslDiagnosticCode = 'PSL_INVALID_ATTRIBUTE_SYNTAX';
|
|
7
|
+
|
|
8
|
+
export function leafDiagnostic(ctx: InterpretCtx, node: AstNode, message: string): PslDiagnostic {
|
|
9
|
+
return {
|
|
10
|
+
code: ATTRIBUTE_DIAGNOSTIC_CODE,
|
|
11
|
+
message,
|
|
12
|
+
sourceId: ctx.sourceId,
|
|
13
|
+
span: nodePslSpan(node.syntax, ctx.sourceFile),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { IdentifierAst } from '../../syntax/ast/identifier';
|
|
4
|
+
import type { ArgType } from '../types';
|
|
5
|
+
import { leafDiagnostic } from './diagnostic';
|
|
6
|
+
|
|
7
|
+
// A bare model-name reference. Existence of a model with this name is resolved
|
|
8
|
+
// downstream (e.g. `resolvePolymorphism`), not here.
|
|
9
|
+
export function entityRef(): ArgType<string> {
|
|
10
|
+
return {
|
|
11
|
+
kind: 'entityRef',
|
|
12
|
+
label: 'model name',
|
|
13
|
+
parse: (arg, ctx): Result<string, readonly PslDiagnostic[]> => {
|
|
14
|
+
if (!(arg instanceof IdentifierAst)) {
|
|
15
|
+
return notOk([leafDiagnostic(ctx, arg, 'Expected a model name')]);
|
|
16
|
+
}
|
|
17
|
+
const name = arg.name();
|
|
18
|
+
if (name === undefined) {
|
|
19
|
+
return notOk([leafDiagnostic(ctx, arg, 'Expected a model name')]);
|
|
20
|
+
}
|
|
21
|
+
return ok(name);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { IdentifierAst } from '../../syntax/ast/identifier';
|
|
4
|
+
import type { ArgType } from '../types';
|
|
5
|
+
import { leafDiagnostic } from './diagnostic';
|
|
6
|
+
|
|
7
|
+
export type FieldRefScope = 'self' | 'referenced';
|
|
8
|
+
|
|
9
|
+
export interface FieldRefArgType extends ArgType<string> {
|
|
10
|
+
readonly scope: FieldRefScope;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function fieldRef(scope: FieldRefScope): FieldRefArgType {
|
|
14
|
+
return {
|
|
15
|
+
kind: 'fieldRef',
|
|
16
|
+
label: 'field name',
|
|
17
|
+
scope,
|
|
18
|
+
parse: (arg, ctx): Result<string, readonly PslDiagnostic[]> => {
|
|
19
|
+
if (!(arg instanceof IdentifierAst)) {
|
|
20
|
+
return notOk([leafDiagnostic(ctx, arg, 'Expected a field name')]);
|
|
21
|
+
}
|
|
22
|
+
const name = arg.name();
|
|
23
|
+
if (name === undefined) {
|
|
24
|
+
return notOk([leafDiagnostic(ctx, arg, 'Expected a field name')]);
|
|
25
|
+
}
|
|
26
|
+
const model = scope === 'self' ? ctx.selfModel : ctx.resolveReferencedModel();
|
|
27
|
+
// A referenced model in another space can't be resolved here (resolveReferencedModel returns undefined); skip the existence check — it runs where that model is known.
|
|
28
|
+
if (model !== undefined && !Object.hasOwn(model.fields, name)) {
|
|
29
|
+
return notOk([
|
|
30
|
+
leafDiagnostic(ctx, arg, `Field "${name}" does not exist on model "${model.name}"`),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
return ok(name);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { IdentifierAst } from '../../syntax/ast/identifier';
|
|
4
|
+
import type { ArgType } from '../types';
|
|
5
|
+
import { leafDiagnostic } from './diagnostic';
|
|
6
|
+
|
|
7
|
+
export function identifier<const N extends string>(name: N): ArgType<N> {
|
|
8
|
+
return {
|
|
9
|
+
kind: 'identifier',
|
|
10
|
+
label: name,
|
|
11
|
+
parse: (arg, ctx): Result<N, readonly PslDiagnostic[]> => {
|
|
12
|
+
if (arg instanceof IdentifierAst && arg.name() === name) return ok(name);
|
|
13
|
+
return notOk([leafDiagnostic(ctx, arg, `Expected ${name}`)]);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { NumberLiteralExprAst } from '../../syntax/ast/expressions';
|
|
4
|
+
import type { ArgType } from '../types';
|
|
5
|
+
import { leafDiagnostic } from './diagnostic';
|
|
6
|
+
|
|
7
|
+
export function int(): ArgType<number> {
|
|
8
|
+
return {
|
|
9
|
+
kind: 'int',
|
|
10
|
+
label: 'integer',
|
|
11
|
+
parse: (arg, ctx): Result<number, readonly PslDiagnostic[]> => {
|
|
12
|
+
if (arg instanceof NumberLiteralExprAst) {
|
|
13
|
+
const value = arg.value();
|
|
14
|
+
if (value !== undefined && Number.isInteger(value)) return ok(value);
|
|
15
|
+
}
|
|
16
|
+
return notOk([leafDiagnostic(ctx, arg, 'Expected an integer literal')]);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { ArrayLiteralAst, type ExpressionAst } from '../../syntax/ast/expressions';
|
|
4
|
+
import type { ArgType } from '../types';
|
|
5
|
+
import { leafDiagnostic } from './diagnostic';
|
|
6
|
+
|
|
7
|
+
export interface ListOptions {
|
|
8
|
+
readonly nonEmpty?: boolean;
|
|
9
|
+
readonly unique?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function list<T>(of: ArgType<T>, opts?: ListOptions): ArgType<T[]> {
|
|
13
|
+
return {
|
|
14
|
+
kind: 'list',
|
|
15
|
+
label: `${of.label}[]`,
|
|
16
|
+
parse: (arg, ctx): Result<T[], readonly PslDiagnostic[]> => {
|
|
17
|
+
if (!(arg instanceof ArrayLiteralAst)) {
|
|
18
|
+
return notOk([leafDiagnostic(ctx, arg, `Expected a list of ${of.label}`)]);
|
|
19
|
+
}
|
|
20
|
+
const diagnostics: PslDiagnostic[] = [];
|
|
21
|
+
const parsed: { node: ExpressionAst; value: T }[] = [];
|
|
22
|
+
let count = 0;
|
|
23
|
+
for (const element of arg.elements()) {
|
|
24
|
+
count += 1;
|
|
25
|
+
const result = of.parse(element, ctx);
|
|
26
|
+
if (result.ok) parsed.push({ node: element, value: result.value });
|
|
27
|
+
else diagnostics.push(...result.failure);
|
|
28
|
+
}
|
|
29
|
+
if (opts?.nonEmpty === true && count === 0) {
|
|
30
|
+
diagnostics.push(leafDiagnostic(ctx, arg, 'Expected a non-empty list'));
|
|
31
|
+
}
|
|
32
|
+
if (opts?.unique === true) {
|
|
33
|
+
const seen = new Set<T>();
|
|
34
|
+
for (const { node, value } of parsed) {
|
|
35
|
+
if (seen.has(value)) diagnostics.push(leafDiagnostic(ctx, node, 'Duplicate list entry'));
|
|
36
|
+
else seen.add(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (diagnostics.length > 0) return notOk(diagnostics);
|
|
40
|
+
return ok(parsed.map((entry) => entry.value));
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
3
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
4
|
+
import type { ArgType, OutOf } from '../types';
|
|
5
|
+
import { leafDiagnostic } from './diagnostic';
|
|
6
|
+
|
|
7
|
+
export function oneOf<Alts extends readonly [ArgType<unknown>, ...ArgType<unknown>[]]>(
|
|
8
|
+
...alts: Alts
|
|
9
|
+
): ArgType<OutOf<Alts[number]>> {
|
|
10
|
+
const label = alts.map((alt) => alt.label).join(' | ');
|
|
11
|
+
return {
|
|
12
|
+
kind: 'oneOf',
|
|
13
|
+
label,
|
|
14
|
+
parse: (arg, ctx): Result<OutOf<Alts[number]>, readonly PslDiagnostic[]> => {
|
|
15
|
+
for (const alt of alts) {
|
|
16
|
+
const result = alt.parse(arg, ctx);
|
|
17
|
+
if (result.ok) {
|
|
18
|
+
return ok(
|
|
19
|
+
blindCast<
|
|
20
|
+
OutOf<Alts[number]>,
|
|
21
|
+
'The matched value comes from an alternative whose output type is a member of the union, but iterating the tuple widens each element to ArgType<unknown>, erasing that relationship.'
|
|
22
|
+
>(result.value),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return notOk([leafDiagnostic(ctx, arg, `Expected one of: ${label}`)]);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { ObjectLiteralExprAst } from '../../syntax/ast/expressions';
|
|
4
|
+
import type { ArgType } from '../types';
|
|
5
|
+
import { leafDiagnostic } from './diagnostic';
|
|
6
|
+
|
|
7
|
+
export function record<T>(of: ArgType<T>): ArgType<Record<string, T>> {
|
|
8
|
+
return {
|
|
9
|
+
kind: 'record',
|
|
10
|
+
label: `{ [key]: ${of.label} }`,
|
|
11
|
+
parse: (arg, ctx): Result<Record<string, T>, readonly PslDiagnostic[]> => {
|
|
12
|
+
if (!(arg instanceof ObjectLiteralExprAst)) {
|
|
13
|
+
return notOk([leafDiagnostic(ctx, arg, 'Expected an object literal')]);
|
|
14
|
+
}
|
|
15
|
+
const diagnostics: PslDiagnostic[] = [];
|
|
16
|
+
const result: Record<string, T> = {};
|
|
17
|
+
for (const field of arg.fields()) {
|
|
18
|
+
const key = field.keyName();
|
|
19
|
+
if (key === undefined) {
|
|
20
|
+
diagnostics.push(leafDiagnostic(ctx, field, 'Expected a key'));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const value = field.value();
|
|
24
|
+
if (value === undefined) {
|
|
25
|
+
diagnostics.push(leafDiagnostic(ctx, field, `Expected a value for key "${key}"`));
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const parsed = of.parse(value, ctx);
|
|
29
|
+
if (!parsed.ok) {
|
|
30
|
+
diagnostics.push(...parsed.failure);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (Object.hasOwn(result, key)) {
|
|
34
|
+
diagnostics.push(leafDiagnostic(ctx, field, `Duplicate key "${key}"`));
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
result[key] = parsed.value;
|
|
38
|
+
}
|
|
39
|
+
if (diagnostics.length > 0) return notOk(diagnostics);
|
|
40
|
+
return ok(result);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { StringLiteralExprAst } from '../../syntax/ast/expressions';
|
|
4
|
+
import type { ArgType } from '../types';
|
|
5
|
+
import { leafDiagnostic } from './diagnostic';
|
|
6
|
+
|
|
7
|
+
export function str(): ArgType<string> {
|
|
8
|
+
return {
|
|
9
|
+
kind: 'str',
|
|
10
|
+
label: 'string',
|
|
11
|
+
parse: (arg, ctx): Result<string, readonly PslDiagnostic[]> => {
|
|
12
|
+
if (arg instanceof StringLiteralExprAst) {
|
|
13
|
+
const value = arg.value();
|
|
14
|
+
if (value !== undefined) return ok(value);
|
|
15
|
+
}
|
|
16
|
+
return notOk([leafDiagnostic(ctx, arg, 'Expected a string literal')]);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import type { AttributeOut, AttributeSpec, InterpretCtx, Param, PositionalParam } from './types';
|
|
3
|
+
|
|
4
|
+
interface FieldAttributeConfig<
|
|
5
|
+
Pos extends readonly PositionalParam[],
|
|
6
|
+
Named extends Record<string, Param<unknown>>,
|
|
7
|
+
> {
|
|
8
|
+
readonly positional?: Pos;
|
|
9
|
+
readonly named?: Named;
|
|
10
|
+
readonly refine?: (
|
|
11
|
+
parsed: AttributeOut<Pos, Named>,
|
|
12
|
+
ctx: InterpretCtx,
|
|
13
|
+
) => readonly PslDiagnostic[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function fieldAttribute<
|
|
17
|
+
const Pos extends readonly PositionalParam[] = readonly [],
|
|
18
|
+
const Named extends Record<string, Param<unknown>> = Record<never, never>,
|
|
19
|
+
>(name: string, config: FieldAttributeConfig<Pos, Named>): AttributeSpec<AttributeOut<Pos, Named>> {
|
|
20
|
+
return {
|
|
21
|
+
level: 'field',
|
|
22
|
+
name,
|
|
23
|
+
positional: config.positional ?? [],
|
|
24
|
+
named: config.named ?? {},
|
|
25
|
+
...(config.refine !== undefined ? { refine: config.refine } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { PslDiagnostic, PslSpan } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
3
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
4
|
+
import { nodePslSpan } from '../resolve';
|
|
5
|
+
import type { FieldAttributeAst, ModelAttributeAst } from '../syntax/ast/attributes';
|
|
6
|
+
import type { AttributeArgAst } from '../syntax/ast/expressions';
|
|
7
|
+
import { ATTRIBUTE_DIAGNOSTIC_CODE } from './combinators/diagnostic';
|
|
8
|
+
import type { ArgType, AttributeSpec, InterpretCtx, OptionalArgType, Param } from './types';
|
|
9
|
+
|
|
10
|
+
export function interpretAttribute<Out>(
|
|
11
|
+
attrNode: FieldAttributeAst | ModelAttributeAst,
|
|
12
|
+
spec: AttributeSpec<Out>,
|
|
13
|
+
ctx: InterpretCtx,
|
|
14
|
+
): Result<Out, readonly PslDiagnostic[]> {
|
|
15
|
+
const diagnostics: PslDiagnostic[] = [];
|
|
16
|
+
const attributeSpan = nodePslSpan(attrNode.syntax, ctx.sourceFile);
|
|
17
|
+
|
|
18
|
+
const output: Record<string, unknown> = {};
|
|
19
|
+
const seen = new Set<string>();
|
|
20
|
+
let positionalSlot = 0;
|
|
21
|
+
let reportedExcess = false;
|
|
22
|
+
|
|
23
|
+
for (const arg of attrNode.argList()?.args() ?? []) {
|
|
24
|
+
const name = arg.name()?.name();
|
|
25
|
+
|
|
26
|
+
let key: string;
|
|
27
|
+
let param: Param<unknown>;
|
|
28
|
+
if (name === undefined) {
|
|
29
|
+
const posParam = spec.positional[positionalSlot];
|
|
30
|
+
if (posParam === undefined) {
|
|
31
|
+
if (!reportedExcess) {
|
|
32
|
+
diagnostics.push(
|
|
33
|
+
diagnostic(
|
|
34
|
+
`Attribute "${spec.name}" received too many positional arguments`,
|
|
35
|
+
ctx,
|
|
36
|
+
attributeSpan,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
reportedExcess = true;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
positionalSlot += 1;
|
|
44
|
+
key = posParam.key;
|
|
45
|
+
param = posParam.type;
|
|
46
|
+
} else {
|
|
47
|
+
const namedParam = Object.hasOwn(spec.named, name) ? spec.named[name] : undefined;
|
|
48
|
+
if (namedParam === undefined) {
|
|
49
|
+
diagnostics.push(
|
|
50
|
+
diagnostic(
|
|
51
|
+
`Attribute "${spec.name}" received unknown argument "${name}"`,
|
|
52
|
+
ctx,
|
|
53
|
+
nodePslSpan(arg.syntax, ctx.sourceFile),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
key = name;
|
|
59
|
+
param = namedParam;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (seen.has(key)) {
|
|
63
|
+
diagnostics.push(
|
|
64
|
+
diagnostic(
|
|
65
|
+
`Attribute "${spec.name}" received duplicate argument "${key}"`,
|
|
66
|
+
ctx,
|
|
67
|
+
nodePslSpan(arg.syntax, ctx.sourceFile),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
seen.add(key);
|
|
73
|
+
const result = parseArgValue(arg, param, ctx, diagnostics);
|
|
74
|
+
if (result.ok) output[key] = result.value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const finalized = new Set<string>();
|
|
78
|
+
const finalizeAbsentKey = (
|
|
79
|
+
key: string,
|
|
80
|
+
positionalParam: Param<unknown> | undefined,
|
|
81
|
+
namedParam: Param<unknown> | undefined,
|
|
82
|
+
): void => {
|
|
83
|
+
if (finalized.has(key) || seen.has(key)) return;
|
|
84
|
+
finalized.add(key);
|
|
85
|
+
const effective = namedParam ?? positionalParam;
|
|
86
|
+
if (effective === undefined) return;
|
|
87
|
+
if (isOptionalArgType(effective)) {
|
|
88
|
+
if (effective.hasDefault) output[key] = effective.defaultValue;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
diagnostics.push(
|
|
92
|
+
diagnostic(
|
|
93
|
+
`Attribute "${spec.name}" is missing required argument "${key}"`,
|
|
94
|
+
ctx,
|
|
95
|
+
attributeSpan,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (const param of spec.positional) {
|
|
101
|
+
const namedParam = Object.hasOwn(spec.named, param.key) ? spec.named[param.key] : undefined;
|
|
102
|
+
finalizeAbsentKey(param.key, param.type, namedParam);
|
|
103
|
+
}
|
|
104
|
+
for (const key of Object.keys(spec.named)) {
|
|
105
|
+
finalizeAbsentKey(key, undefined, spec.named[key]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (diagnostics.length > 0) {
|
|
109
|
+
return notOk<readonly PslDiagnostic[]>(diagnostics);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const value = blindCast<
|
|
113
|
+
Out,
|
|
114
|
+
'The engine builds the output object structurally from the spec; TypeScript cannot relate the dynamically-keyed record to the spec-inferred output type.'
|
|
115
|
+
>(output);
|
|
116
|
+
if (spec.refine !== undefined) {
|
|
117
|
+
const refineDiagnostics = spec.refine(value, ctx);
|
|
118
|
+
if (refineDiagnostics.length > 0) {
|
|
119
|
+
return notOk<readonly PslDiagnostic[]>(refineDiagnostics);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return ok(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseArgValue(
|
|
126
|
+
arg: AttributeArgAst,
|
|
127
|
+
argType: ArgType<unknown>,
|
|
128
|
+
ctx: InterpretCtx,
|
|
129
|
+
diagnostics: PslDiagnostic[],
|
|
130
|
+
): Result<unknown, readonly PslDiagnostic[]> {
|
|
131
|
+
const value = arg.value();
|
|
132
|
+
if (value === undefined) {
|
|
133
|
+
const missing = diagnostic(
|
|
134
|
+
'Attribute argument is missing a value',
|
|
135
|
+
ctx,
|
|
136
|
+
nodePslSpan(arg.syntax, ctx.sourceFile),
|
|
137
|
+
);
|
|
138
|
+
diagnostics.push(missing);
|
|
139
|
+
return notOk<readonly PslDiagnostic[]>([missing]);
|
|
140
|
+
}
|
|
141
|
+
const result = argType.parse(value, ctx);
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
for (const failure of result.failure) diagnostics.push(failure);
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isOptionalArgType(param: Param<unknown>): param is OptionalArgType<unknown> {
|
|
149
|
+
return 'optional' in param && param.optional === true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function diagnostic(message: string, ctx: InterpretCtx, span: PslSpan): PslDiagnostic {
|
|
153
|
+
return { code: ATTRIBUTE_DIAGNOSTIC_CODE, message, sourceId: ctx.sourceId, span };
|
|
154
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import type { AttributeOut, AttributeSpec, InterpretCtx, Param, PositionalParam } from './types';
|
|
3
|
+
|
|
4
|
+
interface ModelAttributeConfig<
|
|
5
|
+
Pos extends readonly PositionalParam[],
|
|
6
|
+
Named extends Record<string, Param<unknown>>,
|
|
7
|
+
> {
|
|
8
|
+
readonly positional?: Pos;
|
|
9
|
+
readonly named?: Named;
|
|
10
|
+
readonly refine?: (
|
|
11
|
+
parsed: AttributeOut<Pos, Named>,
|
|
12
|
+
ctx: InterpretCtx,
|
|
13
|
+
) => readonly PslDiagnostic[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function modelAttribute<
|
|
17
|
+
const Pos extends readonly PositionalParam[] = readonly [],
|
|
18
|
+
const Named extends Record<string, Param<unknown>> = Record<never, never>,
|
|
19
|
+
>(name: string, config: ModelAttributeConfig<Pos, Named>): AttributeSpec<AttributeOut<Pos, Named>> {
|
|
20
|
+
return {
|
|
21
|
+
level: 'model',
|
|
22
|
+
name,
|
|
23
|
+
positional: config.positional ?? [],
|
|
24
|
+
named: config.named ?? {},
|
|
25
|
+
...(config.refine !== undefined ? { refine: config.refine } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ArgType, OptionalArgType } from './types';
|
|
2
|
+
|
|
3
|
+
export function optional<T>(type: ArgType<T>, ...rest: [defaultValue: T] | []): OptionalArgType<T> {
|
|
4
|
+
if (rest.length === 0) {
|
|
5
|
+
return { ...type, optional: true, hasDefault: false };
|
|
6
|
+
}
|
|
7
|
+
return { ...type, optional: true, hasDefault: true, defaultValue: rest[0] };
|
|
8
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
|
|
2
|
+
import type { Result } from '@prisma-next/utils/result';
|
|
3
|
+
import type { Simplify, UnionToIntersection } from '@prisma-next/utils/types';
|
|
4
|
+
import type { SourceFile } from '../source-file';
|
|
5
|
+
import type { FieldSymbol, ModelSymbol } from '../symbol-table';
|
|
6
|
+
import type { ExpressionAst } from '../syntax/ast/expressions';
|
|
7
|
+
|
|
8
|
+
export type AttributeLevel = 'field' | 'model' | 'block';
|
|
9
|
+
|
|
10
|
+
export interface ArgType<T> {
|
|
11
|
+
readonly kind: string;
|
|
12
|
+
readonly label: string;
|
|
13
|
+
// phantom carrier for `T`; never read at runtime.
|
|
14
|
+
readonly _out?: T;
|
|
15
|
+
parse(arg: ExpressionAst, ctx: InterpretCtx): Result<T, readonly PslDiagnostic[]>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InterpretCtx {
|
|
19
|
+
readonly level: AttributeLevel;
|
|
20
|
+
readonly sourceId: string;
|
|
21
|
+
readonly sourceFile: SourceFile;
|
|
22
|
+
readonly selfModel: ModelSymbol;
|
|
23
|
+
resolveReferencedModel(): ModelSymbol | undefined;
|
|
24
|
+
readonly field?: FieldSymbol;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OptionalArgType<T> extends ArgType<T> {
|
|
28
|
+
// the engine detects optionality by checking for this marker (`'optional' in param`).
|
|
29
|
+
readonly optional: true;
|
|
30
|
+
readonly hasDefault: boolean;
|
|
31
|
+
readonly defaultValue?: T;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Param<T> = ArgType<T>;
|
|
35
|
+
|
|
36
|
+
export interface PositionalParam<T = unknown> {
|
|
37
|
+
readonly key: string;
|
|
38
|
+
readonly type: Param<T>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AttributeSpec<Out> {
|
|
42
|
+
readonly level: AttributeLevel;
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly positional: readonly PositionalParam[];
|
|
45
|
+
readonly named: Readonly<Record<string, Param<unknown>>>;
|
|
46
|
+
readonly refine?: (parsed: Out, ctx: InterpretCtx) => readonly PslDiagnostic[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type OutOf<P> = P extends ArgType<infer T> ? T : never;
|
|
50
|
+
|
|
51
|
+
export type NamedOut<N extends Record<string, Param<unknown>>> = Simplify<
|
|
52
|
+
{ [K in keyof N as N[K] extends OptionalArgType<unknown> ? never : K]: OutOf<N[K]> } & {
|
|
53
|
+
[K in keyof N as N[K] extends OptionalArgType<unknown> ? K : never]?: OutOf<N[K]>;
|
|
54
|
+
}
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
type PosEntryObject<E extends PositionalParam> =
|
|
58
|
+
E['type'] extends OptionalArgType<unknown>
|
|
59
|
+
? { [K in E['key']]?: OutOf<E['type']> }
|
|
60
|
+
: { [K in E['key']]: OutOf<E['type']> };
|
|
61
|
+
|
|
62
|
+
export type PosOut<Pos extends readonly PositionalParam[]> = Simplify<
|
|
63
|
+
UnionToIntersection<{ [I in keyof Pos]: PosEntryObject<Pos[I]> }[number]>
|
|
64
|
+
>;
|
|
65
|
+
|
|
66
|
+
export type AttributeOut<
|
|
67
|
+
Pos extends readonly PositionalParam[],
|
|
68
|
+
Named extends Record<string, Param<unknown>>,
|
|
69
|
+
> = Simplify<PosOut<Pos> & NamedOut<Named>>;
|
|
70
|
+
|
|
71
|
+
// `S` is unconstrained on purpose: `refine` makes `Out` contravariant, so a bound like `S extends AttributeSpec<unknown>` would reject every spec that uses `refine`.
|
|
72
|
+
export type InferAttr<S> = S extends AttributeSpec<infer Out> ? Out : never;
|
package/src/exports/index.ts
CHANGED
|
@@ -36,6 +36,36 @@ export {
|
|
|
36
36
|
namespacePslExtensionBlocks,
|
|
37
37
|
} from '@prisma-next/framework-components/psl-ast';
|
|
38
38
|
export { getPositionalArgument, parseQuotedStringLiteral } from '../attribute-helpers';
|
|
39
|
+
export { bool } from '../attribute-spec/combinators/bool';
|
|
40
|
+
export { leafDiagnostic } from '../attribute-spec/combinators/diagnostic';
|
|
41
|
+
export { entityRef } from '../attribute-spec/combinators/entity-ref';
|
|
42
|
+
export type { FieldRefArgType, FieldRefScope } from '../attribute-spec/combinators/field-ref';
|
|
43
|
+
export { fieldRef } from '../attribute-spec/combinators/field-ref';
|
|
44
|
+
export { identifier } from '../attribute-spec/combinators/identifier';
|
|
45
|
+
export { int } from '../attribute-spec/combinators/int';
|
|
46
|
+
export type { ListOptions } from '../attribute-spec/combinators/list';
|
|
47
|
+
export { list } from '../attribute-spec/combinators/list';
|
|
48
|
+
export { oneOf } from '../attribute-spec/combinators/one-of';
|
|
49
|
+
export { record } from '../attribute-spec/combinators/record';
|
|
50
|
+
export { str } from '../attribute-spec/combinators/str';
|
|
51
|
+
export { fieldAttribute } from '../attribute-spec/field-attribute';
|
|
52
|
+
export { interpretAttribute } from '../attribute-spec/interpret';
|
|
53
|
+
export { modelAttribute } from '../attribute-spec/model-attribute';
|
|
54
|
+
export { optional } from '../attribute-spec/optional';
|
|
55
|
+
export type {
|
|
56
|
+
ArgType,
|
|
57
|
+
AttributeLevel,
|
|
58
|
+
AttributeOut,
|
|
59
|
+
AttributeSpec,
|
|
60
|
+
InferAttr,
|
|
61
|
+
InterpretCtx,
|
|
62
|
+
NamedOut,
|
|
63
|
+
OptionalArgType,
|
|
64
|
+
OutOf,
|
|
65
|
+
Param,
|
|
66
|
+
PositionalParam,
|
|
67
|
+
PosOut,
|
|
68
|
+
} from '../attribute-spec/types';
|
|
39
69
|
export { findBlockDescriptor, validateExtensionBlockFromSymbol } from '../extension-block';
|
|
40
70
|
export {
|
|
41
71
|
keywordPslSpan,
|