1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Introduce unionizing functions for input validators

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-05-30 10:49:28 -04:00
parent 87b2c39f67
commit 8ecd19701f
4 changed files with 159 additions and 23 deletions

View File

@ -9,12 +9,13 @@ import { prevDefault } from "../../utils";
import { Button } from "../button";
import { Icon } from "../icon";
import { observer } from "mobx-react";
import { asyncInputValidator, Input, InputValidators } from "../input";
import { Input, InputValidators } from "../input";
import { SubTitle } from "../layout/sub-title";
import { TooltipPosition } from "../tooltip";
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import { unionInputValidatorsAsync } from "../input/input_validators";
export interface InstallProps {
installPath: string;
@ -28,23 +29,14 @@ interface Dependencies {
extensionInstallationStateStore: ExtensionInstallationStateStore;
}
const installInputValidator = asyncInputValidator({
validate: async (value) => {
if (
InputValidators.isUrl.validate(value)
|| InputValidators.isExtensionNameInstall.validate(value)
) {
return;
}
try {
return await InputValidators.isPath.validate(value);
} catch {
throw new Error("Invalid URL, absolute path, or extension name");
}
const installInputValidator = unionInputValidatorsAsync(
{
message: "Invalid URL, absolute path, or extension name",
},
debounce: InputValidators.isPath.debounce,
});
InputValidators.isUrl,
InputValidators.isExtensionNameInstall,
InputValidators.isPath,
);
const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
installPath,

View File

@ -3,11 +3,86 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isEmail, isUrl, systemName } from "../input_validators";
import { isEmail, isUrl, systemName, unionInputValidators, unionInputValidatorsAsync } from "../input_validators";
type TextValidationCase = [string, boolean];
describe("input validation tests", () => {
describe("unionInputValidators()", () => {
const emailOrUrl = unionInputValidators(
{
message: "Not an email or URL",
},
isEmail,
isUrl,
);
it.each([
"abc@news.com",
"abc@news.co.uk",
])("Given '%s' is a valid email, emailOrUrl matches", (input) => {
expect(emailOrUrl.validate(input)).toBe(true);
});
it.each([
"https://github-production-registry-package-file-4f11e5.s3.amazonaws.com/307985088/68bbbf00-309f-11eb-8457-a15e4efe9e77?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201127%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20201127T123754Z&X-Amz-Expires=300&X-Amz-Signature=9b8167f00685a20d980224d397892195abc187cdb2934cefb79edcd7ec600f78&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=0&response-content-disposition=filename%3Dstarboard-lens-extension-0.0.1-alpha.1-npm.tgz&response-content-type=application%2Foctet-stream",
"http://www.google.com",
])("Given '%s' is a valid url, emailOrUrl matches", (input) => {
expect(emailOrUrl.validate(input)).toBe(true);
});
it.each([
"hello",
"57",
])("Given '%s' is neither a valid email nor URL, emailOrUrl does not match", (input) => {
expect(emailOrUrl.validate(input)).toBe(false);
});
});
describe("unionInputValidatorsAsync()", () => {
const emailOrUrl = unionInputValidatorsAsync(
{
message: "Not an email or URL",
},
isEmail,
isUrl,
);
it.each([
"abc@news.com",
"abc@news.co.uk",
])("Given '%s' is a valid email, emailOrUrl matches", async (input) => {
try {
await emailOrUrl.validate(input);
} catch {
fail("Should not throw on valid input");
}
});
it.each([
"https://github-production-registry-package-file-4f11e5.s3.amazonaws.com/307985088/68bbbf00-309f-11eb-8457-a15e4efe9e77?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201127%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20201127T123754Z&X-Amz-Expires=300&X-Amz-Signature=9b8167f00685a20d980224d397892195abc187cdb2934cefb79edcd7ec600f78&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=0&response-content-disposition=filename%3Dstarboard-lens-extension-0.0.1-alpha.1-npm.tgz&response-content-type=application%2Foctet-stream",
"http://www.google.com",
])("Given '%s' is a valid url, emailOrUrl matches", async (input) => {
try {
await emailOrUrl.validate(input);
} catch {
fail("Should not throw on valid input");
}
});
it.each([
"hello",
"57",
])("Given '%s' is neither a valid email nor URL, emailOrUrl does not match", async (input) => {
try {
await emailOrUrl.validate(input);
fail("Should throw on invalid input");
} catch {
// We want this to happen
}
});
});
describe("isEmail tests", () => {
const tests: TextValidationCase[] = [
["abc@news.com", true],

View File

@ -16,13 +16,23 @@ import type { InputValidator, InputValidation, InputValidationResult, SyncValida
import uniqueId from "lodash/uniqueId";
import { debounce } from "lodash";
const { conditionalValidators, asyncInputValidator, inputValidator, inputValidatorWithRequiredProps, ...InputValidators } = Validators;
const {
conditionalValidators,
asyncInputValidator,
inputValidator,
inputValidatorWithRequiredProps,
isAsyncValidator,
unionInputValidatorsAsync,
...InputValidators
} = Validators;
export {
InputValidators,
asyncInputValidator,
inputValidator,
inputValidatorWithRequiredProps,
isAsyncValidator,
unionInputValidatorsAsync,
};
export type {
InputValidator,
@ -87,10 +97,6 @@ const defaultProps: Partial<InputProps> = {
blurOnEnter: true,
};
function isAsyncValidator<RequireProps extends boolean>(validator: InputValidator<boolean, RequireProps>): validator is InputValidator<true, RequireProps> {
return typeof validator.debounce === "number";
}
export class Input extends React.Component<InputProps, State> {
static defaultProps = defaultProps as object;

View File

@ -7,6 +7,7 @@ import type { InputProps } from "./input";
import type React from "react";
import fse from "fs-extra";
import { TypedRegEx } from "typed-regex";
import type { SetRequired } from "type-fest";
export type InputValidationResult<IsAsync extends boolean> =
IsAsync extends true
@ -48,6 +49,10 @@ export type InputValidator<IsAsync extends boolean = boolean, RequireProps exten
}
);
export function isAsyncValidator<RequireProps extends boolean>(validator: InputValidator<boolean, RequireProps>): validator is InputValidator<true, RequireProps> {
return typeof validator.debounce === "number";
}
export function asyncInputValidator(validator: InputValidator<true, false>): InputValidator<true, false> {
return validator;
}
@ -60,6 +65,64 @@ export function inputValidatorWithRequiredProps(validator: InputValidator<false,
return validator;
}
/**
* Create a new input validator from a list of syncronous input validators. Will match as valid if
* one of the input validators matches the input
*/
export function unionInputValidators(
baseValidator: Pick<InputValidator<false, false>, "condition" | "message">,
...validators: InputValidator<false, false>[]
): InputValidator<false, false> {
return inputValidator({
...baseValidator,
validate: (value, props) => validators.some(validator => validator.validate(value, props)),
});
}
/**
* Create a new input validator from a list of syncronous or async input validators. Will match as
* valid if one of the input validators matches the input
*/
export function unionInputValidatorsAsync(
baseValidator: SetRequired<Pick<InputValidator<boolean, false>, "condition" | "message">, "message">,
...validators: InputValidator<boolean, false>[]
): InputValidator<true, false> {
const longestDebounce = Math.max(
...validators
.filter(isAsyncValidator)
.map(validator => validator.debounce),
0,
);
return asyncInputValidator({
debounce: longestDebounce,
validate: async (value, props) => {
for (const validator of validators) {
if (isAsyncValidator(validator)) {
try {
await validator.validate(value, props);
return;
} catch {
// Do nothing
}
} else {
if (validator.validate(value, props)) {
return;
}
}
}
/**
* If no validator returns `true` then mark as invalid by throwing. The message will be
* obtained from the `message` field.
*/
throw {};
},
...baseValidator,
});
}
export const isRequired = inputValidator({
condition: ({ required }) => required,
message: () => `This field is required`,