Skip to content

Commit 89cdbbe

Browse files
committed
Merge branch 'release/v0.28.18'
2 parents cf91fc6 + b8d6249 commit 89cdbbe

6 files changed

Lines changed: 254 additions & 5 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zeed",
33
"type": "module",
4-
"version": "0.28.17",
4+
"version": "0.28.18",
55
"packageManager": "pnpm@10.11.0",
66
"description": "🌱 Simple foundation library",
77
"author": {

src/common/schema/README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Schema Type System
2+
3+
This module provides a flexible, TypeScript-friendly schema/type system for runtime validation, parsing, and type inference. It is inspired by libraries like [valita](https://github.com/badrap/valita).
4+
5+
## Features
6+
- Define schemas for primitives, objects, arrays, tuples, unions, literals, and functions
7+
- Parse and validate data at runtime
8+
- Type inference for static TypeScript types
9+
- Extensible and composable
10+
11+
## Installation
12+
13+
This module is part of the `github-zeed` project. To use it, import from the appropriate path:
14+
15+
```ts
16+
import { array, Infer, literal, number, object, string, union, z } from './schema'
17+
// z.string and string are functionally alike, but z.string is preferred for consistency.
18+
```
19+
20+
## Basic Usage
21+
22+
### Primitives
23+
24+
```ts
25+
const name = z.string()
26+
const age = z.number()
27+
// Type: string, number
28+
```
29+
30+
### Objects
31+
32+
```ts
33+
const user = z.object({
34+
name: z.string(),
35+
age: z.number().optional(),
36+
})
37+
// Type: { name: string; age?: number }
38+
```
39+
40+
### Arrays
41+
42+
```ts
43+
const tags = z.array(z.string())
44+
// Type: string[]
45+
```
46+
47+
### Unions
48+
49+
```ts
50+
const status = z.union([
51+
z.literal('active'),
52+
z.literal('inactive'),
53+
])
54+
// Type: 'active' | 'inactive'
55+
```
56+
57+
### Type Inference
58+
59+
```ts
60+
type User = Infer<typeof user>
61+
// Equivalent to: { name: string; age?: number }
62+
```
63+
64+
### Parsing
65+
66+
```ts
67+
const parsed = user.parse({ name: 'Alice', age: 30 })
68+
// parsed: { name: 'Alice', age: 30 }
69+
```
70+
71+
### Optional and Default Values
72+
73+
```ts
74+
const score = z.number().optional().default(0)
75+
// Type: number | undefined (default: 0)
76+
```
77+
78+
### Function and RPC Types
79+
80+
```ts
81+
const add = z.func([z.number(), z.number()], z.number())
82+
// Type: (a: number, b: number) => number
83+
const rpcCall = z.rpc(z.object({ id: z.string() }), z.number())
84+
// Type: (info: { id: string }) => number | Promise<number>
85+
```
86+
87+
## API Reference
88+
89+
- `string()`, `number()`, `int()`, `boolean()`, `none()`, `any()`
90+
- `object({...})`, `array(type)`, `tuple([type1, type2, ...])`, `record(type)`
91+
- `union([type1, type2, ...])`
92+
- `literal(value)`, `stringLiterals(['a', 'b', ...])`
93+
- `func(args, ret)`, `rpc(info, ret)`
94+
- `.optional()`, `.default(value)`, `.props({ desc })`, `.extend({...})`
95+
- `parse(obj)`, `map(obj, fn)`
96+
97+
## Notes
98+
99+
- `z.string` and `string` are functionally equivalent, but `z.string` is preferred for consistency and clarity.
100+
- Each schema definition results in a corresponding TypeScript type, which can be extracted using `Infer<typeof schema>`.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { Infer } from './schema'
2+
import { schemaExportJsonSchema } from './export-json-schema'
3+
import { z } from './schema'
4+
5+
describe('json-schema.spec', () => {
6+
it('parse args', async () => {
7+
const schema = z.object({
8+
fixed: z.enum(['a', 'b', 'c']).describe('This is a fixed value'),
9+
anInt: z.int().optional().default(0),
10+
aBool: z.boolean().describe('This is a boolean'),
11+
aNumber: z.number().props({
12+
desc: 'This is a number',
13+
}),
14+
aString: z.string(),
15+
})
16+
17+
type t = Infer<typeof schema>
18+
expectTypeOf<t>().toMatchObjectType<{
19+
fixed: 'a' | 'b' | 'c'
20+
anInt?: number | undefined
21+
aBool: boolean
22+
aNumber: number
23+
aString: string
24+
}>()
25+
26+
const r = schemaExportJsonSchema(schema, 'Test')
27+
28+
expect(r).toMatchInlineSnapshot(`
29+
"{
30+
"$schema": "http://json-schema.org/draft-07/schema#",
31+
"title": "Test",
32+
"type": "object",
33+
"properties": {
34+
"fixed": {
35+
"type": "string",
36+
"enum": [
37+
"a",
38+
"b",
39+
"c"
40+
],
41+
"description": "This is a fixed value"
42+
},
43+
"anInt": {
44+
"type": "integer",
45+
"default": 0
46+
},
47+
"aBool": {
48+
"type": "boolean",
49+
"description": "This is a boolean"
50+
},
51+
"aNumber": {
52+
"type": "number",
53+
"description": "This is a number"
54+
},
55+
"aString": {
56+
"type": "string"
57+
}
58+
},
59+
"required": [
60+
"fixed",
61+
"aBool",
62+
"aNumber",
63+
"aString"
64+
]
65+
}"
66+
`)
67+
})
68+
})
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Type } from './schema'
2+
import { assert } from '../assert'
3+
import { objectMap } from '../data/object'
4+
import { isSchemaObjectFlat } from './utils'
5+
6+
const _mapJsonSchemaType: Record<string, string> = {
7+
string: 'string',
8+
number: 'number',
9+
boolean: 'boolean',
10+
int: 'integer',
11+
// Add more type mappings as needed
12+
}
13+
14+
export function schemaExportJsonSchema<T>(schema: Type<T>, name: string = 'Example'): string {
15+
assert(isSchemaObjectFlat(schema), 'schema should be a flat object')
16+
17+
const properties: Record<string, any> = {}
18+
const required: string[] = []
19+
20+
objectMap(schema._object!, (key, schema: Type<any>) => {
21+
properties[key] = {
22+
type: _mapJsonSchemaType[schema.type] ?? schema.type,
23+
}
24+
const enumValues = (schema as any)._enumValues
25+
if (enumValues) {
26+
properties[key].enum = enumValues
27+
}
28+
if (schema._default !== undefined) {
29+
properties[key].default = schema._default
30+
}
31+
if (schema._props?.desc) {
32+
properties[key].description = schema._props.desc
33+
}
34+
if (schema._optional !== true) {
35+
required.push(key)
36+
}
37+
})
38+
39+
const jsonSchema = {
40+
$schema: 'http://json-schema.org/draft-07/schema#',
41+
title: name,
42+
type: 'object',
43+
properties,
44+
...(required.length > 0 ? { required } : {}),
45+
}
46+
47+
return JSON.stringify(jsonSchema, null, 2)
48+
}

src/common/schema/schema.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ describe('schema', () => {
8686
"type": "any",
8787
},
8888
"lit": Object {
89+
"_enumValues": Array [
90+
"active",
91+
"trialing",
92+
"past_due",
93+
"paused",
94+
"deleted",
95+
],
8996
"type": "string",
9097
},
9198
"log": Object {

src/common/schema/schema.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface Type<T = unknown> {
2424
parse: (obj: any) => T
2525
map: (obj: any, fn: (this: Type<T>, obj: any, schema: Type<T>) => any) => any
2626
props: (props: TypeProps) => Type<T>
27+
describe: (msg: string) => Type<T>
2728
extend: <O>(obj: O) => Type<T & InferObject<O>>
2829
}
2930

@@ -81,6 +82,13 @@ export abstract class TypeClass<T = unknown> implements Type<T> {
8182
return this
8283
}
8384

85+
describe(msg: string) {
86+
if (!this._props)
87+
this._props = {}
88+
this._props.desc = msg
89+
return this
90+
}
91+
8492
extend: <O>(obj: O) => Type<T & InferObject<O>> = (obj: any) => {
8593
const newObj = { ...this._object, ...obj }
8694
return object(newObj) as any
@@ -235,6 +243,7 @@ export function record<T extends Type>(tobj: T): Type<Record<string, Infer<T>>>
235243

236244
type TransformToUnion<T extends (Type<any>)[]> = T extends Array<infer U> ? Infer<U> : never
237245

246+
/// Union of types, like `string | number | boolean`
238247
export function union<T extends (Type<any>)[]>(options: T): Type<TransformToUnion<T>> {
239248
return generic<any>(first(options)?.type ?? 'any', {
240249
// _union: options,
@@ -248,6 +257,15 @@ export function union<T extends (Type<any>)[]>(options: T): Type<TransformToUnio
248257

249258
type Literal = string | number | bigint | boolean
250259

260+
export class TypeStringLiterals<T> extends TypeClass<T> {
261+
constructor(values: string[]) {
262+
super('string', v => values.includes(v))
263+
this._enumValues = values
264+
}
265+
266+
_enumValues: string[]
267+
}
268+
251269
/// todo: string?
252270
export function literal<T extends Literal>(value: T): Type<T> {
253271
return generic<T>('literal', {
@@ -257,12 +275,19 @@ export function literal<T extends Literal>(value: T): Type<T> {
257275
}
258276

259277
/// Sting that can only be one of the values, like: `"a" | "b" | "c"``
260-
export function stringLiterals<const T extends readonly string[], O = T[number]>(value: T): Type<O> {
261-
return generic<O>('string', {
262-
_check: v => value.includes(v),
263-
})
278+
export function stringLiterals<const T extends readonly string[], O = T[number]>(values: T): Type<O> {
279+
return new TypeStringLiterals<O>(values as any)
264280
}
265281

282+
/// Sting that can only be one of the values, like: `"a" | "b" | "c"``
283+
// function zEnum<const T extends readonly (string | number | boolean | bigint)[], O = T[number]>(value: T): Type<O> {
284+
// return generic<O>('enum', {
285+
// _check: v => value.includes(v),
286+
// })
287+
// }
288+
289+
// export { zEnum as enum } // Export as enum to avoid conflicts with real enum types
290+
266291
// Functions
267292

268293
type TupleOutput<T extends Type[]> = {
@@ -363,6 +388,7 @@ export const z = {
363388
boolean,
364389
none,
365390
any,
391+
enum: stringLiterals,
366392
object,
367393
array,
368394
tuple,

0 commit comments

Comments
 (0)