diff --git a/.codex/AGENTS.md b/.codex/AGENTS.md index 61a102a12..171794fda 100644 --- a/.codex/AGENTS.md +++ b/.codex/AGENTS.md @@ -9,6 +9,7 @@ La librairie est organisée par namespaces (`DArray`, `DObject`, etc.) et couvre - outils orientés DDD (`DClean`) - concepts monadiques (`DEither`) - pattern matching (`DPattern`) +- Process et flux (`DFlow`) ## Philosophie @@ -40,6 +41,7 @@ DPattern DDataParser DDate DClean +DFlow ## Répertoires de travail - `scripts/` : code source diff --git a/.codex/config.toml b/.codex/config.toml index 0bd04860d..7503b7ea6 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -1,5 +1,5 @@ -model = "gpt-5.2-codex" -review_model = "gpt-5.2-codex" +model = "gpt-5.3-codex" +review_model = "gpt-5.3-codex" model_provider = "openai" approval_policy = "untrusted" diff --git a/docs/en/v1/api/clean/constraints.md b/docs/en/v1/api/clean/constraints.md index acc645eb2..533f857f7 100644 --- a/docs/en/v1/api/clean/constraints.md +++ b/docs/en/v1/api/clean/constraints.md @@ -171,7 +171,7 @@ Validates an integer. ### `Positive` -Validates a strictly positive number (>= 1). +Validates a positive or zero number (>= 0). = 1). height="240px" /> +### `StrictPositive` + +Validates a strictly positive number (> 0). + + + ### `Negative` -Validates a strictly negative number (<= -1). +Validates a negative or zero number (<= 0). +### `StrictNegative` + +Validates a strictly negative number (< 0). + + + ### `NumberMin` Validates a number greater than or equal to min. @@ -229,6 +249,60 @@ Validates a strictly negative duration (<= -1 millisecond) on the `C.Time` primi height="271px" /> +## Constraint sets provided by the library + +The library also exports ready-to-use constraint sets via `C.*`. They group multiple constraints and can be used directly with `C.createNewType(...)` or as validation handlers. + +### `PositiveInt` + +Validates a positive or zero integer (`Positive` + `Int`). + + + +### `StrictPositiveInt` + +Validates a strictly positive integer (`StrictPositive` + `Int`). + + + +### `NegativeInt` + +Validates a negative or zero integer (`Negative` + `Int`). + + + +### `StrictNegativeInt` + +Validates a strictly negative integer (`StrictNegative` + `Int`). + + + +### `Percent` + +Validates a number between 0 and 100 included (`NumberMin(0)` + `NumberMax(100)`). + + + ## See also - [Primitives](/en/v1/api/clean/primitives/) diff --git a/docs/examples/v1/api/clean/constraints/negativeInt.doc.ts b/docs/examples/v1/api/clean/constraints/negativeInt.doc.ts new file mode 100644 index 000000000..115cccf34 --- /dev/null +++ b/docs/examples/v1/api/clean/constraints/negativeInt.doc.ts @@ -0,0 +1,11 @@ +import { type ExpectType, C, DP } from "@duplojs/utils"; + +const Balance = C.createNewType("balance", DP.number(), C.NegativeInt); + +const balance = Balance.createOrThrow(0); + +type check = ExpectType< + typeof balance, + C.NewType<"balance", 0, "negative" | "int">, + "strict" +>; diff --git a/docs/examples/v1/api/clean/constraints/percent.doc.ts b/docs/examples/v1/api/clean/constraints/percent.doc.ts new file mode 100644 index 000000000..44a89120f --- /dev/null +++ b/docs/examples/v1/api/clean/constraints/percent.doc.ts @@ -0,0 +1,11 @@ +import { type ExpectType, C, DP } from "@duplojs/utils"; + +const Progress = C.createNewType("progress", DP.number(), C.Percent); + +const progress = Progress.createOrThrow(100); + +type check = ExpectType< + typeof progress, + C.NewType<"progress", 100, "number-min-0" | "number-max-100">, + "strict" +>; diff --git a/docs/examples/v1/api/clean/constraints/positiveInt.doc.ts b/docs/examples/v1/api/clean/constraints/positiveInt.doc.ts new file mode 100644 index 000000000..116512093 --- /dev/null +++ b/docs/examples/v1/api/clean/constraints/positiveInt.doc.ts @@ -0,0 +1,11 @@ +import { type ExpectType, C, DP } from "@duplojs/utils"; + +const Count = C.createNewType("count", DP.number(), C.PositiveInt); + +const count = Count.createOrThrow(0); + +type check = ExpectType< + typeof count, + C.NewType<"count", 0, "positive" | "int">, + "strict" +>; diff --git a/docs/examples/v1/api/clean/constraints/strictNegative.doc.ts b/docs/examples/v1/api/clean/constraints/strictNegative.doc.ts new file mode 100644 index 000000000..add9d04ee --- /dev/null +++ b/docs/examples/v1/api/clean/constraints/strictNegative.doc.ts @@ -0,0 +1,11 @@ +import { type ExpectType, C, DP } from "@duplojs/utils"; + +const Delta = C.createNewType("delta", DP.number(), C.StrictNegative); + +const delta = Delta.createOrThrow(-1); + +type check = ExpectType< + typeof delta, + C.NewType<"delta", -1, "strict-negative">, + "strict" +>; diff --git a/docs/examples/v1/api/clean/constraints/strictNegativeInt.doc.ts b/docs/examples/v1/api/clean/constraints/strictNegativeInt.doc.ts new file mode 100644 index 000000000..4483d52bf --- /dev/null +++ b/docs/examples/v1/api/clean/constraints/strictNegativeInt.doc.ts @@ -0,0 +1,11 @@ +import { type ExpectType, C, DP } from "@duplojs/utils"; + +const Debt = C.createNewType("debt", DP.number(), C.StrictNegativeInt); + +const debt = Debt.createOrThrow(-1); + +type check = ExpectType< + typeof debt, + C.NewType<"debt", -1, "strict-negative" | "int">, + "strict" +>; diff --git a/docs/examples/v1/api/clean/constraints/strictPositive.doc.ts b/docs/examples/v1/api/clean/constraints/strictPositive.doc.ts new file mode 100644 index 000000000..4b4cd0d93 --- /dev/null +++ b/docs/examples/v1/api/clean/constraints/strictPositive.doc.ts @@ -0,0 +1,11 @@ +import { type ExpectType, C, DP } from "@duplojs/utils"; + +const Rating = C.createNewType("rating", DP.number(), C.StrictPositive); + +const rating = Rating.createOrThrow(1); + +type check = ExpectType< + typeof rating, + C.NewType<"rating", 1, "strict-positive">, + "strict" +>; diff --git a/docs/examples/v1/api/clean/constraints/strictPositiveInt.doc.ts b/docs/examples/v1/api/clean/constraints/strictPositiveInt.doc.ts new file mode 100644 index 000000000..8e86f27e6 --- /dev/null +++ b/docs/examples/v1/api/clean/constraints/strictPositiveInt.doc.ts @@ -0,0 +1,11 @@ +import { type ExpectType, C, DP } from "@duplojs/utils"; + +const Quantity = C.createNewType("quantity", DP.number(), C.StrictPositiveInt); + +const quantity = Quantity.createOrThrow(1); + +type check = ExpectType< + typeof quantity, + C.NewType<"quantity", 1, "strict-positive" | "int">, + "strict" +>; diff --git a/docs/fr/v1/api/clean/constraints.md b/docs/fr/v1/api/clean/constraints.md index 238f42bca..6b7392709 100644 --- a/docs/fr/v1/api/clean/constraints.md +++ b/docs/fr/v1/api/clean/constraints.md @@ -171,7 +171,7 @@ Valide un nombre entier. ### `Positive` -Valide un nombre strictement positif (>= 1). +Valide un nombre positif ou nul (>= 0). = 1). height="240px" /> +### `StrictPositive` + +Valide un nombre strictement positif (> 0). + + + ### `Negative` -Valide un nombre strictement négatif (<= -1). +Valide un nombre négatif ou nul (<= 0). +### `StrictNegative` + +Valide un nombre strictement négatif (< 0). + + + ### `NumberMin` Valide un nombre supérieur ou égal à min. @@ -229,6 +249,60 @@ Valide une durée strictement négative (<= -1 milliseconde) sur la primitive `C height="271px" /> +## Ensembles de contraintes fournis par la librairie + +La librairie exporte aussi des ensembles de contraintes prêts à l'emploi via `C.*`. Ils regroupent plusieurs contraintes et peuvent être utilisés directement avec `C.createNewType(...)` ou comme handlers de validation. + +### `PositiveInt` + +Valide un nombre entier positif ou nul (`Positive` + `Int`). + + + +### `StrictPositiveInt` + +Valide un nombre entier strictement positif (`StrictPositive` + `Int`). + + + +### `NegativeInt` + +Valide un nombre entier négatif ou nul (`Negative` + `Int`). + + + +### `StrictNegativeInt` + +Valide un nombre entier strictement négatif (`StrictNegative` + `Int`). + + + +### `Percent` + +Valide un nombre compris entre 0 et 100 inclus (`NumberMin(0)` + `NumberMax(100)`). + + + ## Voir aussi - [Primitives](/fr/v1/api/clean/primitives/) diff --git a/docs/public/libs/v1/clean/chainedFunction.d.ts b/docs/public/libs/v1/clean/chainedFunction.d.ts index 691a5dbba..d314dbf23 100644 --- a/docs/public/libs/v1/clean/chainedFunction.d.ts +++ b/docs/public/libs/v1/clean/chainedFunction.d.ts @@ -45,34 +45,6 @@ export type ChainedFunction * Use it inside a Clean Architecture use case when several pure domain operations that update different entities must belong to the same business consistency boundary. Each link exposes exactly one named action, yields `Left` values to short-circuit the implementation, and provides the next link until the last step returns `chainEnd(value)`. Repository calls stay in the use case through the library repository system; functions passed to `chainedFunction` remain pure domain functions. * * ```ts - * interface CommentDraft { - * articleId: number; - * content: string; - * } - * - * interface Comment { - * id: number; - * articleId: number; - * content: string; - * } - * - * interface Article { - * id: number; - * commentCount: number; - * } - * - * interface CommentRepository { - * save(comment: Comment): Comment | E.Fail; - * } - * - * interface ArticleRepository { - * findById(articleId: number): Article | E.Fail; - * save(article: Article): Article | E.Fail; - * } - * - * const CommentRepository = C.createRepository(); - * const ArticleRepository = C.createRepository(); - * * const CommentPublicationAggregate = C.chainedFunction( * [ * "createComment", @@ -101,27 +73,18 @@ export type ChainedFunction * ({ * commentRepository, * articleRepository, - * }) => (draft: CommentDraft) => CommentPublicationAggregate(function *(link1) { + * }) => (draft: CommentDraft) => CommentPublicationAggregate(function *(link1, { breakIfLeft }) { * const [comment, link2] = yield *link1(({ createComment }) => createComment(draft)); * - * const savedComment = commentRepository.save(comment); - * if (E.isLeft(savedComment)) { - * return savedComment; - * } + * const savedComment = yield *breakIfLeft(commentRepository.save(comment)); * - * const article = articleRepository.findById(savedComment.articleId); - * if (E.isLeft(article)) { - * return article; - * } + * const article = yield *breakIfLeft(articleRepository.findById(savedComment.articleId)); * * const [updatedArticle, chainEnd] = yield *link2( * ({ incrementArticleCommentCount }) => incrementArticleCommentCount(article), * ); * - * const savedArticle = articleRepository.save(updatedArticle); - * if (E.isLeft(savedArticle)) { - * return savedArticle; - * } + * const savedArticle = yield *breakIfLeft(articleRepository.save(updatedArticle)); * * return chainEnd({ * comment: savedComment, @@ -200,6 +163,7 @@ export type ChainedFunction * ``` * * @remarks `chainedFunction` expects at least two functions in the chain. It does not catch thrown exceptions or rejected promises; model handled business errors with `Either.Left`. + * The callback receives `(firstLink, { breakIfLeft })`. `breakIfLeft` is synchronous and narrows `value | Left` to `value`, yielding the `Left` branch to short-circuit when needed. * * @see https://utils.duplojs.dev/en/v1/api/clean/chainedFunction * @see [`C.createUseCase`](https://utils.duplojs.dev/en/v1/api/clean/useCase) diff --git a/docs/public/libs/v1/clean/constraint/cast.d.ts b/docs/public/libs/v1/clean/constraint/cast.d.ts index 87f535d79..49c315fc8 100644 --- a/docs/public/libs/v1/clean/constraint/cast.d.ts +++ b/docs/public/libs/v1/clean/constraint/cast.d.ts @@ -1,15 +1,21 @@ import { type UnionToIntersection, type SimplifyTopLevel, type NeverCoalescing, type And, type Not, type IsEqual } from "../../common"; import { type ConstraintHandler, type ConstrainedType, type GetConstraint } from "./base"; -import { type Negative, type NumberMaxHandlerInternal, type NumberMaxInternal, type NumberMinHandlerInternal, type NumberMinInternal, type Positive, type StringMaxHandlerInternal, type StringMaxInternal, type StringMinHandlerInternal, type StringMinInternal } from "./defaultConstraint"; +import { type StrictNegative, type StrictPositive, type Negative, type NumberMaxHandlerInternal, type NumberMaxInternal, type NumberMinHandlerInternal, type NumberMinInternal, type Positive, type StringMaxHandlerInternal, type StringMaxInternal, type StringMinHandlerInternal, type StringMinInternal } from "./defaultConstraint"; import { type IsLess, type IsGreater } from "../../number"; declare const SymbolCastErrorMessage: unique symbol; type CastConstraintError = SimplifyTopLevel<{ [SymbolCastErrorMessage]: `The constraint "${GenericConstrainHandler["name"]}" is not applicable: ${GenericReason}.`; }>; -type ForbiddenBadCast = ((GenericConstrainHandler extends NumberMinHandlerInternal ? GenericConstrainedType extends Positive ? IsLess extends true ? never : CastConstraintError : GenericConstrainedType extends NumberMinInternal ? And<[ +type ForbiddenBadCast = ((GenericConstrainHandler extends NumberMinHandlerInternal ? GenericConstrainedType extends Positive ? IsLess extends true ? never : CastConstraintError : GenericConstrainedType extends StrictPositive ? And<[ + IsLess, + Not> +]> extends true ? never : CastConstraintError : GenericConstrainedType extends NumberMinInternal ? And<[ IsLess, Not> -]> extends true ? never : CastConstraintError : CastConstraintError : never) | (GenericConstrainHandler extends NumberMaxHandlerInternal ? GenericConstrainedType extends Negative ? IsGreater extends true ? never : CastConstraintError : GenericConstrainedType extends NumberMaxInternal ? And<[ +]> extends true ? never : CastConstraintError : CastConstraintError : never) | (GenericConstrainHandler extends NumberMaxHandlerInternal ? GenericConstrainedType extends Negative ? IsGreater extends true ? never : CastConstraintError : GenericConstrainedType extends StrictNegative ? And<[ + IsGreater, + Not> +]> extends true ? never : CastConstraintError : GenericConstrainedType extends NumberMaxInternal ? And<[ IsGreater, Not> ]> extends true ? never : CastConstraintError : CastConstraintError : never) | (GenericConstrainHandler extends StringMinHandlerInternal ? GenericConstrainedType extends StringMinInternal ? And<[ @@ -24,6 +30,14 @@ type ForbiddenBadCast extends true ? never : CastConstraintError : CastConstraintError : never) | (GenericConstrainHandler extends typeof Negative ? GenericConstrainedType extends NumberMaxInternal ? And<[ IsGreater<0, InferredReferenceValue>, Not> -]> extends true ? never : CastConstraintError : CastConstraintError : never) | (GenericConstrainHandler extends (typeof Negative | typeof Positive | StringMaxHandlerInternal | StringMinHandlerInternal | NumberMaxHandlerInternal | NumberMinHandlerInternal) ? never : CastConstraintError)) extends infer InferredResult ? NeverCoalescing : never; +]> extends true ? never : CastConstraintError : CastConstraintError : never) | (GenericConstrainHandler extends typeof StrictPositive ? GenericConstrainedType extends NumberMinInternal ? And<[ + IsLess<0, InferredReferenceValue>, + Not>, + Not> +]> extends true ? never : CastConstraintError : CastConstraintError : never) | (GenericConstrainHandler extends typeof StrictNegative ? GenericConstrainedType extends NumberMaxInternal ? And<[ + IsGreater<0, InferredReferenceValue>, + Not>, + Not> +]> extends true ? never : CastConstraintError : CastConstraintError : never) | (GenericConstrainHandler extends (typeof Negative | typeof Positive | typeof StrictNegative | typeof StrictPositive | StringMaxHandlerInternal | StringMinHandlerInternal | NumberMaxHandlerInternal | NumberMinHandlerInternal) ? never : CastConstraintError)) extends infer InferredResult ? NeverCoalescing : never; export declare function castConstraint(constrainedType: (GenericConstrainedType & ForbiddenBadCast), constraintHandler: GenericConstrainHandler | GenericConstrainHandler[]): (GenericConstrainedType & UnionToIntersection : never>); export {}; diff --git a/docs/public/libs/v1/clean/constraint/defaultConstraint/number.cjs b/docs/public/libs/v1/clean/constraint/defaultConstraint/number.cjs index 3a5953865..ccef4d9aa 100644 --- a/docs/public/libs/v1/clean/constraint/defaultConstraint/number.cjs +++ b/docs/public/libs/v1/clean/constraint/defaultConstraint/number.cjs @@ -14,10 +14,18 @@ const Int = base.createConstraint("int", base$1.Number, int.checkerInt()); * {@include clean/Positive/index.md} */ const Positive = base.createConstraint("positive", base$1.Number, min.checkerNumberMin(0)); +/** + * {@include clean/StrictPositive/index.md} + */ +const StrictPositive = base.createConstraint("strict-positive", base$1.Number, min.checkerNumberMin(0, { exclusive: true })); /** * {@include clean/Negative/index.md} */ const Negative = base.createConstraint("negative", base$1.Number, max.checkerNumberMax(0)); +/** + * {@include clean/StrictNegative/index.md} + */ +const StrictNegative = base.createConstraint("strict-negative", base$1.Number, max.checkerNumberMax(0, { exclusive: true })); /** * {@include clean/NumberMin/index.md} */ @@ -36,3 +44,5 @@ exports.Negative = Negative; exports.NumberMax = NumberMax; exports.NumberMin = NumberMin; exports.Positive = Positive; +exports.StrictNegative = StrictNegative; +exports.StrictPositive = StrictPositive; diff --git a/docs/public/libs/v1/clean/constraint/defaultConstraint/number.d.ts b/docs/public/libs/v1/clean/constraint/defaultConstraint/number.d.ts index 2d636fb14..f433e4321 100644 --- a/docs/public/libs/v1/clean/constraint/defaultConstraint/number.d.ts +++ b/docs/public/libs/v1/clean/constraint/defaultConstraint/number.d.ts @@ -59,6 +59,35 @@ export type Int = GetConstraint; */ export declare const Positive: ConstraintHandler<"positive", number, readonly [DDataParser.DataParserCheckerNumberMin], never>; export type Positive = GetConstraint; +/** + * Constraint handler that validates strictly positive numbers (> 0). + * + * **Supported call styles:** + * - Classic: `StrictPositive.create(value)` -> returns Either + * + * Use it as a reusable rule to validate inputs and to constrain NewTypes to numbers strictly greater than zero. + * + * ```ts + * const result = C.StrictPositive.create(4); + * + * if (E.isRight(result)) { + * // result: E.Right<"createConstrainedType", C.ConstrainedType<"strict-positive", 4>> + * } + * + * const value = C.StrictPositive.createOrThrow(10); + * // value: C.ConstrainedType<"strict-positive", 10> + * + * C.StrictPositive.is(value); // type guard + * + * ``` + * + * @see https://utils.duplojs.dev/en/v1/api/clean/constraints#strictpositive + * + * @namespace C + * + */ +export declare const StrictPositive: ConstraintHandler<"strict-positive", number, readonly [DDataParser.DataParserCheckerNumberMin], never>; +export type StrictPositive = GetConstraint; /** * Constraint handler that validates strictly negative numbers (<= -1). * @@ -88,6 +117,35 @@ export type Positive = GetConstraint; */ export declare const Negative: ConstraintHandler<"negative", number, readonly [DDataParser.DataParserCheckerNumberMax], never>; export type Negative = GetConstraint; +/** + * Constraint handler that validates strictly negative numbers (< 0). + * + * **Supported call styles:** + * - Classic: `StrictNegative.create(value)` -> returns Either + * + * Use it as a reusable rule to validate inputs and to constrain NewTypes to numbers strictly less than zero. + * + * ```ts + * const result = C.StrictNegative.create(-4); + * + * if (E.isRight(result)) { + * // result: E.Right<"createConstrainedType", C.ConstrainedType<"strict-negative", -4>> + * } + * + * const value = C.StrictNegative.createOrThrow(-10); + * // value: C.ConstrainedType<"strict-negative", -10> + * + * C.StrictNegative.is(value); // type guard + * + * ``` + * + * @see https://utils.duplojs.dev/en/v1/api/clean/constraints#strictnegative + * + * @namespace C + * + */ +export declare const StrictNegative: ConstraintHandler<"strict-negative", number, readonly [DDataParser.DataParserCheckerNumberMax], never>; +export type StrictNegative = GetConstraint; export type NumberMinHandlerInternal = Extract, any>; export type NumberMinInternal = GetConstraint>; /** diff --git a/docs/public/libs/v1/clean/constraint/defaultConstraint/number.mjs b/docs/public/libs/v1/clean/constraint/defaultConstraint/number.mjs index 962e1b2ed..92a7418c0 100644 --- a/docs/public/libs/v1/clean/constraint/defaultConstraint/number.mjs +++ b/docs/public/libs/v1/clean/constraint/defaultConstraint/number.mjs @@ -12,10 +12,18 @@ const Int = createConstraint("int", Number, checkerInt()); * {@include clean/Positive/index.md} */ const Positive = createConstraint("positive", Number, checkerNumberMin(0)); +/** + * {@include clean/StrictPositive/index.md} + */ +const StrictPositive = createConstraint("strict-positive", Number, checkerNumberMin(0, { exclusive: true })); /** * {@include clean/Negative/index.md} */ const Negative = createConstraint("negative", Number, checkerNumberMax(0)); +/** + * {@include clean/StrictNegative/index.md} + */ +const StrictNegative = createConstraint("strict-negative", Number, checkerNumberMax(0, { exclusive: true })); /** * {@include clean/NumberMin/index.md} */ @@ -29,4 +37,4 @@ function NumberMax(value) { return createConstraint(`number-max-${value}`, Number, checkerNumberMax(value)); } -export { Int, Negative, NumberMax, NumberMin, Positive }; +export { Int, Negative, NumberMax, NumberMin, Positive, StrictNegative, StrictPositive }; diff --git a/docs/public/libs/v1/clean/constraint/defaultConstraint/string.d.ts b/docs/public/libs/v1/clean/constraint/defaultConstraint/string.d.ts index bb7fc7f6a..9f635bea6 100644 --- a/docs/public/libs/v1/clean/constraint/defaultConstraint/string.d.ts +++ b/docs/public/libs/v1/clean/constraint/defaultConstraint/string.d.ts @@ -31,7 +31,7 @@ import { type OnlyLiteralNumber } from "../../../common"; export declare const Email: ConstraintHandler<"email", string, readonly [DDataParser.DataParserCheckerEmail], never>; export type Email = GetConstraint; export declare const Uuid: ConstraintHandler<"uuid", string, readonly [DDataParser.DataParserCheckerUuid], never>; -export type Uuid = GetConstraint; +export type Uuid = GetConstraint; /** * Constraint handler that validates a URL string. * diff --git a/docs/public/libs/v1/clean/constraint/defaultConstraintSet/index.d.ts b/docs/public/libs/v1/clean/constraint/defaultConstraintSet/index.d.ts new file mode 100644 index 000000000..2f0dcabd3 --- /dev/null +++ b/docs/public/libs/v1/clean/constraint/defaultConstraintSet/index.d.ts @@ -0,0 +1 @@ +export * from "./number"; diff --git a/docs/public/libs/v1/clean/constraint/defaultConstraintSet/number.cjs b/docs/public/libs/v1/clean/constraint/defaultConstraintSet/number.cjs new file mode 100644 index 000000000..3f5b995a3 --- /dev/null +++ b/docs/public/libs/v1/clean/constraint/defaultConstraintSet/number.cjs @@ -0,0 +1,47 @@ +'use strict'; + +var set = require('../set.cjs'); +var number = require('../defaultConstraint/number.cjs'); +var base = require('../../primitive/base.cjs'); + +/** + * {@include clean/PositiveInt/index.md} + */ +const PositiveInt = set.createConstraintsSet(base.Number, [ + number.Positive, + number.Int, +]); +/** + * {@include clean/StrictPositiveInt/index.md} + */ +const StrictPositiveInt = set.createConstraintsSet(base.Number, [ + number.StrictPositive, + number.Int, +]); +/** + * {@include clean/NegativeInt/index.md} + */ +const NegativeInt = set.createConstraintsSet(base.Number, [ + number.Negative, + number.Int, +]); +/** + * {@include clean/StrictNegativeInt/index.md} + */ +const StrictNegativeInt = set.createConstraintsSet(base.Number, [ + number.StrictNegative, + number.Int, +]); +/** + * {@include clean/Percent/index.md} + */ +const Percent = set.createConstraintsSet(base.Number, [ + number.NumberMin(0), + number.NumberMax(100), +]); + +exports.NegativeInt = NegativeInt; +exports.Percent = Percent; +exports.PositiveInt = PositiveInt; +exports.StrictNegativeInt = StrictNegativeInt; +exports.StrictPositiveInt = StrictPositiveInt; diff --git a/docs/public/libs/v1/clean/constraint/defaultConstraintSet/number.d.ts b/docs/public/libs/v1/clean/constraint/defaultConstraintSet/number.d.ts new file mode 100644 index 000000000..46773c6cb --- /dev/null +++ b/docs/public/libs/v1/clean/constraint/defaultConstraintSet/number.d.ts @@ -0,0 +1,146 @@ +import { type GetConstraints } from "../set"; +/** + * Constraint set handler that validates positive integer numbers (>= 0 and integer). + * + * **Supported call styles:** + * - Classic: `PositiveInt.create(value)` -> returns Either + * + * Use it as a reusable rule to validate inputs and to constrain NewTypes to positive integer numbers. + * + * ```ts + * const result = C.PositiveInt.create(12); + * + * if (E.isRight(result)) { + * // result: E.Right<"createConstraintsSet", C.Primitive<12> & C.PositiveInt> + * } + * + * const value = C.PositiveInt.createOrThrow(0); + * // value: C.Primitive<0> & C.PositiveInt + * + * C.PositiveInt.is(value); // type guard + * + * ``` + * + * @see https://utils.duplojs.dev/en/v1/api/clean/constraints + * + * @namespace C + * + */ +export declare const PositiveInt: import("..").ConstraintsSetHandler, import("..").ConstraintHandler<"int", number, readonly [import("../../../dataParser").DataParserCheckerInt], never>], never>; +export type PositiveInt = GetConstraints; +/** + * Constraint set handler that validates strictly positive integer numbers (>= 1 and integer). + * + * **Supported call styles:** + * - Classic: `StrictPositiveInt.create(value)` -> returns Either + * + * Use it as a reusable rule to validate inputs and to constrain NewTypes to strictly positive integer numbers. + * + * ```ts + * const result = C.StrictPositiveInt.create(12); + * + * if (E.isRight(result)) { + * // result: E.Right<"createConstraintsSet", C.Primitive<12> & C.StrictPositiveInt> + * } + * + * const value = C.StrictPositiveInt.createOrThrow(1); + * // value: C.Primitive<1> & C.StrictPositiveInt + * + * C.StrictPositiveInt.is(value); // type guard + * + * ``` + * + * @see https://utils.duplojs.dev/en/v1/api/clean/constraints + * + * @namespace C + * + */ +export declare const StrictPositiveInt: import("..").ConstraintsSetHandler, import("..").ConstraintHandler<"int", number, readonly [import("../../../dataParser").DataParserCheckerInt], never>], never>; +export type StrictPositiveInt = GetConstraints; +/** + * Constraint set handler that validates negative integer numbers (<= 0 and integer). + * + * **Supported call styles:** + * - Classic: `NegativeInt.create(value)` -> returns Either + * + * Use it as a reusable rule to validate inputs and to constrain NewTypes to negative integer numbers. + * + * ```ts + * const result = C.NegativeInt.create(-12); + * + * if (E.isRight(result)) { + * // result: E.Right<"createConstraintsSet", C.Primitive<-12> & C.NegativeInt> + * } + * + * const value = C.NegativeInt.createOrThrow(0); + * // value: C.Primitive<0> & C.NegativeInt + * + * C.NegativeInt.is(value); // type guard + * + * ``` + * + * @see https://utils.duplojs.dev/en/v1/api/clean/constraints + * + * @namespace C + * + */ +export declare const NegativeInt: import("..").ConstraintsSetHandler, import("..").ConstraintHandler<"int", number, readonly [import("../../../dataParser").DataParserCheckerInt], never>], never>; +export type NegativeInt = GetConstraints; +/** + * Constraint set handler that validates strictly negative integer numbers (<= -1 and integer). + * + * **Supported call styles:** + * - Classic: `StrictNegativeInt.create(value)` -> returns Either + * + * Use it as a reusable rule to validate inputs and to constrain NewTypes to strictly negative integer numbers. + * + * ```ts + * const result = C.StrictNegativeInt.create(-12); + * + * if (E.isRight(result)) { + * // result: E.Right<"createConstraintsSet", C.Primitive<-12> & C.StrictNegativeInt> + * } + * + * const value = C.StrictNegativeInt.createOrThrow(-1); + * // value: C.Primitive<-1> & C.StrictNegativeInt + * + * C.StrictNegativeInt.is(value); // type guard + * + * ``` + * + * @see https://utils.duplojs.dev/en/v1/api/clean/constraints + * + * @namespace C + * + */ +export declare const StrictNegativeInt: import("..").ConstraintsSetHandler, import("..").ConstraintHandler<"int", number, readonly [import("../../../dataParser").DataParserCheckerInt], never>], never>; +export type StrictNegativeInt = GetConstraints; +/** + * Constraint set handler that validates percent numbers (between 0 and 100 included). + * + * **Supported call styles:** + * - Classic: `Percent.create(value)` -> returns Either + * + * Use it as a reusable rule to validate inputs and to constrain NewTypes to values in the percent range. + * + * ```ts + * const result = C.Percent.create(75); + * + * if (E.isRight(result)) { + * // result: E.Right<"createConstraintsSet", C.Primitive<75> & C.Percent> + * } + * + * const value = C.Percent.createOrThrow(100); + * // value: C.Primitive<100> & C.Percent + * + * C.Percent.is(value); // type guard + * + * ``` + * + * @see https://utils.duplojs.dev/en/v1/api/clean/constraints + * + * @namespace C + * + */ +export declare const Percent: import("..").ConstraintsSetHandler, import("..").ConstraintHandler<"number-max-100", number, readonly [import("../../../dataParser").DataParserCheckerNumberMax], never>], never>; +export type Percent = GetConstraints; diff --git a/docs/public/libs/v1/clean/constraint/defaultConstraintSet/number.mjs b/docs/public/libs/v1/clean/constraint/defaultConstraintSet/number.mjs new file mode 100644 index 000000000..f5055082a --- /dev/null +++ b/docs/public/libs/v1/clean/constraint/defaultConstraintSet/number.mjs @@ -0,0 +1,41 @@ +import { createConstraintsSet } from '../set.mjs'; +import { Positive, Int, StrictPositive, Negative, StrictNegative, NumberMin, NumberMax } from '../defaultConstraint/number.mjs'; +import { Number } from '../../primitive/base.mjs'; + +/** + * {@include clean/PositiveInt/index.md} + */ +const PositiveInt = createConstraintsSet(Number, [ + Positive, + Int, +]); +/** + * {@include clean/StrictPositiveInt/index.md} + */ +const StrictPositiveInt = createConstraintsSet(Number, [ + StrictPositive, + Int, +]); +/** + * {@include clean/NegativeInt/index.md} + */ +const NegativeInt = createConstraintsSet(Number, [ + Negative, + Int, +]); +/** + * {@include clean/StrictNegativeInt/index.md} + */ +const StrictNegativeInt = createConstraintsSet(Number, [ + StrictNegative, + Int, +]); +/** + * {@include clean/Percent/index.md} + */ +const Percent = createConstraintsSet(Number, [ + NumberMin(0), + NumberMax(100), +]); + +export { NegativeInt, Percent, PositiveInt, StrictNegativeInt, StrictPositiveInt }; diff --git a/docs/public/libs/v1/clean/constraint/index.d.ts b/docs/public/libs/v1/clean/constraint/index.d.ts index 626baee44..efdeab003 100644 --- a/docs/public/libs/v1/clean/constraint/index.d.ts +++ b/docs/public/libs/v1/clean/constraint/index.d.ts @@ -2,3 +2,4 @@ export * from "./base"; export * from "./cast"; export * from "./defaultConstraint"; export * from "./set"; +export * from "./defaultConstraintSet"; diff --git a/docs/public/libs/v1/clean/constraint/set.cjs b/docs/public/libs/v1/clean/constraint/set.cjs index b7a01b530..97d51c7de 100644 --- a/docs/public/libs/v1/clean/constraint/set.cjs +++ b/docs/public/libs/v1/clean/constraint/set.cjs @@ -31,7 +31,9 @@ class CreateConstraintsSetError extends kind$1.kindHeritage("create-constraint-s * {@include clean/createConstraintsSet/index.md} */ function createConstraintsSet(primitiveHandler, constraint) { - const constraints = coalescing.coalescing(constraint); + const constraints = flatMap.flatMap(coalescing.coalescing(constraint), (constraint) => constraintsSetHandlerKind.has(constraint) + ? constraint.internal.constraints + : constraint); const checkers = flatMap.flatMap(constraints, ({ internal }) => internal.checkers); const dataParserWithCheckers = primitiveHandler .dataParser diff --git a/docs/public/libs/v1/clean/constraint/set.d.ts b/docs/public/libs/v1/clean/constraint/set.d.ts index a924160f6..d7af7f970 100644 --- a/docs/public/libs/v1/clean/constraint/set.d.ts +++ b/docs/public/libs/v1/clean/constraint/set.d.ts @@ -2,7 +2,6 @@ import { type Kind, type WrappedValue, type UnionToIntersection } from "../.."; import { type GetConstraint, type ConstraintHandler } from "../constraint"; import { type Primitive, type EligiblePrimitive, type PrimitiveHandler } from "../primitive"; import * as DEither from "../../either"; -import * as DArray from "../../array"; import type * as DDataParser from "../../dataParser"; export declare const constraintsSetHandlerKind: import("../..").KindHandler>; export interface ConstraintsSetHandler extends Kind { @@ -113,6 +112,18 @@ export declare class CreateConstraintsSetError extends CreateConstraintsSetError dataParserError: DDataParser.DataParserError; constructor(data: unknown, dataParserError: DDataParser.DataParserError); } +export type ConstraintSetInputConstraint = (ConstraintHandler[]> | ConstraintsSetHandler[]>[]>); +export type ConstraintsHandlerArguments = (ConstraintSetInputConstraint | readonly [ + ConstraintSetInputConstraint, + ...ConstraintSetInputConstraint[] +]); +export type ExtractConstraintSetConstraintHandlers | readonly ConstraintSetInputConstraint[] | readonly [])> = GenericConstraint extends ConstraintHandler ? readonly [GenericConstraint] : GenericConstraint extends ConstraintsSetHandler ? ExtractConstraintSetConstraintHandlers : GenericConstraint extends readonly [] ? GenericConstraint : GenericConstraint extends readonly [ + infer InferredFirst extends ConstraintSetInputConstraint, + ...infer InferredRest extends readonly ConstraintSetInputConstraint[] +] ? ExtractConstraintSetConstraintHandlers extends infer InferredResultRest extends readonly any[] ? readonly [ + ...ExtractConstraintSetConstraintHandlers, + ...InferredResultRest +] : never : never; /** * Creates a constraints set handler to apply multiple constraints to a primitive. * @@ -150,7 +161,8 @@ export declare class CreateConstraintsSetError extends CreateConstraintsSetError * ``` * * @remarks - * - You can pass a single constraint handler or a tuple of constraints. + * - You can pass a single constraint handler, a constraints set handler, or a tuple mixing constraints and constraints sets. + * - Constraints sets are expanded internally, so their contained constraints are applied in declaration order. * - The handler accepts both raw values and primitives. * * @see https://utils.duplojs.dev/en/v1/api/clean/constraints @@ -158,10 +170,7 @@ export declare class CreateConstraintsSetError extends CreateConstraintsSetError * @namespace C * */ -export declare function createConstraintsSet[]> | readonly [ - ConstraintHandler[]>, - ...ConstraintHandler[]>[] -]) = never>(primitiveHandler: PrimitiveHandler, constraint: GenericConstrainHandler): ConstraintsSetHandler, GenericPrimitiveInput>; +export declare function createConstraintsSet = never>(primitiveHandler: PrimitiveHandler, constraint: GenericConstrainHandler): ConstraintsSetHandler, GenericPrimitiveInput>; export declare namespace createConstraintsSet { var overrideHandler: import("../..").OverrideHandler>; } diff --git a/docs/public/libs/v1/clean/constraint/set.mjs b/docs/public/libs/v1/clean/constraint/set.mjs index a13cfa346..bee96937f 100644 --- a/docs/public/libs/v1/clean/constraint/set.mjs +++ b/docs/public/libs/v1/clean/constraint/set.mjs @@ -29,7 +29,9 @@ class CreateConstraintsSetError extends kindHeritage("create-constraint-set-erro * {@include clean/createConstraintsSet/index.md} */ function createConstraintsSet(primitiveHandler, constraint) { - const constraints = coalescing(constraint); + const constraints = flatMap(coalescing(constraint), (constraint) => constraintsSetHandlerKind.has(constraint) + ? constraint.internal.constraints + : constraint); const checkers = flatMap(constraints, ({ internal }) => internal.checkers); const dataParserWithCheckers = primitiveHandler .dataParser diff --git a/docs/public/libs/v1/clean/entity/unwrap.cjs b/docs/public/libs/v1/clean/entity/unwrap.cjs index cfa014b25..f1316dabc 100644 --- a/docs/public/libs/v1/clean/entity/unwrap.cjs +++ b/docs/public/libs/v1/clean/entity/unwrap.cjs @@ -4,6 +4,7 @@ var index = require('./index.cjs'); var flag = require('../flag.cjs'); var wrapValue = require('../../common/wrapValue.cjs'); var unwrap = require('../../common/unwrap.cjs'); +var kind = require('../../common/kind.cjs'); /** * {@include clean/unwrapEntity/index.md} @@ -50,7 +51,7 @@ function unwrapEntity(entity, params) { else if (prop === flag.flagKind.runTimeKey) { unwrapEntity._flags = entity[prop]; } - else { + else if (!kind.isRuntimeKind(prop)) { unwrapEntity[prop] = unwrapEntityProperty(entity[prop], params); } } diff --git a/docs/public/libs/v1/clean/entity/unwrap.d.ts b/docs/public/libs/v1/clean/entity/unwrap.d.ts index 7c0f6a01c..cbbcec7fa 100644 --- a/docs/public/libs/v1/clean/entity/unwrap.d.ts +++ b/docs/public/libs/v1/clean/entity/unwrap.d.ts @@ -13,7 +13,9 @@ export type UnwrapEntity; } & (GenericEntity extends Kind ? { [Prop in "_flags"]: SimplifyTopLevel>; -} : {})>>; +} : { + _flags?: Record; +})>>; /** * Unwraps a `Clean` entity into a readonly plain object. * diff --git a/docs/public/libs/v1/clean/entity/unwrap.mjs b/docs/public/libs/v1/clean/entity/unwrap.mjs index 72130e3ca..ed0110810 100644 --- a/docs/public/libs/v1/clean/entity/unwrap.mjs +++ b/docs/public/libs/v1/clean/entity/unwrap.mjs @@ -2,6 +2,7 @@ import { entityKind } from './index.mjs'; import { flagKind } from '../flag.mjs'; import { isWrappedValue } from '../../common/wrapValue.mjs'; import { unwrap } from '../../common/unwrap.mjs'; +import { isRuntimeKind } from '../../common/kind.mjs'; /** * {@include clean/unwrapEntity/index.md} @@ -48,7 +49,7 @@ function unwrapEntity(entity, params) { else if (prop === flagKind.runTimeKey) { unwrapEntity._flags = entity[prop]; } - else { + else if (!isRuntimeKind(prop)) { unwrapEntity[prop] = unwrapEntityProperty(entity[prop], params); } } diff --git a/docs/public/libs/v1/clean/index.cjs b/docs/public/libs/v1/clean/index.cjs index 45d9967dc..68ac822b5 100644 --- a/docs/public/libs/v1/clean/index.cjs +++ b/docs/public/libs/v1/clean/index.cjs @@ -17,6 +17,7 @@ var number = require('./constraint/defaultConstraint/number.cjs'); var string = require('./constraint/defaultConstraint/string.cjs'); var time = require('./constraint/defaultConstraint/time.cjs'); var set = require('./constraint/set.cjs'); +var number$1 = require('./constraint/defaultConstraintSet/number.cjs'); var base$1 = require('./primitive/base.cjs'); var equal = require('./primitive/operations/equal.cjs'); var add = require('./primitive/operations/number/add.cjs'); @@ -88,6 +89,8 @@ exports.Negative = number.Negative; exports.NumberMax = number.NumberMax; exports.NumberMin = number.NumberMin; exports.Positive = number.Positive; +exports.StrictNegative = number.StrictNegative; +exports.StrictPositive = number.StrictPositive; exports.Email = string.Email; exports.StringMax = string.StringMax; exports.StringMin = string.StringMin; @@ -98,6 +101,11 @@ exports.PositiveTime = time.PositiveTime; exports.CreateConstraintsSetError = set.CreateConstraintsSetError; exports.constraintsSetHandlerKind = set.constraintsSetHandlerKind; exports.createConstraintsSet = set.createConstraintsSet; +exports.NegativeInt = number$1.NegativeInt; +exports.Percent = number$1.Percent; +exports.PositiveInt = number$1.PositiveInt; +exports.StrictNegativeInt = number$1.StrictNegativeInt; +exports.StrictPositiveInt = number$1.StrictPositiveInt; exports.BigInt = base$1.BigInt; exports.Boolean = base$1.Boolean; exports.CreatePrimitiveError = base$1.CreatePrimitiveError; diff --git a/docs/public/libs/v1/clean/index.mjs b/docs/public/libs/v1/clean/index.mjs index 58d0f7da9..b7fc5ae78 100644 --- a/docs/public/libs/v1/clean/index.mjs +++ b/docs/public/libs/v1/clean/index.mjs @@ -11,10 +11,11 @@ export { entityPropertyArrayKind, entityPropertyDefinitionToDataParser, entityPr export { unwrapEntity, unwrapEntityProperty } from './entity/unwrap.mjs'; export { CreateConstrainedTypeError, constrainedTypeKind, constraintHandlerKind, createConstraint } from './constraint/base.mjs'; export { castConstraint } from './constraint/cast.mjs'; -export { Int, Negative, NumberMax, NumberMin, Positive } from './constraint/defaultConstraint/number.mjs'; +export { Int, Negative, NumberMax, NumberMin, Positive, StrictNegative, StrictPositive } from './constraint/defaultConstraint/number.mjs'; export { Email, StringMax, StringMin, Url, Uuid } from './constraint/defaultConstraint/string.mjs'; export { NegativeTime, PositiveTime } from './constraint/defaultConstraint/time.mjs'; export { CreateConstraintsSetError, constraintsSetHandlerKind, createConstraintsSet } from './constraint/set.mjs'; +export { NegativeInt, Percent, PositiveInt, StrictNegativeInt, StrictPositiveInt } from './constraint/defaultConstraintSet/number.mjs'; export { BigInt, Boolean, CreatePrimitiveError, Date, Number, String, Time, createPrimitive, primitiveHandlerKind } from './primitive/base.mjs'; export { equal } from './primitive/operations/equal.mjs'; export { add } from './primitive/operations/number/add.mjs'; diff --git a/docs/public/libs/v1/clean/newType.cjs b/docs/public/libs/v1/clean/newType.cjs index 8084ca085..6e4b99cfb 100644 --- a/docs/public/libs/v1/clean/newType.cjs +++ b/docs/public/libs/v1/clean/newType.cjs @@ -1,6 +1,7 @@ 'use strict'; var kind = require('./kind.cjs'); +var set = require('./constraint/set.cjs'); var kind$1 = require('../common/kind.cjs'); var flatMap = require('../array/flatMap.cjs'); var coalescing = require('../array/coalescing.cjs'); @@ -34,7 +35,9 @@ class CreateNewTypeError extends kind$1.kindHeritage("create-new-type-error", er * {@include clean/createNewType/index.md} */ function createNewType(name, dataParser, constraint) { - const constraints = coalescing.coalescing(constraint ?? []); + const constraints = flatMap.flatMap(coalescing.coalescing(constraint ?? []), (constraint) => set.constraintsSetHandlerKind.has(constraint) + ? constraint.internal.constraints + : constraint); const checkers = flatMap.flatMap(constraints, ({ internal }) => internal.checkers); const dataParserWithCheckers = constraint ? dataParser.addChecker(...checkers) diff --git a/docs/public/libs/v1/clean/newType.d.ts b/docs/public/libs/v1/clean/newType.d.ts index 42bff169c..668b4c5ba 100644 --- a/docs/public/libs/v1/clean/newType.d.ts +++ b/docs/public/libs/v1/clean/newType.d.ts @@ -1,8 +1,7 @@ -import { type Kind, type WrappedValue, type Unwrap, type NeverCoalescing, type DeepReadonly, type IsEqual } from ".."; -import { constrainedTypeKind, type ConstraintHandler } from "./constraint"; +import { type Kind, type WrappedValue, type Unwrap, type DeepReadonly, type IsEqual } from ".."; +import { constrainedTypeKind, type ConstraintsHandlerArguments, type ConstraintHandler, type ExtractConstraintSetConstraintHandlers } from "./constraint"; import { type Primitive, type EligiblePrimitive } from "./primitive"; import * as DEither from "../either"; -import * as DArray from "../array"; import type * as DDataParser from "../dataParser"; import { type DataParserContainTransform } from "./types"; export declare const newTypeKind: import("..").KindHandler>; @@ -159,15 +158,16 @@ export declare class CreateNewTypeError extends CreateNewTypeError_base { * * ``` * + * @remarks + * - You can pass a single constraint handler, a constraints set handler, or a tuple mixing constraints and constraints sets. + * - Constraints sets are expanded internally before being added to the NewType, preserving declaration order. + * * @see https://utils.duplojs.dev/en/v1/api/clean/newType * * @namespace C * */ -export declare function createNewType>[]> | readonly [ - ConstraintHandler>[]>, - ...ConstraintHandler>[]>[] -]) = never>(name: GenericName, dataParser: GenericDataParser & DataParserContainTransform, constraint?: GenericConstraintsHandler): NewTypeHandler>, DArray.ArrayCoalescing>, IsEqual, DDataParser.Input> extends true ? never : DDataParser.Input>; +export declare function createNewType, EligiblePrimitive>> = never>(name: GenericName, dataParser: GenericDataParser & DataParserContainTransform, constraint?: GenericConstraintsHandler): NewTypeHandler>, ExtractConstraintSetConstraintHandlers, IsEqual, DDataParser.Input> extends true ? never : DDataParser.Input>; export declare namespace createNewType { var overrideHandler: import("..").OverrideHandler[], unknown>[], unknown>>; } diff --git a/docs/public/libs/v1/clean/newType.mjs b/docs/public/libs/v1/clean/newType.mjs index a337a3a8c..92efce6a9 100644 --- a/docs/public/libs/v1/clean/newType.mjs +++ b/docs/public/libs/v1/clean/newType.mjs @@ -1,4 +1,5 @@ import { createCleanKind } from './kind.mjs'; +import { constraintsSetHandlerKind } from './constraint/set.mjs'; import { kindHeritage } from '../common/kind.mjs'; import { flatMap } from '../array/flatMap.mjs'; import { coalescing } from '../array/coalescing.mjs'; @@ -32,7 +33,9 @@ class CreateNewTypeError extends kindHeritage("create-new-type-error", createErr * {@include clean/createNewType/index.md} */ function createNewType(name, dataParser, constraint) { - const constraints = coalescing(constraint ?? []); + const constraints = flatMap(coalescing(constraint ?? []), (constraint) => constraintsSetHandlerKind.has(constraint) + ? constraint.internal.constraints + : constraint); const checkers = flatMap(constraints, ({ internal }) => internal.checkers); const dataParserWithCheckers = constraint ? dataParser.addChecker(...checkers) diff --git a/docs/public/libs/v1/clean/toMapDataParser.cjs b/docs/public/libs/v1/clean/toMapDataParser.cjs index e2b14c501..651578811 100644 --- a/docs/public/libs/v1/clean/toMapDataParser.cjs +++ b/docs/public/libs/v1/clean/toMapDataParser.cjs @@ -5,7 +5,6 @@ var hasSomeKinds = require('../common/hasSomeKinds.cjs'); var property = require('./entity/property.cjs'); var base = require('./primitive/base.cjs'); var base$1 = require('./constraint/base.cjs'); -var set = require('./constraint/set.cjs'); var index = require('../pattern/match/index.cjs'); var transform = require('../dataParser/parsers/transform.cjs'); var index$1 = require('../dataParser/parsers/string/index.cjs'); @@ -16,6 +15,7 @@ var date = require('../dataParser/parsers/date.cjs'); var index$4 = require('../dataParser/parsers/time/index.cjs'); var empty = require('../dataParser/parsers/empty.cjs'); var nil = require('../dataParser/parsers/nil.cjs'); +var set = require('./constraint/set.cjs'); var wrapValue = require('../common/wrapValue.cjs'); function toMapDataParser(input, params) { diff --git a/docs/public/libs/v1/clean/toMapDataParser.mjs b/docs/public/libs/v1/clean/toMapDataParser.mjs index 03ba64dfe..84678914c 100644 --- a/docs/public/libs/v1/clean/toMapDataParser.mjs +++ b/docs/public/libs/v1/clean/toMapDataParser.mjs @@ -3,7 +3,6 @@ import { hasSomeKinds } from '../common/hasSomeKinds.mjs'; import { entityPropertyDefinitionToDataParser, entityPropertyNullableKind, entityPropertyArrayKind, entityPropertyStructureKind, entityPropertyIdentifierKind, entityPropertyUnionKind } from './entity/property.mjs'; import { primitiveHandlerKind } from './primitive/base.mjs'; import { constrainedTypeKind, constraintHandlerKind } from './constraint/base.mjs'; -import { constraintsSetHandlerKind } from './constraint/set.mjs'; import { match } from '../pattern/match/index.mjs'; import { transform } from '../dataParser/parsers/transform.mjs'; import { stringKind } from '../dataParser/parsers/string/index.mjs'; @@ -14,6 +13,7 @@ import { dateKind } from '../dataParser/parsers/date.mjs'; import { timeKind } from '../dataParser/parsers/time/index.mjs'; import { emptyKind } from '../dataParser/parsers/empty.mjs'; import { nilKind } from '../dataParser/parsers/nil.mjs'; +import { constraintsSetHandlerKind } from './constraint/set.mjs'; import { keyWrappedValue } from '../common/wrapValue.mjs'; function toMapDataParser(input, params) { diff --git a/docs/public/libs/v1/dataParser/parsers/number/checkers/max.cjs b/docs/public/libs/v1/dataParser/parsers/number/checkers/max.cjs index fef70252e..442ce4d45 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/max.cjs +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/max.cjs @@ -9,11 +9,18 @@ function checkerNumberMax(max, definition = {}) { return base.dataParserCheckerInit(checkerNumberMaxKind, { definition: { ...definition, + exclusive: definition.exclusive ?? false, max, }, - }, (value, error$1, self, dataParser) => value <= self.definition.max - ? value - : error.addIssue(error$1, `number <= ${self.definition.max}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); + }, (value, error$1, self, dataParser) => { + const isValid = self.definition.exclusive + ? value < self.definition.max + : value <= self.definition.max; + if (isValid) { + return value; + } + return error.addIssue(error$1, `number ${self.definition.exclusive ? "<" : "<="} ${self.definition.max}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage); + }); } exports.checkerNumberMax = checkerNumberMax; diff --git a/docs/public/libs/v1/dataParser/parsers/number/checkers/max.d.ts b/docs/public/libs/v1/dataParser/parsers/number/checkers/max.d.ts index 42a315fac..33158f3d6 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/max.d.ts +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/max.d.ts @@ -2,6 +2,7 @@ import { type Kind } from "../../../../common"; import { type DataParserCheckerDefinition, type DataParserChecker } from "../../../../dataParser/base"; export interface DataParserCheckerDefinitionNumberMax extends DataParserCheckerDefinition { max: number; + exclusive: boolean; } export declare const checkerNumberMaxKind: import("../../../../common").KindHandler>; type _DataParserCheckerNumberMax = (Kind & DataParserChecker); diff --git a/docs/public/libs/v1/dataParser/parsers/number/checkers/max.mjs b/docs/public/libs/v1/dataParser/parsers/number/checkers/max.mjs index 67ee0bfda..d41189a4e 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/max.mjs +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/max.mjs @@ -7,11 +7,18 @@ function checkerNumberMax(max, definition = {}) { return dataParserCheckerInit(checkerNumberMaxKind, { definition: { ...definition, + exclusive: definition.exclusive ?? false, max, }, - }, (value, error, self, dataParser) => value <= self.definition.max - ? value - : addIssue(error, `number <= ${self.definition.max}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); + }, (value, error, self, dataParser) => { + const isValid = self.definition.exclusive + ? value < self.definition.max + : value <= self.definition.max; + if (isValid) { + return value; + } + return addIssue(error, `number ${self.definition.exclusive ? "<" : "<="} ${self.definition.max}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage); + }); } export { checkerNumberMax, checkerNumberMaxKind }; diff --git a/docs/public/libs/v1/dataParser/parsers/number/checkers/min.cjs b/docs/public/libs/v1/dataParser/parsers/number/checkers/min.cjs index c160ffa8f..e2b1231d2 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/min.cjs +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/min.cjs @@ -9,11 +9,18 @@ function checkerNumberMin(min, definition = {}) { return base.dataParserCheckerInit(checkerNumberMinKind, { definition: { ...definition, + exclusive: definition.exclusive ?? false, min, }, - }, (value, error$1, self, dataParser) => value >= self.definition.min - ? value - : error.addIssue(error$1, `number >= ${self.definition.min}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); + }, (value, error$1, self, dataParser) => { + const isValid = self.definition.exclusive + ? value > self.definition.min + : value >= self.definition.min; + if (isValid) { + return value; + } + return error.addIssue(error$1, `number ${self.definition.exclusive ? ">" : ">="} ${self.definition.min}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage); + }); } exports.checkerNumberMin = checkerNumberMin; diff --git a/docs/public/libs/v1/dataParser/parsers/number/checkers/min.d.ts b/docs/public/libs/v1/dataParser/parsers/number/checkers/min.d.ts index 4eb178c15..f50ce3933 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/min.d.ts +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/min.d.ts @@ -2,6 +2,7 @@ import { type Kind } from "../../../../common"; import { type DataParserCheckerDefinition, type DataParserChecker } from "../../../../dataParser/base"; export interface DataParserCheckerDefinitionNumberMin extends DataParserCheckerDefinition { min: number; + exclusive: boolean; } export declare const checkerNumberMinKind: import("../../../../common").KindHandler>; type _DataParserCheckerNumberMin = (Kind & DataParserChecker); diff --git a/docs/public/libs/v1/dataParser/parsers/number/checkers/min.mjs b/docs/public/libs/v1/dataParser/parsers/number/checkers/min.mjs index 330ab008f..3433ab82e 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/min.mjs +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/min.mjs @@ -7,11 +7,18 @@ function checkerNumberMin(min, definition = {}) { return dataParserCheckerInit(checkerNumberMinKind, { definition: { ...definition, + exclusive: definition.exclusive ?? false, min, }, - }, (value, error, self, dataParser) => value >= self.definition.min - ? value - : addIssue(error, `number >= ${self.definition.min}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); + }, (value, error, self, dataParser) => { + const isValid = self.definition.exclusive + ? value > self.definition.min + : value >= self.definition.min; + if (isValid) { + return value; + } + return addIssue(error, `number ${self.definition.exclusive ? ">" : ">="} ${self.definition.min}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage); + }); } export { checkerNumberMin, checkerNumberMinKind }; diff --git a/docs/public/libs/v1/metadata.json b/docs/public/libs/v1/metadata.json index 6c0a53e28..2f2e30f07 100644 --- a/docs/public/libs/v1/metadata.json +++ b/docs/public/libs/v1/metadata.json @@ -645,6 +645,23 @@ } ] }, + { + "name": "defaultConstraintSet", + "files": [ + { + "name": "index.d.ts" + }, + { + "name": "number.cjs" + }, + { + "name": "number.d.ts" + }, + { + "name": "number.mjs" + } + ] + }, { "name": "base.cjs" }, diff --git a/jsDoc/clean/NegativeInt/example.ts b/jsDoc/clean/NegativeInt/example.ts new file mode 100644 index 000000000..2baf4ffc8 --- /dev/null +++ b/jsDoc/clean/NegativeInt/example.ts @@ -0,0 +1,12 @@ +import { C, E } from "@scripts"; + +const result = C.NegativeInt.create(-12); + +if (E.isRight(result)) { + // result: E.Right<"createConstraintsSet", C.Primitive<-12> & C.NegativeInt> +} + +const value = C.NegativeInt.createOrThrow(0); +// value: C.Primitive<0> & C.NegativeInt + +C.NegativeInt.is(value); // type guard diff --git a/jsDoc/clean/NegativeInt/index.md b/jsDoc/clean/NegativeInt/index.md new file mode 100644 index 000000000..19179de93 --- /dev/null +++ b/jsDoc/clean/NegativeInt/index.md @@ -0,0 +1,14 @@ +Constraint set handler that validates negative integer numbers (<= 0 and integer). + +**Supported call styles:** +- Classic: `NegativeInt.create(value)` -> returns Either + +Use it as a reusable rule to validate inputs and to constrain NewTypes to negative integer numbers. + +```ts +{@include clean/NegativeInt/example.ts[3,13]} +``` + +@see https://utils.duplojs.dev/en/v1/api/clean/constraints + +@namespace C diff --git a/jsDoc/clean/Percent/example.ts b/jsDoc/clean/Percent/example.ts new file mode 100644 index 000000000..0bab5a7fe --- /dev/null +++ b/jsDoc/clean/Percent/example.ts @@ -0,0 +1,12 @@ +import { C, E } from "@scripts"; + +const result = C.Percent.create(75); + +if (E.isRight(result)) { + // result: E.Right<"createConstraintsSet", C.Primitive<75> & C.Percent> +} + +const value = C.Percent.createOrThrow(100); +// value: C.Primitive<100> & C.Percent + +C.Percent.is(value); // type guard diff --git a/jsDoc/clean/Percent/index.md b/jsDoc/clean/Percent/index.md new file mode 100644 index 000000000..04eca5b1a --- /dev/null +++ b/jsDoc/clean/Percent/index.md @@ -0,0 +1,14 @@ +Constraint set handler that validates percent numbers (between 0 and 100 included). + +**Supported call styles:** +- Classic: `Percent.create(value)` -> returns Either + +Use it as a reusable rule to validate inputs and to constrain NewTypes to values in the percent range. + +```ts +{@include clean/Percent/example.ts[3,13]} +``` + +@see https://utils.duplojs.dev/en/v1/api/clean/constraints + +@namespace C diff --git a/jsDoc/clean/PositiveInt/example.ts b/jsDoc/clean/PositiveInt/example.ts new file mode 100644 index 000000000..1ceac4237 --- /dev/null +++ b/jsDoc/clean/PositiveInt/example.ts @@ -0,0 +1,12 @@ +import { C, E } from "@scripts"; + +const result = C.PositiveInt.create(12); + +if (E.isRight(result)) { + // result: E.Right<"createConstraintsSet", C.Primitive<12> & C.PositiveInt> +} + +const value = C.PositiveInt.createOrThrow(0); +// value: C.Primitive<0> & C.PositiveInt + +C.PositiveInt.is(value); // type guard diff --git a/jsDoc/clean/PositiveInt/index.md b/jsDoc/clean/PositiveInt/index.md new file mode 100644 index 000000000..cf2c9c198 --- /dev/null +++ b/jsDoc/clean/PositiveInt/index.md @@ -0,0 +1,14 @@ +Constraint set handler that validates positive integer numbers (>= 0 and integer). + +**Supported call styles:** +- Classic: `PositiveInt.create(value)` -> returns Either + +Use it as a reusable rule to validate inputs and to constrain NewTypes to positive integer numbers. + +```ts +{@include clean/PositiveInt/example.ts[3,13]} +``` + +@see https://utils.duplojs.dev/en/v1/api/clean/constraints + +@namespace C diff --git a/jsDoc/clean/StrictNegative/example.ts b/jsDoc/clean/StrictNegative/example.ts new file mode 100644 index 000000000..4ddda0f8c --- /dev/null +++ b/jsDoc/clean/StrictNegative/example.ts @@ -0,0 +1,12 @@ +import { C, E } from "@scripts"; + +const result = C.StrictNegative.create(-4); + +if (E.isRight(result)) { + // result: E.Right<"createConstrainedType", C.ConstrainedType<"strict-negative", -4>> +} + +const value = C.StrictNegative.createOrThrow(-10); +// value: C.ConstrainedType<"strict-negative", -10> + +C.StrictNegative.is(value); // type guard diff --git a/jsDoc/clean/StrictNegative/index.md b/jsDoc/clean/StrictNegative/index.md new file mode 100644 index 000000000..27a2075c7 --- /dev/null +++ b/jsDoc/clean/StrictNegative/index.md @@ -0,0 +1,14 @@ +Constraint handler that validates strictly negative numbers (< 0). + +**Supported call styles:** +- Classic: `StrictNegative.create(value)` -> returns Either + +Use it as a reusable rule to validate inputs and to constrain NewTypes to numbers strictly less than zero. + +```ts +{@include clean/StrictNegative/example.ts[3,13]} +``` + +@see https://utils.duplojs.dev/en/v1/api/clean/constraints#strictnegative + +@namespace C diff --git a/jsDoc/clean/StrictNegativeInt/example.ts b/jsDoc/clean/StrictNegativeInt/example.ts new file mode 100644 index 000000000..ad9d48a30 --- /dev/null +++ b/jsDoc/clean/StrictNegativeInt/example.ts @@ -0,0 +1,12 @@ +import { C, E } from "@scripts"; + +const result = C.StrictNegativeInt.create(-12); + +if (E.isRight(result)) { + // result: E.Right<"createConstraintsSet", C.Primitive<-12> & C.StrictNegativeInt> +} + +const value = C.StrictNegativeInt.createOrThrow(-1); +// value: C.Primitive<-1> & C.StrictNegativeInt + +C.StrictNegativeInt.is(value); // type guard diff --git a/jsDoc/clean/StrictNegativeInt/index.md b/jsDoc/clean/StrictNegativeInt/index.md new file mode 100644 index 000000000..387a94867 --- /dev/null +++ b/jsDoc/clean/StrictNegativeInt/index.md @@ -0,0 +1,14 @@ +Constraint set handler that validates strictly negative integer numbers (<= -1 and integer). + +**Supported call styles:** +- Classic: `StrictNegativeInt.create(value)` -> returns Either + +Use it as a reusable rule to validate inputs and to constrain NewTypes to strictly negative integer numbers. + +```ts +{@include clean/StrictNegativeInt/example.ts[3,13]} +``` + +@see https://utils.duplojs.dev/en/v1/api/clean/constraints + +@namespace C diff --git a/jsDoc/clean/StrictPositive/example.ts b/jsDoc/clean/StrictPositive/example.ts new file mode 100644 index 000000000..675c2362e --- /dev/null +++ b/jsDoc/clean/StrictPositive/example.ts @@ -0,0 +1,12 @@ +import { C, E } from "@scripts"; + +const result = C.StrictPositive.create(4); + +if (E.isRight(result)) { + // result: E.Right<"createConstrainedType", C.ConstrainedType<"strict-positive", 4>> +} + +const value = C.StrictPositive.createOrThrow(10); +// value: C.ConstrainedType<"strict-positive", 10> + +C.StrictPositive.is(value); // type guard diff --git a/jsDoc/clean/StrictPositive/index.md b/jsDoc/clean/StrictPositive/index.md new file mode 100644 index 000000000..6d2bfa69d --- /dev/null +++ b/jsDoc/clean/StrictPositive/index.md @@ -0,0 +1,14 @@ +Constraint handler that validates strictly positive numbers (> 0). + +**Supported call styles:** +- Classic: `StrictPositive.create(value)` -> returns Either + +Use it as a reusable rule to validate inputs and to constrain NewTypes to numbers strictly greater than zero. + +```ts +{@include clean/StrictPositive/example.ts[3,13]} +``` + +@see https://utils.duplojs.dev/en/v1/api/clean/constraints#strictpositive + +@namespace C diff --git a/jsDoc/clean/StrictPositiveInt/example.ts b/jsDoc/clean/StrictPositiveInt/example.ts new file mode 100644 index 000000000..5ca0cd27d --- /dev/null +++ b/jsDoc/clean/StrictPositiveInt/example.ts @@ -0,0 +1,12 @@ +import { C, E } from "@scripts"; + +const result = C.StrictPositiveInt.create(12); + +if (E.isRight(result)) { + // result: E.Right<"createConstraintsSet", C.Primitive<12> & C.StrictPositiveInt> +} + +const value = C.StrictPositiveInt.createOrThrow(1); +// value: C.Primitive<1> & C.StrictPositiveInt + +C.StrictPositiveInt.is(value); // type guard diff --git a/jsDoc/clean/StrictPositiveInt/index.md b/jsDoc/clean/StrictPositiveInt/index.md new file mode 100644 index 000000000..a59bb0304 --- /dev/null +++ b/jsDoc/clean/StrictPositiveInt/index.md @@ -0,0 +1,14 @@ +Constraint set handler that validates strictly positive integer numbers (>= 1 and integer). + +**Supported call styles:** +- Classic: `StrictPositiveInt.create(value)` -> returns Either + +Use it as a reusable rule to validate inputs and to constrain NewTypes to strictly positive integer numbers. + +```ts +{@include clean/StrictPositiveInt/example.ts[3,13]} +``` + +@see https://utils.duplojs.dev/en/v1/api/clean/constraints + +@namespace C diff --git a/jsDoc/clean/createConstraintsSet/index.md b/jsDoc/clean/createConstraintsSet/index.md index da741dd30..be2d0022c 100644 --- a/jsDoc/clean/createConstraintsSet/index.md +++ b/jsDoc/clean/createConstraintsSet/index.md @@ -10,7 +10,8 @@ A constraints set validates input with the primitive DataParser, applies all con ``` @remarks -- You can pass a single constraint handler or a tuple of constraints. +- You can pass a single constraint handler, a constraints set handler, or a tuple mixing constraints and constraints sets. +- Constraints sets are expanded internally, so their contained constraints are applied in declaration order. - The handler accepts both raw values and primitives. @see https://utils.duplojs.dev/en/v1/api/clean/constraints diff --git a/jsDoc/clean/createNewType/index.md b/jsDoc/clean/createNewType/index.md index 17d879595..9ac5ac783 100644 --- a/jsDoc/clean/createNewType/index.md +++ b/jsDoc/clean/createNewType/index.md @@ -9,6 +9,10 @@ A NewType validates input with a DataParser, applies optional constraints, and b {@include clean/createNewType/example.ts[3,29]} ``` +@remarks +- You can pass a single constraint handler, a constraints set handler, or a tuple mixing constraints and constraints sets. +- Constraints sets are expanded internally before being added to the NewType, preserving declaration order. + @see https://utils.duplojs.dev/en/v1/api/clean/newType @namespace C diff --git a/scripts/clean/constraint/cast.ts b/scripts/clean/constraint/cast.ts index ae443553f..8d435d3c4 100644 --- a/scripts/clean/constraint/cast.ts +++ b/scripts/clean/constraint/cast.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ import { type UnionToIntersection, type SimplifyTopLevel, type NeverCoalescing, type And, type Not, type IsEqual } from "@scripts/common"; import { type ConstraintHandler, type ConstrainedType, type GetConstraint, constrainedTypeKind } from "./base"; -import { type Negative, type NumberMaxHandlerInternal, type NumberMaxInternal, type NumberMinHandlerInternal, type NumberMinInternal, type Positive, type StringMaxHandlerInternal, type StringMaxInternal, type StringMinHandlerInternal, type StringMinInternal } from "./defaultConstraint"; +import { type StrictNegative, type StrictPositive, type Negative, type NumberMaxHandlerInternal, type NumberMaxInternal, type NumberMinHandlerInternal, type NumberMinInternal, type Positive, type StringMaxHandlerInternal, type StringMaxInternal, type StringMinHandlerInternal, type StringMinInternal } from "./defaultConstraint"; import { type IsLess, type IsGreater } from "@scripts/number"; import * as DArray from "@scripts/array"; @@ -24,14 +24,21 @@ type ForbiddenBadCast< ? IsLess extends true ? never : CastConstraintError - : GenericConstrainedType extends NumberMinInternal + : GenericConstrainedType extends StrictPositive ? And<[ - IsLess, - Not>, + IsLess, + Not>, ]> extends true ? never - : CastConstraintError - : CastConstraintError + : CastConstraintError + : GenericConstrainedType extends NumberMinInternal + ? And<[ + IsLess, + Not>, + ]> extends true + ? never + : CastConstraintError + : CastConstraintError : never ) | ( @@ -40,14 +47,22 @@ type ForbiddenBadCast< ? IsGreater extends true ? never : CastConstraintError - : GenericConstrainedType extends NumberMaxInternal + : GenericConstrainedType extends StrictNegative ? And<[ - IsGreater, - Not>, + IsGreater, + Not>, ]> extends true ? never - : CastConstraintError - : CastConstraintError + : CastConstraintError + + : GenericConstrainedType extends NumberMaxInternal + ? And<[ + IsGreater, + Not>, + ]> extends true + ? never + : CastConstraintError + : CastConstraintError : never ) | ( @@ -98,10 +113,38 @@ type ForbiddenBadCast< : CastConstraintError : never ) + | ( + GenericConstrainHandler extends typeof StrictPositive + ? GenericConstrainedType extends NumberMinInternal + ? And<[ + IsLess<0, InferredReferenceValue>, + Not>, + Not>, + ]> extends true + ? never + : CastConstraintError + : CastConstraintError + : never + ) + | ( + GenericConstrainHandler extends typeof StrictNegative + ? GenericConstrainedType extends NumberMaxInternal + ? And<[ + IsGreater<0, InferredReferenceValue>, + Not>, + Not>, + ]> extends true + ? never + : CastConstraintError + : CastConstraintError + : never + ) | ( GenericConstrainHandler extends ( | typeof Negative | typeof Positive + | typeof StrictNegative + | typeof StrictPositive | StringMaxHandlerInternal | StringMinHandlerInternal | NumberMaxHandlerInternal diff --git a/scripts/clean/constraint/defaultConstraint/number.ts b/scripts/clean/constraint/defaultConstraint/number.ts index 595d3ddf0..c263441df 100644 --- a/scripts/clean/constraint/defaultConstraint/number.ts +++ b/scripts/clean/constraint/defaultConstraint/number.ts @@ -23,6 +23,16 @@ export const Positive = createConstraint( ); export type Positive = GetConstraint; +/** + * {@include clean/StrictPositive/index.md} + */ +export const StrictPositive = createConstraint( + "strict-positive", + Number, + DDataParser.checkerNumberMin(0, { exclusive: true }), +); +export type StrictPositive = GetConstraint; + /** * {@include clean/Negative/index.md} */ @@ -33,11 +43,21 @@ export const Negative = createConstraint( ); export type Negative = GetConstraint; +/** + * {@include clean/StrictNegative/index.md} + */ +export const StrictNegative = createConstraint( + "strict-negative", + Number, + DDataParser.checkerNumberMax(0, { exclusive: true }), +); +export type StrictNegative = GetConstraint; + export type NumberMinHandlerInternal< GenericValue extends number = number, > = Extract< ConstraintHandler< - `number-min-${GenericValue}`, + `number-min-${GenericValue}`, number, readonly [DDataParser.DataParserCheckerNumberMin], never @@ -78,7 +98,7 @@ export type NumberMaxHandlerInternal< GenericValue extends number = number, > = Extract< ConstraintHandler< - `number-max-${GenericValue}`, + `number-max-${GenericValue}`, number, readonly [DDataParser.DataParserCheckerNumberMax], never diff --git a/scripts/clean/constraint/defaultConstraint/string.ts b/scripts/clean/constraint/defaultConstraint/string.ts index 68fb3ffc0..5a34e94c8 100644 --- a/scripts/clean/constraint/defaultConstraint/string.ts +++ b/scripts/clean/constraint/defaultConstraint/string.ts @@ -19,7 +19,7 @@ export const Uuid = createConstraint( DDataParser.checkerUuid(), ); -export type Uuid = GetConstraint; +export type Uuid = GetConstraint; /** * {@include clean/Url/index.md} diff --git a/scripts/clean/constraint/defaultConstraintSet/index.ts b/scripts/clean/constraint/defaultConstraintSet/index.ts new file mode 100644 index 000000000..2f0dcabd3 --- /dev/null +++ b/scripts/clean/constraint/defaultConstraintSet/index.ts @@ -0,0 +1 @@ +export * from "./number"; diff --git a/scripts/clean/constraint/defaultConstraintSet/number.ts b/scripts/clean/constraint/defaultConstraintSet/number.ts new file mode 100644 index 000000000..76bee21aa --- /dev/null +++ b/scripts/clean/constraint/defaultConstraintSet/number.ts @@ -0,0 +1,63 @@ +import { createConstraintsSet, type GetConstraints } from "../set"; +import { Number } from "../../primitive"; +import { Int, Negative, NumberMax, NumberMin, Positive, StrictNegative, StrictPositive } from "../defaultConstraint"; + +/** + * {@include clean/PositiveInt/index.md} + */ +export const PositiveInt = createConstraintsSet( + Number, + [ + Positive, + Int, + ], +); +export type PositiveInt = GetConstraints; + +/** + * {@include clean/StrictPositiveInt/index.md} + */ +export const StrictPositiveInt = createConstraintsSet( + Number, + [ + StrictPositive, + Int, + ], +); +export type StrictPositiveInt = GetConstraints; + +/** + * {@include clean/NegativeInt/index.md} + */ +export const NegativeInt = createConstraintsSet( + Number, + [ + Negative, + Int, + ], +); +export type NegativeInt = GetConstraints; + +/** + * {@include clean/StrictNegativeInt/index.md} + */ +export const StrictNegativeInt = createConstraintsSet( + Number, + [ + StrictNegative, + Int, + ], +); +export type StrictNegativeInt = GetConstraints; + +/** + * {@include clean/Percent/index.md} + */ +export const Percent = createConstraintsSet( + Number, + [ + NumberMin(0), + NumberMax(100), + ], +); +export type Percent = GetConstraints; diff --git a/scripts/clean/constraint/index.ts b/scripts/clean/constraint/index.ts index 626baee44..efdeab003 100644 --- a/scripts/clean/constraint/index.ts +++ b/scripts/clean/constraint/index.ts @@ -2,3 +2,4 @@ export * from "./base"; export * from "./cast"; export * from "./defaultConstraint"; export * from "./set"; +export * from "./defaultConstraintSet"; diff --git a/scripts/clean/constraint/set.ts b/scripts/clean/constraint/set.ts index 54de77a6e..204500fa4 100644 --- a/scripts/clean/constraint/set.ts +++ b/scripts/clean/constraint/set.ts @@ -257,51 +257,86 @@ export class CreateConstraintsSetError extends kindHeritage( } } +export type ConstraintSetInputConstraint< + GenericValue extends EligiblePrimitive = EligiblePrimitive, +> = ( + ConstraintHandler< + string, + GenericValue, + readonly DDataParser.DataParserChecker< + DDataParser.DataParserCheckerDefinition, + GenericValue + >[] + > + | ConstraintsSetHandler< + GenericValue, + readonly ConstraintHandler< + string, + GenericValue, + readonly DDataParser.DataParserChecker< + DDataParser.DataParserCheckerDefinition, + GenericValue + >[] + >[] + > +); + +export type ConstraintsHandlerArguments< + GenericValue extends EligiblePrimitive, +> = ( + | ConstraintSetInputConstraint + | readonly [ + ConstraintSetInputConstraint, + ...ConstraintSetInputConstraint[], + ] +); + +export type ExtractConstraintSetConstraintHandlers< + GenericConstraint extends ( + | ConstraintSetInputConstraint + | readonly ConstraintSetInputConstraint[] + | readonly [] + ), +> = GenericConstraint extends ConstraintHandler + ? readonly [GenericConstraint] + : GenericConstraint extends ConstraintsSetHandler + ? ExtractConstraintSetConstraintHandlers + : GenericConstraint extends readonly [] + ? GenericConstraint + : GenericConstraint extends readonly [ + infer InferredFirst extends ConstraintSetInputConstraint, + ...infer InferredRest extends readonly ConstraintSetInputConstraint[], + ] + ? ExtractConstraintSetConstraintHandlers extends infer + InferredResultRest extends readonly any[] + ? readonly [ + ...ExtractConstraintSetConstraintHandlers, + ...InferredResultRest, + ] + : never + : never; + /** * {@include clean/createConstraintsSet/index.md} */ export function createConstraintsSet< GenericPrimitiveValue extends EligiblePrimitive, GenericPrimitiveInput extends unknown, - const GenericConstrainHandler extends( - | ConstraintHandler< - string, - EligiblePrimitive, - readonly DDataParser.DataParserChecker< - DDataParser.DataParserCheckerDefinition, - GenericPrimitiveValue - >[] - > - | readonly [ - ConstraintHandler< - string, - EligiblePrimitive, - readonly DDataParser.DataParserChecker< - DDataParser.DataParserCheckerDefinition, - GenericPrimitiveValue - >[] - >, - ...ConstraintHandler< - string, - EligiblePrimitive, - readonly DDataParser.DataParserChecker< - DDataParser.DataParserCheckerDefinition, - GenericPrimitiveValue - >[] - >[], - ] - ) = never, + const GenericConstrainHandler extends ConstraintsHandlerArguments = never, >( primitiveHandler: PrimitiveHandler, constraint: GenericConstrainHandler, ): ConstraintsSetHandler< GenericPrimitiveValue, - DArray.ArrayCoalescing< - GenericConstrainHandler - >, + ExtractConstraintSetConstraintHandlers, GenericPrimitiveInput > { - const constraints = DArray.coalescing(constraint); + const constraints = DArray.flatMap( + DArray.coalescing(constraint), + (constraint) => constraintsSetHandlerKind.has(constraint) + ? constraint.internal.constraints + : constraint, + ) as ConstraintHandler[]; const checkers = DArray.flatMap( constraints, diff --git a/scripts/clean/newType.ts b/scripts/clean/newType.ts index 9316be157..6ab15518f 100644 --- a/scripts/clean/newType.ts +++ b/scripts/clean/newType.ts @@ -1,6 +1,6 @@ -import { type Kind, type WrappedValue, unwrap, wrapValue, kindHeritage, createErrorKind, type Unwrap, pipe, type NeverCoalescing, type DeepReadonly, type RemoveKind, createOverride, type AnyFunction, type IsEqual } from "@scripts"; +import { type Kind, type WrappedValue, unwrap, wrapValue, kindHeritage, createErrorKind, type Unwrap, pipe, type DeepReadonly, type RemoveKind, createOverride, type AnyFunction, type IsEqual } from "@scripts"; import { createCleanKind } from "./kind"; -import { constrainedTypeKind, type ConstraintHandler } from "./constraint"; +import { constrainedTypeKind, type ConstraintsHandlerArguments, constraintsSetHandlerKind, type ConstraintHandler, type ConstraintSetInputConstraint, type ExtractConstraintSetConstraintHandlers } from "./constraint"; import { type Primitive, type EligiblePrimitive } from "./primitive"; import * as DEither from "../either"; import * as DArray from "../array"; @@ -252,34 +252,9 @@ export class CreateNewTypeError extends kindHeritage( export function createNewType< GenericName extends string, GenericDataParser extends DDataParser.DataParser, - const GenericConstraintsHandler extends( - | ConstraintHandler< - string, - EligiblePrimitive, - readonly DDataParser.DataParserChecker< - DDataParser.DataParserCheckerDefinition, - DDataParser.Output - >[] - > - | readonly [ - ConstraintHandler< - string, - EligiblePrimitive, - readonly DDataParser.DataParserChecker< - DDataParser.DataParserCheckerDefinition, - DDataParser.Output - >[] - >, - ...ConstraintHandler< - string, - EligiblePrimitive, - readonly DDataParser.DataParserChecker< - DDataParser.DataParserCheckerDefinition, - DDataParser.Output - >[] - >[], - ] - ) = never, + const GenericConstraintsHandler extends ConstraintsHandlerArguments< + Extract, EligiblePrimitive> + > = never, >( name: GenericName, dataParser: GenericDataParser & DataParserContainTransform, @@ -287,14 +262,17 @@ export function createNewType< ): NewTypeHandler< GenericName, DeepReadonly>, - DArray.ArrayCoalescing< - NeverCoalescing - >, + ExtractConstraintSetConstraintHandlers, IsEqual, DDataParser.Input> extends true ? never : DDataParser.Input > { - const constraints = DArray.coalescing(constraint ?? []); + const constraints = DArray.flatMap( + DArray.coalescing(constraint ?? []), + (constraint) => constraintsSetHandlerKind.has(constraint) + ? constraint.internal.constraints + : constraint, + ) as ConstraintHandler[]; const checkers = DArray.flatMap( constraints, diff --git a/scripts/dataParser/parsers/number/checkers/max.ts b/scripts/dataParser/parsers/number/checkers/max.ts index 712196cc2..52e570ce7 100644 --- a/scripts/dataParser/parsers/number/checkers/max.ts +++ b/scripts/dataParser/parsers/number/checkers/max.ts @@ -5,6 +5,7 @@ import { createDataParserKind } from "../../../kind"; export interface DataParserCheckerDefinitionNumberMax extends DataParserCheckerDefinition { max: number; + exclusive: boolean; } export const checkerNumberMaxKind = createDataParserKind("checker-number-max"); @@ -32,11 +33,25 @@ export function checkerNumberMax( { definition: { ...definition, + exclusive: definition.exclusive ?? false, max, }, }, - (value, error, self, dataParser) => value <= self.definition.max - ? value - : addIssue(error, `number <= ${self.definition.max}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage), + (value, error, self, dataParser) => { + const isValid = self.definition.exclusive + ? value < self.definition.max + : value <= self.definition.max; + + if (isValid) { + return value; + } + + return addIssue( + error, + `number ${self.definition.exclusive ? "<" : "<="} ${self.definition.max}`, + value, + self.definition.errorMessage ?? dataParser.definition.errorMessage, + ); + }, ); } diff --git a/scripts/dataParser/parsers/number/checkers/min.ts b/scripts/dataParser/parsers/number/checkers/min.ts index 8c2c7d23a..bef48d955 100644 --- a/scripts/dataParser/parsers/number/checkers/min.ts +++ b/scripts/dataParser/parsers/number/checkers/min.ts @@ -5,6 +5,7 @@ import { createDataParserKind } from "../../../kind"; export interface DataParserCheckerDefinitionNumberMin extends DataParserCheckerDefinition { min: number; + exclusive: boolean; } export const checkerNumberMinKind = createDataParserKind("checker-number-min"); @@ -32,11 +33,25 @@ export function checkerNumberMin( { definition: { ...definition, + exclusive: definition.exclusive ?? false, min, }, }, - (value, error, self, dataParser) => value >= self.definition.min - ? value - : addIssue(error, `number >= ${self.definition.min}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage), + (value, error, self, dataParser) => { + const isValid = self.definition.exclusive + ? value > self.definition.min + : value >= self.definition.min; + + if (isValid) { + return value; + } + + return addIssue( + error, + `number ${self.definition.exclusive ? ">" : ">="} ${self.definition.min}`, + value, + self.definition.errorMessage ?? dataParser.definition.errorMessage, + ); + }, ); } diff --git a/tests/clean/constraint/cast.test.ts b/tests/clean/constraint/cast.test.ts index 466188a7d..c4115abae 100644 --- a/tests/clean/constraint/cast.test.ts +++ b/tests/clean/constraint/cast.test.ts @@ -218,4 +218,82 @@ describe("castConstraint", () => { DClean.Email, ); }); + + it("supports strict positive and strict negative cast boundaries at the type level", () => { + // StrictPositive + const strictPositiveFromNumberMin = DClean.castConstraint( + DClean.NumberMin(1).createOrThrow(2), + DClean.StrictPositive, + ); + type CheckStrictPositiveFromNumberMin = ExpectType< + typeof strictPositiveFromNumberMin, + & DClean.ConstrainedType<"number-min-1", 2> + & DClean.ConstrainedType<"strict-positive", number>, + "strict" + >; + + DClean.castConstraint( + DClean.NumberMin(0.5).createOrThrow(2), + DClean.StrictPositive, + ); + + DClean.castConstraint( + //@ts-expect-error strict-positive is potentially less 0.5 + DClean.StrictPositive.createOrThrow(2), + DClean.NumberMin(0.5), + ); + + DClean.castConstraint( + //@ts-expect-error strict-positive is not negative + DClean.StrictPositive.createOrThrow(2), + DClean.Negative, + ); + + DClean.castConstraint( + //@ts-expect-error no casting possible + DClean.StrictPositive.createOrThrow(2), + DClean.Email, + ); + + // StrictNegative + const strictNegativeFromNumberMax = DClean.castConstraint( + DClean.NumberMax(-1).createOrThrow(-2), + DClean.StrictNegative, + ); + type CheckStrictNegativeFromNumberMax = ExpectType< + typeof strictNegativeFromNumberMax, + & DClean.ConstrainedType<"number-max--1", -2> + & DClean.ConstrainedType<"strict-negative", number>, + "strict" + >; + + DClean.castConstraint( + DClean.NumberMax(-0.5).createOrThrow(-2), + DClean.StrictNegative, + ); + + DClean.castConstraint( + //@ts-expect-error strict-negative is less 0.5 + DClean.StrictNegative.createOrThrow(-2), + DClean.NumberMin(0.5), + ); + + DClean.castConstraint( + //@ts-expect-error strict-negative is not positive + DClean.StrictNegative.createOrThrow(-2), + DClean.Positive, + ); + + DClean.castConstraint( + //@ts-expect-error strict-negative is less 0 + DClean.StrictNegative.createOrThrow(-2), + DClean.Negative, + ); + + DClean.castConstraint( + //@ts-expect-error no casting possible + DClean.StrictNegative.createOrThrow(-2), + DClean.Email, + ); + }); }); diff --git a/tests/clean/constraint/defaultConstraint/number.test.ts b/tests/clean/constraint/defaultConstraint/number.test.ts new file mode 100644 index 000000000..3beb61058 --- /dev/null +++ b/tests/clean/constraint/defaultConstraint/number.test.ts @@ -0,0 +1,179 @@ +import { DClean, type DDataParser, DEither, wrapValue, type ExpectType } from "@scripts"; + +describe("defaultConstraint number", () => { + it("validates integer numbers", () => { + const result = DClean.Int.create(1); + + expect(result).toStrictEqual( + DEither.right( + "createConstrainedType", + DClean.constrainedTypeKind.setTo( + wrapValue(1), + { int: null }, + ), + ), + ); + expect(() => DClean.Int.createOrThrow(1.5)) + .toThrow(DClean.CreateConstrainedTypeError); + + type check = ExpectType< + typeof DClean.Int, + DClean.ConstraintHandler<"int", number, readonly [DDataParser.DataParserCheckerInt], never>, + "strict" + >; + + type check1 = ExpectType< + DClean.Int, + DClean.ConstrainedType<"int", number>, + "strict" + >; + }); + + it("validates positive numbers", () => { + const value = DClean.Positive.createOrThrow(0); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(0), + { positive: null }, + ), + ); + expect(() => DClean.Positive.createOrThrow(-1)) + .toThrow(DClean.CreateConstrainedTypeError); + + type check = ExpectType< + typeof DClean.Positive, + DClean.ConstraintHandler<"positive", number, readonly [DDataParser.DataParserCheckerNumberMin], never>, + "strict" + >; + + type check1 = ExpectType< + DClean.Positive, + DClean.ConstrainedType<"positive", number>, + "strict" + >; + }); + + it("validates strictly positive numbers", () => { + const value = DClean.StrictPositive.createOrThrow(Number.EPSILON); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(Number.EPSILON), + { "strict-positive": null }, + ), + ); + expect(DClean.Positive.createOrThrow(0)).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(0), + { positive: null }, + ), + ); + expect(() => DClean.StrictPositive.createOrThrow(0)) + .toThrow(DClean.CreateConstrainedTypeError); + + type check = ExpectType< + typeof DClean.StrictPositive, + DClean.ConstraintHandler<"strict-positive", number, readonly [DDataParser.DataParserCheckerNumberMin], never>, + "strict" + >; + + type check1 = ExpectType< + DClean.StrictPositive, + DClean.ConstrainedType<"strict-positive", number>, + "strict" + >; + }); + + it("validates negative numbers", () => { + const value = DClean.Negative.createOrThrow(0); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(0), + { negative: null }, + ), + ); + expect(() => DClean.Negative.createOrThrow(1)) + .toThrow(DClean.CreateConstrainedTypeError); + + type check = ExpectType< + typeof DClean.Negative, + DClean.ConstraintHandler<"negative", number, readonly [DDataParser.DataParserCheckerNumberMax], never>, + "strict" + >; + + type check1 = ExpectType< + DClean.Negative, + DClean.ConstrainedType<"negative", number>, + "strict" + >; + }); + + it("validates strictly negative numbers", () => { + const value = DClean.StrictNegative.createOrThrow(-Number.EPSILON); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(-Number.EPSILON), + { "strict-negative": null }, + ), + ); + expect(DClean.Negative.createOrThrow(0)).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(0), + { negative: null }, + ), + ); + expect(() => DClean.StrictNegative.createOrThrow(0)) + .toThrow(DClean.CreateConstrainedTypeError); + + type check = ExpectType< + typeof DClean.StrictNegative, + DClean.ConstraintHandler<"strict-negative", number, readonly [DDataParser.DataParserCheckerNumberMax], never>, + "strict" + >; + + type check1 = ExpectType< + DClean.StrictNegative, + DClean.ConstrainedType<"strict-negative", number>, + "strict" + >; + }); + + it("creates minimum and maximum number constraints", () => { + const numberMin = DClean.NumberMin(10); + const numberMax = DClean.NumberMax(20); + + expect(numberMin.name).toBe("number-min-10"); + expect(numberMax.name).toBe("number-max-20"); + expect(numberMin.createOrThrow(10)).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(10), + { "number-min-10": null }, + ), + ); + expect(numberMax.createOrThrow(20)).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(20), + { "number-max-20": null }, + ), + ); + expect(() => numberMin.createOrThrow(9)) + .toThrow(DClean.CreateConstrainedTypeError); + expect(() => numberMax.createOrThrow(21)) + .toThrow(DClean.CreateConstrainedTypeError); + + type check = ExpectType< + typeof numberMin, + DClean.ConstraintHandler<"number-min-10", number, readonly [DDataParser.DataParserCheckerNumberMin], never>, + "strict" + >; + + type check1 = ExpectType< + typeof numberMax, + DClean.ConstraintHandler<"number-max-20", number, readonly [DDataParser.DataParserCheckerNumberMax], never>, + "strict" + >; + }); +}); diff --git a/tests/clean/constraint/defaultConstraint/string.test.ts b/tests/clean/constraint/defaultConstraint/string.test.ts new file mode 100644 index 000000000..16613a98b --- /dev/null +++ b/tests/clean/constraint/defaultConstraint/string.test.ts @@ -0,0 +1,117 @@ +import { DClean, type DDataParser, DEither, wrapValue, type ExpectType } from "@scripts"; + +describe("defaultConstraint string", () => { + it("validates email strings", () => { + const result = DClean.Email.create("user@example.com"); + + expect(result).toStrictEqual( + DEither.right( + "createConstrainedType", + DClean.constrainedTypeKind.setTo( + wrapValue("user@example.com"), + { email: null }, + ), + ), + ); + expect(() => DClean.Email.createOrThrow("invalid")) + .toThrow(DClean.CreateConstrainedTypeError); + + type check = ExpectType< + typeof DClean.Email, + DClean.ConstraintHandler<"email", string, readonly [DDataParser.DataParserCheckerEmail], never>, + "strict" + >; + + type check1 = ExpectType< + DClean.Email, + DClean.ConstrainedType<"email", string>, + "strict" + >; + }); + + it("validates uuid strings", () => { + const value = DClean.Uuid.createOrThrow("550e8400-e29b-41d4-a716-446655440000"); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue("550e8400-e29b-41d4-a716-446655440000"), + { uuid: null }, + ), + ); + expect(() => DClean.Uuid.createOrThrow("invalid")) + .toThrow(DClean.CreateConstrainedTypeError); + + type check = ExpectType< + typeof DClean.Uuid, + DClean.ConstraintHandler<"uuid", string, readonly [DDataParser.DataParserCheckerUuid], never>, + "strict" + >; + + type check1 = ExpectType< + DClean.Uuid, + DClean.ConstrainedType<"uuid", string>, + "strict" + >; + }); + + it("validates url strings", () => { + const value = DClean.Url.createOrThrow("https://utils.duplojs.dev"); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue("https://utils.duplojs.dev"), + { url: null }, + ), + ); + expect(() => DClean.Url.createOrThrow("invalid")) + .toThrow(DClean.CreateConstrainedTypeError); + + type check = ExpectType< + typeof DClean.Url, + DClean.ConstraintHandler<"url", string, readonly [DDataParser.DataParserCheckerUrl], never>, + "strict" + >; + + type check1 = ExpectType< + DClean.Url, + DClean.ConstrainedType<"url", string>, + "strict" + >; + }); + + it("creates minimum and maximum string constraints", () => { + const stringMin = DClean.StringMin(3); + const stringMax = DClean.StringMax(5); + + expect(stringMin.name).toBe("string-min-3"); + expect(stringMax.name).toBe("string-max-5"); + expect(stringMin.createOrThrow("abc")).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue("abc"), + { "string-min-3": null }, + ), + ); + expect(stringMax.createOrThrow("abcde")).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue("abcde"), + { "string-max-5": null }, + ), + ); + expect(() => stringMin.createOrThrow("ab")) + .toThrow(DClean.CreateConstrainedTypeError); + expect(() => stringMax.createOrThrow("abcdef")) + .toThrow(DClean.CreateConstrainedTypeError); + + type check = ExpectType< + typeof stringMin, + DClean.ConstraintHandler<"string-min-3", string, readonly [DDataParser.DataParserCheckerStringMin], never>, + "strict" + >; + + type check1 = ExpectType< + typeof stringMax, + DClean.ConstraintHandler<"string-max-5", string, readonly [DDataParser.DataParserCheckerStringMax], never>, + "strict" + >; + }); +}); diff --git a/tests/clean/constraint/defaultConstraint/time.test.ts b/tests/clean/constraint/defaultConstraint/time.test.ts index 7dc593bbb..a45434561 100644 --- a/tests/clean/constraint/defaultConstraint/time.test.ts +++ b/tests/clean/constraint/defaultConstraint/time.test.ts @@ -1,4 +1,4 @@ -import { DEither, DClean, pipe, type ExpectType, type DDate, type DDataParser } from "@scripts"; +import { DEither, DClean, type ExpectType, type DDate, type DDataParser } from "@scripts"; describe("defaultConstraint time", () => { it("creates a positive time constraint for positive input", () => { diff --git a/tests/clean/constraint/defaultConstraintSet/number.test.ts b/tests/clean/constraint/defaultConstraintSet/number.test.ts new file mode 100644 index 000000000..cfd8ef9bf --- /dev/null +++ b/tests/clean/constraint/defaultConstraintSet/number.test.ts @@ -0,0 +1,161 @@ +import { DClean, DEither, wrapValue, type ExpectType } from "@scripts"; + +describe("default number constraints sets", () => { + it("validates positive integer numbers", () => { + const result = DClean.PositiveInt.create(0); + + expect(result).toStrictEqual( + DEither.right( + "createConstraintsSet", + DClean.constrainedTypeKind.setTo( + wrapValue(0), + { + positive: null, + int: null, + }, + ), + ), + ); + expect(() => DClean.PositiveInt.createOrThrow(-1)) + .toThrow(DClean.CreateConstraintsSetError); + expect(() => DClean.PositiveInt.createOrThrow(1.5)) + .toThrow(DClean.CreateConstraintsSetError); + + type check = ExpectType< + typeof DClean.PositiveInt, + DClean.ConstraintsSetHandler< + number, + readonly [ + typeof DClean.Positive, + typeof DClean.Int, + ], + never + >, + "strict" + >; + }); + + it("validates strictly positive integer numbers", () => { + const value = DClean.StrictPositiveInt.createOrThrow(1); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(1), + { + "strict-positive": null, + int: null, + }, + ), + ); + expect(() => DClean.StrictPositiveInt.createOrThrow(0)) + .toThrow(DClean.CreateConstraintsSetError); + expect(() => DClean.StrictPositiveInt.createOrThrow(1.5)) + .toThrow(DClean.CreateConstraintsSetError); + + type check = ExpectType< + typeof DClean.StrictPositiveInt, + DClean.ConstraintsSetHandler< + number, + readonly [ + typeof DClean.StrictPositive, + typeof DClean.Int, + ], + never + >, + "strict" + >; + }); + + it("validates negative integer numbers", () => { + const value = DClean.NegativeInt.createOrThrow(0); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(0), + { + negative: null, + int: null, + }, + ), + ); + expect(() => DClean.NegativeInt.createOrThrow(1)) + .toThrow(DClean.CreateConstraintsSetError); + expect(() => DClean.NegativeInt.createOrThrow(-1.5)) + .toThrow(DClean.CreateConstraintsSetError); + + type check = ExpectType< + typeof DClean.NegativeInt, + DClean.ConstraintsSetHandler< + number, + readonly [ + typeof DClean.Negative, + typeof DClean.Int, + ], + never + >, + "strict" + >; + }); + + it("validates strictly negative integer numbers", () => { + const value = DClean.StrictNegativeInt.createOrThrow(-1); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(-1), + { + "strict-negative": null, + int: null, + }, + ), + ); + expect(() => DClean.StrictNegativeInt.createOrThrow(0)) + .toThrow(DClean.CreateConstraintsSetError); + expect(() => DClean.StrictNegativeInt.createOrThrow(-1.5)) + .toThrow(DClean.CreateConstraintsSetError); + + type check = ExpectType< + typeof DClean.StrictNegativeInt, + DClean.ConstraintsSetHandler< + number, + readonly [ + typeof DClean.StrictNegative, + typeof DClean.Int, + ], + never + >, + "strict" + >; + }); + + it("validates percent numbers", () => { + const value = DClean.Percent.createOrThrow(100); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue(100), + { + "number-min-0": null, + "number-max-100": null, + }, + ), + ); + expect(() => DClean.Percent.createOrThrow(-1)) + .toThrow(DClean.CreateConstraintsSetError); + expect(() => DClean.Percent.createOrThrow(101)) + .toThrow(DClean.CreateConstraintsSetError); + + type check = ExpectType< + typeof DClean.Percent, + DClean.ConstraintsSetHandler< + number, + readonly [ + DClean.NumberMinHandlerInternal<0>, + DClean.NumberMaxHandlerInternal<100>, + ], + never + >, + "strict" + >; + }); +}); diff --git a/tests/clean/constraint/index.test.ts b/tests/clean/constraint/index.test.ts index 3e893f59a..21d2a4c98 100644 --- a/tests/clean/constraint/index.test.ts +++ b/tests/clean/constraint/index.test.ts @@ -163,44 +163,4 @@ describe("createConstraint", () => { expect(result).toStrictEqual(createResult); }); - - it("string nim and max", () => { - const stringMax = DClean.StringMax(10); - const stringMin = DClean.StringMin(10); - - expect(stringMax.name).toBe("string-max-10"); - expect(stringMin.name).toBe("string-min-10"); - - type check = ExpectType< - typeof stringMax, - DClean.ConstraintHandler<"string-max-10", string, readonly [DDataParser.DataParserCheckerStringMax], never>, - "strict" - >; - - type check1 = ExpectType< - typeof stringMin, - DClean.ConstraintHandler<"string-min-10", string, readonly [DDataParser.DataParserCheckerStringMin], never>, - "strict" - >; - }); - - it("number nim and max", () => { - const numberMax = DClean.NumberMax(10); - const numberMin = DClean.NumberMin(10); - - expect(numberMax.name).toBe("number-max-10"); - expect(numberMin.name).toBe("number-min-10"); - - type check = ExpectType< - typeof numberMax, - DClean.ConstraintHandler<"number-max-10", number, readonly [DDataParser.DataParserCheckerNumberMax], never>, - "strict" - >; - - type check1 = ExpectType< - typeof numberMin, - DClean.ConstraintHandler<"number-min-10", number, readonly [DDataParser.DataParserCheckerNumberMin], never>, - "strict" - >; - }); }); diff --git a/tests/clean/constraint/set.test.ts b/tests/clean/constraint/set.test.ts index fb6c9737b..876cdcc14 100644 --- a/tests/clean/constraint/set.test.ts +++ b/tests/clean/constraint/set.test.ts @@ -219,4 +219,151 @@ describe("createConstraintsSet", () => { ), ); }); + + it("accepts a constraints set as input", () => { + const minConstraint = DClean.StringMin(2); + const maxConstraint = DClean.StringMax(5); + const constraintsSet = DClean.createConstraintsSet( + DClean.String, + [ + minConstraint, + maxConstraint, + ], + ); + + const handler = DClean.createConstraintsSet( + DClean.String, + constraintsSet, + ); + + const value = handler.createOrThrow("test"); + + expect(value).toStrictEqual( + DClean.constrainedTypeKind.setTo( + wrapValue("test"), + { + "string-min-2": null, + "string-max-5": null, + }, + ), + ); + + type CheckHandler = ExpectType< + typeof handler, + DClean.ConstraintsSetHandler< + string, + readonly [ + typeof minConstraint, + typeof maxConstraint, + ], + never + >, + "strict" + >; + }); + + it("preserves mixed constraints and constraints sets order in types and runtime", () => { + const executionOrder: string[] = []; + const firstConstraint = DClean.createConstraint( + "first", + DClean.String, + DDataParser.checkerRefine(() => { + executionOrder.push("first"); + return true; + }), + ); + const secondConstraint = DClean.createConstraint( + "second", + DClean.String, + DDataParser.checkerRefine(() => { + executionOrder.push("second"); + return true; + }), + ); + const thirdConstraint = DClean.createConstraint( + "third", + DClean.String, + DDataParser.checkerRefine(() => { + executionOrder.push("third"); + return true; + }), + ); + const fourthConstraint = DClean.createConstraint( + "fourth", + DClean.String, + DDataParser.checkerRefine(() => { + executionOrder.push("fourth"); + return true; + }), + ); + const fifthConstraint = DClean.createConstraint( + "fifth", + DClean.String, + DDataParser.checkerRefine(() => { + executionOrder.push("fifth"); + return true; + }), + ); + const middleConstraintsSet = DClean.createConstraintsSet( + DClean.String, + [ + secondConstraint, + thirdConstraint, + ], + ); + const lastConstraintsSet = DClean.createConstraintsSet( + DClean.String, + fifthConstraint, + ); + + const handler = DClean.createConstraintsSet( + DClean.String, + [ + firstConstraint, + middleConstraintsSet, + fourthConstraint, + lastConstraintsSet, + ], + ); + + type CheckHandler = ExpectType< + typeof handler, + DClean.ConstraintsSetHandler< + string, + readonly [ + typeof firstConstraint, + typeof secondConstraint, + typeof thirdConstraint, + typeof fourthConstraint, + typeof fifthConstraint, + ], + never + >, + "strict" + >; + + const value = handler.createOrThrow("test"); + + expect(handler.internal.constraints.map(({ name }) => name)).toStrictEqual([ + "first", + "second", + "third", + "fourth", + "fifth", + ]); + expect(Object.keys(DClean.constrainedTypeKind.getValue(value))).toStrictEqual([ + "first", + "second", + "third", + "fourth", + "fifth", + ]); + expect(executionOrder).toStrictEqual([ + "first", + "second", + "third", + "fourth", + "fifth", + ]); + }); }); diff --git a/tests/clean/newType.test.ts b/tests/clean/newType.test.ts index 90d96d906..87b02aebe 100644 --- a/tests/clean/newType.test.ts +++ b/tests/clean/newType.test.ts @@ -215,4 +215,164 @@ describe("createNewType", () => { "strict" >; }); + + it("supports constraints set", () => { + const minConstraint = DClean.StringMin(2); + const maxConstraint = DClean.StringMax(5); + const constraintsSet = DClean.createConstraintsSet( + DClean.String, + [ + minConstraint, + maxConstraint, + ], + ); + + const handler = DClean.createNewType( + "Label", + DDataParser.string(), + constraintsSet, + ); + + type check = ExpectType< + typeof handler, + DClean.NewTypeHandler< + "Label", + string, + readonly [ + DClean.ConstraintHandler< + "string-min-2", + string, + readonly [DDataParser.DataParserCheckerStringMin], + never + >, + DClean.ConstraintHandler< + "string-max-5", + string, + readonly [DDataParser.DataParserCheckerStringMax], + never + >, + ], + never + >, + "strict" + >; + + const value = handler.createOrThrow("test"); + + expect(value).toStrictEqual( + DClean.newTypeKind.setTo( + DClean.constrainedTypeKind.setTo( + wrapValue("test"), + { + "string-min-2": null, + "string-max-5": null, + }, + ), + "Label", + ), + ); + expect(handler.is(value)).toBe(true); + expect(handler.getConstraint("string-min-2")).toBe(minConstraint); + expect(handler.getConstraint("string-max-5")).toBe(maxConstraint); + expect(() => handler.createOrThrow("t")).toThrow(DClean.CreateNewTypeError); + + type Check = ExpectType< + typeof value, + DClean.NewType< + "Label", + "test", + "string-min-2" | "string-max-5" + >, + "strict" + >; + }); + + it("preserves mixed constraints and constraints sets order at runtime", () => { + const executionOrder: string[] = []; + const firstConstraint = DClean.createConstraint( + "first", + DClean.String, + DDataParser.checkerRefine(() => { + executionOrder.push("first"); + return true; + }), + ); + const secondConstraint = DClean.createConstraint( + "second", + DClean.String, + DDataParser.checkerRefine(() => { + executionOrder.push("second"); + return true; + }), + ); + const thirdConstraint = DClean.createConstraint( + "third", + DClean.String, + DDataParser.checkerRefine(() => { + executionOrder.push("third"); + return true; + }), + ); + const fourthConstraint = DClean.createConstraint( + "fourth", + DClean.String, + DDataParser.checkerRefine(() => { + executionOrder.push("fourth"); + return true; + }), + ); + const fifthConstraint = DClean.createConstraint( + "fifth", + DClean.String, + DDataParser.checkerRefine(() => { + executionOrder.push("fifth"); + return true; + }), + ); + const middleConstraintsSet = DClean.createConstraintsSet( + DClean.String, + [ + secondConstraint, + thirdConstraint, + ], + ); + const lastConstraintsSet = DClean.createConstraintsSet( + DClean.String, + fifthConstraint, + ); + + const handler = DClean.createNewType( + "Label", + DDataParser.string(), + [ + firstConstraint, + middleConstraintsSet, + fourthConstraint, + lastConstraintsSet, + ], + ); + const value = handler.createOrThrow("test"); + + expect(handler.internal.constraints.map(({ name }) => name)).toStrictEqual([ + "first", + "second", + "third", + "fourth", + "fifth", + ]); + expect(Object.keys(DClean.constrainedTypeKind.getValue(value))).toStrictEqual([ + "first", + "second", + "third", + "fourth", + "fifth", + ]); + expect(executionOrder).toStrictEqual([ + "first", + "second", + "third", + "fourth", + "fifth", + ]); + }); }); diff --git a/tests/dataParser/parsers/number/checkers/max.test.ts b/tests/dataParser/parsers/number/checkers/max.test.ts index dd5675cca..ad0207456 100644 --- a/tests/dataParser/parsers/number/checkers/max.test.ts +++ b/tests/dataParser/parsers/number/checkers/max.test.ts @@ -11,6 +11,17 @@ describe("DDataParser number checker max", () => { expect(schema.parse(5)).toStrictEqual(DEither.success(5)); }); + it("allows numbers strictly less than max when exclusive is true", () => { + const checker = DDataParser.checkerNumberMax(10, { + exclusive: true, + }); + const schema = DDataParser.number({ + checkers: [checker], + }); + + expect(schema.parse(9)).toStrictEqual(DEither.success(9)); + }); + it("fails numbers above maximum", () => { const checker = DDataParser.checkerNumberMax(10); const schema = DDataParser.number({ @@ -36,4 +47,32 @@ describe("DDataParser number checker max", () => { ), ); }); + + it("fails number equal to max when exclusive is true", () => { + const checker = DDataParser.checkerNumberMax(10, { + exclusive: true, + }); + const schema = DDataParser.number({ + checkers: [checker], + errorMessage: "number.max.exclusive", + }); + + const result = schema.parse(10); + + expect(result).toStrictEqual( + DEither.error( + DDataParser.errorKind.addTo({ + issues: [ + DDataParser.errorIssueKind.addTo({ + expected: "number < 10", + path: "", + data: 10, + message: "number.max.exclusive", + }), + ], + currentPath: [], + }), + ), + ); + }); }); diff --git a/tests/dataParser/parsers/number/checkers/min.test.ts b/tests/dataParser/parsers/number/checkers/min.test.ts index b17a7f645..a1bf307c2 100644 --- a/tests/dataParser/parsers/number/checkers/min.test.ts +++ b/tests/dataParser/parsers/number/checkers/min.test.ts @@ -11,6 +11,17 @@ describe("DDataParser number checker min", () => { expect(schema.parse(10)).toStrictEqual(DEither.success(10)); }); + it("allows numbers strictly greater than min when exclusive is true", () => { + const checker = DDataParser.checkerNumberMin(5, { + exclusive: true, + }); + const schema = DDataParser.number({ + checkers: [checker], + }); + + expect(schema.parse(6)).toStrictEqual(DEither.success(6)); + }); + it("fails numbers below minimum", () => { const checker = DDataParser.checkerNumberMin(0); const schema = DDataParser.number({ @@ -36,4 +47,32 @@ describe("DDataParser number checker min", () => { ), ); }); + + it("fails number equal to min when exclusive is true", () => { + const checker = DDataParser.checkerNumberMin(5, { + exclusive: true, + }); + const schema = DDataParser.number({ + checkers: [checker], + errorMessage: "number.min.exclusive", + }); + + const result = schema.parse(5); + + expect(result).toStrictEqual( + DEither.error( + DDataParser.errorKind.addTo({ + issues: [ + DDataParser.errorIssueKind.addTo({ + expected: "number > 5", + path: "", + data: 5, + message: "number.min.exclusive", + }), + ], + currentPath: [], + }), + ), + ); + }); });