-
Notifications
You must be signed in to change notification settings - Fork 105
Description
I have a .proto file as follows:
message SignupRequest {
string full_name = 1 [(buf.validate.field).string = {
min_len: 2
max_len: 100
// Support Unicode letters, spaces, hyphens, apostrophes, and periods
// No leading/trailing spaces, no multiple consecutive spaces
// Allows names in any language/script (Unicode letters)
pattern: "^[\\p{L}][\\p{L}\\s'\\-\\.]*[\\p{L}]$"
}];
string email = 2 [(buf.validate.field).string.email = true];
string password = 3 [(buf.validate.field).string = {
min_len: 8
max_len: 128
}];
Region region = 4 [(buf.validate.field).enum = {
defined_only: true
not_in: [0] /* REGION_UNSPECIFIED is not allowed */
}];
}I generated the _pb.ts files using protoc-gen-es and I am able to use it in a simple form library as below:
<Form
form={form}
onFinish={handleSubmit}
onFinishFailed={onFinishFailed}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
autoComplete="off"
>
<Form.Item<FieldType> label="Full Name" name="full_name">
<Input
placeholder="John Doe"
onBlur={(e) => {
const value = e.target.value;
const request = create(SignupRequestSchema, { fullName: value });
const result = validator.validate(SignupRequestSchema, request);
if (result.kind !== "valid") {
const errors =
result.violations
?.filter((v) => v.field[0] === fullNameField)
.map((v) => v.message) || [];
form.setFields([{ name: "full_name", errors }]);
} else {
form.setFields([{ name: "full_name", errors: [] }]);
}
}}
/>
</Form.Item>
<Form.Item<FieldType> label="Email" name="email">
<Input
placeholder="[email protected]"
onBlur={(e) => {
const value = e.target.value;
const request = create(SignupRequestSchema, { email: value });
const result = validator.validate(SignupRequestSchema, request);
if (result.kind !== "valid") {
const errors =
result.violations
?.filter((v) => v.field[0] === emailField)
.map((v) => v.message) || [];
form.setFields([{ name: "email", errors }]);
} else {
form.setFields([{ name: "email", errors: [] }]);
}
}}
/>
</Form.Item>
<Form.Item<FieldType> label="Password" name="password">
<Input.Password
placeholder="Enter a strong password"
onBlur={(e) => {
const value = e.target.value;
const request = create(SignupRequestSchema, { password: value });
const result = validator.validate(SignupRequestSchema, request);
if (result.kind !== "valid") {
const errors =
result.violations
?.filter((v) => v.field[0] === passwordField)
.map((v) => v.message) || [];
form.setFields([{ name: "password", errors }]);
} else {
form.setFields([{ name: "password", errors: [] }]);
}
}}
/>
</Form.Item>
<Form.Item<FieldType> label="Region" name="region">
<Select
placeholder="Select a region"
onBlur={() => {
const value = form.getFieldValue("region");
const request = create(SignupRequestSchema, { region: value });
const result = validator.validate(SignupRequestSchema, request);
if (result.kind !== "valid") {
const errors =
result.violations
?.filter((v) => v.field[0] === regionField)
.map((v) => v.message) || [];
form.setFields([{ name: "region", errors }]);
} else {
form.setFields([{ name: "region", errors: [] }]);
}
}}
onChange={(value) => form.setFieldValue("region", value)}
>
<Select.Option value={Region.USA}>USA</Select.Option>
<Select.Option value={Region.EUR}>Europe</Select.Option>
<Select.Option value={Region.IND}>India</Select.Option>
<Select.Option value={Region.SGP}>Singapore</Select.Option>
</Select>
</Form.Item>
<Form.Item label={null}>
<Button type="primary" htmlType="submit" loading={loading}>
Submit
</Button>
</Form.Item>
</Form>It is a simple form with just 4 fields, where each field on losing focus (when tab is pressed after entering values there), a validation is performed using the buf generated code.
The tsx above is formatted using the standard prettier tool.
The frustration here is, the validation code is quite verbose, long and occupies a lot more lines than the core UI code. If buf can generate zod or jsonschema or some such, the validation code will become a lot shorter like:
export const signupSchema = z.object({
full_name: z
.string()
.min(2, "Full name must be at least 2 characters")
.max(100, "Full name must not exceed 100 characters")
.regex(
/^[\p{L}][\p{L}\s'\-.]*[\p{L}]$/u,
"Full name must start and end with a letter, and can only contain letters, spaces, hyphens, apostrophes, and periods"
),
email: z.string().email("Invalid email address"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.max(128, "Password must not exceed 128 characters"),
region: z.nativeEnum(Region).refine((val) => val !== Region.UNSPECIFIED, {
message: "Please select a region",
}),
});
const validateField = (fieldName: keyof SignupFormData, value: any) => {
try {
signupSchema.shape[fieldName].parse(value);
form.setFields([{ name: fieldName, errors: [] }]);
} catch (error: any) {
if (error.errors) {
form.setFields([{ name: fieldName, errors: [error.errors[0].message] }]);
}
}
};<Form
form={form}
onFinish={handleSubmit}
onFinishFailed={onFinishFailed}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
autoComplete="off"
>
<Form.Item<SignupFormData> label="Full Name" name="full_name">
<Input
placeholder="John Doe"
onBlur={(e) => validateField("full_name", e.target.value)}
/>
</Form.Item>
<Form.Item<SignupFormData> label="Email" name="email">
<Input
placeholder="[email protected]"
onBlur={(e) => validateField("email", e.target.value)}
/>
</Form.Item>
<Form.Item<SignupFormData> label="Password" name="password">
<Input.Password
placeholder="Enter a strong password"
onBlur={(e) => validateField("password", e.target.value)}
/>
</Form.Item>
<Form.Item<SignupFormData> label="Region" name="region">
<Select
placeholder="Select a region"
onBlur={() => validateField("region", form.getFieldValue("region"))}
onChange={(value) => form.setFieldValue("region", value)}
>
<Select.Option value={Region.USA}>USA</Select.Option>
<Select.Option value={Region.EUR}>Europe</Select.Option>
<Select.Option value={Region.IND}>India</Select.Option>
<Select.Option value={Region.SGP}>Singapore</Select.Option>
</Select>
</Form.Item>
<Form.Item label={null}>
<Button type="primary" htmlType="submit" loading={loading}>
Submit
</Button>
</Form.Item>
</Form>As you can see, the code is a lot shorter with zod.
I wish connect-es generates zod or jsonschema or some such to make the validations on the client side easier.
The only alternative that I could think of is, to create the zod schema manually from the protobuf, but, that is highly error prone.
If you can recommend any alternative to keep the client side code shorter, I am open to it. I tried creating a common function for the _pb.ts file also, but I could not achieve it without losing on the type safety or across multiple types of widgets.