@agentuity/schema 0.0.69

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.
Files changed (115) hide show
  1. package/AGENTS.md +86 -0
  2. package/README.md +323 -0
  3. package/dist/base.d.ts +111 -0
  4. package/dist/base.d.ts.map +1 -0
  5. package/dist/base.js +93 -0
  6. package/dist/base.js.map +1 -0
  7. package/dist/coerce/boolean.d.ts +37 -0
  8. package/dist/coerce/boolean.d.ts.map +1 -0
  9. package/dist/coerce/boolean.js +49 -0
  10. package/dist/coerce/boolean.js.map +1 -0
  11. package/dist/coerce/date.d.ts +36 -0
  12. package/dist/coerce/date.d.ts.map +1 -0
  13. package/dist/coerce/date.js +60 -0
  14. package/dist/coerce/date.js.map +1 -0
  15. package/dist/coerce/number.d.ts +36 -0
  16. package/dist/coerce/number.d.ts.map +1 -0
  17. package/dist/coerce/number.js +59 -0
  18. package/dist/coerce/number.js.map +1 -0
  19. package/dist/coerce/string.d.ts +35 -0
  20. package/dist/coerce/string.d.ts.map +1 -0
  21. package/dist/coerce/string.js +47 -0
  22. package/dist/coerce/string.js.map +1 -0
  23. package/dist/complex/array.d.ts +56 -0
  24. package/dist/complex/array.d.ts.map +1 -0
  25. package/dist/complex/array.js +96 -0
  26. package/dist/complex/array.js.map +1 -0
  27. package/dist/complex/object.d.ts +76 -0
  28. package/dist/complex/object.d.ts.map +1 -0
  29. package/dist/complex/object.js +104 -0
  30. package/dist/complex/object.js.map +1 -0
  31. package/dist/complex/record.d.ts +53 -0
  32. package/dist/complex/record.d.ts.map +1 -0
  33. package/dist/complex/record.js +109 -0
  34. package/dist/complex/record.js.map +1 -0
  35. package/dist/index.d.ts +151 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +128 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/json-schema.d.ts +60 -0
  40. package/dist/json-schema.d.ts.map +1 -0
  41. package/dist/json-schema.js +280 -0
  42. package/dist/json-schema.js.map +1 -0
  43. package/dist/primitives/any.d.ts +44 -0
  44. package/dist/primitives/any.d.ts.map +1 -0
  45. package/dist/primitives/any.js +57 -0
  46. package/dist/primitives/any.js.map +1 -0
  47. package/dist/primitives/boolean.d.ts +39 -0
  48. package/dist/primitives/boolean.d.ts.map +1 -0
  49. package/dist/primitives/boolean.js +53 -0
  50. package/dist/primitives/boolean.js.map +1 -0
  51. package/dist/primitives/null.d.ts +26 -0
  52. package/dist/primitives/null.d.ts.map +1 -0
  53. package/dist/primitives/null.js +40 -0
  54. package/dist/primitives/null.js.map +1 -0
  55. package/dist/primitives/number.d.ts +87 -0
  56. package/dist/primitives/number.d.ts.map +1 -0
  57. package/dist/primitives/number.js +129 -0
  58. package/dist/primitives/number.js.map +1 -0
  59. package/dist/primitives/string.d.ts +64 -0
  60. package/dist/primitives/string.d.ts.map +1 -0
  61. package/dist/primitives/string.js +102 -0
  62. package/dist/primitives/string.js.map +1 -0
  63. package/dist/primitives/undefined.d.ts +26 -0
  64. package/dist/primitives/undefined.d.ts.map +1 -0
  65. package/dist/primitives/undefined.js +40 -0
  66. package/dist/primitives/undefined.js.map +1 -0
  67. package/dist/primitives/unknown.d.ts +47 -0
  68. package/dist/primitives/unknown.d.ts.map +1 -0
  69. package/dist/primitives/unknown.js +56 -0
  70. package/dist/primitives/unknown.js.map +1 -0
  71. package/dist/utils/literal.d.ts +47 -0
  72. package/dist/utils/literal.d.ts.map +1 -0
  73. package/dist/utils/literal.js +64 -0
  74. package/dist/utils/literal.js.map +1 -0
  75. package/dist/utils/nullable.d.ts +50 -0
  76. package/dist/utils/nullable.d.ts.map +1 -0
  77. package/dist/utils/nullable.js +69 -0
  78. package/dist/utils/nullable.js.map +1 -0
  79. package/dist/utils/optional.d.ts +50 -0
  80. package/dist/utils/optional.d.ts.map +1 -0
  81. package/dist/utils/optional.js +69 -0
  82. package/dist/utils/optional.js.map +1 -0
  83. package/dist/utils/union.d.ts +60 -0
  84. package/dist/utils/union.d.ts.map +1 -0
  85. package/dist/utils/union.js +87 -0
  86. package/dist/utils/union.js.map +1 -0
  87. package/package.json +39 -0
  88. package/src/__tests__/coerce.test.ts +88 -0
  89. package/src/__tests__/complex.test.ts +124 -0
  90. package/src/__tests__/errors.test.ts +129 -0
  91. package/src/__tests__/json-schema.test.ts +138 -0
  92. package/src/__tests__/primitives.test.ts +184 -0
  93. package/src/__tests__/type-inference.test.ts +68 -0
  94. package/src/__tests__/utils.test.ts +100 -0
  95. package/src/base.ts +185 -0
  96. package/src/coerce/boolean.ts +56 -0
  97. package/src/coerce/date.ts +68 -0
  98. package/src/coerce/number.ts +67 -0
  99. package/src/coerce/string.ts +54 -0
  100. package/src/complex/array.ts +108 -0
  101. package/src/complex/object.ts +141 -0
  102. package/src/complex/record.ts +129 -0
  103. package/src/index.ts +177 -0
  104. package/src/json-schema.ts +331 -0
  105. package/src/primitives/any.ts +64 -0
  106. package/src/primitives/boolean.ts +60 -0
  107. package/src/primitives/null.ts +47 -0
  108. package/src/primitives/number.ts +141 -0
  109. package/src/primitives/string.ts +113 -0
  110. package/src/primitives/undefined.ts +47 -0
  111. package/src/primitives/unknown.ts +63 -0
  112. package/src/utils/literal.ts +71 -0
  113. package/src/utils/nullable.ts +80 -0
  114. package/src/utils/optional.ts +80 -0
  115. package/src/utils/union.ts +103 -0
