From b86d986cbdf4aa0537181dfe43d93f8371ee2cdf Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Sun, 15 Feb 2026 04:37:51 +0500 Subject: [PATCH] util: result: extend functionality with some common utils (#3750) --- extensions/copilot/src/util/common/result.ts | 79 +++++++- .../src/util/common/test/result.spec.ts | 191 ++++++++++++++++++ 2 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 extensions/copilot/src/util/common/test/result.spec.ts diff --git a/extensions/copilot/src/util/common/result.ts b/extensions/copilot/src/util/common/result.ts index 0da78e0b8d8..296cd934d7c 100644 --- a/extensions/copilot/src/util/common/result.ts +++ b/extensions/copilot/src/util/common/result.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export type Result = ResultOk | ResultError; +import * as errors from './errors'; + +export type Result = ResultOk | ResultError; export namespace Result { @@ -11,13 +13,29 @@ export namespace Result { return new ResultOk(value); } - export function error(value: K): ResultError { + export function error(value: E): ResultError { return new ResultError(value); } export function fromString(errorMessage: string): ResultError { return Result.error(new Error(errorMessage)); } + + export function tryWith(f: () => T): Result { + try { + return Result.ok(f()); + } catch (err) { + return Result.error(errors.fromUnknown(err)); + } + } + + export async function tryWithAsync(f: () => Promise): Promise> { + try { + return Result.ok(await f()); + } catch (err) { + return Result.error(errors.fromUnknown(err)); + } + } } /** @@ -27,14 +45,35 @@ export namespace Result { class ResultOk { constructor(readonly val: T) { } - map(f: (result: T) => K) { + map(f: (value: T) => U): ResultOk { return new ResultOk(f(this.val)); } - flatMap(f: (result: T) => Result) { + mapError(_f: (error: never) => E2): ResultOk { + return this; + } + + flatMap(f: (value: T) => Result): Result { return f(this.val); } + /** + * Returns the contained ok value. + * @throws if this is an error (which is impossible for `ResultOk`, + * but provided for use on the `Result` union type). + */ + unwrap(): T { + return this.val; + } + + /** + * Returns the contained ok value, or the provided default if this + * is an error. + */ + unwrapOr(_defaultValue: T): T { + return this.val; + } + isOk(): this is ResultOk { return true; } @@ -48,24 +87,46 @@ class ResultOk { * To instantiate a ResultOk, use `Result.ok(value)`. * To instantiate a ResultError, use `Result.error(value)`. */ -class ResultError { +class ResultError { constructor( - public readonly err: K, + public readonly err: E, ) { } - map(f: unknown) { + map(_f: (value: never) => U): ResultError { return this; } - flatMap(f: unknown) { + mapError(f: (error: E) => E2): ResultError { + return new ResultError(f(this.err)); + } + + flatMap(_f: (value: never) => Result): ResultError { return this; } + /** + * Always throws since this is an error result. + * @throws The contained error value (wrapped in Error if not already one). + */ + unwrap(): never { + if (this.err instanceof Error) { + throw this.err; + } + throw errors.fromUnknown(this.err); + } + + /** + * Returns the provided default value since this is an error. + */ + unwrapOr(defaultValue: T): T { + return defaultValue; + } + isOk(): this is ResultOk { return false; } - isError(): this is ResultError { + isError(): this is ResultError { return true; } } diff --git a/extensions/copilot/src/util/common/test/result.spec.ts b/extensions/copilot/src/util/common/test/result.spec.ts new file mode 100644 index 00000000000..3ab58245245 --- /dev/null +++ b/extensions/copilot/src/util/common/test/result.spec.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from 'vitest'; +import { Result } from '../result'; + +describe('Result', () => { + + describe('Result.ok', () => { + it('creates an ok result', () => { + const r = Result.ok(42); + expect(r.isOk()).toBe(true); + expect(r.isError()).toBe(false); + expect(r.val).toBe(42); + }); + }); + + describe('Result.error', () => { + it('creates an error result', () => { + const r = Result.error('bad'); + expect(r.isOk()).toBe(false); + expect(r.isError()).toBe(true); + expect(r.err).toBe('bad'); + }); + }); + + describe('Result.fromString', () => { + it('creates an error result with an Error instance', () => { + const r = Result.fromString('something failed'); + expect(r.isError()).toBe(true); + expect(r.err).toBeInstanceOf(Error); + expect(r.err.message).toBe('something failed'); + }); + }); + + describe('Result.tryWith', () => { + it('returns ok when the function succeeds', () => { + const r = Result.tryWith(() => 10 + 5); + expect(r.isOk()).toBe(true); + if (r.isOk()) { + expect(r.val).toBe(15); + } + }); + + it('returns error when the function throws', () => { + const r = Result.tryWith(() => { throw new Error('boom'); }); + expect(r.isError()).toBe(true); + if (r.isError()) { + expect(r.err.message).toBe('boom'); + } + }); + }); + + describe('Result.tryWithAsync', () => { + it('returns ok when the async function resolves', async () => { + const r = await Result.tryWithAsync(async () => 99); + expect(r.isOk()).toBe(true); + if (r.isOk()) { + expect(r.val).toBe(99); + } + }); + + it('returns error when the async function rejects', async () => { + const r = await Result.tryWithAsync(async () => { throw new Error('async boom'); }); + expect(r.isError()).toBe(true); + if (r.isError()) { + expect(r.err.message).toBe('async boom'); + } + }); + }); + + describe('map', () => { + it('transforms the value of an ok result', () => { + const r = Result.ok(3).map(x => x * 2); + expect(r.isOk()).toBe(true); + if (r.isOk()) { + expect(r.val).toBe(6); + } + }); + + it('is a no-op on an error result', () => { + const r: Result = Result.error('fail'); + const mapped = r.map(x => x * 2); + expect(mapped.isError()).toBe(true); + if (mapped.isError()) { + expect(mapped.err).toBe('fail'); + } + }); + }); + + describe('mapError', () => { + it('is a no-op on an ok result', () => { + const r: Result = Result.ok(5); + const mapped = r.mapError(e => new Error(e)); + expect(mapped.isOk()).toBe(true); + if (mapped.isOk()) { + expect(mapped.val).toBe(5); + } + }); + + it('transforms the error of an error result', () => { + const r = Result.error('oops'); + const mapped = r.mapError(e => ({ reason: e })); + expect(mapped.isError()).toBe(true); + if (mapped.isError()) { + expect(mapped.err).toEqual({ reason: 'oops' }); + } + }); + }); + + describe('flatMap', () => { + it('chains ok results', () => { + const r = Result.ok(10).flatMap(x => + x > 0 ? Result.ok(x.toString()) : Result.error('negative' as const) + ); + expect(r.isOk()).toBe(true); + if (r.isOk()) { + expect(r.val).toBe('10'); + } + }); + + it('chains to an error result', () => { + const r = Result.ok(-1).flatMap(x => + x > 0 ? Result.ok(x.toString()) : Result.error('negative' as const) + ); + expect(r.isError()).toBe(true); + if (r.isError()) { + expect(r.err).toBe('negative'); + } + }); + + it('is a no-op on an error result', () => { + const r: Result = Result.error('already bad'); + const chained = r.flatMap(x => Result.ok(x * 2)); + expect(chained.isError()).toBe(true); + if (chained.isError()) { + expect(chained.err).toBe('already bad'); + } + }); + }); + + describe('unwrap', () => { + it('returns the value for ok results', () => { + expect(Result.ok('hello').unwrap()).toBe('hello'); + }); + + it('throws for error results with an Error', () => { + const r: Result = Result.error(new Error('fail')); + expect(() => r.unwrap()).toThrow('fail'); + }); + + it('throws a wrapped error for non-Error error values', () => { + const r: Result = Result.error('string error'); + expect(() => r.unwrap()).toThrow('string error'); + }); + }); + + describe('unwrapOr', () => { + it('returns the value for ok results', () => { + const r: Result = Result.ok(42); + expect(r.unwrapOr(0)).toBe(42); + }); + + it('returns the default for error results', () => { + const r: Result = Result.error('nope'); + expect(r.unwrapOr(0)).toBe(0); + }); + }); + + describe('type narrowing', () => { + it('narrows to ok after isOk check', () => { + const r: Result = Result.ok(1); + if (r.isOk()) { + // TypeScript should know r.val exists here + const _v: number = r.val; + expect(_v).toBe(1); + } + }); + + it('narrows to error after isError check', () => { + const r: Result = Result.error('e'); + if (r.isError()) { + // TypeScript should know r.err exists here + const _e: string = r.err; + expect(_e).toBe('e'); + } + }); + }); +});