From 5810a7eb3b19ef9868db170c9214686bfc613eee Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Tue, 16 Nov 2021 03:00:45 +0100 Subject: 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. --- .../src/main/js/xtext/ContentAssistService.ts | 6 +- .../src/main/js/xtext/HighlightingService.ts | 12 +- .../src/main/js/xtext/OccurrencesService.ts | 31 ++- language-web/src/main/js/xtext/UpdateService.ts | 46 ++-- .../src/main/js/xtext/ValidationService.ts | 11 +- language-web/src/main/js/xtext/XtextClient.ts | 7 +- .../src/main/js/xtext/XtextWebSocketClient.ts | 67 +++-- language-web/src/main/js/xtext/xtextMessages.ts | 78 ++---- .../src/main/js/xtext/xtextServiceResults.ts | 284 ++++++--------------- 9 files changed, 201 insertions(+), 341 deletions(-) (limited to 'language-web/src/main/js') diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts index aa9a80b0..8b872e06 100644 --- a/language-web/src/main/js/xtext/ContentAssistService.ts +++ b/language-web/src/main/js/xtext/ContentAssistService.ts @@ -10,7 +10,7 @@ import escapeStringRegexp from 'escape-string-regexp'; import { implicitCompletion } from '../language/props'; import type { UpdateService } from './UpdateService'; import { getLogger } from '../utils/logger'; -import type { IContentAssistEntry } from './xtextServiceResults'; +import type { ContentAssistEntry } from './xtextServiceResults'; const PROPOSALS_LIMIT = 1000; @@ -67,8 +67,8 @@ function computeSpan(prefix: string, entryCount: number): RegExp { return new RegExp(`^${escapedPrefix}$`); } -function createCompletion(entry: IContentAssistEntry): Completion { - let boost; +function createCompletion(entry: ContentAssistEntry): Completion { + let boost: number; switch (entry.kind) { case 'KEYWORD': // Some hard-to-type operators should be on top. diff --git a/language-web/src/main/js/xtext/HighlightingService.ts b/language-web/src/main/js/xtext/HighlightingService.ts index fc3e9e53..dfbb4a19 100644 --- a/language-web/src/main/js/xtext/HighlightingService.ts +++ b/language-web/src/main/js/xtext/HighlightingService.ts @@ -1,10 +1,7 @@ import type { EditorStore } from '../editor/EditorStore'; import type { IHighlightRange } from '../editor/semanticHighlighting'; import type { UpdateService } from './UpdateService'; -import { getLogger } from '../utils/logger'; -import { isHighlightingResult } from './xtextServiceResults'; - -const log = getLogger('xtext.ValidationService'); +import { highlightingResult } from './xtextServiceResults'; export class HighlightingService { private readonly store: EditorStore; @@ -17,13 +14,10 @@ export class HighlightingService { } onPush(push: unknown): void { - if (!isHighlightingResult(push)) { - log.error('Invalid highlighting result', push); - return; - } + const { regions } = highlightingResult.parse(push); const allChanges = this.updateService.computeChangesSinceLastUpdate(); const ranges: IHighlightRange[] = []; - push.regions.forEach(({ offset, length, styleClasses }) => { + regions.forEach(({ offset, length, styleClasses }) => { if (styleClasses.length === 0) { return; } diff --git a/language-web/src/main/js/xtext/OccurrencesService.ts b/language-web/src/main/js/xtext/OccurrencesService.ts index d1dec9e9..bc865537 100644 --- a/language-web/src/main/js/xtext/OccurrencesService.ts +++ b/language-web/src/main/js/xtext/OccurrencesService.ts @@ -7,9 +7,9 @@ import { getLogger } from '../utils/logger'; import { Timer } from '../utils/Timer'; import { XtextWebSocketClient } from './XtextWebSocketClient'; import { - isOccurrencesResult, - isServiceConflictResult, - ITextRegion, + isConflictResult, + occurrencesResult, + TextRegion, } from './xtextServiceResults'; const FIND_OCCURRENCES_TIMEOUT_MS = 1000; @@ -20,7 +20,7 @@ const CLEAR_OCCURRENCES_TIMEOUT_MS = 10; const log = getLogger('xtext.OccurrencesService'); -function transformOccurrences(regions: ITextRegion[]): IOccurrence[] { +function transformOccurrences(regions: TextRegion[]): IOccurrence[] { const occurrences: IOccurrence[] = []; regions.forEach(({ offset, length }) => { if (length > 0) { @@ -87,21 +87,32 @@ export class OccurrencesService { caretOffset: this.store.state.selection.main.head, }); const allChanges = this.updateService.computeChangesSinceLastUpdate(); - if (!allChanges.empty - || (isServiceConflictResult(result) && result.conflict === 'canceled')) { + if (!allChanges.empty || isConflictResult(result, 'canceled')) { // Stale occurrences result, the user already made some changes. // We can safely ignore the occurrences and schedule a new find occurrences call. this.clearOccurrences(); this.findOccurrencesTimer.schedule(); return; } - if (!isOccurrencesResult(result) || result.stateId !== this.updateService.xtextStateId) { - log.error('Unexpected occurrences result', result); + const parsedOccurrencesResult = occurrencesResult.safeParse(result); + if (!parsedOccurrencesResult.success) { + log.error( + 'Unexpected occurences result', + result, + 'not an OccurrencesResult: ', + parsedOccurrencesResult.error, + ); this.clearOccurrences(); return; } - const write = transformOccurrences(result.writeRegions); - const read = transformOccurrences(result.readRegions); + const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data; + if (stateId !== this.updateService.xtextStateId) { + log.error('Unexpected state id, expected:', this.updateService.xtextStateId, 'got:', stateId); + this.clearOccurrences(); + return; + } + const write = transformOccurrences(writeRegions); + const read = transformOccurrences(readRegions); this.hasOccurrences = write.length > 0 || read.length > 0; log.debug('Found', write.length, 'write and', read.length, 'read occurrences'); this.store.updateOccurrences(write, read); diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts index 9b672e79..fa48c5ab 100644 --- a/language-web/src/main/js/xtext/UpdateService.ts +++ b/language-web/src/main/js/xtext/UpdateService.ts @@ -11,10 +11,10 @@ import { ConditionVariable } from '../utils/ConditionVariable'; import { getLogger } from '../utils/logger'; import { Timer } from '../utils/Timer'; import { - IContentAssistEntry, - isContentAssistResult, - isDocumentStateResult, - isInvalidStateIdConflictResult, + ContentAssistEntry, + contentAssistResult, + documentStateResult, + isConflictResult, } from './xtextServiceResults'; const UPDATE_TIMEOUT_MS = 500; @@ -116,11 +116,8 @@ export class UpdateService { serviceType: 'update', fullText: this.store.state.doc.sliceString(0), }); - if (isDocumentStateResult(result)) { - return [result.stateId, undefined]; - } - log.error('Unexpected full text update result:', result); - throw new Error('Full text update failed'); + const { stateId } = documentStateResult.parse(result); + return [stateId, undefined]; } /** @@ -146,14 +143,14 @@ export class UpdateService { requiredStateId: this.xtextStateId, ...delta, }); - if (isDocumentStateResult(result)) { - return [result.stateId, undefined]; + const parsedDocumentStateResult = documentStateResult.safeParse(result); + if (parsedDocumentStateResult.success) { + return [parsedDocumentStateResult.data.stateId, undefined]; } - if (isInvalidStateIdConflictResult(result)) { + if (isConflictResult(result, 'invalidStateId')) { return this.doFallbackToUpdateFullText(); } - log.error('Unexpected delta text update result:', result); - throw new Error('Delta text update failed'); + throw parsedDocumentStateResult.error; }); } @@ -171,7 +168,7 @@ export class UpdateService { async fetchContentAssist( params: Record, signal: IAbortSignal, - ): Promise { + ): Promise { await this.prepareForDeltaUpdate(); if (signal.aborted) { return []; @@ -185,18 +182,19 @@ export class UpdateService { requiredStateId: this.xtextStateId, ...delta, }); - if (isContentAssistResult(result)) { - return [result.stateId, result.entries]; + const parsedContentAssistResult = contentAssistResult.safeParse(result); + if (parsedContentAssistResult.success) { + const { stateId, entries: resultEntries } = parsedContentAssistResult.data; + return [stateId, resultEntries]; } - if (isInvalidStateIdConflictResult(result)) { + if (isConflictResult(result, 'invalidStateId')) { const [newStateId] = await this.doFallbackToUpdateFullText(); // We must finish this state update transaction to prepare for any push events // before querying for content assist, so we just return `null` and will query // the content assist service later. return [newStateId, null]; } - log.error('Unextpected content assist result with delta update', result); - throw new Error('Unexpexted content assist result with delta update'); + throw parsedContentAssistResult.error; }); if (entries !== null) { return entries; @@ -214,11 +212,11 @@ export class UpdateService { ...params, requiredStateId: expectedStateId, }); - if (isContentAssistResult(result) && result.stateId === expectedStateId) { - return result.entries; + const { stateId, entries } = contentAssistResult.parse(result); + if (stateId !== expectedStateId) { + throw new Error(`Unexpected state id, expected: ${expectedStateId} got: ${stateId}`); } - log.error('Unexpected content assist result', result); - throw new Error('Unexpected content assist result'); + return entries; } private computeDelta() { diff --git a/language-web/src/main/js/xtext/ValidationService.ts b/language-web/src/main/js/xtext/ValidationService.ts index 8e4934ac..c7f6ac7f 100644 --- a/language-web/src/main/js/xtext/ValidationService.ts +++ b/language-web/src/main/js/xtext/ValidationService.ts @@ -3,9 +3,7 @@ import type { Diagnostic } from '@codemirror/lint'; import type { EditorStore } from '../editor/EditorStore'; import type { UpdateService } from './UpdateService'; import { getLogger } from '../utils/logger'; -import { isValidationResult } from './xtextServiceResults'; - -const log = getLogger('xtext.ValidationService'); +import { validationResult } from './xtextServiceResults'; export class ValidationService { private readonly store: EditorStore; @@ -18,13 +16,10 @@ export class ValidationService { } onPush(push: unknown): void { - if (!isValidationResult(push)) { - log.error('Invalid validation result', push); - return; - } + const { issues } = validationResult.parse(push); const allChanges = this.updateService.computeChangesSinceLastUpdate(); const diagnostics: Diagnostic[] = []; - push.issues.forEach(({ + issues.forEach(({ offset, length, severity, diff --git a/language-web/src/main/js/xtext/XtextClient.ts b/language-web/src/main/js/xtext/XtextClient.ts index 28f3d0cc..3922b230 100644 --- a/language-web/src/main/js/xtext/XtextClient.ts +++ b/language-web/src/main/js/xtext/XtextClient.ts @@ -12,6 +12,7 @@ import { UpdateService } from './UpdateService'; import { getLogger } from '../utils/logger'; import { ValidationService } from './ValidationService'; import { XtextWebSocketClient } from './XtextWebSocketClient'; +import { XtextWebPushService } from './xtextMessages'; const log = getLogger('xtext.XtextClient'); @@ -52,7 +53,7 @@ export class XtextClient { this.occurrencesService.onTransaction(transaction); } - private onPush(resource: string, stateId: string, service: string, push: unknown) { + private onPush(resource: string, stateId: string, service: XtextWebPushService, push: unknown) { const { resourceName, xtextStateId } = this.updateService; if (resource !== resourceName) { log.error('Unknown resource name: expected:', resourceName, 'got:', resource); @@ -70,10 +71,6 @@ export class XtextClient { return; case 'validate': this.validationService.onPush(push); - return; - default: - log.error('Unknown push service:', service); - break; } } diff --git a/language-web/src/main/js/xtext/XtextWebSocketClient.ts b/language-web/src/main/js/xtext/XtextWebSocketClient.ts index 488e4b3b..2ce20a54 100644 --- a/language-web/src/main/js/xtext/XtextWebSocketClient.ts +++ b/language-web/src/main/js/xtext/XtextWebSocketClient.ts @@ -4,12 +4,13 @@ import { getLogger } from '../utils/logger'; import { PendingTask } from '../utils/PendingTask'; import { Timer } from '../utils/Timer'; import { - isErrorResponse, - isOkResponse, - isPushMessage, - IXtextWebRequest, + xtextWebErrorResponse, + XtextWebRequest, + xtextWebOkResponse, + xtextWebPushMessage, + XtextWebPushService, } from './xtextMessages'; -import { isPongResult } from './xtextServiceResults'; +import { pongResult } from './xtextServiceResults'; const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; @@ -32,7 +33,7 @@ export type ReconnectHandler = () => void; export type PushHandler = ( resourceId: string, stateId: string, - service: string, + service: XtextWebPushService, data: unknown, ) => void; @@ -192,11 +193,12 @@ export class XtextWebSocketClient { const ping = nanoid(); log.trace('Ping', ping); this.send({ ping }).then((result) => { - if (isPongResult(result) && result.pong === ping) { + const parsedPongResult = pongResult.safeParse(result); + if (parsedPongResult.success && parsedPongResult.data.pong === ping) { log.trace('Pong', ping); this.pingTimer.schedule(); } else { - log.error('Invalid pong'); + log.error('Invalid pong:', parsedPongResult, 'expected:', ping); this.forceReconnectOnError(); } }).catch((error) => { @@ -222,7 +224,7 @@ export class XtextWebSocketClient { const message = JSON.stringify({ id: messageId, request, - } as IXtextWebRequest); + } as XtextWebRequest); log.trace('Sending message', message); return new Promise((resolve, reject) => { const task = new PendingTask(resolve, reject, REQUEST_TIMEOUT_MS, () => { @@ -248,23 +250,42 @@ export class XtextWebSocketClient { this.forceReconnectOnError(); return; } - if (isOkResponse(message)) { - this.resolveRequest(message.id, message.response); - } else if (isErrorResponse(message)) { - this.rejectRequest(message.id, new Error(`${message.error} error: ${message.message}`)); - if (message.error === 'server') { - log.error('Reconnecting due to server error: ', message.message); + const okResponse = xtextWebOkResponse.safeParse(message); + if (okResponse.success) { + const { id, response } = okResponse.data; + this.resolveRequest(id, response); + return; + } + const errorResponse = xtextWebErrorResponse.safeParse(message); + if (errorResponse.success) { + const { id, error, message: errorMessage } = errorResponse.data; + this.rejectRequest(id, new Error(`${error} error: ${errorMessage}`)); + if (error === 'server') { + log.error('Reconnecting due to server error: ', errorMessage); this.forceReconnectOnError(); } - } else if (isPushMessage(message)) { - this.onPush( - message.resource, - message.stateId, - message.service, - message.push, - ); + return; + } + const pushMessage = xtextWebPushMessage.safeParse(message); + if (pushMessage.success) { + const { + resource, + stateId, + service, + push, + } = pushMessage.data; + this.onPush(resource, stateId, service, push); } else { - log.error('Unexpected websocket message', message); + log.error( + 'Unexpected websocket message:', + message, + 'not ok response because:', + okResponse.error, + 'not error response because:', + errorResponse.error, + 'not push message because:', + pushMessage.error, + ); this.forceReconnectOnError(); } } diff --git a/language-web/src/main/js/xtext/xtextMessages.ts b/language-web/src/main/js/xtext/xtextMessages.ts index 68737958..c4305fcf 100644 --- a/language-web/src/main/js/xtext/xtextMessages.ts +++ b/language-web/src/main/js/xtext/xtextMessages.ts @@ -1,62 +1,40 @@ -export interface IXtextWebRequest { - id: string; +import { z } from 'zod'; - request: unknown; -} +export const xtextWebRequest = z.object({ + id: z.string().nonempty(), + request: z.unknown(), +}); -export interface IXtextWebOkResponse { - id: string; +export type XtextWebRequest = z.infer; - response: unknown; -} +export const xtextWebOkResponse = z.object({ + id: z.string().nonempty(), + response: z.unknown(), +}); -export function isOkResponse(response: unknown): response is IXtextWebOkResponse { - const okResponse = response as IXtextWebOkResponse; - return typeof okResponse === 'object' - && typeof okResponse.id === 'string' - && typeof okResponse.response !== 'undefined'; -} +export type XtextWebOkResponse = z.infer; -export const VALID_XTEXT_WEB_ERROR_KINDS = ['request', 'server'] as const; +export const xtextWebErrorKind = z.enum(['request', 'server']); -export type XtextWebErrorKind = typeof VALID_XTEXT_WEB_ERROR_KINDS[number]; +export type XtextWebErrorKind = z.infer; -export function isXtextWebErrorKind(value: unknown): value is XtextWebErrorKind { - return typeof value === 'string' - && VALID_XTEXT_WEB_ERROR_KINDS.includes(value as XtextWebErrorKind); -} +export const xtextWebErrorResponse = z.object({ + id: z.string().nonempty(), + error: xtextWebErrorKind, + message: z.string(), +}); -export interface IXtextWebErrorResponse { - id: string; +export type XtextWebErrorResponse = z.infer; - error: XtextWebErrorKind; +export const xtextWebPushService = z.enum(['highlight', 'validate']); - message: string; -} +export type XtextWebPushService = z.infer; -export function isErrorResponse(response: unknown): response is IXtextWebErrorResponse { - const errorResponse = response as IXtextWebErrorResponse; - return typeof errorResponse === 'object' - && typeof errorResponse.id === 'string' - && isXtextWebErrorKind(errorResponse.error) - && typeof errorResponse.message === 'string'; -} +export const xtextWebPushMessage = z.object({ + resource: z.string().nonempty(), + stateId: z.string().nonempty(), + service: xtextWebPushService, + push: z.unknown(), +}); -export interface IXtextWebPushMessage { - resource: string; - - stateId: string; - - service: string; - - push: unknown; -} - -export function isPushMessage(response: unknown): response is IXtextWebPushMessage { - const pushMessage = response as IXtextWebPushMessage; - return typeof pushMessage === 'object' - && typeof pushMessage.resource === 'string' - && typeof pushMessage.stateId === 'string' - && typeof pushMessage.service === 'string' - && typeof pushMessage.push !== 'undefined'; -} +export type XtextWebPushMessage = z.infer; 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 @@ -export interface IPongResult { - pong: string; -} - -export function isPongResult(result: unknown): result is IPongResult { - const pongResult = result as IPongResult; - return typeof pongResult === 'object' - && typeof pongResult.pong === 'string'; -} - -export interface IDocumentStateResult { - stateId: string; -} - -export function isDocumentStateResult(result: unknown): result is IDocumentStateResult { - const documentStateResult = result as IDocumentStateResult; - return typeof documentStateResult === 'object' - && typeof documentStateResult.stateId === 'string'; -} - -export const VALID_CONFLICTS = ['invalidStateId', 'canceled'] as const; - -export type Conflict = typeof VALID_CONFLICTS[number]; - -export function isConflict(value: unknown): value is Conflict { - return typeof value === 'string' && VALID_CONFLICTS.includes(value as Conflict); -} - -export interface IServiceConflictResult { - conflict: Conflict; -} - -export function isServiceConflictResult(result: unknown): result is IServiceConflictResult { - const serviceConflictResult = result as IServiceConflictResult; - return typeof serviceConflictResult === 'object' - && isConflict(serviceConflictResult.conflict); -} - -export function isInvalidStateIdConflictResult(result: unknown): boolean { - return isServiceConflictResult(result) && result.conflict === 'invalidStateId'; -} - -export const VALID_SEVERITIES = ['error', 'warning', 'info', 'ignore'] as const; - -export type Severity = typeof VALID_SEVERITIES[number]; - -export function isSeverity(value: unknown): value is Severity { - return typeof value === 'string' && VALID_SEVERITIES.includes(value as Severity); -} - -export interface IIssue { - description: string; - - severity: Severity; - - line: number; - - column: number; - - offset: number; - - length: number; -} +import { z } from 'zod'; -export function isIssue(value: unknown): value is IIssue { - const issue = value as IIssue; - return typeof issue === 'object' - && typeof issue.description === 'string' - && isSeverity(issue.severity) - && typeof issue.line === 'number' - && typeof issue.column === 'number' - && typeof issue.offset === 'number' - && typeof issue.length === 'number'; -} +export const pongResult = z.object({ + pong: z.string().nonempty(), +}); -export interface IValidationResult { - issues: IIssue[]; -} +export type PongResult = z.infer; -function isArrayOfType(value: unknown, check: (entry: unknown) => entry is T): value is T[] { - return Array.isArray(value) && (value as T[]).every(check); -} +export const documentStateResult = z.object({ + stateId: z.string().nonempty(), +}); -export function isValidationResult(result: unknown): result is IValidationResult { - const validationResult = result as IValidationResult; - return typeof validationResult === 'object' - && isArrayOfType(validationResult.issues, isIssue); -} - -export interface IReplaceRegion { - offset: number; +export type DocumentStateResult = z.infer; - length: number; +export const conflict = z.enum(['invalidStateId', 'canceled']); - text: string; -} +export type Conflict = z.infer; -export function isReplaceRegion(value: unknown): value is IReplaceRegion { - const replaceRegion = value as IReplaceRegion; - return typeof replaceRegion === 'object' - && typeof replaceRegion.offset === 'number' - && typeof replaceRegion.length === 'number' - && typeof replaceRegion.text === 'string'; -} +export const serviceConflictResult = z.object({ + conflict, +}); -export interface ITextRegion { - offset: number; +export type ServiceConflictResult = z.infer; - length: number; +export function isConflictResult(result: unknown, conflictType: Conflict): boolean { + const parsedConflictResult = serviceConflictResult.safeParse(result); + return parsedConflictResult.success && parsedConflictResult.data.conflict === conflictType; } -export function isTextRegion(value: unknown): value is ITextRegion { - const textRegion = value as ITextRegion; - return typeof textRegion === 'object' - && typeof textRegion.offset === 'number' - && typeof textRegion.length === 'number'; -} +export const severity = z.enum(['error', 'warning', 'info', 'ignore']); -export const VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS = [ - 'TEXT', - 'METHOD', - 'FUNCTION', - 'CONSTRUCTOR', - 'FIELD', - 'VARIABLE', - 'CLASS', - 'INTERFACE', - 'MODULE', - 'PROPERTY', - 'UNIT', - 'VALUE', - 'ENUM', - 'KEYWORD', - 'SNIPPET', - 'COLOR', - 'FILE', - 'REFERENCE', - 'UNKNOWN', -] as const; - -export type XtextContentAssistEntryKind = typeof VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS[number]; - -export function isXtextContentAssistEntryKind( - value: unknown, -): value is XtextContentAssistEntryKind { - return typeof value === 'string' - && VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS.includes(value as XtextContentAssistEntryKind); -} +export type Severity = z.infer; -export interface IContentAssistEntry { - prefix: string; +export const issue = z.object({ + description: z.string().nonempty(), + severity, + line: z.number().int(), + column: z.number().int().nonnegative(), + offset: z.number().int().nonnegative(), + length: z.number().int().nonnegative(), +}); - proposal: string; +export type Issue = z.infer; - label?: string; +export const validationResult = z.object({ + issues: issue.array(), +}); - description?: string; +export type ValidationResult = z.infer; - documentation?: string; +export const replaceRegion = z.object({ + offset: z.number().int().nonnegative(), + length: z.number().int().nonnegative(), + text: z.string(), +}); - escapePosition?: number; +export type ReplaceRegion = z.infer; - textReplacements: IReplaceRegion[]; +export const textRegion = z.object({ + offset: z.number().int().nonnegative(), + length: z.number().int().nonnegative(), +}); - editPositions: ITextRegion[]; +export type TextRegion = z.infer; - kind: XtextContentAssistEntryKind | string; -} +export const contentAssistEntry = z.object({ + prefix: z.string(), + proposal: z.string().nonempty(), + label: z.string().optional(), + description: z.string().nonempty().optional(), + documentation: z.string().nonempty().optional(), + escapePosition: z.number().int().nonnegative().optional(), + textReplacements: replaceRegion.array(), + editPositions: textRegion.array(), + kind: z.string().nonempty(), +}); -function isStringOrUndefined(value: unknown): value is string | undefined { - return typeof value === 'string' || typeof value === 'undefined'; -} +export type ContentAssistEntry = z.infer; -function isNumberOrUndefined(value: unknown): value is number | undefined { - return typeof value === 'number' || typeof value === 'undefined'; -} +export const contentAssistResult = documentStateResult.extend({ + entries: contentAssistEntry.array(), +}); -export function isContentAssistEntry(value: unknown): value is IContentAssistEntry { - const entry = value as IContentAssistEntry; - return typeof entry === 'object' - && typeof entry.prefix === 'string' - && typeof entry.proposal === 'string' - && isStringOrUndefined(entry.label) - && isStringOrUndefined(entry.description) - && isStringOrUndefined(entry.documentation) - && isNumberOrUndefined(entry.escapePosition) - && isArrayOfType(entry.textReplacements, isReplaceRegion) - && isArrayOfType(entry.editPositions, isTextRegion) - && typeof entry.kind === 'string'; -} +export type ContentAssistResult = z.infer; -export interface IContentAssistResult extends IDocumentStateResult { - entries: IContentAssistEntry[]; -} - -export function isContentAssistResult(result: unknown): result is IContentAssistResult { - const contentAssistResult = result as IContentAssistResult; - return isDocumentStateResult(result) - && isArrayOfType(contentAssistResult.entries, isContentAssistEntry); -} +export const highlightingRegion = z.object({ + offset: z.number().int().nonnegative(), + length: z.number().int().nonnegative(), + styleClasses: z.string().nonempty().array(), +}); -export interface IHighlightingRegion { - offset: number; +export type HighlightingRegion = z.infer; - length: number; +export const highlightingResult = z.object({ + regions: highlightingRegion.array(), +}); - styleClasses: string[]; -} +export type HighlightingResult = z.infer; -export function isHighlightingRegion(value: unknown): value is IHighlightingRegion { - const region = value as IHighlightingRegion; - return typeof region === 'object' - && typeof region.offset === 'number' - && typeof region.length === 'number' - && isArrayOfType(region.styleClasses, (s): s is string => typeof s === 'string'); -} - -export interface IHighlightingResult { - regions: IHighlightingRegion[]; -} +export const occurrencesResult = documentStateResult.extend({ + writeRegions: textRegion.array(), + readRegions: textRegion.array(), +}); -export function isHighlightingResult(result: unknown): result is IHighlightingResult { - const highlightingResult = result as IHighlightingResult; - return typeof highlightingResult === 'object' - && isArrayOfType(highlightingResult.regions, isHighlightingRegion); -} - -export interface IOccurrencesResult extends IDocumentStateResult { - writeRegions: ITextRegion[]; - - readRegions: ITextRegion[]; -} - -export function isOccurrencesResult(result: unknown): result is IOccurrencesResult { - const occurrencesResult = result as IOccurrencesResult; - return isDocumentStateResult(occurrencesResult) - && isArrayOfType(occurrencesResult.writeRegions, isTextRegion) - && isArrayOfType(occurrencesResult.readRegions, isTextRegion); -} +export type OccurrencesResult = z.infer; -- cgit v1.2.3-54-g00ecf