diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-11-16 03:00:45 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-11-16 03:11:00 +0100 |
commit | 5810a7eb3b19ef9868db170c9214686bfc613eee (patch) | |
tree | da122997d4ad58f4104d0f84b06a00fe14e7ad02 /language-web/src/main/js/xtext/xtextServiceResults.ts | |
parent | feat(lang): basic formatting (diff) | |
download | refinery-5810a7eb3b19ef9868db170c9214686bfc613eee.tar.gz refinery-5810a7eb3b19ef9868db170c9214686bfc613eee.tar.zst refinery-5810a7eb3b19ef9868db170c9214686bfc613eee.zip |
chore(web): json validation with zod
Use the zod library instead of manually written type assertions for
validating json messages from the server. This makes it easier to add
and handle new messages.
Diffstat (limited to 'language-web/src/main/js/xtext/xtextServiceResults.ts')
-rw-r--r-- | language-web/src/main/js/xtext/xtextServiceResults.ts | 284 |
1 files changed, 75 insertions, 209 deletions
diff --git a/language-web/src/main/js/xtext/xtextServiceResults.ts b/language-web/src/main/js/xtext/xtextServiceResults.ts index b2de1e4a..b6867e2f 100644 --- a/language-web/src/main/js/xtext/xtextServiceResults.ts +++ b/language-web/src/main/js/xtext/xtextServiceResults.ts | |||
@@ -1,239 +1,105 @@ | |||
1 | export interface IPongResult { | 1 | import { z } from 'zod'; |
2 | pong: string; | ||
3 | } | ||
4 | |||
5 | export function isPongResult(result: unknown): result is IPongResult { | ||
6 | const pongResult = result as IPongResult; | ||
7 | return typeof pongResult === 'object' | ||
8 | && typeof pongResult.pong === 'string'; | ||
9 | } | ||
10 | |||
11 | export interface IDocumentStateResult { | ||
12 | stateId: string; | ||
13 | } | ||
14 | |||
15 | export function isDocumentStateResult(result: unknown): result is IDocumentStateResult { | ||
16 | const documentStateResult = result as IDocumentStateResult; | ||
17 | return typeof documentStateResult === 'object' | ||
18 | && typeof documentStateResult.stateId === 'string'; | ||
19 | } | ||
20 | |||
21 | export const VALID_CONFLICTS = ['invalidStateId', 'canceled'] as const; | ||
22 | |||
23 | export type Conflict = typeof VALID_CONFLICTS[number]; | ||
24 | |||
25 | export function isConflict(value: unknown): value is Conflict { | ||
26 | return typeof value === 'string' && VALID_CONFLICTS.includes(value as Conflict); | ||
27 | } | ||
28 | |||
29 | export interface IServiceConflictResult { | ||
30 | conflict: Conflict; | ||
31 | } | ||
32 | |||
33 | export function isServiceConflictResult(result: unknown): result is IServiceConflictResult { | ||
34 | const serviceConflictResult = result as IServiceConflictResult; | ||
35 | return typeof serviceConflictResult === 'object' | ||
36 | && isConflict(serviceConflictResult.conflict); | ||
37 | } | ||
38 | |||
39 | export function isInvalidStateIdConflictResult(result: unknown): boolean { | ||
40 | return isServiceConflictResult(result) && result.conflict === 'invalidStateId'; | ||
41 | } | ||
42 | |||
43 | export const VALID_SEVERITIES = ['error', 'warning', 'info', 'ignore'] as const; | ||
44 | |||
45 | export type Severity = typeof VALID_SEVERITIES[number]; | ||
46 | |||
47 | export function isSeverity(value: unknown): value is Severity { | ||
48 | return typeof value === 'string' && VALID_SEVERITIES.includes(value as Severity); | ||
49 | } | ||
50 | |||
51 | export interface IIssue { | ||
52 | description: string; | ||
53 | |||
54 | severity: Severity; | ||
55 | |||
56 | line: number; | ||
57 | |||
58 | column: number; | ||
59 | |||
60 | offset: number; | ||
61 | |||
62 | length: number; | ||
63 | } | ||
64 | 2 | ||
65 | export function isIssue(value: unknown): value is IIssue { | 3 | export const pongResult = z.object({ |
66 | const issue = value as IIssue; | 4 | pong: z.string().nonempty(), |
67 | return typeof issue === 'object' | 5 | }); |
68 | && typeof issue.description === 'string' | ||
69 | && isSeverity(issue.severity) | ||
70 | && typeof issue.line === 'number' | ||
71 | && typeof issue.column === 'number' | ||
72 | && typeof issue.offset === 'number' | ||
73 | && typeof issue.length === 'number'; | ||
74 | } | ||
75 | 6 | ||
76 | export interface IValidationResult { | 7 | export type PongResult = z.infer<typeof pongResult>; |
77 | issues: IIssue[]; | ||
78 | } | ||
79 | 8 | ||
80 | function isArrayOfType<T>(value: unknown, check: (entry: unknown) => entry is T): value is T[] { | 9 | export const documentStateResult = z.object({ |
81 | return Array.isArray(value) && (value as T[]).every(check); | 10 | stateId: z.string().nonempty(), |
82 | } | 11 | }); |
83 | 12 | ||
84 | export function isValidationResult(result: unknown): result is IValidationResult { | 13 | export type DocumentStateResult = z.infer<typeof documentStateResult>; |
85 | const validationResult = result as IValidationResult; | ||
86 | return typeof validationResult === 'object' | ||
87 | && isArrayOfType(validationResult.issues, isIssue); | ||
88 | } | ||
89 | |||
90 | export interface IReplaceRegion { | ||
91 | offset: number; | ||
92 | 14 | ||
93 | length: number; | 15 | export const conflict = z.enum(['invalidStateId', 'canceled']); |
94 | 16 | ||
95 | text: string; | 17 | export type Conflict = z.infer<typeof conflict>; |
96 | } | ||
97 | 18 | ||
98 | export function isReplaceRegion(value: unknown): value is IReplaceRegion { | 19 | export const serviceConflictResult = z.object({ |
99 | const replaceRegion = value as IReplaceRegion; | 20 | conflict, |
100 | return typeof replaceRegion === 'object' | 21 | }); |
101 | && typeof replaceRegion.offset === 'number' | ||
102 | && typeof replaceRegion.length === 'number' | ||
103 | && typeof replaceRegion.text === 'string'; | ||
104 | } | ||
105 | 22 | ||
106 | export interface ITextRegion { | 23 | export type ServiceConflictResult = z.infer<typeof serviceConflictResult>; |
107 | offset: number; | ||
108 | 24 | ||
109 | length: number; | 25 | export function isConflictResult(result: unknown, conflictType: Conflict): boolean { |
26 | const parsedConflictResult = serviceConflictResult.safeParse(result); | ||
27 | return parsedConflictResult.success && parsedConflictResult.data.conflict === conflictType; | ||
110 | } | 28 | } |
111 | 29 | ||
112 | export function isTextRegion(value: unknown): value is ITextRegion { | 30 | export const severity = z.enum(['error', 'warning', 'info', 'ignore']); |
113 | const textRegion = value as ITextRegion; | ||
114 | return typeof textRegion === 'object' | ||
115 | && typeof textRegion.offset === 'number' | ||
116 | && typeof textRegion.length === 'number'; | ||
117 | } | ||
118 | 31 | ||
119 | export const VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS = [ | 32 | export type Severity = z.infer<typeof severity>; |
120 | 'TEXT', | ||
121 | 'METHOD', | ||
122 | 'FUNCTION', | ||
123 | 'CONSTRUCTOR', | ||
124 | 'FIELD', | ||
125 | 'VARIABLE', | ||
126 | 'CLASS', | ||
127 | 'INTERFACE', | ||
128 | 'MODULE', | ||
129 | 'PROPERTY', | ||
130 | 'UNIT', | ||
131 | 'VALUE', | ||
132 | 'ENUM', | ||
133 | 'KEYWORD', | ||
134 | 'SNIPPET', | ||
135 | 'COLOR', | ||
136 | 'FILE', | ||
137 | 'REFERENCE', | ||
138 | 'UNKNOWN', | ||
139 | ] as const; | ||
140 | |||
141 | export type XtextContentAssistEntryKind = typeof VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS[number]; | ||
142 | |||
143 | export function isXtextContentAssistEntryKind( | ||
144 | value: unknown, | ||
145 | ): value is XtextContentAssistEntryKind { | ||
146 | return typeof value === 'string' | ||
147 | && VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS.includes(value as XtextContentAssistEntryKind); | ||
148 | } | ||
149 | 33 | ||
150 | export interface IContentAssistEntry { | 34 | export const issue = z.object({ |
151 | prefix: string; | 35 | description: z.string().nonempty(), |
36 | severity, | ||
37 | line: z.number().int(), | ||
38 | column: z.number().int().nonnegative(), | ||
39 | offset: z.number().int().nonnegative(), | ||
40 | length: z.number().int().nonnegative(), | ||
41 | }); | ||
152 | 42 | ||
153 | proposal: string; | 43 | export type Issue = z.infer<typeof issue>; |
154 | 44 | ||
155 | label?: string; | 45 | export const validationResult = z.object({ |
46 | issues: issue.array(), | ||
47 | }); | ||
156 | 48 | ||
157 | description?: string; | 49 | export type ValidationResult = z.infer<typeof validationResult>; |
158 | 50 | ||
159 | documentation?: string; | 51 | export const replaceRegion = z.object({ |
52 | offset: z.number().int().nonnegative(), | ||
53 | length: z.number().int().nonnegative(), | ||
54 | text: z.string(), | ||
55 | }); | ||
160 | 56 | ||
161 | escapePosition?: number; | 57 | export type ReplaceRegion = z.infer<typeof replaceRegion>; |
162 | 58 | ||
163 | textReplacements: IReplaceRegion[]; | 59 | export const textRegion = z.object({ |
60 | offset: z.number().int().nonnegative(), | ||
61 | length: z.number().int().nonnegative(), | ||
62 | }); | ||
164 | 63 | ||
165 | editPositions: ITextRegion[]; | 64 | export type TextRegion = z.infer<typeof textRegion>; |
166 | 65 | ||
167 | kind: XtextContentAssistEntryKind | string; | 66 | export const contentAssistEntry = z.object({ |
168 | } | 67 | prefix: z.string(), |
68 | proposal: z.string().nonempty(), | ||
69 | label: z.string().optional(), | ||
70 | description: z.string().nonempty().optional(), | ||
71 | documentation: z.string().nonempty().optional(), | ||
72 | escapePosition: z.number().int().nonnegative().optional(), | ||
73 | textReplacements: replaceRegion.array(), | ||
74 | editPositions: textRegion.array(), | ||
75 | kind: z.string().nonempty(), | ||
76 | }); | ||
169 | 77 | ||
170 | function isStringOrUndefined(value: unknown): value is string | undefined { | 78 | export type ContentAssistEntry = z.infer<typeof contentAssistEntry>; |
171 | return typeof value === 'string' || typeof value === 'undefined'; | ||
172 | } | ||
173 | 79 | ||
174 | function isNumberOrUndefined(value: unknown): value is number | undefined { | 80 | export const contentAssistResult = documentStateResult.extend({ |
175 | return typeof value === 'number' || typeof value === 'undefined'; | 81 | entries: contentAssistEntry.array(), |
176 | } | 82 | }); |
177 | 83 | ||
178 | export function isContentAssistEntry(value: unknown): value is IContentAssistEntry { | 84 | export type ContentAssistResult = z.infer<typeof contentAssistResult>; |
179 | const entry = value as IContentAssistEntry; | ||
180 | return typeof entry === 'object' | ||
181 | && typeof entry.prefix === 'string' | ||
182 | && typeof entry.proposal === 'string' | ||
183 | && isStringOrUndefined(entry.label) | ||
184 | && isStringOrUndefined(entry.description) | ||
185 | && isStringOrUndefined(entry.documentation) | ||
186 | && isNumberOrUndefined(entry.escapePosition) | ||
187 | && isArrayOfType(entry.textReplacements, isReplaceRegion) | ||
188 | && isArrayOfType(entry.editPositions, isTextRegion) | ||
189 | && typeof entry.kind === 'string'; | ||
190 | } | ||
191 | 85 | ||
192 | export interface IContentAssistResult extends IDocumentStateResult { | 86 | export const highlightingRegion = z.object({ |
193 | entries: IContentAssistEntry[]; | 87 | offset: z.number().int().nonnegative(), |
194 | } | 88 | length: z.number().int().nonnegative(), |
195 | 89 | styleClasses: z.string().nonempty().array(), | |
196 | export function isContentAssistResult(result: unknown): result is IContentAssistResult { | 90 | }); |
197 | const contentAssistResult = result as IContentAssistResult; | ||
198 | return isDocumentStateResult(result) | ||
199 | && isArrayOfType(contentAssistResult.entries, isContentAssistEntry); | ||
200 | } | ||
201 | 91 | ||
202 | export interface IHighlightingRegion { | 92 | export type HighlightingRegion = z.infer<typeof highlightingRegion>; |
203 | offset: number; | ||
204 | 93 | ||
205 | length: number; | 94 | export const highlightingResult = z.object({ |
95 | regions: highlightingRegion.array(), | ||
96 | }); | ||
206 | 97 | ||
207 | styleClasses: string[]; | 98 | export type HighlightingResult = z.infer<typeof highlightingResult>; |
208 | } | ||
209 | 99 | ||
210 | export function isHighlightingRegion(value: unknown): value is IHighlightingRegion { | 100 | export const occurrencesResult = documentStateResult.extend({ |
211 | const region = value as IHighlightingRegion; | 101 | writeRegions: textRegion.array(), |
212 | return typeof region === 'object' | 102 | readRegions: textRegion.array(), |
213 | && typeof region.offset === 'number' | 103 | }); |
214 | && typeof region.length === 'number' | ||
215 | && isArrayOfType(region.styleClasses, (s): s is string => typeof s === 'string'); | ||
216 | } | ||
217 | |||
218 | export interface IHighlightingResult { | ||
219 | regions: IHighlightingRegion[]; | ||
220 | } | ||
221 | 104 | ||
222 | export function isHighlightingResult(result: unknown): result is IHighlightingResult { | 105 | export type OccurrencesResult = z.infer<typeof occurrencesResult>; |
223 | const highlightingResult = result as IHighlightingResult; | ||
224 | return typeof highlightingResult === 'object' | ||
225 | && isArrayOfType(highlightingResult.regions, isHighlightingRegion); | ||
226 | } | ||
227 | |||
228 | export interface IOccurrencesResult extends IDocumentStateResult { | ||
229 | writeRegions: ITextRegion[]; | ||
230 | |||
231 | readRegions: ITextRegion[]; | ||
232 | } | ||
233 | |||
234 | export function isOccurrencesResult(result: unknown): result is IOccurrencesResult { | ||
235 | const occurrencesResult = result as IOccurrencesResult; | ||
236 | return isDocumentStateResult(occurrencesResult) | ||
237 | && isArrayOfType(occurrencesResult.writeRegions, isTextRegion) | ||
238 | && isArrayOfType(occurrencesResult.readRegions, isTextRegion); | ||
239 | } | ||