package/src/base.ts ADDED
@@ -0,0 +1,185 @@
1
+ import type { StandardSchemaV1 } from '@agentuity/core';
2
+
3
+ /**
4
+ * A validation issue from a failed schema validation.
5
+ */
6
+ export type ValidationIssue = StandardSchemaV1.Issue;
7
+
8
+ /**
9
+ * The result of a schema validation (success or failure).
10
+ */
11
+ export type ValidationResult<T> = StandardSchemaV1.Result<T>;
12
+
13
+ /**
14
+ * Successful parse result containing validated data.
15
+ */
16
+ export interface SafeParseSuccess<T> {
17
+ /** Indicates successful validation */
18
+ success: true;
19
+ /** The validated and typed data */
20
+ data: T;
21
+ }
22
+
23
+ /**
24
+ * Failed parse result containing validation error.
25
+ */
26
+ export interface SafeParseError {
27
+ /** Indicates failed validation */
28
+ success: false;
29
+ /** The validation error with detailed issues */
30
+ error: ValidationError;
31
+ }
32
+
33
+ /**
34
+ * Result of safeParse - either success with data or failure with error.
35
+ */
36
+ export type SafeParseResult<T> = SafeParseSuccess<T> | SafeParseError;
37
+
38
+ /**
39
+ * Error thrown when schema validation fails.
40
+ * Contains detailed information about all validation issues including field paths.
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * try {
45
+ * schema.parse(data);
46
+ * } catch (error) {
47
+ * if (error instanceof ValidationError) {
48
+ * console.log(error.message); // Human-readable error
49
+ * console.log(error.issues); // Detailed issues array
50
+ * }
51
+ * }
52
+ * ```
53
+ */
54
+ export class ValidationError extends Error {
55
+ /** Array of validation issues with paths and messages */
56
+ readonly issues: readonly ValidationIssue[];
57
+
58
+ constructor(issues: readonly ValidationIssue[]) {
59
+ const message = issues
60
+ .map((issue) => {
61
+ const path = issue.path
62
+ ? `[${issue.path
63
+ .map((p) =>
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ typeof p === 'object' ? (p as any).key : p
66
+ )
67
+ .join('.')}]`
68
+ : '';
69
+ return path ? `${path}: ${issue.message}` : issue.message;
70
+ })
71
+ .join('\n');
72
+
73
+ super(message);
74
+ this.name = 'ValidationError';
75
+ this.issues = issues;
76
+
77
+ // Maintain proper stack trace for where our error was thrown
78
+ if (Error.captureStackTrace) {
79
+ Error.captureStackTrace(this, ValidationError);
80
+ }
81
+ }
82
+
83
+ toString(): string {
84
+ return `${this.name}: ${this.message}`;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Base schema interface that all schemas implement.
90
+ * Provides StandardSchema v1 compliance plus additional methods for parsing and description.
91
+ */
92
+ export interface Schema<Input = unknown, Output = Input> extends StandardSchemaV1<Input, Output> {
93
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
94
+ /** Optional description for documentation */
95
+ description?: string;
96
+ /** Add a description to the schema for documentation and JSON Schema */
97
+ describe(description: string): this;
98
+ /** Parse and validate data, throwing ValidationError on failure */
99
+ parse(value: unknown): Output;
100
+ /** Parse and validate data, returning result object without throwing */
101
+ safeParse(value: unknown): SafeParseResult<Output>;
102
+ /** Make this schema optional (allow undefined) */
103
+ optional(): Schema<Input | undefined, Output | undefined>;
104
+ /** Make this schema nullable (allow null) */
105
+ nullable(): Schema<Input | null, Output | null>;
106
+ }
107
+
108
+ /**
109
+ * Extract the output type from a schema (like zod's z.infer).
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * const User = s.object({ name: s.string(), age: s.number() });
114
+ * type User = Infer<typeof User>; // { name: string; age: number }
115
+ * ```
116
+ */
117
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
+ export type Infer<T extends Schema<any, any>> = StandardSchemaV1.InferOutput<T>;
119
+
120
+ /**
121
+ * Extract the input type from a schema.
122
+ */
123
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
124
+ export type InferInput<T extends Schema<any, any>> = StandardSchemaV1.InferInput<T>;
125
+
126
+ /**
127
+ * Extract the output type from a schema (alias for Infer).
128
+ */
129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
+ export type InferOutput<T extends Schema<any, any>> = StandardSchemaV1.InferOutput<T>;
131
+
132
+ /**
133
+ * Create a validation issue with an optional field path.
134
+ */
135
+ export function createIssue(
136
+ message: string,
137
+ path?: ReadonlyArray<PropertyKey | StandardSchemaV1.PathSegment>
138
+ ): ValidationIssue {
139
+ return path ? { message, path } : { message };
140
+ }
141
+
142
+ /**
143
+ * Create a successful validation result.
144
+ */
145
+ export function success<T>(value: T): StandardSchemaV1.SuccessResult<T> {
146
+ return { value };
147
+ }
148
+
149
+ /**
150
+ * Create a failed validation result.
151
+ */
152
+ export function failure(issues: ValidationIssue[]): StandardSchemaV1.FailureResult {
153
+ return { issues };
154
+ }
155
+
156
+ /**
157
+ * Create parse and safeParse methods for a schema.
158
+ * @internal
159
+ */
160
+ export function createParseMethods<Output>() {
161
+ return {
162
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
+ parse(this: Schema<any, Output>, value: unknown): Output {
164
+ const result = this['~standard'].validate(value);
165
+ if (result instanceof Promise) {
166
+ throw new Error('Async validation not supported in parse()');
167
+ }
168
+ if (result.issues) {
169
+ throw new ValidationError(result.issues);
170
+ }
171
+ return result.value;
172
+ },
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ safeParse(this: Schema<any, Output>, value: unknown): SafeParseResult<Output> {
175
+ const result = this['~standard'].validate(value);
176
+ if (result instanceof Promise) {
177
+ throw new Error('Async validation not supported in safeParse()');
178
+ }
179
+ if (result.issues) {
180
+ return { success: false, error: new ValidationError(result.issues) };
181
+ }
182
+ return { success: true, data: result.value };
183
+ },
184
+ };
185
+ }
@@ -0,0 +1,56 @@
1
+ import type { Schema } from '../base';
2
+ import { success, createParseMethods } from '../base';
3
+ import { optional } from '../utils/optional';
4
+ import { nullable } from '../utils/nullable';
5
+
6
+ const parseMethods = createParseMethods<boolean>();
7
+
8
+ /**
9
+ * Schema that coerces values to booleans using Boolean(value).
10
+ * Uses JavaScript truthy/falsy rules.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const schema = s.coerce.boolean();
15
+ * schema.parse(1); // true
16
+ * schema.parse(0); // false
17
+ * schema.parse(''); // false
18
+ * schema.parse('hello'); // true
19
+ * ```
20
+ */
21
+ export class CoerceBooleanSchema implements Schema<unknown, boolean> {
22
+ description?: string;
23
+
24
+ readonly '~standard' = {
25
+ version: 1 as const,
26
+ vendor: 'agentuity',
27
+ validate: (value: unknown) => {
28
+ // Coerce to boolean using JavaScript truthiness rules
29
+ return success(Boolean(value));
30
+ },
31
+ types: undefined as unknown as { input: unknown; output: boolean },
32
+ };
33
+
34
+ describe(description: string): this {
35
+ this.description = description;
36
+ return this;
37
+ }
38
+
39
+ optional() {
40
+ return optional(this);
41
+ }
42
+
43
+ nullable() {
44
+ return nullable(this);
45
+ }
46
+ parse = parseMethods.parse;
47
+ safeParse = parseMethods.safeParse;
48
+ }
49
+
50
+ /**
51
+ * Create a schema that coerces values to booleans.
52
+ * Useful for parsing checkboxes or boolean flags from strings.
53
+ */
54
+ export function coerceBoolean(): CoerceBooleanSchema {
55
+ return new CoerceBooleanSchema();
56
+ }
@@ -0,0 +1,68 @@
1
+ import type { Schema } from '../base';
2
+ import { createIssue, failure, success, createParseMethods } from '../base';
3
+ import { optional } from '../utils/optional';
4
+ import { nullable } from '../utils/nullable';
5
+
6
+ const parseMethods = createParseMethods<Date>();
7
+
8
+ /**
9
+ * Schema that coerces values to Date objects using new Date(value).
10
+ * Fails if the result is an invalid date.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const schema = s.coerce.date();
15
+ * schema.parse('2025-01-01'); // Date object
16
+ * schema.parse(1609459200000); // Date from timestamp
17
+ * schema.parse('invalid'); // throws ValidationError
18
+ * ```
19
+ */
20
+ export class CoerceDateSchema implements Schema<unknown, Date> {
21
+ description?: string;
22
+
23
+ readonly '~standard' = {
24
+ version: 1 as const,
25
+ vendor: 'agentuity',
26
+ validate: (value: unknown) => {
27
+ // Already a Date
28
+ if (value instanceof Date) {
29
+ if (isNaN(value.getTime())) {
30
+ return failure([createIssue('Invalid date')]);
31
+ }
32
+ return success(value);
33
+ }
34
+
35
+ // Coerce to Date
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const coerced = new Date(value as any);
38
+ if (isNaN(coerced.getTime())) {
39
+ return failure([createIssue(`Cannot coerce ${typeof value} to date`)]);
40
+ }
41
+ return success(coerced);
42
+ },
43
+ types: undefined as unknown as { input: unknown; output: Date },
44
+ };
45
+
46
+ describe(description: string): this {
47
+ this.description = description;
48
+ return this;
49
+ }
50
+
51
+ optional() {
52
+ return optional(this);
53
+ }
54
+
55
+ nullable() {
56
+ return nullable(this);
57
+ }
58
+ parse = parseMethods.parse;
59
+ safeParse = parseMethods.safeParse;
60
+ }
61
+
62
+ /**
63
+ * Create a schema that coerces values to Date objects.
64
+ * Useful for parsing ISO date strings or timestamps.
65
+ */
66
+ export function coerceDate(): CoerceDateSchema {
67
+ return new CoerceDateSchema();
68
+ }
@@ -0,0 +1,67 @@
1
+ import type { Schema } from '../base';
2
+ import { createIssue, failure, success, createParseMethods } from '../base';
3
+ import { optional } from '../utils/optional';
4
+ import { nullable } from '../utils/nullable';
5
+
6
+ const parseMethods = createParseMethods<number>();
7
+
8
+ /**
9
+ * Schema that coerces values to numbers using Number(value).
10
+ * Fails if the result is NaN.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const schema = s.coerce.number();
15
+ * schema.parse('123'); // 123
16
+ * schema.parse(true); // 1
17
+ * schema.parse('abc'); // throws ValidationError
18
+ * ```
19
+ */
20
+ export class CoerceNumberSchema implements Schema<unknown, number> {
21
+ description?: string;
22
+
23
+ readonly '~standard' = {
24
+ version: 1 as const,
25
+ vendor: 'agentuity',
26
+ validate: (value: unknown) => {
27
+ // Already a number
28
+ if (typeof value === 'number') {
29
+ if (Number.isNaN(value)) {
30
+ return failure([createIssue('Cannot coerce NaN to number')]);
31
+ }
32
+ return success(value);
33
+ }
34
+
35
+ // Coerce to number
36
+ const coerced = Number(value);
37
+ if (Number.isNaN(coerced)) {
38
+ return failure([createIssue(`Cannot coerce ${typeof value} to number`)]);
39
+ }
40
+ return success(coerced);
41
+ },
42
+ types: undefined as unknown as { input: unknown; output: number },
43
+ };
44
+
45
+ describe(description: string): this {
46
+ this.description = description;
47
+ return this;
48
+ }
49
+
50
+ optional() {
51
+ return optional(this);
52
+ }
53
+
54
+ nullable() {
55
+ return nullable(this);
56
+ }
57
+ parse = parseMethods.parse;
58
+ safeParse = parseMethods.safeParse;
59
+ }
60
+
61
+ /**
62
+ * Create a schema that coerces values to numbers.
63
+ * Useful for parsing form data or query parameters where numbers come as strings.
64
+ */
65
+ export function coerceNumber(): CoerceNumberSchema {
66
+ return new CoerceNumberSchema();
67
+ }
@@ -0,0 +1,54 @@
1
+ import type { Schema } from '../base';
2
+ import { success, createParseMethods } from '../base';
3
+ import { optional } from '../utils/optional';
4
+ import { nullable } from '../utils/nullable';
5
+
6
+ const parseMethods = createParseMethods<string>();
7
+
8
+ /**
9
+ * Schema that coerces any value to a string using String(value).
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const schema = s.coerce.string();
14
+ * schema.parse(123); // '123'
15
+ * schema.parse(true); // 'true'
16
+ * schema.parse(null); // 'null'
17
+ * ```
18
+ */
19
+ export class CoerceStringSchema implements Schema<unknown, string> {
20
+ description?: string;
21
+
22
+ readonly '~standard' = {
23
+ version: 1 as const,
24
+ vendor: 'agentuity',
25
+ validate: (value: unknown) => {
26
+ // Coerce to string
27
+ return success(String(value));
28
+ },
29
+ types: undefined as unknown as { input: unknown; output: string },
30
+ };
31
+
32
+ describe(description: string): this {
33
+ this.description = description;
34
+ return this;
35
+ }
36
+
37
+ optional() {
38
+ return optional(this);
39
+ }
40
+
41
+ nullable() {
42
+ return nullable(this);
43
+ }
44
+ parse = parseMethods.parse;
45
+ safeParse = parseMethods.safeParse;
46
+ }
47
+
48
+ /**
49
+ * Create a schema that coerces values to strings.
50
+ * Useful for parsing form data or query parameters.
51
+ */
52
+ export function coerceString(): CoerceStringSchema {
53
+ return new CoerceStringSchema();
54
+ }
@@ -0,0 +1,108 @@
1
+ import type { Schema, Infer } from '../base';
2
+ import { createIssue, failure, success, createParseMethods } from '../base';
3
+ import { optional } from '../utils/optional';
4
+ import { nullable } from '../utils/nullable';
5
+
6
+ /**
7
+ * Schema for validating arrays with typed elements.
8
+ * Validates each element and collects all validation errors with array indices in paths.
9
+ *
10
+ * @template T - The schema type for array elements
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const tagsSchema = s.array(s.string());
15
+ * const tags = tagsSchema.parse(['tag1', 'tag2']);
16
+ *
17
+ * const usersSchema = s.array(s.object({
18
+ * name: s.string(),
19
+ * age: s.number()
20
+ * }));
21
+ * ```
22
+ */
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ export class ArraySchema<T extends Schema<any, any>>
25
+ implements Schema<Array<Infer<T>>, Array<Infer<T>>>
26
+ {
27
+ description?: string;
28
+ private parseMethods = createParseMethods<Array<Infer<T>>>();
29
+
30
+ constructor(private itemSchema: T) {}
31
+
32
+ readonly '~standard' = {
33
+ version: 1 as const,
34
+ vendor: 'agentuity',
35
+ validate: (value: unknown) => {
36
+ if (value === null) {
37
+ return failure([createIssue('Expected array, got null')]);
38
+ }
39
+ if (!Array.isArray(value)) {
40
+ return failure([createIssue(`Expected array, got ${typeof value}`)]);
41
+ }
42
+
43
+ const result: Infer<T>[] = [];
44
+ const issues: ReturnType<typeof createIssue>[] = [];
45
+
46
+ for (let i = 0; i < value.length; i++) {
47
+ const validation = this.itemSchema['~standard'].validate(value[i]);
48
+
49
+ // Only support synchronous validation for now
50
+ if (validation instanceof Promise) {
51
+ throw new Error('Async validation not supported');
52
+ }
53
+
54
+ if (validation.issues) {
55
+ for (const issue of validation.issues) {
56
+ issues.push(createIssue(issue.message, issue.path ? [i, ...issue.path] : [i]));
57
+ }
58
+ } else {
59
+ result.push(validation.value);
60
+ }
61
+ }
62
+
63
+ if (issues.length > 0) {
64
+ return failure(issues);
65
+ }
66
+
67
+ return success(result);
68
+ },
69
+ types: undefined as unknown as { input: Array<Infer<T>>; output: Array<Infer<T>> },
70
+ };
71
+
72
+ describe(description: string): this {
73
+ this.description = description;
74
+ return this;
75
+ }
76
+
77
+ optional() {
78
+ return optional(this);
79
+ }
80
+
81
+ nullable() {
82
+ return nullable(this);
83
+ }
84
+
85
+ parse = this.parseMethods.parse;
86
+ safeParse = this.parseMethods.safeParse;
87
+ }
88
+
89
+ /**
90
+ * Create an array schema with typed elements.
91
+ *
92
+ * @param itemSchema - The schema for validating each array element
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const stringArray = s.array(s.string());
97
+ * const tags = stringArray.parse(['tag1', 'tag2']);
98
+ *
99
+ * const userArray = s.array(s.object({
100
+ * name: s.string(),
101
+ * age: s.number()
102
+ * }));
103
+ * ```
104
+ */
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ export function array<T extends Schema<any, any>>(itemSchema: T): ArraySchema<T> {
107
+ return new ArraySchema(itemSchema);
108
+ }
@@ -0,0 +1,141 @@
1
+ import type { Schema, Infer } from '../base';
2
+ import { createIssue, failure, success, createParseMethods } from '../base';
3
+ import { optional, OptionalSchema } from '../utils/optional';
4
+ import { nullable } from '../utils/nullable';
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ type ObjectShape = Record<string, Schema<any, any>>;
8
+
9
+ // Helper to check if a schema is optional
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ type IsOptional<T> = T extends OptionalSchema<any> ? true : false;
12
+
13
+ // Split required and optional keys
14
+ type RequiredKeys<T extends ObjectShape> = {
15
+ [K in keyof T]: IsOptional<T[K]> extends true ? never : K;
16
+ }[keyof T];
17
+
18
+ type OptionalKeys<T extends ObjectShape> = {
19
+ [K in keyof T]: IsOptional<T[K]> extends true ? K : never;
20
+ }[keyof T];
21
+
22
+ // Infer object shape with proper optional handling
23
+ type InferObjectShape<T extends ObjectShape> = {
24
+ [K in RequiredKeys<T>]: Infer<T[K]>;
25
+ } & {
26
+ [K in OptionalKeys<T>]?: Infer<T[K]>;
27
+ };
28
+
29
+ /**
30
+ * Schema for validating objects with typed properties.
31
+ * Validates each property according to its schema and collects all validation errors.
32
+ *
33
+ * @template T - The object shape definition
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const userSchema = s.object({
38
+ * name: s.string(),
39
+ * age: s.number(),
40
+ * email: s.string()
41
+ * });
42
+ *
43
+ * const user = userSchema.parse({
44
+ * name: 'John',
45
+ * age: 30,
46
+ * email: 'john@example.com'
47
+ * });
48
+ * ```
49
+ */
50
+ export class ObjectSchema<T extends ObjectShape>
51
+ implements Schema<InferObjectShape<T>, InferObjectShape<T>>
52
+ {
53
+ description?: string;
54
+ private parseMethods = createParseMethods<InferObjectShape<T>>();
55
+
56
+ constructor(private shape: T) {}
57
+
58
+ readonly '~standard' = {
59
+ version: 1 as const,
60
+ vendor: 'agentuity',
61
+ validate: (value: unknown) => {
62
+ if (value === null) {
63
+ return failure([createIssue('Expected object, got null')]);
64
+ }
65
+ if (Array.isArray(value)) {
66
+ return failure([createIssue('Expected object, got array')]);
67
+ }
68
+ if (typeof value !== 'object') {
69
+ return failure([createIssue(`Expected object, got ${typeof value}`)]);
70
+ }
71
+
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ const result: Record<string, any> = {};
74
+ const issues: ReturnType<typeof createIssue>[] = [];
75
+
76
+ for (const [key, schema] of Object.entries(this.shape)) {
77
+ const fieldValue = (value as Record<string, unknown>)[key];
78
+ const validation = schema['~standard'].validate(fieldValue);
79
+
80
+ // Only support synchronous validation for now
81
+ if (validation instanceof Promise) {
82
+ throw new Error('Async validation not supported');
83
+ }
84
+
85
+ if (validation.issues) {
86
+ for (const issue of validation.issues) {
87
+ issues.push(
88
+ createIssue(issue.message, issue.path ? [key, ...issue.path] : [key])
89
+ );
90
+ }
91
+ } else {
92
+ result[key] = validation.value;
93
+ }
94
+ }
95
+
96
+ if (issues.length > 0) {
97
+ return failure(issues);
98
+ }
99
+
100
+ return success(result as InferObjectShape<T>);
101
+ },
102
+ types: undefined as unknown as { input: InferObjectShape<T>; output: InferObjectShape<T> },
103
+ };
104
+
105
+ describe(description: string): this {
106
+ this.description = description;
107
+ return this;
108
+ }
109
+
110
+ optional() {
111
+ return optional(this);
112
+ }
113
+
114
+ nullable() {
115
+ return nullable(this);
116
+ }
117
+
118
+ parse = this.parseMethods.parse;
119
+ safeParse = this.parseMethods.safeParse;
120
+ }
121
+
122
+ /**
123
+ * Create an object schema with typed properties.
124
+ *
125
+ * @param shape - Object defining the schema for each property
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * const userSchema = s.object({
130
+ * name: s.string().describe('Full name'),
131
+ * age: s.number().describe('Age in years'),
132
+ * email: s.optional(s.string())
133
+ * });
134
+ *
135
+ * type User = s.infer<typeof userSchema>;
136
+ * const user = userSchema.parse(data);
137
+ * ```
138
+ */
139
+ export function object<T extends ObjectShape>(shape: T): ObjectSchema<T> {
140
+ return new ObjectSchema(shape);
141
+ }