Skip to content

Commit

Permalink
feat: added deep nested check for $validate type output
Browse files Browse the repository at this point in the history
  • Loading branch information
victorgarciaesgi committed Dec 11, 2024
1 parent 0e775fe commit 22bdf48
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 44 deletions.
85 changes: 49 additions & 36 deletions packages/core/src/types/core/useRegle.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ import type { MaybeRef } from 'vue';
import type {
CustomRulesDeclarationTree,
RegleCollectionRuleDecl,
RegleCollectionRuleDefinition,
RegleFormPropertyType,
ReglePartialRuleTree,
RegleRoot,
RegleRuleDecl,
RegleRuleDefinition,
} from '../rules';
import type { ExtendOnlyRealRecord, ExtractFromGetter, Maybe, Prettify } from '../utils';
import type { ArrayElement, ExtendOnlyRealRecord, ExtractFromGetter, Maybe, Prettify } from '../utils';
import type { RegleShortcutDefinition, RegleValidationGroupEntry } from './options.types';

export interface Regle<
Expand Down Expand Up @@ -50,21 +49,24 @@ export type DeepReactiveState<T extends Record<string, any>> = {

export type DeepSafeFormState<
TState extends Record<string, any>,
TRules extends ReglePartialRuleTree<TState, CustomRulesDeclarationTree>,
TRules extends ReglePartialRuleTree<TState, CustomRulesDeclarationTree> | undefined,
> = [unknown] extends [TState]
? {}
: Prettify<
{
[K in keyof TState as IsPropertyOutputRequired<TState[K], TRules[K]> extends false ? K : never]?: SafeProperty<
TState[K],
TRules[K]
>;
} & {
[K in keyof TState as IsPropertyOutputRequired<TState[K], TRules[K]> extends false ? never : K]-?: NonNullable<
SafeProperty<TState[K], TRules[K]>
>;
}
>;
: TRules extends undefined
? TState
: TRules extends ReglePartialRuleTree<TState, CustomRulesDeclarationTree>
? Prettify<
{
[K in keyof TState as IsPropertyOutputRequired<TState[K], TRules[K]> extends false
? K
: never]?: SafeProperty<TState[K], TRules[K]>;
} & {
[K in keyof TState as IsPropertyOutputRequired<TState[K], TRules[K]> extends false
? never
: K]-?: NonNullable<SafeProperty<TState[K], TRules[K]>>;
}
>
: TState;

type FieldHaveRequiredRule<TRule extends RegleRuleDecl> = unknown extends TRule['required']
? false
Expand All @@ -76,25 +78,33 @@ type FieldHaveRequiredRule<TRule extends RegleRuleDecl> = unknown extends TRule[
: false
: false;

type ObjectHaveAtLeastOneRequiredField<TState, TRule extends ReglePartialRuleTree<any, any>> =
type ObjectHaveAtLeastOneRequiredField<
TState extends Record<string, any>,
TRule extends ReglePartialRuleTree<TState, any>,
> =
TState extends Maybe<TState>
? {
[K in keyof TRule]: TRule[K] extends RegleRuleDecl ? FieldHaveRequiredRule<TRule[K]> : false;
}[keyof TRule]
[K in keyof NonNullable<TState>]-?: IsPropertyOutputRequired<NonNullable<TState>[K], TRule[K]>;
}[keyof TState] extends false
? false
: true
: true;

type ArrayHaveAtLeastOneRequiredField<TState, TRule extends RegleCollectionRuleDefinition<any, any>> =
type ArrayHaveAtLeastOneRequiredField<TState extends Maybe<any[]>, TRule extends RegleCollectionRuleDecl<TState>> =
TState extends Maybe<TState>
? {
[K in keyof ExtractFromGetter<TRule['$each']>]: ExtractFromGetter<TRule['$each']>[K] extends RegleRuleDecl
? FieldHaveRequiredRule<ExtractFromGetter<TRule['$each']>[K]>
: false;
}[keyof ExtractFromGetter<TRule['$each']>]
?
| FieldHaveRequiredRule<Omit<TRule, '$each'> extends RegleRuleDecl ? Omit<TRule, '$each'> : {}>
| ObjectHaveAtLeastOneRequiredField<
ArrayElement<NonNullable<TState>>,
ExtractFromGetter<TRule['$each']> extends undefined ? {} : NonNullable<ExtractFromGetter<TRule['$each']>>
> extends false
? false
: true
: true;

export type SafeProperty<TState, TRule extends RegleFormPropertyType<any, any> | undefined> = [unknown] extends [TState]
? unknown
: TRule extends RegleCollectionRuleDefinition<any, any>
: TRule extends RegleCollectionRuleDecl<any, any>
? TState extends Array<infer U extends Record<string, any>>
? DeepSafeFormState<U, ExtractFromGetter<TRule['$each']>>[]
: TState
Expand All @@ -112,21 +122,24 @@ export type IsPropertyOutputRequired<TState, TRule extends RegleFormPropertyType
unknown,
] extends [TState]
? unknown
: TRule extends RegleCollectionRuleDefinition<any, any>
? TState extends Array<any>
? ArrayHaveAtLeastOneRequiredField<TState, TRule> extends true
? true
: false
: NonNullable<TState> extends Array<any>
? TRule extends RegleCollectionRuleDecl<any, any>
? ArrayHaveAtLeastOneRequiredField<NonNullable<TState>, TRule> extends false
? false
: true
: false
: TRule extends ReglePartialRuleTree<any, any>
? ExtendOnlyRealRecord<TState> extends true
? ObjectHaveAtLeastOneRequiredField<TState, TRule> extends true
? true
: false
? ObjectHaveAtLeastOneRequiredField<
NonNullable<TState> extends Record<string, any> ? NonNullable<TState> : {},
TRule
> extends false
? false
: true
: TRule extends RegleRuleDecl<any, any>
? FieldHaveRequiredRule<TRule> extends true
? true
: false
? FieldHaveRequiredRule<TRule> extends false
? false
: true
: false
: false;

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types/rules/rule.declaration.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export type RegleRuleDecl<
* @internal
* @reference {@link RegleRuleDecl}
*/
export type $InternalRegleRuleDecl = Record<string, FormRuleDeclaration<any, any>>;
export type $InternalRegleRuleDecl = FieldRegleBehaviourOptions & Record<string, FormRuleDeclaration<any, any>>;

