Skip to content

Commit

Permalink
feat: specific field/input error reporting methods
Browse files Browse the repository at this point in the history
  • Loading branch information
danielo515 committed Oct 28, 2023
1 parent 1995286 commit 3bfd22d
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 33 deletions.
56 changes: 48 additions & 8 deletions src/core/findInputDefinitionSchema.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import { parse, pipe } from "@std";
import { A, parse, pipe } from "@std";
import * as E from "fp-ts/Either";
import { ValiError, BaseSchema } from "valibot";
import { FieldMinimal, FieldMinimalSchema, InputTypeToParserMap } from "./formDefinitionSchema";
import { AllFieldTypes } from "./formDefinition";
import { FieldMinimal, InputTypeToParserMap, FieldMinimalSchema } from "./formDefinitionSchema";


function stringifyError(error: ValiError) {
return error.issues.map((issue) => `${issue.path?.map((i) => i.key)}: ${issue.message} got ${issue.input}`).join(', ');
}
export class InvalidInputTypeError {
static readonly _tag = "InvalidInputTypeError" as const;
constructor(public input: unknown) { }
constructor(readonly input: unknown) { }
toString(): string {
return `InvalidInputTypeError: ${JSON.stringify(this.input)}`;
return `InvalidInputTypeError: "input.type" is invalid, got: ${JSON.stringify(this.input)}`;
}
}

export class InvalidInputError {
static readonly _tag = "InvalidInputError" as const;
constructor(public input: FieldMinimal, readonly error: ValiError) { }
toString(): string {
return `InvalidInputError: ${this.error.issues.map((issue) => issue.message).join(', ')}`;
return `InvalidInputError: ${stringifyError(this.error)}`;
}
}

export class InvalidFieldError {
static readonly _tag = "InvalidFieldError" as const;
constructor(public field: unknown, readonly error: ValiError) { }
toString(): string {
return `InvalidFieldError: ${stringifyError(this.error)}`;
}
static of(field: unknown) {
return (error: ValiError) => new InvalidFieldError(field, error);
}
}

