foldkit 0.104.1 → 0.105.0
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.
- package/dist/fieldValidation/fieldValidation.d.ts +125 -0
- package/dist/fieldValidation/fieldValidation.d.ts.map +1 -0
- package/dist/fieldValidation/fieldValidation.js +127 -0
- package/dist/fieldValidation/index.d.ts +2 -120
- package/dist/fieldValidation/index.d.ts.map +1 -1
- package/dist/fieldValidation/index.js +2 -144
- package/dist/fieldValidation/public.d.ts +3 -2
- package/dist/fieldValidation/public.d.ts.map +1 -1
- package/dist/fieldValidation/public.js +2 -1
- package/dist/fieldValidation/rule.d.ts +46 -0
- package/dist/fieldValidation/rule.d.ts.map +1 -0
- package/dist/fieldValidation/rule.js +77 -0
- package/dist/file/select.d.ts +9 -4
- package/dist/file/select.d.ts.map +1 -1
- package/dist/file/select.js +8 -3
- package/dist/test/apps/resumeUpload.d.ts +3 -3
- package/dist/test/apps/resumeUpload.d.ts.map +1 -1
- package/dist/test/apps/resumeUpload.js +13 -16
- package/package.json +2 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Array, Option, Schema as S } from 'effect';
|
|
2
|
+
import { type Rule, type RuleMessage } from './rule.js';
|
|
3
|
+
/** The `NotValidated` state: user hasn't interacted yet. */
|
|
4
|
+
export type NotValidated<A> = Readonly<{
|
|
5
|
+
_tag: 'NotValidated';
|
|
6
|
+
value: A;
|
|
7
|
+
}>;
|
|
8
|
+
/** The `Validating` state: async validation is in flight. */
|
|
9
|
+
export type Validating<A> = Readonly<{
|
|
10
|
+
_tag: 'Validating';
|
|
11
|
+
value: A;
|
|
12
|
+
}>;
|
|
13
|
+
/** The `Valid` state: every rule passed. */
|
|
14
|
+
export type Valid<A> = Readonly<{
|
|
15
|
+
_tag: 'Valid';
|
|
16
|
+
value: A;
|
|
17
|
+
}>;
|
|
18
|
+
/** The `Invalid` state: one or more rules failed. Carries a non-empty `errors` array. */
|
|
19
|
+
export type Invalid<A> = Readonly<{
|
|
20
|
+
_tag: 'Invalid';
|
|
21
|
+
value: A;
|
|
22
|
+
errors: Array.NonEmptyReadonlyArray<string>;
|
|
23
|
+
}>;
|
|
24
|
+
/** The four-state union that represents a field's value in the Model. */
|
|
25
|
+
export type Field<A> = NotValidated<A> | Validating<A> | Valid<A> | Invalid<A>;
|
|
26
|
+
/** Constructs a `NotValidated` state. */
|
|
27
|
+
export declare const NotValidated: <A>(field: Readonly<{
|
|
28
|
+
value: A;
|
|
29
|
+
}>) => NotValidated<A>;
|
|
30
|
+
/** Constructs a `Validating` state. */
|
|
31
|
+
export declare const Validating: <A>(field: Readonly<{
|
|
32
|
+
value: A;
|
|
33
|
+
}>) => Validating<A>;
|
|
34
|
+
/** Constructs a `Valid` state. */
|
|
35
|
+
export declare const Valid: <A>(field: Readonly<{
|
|
36
|
+
value: A;
|
|
37
|
+
}>) => Valid<A>;
|
|
38
|
+
/** Constructs an `Invalid` state. */
|
|
39
|
+
export declare const Invalid: <A>(field: Readonly<{
|
|
40
|
+
value: A;
|
|
41
|
+
errors: Array.NonEmptyReadonlyArray<string>;
|
|
42
|
+
}>) => Invalid<A>;
|
|
43
|
+
/** Builds the four-state `Field` Schema for a value of the given Schema. Put the
|
|
44
|
+
* result in your Model. The value Schema should match what the control
|
|
45
|
+
* actually holds as the user edits, not the type you parse it into:
|
|
46
|
+
* `Field(S.String)` for text inputs, `Field(S.Array(S.String))` for a
|
|
47
|
+
* multi-select. A scalar like a checkbox's boolean usually stays plain
|
|
48
|
+
* `S.Boolean` in the Model; wrap it in `Field` only when it needs the
|
|
49
|
+
* validation lifecycle. Validation rules stay separate, in a `makeRules`
|
|
50
|
+
* bundle. */
|
|
51
|
+
export declare const Field: <A, I>(valueSchema: S.Codec<A, I>) => S.Union<readonly [S.TaggedStruct<"NotValidated", {
|
|
52
|
+
readonly value: S.Codec<A, I, never, never>;
|
|
53
|
+
}>, S.TaggedStruct<"Validating", {
|
|
54
|
+
readonly value: S.Codec<A, I, never, never>;
|
|
55
|
+
}>, S.TaggedStruct<"Valid", {
|
|
56
|
+
readonly value: S.Codec<A, I, never, never>;
|
|
57
|
+
}>, S.TaggedStruct<"Invalid", {
|
|
58
|
+
readonly value: S.Codec<A, I, never, never>;
|
|
59
|
+
readonly errors: S.NonEmptyArray<S.String>;
|
|
60
|
+
}>]>;
|
|
61
|
+
/** A field's validation rules: the required message (if any), the list of rules,
|
|
62
|
+
* and an empty predicate. Produced by `makeRules`; consumed by the module's
|
|
63
|
+
* operations (`validate`, `validateAll`, `isValid`, `isRequired`). The fields
|
|
64
|
+
* are accessible but treating them as stable is discouraged. Prefer the
|
|
65
|
+
* operations so internal shape changes don't break callers. */
|
|
66
|
+
export type Rules<A> = Readonly<{
|
|
67
|
+
requiredMessage: Option.Option<RuleMessage<A>>;
|
|
68
|
+
rules: ReadonlyArray<Rule<A>>;
|
|
69
|
+
isEmpty: (value: A) => boolean;
|
|
70
|
+
}>;
|
|
71
|
+
/** Options accepted by `makeRules`. */
|
|
72
|
+
export type MakeRulesOptions<A> = Readonly<{
|
|
73
|
+
/** When present, the field is required: empty values become `Invalid`
|
|
74
|
+
* with the given message, and `isValid` requires `Valid`. Absent
|
|
75
|
+
* means the field is optional: empty values stay `NotValidated`, and
|
|
76
|
+
* `isValid` accepts `Valid` or `NotValidated`. */
|
|
77
|
+
required?: RuleMessage<A>;
|
|
78
|
+
rules?: ReadonlyArray<Rule<A>>;
|
|
79
|
+
/** Predicate for what counts as "empty" for this field. Defaults to empty
|
|
80
|
+
* string and empty array; every other value is treated as present. Pass
|
|
81
|
+
* `(value) => value.trim() === ''` to treat whitespace-only input as empty. */
|
|
82
|
+
isEmpty?: (value: A) => boolean;
|
|
83
|
+
}>;
|
|
84
|
+
/** Creates a `Rules` bundle from options. The value type defaults to `string`;
|
|
85
|
+
* for other field values, annotate it: `makeRules<ReadonlyArray<Tag>>({ ... })`. */
|
|
86
|
+
export declare const makeRules: <A = string>(options?: MakeRulesOptions<A>) => Rules<A>;
|
|
87
|
+
/** Validates a new value and returns the next field state.
|
|
88
|
+
*
|
|
89
|
+
* For required fields, an empty value produces `Invalid` with the
|
|
90
|
+
* required message. For optional fields, an empty value produces
|
|
91
|
+
* `NotValidated`. Non-empty values run through the field's rules;
|
|
92
|
+
* the first failure becomes `Invalid`, otherwise the result is `Valid`. */
|
|
93
|
+
export declare const validate: <A>(rules: Rules<A>) => (value: A) => Field<A>;
|
|
94
|
+
/** Like `validate` but collects every failing rule into the
|
|
95
|
+
* `Invalid` state's errors array instead of stopping at the first. */
|
|
96
|
+
export declare const validateAll: <A>(rules: Rules<A>) => (value: A) => Field<A>;
|
|
97
|
+
/** Returns true when the field's current state is acceptable given its
|
|
98
|
+
* rules. For required fields, only `Valid` returns `true`. For optional
|
|
99
|
+
* fields, `Valid` or `NotValidated` both return `true`. `Invalid` and
|
|
100
|
+
* `Validating` always return `false`.
|
|
101
|
+
*
|
|
102
|
+
* The name is distinct from the `Valid` tag on purpose: `isValid`
|
|
103
|
+
* answers "is this state an acceptable result?", which for an optional
|
|
104
|
+
* field is broader than `_tag === 'Valid'`. For pattern-matching on the
|
|
105
|
+
* state itself, check the `_tag` directly. */
|
|
106
|
+
export declare const isValid: <A>(rules: Rules<A>) => (state: Field<A>) => boolean;
|
|
107
|
+
/** Returns true when the rules mark the field as required. Useful for
|
|
108
|
+
* rendering affordances like a `*` next to required field labels. */
|
|
109
|
+
export declare const isRequired: <A>(rules: Rules<A>) => boolean;
|
|
110
|
+
/** Returns true when the state's tag is `Invalid`. Tag-only predicate;
|
|
111
|
+
* unlike `!isValid(rules)(state)`, this does not treat `NotValidated`
|
|
112
|
+
* or `Validating` as errors. Use for "has the user seen a rule failure
|
|
113
|
+
* on this field?" affordances like red borders or per-step error
|
|
114
|
+
* indicators. */
|
|
115
|
+
export declare const isInvalid: (state: Field<unknown>) => boolean;
|
|
116
|
+
/** Returns true when every field is acceptable per its rules, by `isValid`.
|
|
117
|
+
* Each pair is a field's `[state, rules]`. The pairs in one call share a
|
|
118
|
+
* value type, so a call gates fields of a single type; for a form that mixes
|
|
119
|
+
* types, call `allValid` once per type and combine the results with `&&`.
|
|
120
|
+
* Use for form-level submit gates. */
|
|
121
|
+
export declare const allValid: <A>(pairs: ReadonlyArray<readonly [Field<A>, Rules<A>]>) => boolean;
|
|
122
|
+
/** Returns true when any state in the input has tag `Invalid`. Use for
|
|
123
|
+
* "this step/section has errors" affordances, independent of rules. */
|
|
124
|
+
export declare const anyInvalid: (states: ReadonlyArray<Field<unknown>>) => boolean;
|
|
125
|
+
//# sourceMappingURL=fieldValidation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldValidation.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/fieldValidation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Array, Option, Result, Schema as S, String, pipe } from 'effect';
|
|
2
|
+
import { resolveMessage } from './rule.js';
|
|
3
|
+
/** Constructs a `NotValidated` state. */
|
|
4
|
+
export const NotValidated = (field) => ({
|
|
5
|
+
_tag: 'NotValidated',
|
|
6
|
+
value: field.value,
|
|
7
|
+
});
|
|
8
|
+
/** Constructs a `Validating` state. */
|
|
9
|
+
export const Validating = (field) => ({
|
|
10
|
+
_tag: 'Validating',
|
|
11
|
+
value: field.value,
|
|
12
|
+
});
|
|
13
|
+
/** Constructs a `Valid` state. */
|
|
14
|
+
export const Valid = (field) => ({
|
|
15
|
+
_tag: 'Valid',
|
|
16
|
+
value: field.value,
|
|
17
|
+
});
|
|
18
|
+
/** Constructs an `Invalid` state. */
|
|
19
|
+
export const Invalid = (field) => ({
|
|
20
|
+
_tag: 'Invalid',
|
|
21
|
+
value: field.value,
|
|
22
|
+
errors: field.errors,
|
|
23
|
+
});
|
|
24
|
+
/** Builds the four-state `Field` Schema for a value of the given Schema. Put the
|
|
25
|
+
* result in your Model. The value Schema should match what the control
|
|
26
|
+
* actually holds as the user edits, not the type you parse it into:
|
|
27
|
+
* `Field(S.String)` for text inputs, `Field(S.Array(S.String))` for a
|
|
28
|
+
* multi-select. A scalar like a checkbox's boolean usually stays plain
|
|
29
|
+
* `S.Boolean` in the Model; wrap it in `Field` only when it needs the
|
|
30
|
+
* validation lifecycle. Validation rules stay separate, in a `makeRules`
|
|
31
|
+
* bundle. */
|
|
32
|
+
export const Field = (valueSchema) => S.Union([
|
|
33
|
+
S.TaggedStruct('NotValidated', { value: valueSchema }),
|
|
34
|
+
S.TaggedStruct('Validating', { value: valueSchema }),
|
|
35
|
+
S.TaggedStruct('Valid', { value: valueSchema }),
|
|
36
|
+
S.TaggedStruct('Invalid', {
|
|
37
|
+
value: valueSchema,
|
|
38
|
+
errors: S.NonEmptyArray(S.String),
|
|
39
|
+
}),
|
|
40
|
+
]);
|
|
41
|
+
const isEmptyValue = (value) => {
|
|
42
|
+
if (typeof value === 'string') {
|
|
43
|
+
return String.isEmpty(value);
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return Array.isReadonlyArrayEmpty(value);
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
/** Creates a `Rules` bundle from options. The value type defaults to `string`;
|
|
51
|
+
* for other field values, annotate it: `makeRules<ReadonlyArray<Tag>>({ ... })`. */
|
|
52
|
+
export const makeRules = (options = {}) => ({
|
|
53
|
+
requiredMessage: Option.fromNullishOr(options.required),
|
|
54
|
+
rules: options.rules ?? [],
|
|
55
|
+
isEmpty: options.isEmpty ?? isEmptyValue,
|
|
56
|
+
});
|
|
57
|
+
// OPERATIONS
|
|
58
|
+
/** Validates a new value and returns the next field state.
|
|
59
|
+
*
|
|
60
|
+
* For required fields, an empty value produces `Invalid` with the
|
|
61
|
+
* required message. For optional fields, an empty value produces
|
|
62
|
+
* `NotValidated`. Non-empty values run through the field's rules;
|
|
63
|
+
* the first failure becomes `Invalid`, otherwise the result is `Valid`. */
|
|
64
|
+
export const validate = (rules) => (value) => {
|
|
65
|
+
if (rules.isEmpty(value)) {
|
|
66
|
+
return Option.match(rules.requiredMessage, {
|
|
67
|
+
onNone: () => NotValidated({ value }),
|
|
68
|
+
onSome: message => Invalid({ value, errors: [resolveMessage(message, value)] }),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return pipe(rules.rules, Array.findFirst(([predicate]) => !predicate(value)), Option.match({
|
|
72
|
+
onNone: () => Valid({ value }),
|
|
73
|
+
onSome: ([, message]) => Invalid({ value, errors: [resolveMessage(message, value)] }),
|
|
74
|
+
}));
|
|
75
|
+
};
|
|
76
|
+
/** Like `validate` but collects every failing rule into the
|
|
77
|
+
* `Invalid` state's errors array instead of stopping at the first. */
|
|
78
|
+
export const validateAll = (rules) => (value) => {
|
|
79
|
+
if (rules.isEmpty(value)) {
|
|
80
|
+
return Option.match(rules.requiredMessage, {
|
|
81
|
+
onNone: () => NotValidated({ value }),
|
|
82
|
+
onSome: message => Invalid({ value, errors: [resolveMessage(message, value)] }),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return pipe(rules.rules, Array.filterMap(([predicate, message]) => !predicate(value)
|
|
86
|
+
? Result.succeed(resolveMessage(message, value))
|
|
87
|
+
: Result.failVoid), Array.match({
|
|
88
|
+
onEmpty: () => Valid({ value }),
|
|
89
|
+
onNonEmpty: errors => Invalid({ value, errors }),
|
|
90
|
+
}));
|
|
91
|
+
};
|
|
92
|
+
/** Returns true when the field's current state is acceptable given its
|
|
93
|
+
* rules. For required fields, only `Valid` returns `true`. For optional
|
|
94
|
+
* fields, `Valid` or `NotValidated` both return `true`. `Invalid` and
|
|
95
|
+
* `Validating` always return `false`.
|
|
96
|
+
*
|
|
97
|
+
* The name is distinct from the `Valid` tag on purpose: `isValid`
|
|
98
|
+
* answers "is this state an acceptable result?", which for an optional
|
|
99
|
+
* field is broader than `_tag === 'Valid'`. For pattern-matching on the
|
|
100
|
+
* state itself, check the `_tag` directly. */
|
|
101
|
+
export const isValid = (rules) => (state) => {
|
|
102
|
+
if (state._tag === 'Invalid' || state._tag === 'Validating') {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (Option.isSome(rules.requiredMessage)) {
|
|
106
|
+
return state._tag === 'Valid';
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
};
|
|
110
|
+
/** Returns true when the rules mark the field as required. Useful for
|
|
111
|
+
* rendering affordances like a `*` next to required field labels. */
|
|
112
|
+
export const isRequired = (rules) => Option.isSome(rules.requiredMessage);
|
|
113
|
+
/** Returns true when the state's tag is `Invalid`. Tag-only predicate;
|
|
114
|
+
* unlike `!isValid(rules)(state)`, this does not treat `NotValidated`
|
|
115
|
+
* or `Validating` as errors. Use for "has the user seen a rule failure
|
|
116
|
+
* on this field?" affordances like red borders or per-step error
|
|
117
|
+
* indicators. */
|
|
118
|
+
export const isInvalid = (state) => state._tag === 'Invalid';
|
|
119
|
+
/** Returns true when every field is acceptable per its rules, by `isValid`.
|
|
120
|
+
* Each pair is a field's `[state, rules]`. The pairs in one call share a
|
|
121
|
+
* value type, so a call gates fields of a single type; for a form that mixes
|
|
122
|
+
* types, call `allValid` once per type and combine the results with `&&`.
|
|
123
|
+
* Use for form-level submit gates. */
|
|
124
|
+
export const allValid = (pairs) => Array.every(pairs, ([state, rules]) => isValid(rules)(state));
|
|
125
|
+
/** Returns true when any state in the input has tag `Invalid`. Use for
|
|
126
|
+
* "this step/section has errors" affordances, independent of rules. */
|
|
127
|
+
export const anyInvalid = (states) => Array.some(states, isInvalid);
|
|
@@ -1,121 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export type RuleMessage = string | ((value: string) => string);
|
|
4
|
-
/** A tuple of a predicate and error message used for field validation. */
|
|
5
|
-
export type Rule = readonly [Predicate.Predicate<string>, RuleMessage];
|
|
6
|
-
export declare const resolveMessage: (message: RuleMessage, value: string) => string;
|
|
7
|
-
/** The `NotValidated` state: user hasn't interacted yet. */
|
|
8
|
-
export declare const NotValidated: import("../schema/index.js").CallableTaggedStruct<"NotValidated", {
|
|
9
|
-
value: S.String;
|
|
10
|
-
}>;
|
|
11
|
-
/** The `Validating` state: async validation is in flight. */
|
|
12
|
-
export declare const Validating: import("../schema/index.js").CallableTaggedStruct<"Validating", {
|
|
13
|
-
value: S.String;
|
|
14
|
-
}>;
|
|
15
|
-
/** The `Valid` state: every rule passed. */
|
|
16
|
-
export declare const Valid: import("../schema/index.js").CallableTaggedStruct<"Valid", {
|
|
17
|
-
value: S.String;
|
|
18
|
-
}>;
|
|
19
|
-
/** The `Invalid` state: one or more rules failed. Carries a non-empty `errors` array. */
|
|
20
|
-
export declare const Invalid: import("../schema/index.js").CallableTaggedStruct<"Invalid", {
|
|
21
|
-
value: S.String;
|
|
22
|
-
errors: S.NonEmptyArray<S.String>;
|
|
23
|
-
}>;
|
|
24
|
-
/** The four-state union that represents a field's value in the Model. */
|
|
25
|
-
export declare const Field: S.Union<readonly [import("../schema/index.js").CallableTaggedStruct<"NotValidated", {
|
|
26
|
-
value: S.String;
|
|
27
|
-
}>, import("../schema/index.js").CallableTaggedStruct<"Validating", {
|
|
28
|
-
value: S.String;
|
|
29
|
-
}>, import("../schema/index.js").CallableTaggedStruct<"Valid", {
|
|
30
|
-
value: S.String;
|
|
31
|
-
}>, import("../schema/index.js").CallableTaggedStruct<"Invalid", {
|
|
32
|
-
value: S.String;
|
|
33
|
-
errors: S.NonEmptyArray<S.String>;
|
|
34
|
-
}>]>;
|
|
35
|
-
export type Field = typeof Field.Type;
|
|
36
|
-
/** A field's validation rules: the required message (if any), the list of rules,
|
|
37
|
-
* and an empty predicate. Produced by `makeRules`; consumed by the module's
|
|
38
|
-
* operations (`validate`, `validateAll`, `isValid`, `isRequired`, `allValid`).
|
|
39
|
-
* The fields are accessible but treating them as stable is discouraged.
|
|
40
|
-
* Prefer the operations so internal shape changes don't break callers. */
|
|
41
|
-
export type Rules = Readonly<{
|
|
42
|
-
requiredMessage: Option.Option<RuleMessage>;
|
|
43
|
-
rules: ReadonlyArray<Rule>;
|
|
44
|
-
isEmpty: (value: string) => boolean;
|
|
45
|
-
}>;
|
|
46
|
-
/** Options accepted by `makeRules`. */
|
|
47
|
-
export type MakeRulesOptions = Readonly<{
|
|
48
|
-
/** When present, the field is required: empty values become `Invalid`
|
|
49
|
-
* with the given message, and `isValid` requires `Valid`. Absent
|
|
50
|
-
* means the field is optional: empty values stay `NotValidated`, and
|
|
51
|
-
* `isValid` accepts `Valid` or `NotValidated`. */
|
|
52
|
-
required?: RuleMessage;
|
|
53
|
-
rules?: ReadonlyArray<Rule>;
|
|
54
|
-
/** Predicate for what counts as "empty" for this field. Defaults to
|
|
55
|
-
* `String.isEmpty` (the empty string only). Pass `(v) => v.trim() === ''`
|
|
56
|
-
* to treat whitespace-only input as empty. */
|
|
57
|
-
isEmpty?: (value: string) => boolean;
|
|
58
|
-
}>;
|
|
59
|
-
export declare const makeRules: (options?: MakeRulesOptions) => Rules;
|
|
60
|
-
/** Validates a new value and returns the next field state.
|
|
61
|
-
*
|
|
62
|
-
* For required fields, an empty value produces `Invalid` with the
|
|
63
|
-
* required message. For optional fields, an empty value produces
|
|
64
|
-
* `NotValidated`. Non-empty values run through the field's rules;
|
|
65
|
-
* the first failure becomes `Invalid`, otherwise the result is `Valid`. */
|
|
66
|
-
export declare const validate: (rules: Rules) => (value: string) => Field;
|
|
67
|
-
/** Like `validate` but collects every failing rule into the
|
|
68
|
-
* `Invalid` state's errors array instead of stopping at the first. */
|
|
69
|
-
export declare const validateAll: (rules: Rules) => (value: string) => Field;
|
|
70
|
-
/** Returns true when the field's current state is acceptable given its
|
|
71
|
-
* rules. For required fields, only `Valid` returns `true`. For optional
|
|
72
|
-
* fields, `Valid` or `NotValidated` both return `true`. `Invalid` and
|
|
73
|
-
* `Validating` always return `false`.
|
|
74
|
-
*
|
|
75
|
-
* The name is distinct from the `Valid` tag on purpose: `isValid`
|
|
76
|
-
* answers "is this state an acceptable result?", which for an optional
|
|
77
|
-
* field is broader than `_tag === 'Valid'`. For pattern-matching on the
|
|
78
|
-
* state itself, check the `_tag` directly. */
|
|
79
|
-
export declare const isValid: (rules: Rules) => (state: Field) => boolean;
|
|
80
|
-
/** Returns true when the rules mark the field as required. Useful for
|
|
81
|
-
* rendering affordances like a `*` next to required field labels. */
|
|
82
|
-
export declare const isRequired: (rules: Rules) => boolean;
|
|
83
|
-
/** Returns true when the state's tag is `Invalid`. Tag-only predicate;
|
|
84
|
-
* unlike `!isValid(rules)(state)`, this does not treat `NotValidated`
|
|
85
|
-
* or `Validating` as errors. Use for "has the user seen a rule failure
|
|
86
|
-
* on this field?" affordances like red borders or per-step error
|
|
87
|
-
* indicators. */
|
|
88
|
-
export declare const isInvalid: (state: Field) => boolean;
|
|
89
|
-
/** Returns true when every `(state, rules)` pair in the input is
|
|
90
|
-
* acceptable per `isValid`. Use for form-level submit gates. */
|
|
91
|
-
export declare const allValid: (pairs: ReadonlyArray<readonly [Field, Rules]>) => boolean;
|
|
92
|
-
/** Returns true when any state in the input has tag `Invalid`. Use for
|
|
93
|
-
* "this step/section has errors" affordances, independent of rules. */
|
|
94
|
-
export declare const anyInvalid: (states: ReadonlyArray<Field>) => boolean;
|
|
95
|
-
/** Creates a `Rule` that checks if a string meets a minimum length. */
|
|
96
|
-
export declare const minLength: (min: number, message?: RuleMessage) => Rule;
|
|
97
|
-
/** Creates a `Rule` that checks if a string does not exceed a maximum length. */
|
|
98
|
-
export declare const maxLength: (max: number, message?: RuleMessage) => Rule;
|
|
99
|
-
/** Creates a `Rule` that checks if a string matches a regular expression. */
|
|
100
|
-
export declare const pattern: (regex: RegExp, message?: RuleMessage) => Rule;
|
|
101
|
-
/** Creates a `Rule` that checks if a string is a valid email format. */
|
|
102
|
-
export declare const email: (message?: RuleMessage) => Rule;
|
|
103
|
-
/** Creates a `Rule` that checks if a string is a valid URL format.
|
|
104
|
-
*
|
|
105
|
-
* By default the URL must include an `http://` or `https://` protocol.
|
|
106
|
-
* Pass `{ requireProtocol: false }` to accept bare domains. */
|
|
107
|
-
export declare const url: (options?: Readonly<{
|
|
108
|
-
message?: RuleMessage;
|
|
109
|
-
requireProtocol?: boolean;
|
|
110
|
-
}>) => Rule;
|
|
111
|
-
/** Creates a `Rule` that checks if a string begins with a specified prefix. */
|
|
112
|
-
export declare const startsWith: (prefix: string, message?: RuleMessage) => Rule;
|
|
113
|
-
/** Creates a `Rule` that checks if a string ends with a specified suffix. */
|
|
114
|
-
export declare const endsWith: (suffix: string, message?: RuleMessage) => Rule;
|
|
115
|
-
/** Creates a `Rule` that checks if a string contains a specified substring. */
|
|
116
|
-
export declare const includes: (substring: string, message?: RuleMessage) => Rule;
|
|
117
|
-
/** Creates a `Rule` that checks if a string exactly matches an expected value. */
|
|
118
|
-
export declare const equals: (expected: string, message?: RuleMessage) => Rule;
|
|
119
|
-
/** Creates a `Rule` that checks if a string is one of a specified set of allowed values. */
|
|
120
|
-
export declare const oneOf: (values: ReadonlyArray<string>, message?: RuleMessage) => Rule;
|
|
1
|
+
export * from './fieldValidation.js';
|
|
2
|
+
export * as Rule from './rule.js';
|
|
121
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AAEpC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA"}
|
|
@@ -1,144 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const resolveMessage = (message, value) => typeof message === 'string' ? message : message(value);
|
|
4
|
-
// STATE
|
|
5
|
-
/** The `NotValidated` state: user hasn't interacted yet. */
|
|
6
|
-
export const NotValidated = ts('NotValidated', { value: S.String });
|
|
7
|
-
/** The `Validating` state: async validation is in flight. */
|
|
8
|
-
export const Validating = ts('Validating', { value: S.String });
|
|
9
|
-
/** The `Valid` state: every rule passed. */
|
|
10
|
-
export const Valid = ts('Valid', { value: S.String });
|
|
11
|
-
/** The `Invalid` state: one or more rules failed. Carries a non-empty `errors` array. */
|
|
12
|
-
export const Invalid = ts('Invalid', {
|
|
13
|
-
value: S.String,
|
|
14
|
-
errors: S.NonEmptyArray(S.String),
|
|
15
|
-
});
|
|
16
|
-
/** The four-state union that represents a field's value in the Model. */
|
|
17
|
-
export const Field = S.Union([NotValidated, Validating, Valid, Invalid]);
|
|
18
|
-
export const makeRules = (options = {}) => ({
|
|
19
|
-
requiredMessage: Option.fromNullishOr(options.required),
|
|
20
|
-
rules: options.rules ?? [],
|
|
21
|
-
isEmpty: options.isEmpty ?? String.isEmpty,
|
|
22
|
-
});
|
|
23
|
-
// OPERATIONS
|
|
24
|
-
/** Validates a new value and returns the next field state.
|
|
25
|
-
*
|
|
26
|
-
* For required fields, an empty value produces `Invalid` with the
|
|
27
|
-
* required message. For optional fields, an empty value produces
|
|
28
|
-
* `NotValidated`. Non-empty values run through the field's rules;
|
|
29
|
-
* the first failure becomes `Invalid`, otherwise the result is `Valid`. */
|
|
30
|
-
export const validate = (rules) => (value) => {
|
|
31
|
-
if (rules.isEmpty(value)) {
|
|
32
|
-
return Option.match(rules.requiredMessage, {
|
|
33
|
-
onNone: () => NotValidated({ value }),
|
|
34
|
-
onSome: message => Invalid({ value, errors: [resolveMessage(message, value)] }),
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
return pipe(rules.rules, Array.findFirst(([predicate]) => !predicate(value)), Option.match({
|
|
38
|
-
onNone: () => Valid({ value }),
|
|
39
|
-
onSome: ([, message]) => Invalid({ value, errors: [resolveMessage(message, value)] }),
|
|
40
|
-
}));
|
|
41
|
-
};
|
|
42
|
-
/** Like `validate` but collects every failing rule into the
|
|
43
|
-
* `Invalid` state's errors array instead of stopping at the first. */
|
|
44
|
-
export const validateAll = (rules) => (value) => {
|
|
45
|
-
if (rules.isEmpty(value)) {
|
|
46
|
-
return Option.match(rules.requiredMessage, {
|
|
47
|
-
onNone: () => NotValidated({ value }),
|
|
48
|
-
onSome: message => Invalid({ value, errors: [resolveMessage(message, value)] }),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return pipe(rules.rules, Array.filterMap(([predicate, message]) => !predicate(value)
|
|
52
|
-
? Result.succeed(resolveMessage(message, value))
|
|
53
|
-
: Result.failVoid), Array.match({
|
|
54
|
-
onEmpty: () => Valid({ value }),
|
|
55
|
-
onNonEmpty: errors => Invalid({ value, errors }),
|
|
56
|
-
}));
|
|
57
|
-
};
|
|
58
|
-
/** Returns true when the field's current state is acceptable given its
|
|
59
|
-
* rules. For required fields, only `Valid` returns `true`. For optional
|
|
60
|
-
* fields, `Valid` or `NotValidated` both return `true`. `Invalid` and
|
|
61
|
-
* `Validating` always return `false`.
|
|
62
|
-
*
|
|
63
|
-
* The name is distinct from the `Valid` tag on purpose: `isValid`
|
|
64
|
-
* answers "is this state an acceptable result?", which for an optional
|
|
65
|
-
* field is broader than `_tag === 'Valid'`. For pattern-matching on the
|
|
66
|
-
* state itself, check the `_tag` directly. */
|
|
67
|
-
export const isValid = (rules) => (state) => {
|
|
68
|
-
if (state._tag === 'Invalid' || state._tag === 'Validating') {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
if (Option.isSome(rules.requiredMessage)) {
|
|
72
|
-
return state._tag === 'Valid';
|
|
73
|
-
}
|
|
74
|
-
return true;
|
|
75
|
-
};
|
|
76
|
-
/** Returns true when the rules mark the field as required. Useful for
|
|
77
|
-
* rendering affordances like a `*` next to required field labels. */
|
|
78
|
-
export const isRequired = (rules) => Option.isSome(rules.requiredMessage);
|
|
79
|
-
/** Returns true when the state's tag is `Invalid`. Tag-only predicate;
|
|
80
|
-
* unlike `!isValid(rules)(state)`, this does not treat `NotValidated`
|
|
81
|
-
* or `Validating` as errors. Use for "has the user seen a rule failure
|
|
82
|
-
* on this field?" affordances like red borders or per-step error
|
|
83
|
-
* indicators. */
|
|
84
|
-
export const isInvalid = (state) => state._tag === 'Invalid';
|
|
85
|
-
/** Returns true when every `(state, rules)` pair in the input is
|
|
86
|
-
* acceptable per `isValid`. Use for form-level submit gates. */
|
|
87
|
-
export const allValid = (pairs) => Array.every(pairs, ([state, rules]) => isValid(rules)(state));
|
|
88
|
-
/** Returns true when any state in the input has tag `Invalid`. Use for
|
|
89
|
-
* "this step/section has errors" affordances, independent of rules. */
|
|
90
|
-
export const anyInvalid = (states) => Array.some(states, isInvalid);
|
|
91
|
-
// STRING RULES
|
|
92
|
-
/** Creates a `Rule` that checks if a string meets a minimum length. */
|
|
93
|
-
export const minLength = (min, message) => [
|
|
94
|
-
flow(String.length, Number_.isGreaterThanOrEqualTo(min)),
|
|
95
|
-
message ?? `Must be at least ${min} characters`,
|
|
96
|
-
];
|
|
97
|
-
/** Creates a `Rule` that checks if a string does not exceed a maximum length. */
|
|
98
|
-
export const maxLength = (max, message) => [
|
|
99
|
-
flow(String.length, Number_.isLessThanOrEqualTo(max)),
|
|
100
|
-
message ?? `Must be at most ${max} characters`,
|
|
101
|
-
];
|
|
102
|
-
/** Creates a `Rule` that checks if a string matches a regular expression. */
|
|
103
|
-
export const pattern = (regex, message = 'Invalid format') => [flow(String.match(regex), Option.isSome), message];
|
|
104
|
-
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
105
|
-
/** Creates a `Rule` that checks if a string is a valid email format. */
|
|
106
|
-
export const email = (message = 'Invalid email address') => pattern(EMAIL_REGEX, message);
|
|
107
|
-
const STRICT_URL_REGEX = /^https?:\/\/.+/;
|
|
108
|
-
const PERMISSIVE_URL_REGEX = /^(https?:\/\/)?\S+\.\S+$/;
|
|
109
|
-
/** Creates a `Rule` that checks if a string is a valid URL format.
|
|
110
|
-
*
|
|
111
|
-
* By default the URL must include an `http://` or `https://` protocol.
|
|
112
|
-
* Pass `{ requireProtocol: false }` to accept bare domains. */
|
|
113
|
-
export const url = (options = {}) => {
|
|
114
|
-
const { message = 'Invalid URL', requireProtocol = true } = options;
|
|
115
|
-
return pattern(requireProtocol ? STRICT_URL_REGEX : PERMISSIVE_URL_REGEX, message);
|
|
116
|
-
};
|
|
117
|
-
/** Creates a `Rule` that checks if a string begins with a specified prefix. */
|
|
118
|
-
export const startsWith = (prefix, message) => [
|
|
119
|
-
flow(String.startsWith(prefix)),
|
|
120
|
-
message ?? `Must start with ${prefix}`,
|
|
121
|
-
];
|
|
122
|
-
/** Creates a `Rule` that checks if a string ends with a specified suffix. */
|
|
123
|
-
export const endsWith = (suffix, message) => [
|
|
124
|
-
flow(String.endsWith(suffix)),
|
|
125
|
-
message ?? `Must end with ${suffix}`,
|
|
126
|
-
];
|
|
127
|
-
/** Creates a `Rule` that checks if a string contains a specified substring. */
|
|
128
|
-
export const includes = (substring, message) => [
|
|
129
|
-
flow(String.includes(substring)),
|
|
130
|
-
message ?? `Must contain ${substring}`,
|
|
131
|
-
];
|
|
132
|
-
/** Creates a `Rule` that checks if a string exactly matches an expected value. */
|
|
133
|
-
export const equals = (expected, message) => [
|
|
134
|
-
value => value === expected,
|
|
135
|
-
message ?? `Must match ${expected}`,
|
|
136
|
-
];
|
|
137
|
-
/** Creates a `Rule` that checks if a string is one of a specified set of allowed values. */
|
|
138
|
-
export const oneOf = (values, message) => {
|
|
139
|
-
const joinedValues = Array.join(values, ', ');
|
|
140
|
-
return [
|
|
141
|
-
value => Array.contains(values, value),
|
|
142
|
-
message ?? `Must be one of: ${joinedValues}`,
|
|
143
|
-
];
|
|
144
|
-
};
|
|
1
|
+
export * from './fieldValidation.js';
|
|
2
|
+
export * as Rule from './rule.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { makeRules, validate, validateAll, isValid, isInvalid, isRequired, allValid, anyInvalid,
|
|
2
|
-
export type { Rules, MakeRulesOptions
|
|
1
|
+
export { makeRules, validate, validateAll, isValid, isInvalid, isRequired, allValid, anyInvalid, NotValidated, Validating, Valid, Invalid, Field, } from './index.js';
|
|
2
|
+
export type { Rules, MakeRulesOptions } from './index.js';
|
|
3
|
+
export * as Rule from './rule.js';
|
|
3
4
|
//# sourceMappingURL=public.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,QAAQ,EACR,WAAW,EACX,OAAO,EACP,SAAS,EACT,UAAU,EACV,QAAQ,EACR,UAAU,EACV,
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,QAAQ,EACR,WAAW,EACX,OAAO,EACP,SAAS,EACT,UAAU,EACV,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,UAAU,EACV,KAAK,EACL,OAAO,EACP,KAAK,GACN,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAEzD,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA"}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { makeRules, validate, validateAll, isValid, isInvalid, isRequired, allValid, anyInvalid,
|
|
1
|
+
export { makeRules, validate, validateAll, isValid, isInvalid, isRequired, allValid, anyInvalid, NotValidated, Validating, Valid, Invalid, Field, } from './index.js';
|
|
2
|
+
export * as Rule from './rule.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Predicate, Schema as S } from 'effect';
|
|
2
|
+
/** An error message for a rule: either a static string, or a function that receives the invalid value. */
|
|
3
|
+
export type RuleMessage<A> = string | ((value: A) => string);
|
|
4
|
+
/** A tuple of a predicate and error message used for field validation. */
|
|
5
|
+
export type Rule<A> = readonly [Predicate.Predicate<A>, RuleMessage<A>];
|
|
6
|
+
/** Resolves a `RuleMessage` to a concrete string, applying it to the value when the message is a function. */
|
|
7
|
+
export declare const resolveMessage: <A>(message: RuleMessage<A>, value: A) => string;
|
|
8
|
+
/** Creates a `Rule` that checks if a string meets a minimum length. */
|
|
9
|
+
export declare const minLength: (min: number, message?: RuleMessage<string>) => Rule<string>;
|
|
10
|
+
/** Creates a `Rule` that checks if a string does not exceed a maximum length. */
|
|
11
|
+
export declare const maxLength: (max: number, message?: RuleMessage<string>) => Rule<string>;
|
|
12
|
+
/** Creates a `Rule` that checks if a string matches a regular expression. */
|
|
13
|
+
export declare const pattern: (regex: RegExp, message?: RuleMessage<string>) => Rule<string>;
|
|
14
|
+
/** Creates a `Rule` that checks if a string is a valid email format. */
|
|
15
|
+
export declare const email: (message?: RuleMessage<string>) => Rule<string>;
|
|
16
|
+
/** Creates a `Rule` that checks if a string is a valid URL format.
|
|
17
|
+
*
|
|
18
|
+
* By default the URL must include an `http://` or `https://` protocol.
|
|
19
|
+
* Pass `{ requireProtocol: false }` to accept bare domains. */
|
|
20
|
+
export declare const url: (options?: Readonly<{
|
|
21
|
+
message?: RuleMessage<string>;
|
|
22
|
+
requireProtocol?: boolean;
|
|
23
|
+
}>) => Rule<string>;
|
|
24
|
+
/** Creates a `Rule` that checks if a string begins with a specified prefix. */
|
|
25
|
+
export declare const startsWith: (prefix: string, message?: RuleMessage<string>) => Rule<string>;
|
|
26
|
+
/** Creates a `Rule` that checks if a string ends with a specified suffix. */
|
|
27
|
+
export declare const endsWith: (suffix: string, message?: RuleMessage<string>) => Rule<string>;
|
|
28
|
+
/** Creates a `Rule` that checks if a string contains a specified substring. */
|
|
29
|
+
export declare const includes: (substring: string, message?: RuleMessage<string>) => Rule<string>;
|
|
30
|
+
/** Creates a `Rule` that checks if a string exactly matches an expected value. */
|
|
31
|
+
export declare const equals: (expected: string, message?: RuleMessage<string>) => Rule<string>;
|
|
32
|
+
/** Creates a `Rule` that checks if a string is one of a specified set of allowed values. */
|
|
33
|
+
export declare const oneOf: (values: ReadonlyArray<string>, message?: RuleMessage<string>) => Rule<string>;
|
|
34
|
+
/** Creates a `Rule` that checks an array holds at least `min` items. */
|
|
35
|
+
export declare const minItems: (min: number, message?: RuleMessage<ReadonlyArray<unknown>>) => Rule<ReadonlyArray<unknown>>;
|
|
36
|
+
/** Creates a `Rule` that checks an array holds at most `max` items. */
|
|
37
|
+
export declare const maxItems: (max: number, message?: RuleMessage<ReadonlyArray<unknown>>) => Rule<ReadonlyArray<unknown>>;
|
|
38
|
+
/** Creates a `Rule` that passes when the value decodes through `schema`.
|
|
39
|
+
*
|
|
40
|
+
* Use it to reuse a Schema you already maintain (a domain codec, a refined or
|
|
41
|
+
* branded type you decode to on submit) as a field rule, so the rule stays in
|
|
42
|
+
* sync with the schema instead of duplicating its logic. It does nothing a
|
|
43
|
+
* custom rule can't, so prefer the dedicated rules for plain checks. Decoding
|
|
44
|
+
* is synchronous: the schema must decode without running an effect. */
|
|
45
|
+
export declare const fromSchema: <A, I>(schema: S.Codec<A, I>, message: RuleMessage<I>) => Rule<I>;
|
|
46
|
+
//# sourceMappingURL=rule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.d.ts","sourceRoot":"","sources":["../../src/fieldValidation/rule.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Array, Number as Number_, Option, Schema as S, String, flow, } from 'effect';
|
|
2
|
+
/** Resolves a `RuleMessage` to a concrete string, applying it to the value when the message is a function. */
|
|
3
|
+
export const resolveMessage = (message, value) => typeof message === 'string' ? message : message(value);
|
|
4
|
+
// STRING RULES
|
|
5
|
+
/** Creates a `Rule` that checks if a string meets a minimum length. */
|
|
6
|
+
export const minLength = (min, message) => [
|
|
7
|
+
flow(String.length, Number_.isGreaterThanOrEqualTo(min)),
|
|
8
|
+
message ?? `Must be at least ${min} characters`,
|
|
9
|
+
];
|
|
10
|
+
/** Creates a `Rule` that checks if a string does not exceed a maximum length. */
|
|
11
|
+
export const maxLength = (max, message) => [
|
|
12
|
+
flow(String.length, Number_.isLessThanOrEqualTo(max)),
|
|
13
|
+
message ?? `Must be at most ${max} characters`,
|
|
14
|
+
];
|
|
15
|
+
/** Creates a `Rule` that checks if a string matches a regular expression. */
|
|
16
|
+
export const pattern = (regex, message = 'Invalid format') => [flow(String.match(regex), Option.isSome), message];
|
|
17
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
18
|
+
/** Creates a `Rule` that checks if a string is a valid email format. */
|
|
19
|
+
export const email = (message = 'Invalid email address') => pattern(EMAIL_REGEX, message);
|
|
20
|
+
const STRICT_URL_REGEX = /^https?:\/\/.+/;
|
|
21
|
+
const PERMISSIVE_URL_REGEX = /^(https?:\/\/)?\S+\.\S+$/;
|
|
22
|
+
/** Creates a `Rule` that checks if a string is a valid URL format.
|
|
23
|
+
*
|
|
24
|
+
* By default the URL must include an `http://` or `https://` protocol.
|
|
25
|
+
* Pass `{ requireProtocol: false }` to accept bare domains. */
|
|
26
|
+
export const url = (options = {}) => {
|
|
27
|
+
const { message = 'Invalid URL', requireProtocol = true } = options;
|
|
28
|
+
return pattern(requireProtocol ? STRICT_URL_REGEX : PERMISSIVE_URL_REGEX, message);
|
|
29
|
+
};
|
|
30
|
+
/** Creates a `Rule` that checks if a string begins with a specified prefix. */
|
|
31
|
+
export const startsWith = (prefix, message) => [
|
|
32
|
+
String.startsWith(prefix),
|
|
33
|
+
message ?? `Must start with ${prefix}`,
|
|
34
|
+
];
|
|
35
|
+
/** Creates a `Rule` that checks if a string ends with a specified suffix. */
|
|
36
|
+
export const endsWith = (suffix, message) => [
|
|
37
|
+
String.endsWith(suffix),
|
|
38
|
+
message ?? `Must end with ${suffix}`,
|
|
39
|
+
];
|
|
40
|
+
/** Creates a `Rule` that checks if a string contains a specified substring. */
|
|
41
|
+
export const includes = (substring, message) => [
|
|
42
|
+
String.includes(substring),
|
|
43
|
+
message ?? `Must contain ${substring}`,
|
|
44
|
+
];
|
|
45
|
+
/** Creates a `Rule` that checks if a string exactly matches an expected value. */
|
|
46
|
+
export const equals = (expected, message) => [
|
|
47
|
+
value => value === expected,
|
|
48
|
+
message ?? `Must match ${expected}`,
|
|
49
|
+
];
|
|
50
|
+
/** Creates a `Rule` that checks if a string is one of a specified set of allowed values. */
|
|
51
|
+
export const oneOf = (values, message) => {
|
|
52
|
+
const joinedValues = Array.join(values, ', ');
|
|
53
|
+
return [
|
|
54
|
+
value => Array.contains(values, value),
|
|
55
|
+
message ?? `Must be one of: ${joinedValues}`,
|
|
56
|
+
];
|
|
57
|
+
};
|
|
58
|
+
// ARRAY RULES
|
|
59
|
+
/** Creates a `Rule` that checks an array holds at least `min` items. */
|
|
60
|
+
export const minItems = (min, message) => [
|
|
61
|
+
flow(Array.length, Number_.isGreaterThanOrEqualTo(min)),
|
|
62
|
+
message ?? `Must select at least ${min}`,
|
|
63
|
+
];
|
|
64
|
+
/** Creates a `Rule` that checks an array holds at most `max` items. */
|
|
65
|
+
export const maxItems = (max, message) => [
|
|
66
|
+
flow(Array.length, Number_.isLessThanOrEqualTo(max)),
|
|
67
|
+
message ?? `Must select at most ${max}`,
|
|
68
|
+
];
|
|
69
|
+
// SCHEMA RULES
|
|
70
|
+
/** Creates a `Rule` that passes when the value decodes through `schema`.
|
|
71
|
+
*
|
|
72
|
+
* Use it to reuse a Schema you already maintain (a domain codec, a refined or
|
|
73
|
+
* branded type you decode to on submit) as a field rule, so the rule stays in
|
|
74
|
+
* sync with the schema instead of duplicating its logic. It does nothing a
|
|
75
|
+
* custom rule can't, so prefer the dedicated rules for plain checks. Decoding
|
|
76
|
+
* is synchronous: the schema must decode without running an effect. */
|
|
77
|
+
export const fromSchema = (schema, message) => [flow(S.decodeOption(schema), Option.isSome), message];
|
package/dist/file/select.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Effect } from 'effect';
|
|
1
|
+
import { Effect, Option } from 'effect';
|
|
2
2
|
import type { File } from './file.js';
|
|
3
3
|
/**
|
|
4
4
|
* Opens the native file picker allowing a single file to be selected. Resolves
|
|
5
|
-
* with
|
|
5
|
+
* with `Option.some(file)` if the user picked one, or `Option.none()` if they
|
|
6
6
|
* cancelled. Mirrors Elm's `File.Select.file`.
|
|
7
7
|
*
|
|
8
8
|
* The `accept` argument is a list of MIME types or file extensions that
|
|
@@ -12,12 +12,17 @@ import type { File } from './file.js';
|
|
|
12
12
|
* ```typescript
|
|
13
13
|
* SelectResume(
|
|
14
14
|
* File.select(['application/pdf']).pipe(
|
|
15
|
-
* Effect.map(
|
|
15
|
+
* Effect.map(
|
|
16
|
+
* Option.match({
|
|
17
|
+
* onNone: () => CancelledSelectResume(),
|
|
18
|
+
* onSome: file => SelectedResume({ file }),
|
|
19
|
+
* }),
|
|
20
|
+
* ),
|
|
16
21
|
* ),
|
|
17
22
|
* )
|
|
18
23
|
* ```
|
|
19
24
|
*/
|
|
20
|
-
export declare const select: (accept: ReadonlyArray<string>) => Effect.Effect<
|
|
25
|
+
export declare const select: (accept: ReadonlyArray<string>) => Effect.Effect<Option.Option<File>>;
|
|
21
26
|
/**
|
|
22
27
|
* Opens the native file picker allowing multiple files to be selected at
|
|
23
28
|
* once. Resolves with the array of selected files, or an empty array if the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/file/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/file/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AA2CrC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,MAAM,GACjB,QAAQ,aAAa,CAAC,MAAM,CAAC,KAC5B,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CACkC,CAAA;AAEtE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,aAAa,CAAC,MAAM,CAAC,KAC5B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAA2C,CAAA"}
|
package/dist/file/select.js
CHANGED
|
@@ -27,7 +27,7 @@ const openPicker = ({ accept, multiple, }) => Effect.callback((resume, signal) =
|
|
|
27
27
|
});
|
|
28
28
|
/**
|
|
29
29
|
* Opens the native file picker allowing a single file to be selected. Resolves
|
|
30
|
-
* with
|
|
30
|
+
* with `Option.some(file)` if the user picked one, or `Option.none()` if they
|
|
31
31
|
* cancelled. Mirrors Elm's `File.Select.file`.
|
|
32
32
|
*
|
|
33
33
|
* The `accept` argument is a list of MIME types or file extensions that
|
|
@@ -37,12 +37,17 @@ const openPicker = ({ accept, multiple, }) => Effect.callback((resume, signal) =
|
|
|
37
37
|
* ```typescript
|
|
38
38
|
* SelectResume(
|
|
39
39
|
* File.select(['application/pdf']).pipe(
|
|
40
|
-
* Effect.map(
|
|
40
|
+
* Effect.map(
|
|
41
|
+
* Option.match({
|
|
42
|
+
* onNone: () => CancelledSelectResume(),
|
|
43
|
+
* onSome: file => SelectedResume({ file }),
|
|
44
|
+
* }),
|
|
45
|
+
* ),
|
|
41
46
|
* ),
|
|
42
47
|
* )
|
|
43
48
|
* ```
|
|
44
49
|
*/
|
|
45
|
-
export const select = (accept) => openPicker({ accept, multiple: false });
|
|
50
|
+
export const select = (accept) => openPicker({ accept, multiple: false }).pipe(Effect.map(Array.head));
|
|
46
51
|
/**
|
|
47
52
|
* Opens the native file picker allowing multiple files to be selected at
|
|
48
53
|
* once. Resolves with the array of selected files, or an empty array if the
|
|
@@ -9,7 +9,7 @@ export declare const Model: S.Struct<{
|
|
|
9
9
|
export type Model = typeof Model.Type;
|
|
10
10
|
export declare const ClickedChooseResume: import("../../schema/index.js").CallableTaggedStruct<"ClickedChooseResume", {}>;
|
|
11
11
|
export declare const SelectedResume: import("../../schema/index.js").CallableTaggedStruct<"SelectedResume", {
|
|
12
|
-
|
|
12
|
+
file: S.Schema<File>;
|
|
13
13
|
}>;
|
|
14
14
|
export declare const CancelledSelectResume: import("../../schema/index.js").CallableTaggedStruct<"CancelledSelectResume", {}>;
|
|
15
15
|
export declare const SucceededReadPreview: import("../../schema/index.js").CallableTaggedStruct<"SucceededReadPreview", {
|
|
@@ -18,7 +18,7 @@ export declare const SucceededReadPreview: import("../../schema/index.js").Calla
|
|
|
18
18
|
export declare const FailedReadPreview: import("../../schema/index.js").CallableTaggedStruct<"FailedReadPreview", {}>;
|
|
19
19
|
export declare const ClickedRemoveResume: import("../../schema/index.js").CallableTaggedStruct<"ClickedRemoveResume", {}>;
|
|
20
20
|
export declare const Message: S.Union<readonly [import("../../schema/index.js").CallableTaggedStruct<"ClickedChooseResume", {}>, import("../../schema/index.js").CallableTaggedStruct<"SelectedResume", {
|
|
21
|
-
|
|
21
|
+
file: S.Schema<File>;
|
|
22
22
|
}>, import("../../schema/index.js").CallableTaggedStruct<"CancelledSelectResume", {}>, import("../../schema/index.js").CallableTaggedStruct<"SucceededReadPreview", {
|
|
23
23
|
dataUrl: S.String;
|
|
24
24
|
}>, import("../../schema/index.js").CallableTaggedStruct<"FailedReadPreview", {}>, import("../../schema/index.js").CallableTaggedStruct<"ClickedRemoveResume", {}>]>;
|
|
@@ -27,7 +27,7 @@ export declare const SelectResume: Command.CommandDefinitionNoArgs<"SelectResume
|
|
|
27
27
|
readonly _tag: "CancelledSelectResume";
|
|
28
28
|
} | {
|
|
29
29
|
readonly _tag: "SelectedResume";
|
|
30
|
-
readonly
|
|
30
|
+
readonly file: File;
|
|
31
31
|
}, never, never>>;
|
|
32
32
|
export declare const ReadResumePreview: Command.CommandDefinitionWithArgs<"ReadResumePreview", {
|
|
33
33
|
file: S.Schema<File>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resumeUpload.d.ts","sourceRoot":"","sources":["../../../src/test/apps/resumeUpload.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"resumeUpload.d.ts","sourceRoot":"","sources":["../../../src/test/apps/resumeUpload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAsB,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhE,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AAEjD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAMrD,eAAO,MAAM,KAAK;;;;EAIhB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,cAAc;;EAEzB,CAAA;AACF,eAAO,MAAM,qBAAqB,mFAA6B,CAAA;AAC/D,eAAO,MAAM,oBAAoB;;EAE/B,CAAA;AACF,eAAO,MAAM,iBAAiB,+EAAyB,CAAA;AACvD,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAE3D,eAAO,MAAM,OAAO;;;;oKAOlB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,eAAO,MAAM,YAAY;;;;;iBAaxB,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;;;;iBAU7B,CAAA;AAID,eAAO,MAAM,YAAY,EAAE,KAI1B,CAAA;AAID,KAAK,YAAY,GAAG,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AAE7E,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,EAAE,SAAS,OAAO,KAAG,YA+BrD,CAAA;AAwBH,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAqBnC,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Effect, Match as M, Option, Schema as S } from 'effect';
|
|
2
2
|
import * as Command from '../../command/index.js';
|
|
3
3
|
import * as File from '../../file/index.js';
|
|
4
4
|
import { html } from '../../html/index.js';
|
|
@@ -13,7 +13,7 @@ export const Model = S.Struct({
|
|
|
13
13
|
// MESSAGE
|
|
14
14
|
export const ClickedChooseResume = m('ClickedChooseResume');
|
|
15
15
|
export const SelectedResume = m('SelectedResume', {
|
|
16
|
-
|
|
16
|
+
file: File.File,
|
|
17
17
|
});
|
|
18
18
|
export const CancelledSelectResume = m('CancelledSelectResume');
|
|
19
19
|
export const SucceededReadPreview = m('SucceededReadPreview', {
|
|
@@ -30,9 +30,9 @@ export const Message = S.Union([
|
|
|
30
30
|
ClickedRemoveResume,
|
|
31
31
|
]);
|
|
32
32
|
// COMMAND
|
|
33
|
-
export const SelectResume = Command.define('SelectResume', SelectedResume, CancelledSelectResume)(File.select(['application/pdf']).pipe(Effect.map(
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
export const SelectResume = Command.define('SelectResume', SelectedResume, CancelledSelectResume)(File.select(['application/pdf']).pipe(Effect.map(Option.match({
|
|
34
|
+
onNone: () => CancelledSelectResume(),
|
|
35
|
+
onSome: file => SelectedResume({ file }),
|
|
36
36
|
}))));
|
|
37
37
|
export const ReadResumePreview = Command.define('ReadResumePreview', { file: File.File }, SucceededReadPreview, FailedReadPreview)(({ file }) => File.readAsDataUrl(file).pipe(Effect.map(dataUrl => SucceededReadPreview({ dataUrl })), Effect.catch(() => Effect.succeed(FailedReadPreview()))));
|
|
38
38
|
// INIT
|
|
@@ -43,17 +43,14 @@ export const initialModel = {
|
|
|
43
43
|
};
|
|
44
44
|
export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
|
|
45
45
|
ClickedChooseResume: () => [model, [SelectResume()]],
|
|
46
|
-
SelectedResume: ({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
[ReadResumePreview({ file: firstFile })],
|
|
55
|
-
],
|
|
56
|
-
}),
|
|
46
|
+
SelectedResume: ({ file }) => [
|
|
47
|
+
evo(model, {
|
|
48
|
+
maybeResume: () => Option.some(file),
|
|
49
|
+
maybePreviewDataUrl: () => Option.none(),
|
|
50
|
+
readStatus: () => 'Reading',
|
|
51
|
+
}),
|
|
52
|
+
[ReadResumePreview({ file })],
|
|
53
|
+
],
|
|
57
54
|
CancelledSelectResume: () => [model, []],
|
|
58
55
|
SucceededReadPreview: ({ dataUrl }) => [
|
|
59
56
|
evo(model, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foldkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.105.0",
|
|
4
4
|
"description": "A TypeScript frontend framework, built on Effect and architected like Elm",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -200,6 +200,7 @@
|
|
|
200
200
|
"import": "./dist/devTools/public.js"
|
|
201
201
|
}
|
|
202
202
|
},
|
|
203
|
+
"sideEffects": false,
|
|
203
204
|
"files": [
|
|
204
205
|
"dist"
|
|
205
206
|
],
|