Skip to content

Commit da4c544

Browse files
committed
refactor: replace safeArkEnv utility with a safe option in the arkenv function
1 parent 101fbc9 commit da4c544

7 files changed

Lines changed: 77 additions & 66 deletions

File tree

apps/www/content/docs/arkenv/quickstart.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,14 @@ console.log(`Connecting to ${dbConfig.host}:${dbConfig.port}...`);
166166

167167
## Safe parsing (Non-throwing)
168168

169-
By default, calling `arkenv()` will throw an `ArkEnvError` if validation fails. If you prefer to handle validation issues without catching thrown exceptions, you can use the non-throwing `safeArkEnv()` function instead. It returns a result object containing either the successfully parsed data or an array of validation issues.
169+
By default, calling `arkenv()` will throw an `ArkEnvError` if validation fails. If you prefer to handle validation issues without catching thrown exceptions, you can pass the `{ safe: true }` option instead. It returns a result object containing either the successfully parsed data or an array of validation issues.
170170

171171
```ts title="env.ts" twoslash
172-
import { safeArkEnv } from 'arkenv';
172+
import { arkenv } from 'arkenv';
173173

174-
const result = safeArkEnv({
174+
const result = arkenv({
175175
PORT: "number = 3000",
176-
});
176+
}, { safe: true });
177177

178178
if (result.success) {
179179
console.log("Validated config:", result.data);

packages/arkenv/src/arkenv.ts

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ export type ArkEnvConfig = {
9898
* @default false
9999
*/
100100
emptyAsUndefined?: boolean;
101+
102+
/**
103+
* Whether to return a safe result object instead of throwing an error on validation failure.
104+
*
105+
* When enabled, the function returns an object with `{ success: true, data }` or `{ success: false, issues }`.
106+
*
107+
* @default false
108+
*/
109+
safe?: boolean;
101110
};
102111

103112
export type { SafeArkEnvResult };
@@ -123,49 +132,38 @@ export type ArkenvOutput<T extends SchemaShape, D> =
123132
*/
124133
export function arkenv<const T extends SchemaShape>(
125134
def: EnvSchema<T>,
126-
config?: ArkEnvConfig,
135+
config?: ArkEnvConfig & { safe?: false },
127136
): distill.Out<at.infer<T, $>>;
128137
export function arkenv<T extends CompiledEnvSchema>(
129138
def: T,
130-
config?: ArkEnvConfig,
139+
config?: ArkEnvConfig & { safe?: false },
131140
): InferType<T>;
132141
export function arkenv<
133142
const T extends SchemaShape,
134143
const D extends EnvSchema<T> | CompiledEnvSchema,
135-
>(def: D, config?: ArkEnvConfig): ArkenvOutput<T, D>;
136-
export function arkenv<
137-
const T extends SchemaShape,
138-
const D extends EnvSchema<T> | CompiledEnvSchema,
139-
>(def: D, config: ArkEnvConfig = {}): ArkenvOutput<T, D> {
140-
// biome-ignore lint/suspicious/noExplicitAny: parse handles both EnvSchema<T> and CompiledEnvSchema at runtime
141-
return parse(def as any, config);
142-
}
143-
144-
/**
145-
* Non-throwing utility to parse environment variables using ArkType or Standard Schema.
146-
* Returns a serializable result object containing either the validated data or error issues.
147-
*
148-
* @param def - The schema definition
149-
* @param config - The evaluation configuration
150-
* @returns The SafeArkEnvResult containing the data or the validation error object
151-
*/
152-
export function safeArkEnv<const T extends SchemaShape>(
144+
>(def: D, config?: ArkEnvConfig & { safe?: false }): ArkenvOutput<T, D>;
145+
export function arkenv<const T extends SchemaShape>(
153146
def: EnvSchema<T>,
154-
config?: ArkEnvConfig,
147+
config: ArkEnvConfig & { safe: true },
155148
): SafeArkEnvResult<distill.Out<at.infer<T, $>>>;
156-
export function safeArkEnv<T extends CompiledEnvSchema>(
149+
export function arkenv<T extends CompiledEnvSchema>(
157150
def: T,
158-
config?: ArkEnvConfig,
151+
config: ArkEnvConfig & { safe: true },
159152
): SafeArkEnvResult<InferType<T>>;
160-
export function safeArkEnv<
153+
export function arkenv<
161154
const T extends SchemaShape,
162155
const D extends EnvSchema<T> | CompiledEnvSchema,
163-
>(def: D, config?: ArkEnvConfig): SafeArkEnvResult<ArkenvOutput<T, D>>;
164-
export function safeArkEnv<
156+
>(def: D, config: ArkEnvConfig & { safe: true }): SafeArkEnvResult<ArkenvOutput<T, D>>;
157+
export function arkenv<
165158
const T extends SchemaShape,
166159
const D extends EnvSchema<T> | CompiledEnvSchema,
167-
>(def: D, config: ArkEnvConfig = {}): SafeArkEnvResult<ArkenvOutput<T, D>> {
168-
// Cast to any is required here because 'def' is a union type (EnvSchema | CompiledEnvSchema)
169-
// which TypeScript's overload resolution cannot resolve statically. It is safely resolved at runtime.
170-
return executeSafe(() => arkenv(def as any, config));
160+
>(
161+
def: D,
162+
config: ArkEnvConfig = {},
163+
): ArkenvOutput<T, D> | SafeArkEnvResult<ArkenvOutput<T, D>> {
164+
if (config.safe) {
165+
return executeSafe(() => parse(def as any, config));
166+
}
167+
// biome-ignore lint/suspicious/noExplicitAny: parse handles both EnvSchema<T> and CompiledEnvSchema at runtime
168+
return parse(def as any, config);
171169
}

packages/arkenv/src/errors.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from "vitest";
22
import { safeStringify, shouldRedact } from "@/utils/redact";
3-
import { safeArkEnv } from "./arkenv";
3+
import { arkenv } from "./arkenv";
44
import { ArkEnvError, type EnvIssue, formatError, formatIssues } from "./core";
55

66
describe("shouldRedact", () => {
@@ -101,7 +101,7 @@ describe("formatIssues & formatError", () => {
101101
});
102102
});
103103

104-
describe("ArkEnvError & safeArkEnv", () => {
104+
describe("ArkEnvError & arkenv safe mode", () => {
105105
it("should create error and store issues", () => {
106106
const issues: EnvIssue[] = [
107107
{
@@ -117,16 +117,16 @@ describe("ArkEnvError & safeArkEnv", () => {
117117
expect(error.name).toBe("ArkEnvError");
118118
});
119119

120-
it("should run safeArkEnv successfully", () => {
121-
const result = safeArkEnv({ PORT: "number" }, { env: { PORT: "3000" } });
120+
it("should run arkenv safely", () => {
121+
const result = arkenv({ PORT: "number" }, { safe: true, env: { PORT: "3000" } });
122122
expect(result.success).toBe(true);
123123
if (result.success) {
124124
expect(result.data).toEqual({ PORT: 3000 });
125125
}
126126
});
127127

128-
it("should return failure for safeArkEnv with invalid input", () => {
129-
const result = safeArkEnv({ PORT: "number" }, { env: { PORT: "abc" } });
128+
it("should return failure for arkenv safe mode with invalid input", () => {
129+
const result = arkenv({ PORT: "number" }, { safe: true, env: { PORT: "abc" } });
130130
expect(result.success).toBe(false);
131131
if (!result.success) {
132132
expect(result.issues).toBeDefined();

packages/arkenv/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { $ } from "@repo/scope";
2-
import { arkenv, safeArkEnv } from "./arkenv";
2+
import { arkenv } from "./arkenv";
33
import { getSchemaKeys } from "./schema";
44

5-
export { arkenv, getSchemaKeys, safeArkEnv };
5+
export { arkenv, getSchemaKeys };
66
/**
77
* Like ArkType's `type`, but with ArkEnv's extra keywords, such as:
88
*

packages/arkenv/src/parse-standard.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ export type ParseStandardConfig = {
8989
* @default false
9090
*/
9191
emptyAsUndefined?: boolean;
92+
93+
/**
94+
* Whether to return a safe result object instead of throwing an error on validation failure.
95+
*
96+
* When enabled, the function returns an object with `{ success: true, data }` or `{ success: false, issues }`.
97+
*
98+
* @default false
99+
*/
100+
safe?: boolean;
92101
};
93102

94103
/**

packages/arkenv/src/standard-mode.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, expectTypeOf, it, vi } from "vitest";
22
import { ArkEnvError } from "./core";
3-
import { arkenv, safeArkEnv } from "./standard";
3+
import { arkenv } from "./standard";
44

55
// Mock Standard Schema validators for testing
66
const createMockStandardSchema = <TOutput>(outputValue: TOutput) => ({
@@ -488,7 +488,7 @@ describe("Standard Mode emptyAsUndefined", () => {
488488
}
489489
});
490490

491-
it("should support safeArkEnv standard mode API", () => {
491+
it("should support arkenv({ safe: true }) standard mode API", () => {
492492
const mockZodValidator = {
493493
"~standard": {
494494
version: 1 as const,
@@ -511,22 +511,22 @@ describe("Standard Mode emptyAsUndefined", () => {
511511
},
512512
};
513513

514-
const resultSuccess = safeArkEnv(
514+
const resultSuccess = arkenv(
515515
{
516516
PORT: mockZodValidator,
517517
},
518-
{ env: { PORT: "3000" } },
518+
{ safe: true, env: { PORT: "3000" } },
519519
);
520520
expect(resultSuccess.success).toBe(true);
521521
if (resultSuccess.success) {
522522
expect(resultSuccess.data).toEqual({ PORT: "3000" });
523523
}
524524

525-
const resultFail = safeArkEnv(
525+
const resultFail = arkenv(
526526
{
527527
PORT: mockZodValidator,
528528
},
529-
{ env: {} },
529+
{ safe: true, env: {} },
530530
);
531531
expect(resultFail.success).toBe(false);
532532
if (!resultFail.success) {

packages/arkenv/src/standard.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,18 @@ export type StandardEnvConfig = ParseStandardConfig;
3737
*/
3838
export function arkenv<const T extends Record<string, StandardSchemaV1>>(
3939
def: T,
40-
config?: StandardEnvConfig,
41-
): { [K in keyof T]: StandardSchemaV1.InferOutput<T[K]> } {
40+
config?: StandardEnvConfig & { safe?: false },
41+
): { [K in keyof T]: StandardSchemaV1.InferOutput<T[K]> };
42+
export function arkenv<const T extends Record<string, StandardSchemaV1>>(
43+
def: T,
44+
config: StandardEnvConfig & { safe: true },
45+
): SafeArkEnvResult<{ [K in keyof T]: StandardSchemaV1.InferOutput<T[K]> }>;
46+
export function arkenv<const T extends Record<string, StandardSchemaV1>>(
47+
def: T,
48+
config: StandardEnvConfig = {},
49+
):
50+
| { [K in keyof T]: StandardSchemaV1.InferOutput<T[K]> }
51+
| SafeArkEnvResult<{ [K in keyof T]: StandardSchemaV1.InferOutput<T[K]> }> {
4252
assertStandardSchemaMap(def);
4353

4454
for (const key in def) {
@@ -47,26 +57,20 @@ export function arkenv<const T extends Record<string, StandardSchemaV1>>(
4757
assertStandardSchema(key, validator);
4858
}
4959

50-
return parseStandard(def as Record<string, unknown>, config ?? {}) as {
60+
if (config.safe) {
61+
return executeSafe(
62+
() =>
63+
parseStandard(def as Record<string, unknown>, config) as {
64+
[K in keyof T]: StandardSchemaV1.InferOutput<T[K]>;
65+
},
66+
);
67+
}
68+
69+
return parseStandard(def as Record<string, unknown>, config) as {
5170
[K in keyof T]: StandardSchemaV1.InferOutput<T[K]>;
5271
};
5372
}
5473

55-
/**
56-
* Non-throwing standard mode utility to parse and validate environment variables using Standard Schema 1.0 validators.
57-
* Returns a serializable result object containing either the validated data or error issues.
58-
*
59-
* @param def - An object mapping variable names to Standard Schema validators
60-
* @param config - Optional configuration
61-
* @returns The SafeArkEnvResult containing the data or the validation error object
62-
*/
63-
export function safeArkEnv<const T extends Record<string, StandardSchemaV1>>(
64-
def: T,
65-
config?: StandardEnvConfig,
66-
): SafeArkEnvResult<{ [K in keyof T]: StandardSchemaV1.InferOutput<T[K]> }> {
67-
return executeSafe(() => arkenv(def, config));
68-
}
69-
7074
/**
7175
* ArkEnv's Standard Schema export
7276
*

0 commit comments

Comments
 (0)