Skip to content

Commit de55bce

Browse files
committed
feat(ensnode-sdk): define a generic Result type
1 parent 63617fa commit de55bce

File tree

5 files changed

+359
-0
lines changed

5 files changed

+359
-0
lines changed

packages/ensnode-sdk/src/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from "./interpretation";
1010
export * from "./labelhash";
1111
export * from "./null-bytes";
1212
export * from "./numbers";
13+
export * from "./result";
1314
export * from "./root-registry";
1415
export * from "./serialize";
1516
export * from "./serialized-types";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./types";
2+
export * from "./utils";
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* This module defines a standardized way to represent the outcome of operations,
3+
* encapsulating both successful results and error results.
4+
*/
5+
6+
/**
7+
* Possible Result Codes.
8+
*/
9+
export const ResultCodes = {
10+
Ok: "ok",
11+
Error: "error",
12+
} as const;
13+
14+
export type ResultCode = (typeof ResultCodes)[keyof typeof ResultCodes];
15+
16+
/**
17+
* Value type useful for `ResultOk` type.
18+
*/
19+
export interface ResultOkValue<ValueCodeType> {
20+
valueCode: ValueCodeType;
21+
}
22+
23+
/**
24+
* Result Ok returned by a successful operation call.
25+
*/
26+
export interface ResultOk<ValueType> {
27+
resultCode: typeof ResultCodes.Ok;
28+
value: ValueType;
29+
}
30+
31+
/**
32+
* Value type useful for `ResultError` type.
33+
*/
34+
export interface ResultErrorValue<ErrorCodeType> {
35+
errorCode: ErrorCodeType;
36+
}
37+
38+
/**
39+
* Result Error returned by a failed operation call.
40+
*/
41+
export interface ResultError<ErrorType> {
42+
resultCode: typeof ResultCodes.Error;
43+
value: ErrorType;
44+
}
45+
46+
/**
47+
* Result returned by an operation.
48+
*
49+
* Guarantees:
50+
* - `resultCode` indicates if operation succeeded or failed.
51+
* - `value` describes the outcome of the operation, for example
52+
* - {@link ResultOkValue} for successful operation call.
53+
* - {@link ResultErrorValue} for failed operation call.
54+
*/
55+
export type Result<OkType, ErrorType> = ResultOk<OkType> | ResultError<ErrorType>;
56+
57+
/**
58+
* Type for marking error as a transient one.
59+
*
60+
* It's useful for downstream consumers to know, so they can attempt fetching
61+
* the result once again.
62+
*/
63+
export type ErrorTransient<ErrorType> = ErrorType & { transient: true };
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import type { ErrorTransient, Result, ResultError, ResultOk } from "./types";
4+
import {
5+
errorTransient,
6+
isErrorTransient,
7+
isResult,
8+
isResultError,
9+
isResultOk,
10+
resultError,
11+
resultOk,
12+
} from "./utils";
13+
14+
describe("Result type", () => {
15+
describe("Developer Experience", () => {
16+
// Correct value codes that the test operation can return.
17+
const TestOpValueCodes = { Found: "FOUND", NotFound: "NOT_FOUND" } as const;
18+
19+
// Example of result ok: no records were found for the given request
20+
interface TestOpResultOkNotFound {
21+
valueCode: typeof TestOpValueCodes.NotFound;
22+
}
23+
24+
// Example of result ok: some records were found for the given request
25+
interface TestOpResultFound {
26+
valueCode: typeof TestOpValueCodes.Found;
27+
records: string[];
28+
}
29+
30+
// Union type collecting all ResultOk subtypes
31+
type TestOpResultOk = TestOpResultOkNotFound | TestOpResultFound;
32+
33+
// Error codes that the test operation can return.
34+
const TestOpErrorCodes = {
35+
InvalidRequest: "INVALID_REQUEST",
36+
TransientIssue: "TRANSIENT_ISSUE",
37+
} as const;
38+
39+
// Example of result error: invalid request
40+
interface TestOpResultErrorInvalidRequest {
41+
errorCode: typeof TestOpErrorCodes.InvalidRequest;
42+
message: string;
43+
}
44+
45+
// Example of result error: transient issue, simulates ie. indexing status not ready
46+
type TestOpResultErrorTransientIssue = ErrorTransient<{
47+
errorCode: typeof TestOpErrorCodes.TransientIssue;
48+
}>;
49+
50+
// Union type collecting all ResultError subtypes
51+
type TestOpError = TestOpResultErrorInvalidRequest | TestOpResultErrorTransientIssue;
52+
53+
// Result type for test operation
54+
type TestOpResult = Result<TestOpResultOk, TestOpError>;
55+
56+
interface TestOperationParams {
57+
name: string;
58+
simulate?: {
59+
transientIssue?: boolean;
60+
};
61+
}
62+
63+
// An example of operation returning a Result object
64+
function testOperation(params: TestOperationParams): TestOpResult {
65+
// Check if need to simulate transient server issue
66+
if (params.simulate?.transientIssue) {
67+
return resultError(
68+
errorTransient({
69+
errorCode: TestOpErrorCodes.TransientIssue,
70+
}),
71+
) satisfies ResultError<TestOpResultErrorTransientIssue>;
72+
}
73+
74+
// Check if request is valid
75+
if (params.name.endsWith(".eth") === false) {
76+
return resultError({
77+
errorCode: TestOpErrorCodes.InvalidRequest,
78+
message: `Invalid request, 'name' must end with '.eth'. Provided name: '${params.name}'.`,
79+
}) satisfies ResultError<TestOpResultErrorInvalidRequest>;
80+
}
81+
82+
// Check if requested name has any records indexed
83+
if (params.name !== "vitalik.eth") {
84+
return resultOk({
85+
valueCode: TestOpValueCodes.NotFound,
86+
}) satisfies ResultOk<TestOpResultOkNotFound>;
87+
}
88+
89+
// Return records found for the requested name
90+
return resultOk({
91+
valueCode: TestOpValueCodes.Found,
92+
records: ["a", "b", "c"],
93+
}) satisfies ResultOk<TestOpResultFound>;
94+
}
95+
96+
// Example ResultOk values
97+
const testOperationResultOkFound = testOperation({
98+
name: "vitalik.eth",
99+
});
100+
101+
const testOperationResultOkNotFound = testOperation({
102+
name: "test.eth",
103+
});
104+
105+
// Example ResultError values
106+
const testOperationResultErrorTransientIssue = testOperation({
107+
name: "vitalik.eth",
108+
simulate: {
109+
transientIssue: true,
110+
},
111+
});
112+
113+
const testOperationResultErrorInvalidRequest = testOperation({
114+
name: "test.xyz",
115+
});
116+
117+
// Example values that are instances of Result type
118+
const results = [
119+
testOperationResultOkFound,
120+
testOperationResultOkNotFound,
121+
testOperationResultErrorTransientIssue,
122+
testOperationResultErrorInvalidRequest,
123+
];
124+
// Example values that are not instances of Result type
125+
const notResults = [null, undefined, 42, "invalid", {}, { resultCode: "unknown" }];
126+
127+
describe("Type Guards", () => {
128+
it("should identify Result types correctly", () => {
129+
for (const maybeResult of results) {
130+
expect(isResult(maybeResult)).toBe(true);
131+
}
132+
133+
for (const maybeResult of notResults) {
134+
expect(isResult(maybeResult)).toBe(false);
135+
}
136+
});
137+
138+
it("should identify ResultOk types correctly", () => {
139+
expect(isResultOk(testOperationResultOkFound)).toBe(true);
140+
expect(isResultOk(testOperationResultOkNotFound)).toBe(true);
141+
expect(isResultOk(testOperationResultErrorTransientIssue)).toBe(false);
142+
expect(isResultOk(testOperationResultErrorInvalidRequest)).toBe(false);
143+
144+
for (const resultOkExample of results.filter((result) => isResultOk(result))) {
145+
const { value } = resultOkExample;
146+
147+
switch (value.valueCode) {
148+
case TestOpValueCodes.Found:
149+
expect(value).toStrictEqual({
150+
valueCode: TestOpValueCodes.Found,
151+
records: ["a", "b", "c"],
152+
} satisfies TestOpResultFound);
153+
break;
154+
155+
case TestOpValueCodes.NotFound:
156+
expect(value).toStrictEqual({
157+
valueCode: TestOpValueCodes.NotFound,
158+
} satisfies TestOpResultOkNotFound);
159+
break;
160+
}
161+
}
162+
});
163+
164+
it("should identify ResultError types correctly", () => {
165+
expect(isResultError(testOperationResultOkFound)).toBe(false);
166+
expect(isResultError(testOperationResultOkNotFound)).toBe(false);
167+
expect(isResultError(testOperationResultErrorTransientIssue)).toBe(true);
168+
expect(isResultError(testOperationResultErrorInvalidRequest)).toBe(true);
169+
170+
for (const resultErrorExample of results.filter((result) => isResultError(result))) {
171+
const { value } = resultErrorExample;
172+
173+
switch (value.errorCode) {
174+
case TestOpErrorCodes.InvalidRequest:
175+
expect(value).toStrictEqual({
176+
errorCode: TestOpErrorCodes.InvalidRequest,
177+
message: "Invalid request",
178+
} satisfies TestOpResultErrorInvalidRequest);
179+
break;
180+
181+
case TestOpErrorCodes.TransientIssue:
182+
expect(value).toMatchObject(
183+
errorTransient({
184+
errorCode: TestOpErrorCodes.TransientIssue,
185+
}) satisfies TestOpResultErrorTransientIssue,
186+
);
187+
break;
188+
}
189+
}
190+
});
191+
192+
it("should distinguish transient errors correctly", () => {
193+
expect(isErrorTransient(testOperationResultErrorTransientIssue.value)).toBe(true);
194+
expect(isErrorTransient(testOperationResultErrorInvalidRequest.value)).toBe(false);
195+
});
196+
});
197+
});
198+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* This file defines utilities for working with the Result generic type.
3+
* Functionalities should be use to enhance developer experience while
4+
* interacting with ENSNode APIs.
5+
*/
6+
7+
import {
8+
type ErrorTransient,
9+
type Result,
10+
ResultCodes,
11+
type ResultError,
12+
type ResultErrorValue,
13+
type ResultOk,
14+
type ResultOkValue,
15+
} from "./types";
16+
17+
/**
18+
* Build a Result Ok from provided `data`.
19+
*
20+
* Requires `data` to include the `valueCode` property
21+
* It enables the consumer of a Result object to identify `data` the result hold.
22+
*/
23+
export function resultOk<const OkValueType, OkType extends ResultOkValue<OkValueType>>(
24+
value: OkType,
25+
): ResultOk<OkType> {
26+
return {
27+
resultCode: ResultCodes.Ok,
28+
value,
29+
};
30+
}
31+
32+
/**
33+
* Is a result an instance of ResultOk?
34+
*/
35+
export function isResultOk<DataType, ErrorType>(
36+
result: Pick<Result<DataType, ErrorType>, "resultCode">,
37+
): result is ResultOk<DataType> {
38+
return result.resultCode === ResultCodes.Ok;
39+
}
40+
41+
/**
42+
* Build a Result Error from provided `error`.
43+
*
44+
* Requires `error` to include the `errorCode` property
45+
* It enables the consumer of a Result object to identify `error` the result hold.
46+
*/
47+
export function resultError<
48+
const ErrorValueType,
49+
ErrorType extends ResultErrorValue<ErrorValueType>,
50+
>(value: ErrorType): ResultError<ErrorType> {
51+
return {
52+
resultCode: ResultCodes.Error,
53+
value,
54+
};
55+
}
56+
57+
/**
58+
* Is a result error?
59+
*/
60+
export function isResultError<DataType, ErrorType>(
61+
result: Pick<Result<DataType, ErrorType>, "resultCode">,
62+
): result is ResultError<ErrorType> {
63+
return result.resultCode === ResultCodes.Error;
64+
}
65+
66+
/**
67+
* Is value an instance of a result type?
68+
*/
69+
export function isResult(value: unknown): value is Result<unknown, unknown> {
70+
return (
71+
typeof value === "object" &&
72+
value !== null &&
73+
"resultCode" in value &&
74+
(value.resultCode === ResultCodes.Ok || value.resultCode === ResultCodes.Error)
75+
);
76+
}
77+
78+
/**
79+
* Build a new instance of `error` and mark it as transient.
80+
*
81+
* This "mark" informs downstream consumer about the transient nature of
82+
* the error.
83+
*/
84+
export function errorTransient<ErrorType>(error: ErrorType): ErrorTransient<ErrorType> {
85+
return { ...error, transient: true };
86+
}
87+
88+
/**
89+
* Is error a transient one?
90+
*/
91+
export function isErrorTransient<ErrorType>(error: ErrorType): error is ErrorTransient<ErrorType> {
92+
return (
93+
typeof error === "object" && error !== null && "transient" in error && error.transient === true
94+
);
95+
}

0 commit comments

Comments
 (0)