Skip to content

Commit 1b484de

Browse files
committed
Merge commit 'refs/pull/483/merge' of github.com:ciscoheat/sveltekit-superforms
2 parents 547187d + 7d25a4a commit 1b484de

File tree

7 files changed

+163
-9
lines changed

7 files changed

+163
-9
lines changed

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,15 @@
102102
"!dist/**/*.spec.*"
103103
],
104104
"peerDependencies": {
105+
"@effect/schema": "^0.73.4",
105106
"@exodus/schemasafe": "^1.3.0",
106107
"@sinclair/typebox": ">=0.32.30 <1",
107108
"@sveltejs/kit": "1.x || 2.x",
108109
"@typeschema/class-validator": "^0.2.0",
109110
"@vinejs/vine": "^1.8.0",
110111
"arktype": ">=2.0.0-rc.8",
111112
"class-validator": "^0.14.1",
113+
"effect": "^3.8.2",
112114
"joi": "^17.13.1",
113115
"superstruct": "^2.0.2",
114116
"svelte": "3.x || 4.x || >=5.0.0-next.51",
@@ -149,16 +151,24 @@
149151
},
150152
"@vinejs/vine": {
151153
"optional": true
154+
},
155+
"effect": {
156+
"optional": true
157+
},
158+
"@effect/schema": {
159+
"optional": true
152160
}
153161
},
154162
"optionalDependencies": {
163+
"@effect/schema": "^0.73.4",
155164
"@exodus/schemasafe": "^1.3.0",
156165
"@gcornut/valibot-json-schema": "^0.31.0",
157166
"@sinclair/typebox": "^0.32.35",
158167
"@typeschema/class-validator": "^0.2.0",
159168
"@vinejs/vine": "^1.8.0",
160169
"arktype": "2.0.0-rc.8",
161170
"class-validator": "^0.14.1",
171+
"effect": "^3.8.2",
162172
"joi": "^17.13.3",
163173
"json-schema-to-ts": "^3.1.1",
164174
"superstruct": "^2.0.2",

pnpm-lock.yaml

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

src/lib/adapters/adapters.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export type ValidationLibrary =
3030
| 'zod'
3131
| 'vine'
3232
| 'schemasafe'
33-
| 'superstruct';
33+
| 'superstruct'
34+
| 'effect';
3435

3536
export type AdapterOptions<T> = {
3637
jsonSchema?: JSONSchema;

src/lib/adapters/effect.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Schema, JSONSchema, ArrayFormatter } from '@effect/schema';
2+
import { Either } from 'effect';
3+
import type { JSONSchema as TJSONSchema } from '../jsonSchema/index.js';
4+
import {
5+
createAdapter,
6+
type AdapterOptions,
7+
type ClientValidationAdapter,
8+
type Infer,
9+
type InferIn,
10+
type ValidationAdapter,
11+
type ValidationResult
12+
} from './adapters.js';
13+
import type { ParseOptions } from '@effect/schema/AST';
14+
import { memoize } from '$lib/memoize.js';
15+
16+
export const effectToJSONSchema = <A, I>(schema: Schema.Schema<A, I>) => {
17+
// effect's json schema type is slightly different so we have to cast it
18+
return JSONSchema.make(schema) as TJSONSchema;
19+
};
20+
21+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
22+
type AnySchema = Schema.Schema<any, any>;
23+
24+
async function validate<T extends AnySchema>(
25+
schema: T,
26+
data: unknown,
27+
options?: AdapterOptions<Infer<T>> & { parseOptions?: ParseOptions }
28+
): Promise<ValidationResult<Infer<T>>> {
29+
const result = Schema.decodeUnknownEither(schema, { errors: 'all' })(data, options?.parseOptions);
30+
if (Either.isRight(result)) {
31+
return {
32+
data: result.right as Infer<T>,
33+
success: true
34+
};
35+
}
36+
return {
37+
// get rid of the _tag property
38+
issues: ArrayFormatter.formatErrorSync(result.left).map(({ message, path }) => ({
39+
message,
40+
path: [...path] // path is readonly array so we have to copy it
41+
})),
42+
success: false
43+
} satisfies ValidationResult<Infer<T>>;
44+
}
45+
46+
function _effect<T extends AnySchema>(
47+
schema: T,
48+
options?: AdapterOptions<Infer<T>> & { parseOptions?: ParseOptions }
49+
): ValidationAdapter<Infer<T>, InferIn<T>> {
50+
// @ts-expect-error idk why this happens, it seems to happen in other adapters too
51+
return createAdapter({
52+
superFormValidationLibrary: 'effect',
53+
validate: async (data) => validate(schema, data, options),
54+
jsonSchema: options?.jsonSchema ?? effectToJSONSchema(schema),
55+
defaults: options?.defaults
56+
});
57+
}
58+
59+
function _effectClient<T extends AnySchema>(
60+
schema: T,
61+
options?: AdapterOptions<Infer<T>> & { parseOptions?: ParseOptions }
62+
): ClientValidationAdapter<Infer<T>, InferIn<T>> {
63+
return {
64+
superFormValidationLibrary: 'effect',
65+
validate: async (data) => validate(schema, data, options)
66+
};
67+
}
68+
69+
export const effect = /* @__PURE__ */ memoize(_effect);
70+
export const effectClient = /* @__PURE__ */ memoize(_effectClient);

src/lib/adapters/typeSchema.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { ZodSchema, input, output } from 'zod';
1818
import type { SchemaTypes, Infer as VineInfer } from '@vinejs/vine/types';
1919
import type { FromSchema, JSONSchema } from 'json-schema-to-ts';
2020
import type { Struct, Infer as Infer$2 } from 'superstruct';
21+
import type { Schema as Schema$1 } from '@effect/schema/Schema';
2122

2223
/*
2324
import type { SchemaObject } from 'ajv';
@@ -156,6 +157,15 @@ interface SuperstructResolver extends Resolver {
156157
output: this['schema'] extends Struct<any, any> ? Infer$2<this['schema']> : never;
157158
}
158159

160+
interface EffectResolver extends Resolver {
161+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
162+
base: Schema$1<any>;
163+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
164+
input: this['schema'] extends Schema$1<any> ? Schema$1.Encoded<this['schema']> : never;
165+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
166+
output: this['schema'] extends Schema$1<any> ? Schema$1.Type<this['schema']> : never;
167+
}
168+
159169
/*
160170
interface AjvResolver extends Resolver {
161171
base: SchemaObject;
@@ -165,12 +175,6 @@ interface DeepkitResolver extends Resolver {
165175
base: Type$1;
166176
}
167177
168-
interface EffectResolver extends Resolver {
169-
base: Schema$1<any>;
170-
input: this['schema'] extends Schema$1<any> ? Schema$1.From<this['schema']> : never;
171-
output: this['schema'] extends Schema$1<any> ? Schema$1.To<this['schema']> : never;
172-
}
173-
174178
interface IoTsResolver extends Resolver {
175179
base: Any;
176180
input: this['schema'] extends Any ? OutputOf<this['schema']> : never;
@@ -203,10 +207,10 @@ type Registry = {
203207
vine: VineResolver;
204208
schemasafe: SchemasafeResolver<JSONSchema>;
205209
superstruct: SuperstructResolver;
210+
effect: EffectResolver;
206211
/*
207212
ajv: AjvResolver;
208213
deepkit: DeepkitResolver;
209-
effect: EffectResolver;
210214
'io-ts': IoTsResolver;
211215
ow: OwResolver;
212216
runtypes: RuntypesResolver;

src/tests/superValidate.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ import {
7878

7979
import { schemasafe } from '$lib/adapters/schemasafe.js';
8080

81+
import { effect } from '$lib/adapters/effect.js';
82+
import { Schema } from '@effect/schema';
83+
8184
import { traversePath } from '$lib/traversal.js';
8285
import { splitPath } from '$lib/stringPath.js';
8386
import { SchemaError, type JSONSchema } from '$lib/index.js';
@@ -864,6 +867,33 @@ describe('superstruct', () => {
864867
schemaTest(adapter, undefined, 'simple');
865868
});
866869

870+
describe('Effect', async () => {
871+
// effect deliberately does not provide email parsing out of the box
872+
// https://github.com/Effect-TS/schema/issues/294
873+
// i just found this regex online, does the job
874+
const emailRegex = /^[^@]+@[^@]+\.[^@]+$/;
875+
const schema = Schema.Struct({
876+
name: Schema.String.annotations({ default: 'Unknown' }),
877+
email: Schema.String.pipe(
878+
Schema.filter((s) => emailRegex.test(s) || 'must be a valid email', {
879+
jsonSchema: {}
880+
})
881+
),
882+
tags: Schema.Array(Schema.String.pipe(Schema.minLength(2))).pipe(Schema.minItems(3)),
883+
score: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(0)),
884+
date: Schema.DateFromSelf.annotations({
885+
jsonSchema: {
886+
type: 'date'
887+
}
888+
}).pipe(Schema.optional),
889+
nospace: Schema.String.pipe(Schema.pattern(nospacePattern), Schema.optional),
890+
extra: Schema.String.pipe(Schema.NullOr).annotations({ default: null })
891+
});
892+
893+
const adapter = effect(schema);
894+
schemaTest(adapter);
895+
});
896+
867897
///// Common ////////////////////////////////////////////////////////
868898

869899
describe('Schema In/Out transformations', () => {

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"sourceMap": true,
1313
"strict": true,
1414
"module": "NodeNext",
15-
"moduleResolution": "NodeNext"
15+
"moduleResolution": "NodeNext",
16+
"exactOptionalPropertyTypes": true
1617
}
1718
}

0 commit comments

Comments
 (0)