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 composition of validators #111

Closed
Eoksni opened this issue Dec 12, 2019 · 1 comment
Closed

Add composition of validators #111

Eoksni opened this issue Dec 12, 2019 · 1 comment

Comments

@Eoksni
Copy link

Eoksni commented Dec 12, 2019

I have a need of composing two validators, ie the value can satisfy either of validators. My use-case is origin url for cors setup - it can either be an actual url or a string '*' to allow any origin.

Here is my take on this:

// #region `oneOf` definition
type MkValidator<T> = (spec?: Spec<T>) => ValidatorSpec<T>;

// helper function to encapsulate usage of internals
function parse<T>(val: ValidatorSpec<T>, x: string): T {
  const res = val._parse(x);
  if (val.choices) {
    if (val.choices.indexOf(res) === -1) {
      throw new Error(
        `Value "${x}" not in choices: [${val.choices.join(", ")}]`
      );
    } else {
      return res;
    }
  } else {
    return res;
  }
}

// define `oneOf` combinator
const oneOf = <T>(
  val: ValidatorSpec<T>,
  ...vals: ValidatorSpec<T>[]
): MkValidator<T> => {
  const all = [val, ...vals];

  // I went with simpler approach to specify all documentary stuff on resulting validator
  // and reject all these properties on sub validators.
  for (const v of all) {
    if (v.default !== undefined) {
      throw new Error(
        `'default' property is not supported in oneOf sub validators.`
      );
    }
    if (v.devDefault !== undefined) {
      throw new Error(
        `'devDefault' property is not supported in oneOf sub validators.`
      );
    }
    if (v.desc !== undefined) {
      throw new Error(
        `'desc' property is not supported in oneOf sub validators.`
      );
    }
    if (v.docs !== undefined) {
      throw new Error(
        `'docs' property is not supported in oneOf sub validators.`
      );
    }
    if (v.example !== undefined) {
      throw new Error(
        `'example' property is not supported in oneOf sub validators.`
      );
    }
  }

  const types = all.map(v => v.type);
  const type = `oneOf: [${types.join(", ")}]`;

  return makeValidator(x => {
    try {
      return parse(val, x);
    } catch (err) {
      const errs = [err];
      for (const v of vals) {
        try {
          return parse(v, x);
        } catch (e) {
          errs.push(e);
          continue;
        }
      }
      throw new Error(
        `oneOf validator failed: [\n${errs.map(e => e.message).join(",\n")}\n]`
      );
    }
  }, type);
};
// #endregion

And here is the usage:

// creating `origin` validator using `oneOf` combinator
const origin = oneOf(url(), str({ choices: ["*"] }));

export default cleanEnv(
  process.env,
  {
    // example usage of newly created validator
    FRONTEND_ORIGIN: origin({
      devDefault: "*",
      example: "http://localhost:3000"
    })
  }
);
@af
Copy link
Owner

af commented Dec 19, 2019

This is a neat trick, but I don't think it's going to be used commonly enough to justify inclusion in the library itself. As a result I'm going to close this issue, but thanks for posting this, I think some people might find it useful 👍

@af af closed this as completed Dec 19, 2019
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

No branches or pull requests

2 participants