function isValidInputType(input: unknown): input is AllFieldTypes {
return 'string' === typeof input && input in InputTypeToParserMap;
}
Expand All @@ -33,13 +46,40 @@ function isValidInputType(input: unknown): input is AllFieldTypes {
* @param fieldDefinition a field definition to find the input schema for
* @returns a tuple of the basic field definition and the input schema
*/
export function findInputDefinitionSchema(fieldDefinition: unknown): E.Either<ValiError | InvalidInputTypeError, [FieldMinimal, BaseSchema]> {
export function findInputDefinitionSchema(fieldDefinition: unknown): E.Either<InvalidFieldError | InvalidInputTypeError, [FieldMinimal, BaseSchema]> {
return pipe(
parse(FieldMinimalSchema, fieldDefinition),
E.mapLeft(InvalidFieldError.of(fieldDefinition)),
E.chainW((field) => {
const type = field.input.type;
if (isValidInputType(type)) return E.right([field, InputTypeToParserMap[type]]);
else return E.left(new InvalidInputTypeError(type));
})
);
}
/**
* Given an array of fields that have failed to parse,
* this function tries to find the corresponding input schema
* and then parses the input with that schema to get the specific errors.
* The result is an array of field errors.
* This is needed because valibot doesn't provide a way to get the specific error of union types
*/
export function findFieldErrors(fields: unknown[]) {
return pipe(
fields,
A.map((fieldUnparsed) => {
return pipe(
findInputDefinitionSchema(fieldUnparsed),
E.chainW(([field, inputSchema]) => pipe(
parse(inputSchema, field.input),
E.bimap(
(error) => new InvalidInputError(field, error),
() => field
)),
))
}),
// A.partition(E.isLeft),
// Separated.right,
);

}
23 changes: 23 additions & 0 deletions src/core/formDefinitionSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { A, E, pipe } from "@std";
import { findFieldErrors } from "./findInputDefinitionSchema";

describe("findFieldErrors", () => {
it("should return an empty array of detailed errors or unchanged fields if they are correct", () => {
const fields = [
{ name: 'fieldName', description: 'field description', input: { type: 'text' } },
{ name: 'fieldName', input: { type: 'text' } },
{ name: 'fieldName', description: '', input: { type: '' } },
{},
];
const errors = pipe(
findFieldErrors(fields),
A.map(E.mapLeft((e) => e.toString()))
)
expect(errors).toHaveLength(4);
expect(errors[0]).toEqual(E.right({ name: 'fieldName', description: 'field description', input: { type: 'text' } }));
expect(errors[1]).toEqual(E.left('InvalidFieldError: description: Invalid type got undefined'));
expect(errors[2]).toEqual(E.left('InvalidInputTypeError: "input.type" is invalid, got: ""'));
expect(errors[3]).toEqual(E.left('InvalidFieldError: name: field name should be a string got undefined, description: Invalid type got undefined, input: Invalid type got undefined'));
});

});
37 changes: 14 additions & 23 deletions src/core/formDefinitionSchema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { A, parse, pipe } from "@std";
import * as E from "fp-ts/Either";
import { pipe, parse } from "@std";
import { object, number, literal, type Output, is, array, string, union, optional, minLength, toTrimmed, merge, unknown, ValiError, BaseSchema, enumType, passthrough } from "valibot";
import { AllFieldTypes, FormDefinition } from "./formDefinition";
import * as Separated from "fp-ts/Separated";
import { findInputDefinitionSchema, InvalidInputError } from "./findInputDefinitionSchema";
import { findFieldErrors } from "./findInputDefinitionSchema";

/**
* Here are the core logic around the main domain of the plugin,
Expand Down Expand Up @@ -56,19 +55,24 @@ export const InputTypeToParserMap: Record<AllFieldTypes, BaseSchema> = {
dataview: InputDataviewSourceSchema,
multiselect: MultiselectSchema,
};
export const FieldMinimalSchema = passthrough(object({
name: string(),
input: object({ type: string() })
}));

export type FieldMinimal = Output<typeof FieldMinimalSchema>;

export const FieldDefinitionSchema = object({
name: nonEmptyString('field name'),
label: optional(string()),
description: string(),
input: InputTypeSchema
});
/**
* Only for error reporting purposes
*/
export const FieldMinimalSchema = passthrough(merge([
FieldDefinitionSchema,
object({ input: object({ type: string() }) })
]));

export type FieldMinimal = Output<typeof FieldMinimalSchema>;


export const FieldListSchema = array(FieldDefinitionSchema);
/**
* This is the most basic representation of a form definition.
Expand Down Expand Up @@ -117,20 +121,7 @@ export class MigrationError {
return this.form;
}
get fieldErrors() {
return pipe(
this.form.fields,
A.map((field) => {
return pipe(
findInputDefinitionSchema(field),
E.chainW(([field, inputSchema]) => pipe(
parse(inputSchema, field.input),
E.mapLeft((error) => new InvalidInputError(field, error))
)),
)
}),
A.partition(E.isLeft),
Separated.right,
);
return findFieldErrors(this.form.fields);
}
}
/**
Expand Down
5 changes: 3 additions & 2 deletions src/std/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { pipe as p } from "fp-ts/function";
import { partitionMap, partition, map as mapArr } from "fp-ts/Array";
import { isLeft, isRight, tryCatchK, map, getOrElse, right, left } from "fp-ts/Either";
import { isLeft, isRight, tryCatchK, map, getOrElse, right, left, mapLeft } from "fp-ts/Either";
import { ValiError, parse as parseV } from "valibot";

export const pipe = p
Expand All @@ -17,7 +17,8 @@ export const E = {
right,
tryCatchK,
getOrElse,
map
map,
mapLeft,
}

export const parse = tryCatchK(parseV, (e: unknown) => e as ValiError)

0 comments on commit 3bfd22d

Please sign in to comment.