@distilled.cloud/core 0.0.0-john

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/src/errors.ts ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Common error types shared across SDKs.
3
+ *
4
+ * Each SDK defines its own provider-specific errors (e.g., Unauthorized, NotFound)
5
+ * using the category system. This module provides base error types and utilities
6
+ * that are used across all SDKs.
7
+ */
8
+ import * as Schema from "effect/Schema";
9
+ import * as Category from "./category.ts";
10
+
11
+ // ============================================================================
12
+ // Common HTTP Status Error Classes
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Unauthorized - Authentication failure (401).
17
+ */
18
+ export class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()(
19
+ "Unauthorized",
20
+ { message: Schema.String },
21
+ ).pipe(Category.withAuthError) {}
22
+
23
+ /**
24
+ * Forbidden - Access denied (403).
25
+ */
26
+ export class Forbidden extends Schema.TaggedErrorClass<Forbidden>()(
27
+ "Forbidden",
28
+ { message: Schema.String },
29
+ ).pipe(Category.withAuthError) {}
30
+
31
+ /**
32
+ * NotFound - Resource not found (404).
33
+ */
34
+ export class NotFound extends Schema.TaggedErrorClass<NotFound>()("NotFound", {
35
+ message: Schema.String,
36
+ }).pipe(Category.withNotFoundError) {}
37
+
38
+ /**
39
+ * BadRequest - Invalid request (400).
40
+ */
41
+ export class BadRequest extends Schema.TaggedErrorClass<BadRequest>()(
42
+ "BadRequest",
43
+ { message: Schema.String },
44
+ ).pipe(Category.withBadRequestError) {}
45
+
46
+ /**
47
+ * Conflict - Resource conflict (409).
48
+ */
49
+ export class Conflict extends Schema.TaggedErrorClass<Conflict>()("Conflict", {
50
+ message: Schema.String,
51
+ }).pipe(Category.withConflictError) {}
52
+
53
+ /**
54
+ * UnprocessableEntity - Validation error (422).
55
+ */
56
+ export class UnprocessableEntity extends Schema.TaggedErrorClass<UnprocessableEntity>()(
57
+ "UnprocessableEntity",
58
+ { message: Schema.String },
59
+ ).pipe(Category.withBadRequestError) {}
60
+
61
+ /**
62
+ * TooManyRequests - Rate limited (429).
63
+ */
64
+ export class TooManyRequests extends Schema.TaggedErrorClass<TooManyRequests>()(
65
+ "TooManyRequests",
66
+ { message: Schema.String },
67
+ ).pipe(
68
+ Category.withThrottlingError,
69
+ Category.withRetryable({ throttling: true }),
70
+ ) {}
71
+
72
+ /**
73
+ * Locked - Resource locked (423).
74
+ */
75
+ export class Locked extends Schema.TaggedErrorClass<Locked>()("Locked", {
76
+ message: Schema.String,
77
+ }).pipe(Category.withLockedError, Category.withRetryable()) {}
78
+
79
+ /**
80
+ * InternalServerError - Server error (500).
81
+ */
82
+ export class InternalServerError extends Schema.TaggedErrorClass<InternalServerError>()(
83
+ "InternalServerError",
84
+ { message: Schema.String },
85
+ ).pipe(Category.withServerError, Category.withRetryable()) {}
86
+
87
+ /**
88
+ * BadGateway - Bad gateway (502).
89
+ */
90
+ export class BadGateway extends Schema.TaggedErrorClass<BadGateway>()(
91
+ "BadGateway",
92
+ { message: Schema.String },
93
+ ).pipe(Category.withServerError, Category.withRetryable()) {}
94
+
95
+ /**
96
+ * ServiceUnavailable - Service unavailable (503).
97
+ */
98
+ export class ServiceUnavailable extends Schema.TaggedErrorClass<ServiceUnavailable>()(
99
+ "ServiceUnavailable",
100
+ { message: Schema.String },
101
+ ).pipe(Category.withServerError, Category.withRetryable()) {}
102
+
103
+ /**
104
+ * GatewayTimeout - Gateway timeout (504).
105
+ */
106
+ export class GatewayTimeout extends Schema.TaggedErrorClass<GatewayTimeout>()(
107
+ "GatewayTimeout",
108
+ { message: Schema.String },
109
+ ).pipe(Category.withServerError, Category.withRetryable()) {}
110
+
111
+ /**
112
+ * Configuration error - missing or invalid configuration.
113
+ */
114
+ export class ConfigError extends Schema.TaggedErrorClass<ConfigError>()(
115
+ "ConfigError",
116
+ { message: Schema.String },
117
+ ).pipe(Category.withConfigurationError) {}
118
+
119
+ // ============================================================================
120
+ // Error Maps
121
+ // ============================================================================
122
+
123
+ /**
124
+ * Mapping from HTTP status codes to common error classes.
125
+ */
126
+ export const HTTP_STATUS_MAP = {
127
+ 400: BadRequest,
128
+ 401: Unauthorized,
129
+ 403: Forbidden,
130
+ 404: NotFound,
131
+ 409: Conflict,
132
+ 422: UnprocessableEntity,
133
+ 423: Locked,
134
+ 429: TooManyRequests,
135
+ 500: InternalServerError,
136
+ 502: BadGateway,
137
+ 503: ServiceUnavailable,
138
+ 504: GatewayTimeout,
139
+ } as const;
140
+
141
+ /**
142
+ * HTTP status codes that are considered "default" errors (always present).
143
+ * These are excluded from per-operation error types since they're handled globally.
144
+ */
145
+ export const DEFAULT_ERROR_STATUSES = new Set([401, 429, 500, 502, 503, 504]);
146
+
147
+ /**
148
+ * All common API error classes.
149
+ */
150
+ export const API_ERRORS = [
151
+ Unauthorized,
152
+ Forbidden,
153
+ NotFound,
154
+ BadRequest,
155
+ Conflict,
156
+ UnprocessableEntity,
157
+ TooManyRequests,
158
+ Locked,
159
+ InternalServerError,
160
+ BadGateway,
161
+ ServiceUnavailable,
162
+ GatewayTimeout,
163
+ ] as const;
164
+
165
+ /**
166
+ * Default errors that apply to ALL operations.
167
+ * These are infrastructure-level errors that can occur regardless of the operation.
168
+ */
169
+ export const DEFAULT_ERRORS = [
170
+ Unauthorized,
171
+ TooManyRequests,
172
+ InternalServerError,
173
+ BadGateway,
174
+ ServiceUnavailable,
175
+ GatewayTimeout,
176
+ ] as const;
177
+
178
+ export type DefaultErrors = InstanceType<(typeof DEFAULT_ERRORS)[number]>;
@@ -0,0 +1,261 @@
1
+ /**
2
+ * JSON Patch (RFC 6902) Implementation
3
+ *
4
+ * Provides a unified spec patching system for all SDKs.
5
+ * Patches are applied to OpenAPI/Discovery/Smithy specs before code generation
6
+ * to add error types, fix nullable fields, mark sensitive data, etc.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { applyAllPatches } from "@distilled.cloud/core/json-patch";
11
+ *
12
+ * const spec = JSON.parse(fs.readFileSync("openapi.json", "utf-8"));
13
+ * const { applied, errors } = applyAllPatches(spec, "./patches");
14
+ * ```
15
+ */
16
+ import * as fs from "fs";
17
+ import * as path from "path";
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ export interface JsonPatchOperation {
24
+ op: "add" | "remove" | "replace" | "move" | "copy" | "test";
25
+ path: string;
26
+ value?: unknown;
27
+ from?: string;
28
+ }
29
+
30
+ export type JsonPatch = JsonPatchOperation[];
31
+
32
+ export interface PatchFile {
33
+ description: string;
34
+ patches: JsonPatch;
35
+ }
36
+
37
+ // ============================================================================
38
+ // JSON Pointer (RFC 6901)
39
+ // ============================================================================
40
+
41
+ /**
42
+ * Parse a JSON Pointer (RFC 6901) path into segments.
43
+ */
44
+ export function parseJsonPointer(pointer: string): string[] {
45
+ if (pointer === "") return [];
46
+ if (!pointer.startsWith("/")) {
47
+ throw new Error(`Invalid JSON Pointer: ${pointer}`);
48
+ }
49
+ return pointer
50
+ .slice(1)
51
+ .split("/")
52
+ .map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
53
+ }
54
+
55
+ /**
56
+ * Get a value at a JSON Pointer path.
57
+ */
58
+ export function getValueAtPath(obj: unknown, pointer: string): unknown {
59
+ const segments = parseJsonPointer(pointer);
60
+ let current: unknown = obj;
61
+
62
+ for (const segment of segments) {
63
+ if (current === null || typeof current !== "object") {
64
+ throw new Error(`Cannot traverse path ${pointer}: not an object`);
65
+ }
66
+ if (Array.isArray(current)) {
67
+ const index = segment === "-" ? current.length : parseInt(segment, 10);
68
+ current = current[index];
69
+ } else {
70
+ current = (current as Record<string, unknown>)[segment];
71
+ }
72
+ }
73
+
74
+ return current;
75
+ }
76
+
77
+ /**
78
+ * Set a value at a JSON Pointer path.
79
+ */
80
+ export function setValueAtPath(
81
+ obj: unknown,
82
+ pointer: string,
83
+ value: unknown,
84
+ ): void {
85
+ const segments = parseJsonPointer(pointer);
86
+ if (segments.length === 0) {
87
+ throw new Error("Cannot set value at root path");
88
+ }
89
+
90
+ let current: unknown = obj;
91
+
92
+ for (let i = 0; i < segments.length - 1; i++) {
93
+ const segment = segments[i]!;
94
+ if (current === null || typeof current !== "object") {
95
+ throw new Error(`Cannot traverse path ${pointer}: not an object`);
96
+ }
97
+ if (Array.isArray(current)) {
98
+ const index = parseInt(segment, 10);
99
+ current = current[index];
100
+ } else {
101
+ current = (current as Record<string, unknown>)[segment];
102
+ }
103
+ }
104
+
105
+ const lastSegment = segments[segments.length - 1]!;
106
+ if (current === null || typeof current !== "object") {
107
+ throw new Error(
108
+ `Cannot set value at path ${pointer}: parent is not an object`,
109
+ );
110
+ }
111
+
112
+ if (Array.isArray(current)) {
113
+ const index =
114
+ lastSegment === "-" ? current.length : parseInt(lastSegment, 10);
115
+ if (lastSegment === "-") {
116
+ current.push(value);
117
+ } else {
118
+ current[index] = value;
119
+ }
120
+ } else {
121
+ (current as Record<string, unknown>)[lastSegment] = value;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Remove a value at a JSON Pointer path.
127
+ */
128
+ export function removeValueAtPath(obj: unknown, pointer: string): void {
129
+ const segments = parseJsonPointer(pointer);
130
+ if (segments.length === 0) {
131
+ throw new Error("Cannot remove root");
132
+ }
133
+
134
+ let current: unknown = obj;
135
+
136
+ for (let i = 0; i < segments.length - 1; i++) {
137
+ const segment = segments[i]!;
138
+ if (current === null || typeof current !== "object") {
139
+ throw new Error(`Cannot traverse path ${pointer}: not an object`);
140
+ }
141
+ if (Array.isArray(current)) {
142
+ current = current[parseInt(segment, 10)];
143
+ } else {
144
+ current = (current as Record<string, unknown>)[segment];
145
+ }
146
+ }
147
+
148
+ const lastSegment = segments[segments.length - 1]!;
149
+ if (current === null || typeof current !== "object") {
150
+ throw new Error(
151
+ `Cannot remove at path ${pointer}: parent is not an object`,
152
+ );
153
+ }
154
+
155
+ if (Array.isArray(current)) {
156
+ current.splice(parseInt(lastSegment, 10), 1);
157
+ } else {
158
+ delete (current as Record<string, unknown>)[lastSegment];
159
+ }
160
+ }
161
+
162
+ // ============================================================================
163
+ // Patch Operations
164
+ // ============================================================================
165
+
166
+ /**
167
+ * Apply a single JSON Patch operation.
168
+ */
169
+ export function applyOperation(
170
+ obj: unknown,
171
+ operation: JsonPatchOperation,
172
+ ): void {
173
+ switch (operation.op) {
174
+ case "add":
175
+ setValueAtPath(obj, operation.path, operation.value);
176
+ break;
177
+ case "remove":
178
+ removeValueAtPath(obj, operation.path);
179
+ break;
180
+ case "replace":
181
+ // For replace, the path must exist
182
+ getValueAtPath(obj, operation.path); // throws if doesn't exist
183
+ setValueAtPath(obj, operation.path, operation.value);
184
+ break;
185
+ case "move": {
186
+ if (!operation.from) throw new Error("move operation requires 'from'");
187
+ const moveValue = getValueAtPath(obj, operation.from);
188
+ removeValueAtPath(obj, operation.from);
189
+ setValueAtPath(obj, operation.path, moveValue);
190
+ break;
191
+ }
192
+ case "copy": {
193
+ if (!operation.from) throw new Error("copy operation requires 'from'");
194
+ const copyValue = getValueAtPath(obj, operation.from);
195
+ setValueAtPath(
196
+ obj,
197
+ operation.path,
198
+ JSON.parse(JSON.stringify(copyValue)),
199
+ );
200
+ break;
201
+ }
202
+ case "test": {
203
+ const testValue = getValueAtPath(obj, operation.path);
204
+ if (JSON.stringify(testValue) !== JSON.stringify(operation.value)) {
205
+ throw new Error(
206
+ `Test operation failed at ${operation.path}: expected ${JSON.stringify(operation.value)}, got ${JSON.stringify(testValue)}`,
207
+ );
208
+ }
209
+ break;
210
+ }
211
+ default:
212
+ throw new Error(`Unknown operation: ${(operation as { op: string }).op}`);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Apply a JSON Patch to an object (mutates in place).
218
+ */
219
+ export function applyPatch(obj: unknown, patch: JsonPatch): void {
220
+ for (const operation of patch) {
221
+ applyOperation(obj, operation);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Load and apply all patches from a directory.
227
+ * Finds all *.patch.json files and applies them.
228
+ */
229
+ export function applyAllPatches(
230
+ spec: unknown,
231
+ patchDir: string,
232
+ ): { applied: string[]; errors: string[] } {
233
+ const applied: string[] = [];
234
+ const errors: string[] = [];
235
+
236
+ if (!fs.existsSync(patchDir)) {
237
+ return { applied, errors };
238
+ }
239
+
240
+ // Find all .patch.json files
241
+ const files = fs
242
+ .readdirSync(patchDir)
243
+ .filter((f) => f.endsWith(".patch.json"))
244
+ .sort(); // Sort for deterministic application order
245
+
246
+ for (const file of files) {
247
+ const filePath = path.join(patchDir, file);
248
+ try {
249
+ const content = fs.readFileSync(filePath, "utf-8");
250
+ const patchFile: PatchFile = JSON.parse(content);
251
+ applyPatch(spec, patchFile.patches);
252
+ applied.push(`${file}: ${patchFile.description}`);
253
+ } catch (error) {
254
+ errors.push(
255
+ `${file}: ${error instanceof Error ? error.message : String(error)}`,
256
+ );
257
+ }
258
+ }
259
+
260
+ return { applied, errors };
261
+ }