@wwog/react 1.3.8 → 1.3.9

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/README.md CHANGED
@@ -477,6 +477,96 @@ Development notes: Internally implemented via `mediaQuery`, it does not listen t
477
477
 
478
478
  > Internal functions used by some components, which can also be used if needed
479
479
 
480
+ #### `ruleChecker` (v1.3.9+)
481
+
482
+ A type-safe data validation utility that provides comprehensive validation rules for different data types with full TypeScript support.
483
+
484
+ ```tsx
485
+ import { ruleChecker } from "@wwog/react";
486
+
487
+ // Define your data and validation rules
488
+ const userData = {
489
+ username: 'john',
490
+ email: 'john@example.com',
491
+ age: 25,
492
+ hobbies: ['reading', 'coding']
493
+ };
494
+
495
+ const validationRules = {
496
+ username: { required: true, min: 3, max: 20 },
497
+ email: { required: true, email: true },
498
+ age: { required: true, min: 18, max: 120 },
499
+ hobbies: { min: 1, max: 5, unique: true }
500
+ };
501
+
502
+ // Validate the data
503
+ const result = ruleChecker(userData, validationRules);
504
+
505
+ if (result.valid) {
506
+ console.log('Data is valid:', result.data);
507
+ } else {
508
+ console.log('Validation errors:', result.errors);
509
+ console.log('Field-specific errors:', result.fieldErrors);
510
+ }
511
+ ```
512
+
513
+ **Features:**
514
+ - **Type-safe**: Full TypeScript support with automatic type inference
515
+ - **Multiple data types**: Support for strings, numbers, booleans, and arrays
516
+ - **Comprehensive rules**: Built-in validation for length, range, format, uniqueness, etc.
517
+ - **Custom validators**: Support for custom validation functions
518
+ - **Dependency validation**: Validate fields based on other field values
519
+ - **Array element validation**: Validate individual elements within arrays
520
+ - **Multiple rule support**: Apply multiple validation rules to a single field
521
+ - **Detailed error reporting**: Get both general errors and field-specific errors
522
+
523
+ **Available Rules:**
524
+ - **Common rules**: `required`, `message`, `validator`, `dependsOn`
525
+ - **String rules**: `min`, `max`, `len`, `regex`, `email`, `url`, `phone`
526
+ - **Number rules**: `min`, `max`
527
+ - **Array rules**: `min`, `max`, `len`, `unique`, `elementRule`
528
+ - **Boolean rules**: Basic validation with custom validators
529
+
530
+ **Complex Example:**
531
+ ```tsx
532
+ const registrationData = {
533
+ username: 'user123',
534
+ email: 'user@example.com',
535
+ password: 'SecurePass123',
536
+ confirmPassword: 'SecurePass123',
537
+ age: 25,
538
+ tags: ['developer', 'typescript'],
539
+ terms: true
540
+ };
541
+
542
+ const rules = {
543
+ username: { required: true, min: 3, max: 20, regex: /^[a-zA-Z0-9_]+$/ },
544
+ email: { required: true, email: true },
545
+ password: [
546
+ { required: true, min: 8 },
547
+ { regex: /[A-Z]/, message: 'Password must contain uppercase letter' },
548
+ { regex: /[0-9]/, message: 'Password must contain number' }
549
+ ],
550
+ confirmPassword: {
551
+ required: true,
552
+ validator: (value, data) => value === data.password || 'Passwords do not match'
553
+ },
554
+ age: { required: true, min: 18, max: 120 },
555
+ tags: {
556
+ min: 1,
557
+ max: 10,
558
+ unique: true,
559
+ elementRule: { min: 2, max: 20 } // Each tag must be 2-20 characters
560
+ },
561
+ terms: {
562
+ required: true,
563
+ validator: (value) => value === true || 'You must accept the terms'
564
+ }
565
+ };
566
+
567
+ const result = ruleChecker(registrationData, rules);
568
+ ```
569
+
480
570
  #### `createExternalState` (v1.2.9+, useGetter added in v1.2.13)
481
571
 
482
572
  > v1.2.21: Refactor the API to move sideeffects into options and enhance support for the transform interface
package/dist/index.d.mts CHANGED
@@ -716,8 +716,58 @@ type BreakpointName = (typeof breakpoints)[number];
716
716
  type BreakpointDesc = Partial<Record<BreakpointName, number>>;
717
717
  type Responsive<T> = T | Partial<Record<BreakpointName, T>>;
718
718
 
719
+ type BaseRule<TAll, TValue> = {
720
+ required?: boolean;
721
+ message?: string;
722
+ validator?: (value: TValue, data: Partial<TAll>) => boolean | string;
723
+ dependsOn?: (data: Partial<TAll>) => boolean | string;
724
+ };
725
+ type LengthRuleProps = {
726
+ min?: number;
727
+ max?: number;
728
+ len?: number;
729
+ };
730
+ type NumberRangeProps = {
731
+ min?: number;
732
+ max?: number;
733
+ };
734
+ type StringSpecificProps = {
735
+ regex?: RegExp;
736
+ email?: boolean;
737
+ url?: boolean;
738
+ phone?: boolean;
739
+ };
740
+ type ArraySpecificProps = {
741
+ unique?: boolean;
742
+ };
743
+ type NumberRule<TAll> = BaseRule<TAll, number> & NumberRangeProps;
744
+ type StringRule<TAll> = BaseRule<TAll, string> & LengthRuleProps & StringSpecificProps;
745
+ type BooleanRule<TAll> = BaseRule<TAll, boolean>;
746
+ type ArrayRule<TAll, U> = BaseRule<TAll, U[]> & LengthRuleProps & ArraySpecificProps & {
747
+ elementRule?: FieldRule<U, TAll>;
748
+ };
749
+ type FieldRule<TValue, TAll> = TValue extends string ? StringRule<TAll> : TValue extends number ? NumberRule<TAll> : TValue extends boolean ? BooleanRule<TAll> : TValue extends (infer U)[] ? ArrayRule<TAll, U> : BaseRule<TAll, TValue>;
750
+ type RuleDescription<T extends Record<string, unknown>> = {
751
+ [K in keyof T]?: FieldRule<T[K], T> | FieldRule<T[K], T>[];
752
+ };
753
+ type IsRequired<R> = R extends {
754
+ required: true;
755
+ } ? true : false;
756
+ type ApplyRules<T extends Record<string, unknown>, R extends RuleDescription<T>> = {
757
+ [K in keyof T]: IsRequired<R[K]> extends true ? T[K] : T[K] | undefined;
758
+ };
759
+ type FieldErrors<T> = Partial<Record<keyof T, string[]>>;
760
+ declare function ruleChecker<T extends Record<string, unknown>, R extends RuleDescription<T>>(data: Partial<T>, rules: R): {
761
+ valid: true;
762
+ data: ApplyRules<T, R>;
763
+ } | {
764
+ valid: false;
765
+ errors: string[];
766
+ fieldErrors: FieldErrors<T>;
767
+ };
768
+
719
769
  declare function getCurrentBreakpoint(breakpointDesc: BreakpointDesc, width: number): BreakpointName;
720
770
  declare function useScreen(breakpointDesc?: BreakpointDesc): "base" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
721
771
 
