-
Notifications
You must be signed in to change notification settings - Fork 201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ballerine Validator(WIP) #2874
base: dev
Are you sure you want to change the base?
Ballerine Validator(WIP) #2874
Changes from all commits
75d4a34
b2c7a44
e471f81
7cb51fe
4d2c708
c2987da
ce18a0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { Story } from './_stories/components/Story'; | ||
import { ValidatorProvider } from './ValidatorProvider'; | ||
|
||
export default { | ||
component: ValidatorProvider, | ||
}; | ||
|
||
export const Default = { | ||
render: () => <Story />, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { useMemo } from 'react'; | ||
import { IValidatorContext, ValidatorContext } from './context'; | ||
import { checkIfValid } from './helpers'; | ||
import { useValidate } from './hooks/internal/useValidate'; | ||
import { IValidatorRef, useValidatorRef } from './hooks/internal/useValidatorRef'; | ||
import { IValidationSchema } from './types'; | ||
|
||
export interface IValidatorProviderProps<TValue extends object> { | ||
children: React.ReactNode | React.ReactNode[]; | ||
schema: IValidationSchema[]; | ||
value: TValue; | ||
|
||
ref?: React.RefObject<IValidatorRef>; | ||
validateOnChange?: boolean; | ||
validateSync?: boolean; | ||
validationDelay?: number; | ||
abortEarly?: boolean; | ||
} | ||
|
||
export const ValidatorProvider = <TValue extends object>({ | ||
children, | ||
schema, | ||
value, | ||
validateOnChange, | ||
validateSync, | ||
abortEarly, | ||
validationDelay, | ||
ref, | ||
}: IValidatorProviderProps<TValue>) => { | ||
useValidatorRef(ref); | ||
const { errors, validate } = useValidate(value, schema, { | ||
abortEarly, | ||
validateSync, | ||
validateOnChange, | ||
validationDelay, | ||
}); | ||
|
||
const context: IValidatorContext<TValue> = useMemo( | ||
() => ({ errors, values: value, isValid: checkIfValid(errors), validate }), | ||
[errors, value, validate], | ||
); | ||
|
||
return <ValidatorContext.Provider value={context}>{children}</ValidatorContext.Provider>; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import JSONEditor from 'jsoneditor'; | ||
import 'jsoneditor/dist/jsoneditor.css'; | ||
import { FunctionComponent, useEffect, useRef } from 'react'; | ||
|
||
interface IJSONEditorProps { | ||
value: any; | ||
readOnly?: boolean; | ||
onChange?: (value: any) => void; | ||
} | ||
|
||
export const JSONEditorComponent: FunctionComponent<IJSONEditorProps> = ({ | ||
value, | ||
readOnly, | ||
onChange, | ||
}) => { | ||
const containerRef = useRef<HTMLDivElement | null>(null); | ||
const editorRef = useRef<JSONEditor | null>(null); | ||
|
||
useEffect(() => { | ||
if (!containerRef.current) return; | ||
|
||
if (editorRef.current) return; | ||
|
||
editorRef.current = new JSONEditor(containerRef.current!, { | ||
onChange: () => { | ||
editorRef.current && onChange && onChange(editorRef.current.get()); | ||
}, | ||
}); | ||
}, [containerRef, editorRef]); | ||
|
||
useEffect(() => { | ||
if (!editorRef.current) return; | ||
|
||
//TODO: Each set of value rerenders editor and loses focus, find workarounds | ||
editorRef.current.set(value); | ||
}, [editorRef, readOnly]); | ||
|
||
Comment on lines
+31
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect dependencies and remove redundant effect The effect has - useEffect(() => {
- if (!editorRef.current) return;
-
- //TODO: Each set of value rerenders editor and loses focus, find workarounds
- editorRef.current.set(value);
- }, [editorRef, readOnly]); |
||
useEffect(() => { | ||
if (!editorRef.current) return; | ||
|
||
if (readOnly) { | ||
editorRef.current.set(value); | ||
} | ||
}, [editorRef, readOnly, value]); | ||
|
||
useEffect(() => { | ||
if (!editorRef.current) return; | ||
|
||
editorRef.current.setMode(readOnly ? 'view' : 'code'); | ||
}, [readOnly]); | ||
|
||
return <div className="h-full" ref={containerRef} />; | ||
}; | ||
Comment on lines
+52
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error boundary and loading state The component should handle JSON parsing errors gracefully and show a loading state during initialization. + import { ErrorBoundary } from 'react-error-boundary';
+
+ const EditorErrorFallback = ({ error, resetErrorBoundary }) => (
+ <div className="h-full p-4 text-red-500">
+ <p>Error loading editor: {error.message}</p>
+ <button onClick={resetErrorBoundary}>Try again</button>
+ </div>
+ );
export const JSONEditorComponent: FunctionComponent<IJSONEditorProps> = ({
value,
readOnly,
onChange,
}) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const editorRef = useRef<JSONEditor | null>(null);
+ const [isLoading, setIsLoading] = useState(true);
// ... existing effects ...
+ useEffect(() => {
+ if (editorRef.current) {
+ setIsLoading(false);
+ }
+ }, [editorRef.current]);
- return <div className="h-full" ref={containerRef} />;
+ return (
+ <ErrorBoundary FallbackComponent={EditorErrorFallback}>
+ <div className="h-full relative" ref={containerRef}>
+ {isLoading && (
+ <div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-50">
+ Loading editor...
+ </div>
+ )}
+ </div>
+ </ErrorBoundary>
+ );
};
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { useValidator } from '../../../hooks/external/useValidator'; | ||
|
||
export const ErrorsList = () => { | ||
const { errors } = useValidator(); | ||
|
||
return ( | ||
<div className="flex flex-col gap-1"> | ||
{errors.map((error, index) => ( | ||
<div key={index}>{JSON.stringify(error)}</div> | ||
))} | ||
Comment on lines
+8
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid using array index as React key Using array index as key can lead to rendering issues when the array items change. Consider using a unique identifier from the error object or a stable hash of the error content. -{errors.map((error, index) => (
- <div key={index}>{JSON.stringify(error)}</div>
-))}
+{errors.map((error) => (
+ <div key={typeof error === 'string' ? error : JSON.stringify(error)}>
+ {typeof error === 'string' ? error : JSON.stringify(error)}
+ </div>
+))}
|
||
</div> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,68 @@ | ||||||||||||||||||||||||||||||||||||||||||
import { useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||
import { IValidationSchema, registerValidator, ValidatorProvider } from '../../../../Validator'; | ||||||||||||||||||||||||||||||||||||||||||
import { JSONEditorComponent } from '../../../../Validator/_stories/components/JsonEditor/JsonEditor'; | ||||||||||||||||||||||||||||||||||||||||||
import { ValidatorParams } from '../../../../Validator/_stories/components/ValidatorParams'; | ||||||||||||||||||||||||||||||||||||||||||
import { initialContext } from './context'; | ||||||||||||||||||||||||||||||||||||||||||
import { ErrorsList } from './ErrorsList'; | ||||||||||||||||||||||||||||||||||||||||||
import { initialSchema } from './schema'; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const evenNumbersValidator = (value: number) => { | ||||||||||||||||||||||||||||||||||||||||||
// Ignoring validation if value is not a number | ||||||||||||||||||||||||||||||||||||||||||
if (isNaN(value)) return true; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
if (value % 2 !== 0) { | ||||||||||||||||||||||||||||||||||||||||||
throw new Error('Value is not even'); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+9
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve evenNumbersValidator implementation The validator needs several improvements:
-const evenNumbersValidator = (value: number) => {
+const evenNumbersValidator = (value: unknown): boolean => {
// Ignoring validation if value is not a number
- if (isNaN(value)) return true;
+ if (typeof value !== 'number' || Number.isNaN(value)) return true;
if (value % 2 !== 0) {
- throw new Error('Value is not even');
+ throw new Error(`Value ${value} is not an even number`);
}
return true;
}; 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (1.9.4)[error] 11-11: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead. See the MDN documentation for more details. (lint/suspicious/noGlobalIsNan) |
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
registerValidator('evenNumber', evenNumbersValidator); | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add cleanup for validator registration The validator registration should be cleaned up when the component unmounts. +import { useEffect } from 'react';
// ... other code ...
-registerValidator('evenNumber', evenNumbersValidator);
+useEffect(() => {
+ registerValidator('evenNumber', evenNumbersValidator);
+ return () => {
+ // Assuming there's an unregisterValidator function
+ unregisterValidator('evenNumber');
+ };
+}, []);
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
export const Story = () => { | ||||||||||||||||||||||||||||||||||||||||||
const [context, setContext] = useState(initialContext); | ||||||||||||||||||||||||||||||||||||||||||
const [validatorParams, setValidatorParams] = useState<{ | ||||||||||||||||||||||||||||||||||||||||||
validateOnChange?: boolean; | ||||||||||||||||||||||||||||||||||||||||||
validateSync?: boolean; | ||||||||||||||||||||||||||||||||||||||||||
abortEarly?: boolean; | ||||||||||||||||||||||||||||||||||||||||||
validationDelay?: number; | ||||||||||||||||||||||||||||||||||||||||||
}>({ | ||||||||||||||||||||||||||||||||||||||||||
validateOnChange: true, | ||||||||||||||||||||||||||||||||||||||||||
validateSync: false, | ||||||||||||||||||||||||||||||||||||||||||
abortEarly: false, | ||||||||||||||||||||||||||||||||||||||||||
validationDelay: 500, | ||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||
const [schema, setSchema] = useState<IValidationSchema[]>(initialSchema); | ||||||||||||||||||||||||||||||||||||||||||
const [tempSchema, setTempSchema] = useState<IValidationSchema[]>(initialSchema); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||||||
<ValidatorProvider schema={schema} value={context} {...validatorParams}> | ||||||||||||||||||||||||||||||||||||||||||
<div className="flex min-h-screen flex-col gap-4"> | ||||||||||||||||||||||||||||||||||||||||||
<ValidatorParams | ||||||||||||||||||||||||||||||||||||||||||
params={validatorParams} | ||||||||||||||||||||||||||||||||||||||||||
onChange={setValidatorParams} | ||||||||||||||||||||||||||||||||||||||||||
onSave={() => setSchema(tempSchema)} | ||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||
<div className="flex flex-1 flex-row gap-4"> | ||||||||||||||||||||||||||||||||||||||||||
<div className="flex flex-1 flex-col"> | ||||||||||||||||||||||||||||||||||||||||||
<p className="mb-2">Context</p> | ||||||||||||||||||||||||||||||||||||||||||
<div className="flex-1"> | ||||||||||||||||||||||||||||||||||||||||||
<JSONEditorComponent value={context} onChange={setContext} /> | ||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||
<div className="flex flex-1 flex-col"> | ||||||||||||||||||||||||||||||||||||||||||
<div className="mb-2 flex flex-row gap-2"> | ||||||||||||||||||||||||||||||||||||||||||
<p>Validation Schema</p> | ||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||
<div className="flex-1"> | ||||||||||||||||||||||||||||||||||||||||||
<JSONEditorComponent value={tempSchema} onChange={setTempSchema} /> | ||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||
<div className="h-[240px] overflow-y-auto py-2"> | ||||||||||||||||||||||||||||||||||||||||||
<ErrorsList /> | ||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||
</ValidatorProvider> | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export const initialContext = { | ||
firstName: 'John', | ||
lastName: 'Doe', | ||
age: 20, | ||
list: ['item1', 'item2', 'item3'], | ||
nestedList: [ | ||
{ | ||
value: 'value1', | ||
}, | ||
{ | ||
value: 'value2', | ||
}, | ||
{ | ||
value: 'value3', | ||
sublist: [{ value: 'subitem1' }, { value: 'subitem2' }, { value: 'subitem3' }], | ||
}, | ||
], | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Story'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import { IValidationSchema } from '../../../types'; | ||
|
||
export const initialSchema: IValidationSchema[] = [ | ||
{ | ||
id: 'firstname-field', | ||
valueDestination: 'firstName', | ||
validators: [ | ||
{ | ||
type: 'required', | ||
message: 'Name is required', | ||
value: {}, | ||
}, | ||
{ | ||
type: 'minLength', | ||
value: { minLength: 1 }, | ||
message: 'Name must be at least {minLength} characters long', | ||
}, | ||
{ | ||
type: 'maxLength', | ||
value: { maxLength: 10 }, | ||
message: 'Name must be at most {maxLength} characters long', | ||
}, | ||
], | ||
}, | ||
{ | ||
id: 'lastname-field', | ||
valueDestination: 'lastName', | ||
validators: [ | ||
{ | ||
type: 'required', | ||
message: 'Last name is required', | ||
value: {}, | ||
}, | ||
], | ||
}, | ||
{ | ||
id: 'age-field', | ||
valueDestination: 'age', | ||
validators: [ | ||
{ | ||
type: 'required', | ||
message: 'Age is required', | ||
value: {}, | ||
applyWhen: { | ||
type: 'json-logic', | ||
value: { | ||
and: [{ '!!': { var: 'firstName' } }, { '!!': { var: 'lastName' } }], | ||
}, | ||
}, | ||
}, | ||
{ | ||
type: 'minimum', | ||
value: { minimum: 18 }, | ||
message: 'You must be at least {minimum} years old', | ||
}, | ||
{ | ||
type: 'maximum', | ||
value: { maximum: 65 }, | ||
message: 'You must be at most {maximum} years old', | ||
}, | ||
], | ||
}, | ||
{ | ||
id: 'list-field', | ||
valueDestination: 'list', | ||
validators: [ | ||
{ | ||
type: 'required', | ||
message: 'List is required', | ||
value: {}, | ||
}, | ||
{ | ||
type: 'minLength', | ||
value: { minLength: 1 }, | ||
message: 'List must be at least {minLength} items long', | ||
}, | ||
], | ||
children: [ | ||
{ | ||
id: 'list-item', | ||
valueDestination: 'list[$0]', | ||
validators: [ | ||
{ | ||
type: 'maxLength', | ||
message: 'Item must be at most {maxLength} characters long', | ||
value: { maxLength: 5 }, | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
{ | ||
id: 'nested-list', | ||
valueDestination: 'nestedList', | ||
validators: [ | ||
{ | ||
type: 'required', | ||
value: {}, | ||
message: 'Nested list is required', | ||
}, | ||
], | ||
children: [ | ||
{ | ||
id: 'nested-list-item-value', | ||
valueDestination: 'nestedList[$0].value', | ||
validators: [ | ||
{ | ||
type: 'required', | ||
value: {}, | ||
message: 'Nested list item value is required', | ||
}, | ||
], | ||
}, | ||
{ | ||
id: 'nested-list-item-sublist', | ||
valueDestination: 'nestedList[$0].sublist', | ||
validators: [], | ||
children: [ | ||
{ | ||
id: 'nested-list-subitem', | ||
valueDestination: 'nestedList[$0].sublist[$1].value', | ||
validators: [ | ||
{ | ||
type: 'maxLength', | ||
value: { maxLength: 10 }, | ||
message: 'Subitem must be at most {maxLength} characters long', | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
]; | ||
Comment on lines
+1
to
+134
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Codebase verification Additional test coverage needed for complex validation scenarios Based on the test files examined, there are gaps in test coverage that need to be addressed:
The existing tests only cover basic validator functionality (min-length, required) and registration, but not the complex scenarios present in the schema. 🔗 Analysis chainEnsure comprehensive test coverage Given the complexity of the validation schema, especially with nested structures and conditional validations, ensure that there are comprehensive tests covering:
Let's check the test coverage: 🏁 Scripts executedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Search for related test files
fd -g "*validator*.test.{ts,tsx}"
Length of output: 1186 Script: #!/bin/bash
# Let's check the content of some key test files to verify coverage of complex scenarios
echo "=== Checking test coverage for nested structures ==="
rg -A 5 "describe.*nest" packages/ui/src/components/organisms/Form/Validator/**/*.test.{ts,tsx}
echo -e "\n=== Checking test coverage for conditional validation ==="
rg -A 5 "describe.*condition|applyWhen" packages/ui/src/components/organisms/Form/Validator/**/*.test.{ts,tsx}
echo -e "\n=== Checking test coverage for array handling ==="
rg -A 5 "describe.*array|\[\$\d\]" packages/ui/src/components/organisms/Form/Validator/**/*.test.{ts,tsx}
echo -e "\n=== Checking test coverage for error message formatting ==="
rg -A 5 "message.*\{.*\}" packages/ui/src/components/organisms/Form/Validator/**/*.test.{ts,tsx}
Length of output: 2012 Script: #!/bin/bash
# Let's try a different approach to check test contents
# First, let's check min-length validator tests as they handle array scenarios
cat packages/ui/src/components/organisms/Form/Validator/validators/min-length/min-length.validator.unit.test.ts
echo -e "\n=== Checking required validator for conditional validation ==="
cat packages/ui/src/components/organisms/Form/Validator/validators/required/required.validator.unit.test.ts
echo -e "\n=== Checking validator registration for nested structures ==="
cat packages/ui/src/components/organisms/Form/Validator/utils/register-validator/register-validator.unit.test.ts
Length of output: 4276 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix useEffect dependencies and add cleanup
The current implementation has two issues:
onChange
in the dependencies array could lead to stale closures📝 Committable suggestion