@cratis/screenplay-language 4.17.0 → 4.18.0
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/completion-items.ts +7 -1
- package/diagnostic-codes.ts +5 -0
- package/dist/completion-items.d.ts.map +1 -1
- package/dist/completion-items.js +6 -1
- package/dist/completion-items.js.map +1 -1
- package/dist/diagnostic-codes.d.ts +5 -0
- package/dist/diagnostic-codes.d.ts.map +1 -1
- package/dist/diagnostic-codes.js +5 -0
- package/dist/diagnostic-codes.js.map +1 -1
- package/dist/event-context-catalog.d.ts +14 -0
- package/dist/event-context-catalog.d.ts.map +1 -0
- package/dist/event-context-catalog.js +116 -0
- package/dist/event-context-catalog.js.map +1 -0
- package/dist/event-context.d.ts +38 -0
- package/dist/event-context.d.ts.map +1 -0
- package/dist/event-context.js +88 -0
- package/dist/event-context.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keyword-docs.d.ts.map +1 -1
- package/dist/keyword-docs.js +3 -1
- package/dist/keyword-docs.js.map +1 -1
- package/dist/sub-languages/projection/CompletionProvider.d.ts +1 -0
- package/dist/sub-languages/projection/CompletionProvider.d.ts.map +1 -1
- package/dist/sub-languages/projection/CompletionProvider.js +39 -69
- package/dist/sub-languages/projection/CompletionProvider.js.map +1 -1
- package/dist/sub-languages/projection/HoverProvider.d.ts +1 -0
- package/dist/sub-languages/projection/HoverProvider.d.ts.map +1 -1
- package/dist/sub-languages/projection/HoverProvider.js +36 -4
- package/dist/sub-languages/projection/HoverProvider.js.map +1 -1
- package/dist/sub-languages/projection/Validator.d.ts.map +1 -1
- package/dist/sub-languages/projection/Validator.js +14 -0
- package/dist/sub-languages/projection/Validator.js.map +1 -1
- package/dist/sub-languages/projection/language.d.ts.map +1 -1
- package/dist/sub-languages/projection/language.js +0 -2
- package/dist/sub-languages/projection/language.js.map +1 -1
- package/dist/validation.d.ts +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +52 -0
- package/dist/validation.js.map +1 -1
- package/event-context-catalog.ts +135 -0
- package/event-context.ts +119 -0
- package/for_eventContext/when_resolving_generated_catalog_vectors.ts +15 -0
- package/for_projectionCompletionProvider/when_completing_an_event_context_path.ts +102 -0
- package/for_projectionHoverProvider/when_hovering_an_event_context_path.ts +44 -0
- package/for_projectionValidator/when_validating_event_context_paths.ts +31 -0
- package/for_validateLines/when_validating_a_document/with_a_problem_of_every_coded_kind.ts +7 -0
- package/for_validateLines/when_validating_event_context_paths/in_a_projection.ts +97 -0
- package/index.ts +2 -0
- package/keyword-docs.ts +4 -1
- package/package.json +1 -1
- package/sub-languages/projection/CompletionProvider.ts +47 -72
- package/sub-languages/projection/HoverProvider.ts +40 -4
- package/sub-languages/projection/Validator.ts +15 -0
- package/sub-languages/projection/language.ts +0 -2
- package/validation.ts +59 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { describe, beforeEach, it } from 'vitest';
|
|
5
|
+
import { diagnosticCodes } from '../../diagnostic-codes';
|
|
6
|
+
import { eventContextPaths } from '../../event-context';
|
|
7
|
+
import { ValidationIssue, validateLines } from '../../validation';
|
|
8
|
+
|
|
9
|
+
function validate(...mappings: string[]): ValidationIssue[] {
|
|
10
|
+
return validateLines([
|
|
11
|
+
'projection Statistics => StatisticsReadModel',
|
|
12
|
+
' from Happened',
|
|
13
|
+
...mappings.map((mapping) => ` ${mapping}`),
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('when validating every event context path the catalog lists', () => {
|
|
18
|
+
let issues: ValidationIssue[];
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
issues = validate(
|
|
22
|
+
...eventContextPaths.map((path, index) => `value${index} = $eventContext.${path.path}`),
|
|
23
|
+
...eventContextPaths.map((path) => `count countBy.$eventContext.${path.path}`),
|
|
24
|
+
'week = $eventContext.occurred.Week()',
|
|
25
|
+
'type = $eventContext.EventType.Id',
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should report nothing', () => {
|
|
30
|
+
issues.should.be.empty;
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('when validating an unknown event context member', () => {
|
|
35
|
+
let issues: ValidationIssue[];
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
issues = validate('cause = $eventContext.causationId');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should warn with the compiler code', () => {
|
|
42
|
+
issues.map((issue) => [issue.code, issue.severity]).should.deep.equal([[diagnosticCodes.unknownEventContextMember, 'warning']]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should squiggle the whole path', () => {
|
|
46
|
+
const [issue] = issues;
|
|
47
|
+
[issue.startColumn, issue.endColumn].should.deep.equal([13, 13 + '$eventContext.causationId'.length]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('when validating an unknown event context sub-path', () => {
|
|
52
|
+
let issues: ValidationIssue[];
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
issues = validate('week = $eventContext.occurred.week');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should warn with the compiler code', () => {
|
|
59
|
+
issues.map((issue) => [issue.code, issue.severity]).should.deep.equal([[diagnosticCodes.unknownEventContextPath, 'warning']]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('when validating a path below an event context collection', () => {
|
|
64
|
+
let issues: ValidationIssue[];
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
issues = validate('count byTag.$eventContext.tags.value');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should report an error with the compiler code', () => {
|
|
71
|
+
issues.map((issue) => [issue.code, issue.severity]).should.deep.equal([[diagnosticCodes.eventContextPathBelowCollection, 'error']]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('when validating a dynamic key naming no event context member', () => {
|
|
76
|
+
let issues: ValidationIssue[];
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
issues = validate('count byNothing.$eventContext');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should report an error with the compiler code', () => {
|
|
83
|
+
issues.map((issue) => [issue.code, issue.severity]).should.deep.equal([[diagnosticCodes.missingEventContextPath, 'error']]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('when validating a dynamic key naming another source', () => {
|
|
88
|
+
let issues: ValidationIssue[];
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
issues = validate('count byUser.$causedBy.subject');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should warn with the compiler code', () => {
|
|
95
|
+
issues.map((issue) => [issue.code, issue.severity]).should.deep.equal([[diagnosticCodes.unresolvedDynamicKeySource, 'warning']]);
|
|
96
|
+
});
|
|
97
|
+
});
|
package/index.ts
CHANGED
|
@@ -100,6 +100,8 @@ export type {
|
|
|
100
100
|
TypeSymbol,
|
|
101
101
|
} from './symbols';
|
|
102
102
|
export { attributeDocs, contextVariableDocs, keywordDocs } from './keyword-docs';
|
|
103
|
+
export { eventContextMemberAt, eventContextMembers, eventContextMembersAfter, eventContextPaths, namesEventContextMember } from './event-context';
|
|
104
|
+
export type { EventContextMember, EventContextPath } from './event-context';
|
|
103
105
|
export { contextVariableItems, producesItems } from './completion-items';
|
|
104
106
|
export type { CompletionEntry } from './completion-items';
|
|
105
107
|
export { completionEntriesFor, planCompletions } from './completion-planner';
|
package/keyword-docs.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
+
import { eventContextPaths } from './event-context';
|
|
5
|
+
|
|
4
6
|
export const keywordDocs: Record<string, string> = {
|
|
5
7
|
domain: 'The domain the file belongs to — optional, at most one per file, before everything else.',
|
|
6
8
|
import: 'Imports a type from another module; it becomes available by its short name.',
|
|
@@ -128,5 +130,6 @@ export const contextVariableDocs: Record<string, string> = {
|
|
|
128
130
|
'$env': 'An environment variable, e.g. `$env.SERVICE_NAME`.',
|
|
129
131
|
'$secrets': 'An encrypted secret resolved at runtime, e.g. `$secrets.azureAdClientId` — stays symbolic in the compiled tree.',
|
|
130
132
|
'$strings': 'A localized string resolved from the `.strings` file of the active locale, e.g. `$strings.invoices.title`.',
|
|
131
|
-
|
|
133
|
+
// Every $eventContext path the event-context catalog lists, for projections.
|
|
134
|
+
...Object.fromEntries(eventContextPaths.map((path) => [`$eventContext.${path.path}`, path.description])),
|
|
132
135
|
};
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { editor, languages, IRange, Position } from 'monaco-editor';
|
|
5
5
|
import type { JsonSchema, JsonSchemaProperty, ReadModelInfo } from './index';
|
|
6
|
+
import { eventContextMembers, eventContextMembersAfter } from '../../event-context';
|
|
6
7
|
|
|
7
8
|
interface CompletionContext {
|
|
8
9
|
lineContent: string;
|
|
@@ -13,6 +14,10 @@ interface CompletionContext {
|
|
|
13
14
|
lineNumber: number;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
// The path typed so far after $eventContext. - on its own or as the dynamic key of a mapping target - and the
|
|
18
|
+
// partial segment under the cursor.
|
|
19
|
+
const EVENT_CONTEXT_PATH = /\$eventContext\.((?:\w+\.)*)(\w*)$/;
|
|
20
|
+
|
|
16
21
|
export class CompletionProvider implements languages.CompletionItemProvider {
|
|
17
22
|
private readModels: ReadModelInfo[] = [];
|
|
18
23
|
private eventSchemas: Record<string, JsonSchema> = {};
|
|
@@ -177,7 +182,10 @@ export class CompletionProvider implements languages.CompletionItemProvider {
|
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
private isPropertyDotAccess(context: CompletionContext): boolean {
|
|
180
|
-
|
|
185
|
+
// A member of an event property or a $-expression, or any depth of an event-context path - written as an
|
|
186
|
+
// expression or as the dynamic key of a mapping target (propertyName.$eventContext.<path>).
|
|
187
|
+
return /(\w+|\$\w+)\.(\w*)$/.test(context.textBeforeCursor) ||
|
|
188
|
+
EVENT_CONTEXT_PATH.test(context.textBeforeCursor);
|
|
181
189
|
}
|
|
182
190
|
|
|
183
191
|
private addProjectionLineCompletions(suggestions: languages.CompletionItem[], context: CompletionContext): void {
|
|
@@ -501,37 +509,14 @@ export class CompletionProvider implements languages.CompletionItemProvider {
|
|
|
501
509
|
{
|
|
502
510
|
label: '$causedBy',
|
|
503
511
|
insertText: '$causedBy',
|
|
504
|
-
documentation: '
|
|
505
|
-
detail: 'Identity Expression'
|
|
506
|
-
children: [
|
|
507
|
-
{ name: 'subject', doc: 'The subject identifier' },
|
|
508
|
-
{ name: 'name', doc: 'The display name' },
|
|
509
|
-
{ name: 'userName', doc: 'The username' },
|
|
510
|
-
]
|
|
512
|
+
documentation: 'The identity that caused the event. Chronicle cannot yet evaluate this short form (Cratis/Chronicle#4119) - prefer $eventContext.causedBy.<property>.',
|
|
513
|
+
detail: 'Identity Expression'
|
|
511
514
|
},
|
|
512
515
|
{
|
|
513
516
|
label: '$eventContext',
|
|
514
517
|
insertText: '$eventContext.',
|
|
515
|
-
documentation:
|
|
516
|
-
detail: 'Event Context Expression'
|
|
517
|
-
children: [
|
|
518
|
-
{ name: 'sequenceNumber', doc: 'The event sequence number' },
|
|
519
|
-
{ name: 'occurred', doc: 'When the event occurred' },
|
|
520
|
-
{ name: 'eventSourceId', doc: 'The event source identifier' },
|
|
521
|
-
{ name: 'namespace', doc: 'The event namespace' },
|
|
522
|
-
]
|
|
523
|
-
},
|
|
524
|
-
{
|
|
525
|
-
label: '$occurred',
|
|
526
|
-
insertText: '$occurred',
|
|
527
|
-
documentation: 'Timestamp when the event occurred',
|
|
528
|
-
detail: 'Occurred Expression'
|
|
529
|
-
},
|
|
530
|
-
{
|
|
531
|
-
label: '$namespace',
|
|
532
|
-
insertText: '$namespace',
|
|
533
|
-
documentation: 'The namespace of the event',
|
|
534
|
-
detail: 'Namespace Expression'
|
|
518
|
+
documentation: `Metadata about the event: ${eventContextMembers.map((member) => member.name).join(', ')}`,
|
|
519
|
+
detail: 'Event Context Expression'
|
|
535
520
|
},
|
|
536
521
|
];
|
|
537
522
|
|
|
@@ -550,55 +535,24 @@ export class CompletionProvider implements languages.CompletionItemProvider {
|
|
|
550
535
|
}
|
|
551
536
|
|
|
552
537
|
private addPropertyMemberCompletions(suggestions: languages.CompletionItem[], context: CompletionContext): void {
|
|
538
|
+
// Any depth of an event-context path, as an expression or as a dynamic key (propertyName.$eventContext.<path>)
|
|
539
|
+
const eventContextMatch = context.textBeforeCursor.match(EVENT_CONTEXT_PATH);
|
|
540
|
+
if (eventContextMatch) {
|
|
541
|
+
const segments = eventContextMatch[1].split('.').filter((segment) => segment.length > 0);
|
|
542
|
+
this.addEventContextMemberCompletions(suggestions, context, segments, eventContextMatch[2]);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
553
546
|
const match = context.textBeforeCursor.match(/(\w+|\$\w+)\.(\w*)$/);
|
|
554
547
|
if (!match) return;
|
|
555
548
|
|
|
556
549
|
const [, objectName, partialProp] = match;
|
|
557
550
|
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
{ name: 'userName', doc: 'The username', type: 'string' },
|
|
564
|
-
];
|
|
565
|
-
|
|
566
|
-
causedByProps.forEach((prop) => {
|
|
567
|
-
if (!partialProp || prop.name.startsWith(partialProp)) {
|
|
568
|
-
suggestions.push({
|
|
569
|
-
label: prop.name,
|
|
570
|
-
kind: 9,
|
|
571
|
-
insertText: prop.name,
|
|
572
|
-
documentation: prop.doc,
|
|
573
|
-
detail: prop.type,
|
|
574
|
-
range: this.getRangeForWord(context),
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Handle $eventContext properties
|
|
582
|
-
if (objectName === '$eventContext') {
|
|
583
|
-
const contextProps = [
|
|
584
|
-
{ name: 'sequenceNumber', doc: 'The event sequence number', type: 'EventSequenceNumber' },
|
|
585
|
-
{ name: 'occurred', doc: 'When the event occurred', type: 'DateTimeOffset' },
|
|
586
|
-
{ name: 'eventSourceId', doc: 'The event source identifier', type: 'string' },
|
|
587
|
-
{ name: 'namespace', doc: 'The event namespace', type: 'string' },
|
|
588
|
-
];
|
|
589
|
-
|
|
590
|
-
contextProps.forEach((prop) => {
|
|
591
|
-
if (!partialProp || prop.name.startsWith(partialProp)) {
|
|
592
|
-
suggestions.push({
|
|
593
|
-
label: prop.name,
|
|
594
|
-
kind: 9,
|
|
595
|
-
insertText: prop.name,
|
|
596
|
-
documentation: prop.doc,
|
|
597
|
-
detail: prop.type,
|
|
598
|
-
range: this.getRangeForWord(context),
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
});
|
|
551
|
+
// $causedBy is the short form of $eventContext.causedBy, at any depth
|
|
552
|
+
const causedByMatch = context.textBeforeCursor.match(/\$causedBy\.((?:\w+\.)*)(\w*)$/);
|
|
553
|
+
if (causedByMatch) {
|
|
554
|
+
const segments = causedByMatch[1].split('.').filter((segment) => segment.length > 0);
|
|
555
|
+
this.addEventContextMemberCompletions(suggestions, context, ['causedBy', ...segments], causedByMatch[2]);
|
|
602
556
|
return;
|
|
603
557
|
}
|
|
604
558
|
|
|
@@ -620,6 +574,27 @@ export class CompletionProvider implements languages.CompletionItemProvider {
|
|
|
620
574
|
}
|
|
621
575
|
}
|
|
622
576
|
|
|
577
|
+
private addEventContextMemberCompletions(
|
|
578
|
+
suggestions: languages.CompletionItem[],
|
|
579
|
+
context: CompletionContext,
|
|
580
|
+
segments: readonly string[],
|
|
581
|
+
partial: string
|
|
582
|
+
): void {
|
|
583
|
+
const members = eventContextMembersAfter(segments) ?? [];
|
|
584
|
+
members
|
|
585
|
+
.filter((member) => !partial || member.name.startsWith(partial))
|
|
586
|
+
.forEach((member) => {
|
|
587
|
+
suggestions.push({
|
|
588
|
+
label: member.name,
|
|
589
|
+
kind: member.kind === 'function' ? 1 : 9,
|
|
590
|
+
insertText: member.name,
|
|
591
|
+
documentation: member.description,
|
|
592
|
+
detail: member.type,
|
|
593
|
+
range: this.getRangeForWord(context),
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
623
598
|
private getCurrentEventType(model: editor.ITextModel, lineNumber: number): string | null {
|
|
624
599
|
// Search backwards to find the event type declaration
|
|
625
600
|
for (let i = lineNumber; i >= 1; i--) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { editor, languages, Position } from 'monaco-editor';
|
|
5
5
|
import type { JsonSchema, JsonSchemaProperty, ReadModelInfo, DraftReadModelInfo } from './index';
|
|
6
|
+
import { describeEventContext, describeEventContextMember, eventContextMemberAt } from '../../event-context';
|
|
6
7
|
|
|
7
8
|
export class HoverProvider implements languages.HoverProvider {
|
|
8
9
|
private readModels: ReadModelInfo[] = [];
|
|
@@ -67,6 +68,20 @@ export class HoverProvider implements languages.HoverProvider {
|
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// A segment of an $eventContext path - before keywords, since a member such as 'id' is also a keyword
|
|
72
|
+
const eventContextInfo = this.getEventContextInfo(lineContent, word.startColumn);
|
|
73
|
+
if (eventContextInfo) {
|
|
74
|
+
return {
|
|
75
|
+
contents: [{ value: eventContextInfo }],
|
|
76
|
+
range: {
|
|
77
|
+
startLineNumber: position.lineNumber,
|
|
78
|
+
startColumn: word.startColumn,
|
|
79
|
+
endLineNumber: position.lineNumber,
|
|
80
|
+
endColumn: word.endColumn,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
// Check if it's a keyword
|
|
71
86
|
const keywordInfo = this.getKeywordInfo(wordText);
|
|
72
87
|
if (keywordInfo) {
|
|
@@ -224,15 +239,36 @@ export class HoverProvider implements languages.HoverProvider {
|
|
|
224
239
|
private getExpressionInfo(word: string): string | null {
|
|
225
240
|
const expressions: Record<string, string> = {
|
|
226
241
|
'$eventSourceId': '**$eventSourceId**\n\nThe identifier of the event source (aggregate/entity) that generated the event.',
|
|
227
|
-
'$causedBy': '**$causedBy**\n\
|
|
228
|
-
'$eventContext':
|
|
229
|
-
'$occurred': '**$occurred**\n\nThe timestamp when the event occurred.',
|
|
230
|
-
'$namespace': '**$namespace**\n\nThe namespace of the event.',
|
|
242
|
+
'$causedBy': '**$causedBy**\n\nThe identity that caused the event - `subject`, `name`, `userName` or `onBehalfOf`, itself an identity.\n\nChronicle cannot yet evaluate this short form (Cratis/Chronicle#4119); prefer **$eventContext.causedBy**.*property*.',
|
|
243
|
+
'$eventContext': describeEventContext(),
|
|
231
244
|
};
|
|
232
245
|
|
|
233
246
|
return expressions[word] || null;
|
|
234
247
|
}
|
|
235
248
|
|
|
249
|
+
// Describes the $eventContext path segment starting at the given column, if the word there is one.
|
|
250
|
+
private getEventContextInfo(lineContent: string, startColumn: number): string | null {
|
|
251
|
+
for (const match of lineContent.matchAll(/\$eventContext((?:\.\w+(?:\(\))?)*)/g)) {
|
|
252
|
+
let column = (match.index ?? 0) + 1;
|
|
253
|
+
if (startColumn === column) return describeEventContext();
|
|
254
|
+
|
|
255
|
+
column += '$eventContext'.length;
|
|
256
|
+
const segments = match[1].split('.').slice(1);
|
|
257
|
+
for (let index = 0; index < segments.length; index++) {
|
|
258
|
+
column += 1;
|
|
259
|
+
if (startColumn === column) {
|
|
260
|
+
const path = segments.slice(0, index + 1);
|
|
261
|
+
const member = eventContextMemberAt(path);
|
|
262
|
+
return member ? describeEventContextMember(path.join('.'), member) : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
column += segments[index].length;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
236
272
|
private getPropertyInfo(model: editor.ITextModel, position: Position, propertyName: string): string | null {
|
|
237
273
|
// Try to find the active read model
|
|
238
274
|
const firstLine = model.getLineContent(1).trim();
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import type { editor } from 'monaco-editor';
|
|
5
5
|
import type { JsonSchema, ReadModelInfo, DraftReadModelInfo } from './index';
|
|
6
|
+
import { fenceMap } from '../../document-context';
|
|
7
|
+
import { validateEventContextPaths } from '../../validation';
|
|
6
8
|
|
|
7
9
|
export class Validator {
|
|
8
10
|
private readModels: ReadModelInfo[] = [];
|
|
@@ -349,6 +351,19 @@ export class Validator {
|
|
|
349
351
|
}
|
|
350
352
|
}
|
|
351
353
|
|
|
354
|
+
// Share the host's event-context path checks and catalog resolution, including dynamic dictionary keys.
|
|
355
|
+
for (const issue of validateEventContextPaths(lines, fenceMap(lines))) {
|
|
356
|
+
markers.push({
|
|
357
|
+
severity: issue.severity === 'error' ? 8 : 4,
|
|
358
|
+
startLineNumber: issue.line + 1,
|
|
359
|
+
endLineNumber: issue.line + 1,
|
|
360
|
+
startColumn: issue.startColumn,
|
|
361
|
+
endColumn: issue.endColumn,
|
|
362
|
+
message: issue.message,
|
|
363
|
+
code: issue.code,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
352
367
|
return markers;
|
|
353
368
|
}
|
|
354
369
|
|
package/validation.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { causedByProperties, contextRoots, identityProperties, primitiveTypes, sliceTypes } from './language';
|
|
5
5
|
import { DiagnosticCode, diagnosticCodes } from './diagnostic-codes';
|
|
6
6
|
import { fenceMap, indentOf } from './document-context';
|
|
7
|
+
import { resolveEventContextPath } from './event-context';
|
|
7
8
|
import {
|
|
8
9
|
DocumentSymbols,
|
|
9
10
|
PropertySymbol,
|
|
@@ -305,6 +306,8 @@ export function validateLines(lines: string[]): ValidationIssue[] {
|
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
|
|
309
|
+
issues.push(...validateEventContextPaths(lines, fences));
|
|
310
|
+
|
|
308
311
|
const fenceLines = lines
|
|
309
312
|
.map((line, index) => ({ line, index }))
|
|
310
313
|
.filter(({ line }) => /^\s*```\s*$/.test(line));
|
|
@@ -324,3 +327,59 @@ export function validateLines(lines: string[]): ValidationIssue[] {
|
|
|
324
327
|
|
|
325
328
|
return issues;
|
|
326
329
|
}
|
|
330
|
+
|
|
331
|
+
// A mapping target: the first word of a mapping line, after the keyword that opens it if there is one.
|
|
332
|
+
const mappingTarget = /^(?:(?:increment|decrement|count|clear|add|subtract|set)\s+)?@?([$\w.]+)(?=\s|=|$)/;
|
|
333
|
+
|
|
334
|
+
// Every $eventContext.<path> - in an expression or as the dynamic key of a mapping target - checked against the
|
|
335
|
+
// event-context catalog the way the compiler checks it (Parsing/EventContextPathValidator.cs).
|
|
336
|
+
export function validateEventContextPaths(lines: string[], fences: boolean[]): ValidationIssue[] {
|
|
337
|
+
const issues: ValidationIssue[] = [];
|
|
338
|
+
const names = (members: readonly { name: string }[]) => members.map((member) => member.name).join(', ');
|
|
339
|
+
|
|
340
|
+
for (let index = 0; index < lines.length; index++) {
|
|
341
|
+
const line = lines[index];
|
|
342
|
+
if (fences[index]) continue;
|
|
343
|
+
|
|
344
|
+
for (const match of line.matchAll(/(\.)?\$eventContext((?:\.(?:\w+(?:\(\))?)?)*)/g)) {
|
|
345
|
+
const token = match[0].substring(match[1]?.length ?? 0);
|
|
346
|
+
const written = match[2];
|
|
347
|
+
if (written.length === 0) {
|
|
348
|
+
// A bare $eventContext names nothing only as a dynamic key; in an expression the compiler rejects it as invalid.
|
|
349
|
+
if (match[1]) {
|
|
350
|
+
issues.push(issue('error', index, (match.index ?? 0) + 2, token.length, `'$eventContext' names no member - expected one of ${names(resolveEventContextPath('').expected)}.`, diagnosticCodes.missingEventContextPath));
|
|
351
|
+
}
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const path = written.substring(1);
|
|
356
|
+
const resolution = resolveEventContextPath(path);
|
|
357
|
+
const column = (match.index ?? 0) + (match[1]?.length ?? 0) + 1;
|
|
358
|
+
switch (resolution.status) {
|
|
359
|
+
case 'missing':
|
|
360
|
+
issues.push(issue('error', index, column, token.length, `'$eventContext.${path}' names no member - expected one of ${names(resolution.expected)}.`, diagnosticCodes.missingEventContextPath));
|
|
361
|
+
break;
|
|
362
|
+
case 'unknownMember':
|
|
363
|
+
issues.push(issue('warning', index, column, token.length, `Unknown event context member '${resolution.segment}' in '$eventContext.${path}' - expected one of ${names(resolution.expected)}.`, diagnosticCodes.unknownEventContextMember));
|
|
364
|
+
break;
|
|
365
|
+
case 'unknownSubPath': {
|
|
366
|
+
const has = resolution.expected.length === 0 ? 'no members to address' : `members ${names(resolution.expected)}`;
|
|
367
|
+
issues.push(issue('warning', index, column, token.length, `Unknown event context path '$eventContext.${path}' - '${resolution.member?.name}' (${resolution.member?.type}) has ${has}.`, diagnosticCodes.unknownEventContextPath));
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case 'belowCollection':
|
|
371
|
+
issues.push(issue('error', index, column, token.length, `'$eventContext.${path}' cannot resolve - '${resolution.member?.name}' is a collection (${resolution.member?.type}) and cannot be addressed below.`, diagnosticCodes.eventContextPathBelowCollection));
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Only $eventContext resolves in a dynamic key; any other $ source is kept as the literal key text.
|
|
377
|
+
const target = line.trim().match(mappingTarget)?.[1];
|
|
378
|
+
const source = target?.match(/\.\$(\w+)/)?.[1];
|
|
379
|
+
if (target && source && source !== 'eventContext') {
|
|
380
|
+
issues.push(tokenIssue('warning', index, line, target, `Dynamic dictionary key '$${source}' in '${target}' is never resolved - only '$eventContext.<path>' is, so the literal text '$${source}' becomes the key.`, diagnosticCodes.unresolvedDynamicKeySource));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return issues;
|
|
385
|
+
}
|