722
- export { ArrayRender, Counter, DateRender, DefBreakpointDesc, False, If, Observer, Pipe, Scope, SizeBox, Styles, Switch, Toggle, True, When, breakpoints, childrenLoop, createExternalState, createStorageState, cx, formatDate, getCurrentBreakpoint, safePromiseTry, useControlled, useScreen };
723
- export type { ArrayRenderProps, BreakpointDesc, BreakpointName, CreateStateListener, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalSideEffect, ExternalState, ExternalStateOptions, ExternalWithKernel, FalseProps, IfProps, ObserverProps, PipeProps, Responsive, ScopeProps, StorageStateOptions, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, Transform, TrueProps, UseControlledOptions, WhenProps };
772
+ export { ArrayRender, Counter, DateRender, DefBreakpointDesc, False, If, Observer, Pipe, Scope, SizeBox, Styles, Switch, Toggle, True, When, breakpoints, childrenLoop, createExternalState, createStorageState, cx, formatDate, getCurrentBreakpoint, ruleChecker, safePromiseTry, useControlled, useScreen };
773
+ export type { ApplyRules, ArrayRenderProps, ArrayRule, ArraySpecificProps, BaseRule, BooleanRule, BreakpointDesc, BreakpointName, CreateStateListener, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalSideEffect, ExternalState, ExternalStateOptions, ExternalWithKernel, FalseProps, FieldRule, IfProps, LengthRuleProps, NumberRangeProps, NumberRule, ObserverProps, PipeProps, Responsive, RuleDescription, ScopeProps, StorageStateOptions, StringRule, StringSpecificProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, Transform, TrueProps, UseControlledOptions, WhenProps };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import s,{useMemo as g,Fragment as S,Children as L,isValidElement as H,cloneElement as V,useEffect as w,useState as O,useRef as k,useCallback as Z}from"react";function Y(e,n){if(e===void 0)return;let t=0;if(Array.isArray(e)){for(const r of e)if(n(r,t++)===!1)break}else n(e,t)}const z=(e,n)=>e===n,v=e=>s.createElement(s.Fragment,null,e.children);v.displayName="Switch_Case";const N=e=>s.createElement(s.Fragment,null,e.children);N.displayName="Switch_Default";const y=e=>{const{value:n,compare:t=z,children:r,strict:l=!1}=e,o=new Set;let a=null,u=null,d=!1;return Y(r,(i,c)=>{if(!s.isValidElement(i))throw new Error(`Switch Children only accepts valid React elements at index ${c}`);const f=i.type;if(f.displayName===v.displayName){const m=i.props;if(o.has(m.value))throw new Error(`Switch found duplicate Case value at index ${c}: ${JSON.stringify(m.value)}${l?" (detected in strict mode)":""}`);if(o.add(m.value),!a&&t(n,m.value)&&(a=m.children,l===!1))return!1}else if(f.displayName===N.displayName){if(d)throw new Error(`Switch can only have one Default child at index ${c}`);if(d=!0,u=i.props.children,!l&&a)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(f.displayName||f.name||f)} at index ${c}`)}),s.createElement(s.Fragment,null,a??u)};y.displayName="Switch",y.Case=v,y.Default=N,y.createTyped=function(){return{Switch:y,Case:v,Default:N}};const b=e=>s.createElement(s.Fragment,null,e.children),M=({children:e})=>s.createElement(s.Fragment,null,e),x=e=>s.createElement(s.Fragment,null,e.children);b.displayName="If_Then",M.displayName="If_Else",x.displayName="If_ElseIf";const p=({condition:e,children:n})=>{let t=null,r=null;const l=[];if(s.Children.forEach(n,o=>{if(!s.isValidElement(o))throw new Error("If component only accepts valid React elements");const a=o.type;if(a.displayName===b.displayName){if(t)throw new Error("If component can only have one Then child");t=o}else if(a.displayName===x.displayName)l.push(o);else if(a.displayName===M.displayName){if(r)throw new Error("If component can only have one Else child");r=o}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(a.displayName||a.name||a)}`)}),e)return t?s.createElement(s.Fragment,null,t.props.children):null;for(const o of l)if(o.props.condition)return s.createElement(s.Fragment,null,o.props.children);return r?s.createElement(s.Fragment,null,r.props.children):null};p.displayName="If",p.Then=b,p.ElseIf=x,p.Else=M,p.createTyped=function(){return{If:p,Then:b,ElseIf:x,Else:M}};const U=({condition:e,children:n})=>e?s.createElement(s.Fragment,null,n):null,G=({condition:e,children:n})=>e===!1?s.createElement(s.Fragment,null,n):null,q=({all:e,any:n,none:t,children:r,fallback:l})=>g(()=>(e&&(n||t)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(e&&e.length>0&&e.every(Boolean)||n&&n.length>0&&n.some(Boolean)||t&&t.length>0&&t.every(o=>!o))),[e,n,t])?s.createElement(s.Fragment,null,r):s.createElement(s.Fragment,null,l||null),K=({data:e,transform:n,render:t,fallback:r})=>{const l=g(()=>n.reduce((o,a)=>a(o),e),[e,n]);return l==null?s.createElement(s.Fragment,null,r||null):s.createElement(s.Fragment,null,t(l))},Q=e=>{const{children:n,h:t,w:r,size:l,height:o,width:a,className:u}=e;return s.createElement("div",{style:{width:l||r||a,height:l||t||o,flexShrink:0},className:u},n)},X=({let:e,props:n,children:t,fallback:r})=>{const l=g(()=>typeof e=="function"?e(n):e,[e,n]);return!t||!Object.keys(l).length?s.createElement(s.Fragment,null,r||null):s.createElement(s.Fragment,null,t(l))};function F(...e){const n=new Set;for(const t of e)if(t){if(typeof t=="string")n.add(t);else if(Array.isArray(t))t.forEach(r=>n.add(r));else if(typeof t=="object")for(const[r,l]of Object.entries(t))l&&n.add(r)}return Array.from(n).join(" ")}const ee=e=>typeof e=="object"&&!!e,C=({className:e,children:n,asWrapper:t=!1})=>{if(!n)return null;if(!e)return s.createElement(S,null,n);const r=typeof e=="string"?e:F(...Object.values(e));if(t)return s.createElement(t===!0?"div":t,{className:r},n);if(L.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),s.createElement(S,null,n);if(H(n)){const l=n;let o=l?.props?.className;return l?.type?.displayName===C.displayName&&ee(o)&&(o=F(...Object.values(o))),V(n,{className:F(r,o)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),s.createElement(S,null,n)};C.displayName="W/Styles";const te=e=>{const{index:n=0,options:t,next:r,render:l}=e;w(()=>{if(t.length<n+1)throw new Error(`Index ${n} is out of bounds for options array of length ${t.length}. Defaulting to first option.`)},[n,t]);const[o,a]=O(n),u=()=>{a(d=>t.length?r?r(d,t):(d+1)%t.length:d)};return l(t[o],u)},ne=({onIntersect:e,threshold:n=.1,root:t=null,rootMargin:r="0px",triggerOnce:l=!1,disabled:o=!1,children:a,className:u,style:d})=>{const i=k(null),c=k(null),f=k(!1);return w(()=>{if(o||!i.current)return;if(!window.IntersectionObserver){console.warn("IntersectionObserver is not supported in this browser");return}const m=i.current,h=I=>{I.forEach(D=>{l&&f.current||(e(D,c.current),l&&(f.current=!0,c.current?.unobserve(m)))})};return c.current=new IntersectionObserver(h,{root:t,rootMargin:r,threshold:n}),c.current.observe(m),()=>{c.current&&c.current.disconnect()}},[e,n,t,r,l,o]),w(()=>{l||(f.current=!1)},[l]),s.createElement("div",{ref:i,className:u,style:d},a)};function re(e){const{items:n,renderItem:t,filter:r,renderEmpty:l,sort:o}=e;if(!n)return console.error("ArrayRender: items is null"),null;if(n.length===0)return l?l():null;if(o){let a=[...n];return r&&(a=a.filter(r)),a=a.sort(o),a.length===0?l?l():null:s.createElement(S,null,a.map((u,d)=>t(u,d)))}return s.createElement(S,null,n.map((a,u)=>r&&!r(a)?null:t(a,u)))}function le({source:e,format:n,children:t}){const r=g(()=>{if(e instanceof Date)return e;if(typeof e=="string"||typeof e=="number"){const o=new Date(e);return isNaN(o.getTime())?null:o}return null},[e]),l=g(()=>r?n?n(r):r.toLocaleString():null,[r,n]);return!l||!t?null:s.createElement(s.Fragment,null,t(l))}const oe="onChange",ae="value";function se(e){const{defaultValue:n,onBeforeChange:t,trigger:r=oe,valuePropName:l=ae,props:o}=e,a=Object.prototype.hasOwnProperty.call(o,l),[u,d]=O(n),i=a?o[l]:u,c=g(()=>o[r],[o,r]),f=Z(m=>{const h=typeof m=="function"?m(i):m;t&&t(h,i)===!1||(a||d(h),c&&c(h))},[a,t,i,c]);return[i,f]}function ie(e,...n){try{const t=e(...n);return t instanceof Promise?t:Promise.resolve(t)}catch(t){return Promise.reject(t)}}const j=typeof Promise.try=="function"?Promise.try.bind(Promise):ie;function J(e,n={}){let t=typeof e=="function"?e():e;const r=[],{sideEffect:l,transform:o}=n,a=()=>{const i=t;return o?.get?o.get(i):i},u=i=>{const c=t,f=o?.get?o.get(c):c;t=o?.set?o.set(typeof i=="function"?i(f):i):typeof i=="function"?i(f):i,r.forEach(m=>m(t)),l&&j(l,t,c).catch(m=>{console.error("Error in external state side effect, Please do it within side effects:",m)})},d=()=>{const[i,c]=s.useState(t);return s.useEffect(()=>(r.push(c),()=>{const f=r.indexOf(c);f>-1&&r.splice(f,1)}),[]),[o?.get?o.get(i):i,u]};return{get:a,set:u,use:d,useGetter:()=>{const[i]=d();return i},__listeners:r}}function ce(e,n,t){const{storageType:r="local",sideEffect:l,transform:o}=t??{},a=r==="local"?localStorage:sessionStorage;let u=n;const d=a.getItem(e);if(d)try{u=JSON.parse(d)}catch(i){console.warn(`Failed to parse ${r}Storage value for key "${e}", using initial state:`,i),u=n}return J(u,{sideEffect:i=>{a.setItem(e,JSON.stringify(i)),l?.(i)},transform:o})}function ue(e,n){const t=n||new Date,r=t.getFullYear(),l=t.getMonth()+1,o=t.getDate(),a=t.getHours(),u=t.getMinutes(),d=t.getSeconds(),i=t.getMilliseconds(),c=t.getDay(),f=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],m=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],h=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],I=["January","February","March","April","May","June","July","August","September","October","November","December"],D=m[c],A=f[c],P=l-1,B=I[P],R=h[P],_={YY:r.toString().slice(2),YYYY:r.toString(),M:l.toString(),MM:l.toString().padStart(2,"0"),MMM:R,MMMM:B,D:o.toString(),DD:o.toString().padStart(2,"0"),d:c.toString(),dd:A,ddd:A,dddd:D,H:a.toString(),HH:a.toString().padStart(2,"0"),h:(a%12).toString(),hh:(a%12).toString().padStart(2,"0"),m:u.toString(),mm:u.toString().padStart(2,"0"),s:d.toString(),ss:d.toString().padStart(2,"0"),SSS:i.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:a<12?"AM":"PM",a:a<12?"am":"pm"};return e.replace(/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,$=>_[$])}class de{count=0;next(){return this.count++}}const E=["base","xs","sm","md","lg","xl","2xl","3xl"],W={xs:475,sm:640,md:768,lg:1024,xl:1280,"2xl":1536,"3xl":1920},fe=[...E].reverse();function T(e,n){for(const t of fe){const r=e[t];if(r!==void 0&&!Number.isNaN(r)&&n>=r)return t}return"base"}function me(e=W){const[n,t]=O(T(e,window.innerWidth));return w(()=>{let r=[],l=[];const o=()=>{l.forEach(c=>c()),r=[],l=[];const a=T(e,window.innerWidth);t(a);const u=E.indexOf(a),d=E[u+1];if(d&&e[d]!==void 0){const c=e[d];if(Number.isNaN(c))throw new Error(`Invalid breakpoint value for ${d}: ${e[d]}`);{const f=window.matchMedia(`(min-width: ${c}px)`);r.push(f);const m=()=>o();f.addEventListener("change",m),l.push(()=>f.removeEventListener("change",m))}}const i=E[u-1];if(i&&e[i]!==void 0){const c=e[i];if(Number.isNaN(c))throw new Error(`Invalid breakpoint value for ${i}: ${e[i]}`);{const f=window.matchMedia(`(max-width: ${c-1}px)`);r.push(f);const m=()=>o();f.addEventListener("change",m),l.push(()=>f.removeEventListener("change",m))}}};return o(),()=>{l.forEach(a=>a())}},[e]),n}export{re as ArrayRender,de as Counter,le as DateRender,W as DefBreakpointDesc,G as False,p as If,ne as Observer,K as Pipe,X as Scope,Q as SizeBox,C as Styles,y as Switch,te as Toggle,U as True,q as When,E as breakpoints,Y as childrenLoop,J as createExternalState,ce as createStorageState,F as cx,ue as formatDate,T as getCurrentBreakpoint,j as safePromiseTry,se as useControlled,me as useScreen};
1
+ import u,{useMemo as S,Fragment as F,Children as Z,isValidElement as q,cloneElement as U,useEffect as b,useState as I,useRef as O,useCallback as G}from"react";function R(e,n){if(e===void 0)return;let t=0;if(Array.isArray(e)){for(const l of e)if(n(l,t++)===!1)break}else n(e,t)}const K=(e,n)=>e===n,N=e=>u.createElement(u.Fragment,null,e.children);N.displayName="Switch_Case";const D=e=>u.createElement(u.Fragment,null,e.children);D.displayName="Switch_Default";const w=e=>{const{value:n,compare:t=K,children:l,strict:r=!1}=e,o=new Set;let a=null,i=null,f=!1;return R(l,(s,c)=>{if(!u.isValidElement(s))throw new Error(`Switch Children only accepts valid React elements at index ${c}`);const d=s.type;if(d.displayName===N.displayName){const m=s.props;if(o.has(m.value))throw new Error(`Switch found duplicate Case value at index ${c}: ${JSON.stringify(m.value)}${r?" (detected in strict mode)":""}`);if(o.add(m.value),!a&&t(n,m.value)&&(a=m.children,r===!1))return!1}else if(d.displayName===D.displayName){if(f)throw new Error(`Switch can only have one Default child at index ${c}`);if(f=!0,i=s.props.children,!r&&a)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(d.displayName||d.name||d)} at index ${c}`)}),u.createElement(u.Fragment,null,a??i)};w.displayName="Switch",w.Case=N,w.Default=D,w.createTyped=function(){return{Switch:w,Case:N,Default:D}};const A=e=>u.createElement(u.Fragment,null,e.children),x=({children:e})=>u.createElement(u.Fragment,null,e),M=e=>u.createElement(u.Fragment,null,e.children);A.displayName="If_Then",x.displayName="If_Else",M.displayName="If_ElseIf";const E=({condition:e,children:n})=>{let t=null,l=null;const r=[];if(u.Children.forEach(n,o=>{if(!u.isValidElement(o))throw new Error("If component only accepts valid React elements");const a=o.type;if(a.displayName===A.displayName){if(t)throw new Error("If component can only have one Then child");t=o}else if(a.displayName===M.displayName)r.push(o);else if(a.displayName===x.displayName){if(l)throw new Error("If component can only have one Else child");l=o}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(a.displayName||a.name||a)}`)}),e)return t?u.createElement(u.Fragment,null,t.props.children):null;for(const o of r)if(o.props.condition)return u.createElement(u.Fragment,null,o.props.children);return l?u.createElement(u.Fragment,null,l.props.children):null};E.displayName="If",E.Then=A,E.ElseIf=M,E.Else=x,E.createTyped=function(){return{If:E,Then:A,ElseIf:M,Else:x}};const Q=({condition:e,children:n})=>e?u.createElement(u.Fragment,null,n):null,X=({condition:e,children:n})=>e===!1?u.createElement(u.Fragment,null,n):null,ee=({all:e,any:n,none:t,children:l,fallback:r})=>S(()=>(e&&(n||t)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(e&&e.length>0&&e.every(Boolean)||n&&n.length>0&&n.some(Boolean)||t&&t.length>0&&t.every(o=>!o))),[e,n,t])?u.createElement(u.Fragment,null,l):u.createElement(u.Fragment,null,r||null),te=({data:e,transform:n,render:t,fallback:l})=>{const r=S(()=>n.reduce((o,a)=>a(o),e),[e,n]);return r==null?u.createElement(u.Fragment,null,l||null):u.createElement(u.Fragment,null,t(r))},ne=e=>{const{children:n,h:t,w:l,size:r,height:o,width:a,className:i}=e;return u.createElement("div",{style:{width:r||l||a,height:r||t||o,flexShrink:0},className:i},n)},re=({let:e,props:n,children:t,fallback:l})=>{const r=S(()=>typeof e=="function"?e(n):e,[e,n]);return!t||!Object.keys(r).length?u.createElement(u.Fragment,null,l||null):u.createElement(u.Fragment,null,t(r))};function C(...e){const n=new Set;for(const t of e)if(t){if(typeof t=="string")n.add(t);else if(Array.isArray(t))t.forEach(l=>n.add(l));else if(typeof t=="object")for(const[l,r]of Object.entries(t))r&&n.add(l)}return Array.from(n).join(" ")}const le=e=>typeof e=="object"&&!!e,T=({className:e,children:n,asWrapper:t=!1})=>{if(!n)return null;if(!e)return u.createElement(F,null,n);const l=typeof e=="string"?e:C(...Object.values(e));if(t)return u.createElement(t===!0?"div":t,{className:l},n);if(Z.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),u.createElement(F,null,n);if(q(n)){const r=n;let o=r?.props?.className;return r?.type?.displayName===T.displayName&&le(o)&&(o=C(...Object.values(o))),U(n,{className:C(l,o)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),u.createElement(F,null,n)};T.displayName="W/Styles";const oe=e=>{const{index:n=0,options:t,next:l,render:r}=e;b(()=>{if(t.length<n+1)throw new Error(`Index ${n} is out of bounds for options array of length ${t.length}. Defaulting to first option.`)},[n,t]);const[o,a]=I(n),i=()=>{a(f=>t.length?l?l(f,t):(f+1)%t.length:f)};return r(t[o],i)},ae=({onIntersect:e,threshold:n=.1,root:t=null,rootMargin:l="0px",triggerOnce:r=!1,disabled:o=!1,children:a,className:i,style:f})=>{const s=O(null),c=O(null),d=O(!1);return b(()=>{if(o||!s.current)return;if(!window.IntersectionObserver){console.warn("IntersectionObserver is not supported in this browser");return}const m=s.current,g=y=>{y.forEach($=>{r&&d.current||(e($,c.current),r&&(d.current=!0,c.current?.unobserve(m)))})};return c.current=new IntersectionObserver(g,{root:t,rootMargin:l,threshold:n}),c.current.observe(m),()=>{c.current&&c.current.disconnect()}},[e,n,t,l,r,o]),b(()=>{r||(d.current=!1)},[r]),u.createElement("div",{ref:s,className:i,style:f},a)};function se(e){const{items:n,renderItem:t,filter:l,renderEmpty:r,sort:o}=e;if(!n)return console.error("ArrayRender: items is null"),null;if(n.length===0)return r?r():null;if(o){let a=[...n];return l&&(a=a.filter(l)),a=a.sort(o),a.length===0?r?r():null:u.createElement(F,null,a.map((i,f)=>t(i,f)))}return u.createElement(F,null,n.map((a,i)=>l&&!l(a)?null:t(a,i)))}function ue({source:e,format:n,children:t}){const l=S(()=>{if(e instanceof Date)return e;if(typeof e=="string"||typeof e=="number"){const o=new Date(e);return isNaN(o.getTime())?null:o}return null},[e]),r=S(()=>l?n?n(l):l.toLocaleString():null,[l,n]);return!r||!t?null:u.createElement(u.Fragment,null,t(r))}const ie="onChange",ce="value";function fe(e){const{defaultValue:n,onBeforeChange:t,trigger:l=ie,valuePropName:r=ce,props:o}=e,a=Object.prototype.hasOwnProperty.call(o,r),[i,f]=I(n),s=a?o[r]:i,c=S(()=>o[l],[o,l]),d=G(m=>{const g=typeof m=="function"?m(s):m;t&&t(g,s)===!1||(a||f(g),c&&c(g))},[a,t,s,c]);return[s,d]}function de(e,...n){try{const t=e(...n);return t instanceof Promise?t:Promise.resolve(t)}catch(t){return Promise.reject(t)}}const J=typeof Promise.try=="function"?Promise.try.bind(Promise):de;function W(e,n={}){let t=typeof e=="function"?e():e;const l=[],{sideEffect:r,transform:o}=n,a=()=>{const s=t;return o?.get?o.get(s):s},i=s=>{const c=t,d=o?.get?o.get(c):c;t=o?.set?o.set(typeof s=="function"?s(d):s):typeof s=="function"?s(d):s,l.forEach(m=>m(t)),r&&J(r,t,c).catch(m=>{console.error("Error in external state side effect, Please do it within side effects:",m)})},f=()=>{const[s,c]=u.useState(t);return u.useEffect(()=>(l.push(c),()=>{const d=l.indexOf(c);d>-1&&l.splice(d,1)}),[]),[o?.get?o.get(s):s,i]};return{get:a,set:i,use:f,useGetter:()=>{const[s]=f();return s},__listeners:l}}function me(e,n,t){const{storageType:l="local",sideEffect:r,transform:o}=t??{},a=l==="local"?localStorage:sessionStorage;let i=n;const f=a.getItem(e);if(f)try{i=JSON.parse(f)}catch(s){console.warn(`Failed to parse ${l}Storage value for key "${e}", using initial state:`,s),i=n}return W(i,{sideEffect:s=>{a.setItem(e,JSON.stringify(s)),r?.(s)},transform:o})}function he(e,n){const t=n||new Date,l=t.getFullYear(),r=t.getMonth()+1,o=t.getDate(),a=t.getHours(),i=t.getMinutes(),f=t.getSeconds(),s=t.getMilliseconds(),c=t.getDay(),d=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],m=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],g=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],y=["January","February","March","April","May","June","July","August","September","October","November","December"],$=m[c],Y=d[c],j=r-1,_=y[j],H=g[j],z={YY:l.toString().slice(2),YYYY:l.toString(),M:r.toString(),MM:r.toString().padStart(2,"0"),MMM:H,MMMM:_,D:o.toString(),DD:o.toString().padStart(2,"0"),d:c.toString(),dd:Y,ddd:Y,dddd:$,H:a.toString(),HH:a.toString().padStart(2,"0"),h:(a%12).toString(),hh:(a%12).toString().padStart(2,"0"),m:i.toString(),mm:i.toString().padStart(2,"0"),s:f.toString(),ss:f.toString().padStart(2,"0"),SSS:s.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:a<12?"AM":"PM",a:a<12?"am":"pm"};return e.replace(/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,V=>z[V])}class pe{count=0;next(){return this.count++}}const v=["base","xs","sm","md","lg","xl","2xl","3xl"],L={xs:475,sm:640,md:768,lg:1024,xl:1280,"2xl":1536,"3xl":1920};function h(e,n,t){t&&(e[n]||(e[n]=[]),e[n].push(t))}function ge(e){return e==null?!1:typeof e=="string"?e.trim().length>0:Array.isArray(e)?e.length>0:!0}function ye(e){return e!=null}function p(e,n){return`${String(e)} ${n}`}const k={email:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,url:/^(https?:\/\/)?([\w.-]+)\.([a-z]{2,6})([/\w .-]*)*\/?$/,phone:/^1[3-9]\d{9}$/};function B(e,n,t,l,r){const o=ge(n),a=ye(n);if(t.required&&!o){h(r,e,t.message??p(e,"\u4E3A\u5FC5\u586B\u9879"));return}if(!(!a&&!t.required)){if(t.dependsOn){const i=t.dependsOn(l);i===!1?h(r,e,t.message??p(e,"\u4F9D\u8D56\u6761\u4EF6\u672A\u6EE1\u8DB3")):typeof i=="string"&&h(r,e,i)}if(typeof n=="string"){const i=t,{len:f,min:s,max:c,regex:d,email:m,url:g,phone:y}=i;typeof f=="number"&&n.length!==f&&h(r,e,t.message??p(e,`\u957F\u5EA6\u5FC5\u987B\u4E3A ${f}`)),typeof s=="number"&&n.length<s&&h(r,e,t.message??p(e,`\u957F\u5EA6\u4E0D\u80FD\u5C11\u4E8E ${s}`)),typeof c=="number"&&n.length>c&&h(r,e,t.message??p(e,`\u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${c}`)),d&&!d.test(n)&&h(r,e,t.message??p(e,"\u683C\u5F0F\u4E0D\u6B63\u786E")),m&&!k.email.test(n)&&h(r,e,t.message??p(e,"\u4E0D\u662F\u6709\u6548\u7684\u90AE\u7BB1")),g&&!k.url.test(n)&&h(r,e,t.message??p(e,"\u4E0D\u662F\u6709\u6548\u7684URL")),y&&!k.phone.test(n)&&h(r,e,t.message??p(e,"\u4E0D\u662F\u6709\u6548\u7684\u624B\u673A\u53F7"))}if(typeof n=="number"){const i=t,{min:f,max:s}=i;typeof f=="number"&&n<f&&h(r,e,t.message??p(e,`\u4E0D\u80FD\u5C0F\u4E8E ${f}`)),typeof s=="number"&&n>s&&h(r,e,t.message??p(e,`\u4E0D\u80FD\u5927\u4E8E ${s}`))}if(Array.isArray(n)){const i=t,{len:f,min:s,max:c,unique:d,elementRule:m}=i;typeof f=="number"&&n.length!==f&&h(r,e,t.message??p(e,`\u957F\u5EA6\u5FC5\u987B\u4E3A ${f}`)),typeof s=="number"&&n.length<s&&h(r,e,t.message??p(e,`\u957F\u5EA6\u4E0D\u80FD\u5C0F\u4E8E ${s}`)),typeof c=="number"&&n.length>c&&h(r,e,t.message??p(e,`\u957F\u5EA6\u4E0D\u80FD\u5927\u4E8E ${c}`)),d&&new Set(n).size!==n.length&&h(r,e,t.message??p(e,"\u5143\u7D20\u5FC5\u987B\u552F\u4E00")),m&&n.forEach((g,y)=>{B(`${String(e)}[${y}]`,g,m,l,r)})}if(t.validator){const i=t.validator?.(n,l);i===!1?h(r,e,t.message??p(e,"\u6821\u9A8C\u672A\u901A\u8FC7")):typeof i=="string"&&h(r,e,i)}}}function Ee(e,n){const t={};for(const r in n){const o=r,a=n[o];if(!a)continue;const i=e[o];if(Array.isArray(a))for(const f of a)B(o,i,f,e,t);else B(o,i,a,e,t)}const l=Object.values(t).reduce((r,o)=>(o&&r.push(...o),r),[]);return l.length>0?{valid:!1,errors:l,fieldErrors:t}:{valid:!0,data:e}}const Se=[...v].reverse();function P(e,n){for(const t of Se){const l=e[t];if(l!==void 0&&!Number.isNaN(l)&&n>=l)return t}return"base"}function we(e=L){const[n,t]=I(P(e,window.innerWidth));return b(()=>{let l=[],r=[];const o=()=>{r.forEach(c=>c()),l=[],r=[];const a=P(e,window.innerWidth);t(a);const i=v.indexOf(a),f=v[i+1];if(f&&e[f]!==void 0){const c=e[f];if(Number.isNaN(c))throw new Error(`Invalid breakpoint value for ${f}: ${e[f]}`);{const d=window.matchMedia(`(min-width: ${c}px)`);l.push(d);const m=()=>o();d.addEventListener("change",m),r.push(()=>d.removeEventListener("change",m))}}const s=v[i-1];if(s&&e[s]!==void 0){const c=e[s];if(Number.isNaN(c))throw new Error(`Invalid breakpoint value for ${s}: ${e[s]}`);{const d=window.matchMedia(`(max-width: ${c-1}px)`);l.push(d);const m=()=>o();d.addEventListener("change",m),r.push(()=>d.removeEventListener("change",m))}}};return o(),()=>{r.forEach(a=>a())}},[e]),n}export{se as ArrayRender,pe as Counter,ue as DateRender,L as DefBreakpointDesc,X as False,E as If,ae as Observer,te as Pipe,re as Scope,ne as SizeBox,T as Styles,w as Switch,oe as Toggle,Q as True,ee as When,v as breakpoints,R as childrenLoop,W as createExternalState,me as createStorageState,C as cx,he as formatDate,P as getCurrentBreakpoint,Ee as ruleChecker,J as safePromiseTry,fe as useControlled,we as useScreen};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wwog/react",
3
- "version": "1.3.8",
3
+ "version": "1.3.9",
4
4
  "description": "A practical React component library providing declarative flow control and common UI utility components to make your React code more concise and readable.",