/**
* @public
Expand Down
36 changes: 33 additions & 3 deletions packages/nuxt/test/fixtures/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
<template>
<div>{{ r$.$fields.name.$value }}</div>
<div>{{ r$.$fields.fullName.$value }}</div>
</template>

<script setup lang="ts">
import { required } from '@regle/rules';
import { minLength, required, email, dateBefore, and, checked } from '@regle/rules';
const { r$ } = useRegle({ name: 'hello' }, { name: { required: withMessage(required, 'foo') } });
interface Form {
fullName?: string;
email?: string;
eventDate?: Date;
eventType?: string;
details?: string;
acceptTC?: boolean;
}
const { r$ } = useRegle({ fullName: 'Hello' } as Form, {
fullName: { required, minLength: minLength(6) },
email: { required, email },
eventDate: {
required,
dateBefore: withTooltip(dateBefore(new Date()), ({ $dirty }) => {
if (!$dirty) return 'You must put a date before today';
return '';
}),
},
eventType: { required },
details: {
minLength: withMessage(
minLength(100),
({ $value, $params: [min] }) => `Your details are too short: ${$value?.length}/${min}`
),
},
acceptTC: {
$autoDirty: false,
required: withMessage(and(required, checked), 'You must accept the terms and conditions'),
},
});
</script>

<style lang="scss" scoped></style>
2 changes: 1 addition & 1 deletion packages/nuxt/test/nuxt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ describe('ssr', async () => {

it('renders the index page', async () => {
const html = await $fetch('/');
expect(html).toContain('<div>hello</div>');
expect(html).toContain('<div>Hello</div>');
});
});
12 changes: 9 additions & 3 deletions tests/unit/createRule/createRule.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createRule, type RegleRuleDefinition } from '@regle/core';
import { createRule, type RegleRuleDefinition, type RegleRuleWithParamsDefinition } from '@regle/core';

describe('createRule', () => {
it('should error when creating a rule without a function', () => {
Expand Down Expand Up @@ -69,7 +69,7 @@ describe('createRule', () => {
message: '',
});

expect(await rule.exec('fooo')).toBe(false);
expect(rule.exec('fooo')).toBe(false);
});

it('should recognize mutliple parameters with default', async () => {
Expand All @@ -83,11 +83,15 @@ describe('createRule', () => {
expect(rule().exec('fooo')).toBe(true);
expect(rule(true).exec('fooo')).toBe(true);
expect(rule(true, true).exec('fooo')).toBe(true);

expectTypeOf(rule).toEqualTypeOf<
RegleRuleWithParamsDefinition<unknown, [param?: any, param2?: any], false, true>
>();
});

it('should recognize mutliple parameters with spread', async () => {
const rule = createRule({
validator(value, ...params: any[]) {
validator(value, ...params: boolean[]) {
return true;
},
message: '',
Expand All @@ -96,5 +100,7 @@ describe('createRule', () => {
expect(rule().exec('fooo')).toBe(true);
expect(rule(true).exec('fooo')).toBe(true);
expect(rule(true, true).exec('fooo')).toBe(true);

expectTypeOf(rule).toEqualTypeOf<RegleRuleWithParamsDefinition<unknown, boolean[], false, true>>();
});
});
10 changes: 10 additions & 0 deletions tests/unit/useRegle/validate/$validate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function simpleNestedStateWithMixedValidation() {
lastName?: string;
};
contacts?: [{ name: string }];
collection?: [{ name: string }];
}

return useRegle({} as Form, {
Expand All @@ -23,6 +24,9 @@ function simpleNestedStateWithMixedValidation() {
name: { required },
},
},
collection: {
required,
},
});
}

Expand All @@ -44,6 +48,9 @@ describe('$validate', () => {
contacts: {
name: string;
}[];
collection: {
name: string;
}[];
}>();
} else {
expectTypeOf(data).toEqualTypeOf<{
Expand All @@ -56,6 +63,9 @@ describe('$validate', () => {
contacts?: {
name?: Maybe<string>;
}[];
collection?: {
name?: Maybe<string>;
}[];
}>();
}
});
Expand Down

0 comments on commit 22bdf48

Please sign in to comment.