UNPKG

@macroforge/mcp-server

Version:

MCP server for Macroforge documentation and code analysis

176 lines (137 loc) • 8.6 kB
# Validators Validators run during decoding. Failures are reported through the `errors` array of the result union rather than thrown, so a single call reports every problem at once. ## Three ways to apply them ```typescript /** @derive(Decode) */ class User { /** @endec(validate: ["email", "maxLength(255)"]) */ email: string; /** @endec(validate: [{ validate: "minLength(2)", message: "Name too short" }]) */ name: string; /** @endec(minLength(8), maxLength(64)) */ password: string; } ``` 1. **Array of strings**: `validate: ["email", "maxLength(255)"]` 2. **Array of objects**: adds a custom `message` per validator 3. **Shorthand**: bare validator calls directly in `@endec(...)` Validators check the declared type's value. On a nullable or optional field (`string | null`, `name?: string`) a `null` or absent value is not checked, and a present value is checked like any other. On any other field a `null` value fails with _"is required"_. `validate` must be an **array**. An object literal such as `validate: { email: true }` is rejected with _"validate must be an array"_. Misspelled names are caught with a suggestion: the parser does a Levenshtein match against the known list and tells you what you probably meant. ## On a primitive alias Validators can also go on a type alias of a primitive, plain or branded with [`$Newtype`](/docs/builtin-macros/newtype). They then validate the value itself, and every field typed with the alias gets them through the alias's `decode`: ```typescript /** @derive(Encode, Decode) */ /** @endec(nonNegative, finite) */ type Meters = $Newtype<number>; Meters.decode(-1); // { success: false, errors: [{ field: "_root", message: "Meters must be non-negative" }] } Meters.is(12); // true ``` On a field such as `distance: Meters`, the error's `field` is `distance`. Alias-level validators on any other alias shape are rejected at expansion; put them on the fields. ## On a union arm A primitive arm of a union alias can carry its own validators in a leading comment. They run only when the value takes that arm: ```typescript /** @derive(Decode) */ type Contact = /** @endec(email) */ string | number; Contact.decode('not-an-email'); // { success: false, errors: [{ field: "_root", message: "Contact must be a valid email" }] } Contact.decode(42); // { success: true, value: 42 } ``` Validators on any other arm are rejected at expansion; put them on the variant's fields. ## String validators | Validator | Checks | | ------------------ | -------------------------------------------- | | `email` | Valid email address | | `url` | Parses as a URL (`URL.canParse`), any scheme | | `uuid` | Valid UUID | | `pattern("re")` | Matches the regular expression | | `minLength(n)` | Length ≥ n | | `maxLength(n)` | Length ≤ n | | `length(n)` | Length exactly n | | `length(min, max)` | Length within range | | `nonEmpty` | Not the empty string | | `trimmed` | No leading/trailing whitespace | | `lowercase` | All lowercase | | `uppercase` | All uppercase | | `capitalized` | First character uppercase | | `uncapitalized` | First character lowercase | | `startsWith(s)` | Has the prefix | | `endsWith(s)` | Has the suffix | | `includes(s)` | Contains the substring | ## Number validators | Validator | Checks | | ------------------------- | -------------------------------------------------------------------------------------------------- | | `greaterThan(n)` | > n | | `greaterThanOrEqualTo(n)` | ≥ n | | `lessThan(n)` | &lt; n | | `lessThanOrEqualTo(n)` | ≤ n | | `between(min, max)` | Within range | | `int` | Integer | | `nonNegativeInt` | Integer **and** ≥ 0 | | `uint8` | Integer in 0–255 | | `multipleOf(n)` | Divisible by n, exactly for decimals (`0.3` is a multiple of `0.1`); n must be finite and non-zero | | `positive` | > 0 | | `nonNegative` | ≥ 0 | | `negative` | &lt; 0 | | `nonPositive` | ≤ 0 | | `finite` | Not Infinity, -Infinity or NaN | | `nonNaN` | Not NaN | ## BigInt validators | Validator | Checks | | ------------------------------- | ------------ | | `positiveBigInt` | > 0n | | `nonNegativeBigInt` | ≥ 0n | | `negativeBigInt` | &lt; 0n | | `nonPositiveBigInt` | ≤ 0n | | `greaterThanBigInt(n)` | > n | | `greaterThanOrEqualToBigInt(n)` | ≥ n | | `lessThanBigInt(n)` | &lt; n | | `lessThanOrEqualToBigInt(n)` | ≤ n | | `betweenBigInt(min, max)` | Within range | ## Date validators Note the names: they are comparison-shaped, not `after`/`before`: | Validator | Checks | | ----------------------------- | ---------------------- | | `validDate` | Parses to a valid Date | | `greaterThanDate(d)` | After d | | `greaterThanOrEqualToDate(d)` | At or after d | | `lessThanDate(d)` | Before d | | `lessThanOrEqualToDate(d)` | At or before d | | `betweenDate(start, end)` | Within range | ## Array validators | Validator | Checks | | --------------- | ------------------- | | `minItems(n)` | At least n elements | | `maxItems(n)` | At most n elements | | `itemsCount(n)` | Exactly n elements | ## Custom validators `custom(fnName)` calls a function you supply, which should return `true` when the value is valid. The function must be in scope in the file: declared there, or imported by it. ```typescript /** @endec(validate: ["custom(isStrongPassword)"]) */ password: string; ``` To use a function from another module without importing it yourself, pass an object with the `function` to call and the `source` module it comes from. The expansion imports it under a private name, so the import never looks unused to a linter and cannot be removed by one. `source` is written as you would write it in an import in that file. ```typescript /** @endec(validate: ['custom({ function: "isStrongPassword", source: "./validators" })']) */ password: string; ``` ## Validating without decoding Generated types also expose the validators directly, which is useful for form validation. They take input that has not been decoded yet, such as a form's draft, so a value need not satisfy the field's type to be checked: ```typescript User.validateField('email', 'not-an-email'); User.validateFields({ email: 'a@b.com', name: '' }); ```