5
5
  "keywords": [
6
6
  "react",
@@ -3,4 +3,5 @@ export * from "./cx";
3
3
  export * from "./reactUtils";
4
4
  export * from "./sundry";
5
5
  export * from "./promise";
6
- export * from "./constants";
6
+ export * from "./constants";
7
+ export * from "./ruleChecker";
@@ -0,0 +1,662 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ruleChecker } from './ruleChecker';
3
+ import type { RuleDescription } from './ruleChecker';
4
+
5
+ describe('ruleChecker', () => {
6
+ // ==== 基础功能测试 ====
7
+ describe('基础功能', () => {
8
+ it('应该通过空规则验证', () => {
9
+ const result = ruleChecker({}, {});
10
+ expect(result.valid).toBe(true);
11
+ if (result.valid) {
12
+ expect(result.data).toEqual({});
13
+ }
14
+ });
15
+
16
+ it('应该验证 required 规则 - 成功', () => {
17
+ const data = { name: 'John' };
18
+ const rules: RuleDescription<typeof data> = {
19
+ name: { required: true },
20
+ };
21
+ const result = ruleChecker(data, rules);
22
+ expect(result.valid).toBe(true);
23
+ if (result.valid) {
24
+ expect(result.data.name).toBe('John');
25
+ }
26
+ });
27
+
28
+ it('应该验证 required 规则 - 失败', () => {
29
+ const data = { name: '' };
30
+ const rules: RuleDescription<typeof data> = {
31
+ name: { required: true },
32
+ };
33
+ const result = ruleChecker(data, rules);
34
+ expect(result.valid).toBe(false);
35
+ if (!result.valid) {
36
+ expect(result.errors).toContain('name 为必填项');
37
+ expect(result.fieldErrors.name).toContain('name 为必填项');
38
+ }
39
+ });
40
+
41
+ it('应该使用自定义错误消息', () => {
42
+ const data = { email: '' };
43
+ const rules: RuleDescription<typeof data> = {
44
+ email: { required: true, message: '邮箱不能为空' },
45
+ };
46
+ const result = ruleChecker(data, rules);
47
+ expect(result.valid).toBe(false);
48
+ if (!result.valid) {
49
+ expect(result.errors).toContain('邮箱不能为空');
50
+ }
51
+ });
52
+
53
+ it('应该处理 null 和 undefined 值', () => {
54
+ type DataType = { name: string | null; age?: number };
55
+ const data: Partial<DataType> = { name: null, age: undefined };
56
+ const rules: RuleDescription<DataType> = {
57
+ name: { required: true },
58
+ age: { required: false },
59
+ };
60
+ const result = ruleChecker(data, rules);
61
+ expect(result.valid).toBe(false);
62
+ if (!result.valid) {
63
+ expect(result.fieldErrors.name).toContain('name 为必填项');
64
+ expect(result.fieldErrors.age).toBeUndefined();
65
+ }
66
+ });
67
+
68
+ it('应该验证自定义 validator - 返回 boolean', () => {
69
+ const data = { password: '123' };
70
+ const rules: RuleDescription<typeof data> = {
71
+ password: {
72
+ validator: (value) => value.length >= 6,
73
+ message: '密码至少6位',
74
+ },
75
+ };
76
+ const result = ruleChecker(data, rules);
77
+ expect(result.valid).toBe(false);
78
+ if (!result.valid) {
79
+ expect(result.errors).toContain('密码至少6位');
80
+ }
81
+ });
82
+
83
+ it('应该验证自定义 validator - 返回 string', () => {
84
+ const data = { username: 'admin' };
85
+ const rules: RuleDescription<typeof data> = {
86
+ username: {
87
+ validator: (value) =>
88
+ value === 'admin' ? '用户名不能是admin' : true,
89
+ },
90
+ };
91
+ const result = ruleChecker(data, rules);
92
+ expect(result.valid).toBe(false);
93
+ if (!result.valid) {
94
+ expect(result.errors).toContain('用户名不能是admin');
95
+ }
96
+ });
97
+ });
98
+
99
+ // ==== 字符串规则测试 ====
100
+ describe('字符串规则', () => {
101
+ it('应该验证字符串长度 len', () => {
102
+ const data = { code: '123' };
103
+ const rules: RuleDescription<typeof data> = {
104
+ code: { len: 4 },
105
+ };
106
+ const result = ruleChecker(data, rules);
107
+ expect(result.valid).toBe(false);
108
+ if (!result.valid) {
109
+ expect(result.errors).toContain('code 长度必须为 4');
110
+ }
111
+ });
112
+
113
+ it('应该验证字符串最小长度 min', () => {
114
+ const data = { username: 'ab' };
115
+ const rules: RuleDescription<typeof data> = {
116
+ username: { min: 3 },
117
+ };
118
+ const result = ruleChecker(data, rules);
119
+ expect(result.valid).toBe(false);
120
+ if (!result.valid) {
121
+ expect(result.errors).toContain('username 长度不能少于 3');
122
+ }
123
+ });
124
+
125
+ it('应该验证字符串最大长度 max', () => {
126
+ const data = { comment: 'a'.repeat(101) };
127
+ const rules: RuleDescription<typeof data> = {
128
+ comment: { max: 100 },
129
+ };
130
+ const result = ruleChecker(data, rules);
131
+ expect(result.valid).toBe(false);
132
+ if (!result.valid) {
133
+ expect(result.errors).toContain('comment 长度不能超过 100');
134
+ }
135
+ });
136
+
137
+ it('应该验证正则表达式', () => {
138
+ const data = { phone: '123456' };
139
+ const rules: RuleDescription<typeof data> = {
140
+ phone: { regex: /^\d{11}$/ },
141
+ };
142
+ const result = ruleChecker(data, rules);
143
+ expect(result.valid).toBe(false);
144
+ if (!result.valid) {
145
+ expect(result.errors).toContain('phone 格式不正确');
146
+ }
147
+ });
148
+
149
+ it('应该验证邮箱格式', () => {
150
+ const data = { email: 'invalid-email' };
151
+ const rules: RuleDescription<typeof data> = {
152
+ email: { email: true },
153
+ };
154
+ const result = ruleChecker(data, rules);
155
+ expect(result.valid).toBe(false);
156
+ if (!result.valid) {
157
+ expect(result.errors).toContain('email 不是有效的邮箱');
158
+ }
159
+ });
160
+
161
+ it('应该通过有效的邮箱验证', () => {
162
+ const data = { email: 'test@example.com' };
163
+ const rules: RuleDescription<typeof data> = {
164
+ email: { email: true },
165
+ };
166
+ const result = ruleChecker(data, rules);
167
+ expect(result.valid).toBe(true);
168
+ });
169
+
170
+ it('应该验证URL格式', () => {
171
+ const data = { website: 'invalid-url' };
172
+ const rules: RuleDescription<typeof data> = {
173
+ website: { url: true },
174
+ };
175
+ const result = ruleChecker(data, rules);
176
+ expect(result.valid).toBe(false);
177
+ if (!result.valid) {
178
+ expect(result.errors).toContain('website 不是有效的URL');
179
+ }
180
+ });
181
+
182
+ it('应该通过有效的URL验证', () => {
183
+ const data = { website: 'https://example.com' };
184
+ const rules: RuleDescription<typeof data> = {
185
+ website: { url: true },
186
+ };
187
+ const result = ruleChecker(data, rules);
188
+ expect(result.valid).toBe(true);
189
+ });
190
+
191
+ it('应该验证手机号格式', () => {
192
+ const data = { mobile: '123456789' };
193
+ const rules: RuleDescription<typeof data> = {
194
+ mobile: { phone: true },
195
+ };
196
+ const result = ruleChecker(data, rules);
197
+ expect(result.valid).toBe(false);
198
+ if (!result.valid) {
199
+ expect(result.errors).toContain('mobile 不是有效的手机号');
200
+ }
201
+ });
202
+
203
+ it('应该通过有效的手机号验证', () => {
204
+ const data = { mobile: '13812345678' };
205
+ const rules: RuleDescription<typeof data> = {
206
+ mobile: { phone: true },
207
+ };
208
+ const result = ruleChecker(data, rules);
209
+ expect(result.valid).toBe(true);
210
+ });
211
+ });
212
+
213
+ // ==== 数字规则测试 ====
214
+ describe('数字规则', () => {
215
+ it('应该验证数字最小值', () => {
216
+ const data = { age: 5 };
217
+ const rules: RuleDescription<typeof data> = {
218
+ age: { min: 18 },
219
+ };
220
+ const result = ruleChecker(data, rules);
221
+ expect(result.valid).toBe(false);
222
+ if (!result.valid) {
223
+ expect(result.errors).toContain('age 不能小于 18');
224
+ }
225
+ });
226
+
227
+ it('应该验证数字最大值', () => {
228
+ const data = { score: 105 };
229
+ const rules: RuleDescription<typeof data> = {
230
+ score: { max: 100 },
231
+ };
232
+ const result = ruleChecker(data, rules);
233
+ expect(result.valid).toBe(false);
234
+ if (!result.valid) {
235
+ expect(result.errors).toContain('score 不能大于 100');
236
+ }
237
+ });
238
+
239
+ it('应该通过数字范围验证', () => {
240
+ const data = { percentage: 85 };
241
+ const rules: RuleDescription<typeof data> = {
242
+ percentage: { min: 0, max: 100 },
243
+ };
244
+ const result = ruleChecker(data, rules);
245
+ expect(result.valid).toBe(true);
246
+ });
247
+ });
248
+
249
+ // ==== 数组规则测试 ====
250
+ describe('数组规则', () => {
251
+ it('应该验证数组长度 len', () => {
252
+ const data = { items: [1, 2, 3] };
253
+ const rules: RuleDescription<typeof data> = {
254
+ items: { len: 5 },
255
+ };
256
+ const result = ruleChecker(data, rules);
257
+ expect(result.valid).toBe(false);
258
+ if (!result.valid) {
259
+ expect(result.errors).toContain('items 长度必须为 5');
260
+ }
261
+ });
262
+
263
+ it('应该验证数组最小长度', () => {
264
+ const data = { tags: ['one'] };
265
+ const rules: RuleDescription<typeof data> = {
266
+ tags: { min: 2 },
267
+ };
268
+ const result = ruleChecker(data, rules);
269
+ expect(result.valid).toBe(false);
270
+ if (!result.valid) {
271
+ expect(result.errors).toContain('tags 长度不能小于 2');
272
+ }
273
+ });
274
+
275
+ it('应该验证数组最大长度', () => {
276
+ const data = { categories: [1, 2, 3, 4, 5, 6] };
277
+ const rules: RuleDescription<typeof data> = {
278
+ categories: { max: 5 },
279
+ };
280
+ const result = ruleChecker(data, rules);
281
+ expect(result.valid).toBe(false);
282
+ if (!result.valid) {
283
+ expect(result.errors).toContain('categories 长度不能大于 5');
284
+ }
285
+ });
286
+
287
+ it('应该验证数组元素唯一性', () => {
288
+ const data = { ids: [1, 2, 2, 3] };
289
+ const rules: RuleDescription<typeof data> = {
290
+ ids: { unique: true },
291
+ };
292
+ const result = ruleChecker(data, rules);
293
+ expect(result.valid).toBe(false);
294
+ if (!result.valid) {
295
+ expect(result.errors).toContain('ids 元素必须唯一');
296
+ }
297
+ });
298
+
299
+ it('应该通过数组元素唯一性验证', () => {
300
+ const data = { ids: [1, 2, 3, 4] };
301
+ const rules: RuleDescription<typeof data> = {
302
+ ids: { unique: true },
303
+ };
304
+ const result = ruleChecker(data, rules);
305
+ expect(result.valid).toBe(true);
306
+ });
307
+
308
+ it('应该验证数组元素规则', () => {
309
+ const data = { emails: ['valid@email.com', 'invalid-email'] };
310
+ const rules: RuleDescription<typeof data> = {
311
+ emails: {
312
+ elementRule: { email: true },
313
+ },
314
+ };
315
+ const result = ruleChecker(data, rules);
316
+ expect(result.valid).toBe(false);
317
+ if (!result.valid) {
318
+ // 检查错误信息包含了数组元素索引的错误
319
+ expect(result.errors.some((error) => error.includes('emails[1]'))).toBe(
320
+ true
321
+ );
322
+ }
323
+ });
324
+ });
325
+
326
+ // ==== 依赖验证测试 ====
327
+ describe('依赖验证', () => {
328
+ it('应该验证 dependsOn - 返回 boolean', () => {
329
+ const data = { type: 'premium', discount: 0.9 };
330
+ const rules: RuleDescription<typeof data> = {
331
+ discount: {
332
+ dependsOn: (data) => data.type === 'basic', // 当类型不是basic时会失败
333
+ message: '只有基础用户才能设置折扣',
334
+ },
335
+ };
336
+ const result = ruleChecker(data, rules);
337
+ expect(result.valid).toBe(false);
338
+ if (!result.valid) {
339
+ expect(result.errors).toContain('只有基础用户才能设置折扣');
340
+ }
341
+ });
342
+
343
+ it('应该验证 dependsOn - 返回 string', () => {
344
+ const data = { age: 16, hasLicense: true };
345
+ const rules: RuleDescription<typeof data> = {
346
+ hasLicense: {
347
+ dependsOn: (data) =>
348
+ (data.age || 0) < 18 ? '未成年人不能有驾驶证' : true,
349
+ },
350
+ };
351
+ const result = ruleChecker(data, rules);
352
+ expect(result.valid).toBe(false);
353
+ if (!result.valid) {
354
+ expect(result.errors).toContain('未成年人不能有驾驶证');
355
+ }
356
+ });
357
+
358
+ it('应该通过 dependsOn 验证', () => {
359
+ const data = { type: 'basic', discount: undefined };
360
+ const rules: RuleDescription<typeof data> = {
361
+ discount: {
362
+ dependsOn: (data) => data.type === 'premium',
363
+ },
364
+ };
365
+ const result = ruleChecker(data, rules);
366
+ expect(result.valid).toBe(true);
367
+ });
368
+ });
369
+
370
+ // ==== 复杂场景测试 ====
371
+ describe('复杂场景', () => {
372
+ it('应该处理多个规则数组', () => {
373
+ const data = { password: '123' };
374
+ const rules: RuleDescription<typeof data> = {
375
+ password: [
376
+ { required: true },
377
+ { min: 6, message: '密码至少6位' },
378
+ { regex: /[A-Z]/, message: '密码必须包含大写字母' },
379
+ ],
380
+ };
381
+ const result = ruleChecker(data, rules);
382
+ expect(result.valid).toBe(false);
383
+ if (!result.valid) {
384
+ expect(result.errors).toContain('密码至少6位');
385
+ expect(result.errors).toContain('密码必须包含大写字母');
386
+ }
387
+ });
388
+
389
+ it('应该收集多个字段的错误', () => {
390
+ const data = { name: '', email: 'invalid', age: 5 };
391
+ const rules: RuleDescription<typeof data> = {
392
+ name: { required: true },
393
+ email: { email: true },
394
+ age: { min: 18 },
395
+ };
396
+ const result = ruleChecker(data, rules);
397
+ expect(result.valid).toBe(false);
398
+ if (!result.valid) {
399
+ expect(result.errors).toHaveLength(3);
400
+ expect(result.fieldErrors.name).toContain('name 为必填项');
401
+ expect(result.fieldErrors.email).toContain('email 不是有效的邮箱');
402
+ expect(result.fieldErrors.age).toContain('age 不能小于 18');
403
+ }
404
+ });
405
+
406
+ it('应该处理复杂的用户注册场景', () => {
407
+ const data = {
408
+ username: 'ab',
409
+ email: 'test@example.com',
410
+ password: '123',
411
+ confirmPassword: '456',
412
+ age: 25,
413
+ hobbies: ['reading'],
414
+ terms: false,
415
+ };
416
+
417
+ const rules: RuleDescription<typeof data> = {
418
+ username: { required: true, min: 3, max: 20 },
419
+ email: { required: true, email: true },
420
+ password: [
421
+ { required: true },
422
+ { min: 6 },
423
+ { regex: /[A-Z]/, message: '密码必须包含大写字母' },
424
+ ],
425
+ confirmPassword: {
426
+ required: true,
427
+ validator: (value, data) =>
428
+ value === data.password || '两次密码不一致',
429
+ },
430
+ age: { required: true, min: 18, max: 120 },
431
+ hobbies: { min: 2, max: 5 },
432
+ terms: {
433
+ required: true,
434
+ validator: (value) => value === true || '必须同意用户协议',
435
+ },
436
+ };
437
+
438
+ const result = ruleChecker(data, rules);
439
+ expect(result.valid).toBe(false);
440
+ if (!result.valid) {
441
+ expect(result.fieldErrors.username).toContain(
442
+ 'username 长度不能少于 3'
443
+ );
444
+ expect(result.fieldErrors.password).toContain(
445
+ 'password 长度不能少于 6'
446
+ );
447
+ expect(result.fieldErrors.confirmPassword).toContain('两次密码不一致');
448
+ expect(result.fieldErrors.hobbies).toContain('hobbies 长度不能小于 2');
449
+ expect(result.fieldErrors.terms).toContain('必须同意用户协议');
450
+ }
451
+ });
452
+
453
+ it('应该在所有验证通过时返回正确的数据', () => {
454
+ const data = {
455
+ name: 'John Doe',
456
+ email: 'john@example.com',
457
+ age: 30,
458
+ };
459
+
460
+ const rules: RuleDescription<typeof data> = {
461
+ name: { required: true, min: 2 },
462
+ email: { required: true, email: true },
463
+ age: { required: true, min: 18, max: 120 },
464
+ };
465
+
466
+ const result = ruleChecker(data, rules);
467
+ expect(result.valid).toBe(true);
468
+ if (result.valid) {
469
+ expect(result.data).toEqual(data);
470
+ }
471
+ });
472
+ });
473
+
474
+ // ==== 类型安全测试 ====
475
+ describe('类型安全验证', () => {
476
+ it('数字字段应该只支持数字相关规则', () => {
477
+ const data = { age: 25 };
478
+
479
+ // 这些应该是有效的数字规则
480
+ const validRules: RuleDescription<typeof data> = {
481
+ age: { min: 0, max: 120, required: true },
482
+ };
483
+
484
+ const result = ruleChecker(data, validRules);
485
+ expect(result.valid).toBe(true);
486
+
487
+ // 注意:TypeScript 会阻止以下无效的数字规则
488
+ // age: { email: true }, // ❌ 编译错误
489
+ // age: { regex: /test/ }, // ❌ 编译错误
490
+ // age: { len: 10 }, // ❌ 编译错误
491
+ });
492
+
493
+ it('字符串字段应该支持长度和格式规则', () => {
494
+ const data = { email: 'test@example.com', name: 'John' };
495
+
496
+ const rules: RuleDescription<typeof data> = {
497
+ email: {
498
+ required: true,
499
+ email: true,
500
+ min: 5,
501
+ max: 100,
502
+ },
503
+ name: {
504
+ required: true,
505
+ min: 2,
506
+ max: 50,
507
+ regex: /^[a-zA-Z]+$/,
508
+ },
509
+ };
510
+
511
+ const result = ruleChecker(data, rules);
512
+ expect(result.valid).toBe(true);
513
+ });
514
+
515
+ it('数组字段应该支持长度和唯一性规则', () => {
516
+ const data = { tags: ['react', 'typescript'], ids: [1, 2, 3] };
517
+
518
+ const rules: RuleDescription<typeof data> = {
519
+ tags: {
520
+ min: 1,
521
+ max: 5,
522
+ unique: true,
523
+ elementRule: { min: 2, max: 20 }, // 元素是字符串
524
+ },
525
+ ids: {
526
+ len: 3,
527
+ unique: true,
528
+ elementRule: { min: 1, max: 1000 }, // 元素是数字
529
+ },
530
+ };
531
+
532
+ const result = ruleChecker(data, rules);
533
+ expect(result.valid).toBe(true);
534
+ });
535
+
536
+ it('布尔字段应该只支持基础规则', () => {
537
+ const data = { isActive: false, hasPermission: false };
538
+
539
+ const rules: RuleDescription<typeof data> = {
540
+ isActive: {
541
+ required: true,
542
+ validator: (value) => value === true || '必须激活账户',
543
+ },
544
+ hasPermission: {
545
+ required: true,
546
+ },
547
+ };
548
+
549
+ const result = ruleChecker(data, rules);
550
+ expect(result.valid).toBe(false);
551
+ if (!result.valid) {
552
+ expect(result.errors).toContain('必须激活账户');
553
+ }
554
+ });
555
+
556
+ it('应该正确处理混合类型的复杂对象', () => {
557
+ const data = {
558
+ id: 123,
559
+ name: 'Product Name',
560
+ price: 99.99,
561
+ tags: ['electronics', 'mobile'],
562
+ isActive: true,
563
+ description: 'A great product',
564
+ };
565
+
566
+ const rules: RuleDescription<typeof data> = {
567
+ id: { required: true, min: 1 }, // 数字规则
568
+ name: { required: true, min: 3, max: 50 }, // 字符串长度规则
569
+ price: { required: true, min: 0, max: 10000 }, // 数字范围规则
570
+ tags: { min: 1, max: 10, unique: true }, // 数组规则
571
+ isActive: { required: true }, // 布尔基础规则
572
+ description: { min: 10, max: 500, regex: /^[a-zA-Z\s]+$/ }, // 字符串格式规则
573
+ };
574
+
575
+ const result = ruleChecker(data, rules);
576
+ expect(result.valid).toBe(true);
577
+ });
578
+ });
579
+
580
+ // ==== 边缘情况测试 ====
581
+ describe('边缘情况', () => {
582
+ it('应该正确处理空字符串的各种验证', () => {
583
+ const data = { text: '' };
584
+ const rules: RuleDescription<typeof data> = {
585
+ text: { min: 1 }, // 空字符串长度为0,应该失败
586
+ };
587
+ const result = ruleChecker(data, rules);
588
+ expect(result.valid).toBe(false);
589
+ });
590
+
591
+ it('应该正确处理空数组的各种验证', () => {
592
+ const data = { items: [] as string[] };
593
+ const rules: RuleDescription<typeof data> = {
594
+ items: { min: 1 }, // 空数组长度为0,应该失败
595
+ };
596
+ const result = ruleChecker(data, rules);
597
+ expect(result.valid).toBe(false);
598
+ });
599
+
600
+ it('应该正确处理数字 0 的验证', () => {
601
+ const data = { count: 0 };
602
+ const rules: RuleDescription<typeof data> = {
603
+ count: { required: true, min: 0 }, // 0 是有效值,应该通过
604
+ };
605
+ const result = ruleChecker(data, rules);
606
+ expect(result.valid).toBe(true);
607
+ });
608
+
609
+ it('应该正确处理 false 值的验证', () => {
610
+ const data = { isEnabled: false };
611
+ const rules: RuleDescription<typeof data> = {
612
+ isEnabled: { required: true }, // false 是有效值,应该通过
613
+ };
614
+ const result = ruleChecker(data, rules);
615
+ expect(result.valid).toBe(true);
616
+ });
617
+
618
+ it('应该正确处理只有空格的字符串', () => {
619
+ const data = { text: ' ' };
620
+ const rules: RuleDescription<typeof data> = {
621
+ text: { required: true }, // 只有空格被视为空值,应该失败
622
+ };
623
+ const result = ruleChecker(data, rules);
624
+ expect(result.valid).toBe(false);
625
+ });
626
+
627
+ it('应该处理非常长的字符串', () => {
628
+ const longText = 'a'.repeat(1000);
629
+ const data = { content: longText };
630
+ const rules: RuleDescription<typeof data> = {
631
+ content: { max: 500 }, // 超过最大长度,应该失败
632
+ };
633
+ const result = ruleChecker(data, rules);
634
+ expect(result.valid).toBe(false);
635
+ if (!result.valid) {
636
+ expect(result.errors).toContain('content 长度不能超过 500');
637
+ }
638
+ });
639
+
640
+ it('应该处理复杂的嵌套数组元素验证', () => {
641
+ const data = {
642
+ users: [
643
+ { name: 'Alice', email: 'alice@test.com' },
644
+ { name: 'B', email: 'invalid-email' }, // 名字太短,邮箱无效
645
+ ],
646
+ };
647
+
648
+ // 注意:这个例子展示了当前系统的限制
649
+ // 对于复杂嵌套对象,需要扩展 elementRule 支持
650
+ const rules: RuleDescription<typeof data> = {
651
+ users: {
652
+ min: 1,
653
+ max: 10,
654
+ // elementRule 目前不支持对象验证
655
+ },
656
+ };
657
+
658
+ const result = ruleChecker(data, rules);
659
+ expect(result.valid).toBe(true); // 基本数组规则通过
660
+ });
661
+ });
662
+ });
@@ -0,0 +1,340 @@
1
+ // ==== 基础规则类型 ====
2
+ export type BaseRule<TAll, TValue> = {
3
+ required?: boolean;
4
+ message?: string;
5
+ validator?: (value: TValue, data: Partial<TAll>) => boolean | string;
6
+ dependsOn?: (data: Partial<TAll>) => boolean | string; // 支持依赖校验
7
+ };
8
+
9
+ // ==== 长度相关规则(字符串和数组共用) ====
10
+ export type LengthRuleProps = {
11
+ min?: number;
12
+ max?: number;
13
+ len?: number;
14
+ };
15
+
16
+ // ==== 数字范围规则 ====
17
+ export type NumberRangeProps = {
18
+ min?: number;
19
+ max?: number;
20
+ };
21
+
22
+ // ==== 字符串特有规则 ====
23
+ export type StringSpecificProps = {
24
+ regex?: RegExp;
25
+ email?: boolean;
26
+ url?: boolean;
27
+ phone?: boolean;
28
+ };
29
+
30
+ // ==== 数组特有规则 ====
31
+ export type ArraySpecificProps = {
32
+ unique?: boolean;
33
+ };
34
+
35
+ // ==== 各类型规则 ====
36
+ export type NumberRule<TAll> = BaseRule<TAll, number> & NumberRangeProps;
37
+
38
+ export type StringRule<TAll> = BaseRule<TAll, string> &
39
+ LengthRuleProps &
40
+ StringSpecificProps;
41
+
42
+ export type BooleanRule<TAll> = BaseRule<TAll, boolean>;
43
+
44
+ export type ArrayRule<TAll, U> = BaseRule<TAll, U[]> &
45
+ LengthRuleProps &
46
+ ArraySpecificProps & {
47
+ elementRule?: FieldRule<U, TAll>; // 元素级规则
48
+ };
49
+
50
+ // ==== 泛型推导:字段类型 → 规则类型 ====
51
+ export type FieldRule<TValue, TAll> = TValue extends string
52
+ ? StringRule<TAll>
53
+ : TValue extends number
54
+ ? NumberRule<TAll>
55
+ : TValue extends boolean
56
+ ? BooleanRule<TAll>
57
+ : TValue extends (infer U)[]
58
+ ? ArrayRule<TAll, U>
59
+ : BaseRule<TAll, TValue>;
60
+
61
+ // ==== 描述对象 ====
62
+ export type RuleDescription<T extends Record<string, unknown>> = {
63
+ [K in keyof T]?: FieldRule<T[K], T> | FieldRule<T[K], T>[];
64
+ };
65
+
66
+ // ==== Required 判断 ====
67
+ type IsRequired<R> = R extends { required: true } ? true : false;
68
+
69
+ export type ApplyRules<
70
+ T extends Record<string, unknown>,
71
+ R extends RuleDescription<T>,
72
+ > = {
73
+ [K in keyof T]: IsRequired<R[K]> extends true ? T[K] : T[K] | undefined;
74
+ };
75
+
76
+ // ==== 错误收集 ====
77
+ type FieldErrors<T> = Partial<Record<keyof T, string[]>>;
78
+
79
+ function pushError<T extends Record<string, unknown>>(
80
+ fieldErrors: FieldErrors<T>,
81
+ field: keyof T,
82
+ msg: string
83
+ ) {
84
+ if (!msg) return;
85
+ if (!fieldErrors[field]) fieldErrors[field] = [];
86
+ (fieldErrors[field] as string[]).push(msg);
87
+ }
88
+
89
+ function isPresent(value: unknown): boolean {
90
+ if (value === null || value === undefined) return false;
91
+ if (typeof value === 'string') return value.trim().length > 0;
92
+ if (Array.isArray(value)) return value.length > 0;
93
+ return true;
94
+ }
95
+
96
+ function isValueProvided(value: unknown): boolean {
97
+ return value !== null && value !== undefined;
98
+ }
99
+
100
+ function defaultMsg(field: string | number | symbol, reason: string) {
101
+ return `${String(field)} ${reason}`;
102
+ }
103
+
104
+ // ==== 内置校验器 ====
105
+ const builtins = {
106
+ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
107
+ url: /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,6})([/\w .-]*)*\/?$/,
108
+ phone: /^1[3-9]\d{9}$/, // 中国手机号
109
+ };
110
+
111
+ // ==== 单字段校验 ====
112
+ function validateRule<T extends Record<string, unknown>, V>(
113
+ key: keyof T,
114
+ value: V,
115
+ rule: FieldRule<V, T>,
116
+ data: Partial<T>,
117
+ fieldErrors: FieldErrors<T>
118
+ ) {
119
+ const present = isPresent(value);
120
+ const valueProvided = isValueProvided(value);
121
+
122
+ // required
123
+ if (rule.required && !present) {
124
+ pushError(fieldErrors, key, rule.message ?? defaultMsg(key, '为必填项'));
125
+ return;
126
+ }
127
+ // 如果值没有提供且不是必填,跳过验证
128
+ if (!valueProvided && !rule.required) return;
129
+
130
+ // dependsOn
131
+ if (rule.dependsOn) {
132
+ const res = rule.dependsOn(data);
133
+ if (res === false)
134
+ pushError(
135
+ fieldErrors,
136
+ key,
137
+ rule.message ?? defaultMsg(key, '依赖条件未满足')
138
+ );
139
+ else if (typeof res === 'string') pushError(fieldErrors, key, res);
140
+ }
141
+
142
+ // string 相关校验
143
+ if (typeof value === 'string') {
144
+ const stringRule = rule as StringRule<T>;
145
+ const { len, min, max, regex, email, url, phone } = stringRule;
146
+
147
+ if (typeof len === 'number' && value.length !== len) {
148
+ pushError(
149
+ fieldErrors,
150
+ key,
151
+ rule.message ?? defaultMsg(key, `长度必须为 ${len}`)
152
+ );
153
+ }
154
+ if (typeof min === 'number' && value.length < min) {
155
+ pushError(
156
+ fieldErrors,
157
+ key,
158
+ rule.message ?? defaultMsg(key, `长度不能少于 ${min}`)
159
+ );
160
+ }
161
+ if (typeof max === 'number' && value.length > max) {
162
+ pushError(
163
+ fieldErrors,
164
+ key,
165
+ rule.message ?? defaultMsg(key, `长度不能超过 ${max}`)
166
+ );
167
+ }
168
+ if (regex && !regex.test(value)) {
169
+ pushError(
170
+ fieldErrors,
171
+ key,
172
+ rule.message ?? defaultMsg(key, '格式不正确')
173
+ );
174
+ }
175
+ if (email && !builtins.email.test(value)) {
176
+ pushError(
177
+ fieldErrors,
178
+ key,
179
+ rule.message ?? defaultMsg(key, '不是有效的邮箱')
180
+ );
181
+ }
182
+ if (url && !builtins.url.test(value)) {
183
+ pushError(
184
+ fieldErrors,
185
+ key,
186
+ rule.message ?? defaultMsg(key, '不是有效的URL')
187
+ );
188
+ }
189
+ if (phone && !builtins.phone.test(value)) {
190
+ pushError(
191
+ fieldErrors,
192
+ key,
193
+ rule.message ?? defaultMsg(key, '不是有效的手机号')
194
+ );
195
+ }
196
+ }
197
+
198
+ // number 相关校验
199
+ if (typeof value === 'number') {
200
+ const numberRule = rule as NumberRule<T>;
201
+ const { min, max } = numberRule;
202
+ if (typeof min === 'number' && value < min) {
203
+ pushError(
204
+ fieldErrors,
205
+ key,
206
+ rule.message ?? defaultMsg(key, `不能小于 ${min}`)
207
+ );
208
+ }
209
+ if (typeof max === 'number' && value > max) {
210
+ pushError(
211
+ fieldErrors,
212
+ key,
213
+ rule.message ?? defaultMsg(key, `不能大于 ${max}`)
214
+ );
215
+ }
216
+ }
217
+
218
+ // array 相关校验
219
+ if (Array.isArray(value)) {
220
+ const arrayRule = rule as ArrayRule<T, unknown>;
221
+ const { len, min, max, unique, elementRule } = arrayRule;
222
+
223
+ if (typeof len === 'number' && value.length !== len) {
224
+ pushError(
225
+ fieldErrors,
226
+ key,
227
+ rule.message ?? defaultMsg(key, `长度必须为 ${len}`)
228
+ );
229
+ }
230
+ if (typeof min === 'number' && value.length < min) {
231
+ pushError(
232
+ fieldErrors,
233
+ key,
234
+ rule.message ?? defaultMsg(key, `长度不能小于 ${min}`)
235
+ );
236
+ }
237
+ if (typeof max === 'number' && value.length > max) {
238
+ pushError(
239
+ fieldErrors,
240
+ key,
241
+ rule.message ?? defaultMsg(key, `长度不能大于 ${max}`)
242
+ );
243
+ }
244
+ if (unique && new Set(value).size !== value.length) {
245
+ pushError(
246
+ fieldErrors,
247
+ key,
248
+ rule.message ?? defaultMsg(key, '元素必须唯一')
249
+ );
250
+ }
251
+ if (elementRule) {
252
+ (value as unknown[]).forEach((v, i) => {
253
+ validateRule(
254
+ `${String(key)}[${i}]` as keyof T,
255
+ v as unknown,
256
+ elementRule as FieldRule<unknown, T>,
257
+ data,
258
+ fieldErrors
259
+ );
260
+ });
261
+ }
262
+ }
263
+
264
+ // 自定义 validator
265
+ if (rule.validator) {
266
+ const res = (
267
+ rule.validator as
268
+ | ((value: unknown, data: Partial<T>) => boolean | string)
269
+ | undefined
270
+ )?.(value as unknown, data);
271
+ if (res === false)
272
+ pushError(
273
+ fieldErrors,
274
+ key,
275
+ rule.message ?? defaultMsg(key, '校验未通过')
276
+ );
277
+ else if (typeof res === 'string') pushError(fieldErrors, key, res);
278
+ }
279
+ }
280
+
281
+ // ==== 主函数 ====
282
+ export function ruleChecker<
283
+ T extends Record<string, unknown>,
284
+ R extends RuleDescription<T>,
285
+ >(
286
+ data: Partial<T>,
287
+ rules: R
288
+ ):
289
+ | { valid: true; data: ApplyRules<T, R> }
290
+ | { valid: false; errors: string[]; fieldErrors: FieldErrors<T> } {
291
+ const fieldErrors: FieldErrors<T> = {};
292
+
293
+ for (const k in rules) {
294
+ const key = k as keyof T;
295
+ const ruleOrRules = rules[key] as
296
+ | FieldRule<T[typeof key] | undefined, T>
297
+ | FieldRule<T[typeof key] | undefined, T>[]
298
+ | undefined;
299
+ if (!ruleOrRules) continue;
300
+
301
+ const value = data[key] as T[typeof key] | undefined;
302
+
303
+ // 支持单个规则或规则数组
304
+ if (Array.isArray(ruleOrRules)) {
305
+ // 处理规则数组
306
+ for (const rule of ruleOrRules) {
307
+ validateRule<T, T[typeof key] | undefined>(
308
+ key,
309
+ value,
310
+ rule,
311
+ data,
312
+ fieldErrors
313
+ );
314
+ }
315
+ } else {
316
+ // 处理单个规则
317
+ validateRule<T, T[typeof key] | undefined>(
318
+ key,
319
+ value,
320
+ ruleOrRules,
321
+ data,
322
+ fieldErrors
323
+ );
324
+ }
325
+ }
326
+
327
+ // 聚合错误时,避免 [] 被推断为 never[] 导致的类型问题
328
+ const errors: string[] = (
329
+ Object.values(fieldErrors) as Array<string[] | undefined>
330
+ ).reduce<string[]>((acc, v) => {
331
+ if (v) acc.push(...v);
332
+ return acc;
333
+ }, []);
334
+
335
+ if (errors.length > 0) {
336
+ return { valid: false, errors, fieldErrors };
337
+ }
338
+
339
+ return { valid: true, data: data as ApplyRules<T, R> };
340
+ }