Skip to content
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

Add support for a custom validation function #137

Merged
merged 9 commits into from
May 12, 2019
Merged

Add support for a custom validation function #137

merged 9 commits into from
May 12, 2019

Conversation

SamVerschueren
Copy link
Collaborator

@SamVerschueren SamVerschueren commented Mar 14, 2019

This PR fixes #40 by adding support for

// Custom function
ow('foo', ow.string.validate(value => ({
    message: 'Expected the value to be foo',
    validator: value === 'foo'
});

The message can also be a function which passes in the (inferred) label. Thought it would be nice to have that as it allows you to customize the error message even more.

I also added a test for custom predicates which was actually already supported. Not sure if creating a custom predicate should be documented? I've started my ow-date package so I could link to that one in the docs.

Let me know if something is unclear or should be added.

@SamVerschueren
Copy link
Collaborator Author

SamVerschueren commented Mar 14, 2019

When I was looking into my ow-date package, I noticed that extending from Predicate is actually not that feasible at the moment. The reason is that the addValidator is marked as @internal which means I can't call it in my package as it does not exist for me.

The reason why we do this is to make sure users don't get it autocompleted when they type ow.string.<addValidator>.

Not sure how we should fix this. I see two options here.

1. Export a custom class

We might export a stripped down version of Predicate, something like CustomPredicate which is solely used for the purpose of creating custom predicates.

class CustomPredicate<T> extends Predicate<T> {
	constructor(type: string) {
		super(type);
	}

	/**
	 * Register a new validator.
	 *
	 * @param validator Validator to register.
	 */
	addValidator(validator: Validator<T>) {
		return super.addValidator(validator);
	}
}

The downside of this is that if I would publish my ow-date package, people will still see the owDate.<addValidator> method...

2. Remove addValidator alltogether

We could remove the addValidator method from the Predicate class and use a utility function instead.

const addValidator = <T>(predicate: Predicate<T>, validator: Validator<T>) => {
	predicate[validatorSymbol].push(validator);

	return predicate;
};

And refactor all the this.addValidator(validator) calls to addValidator(this, validator).

The benefit is that it's just gone from Predicate and thus uncalleable because it doesn't exist and we don't need the hacky @internal. Then we can just export the addValidator utility function so people creating custom validators can import it

export class DatePredicate extends Predicate<Date> {
	constructor() {
		super('date');
	}

	/**
	 * Test a date to be before another date.
	 *
	 * @param date - Maximum value.
	 */
	before(date: Date) {
		return addValidator(this, {
			message: (value, label) => `Expected ${label} ${value.toISOString()} to be before ${date.toISOString()}`,
			validator: value => value.getTime() < date.getTime()
		});
	}
}

@sindresorhus
Copy link
Owner

I definitely prefer option 2.

@SamVerschueren
Copy link
Collaborator Author

SamVerschueren commented Mar 15, 2019

Definitely my preference as well, the first one has quite a lot of drawbacks. I'll have it fixed!

@SamVerschueren
Copy link
Collaborator Author

I'm stuck on this problem for a couple of days now and I can't find a solution. So I tried with the addValidator() function which would mean I can do this

greaterThan(x: number) {
	return addValidator(this, {
		message: (value, label) => `Expected ${label} to be greater than ${x}, got ${value}`,
		validator: value => value > x
	});
}

Whereas addValidator is defined as

export default <T, P extends Predicate<T> = Predicate<T>>(predicate: P, validator: Validator<T>) => {
	predicate[validatorSymbol].push(validator);

	return predicate;
};

This doesn't work. TypeScript is unable to infer the type of T because this is a special type in TypeScript. So this is not the same as NumberPredicate and thus not a Predicate<Number>. Which means that it infers T as {}.

A fix would be to provide type T

greaterThan(x: number) {
	return addValidator<number>(this, {
		message: (value, label) => `Expected ${label} to be greater than ${x}, got ${value}`,
		validator: value => value > x
	});
}

And then it correctly verifies that this is assignable to Predicate<number>. But I'm not sure if I like this, it feels like overhead but it's the only way I could make this work.


So I started looking at it again and one approach would be to mark addValidator as protected and still call this.addValidator like we had before.

But then I don't have a solution for the not modifier because it uses predicate.addValidator which doesn't work when it's protected.


After I wrote all of this above I noticed that actually it's impossible to remove addValidator from the Predicate class. The reason is that the not modifier hooks into the addValidator function, which wouldn't be possible with a addValidator utility function.

If we make it protected, this would force us to cast predicate to any in the not modifier and loose the types.

export const not = (predicate: any) => {
	const originalAddValidator = predicate.addValidator;

	predicate.addValidator = (validator: any) => {
		const fn = validator.validator;
		const message = validator.message;

		validator.message = (x: any, label: string) => `[NOT] ${message(x, label)}`;
		validator.validator = (x: any) => !fn(x);

		predicate[validatorSymbol].push(validator);

		predicate.addValidator = originalAddValidator;

		return predicate;
	};

	return predicate;
};

We loose the types inside the not modifier, which is not a big problem as the types are not exposed to the outside etc. but not sure if it's a clean solution.

To be honest, it feels like the only solution at the moment 😂.

@sindresorhus
Copy link
Owner

We loose the types inside the not modifier, which is not a big problem as the types are not exposed to the outside etc. but not sure if it's a clean solution.

Let's go with that and open a new issue about improving it. Maybe someone else sees a solution we don't. Since it's not exposed to users, it's not essential. Better to just make it works now.

@sindresorhus sindresorhus changed the title Add support for a custom validation function - fixes #40 Add support for a custom validation function Apr 17, 2019
readme.md Outdated Show resolved Hide resolved
readme.md Outdated Show resolved Hide resolved
readme.md Outdated Show resolved Hide resolved
readme.md Show resolved Hide resolved
Co-Authored-By: SamVerschueren <sam.verschueren@gmail.com>
readme.md Outdated Show resolved Hide resolved
@sindresorhus
Copy link
Owner

Is this ready to merge?

Co-Authored-By: SamVerschueren <sam.verschueren@gmail.com>
@SamVerschueren
Copy link
Collaborator Author

The only thing I didn't do yet is #137 (comment). Let me do it now.

@SamVerschueren
Copy link
Collaborator Author

Think it's good to go

@sindresorhus sindresorhus merged commit 702c283 into master May 12, 2019
@sindresorhus sindresorhus deleted the iss40 branch May 12, 2019 08:23
@sindresorhus
Copy link
Owner

Awesome :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Make it easy to extend and add custom predicates
2 participants