Skip to content

Commit 3031d49

Browse files
authored
feat(is): add package (#122)
1 parent b5ada5b commit 3031d49

File tree

13 files changed

+761
-0
lines changed

13 files changed

+761
-0
lines changed

deno.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

is/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# 🪪 Is
2+
3+
[![JSR](https://jsr.io/badges/@libs/is)](https://jsr.io/@libs/is) [![JSR Score](https://jsr.io/badges/@libs/is/score)](https://jsr.io/@libs/is)
4+
5+
A wrapper around [Zod](https://zod.dev/) to ease type validation and assertions.
6+
7+
It also contains some additional validators, in addition to a spec checker.
8+
9+
- [`📚 Documentation`](https://jsr.io/@libs/is/doc)
10+
11+
## 📜 License
12+
13+
```plaintext
14+
Copyright (c) Simon Lecoq <@lowlighter>. (MIT License)
15+
https://github.com/lowlighter/libs/blob/main/LICENSE
16+
```

is/deno.jsonc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"icon": "🪪",
3+
"name": "@libs/is",
4+
"version": "0.1.0",
5+
"imports": {
6+
"@std/collections": "jsr:@std/collections@1",
7+
"@zod/zod": "jsr:@zod/zod@^4.1.11"
8+
},
9+
"exports": {
10+
".": "./mod.ts",
11+
"./testing": "./testing/mod.ts"
12+
},
13+
"tasks": {
14+
"lint": {
15+
"description": "Lint code, documentation, package and formatting\n- `--check`: skip actual formatting (check only)",
16+
"command": "cd $INIT_CWD && deno lint && deno publish --quiet --dry-run --allow-dirty && deno fmt"
17+
}
18+
},
19+
"test:permissions": {
20+
"env": true
21+
},
22+
"supported": {
23+
"deno": true
24+
}
25+
}

is/is.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Imports
2+
// deno-lint-ignore-file no-explicit-any ban-types
3+
import type { ParseOptions as ParseArgsOptions } from "@std/cli/parse-args"
4+
import { parseArgs } from "@std/cli/parse-args"
5+
import * as is from "@zod/zod"
6+
export * as is from "@zod/zod"
7+
export type * from "@zod/zod"
8+
9+
/** Coalesces a parsed value to `undefined` so schema defaults can be applied. */
10+
export function coalesce<T extends is.ZodType>(schema: T): is.ZodPipe<is.ZodTransform<{} | undefined, unknown>, T> {
11+
return is.preprocess((value) => {
12+
return value ?? undefined
13+
}, schema)
14+
}
15+
16+
/** Coerces a parsed value into `number` type when possible. */
17+
export function coerce<T extends is.ZodType>(schema: T): is.ZodPipe<is.ZodTransform<unknown, unknown>, T> {
18+
return is.preprocess((value) => {
19+
if (typeof value === "string") {
20+
const coerced = Number(value)
21+
if ((!Number.isNaN(coerced)) || (value === "NaN")) {
22+
return coerced
23+
}
24+
}
25+
return value
26+
}, schema)
27+
}
28+
29+
/** Allows the `null` value and makes it the default value in schemas. */
30+
export function nullable<T extends is.ZodType>(schema: T): is.ZodDefault<is.ZodNullable<T>> {
31+
return schema.nullable().default(null)
32+
}
33+
34+
/** Ensures a parsed value is compatible with `structuredClone` algorithm. */
35+
export function clonable<T extends is.ZodType>(schema: T): is.ZodPipe<is.ZodTransform<any, unknown>, T> {
36+
return is.preprocess((value, context) => {
37+
try {
38+
structuredClone(value)
39+
} catch (error) {
40+
context.addIssue(`Invalid input: Value must be compatible with structuredClone algorithm: ${error}`)
41+
}
42+
return value
43+
}, schema)
44+
}
45+
46+
/** Transforms a parsed value into an array if it is not already one. */
47+
export function arrayable<T extends is.ZodType>(schema: T): is.ZodPipe<is.ZodTransform<any[], unknown>, T> {
48+
return is.preprocess((value) => {
49+
if (!Array.isArray(value)) {
50+
return [value]
51+
}
52+
return value
53+
}, schema)
54+
}
55+
56+
/** Transforms a CLI arguments string into an object or array. */
57+
export function cliable<T extends is.ZodType>(schema: T, options?: ParseArgsOptions): is.ZodPipe<is.ZodTransform<any, unknown>, T> {
58+
return is.preprocess((value, context) => {
59+
if (typeof value === "string") {
60+
const args = []
61+
let current = ""
62+
let quoted = ""
63+
let escape = false
64+
for (const char of value) {
65+
if (escape) {
66+
current += char
67+
escape = false
68+
continue
69+
}
70+
if ((quoted === '"') && (char === "\\")) {
71+
escape = true
72+
continue
73+
}
74+
if ((!quoted) && ((char === '"') || (char === "'"))) {
75+
quoted = char
76+
continue
77+
}
78+
if (quoted === char) {
79+
quoted = ""
80+
continue
81+
}
82+
if ((!quoted) && (char === " ")) {
83+
if (current) {
84+
args.push(current)
85+
current = ""
86+
}
87+
continue
88+
}
89+
current += char
90+
}
91+
if (quoted) {
92+
context.addIssue(`Unclosed quote: ${quoted}${current}`)
93+
return value
94+
}
95+
if (current) {
96+
args.push(current)
97+
}
98+
return options ? parseArgs(args, options) : args
99+
}
100+
return value
101+
}, schema)
102+
}
103+
104+
/** Parses a regular expression back into a `RegExp`. */
105+
export function regex<T extends is.ZodType>(schema: T): is.ZodPipe<is.ZodTransform<unknown, unknown>, T> {
106+
const definition = /^\/(?<pattern>.*?)\/(?<flags>[dgimsuvy]*)$/
107+
return is.preprocess((value) => {
108+
if ((typeof value === "string") && (definition.test(value))) {
109+
const groups = value.match(definition)?.groups
110+
if (groups) {
111+
const { pattern, flags } = groups
112+
try {
113+
return new RegExp(pattern, flags)
114+
} catch {
115+
return value
116+
}
117+
}
118+
}
119+
return value
120+
}, schema)
121+
}
122+
123+
/** Parses a date string back into a `Date`. */
124+
export function date<T extends is.ZodType>(schema: T): is.ZodPipe<is.ZodTransform<unknown, unknown>, T> {
125+
return is.preprocess((value) => {
126+
if (typeof value === "string") {
127+
const date = new Date(value)
128+
if (!Number.isNaN(date.getTime())) {
129+
return date
130+
}
131+
}
132+
return value
133+
}, schema)
134+
}
135+
136+
/** Parses a duration string into milliseconds. */
137+
export function duration<T extends is.ZodType>(schema: T): is.ZodPipe<is.ZodTransform<any, unknown>, T> {
138+
return is.preprocess((value) => {
139+
if (typeof value === "string") {
140+
const groups = value.match(/^\s*(?:(?<days>\d+)d(?:ays?)?)?\s*(?:(?<hours>\d+)h(?:(?:ou)?rs?)?)?\s*(?:(?<minutes>\d+)m(?:in(?:ute)?s?)?)?\s*(?:(?<seconds>\d+)s(?:ec(?:ond)?s?)?)?\s*(?:(?<milliseconds>\d+)(?:m(?:illi)?s(?:ec(?:ond)?s?)?)?)?\s*$/)?.groups
141+
if (groups) {
142+
let { days = 0, hours = 0, minutes = 0, seconds = 0, milliseconds = 0 } = Object.fromEntries(Object.entries(groups).map(([key, value]) => [key, Number(value)]).filter(([_, value]) => Number.isFinite(value)))
143+
hours += days * 24
144+
minutes += hours * 60
145+
seconds += minutes * 60
146+
milliseconds += seconds * 1000
147+
return milliseconds
148+
}
149+
}
150+
return value
151+
}, schema)
152+
}
153+
154+
/** Type alias for primitive values. */
155+
export const primitive = is.union([is.string(), is.number(), is.bigint(), is.boolean(), is.undefined(), is.null(), is.date(), is.instanceof(Error)]) as is.ZodUnion<
156+
readonly [is.ZodString, is.ZodNumber, is.ZodBigInt, is.ZodBoolean, is.ZodUndefined, is.ZodNull, is.ZodDate, is.ZodCustom<Error, Error>]
157+
>
158+
159+
/** Type alias for URLs with supported protocols. */
160+
export const url = is.url({ protocol: /^wasm|file|https?|data|blob|jsr|npm$/, hostname: /.*/ }) as is.ZodURL
161+
162+
/** Type alias for expression strings. */
163+
export const expression = is.string().regex(/\$\{.*\}/) as is.ZodString
164+
165+
/** Type alias for callable values. */
166+
export const callable = is.unknown().refine((value) => typeof value === "function", { message: "Invalid input: Value must be callable" }) as is.ZodUnknown
167+
168+
/** Type alias for Zod schemas. */
169+
export const parser = is.custom((value) => (value instanceof is.ZodType) || (value && (typeof value === "object") && (typeof (value as Record<PropertyKey, unknown>).parse === "function")), { message: "Invalid input: Value must be a Zod schema" }) as is.ZodCustom<unknown, unknown>

0 commit comments

Comments
 (0)