diff --git a/docs/en/v1/api/clean/chainedFunction.md b/docs/en/v1/api/clean/chainedFunction.md new file mode 100644 index 00000000..43d64c53 --- /dev/null +++ b/docs/en/v1/api/clean/chainedFunction.md @@ -0,0 +1,99 @@ +--- +outline: [2, 3] +description: "chainedFunction declares a typed aggregate of pure business actions that must run in order. The use case then orchestrates repositories around that aggregate." +prev: + text: "UseCase" + link: "/en/v1/api/clean/useCase" +next: + text: "Clean" + link: "/en/v1/api/clean/" +--- + +# chainedFunction + +`chainedFunction` solves a Clean Architecture coordination problem: inside a use case, you sometimes need to associate operations that update different entities. The chained function represents the aggregate that makes those operations part of the same business consistency boundary. + +It lets the domain explicitly declare that several pure business actions are linked. Functions passed to `chainedFunction` do not inject dependencies and do not call repositories: they only control entity lifecycle from the business point of view. The use case then orchestrates the aggregate, repositories, and technical effects. + +## Interactive example + + + +## Why use it? + +Use `chainedFunction` when a business aggregate is only consistent if several named operations happen together. + +For example, publishing a comment can require: + +- producing a valid comment entity; +- producing an updated article entity. + +Persistence stays in the use case through the library repository system. The chained function only models the aggregate contract: "creating the comment" and "incrementing the article comment count" are linked business actions. + +## Guarantees + +`chainedFunction` provides these guarantees at the type level: + +- links are exposed one after another, in declaration order; +- the implementation cannot access a later link before calling the current one; +- the success path must end with `chainEnd(...)`; +- a chained function can stop the flow by returning an `Either.Left`; +- the implementation can also return an `Either.Left` directly. +- chained functions remain pure domain functions. + +## Syntax + +### Signature + +```typescript +function chainedFunction( + function1: [name: string, fn: Function], + function2: [name: string, fn: Function], + ...functions: [name: string, fn: Function][] +): ChainedFunction +``` + +### Implementation + +```typescript +const aggregate = chainedFunction(...functions); + +const result = aggregate(function *(firstLink, { breakIfLeft }) { + const [value, nextLink] = yield *firstLink(({ functionName }) => functionName(...args)); + + return chainEnd(value); +}); +``` + +## Parameters + +- `function1`: first pure business function in the chain. +- `function2`: second pure business function in the chain. +- `functions`: additional pure business functions, executed in declaration order. +- `firstLink`: generated first link passed to the implementation callback. +- `breakIfLeft`: synchronous helper injected into the callback. It accepts a value that may contain an `Either.Left`, stops the chain when it is a `Left`, otherwise returns the discriminated non-`Left` value. + +## Return value + +`chainedFunction(...)` returns a chained aggregate function. Calling it returns: + +- the raw value passed to `chainEnd(value)` on the success path; +- the `Either.Left` returned by a chained function; +- the `Either.Left` returned directly by the implementation. + +## Error flow + +When a chained function returns an `Either.Left`, the generator yields it and `chainedFunction` stops the implementation before the next links run. Business errors should be represented as `Either.Left`; thrown exceptions and rejected promises are not caught. + +`breakIfLeft` follows the same rule from inside the callback: use it to explicitly short-circuit from an intermediate synchronous value (`value | Left`) before calling the next link. + +## See also + +- [`useCase`](/en/v1/api/clean/useCase) - Calls application logic with dependencies. +- [`repository`](/en/v1/api/clean/repository) - Declares a repository contract. +- [`Either`](/en/v1/api/either/) - Represents explicit success and error values. diff --git a/docs/en/v1/api/clean/index.md b/docs/en/v1/api/clean/index.md index 527c4ba4..eef405d1 100644 --- a/docs/en/v1/api/clean/index.md +++ b/docs/en/v1/api/clean/index.md @@ -55,6 +55,9 @@ Declares a repository (contract) and type-checks the implementation. ## [UseCase](/en/v1/api/clean/useCase) Declares a use case with dependencies (repositories or other use cases). +## [chainedFunction](/en/v1/api/clean/chainedFunction) +Declares an aggregate of linked business actions that must run in order. + ## Operations on primitives ### [equal](/en/v1/api/clean/primitives/operators/equal) diff --git a/docs/en/v1/api/clean/useCase.md b/docs/en/v1/api/clean/useCase.md index 3f2e7015..46ae0cbe 100644 --- a/docs/en/v1/api/clean/useCase.md +++ b/docs/en/v1/api/clean/useCase.md @@ -5,8 +5,8 @@ prev: text: "Repository" link: "/en/v1/api/clean/repository" next: - text: "Clean" - link: "/en/v1/api/clean/" + text: "chainedFunction" + link: "/en/v1/api/clean/chainedFunction" --- # UseCase diff --git a/docs/examples/v1/api/clean/chainedFunction/tryout.doc.ts b/docs/examples/v1/api/clean/chainedFunction/tryout.doc.ts new file mode 100644 index 00000000..c338ee31 --- /dev/null +++ b/docs/examples/v1/api/clean/chainedFunction/tryout.doc.ts @@ -0,0 +1,104 @@ +import { C, E, type ExpectType } from "@duplojs/utils"; + +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", + (draft: CommentDraft) => draft.content.trim() + ? { + id: 1, + articleId: draft.articleId, + content: draft.content.trim(), + } + : E.fail(), + ], + [ + "incrementArticleCommentCount", + (article: Article): Article => ({ + ...article, + commentCount: article.commentCount + 1, + }), + ], +); + +const PublishCommentUseCase = C.createUseCase( + { + CommentRepository, + ArticleRepository, + }, + ({ + commentRepository, + articleRepository, + }) => (draft: CommentDraft) => CommentPublicationAggregate(function *(link1, { breakIfLeft }) { + const [comment, link2] = yield *link1(({ createComment }) => createComment(draft)); + + const savedComment = yield *breakIfLeft(commentRepository.save(comment)); + + const article = yield *breakIfLeft(articleRepository.findById(savedComment.articleId)); + + const [updatedArticle, chainEnd] = yield *link2( + ({ incrementArticleCommentCount }) => incrementArticleCommentCount(article), + ); + + const savedArticle = yield *breakIfLeft(articleRepository.save(updatedArticle)); + + return chainEnd({ + comment: savedComment, + article: savedArticle, + }); + }), +); + +const publishComment = PublishCommentUseCase.getUseCase({ + commentRepository: CommentRepository.createImplementation({ + save: (comment) => comment, + }), + articleRepository: ArticleRepository.createImplementation({ + findById: (articleId) => ({ + id: articleId, + commentCount: 11, + }), + save: (article) => article, + }), +}); + +const publishedComment = publishComment({ + articleId: 12, + content: " New comment ", +}); + +type CheckPublishedComment = ExpectType< + typeof publishedComment, + { + comment: Comment; + article: Article; + } | E.Fail, + "strict" +>; diff --git a/docs/fr/v1/api/clean/chainedFunction.md b/docs/fr/v1/api/clean/chainedFunction.md new file mode 100644 index 00000000..0eec0156 --- /dev/null +++ b/docs/fr/v1/api/clean/chainedFunction.md @@ -0,0 +1,100 @@ +--- +outline: [2, 3] +description: "chainedFunction déclare un agrégat typé d'actions métier pures qui doivent s'exécuter dans l'ordre. Le use case orchestre ensuite les repositories autour de cet agrégat." +prev: + text: "UseCase" + link: "/fr/v1/api/clean/useCase" +next: + text: "Clean" + link: "/fr/v1/api/clean/" +--- + +# chainedFunction + +`chainedFunction` répond à un problème de coordination en Clean Architecture : dans un use case, on doit parfois associer des opérations qui mettent à jour des entités différentes. La chained function représente alors l'agrégat qui rend ces opérations solidaires dans une même frontière de cohérence métier. + +Elle permet au domaine de déclarer explicitement que plusieurs actions métier pures sont liées. Les fonctions passées à `chainedFunction` ne font pas d'injection de dépendance et n'appellent pas de repository : elles contrôlent uniquement le cycle de vie des entités. Le use case orchestre ensuite l'agrégat, les repositories et les effets techniques. + +## Exemple interactif + + + +## Pourquoi l'utiliser ? + +Utilisez `chainedFunction` quand un agrégat métier n'est cohérent que si plusieurs opérations nommées se produisent ensemble. + +Par exemple, publier un commentaire peut demander : + +- de créer l'entité commentaire ; +- de produire une entité commentaire valide ; +- de produire une entité article mise à jour. + +La persistance reste dans le use case via le système de repository de la librairie. La chained function modélise seulement le contrat d'agrégat : "créer le commentaire" et "incrémenter le nombre de commentaires de l'article" sont des actions métier liées. + +## Garanties + +`chainedFunction` apporte ces garanties au niveau du typage : + +- les liens sont exposés un par un, dans l'ordre de déclaration ; +- l'implémentation ne peut pas accéder à un lien suivant avant d'avoir appelé le lien courant ; +- le chemin de succès doit terminer avec `chainEnd(...)` ; +- une fonction chaînée peut arrêter le flux en retournant un `Either.Left` ; +- l'implémentation peut aussi retourner directement un `Either.Left`. +- les fonctions chaînées restent des fonctions de domaine pures. + +## Syntaxe + +### Signature + +```typescript +function chainedFunction( + function1: [name: string, fn: Function], + function2: [name: string, fn: Function], + ...functions: [name: string, fn: Function][] +): ChainedFunction +``` + +### Implémentation + +```typescript +const aggregate = chainedFunction(...functions); + +const result = aggregate(function *(firstLink, { breakIfLeft }) { + const [value, nextLink] = yield *firstLink(({ functionName }) => functionName(...args)); + + return chainEnd(value); +}); +``` + +## Paramètres + +- `function1` : première fonction métier pure de la chaîne. +- `function2` : deuxième fonction métier pure de la chaîne. +- `functions` : fonctions métier pures supplémentaires, exécutées dans l'ordre de déclaration. +- `firstLink` : premier link généré et passé au callback d'implémentation. +- `breakIfLeft` : helper synchrone injecté dans le callback. Il accepte une valeur potentiellement `Either.Left`, stoppe la chaîne si c'est un `Left`, sinon retourne la valeur discriminée sans le `Left`. + +## Valeur de retour + +`chainedFunction(...)` retourne une fonction d'agrégat chaîné. Son appel retourne : + +- la valeur brute passée à `chainEnd(value)` sur le chemin de succès ; +- le `Either.Left` retourné par une fonction chaînée ; +- le `Either.Left` retourné directement par l'implémentation. + +## Flux d'erreur + +Quand une fonction chaînée retourne un `Either.Left`, le générateur le yield et `chainedFunction` arrête l'implémentation avant d'exécuter les links suivants. Les erreurs métier doivent être représentées avec `Either.Left` ; les exceptions lancées et les promesses rejetées ne sont pas interceptées. + +`breakIfLeft` suit la même règle, mais côté callback : il sert à court-circuiter explicitement le flux à partir d'une valeur intermédiaire synchrone (`value | Left`) avant d'appeler le link suivant. + +## Voir aussi + +- [`useCase`](/fr/v1/api/clean/useCase) - Appelle une logique applicative avec dépendances. +- [`repository`](/fr/v1/api/clean/repository) - Déclare un contrat de repository. +- [`Either`](/fr/v1/api/either/) - Représente des valeurs explicites de succès et d'erreur. diff --git a/docs/fr/v1/api/clean/index.md b/docs/fr/v1/api/clean/index.md index 393e21ee..57533aa3 100644 --- a/docs/fr/v1/api/clean/index.md +++ b/docs/fr/v1/api/clean/index.md @@ -55,6 +55,9 @@ Déclare un repository (contrat) et type-check l'implémentation. ## [UseCase](/fr/v1/api/clean/useCase) Déclare un use case avec des dépendances (repositories ou autres use cases). +## [chainedFunction](/fr/v1/api/clean/chainedFunction) +Déclare un agrégat d'actions métier liées qui doivent s'exécuter dans l'ordre. + ## Opérations sur primitives ### [equal](/fr/v1/api/clean/primitives/operators/equal) diff --git a/docs/fr/v1/api/clean/useCase.md b/docs/fr/v1/api/clean/useCase.md index 50e88bfa..392a29b7 100644 --- a/docs/fr/v1/api/clean/useCase.md +++ b/docs/fr/v1/api/clean/useCase.md @@ -5,8 +5,8 @@ prev: text: "Repository" link: "/fr/v1/api/clean/repository" next: - text: "Clean" - link: "/fr/v1/api/clean/" + text: "chainedFunction" + link: "/fr/v1/api/clean/chainedFunction" --- # UseCase diff --git a/docs/public/libs/v1/clean/chainedFunction.cjs b/docs/public/libs/v1/clean/chainedFunction.cjs new file mode 100644 index 00000000..85523ac9 --- /dev/null +++ b/docs/public/libs/v1/clean/chainedFunction.cjs @@ -0,0 +1,70 @@ +'use strict'; + +var kind = require('./kind.cjs'); +var is = require('../either/left/is.cjs'); + +const chainEndKind = kind.createCleanKind("chain-end"); +function* breakIfLeft(value) { + if (is.isLeft(value)) { + yield value; + } + return value; +} +const chainedFunctionParams = { breakIfLeft }; +/** + * {@include clean/chainedFunction/index.md} + */ +function chainedFunction(function1, function2, ...functions) { + return (theFunction) => { + const functionChain = [function1, function2, ...functions]; + const createLink = (functionChain) => (theFunction) => { + const [functionName, chainedFunction] = functionChain.shift(); + const result = theFunction({ [functionName]: chainedFunction }); + const nextLink = functionChain.length === 0 + ? (value) => chainEndKind.setTo({}, value) + : createLink(functionChain); + if (result instanceof Promise) { + return (async function* () { + const awaitedResult = await result; + if (is.isLeft(awaitedResult)) { + yield awaitedResult; + } + return [awaitedResult, nextLink]; + })(); + } + return (function* () { + if (is.isLeft(result)) { + yield result; + } + return [result, nextLink]; + })(); + }; + const generator = theFunction(createLink(functionChain), chainedFunctionParams); + let result = undefined; + if (Symbol.asyncIterator in generator) { + return (async () => { + try { + result = await generator.next(); + } + finally { + await generator.return(undefined); + } + return (chainEndKind.has(result.value) + ? chainEndKind.getValue(result.value) + : result.value); + })(); + } + try { + result = generator.next(); + } + finally { + generator.return(undefined); + } + return (chainEndKind.has(result.value) + ? chainEndKind.getValue(result.value) + : result.value); + }; +} + +exports.chainEndKind = chainEndKind; +exports.chainedFunction = chainedFunction; diff --git a/docs/public/libs/v1/clean/chainedFunction.d.ts b/docs/public/libs/v1/clean/chainedFunction.d.ts new file mode 100644 index 00000000..691a5dbb --- /dev/null +++ b/docs/public/libs/v1/clean/chainedFunction.d.ts @@ -0,0 +1,216 @@ +import { type AnyFunction, type Kind, type IsEqual, type MaybePromise, type MaybeAsyncGenerator, type GetKindValue } from "../common"; +import * as EE from "../either"; +export type FunctionOfChain = [string, AnyFunction]; +export type FunctionChain = [ + FunctionOfChain, + FunctionOfChain, + ...FunctionOfChain[] +]; +export declare const chainEndKind: import("../common").KindHandler>; +export interface ChainEnd extends Kind { +} +export interface CreateChainEnd { + (): ChainEnd; + (value: GenericValue): ChainEnd; +} +export type Link = >(theFunction: (theFunction: { + [Prop in GenericFunction[0]]: GenericFunction[1]; +}) => GenericOutput) => ((Extract> extends infer InferredPromise ? IsEqual extends true ? never : Awaited extends infer InferredValue extends unknown ? AsyncGenerator, [ + Exclude, + GenericNext +]> : never : never) | (Exclude> extends infer InferredValue ? IsEqual extends true ? never : Generator, [ + Exclude, + GenericNext +]> : never)); +export type Chain = GenericFunctionChain extends readonly [] ? CreateChainEnd : GenericFunctionChain extends [ + infer InferredFirst extends FunctionOfChain, + ...infer InferredRest extends readonly FunctionOfChain[] +] ? Chain extends infer InferredRestResult extends (Link | CreateChainEnd) ? Link : never : never; +declare const SymbolError: unique symbol; +type OutputMustContainChainEnd = IsEqual ? InferredReturnValue extends ChainEnd ? InferredReturnValue : never : never, never> extends true ? { + [SymbolError]: "Output must contain a chainEnd"; +} : unknown; +type ComputeResult = GenericGenerator extends Generator ? (InferredIterateValue | InferredReturnValue) extends infer InferredResult ? InferredResult extends ChainEnd ? GetKindValue : InferredResult : never : GenericGenerator extends AsyncGenerator ? Promise extends infer InferredResult ? InferredResult extends ChainEnd ? GetKindValue : InferredResult : never> : never; +declare function breakIfLeft(value: GenericValue): Generator, Exclude>; +export interface ChainedFunctionParams { + breakIfLeft: typeof breakIfLeft; +} +export type ChainedFunction = , MaybePromise>>(callback: (firstLink: Chain, params: ChainedFunctionParams) => (GenericGenerator & OutputMustContainChainEnd)) => ComputeResult; +/** + * Declares a typed aggregate of pure linked business actions that must run in order. + * + * **Supported call styles:** + * - Classic: `chainedFunction(firstFunction, secondFunction, ...functions)` -> returns an implementation function driven by generator links + * + * 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", + * (draft: CommentDraft): Comment | E.Fail => draft.content.trim() + * ? { + * id: 1, + * articleId: draft.articleId, + * content: draft.content.trim(), + * } + * : E.fail(), + * ], + * [ + * "incrementArticleCommentCount", + * (article: Article): Article => ({ + * ...article, + * commentCount: article.commentCount + 1, + * }), + * ], + * ); + * + * const PublishCommentUseCase = C.createUseCase( + * { + * CommentRepository, + * ArticleRepository, + * }, + * ({ + * commentRepository, + * articleRepository, + * }) => (draft: CommentDraft) => CommentPublicationAggregate(function *(link1) { + * const [comment, link2] = yield *link1(({ createComment }) => createComment(draft)); + * + * const savedComment = commentRepository.save(comment); + * if (E.isLeft(savedComment)) { + * return savedComment; + * } + * + * const article = articleRepository.findById(savedComment.articleId); + * if (E.isLeft(article)) { + * return article; + * } + * + * const [updatedArticle, chainEnd] = yield *link2( + * ({ incrementArticleCommentCount }) => incrementArticleCommentCount(article), + * ); + * + * const savedArticle = articleRepository.save(updatedArticle); + * if (E.isLeft(savedArticle)) { + * return savedArticle; + * } + * + * return chainEnd({ + * comment: savedComment, + * article: savedArticle, + * }); + * }), + * ); + * + * const publishComment = PublishCommentUseCase.getUseCase({ + * commentRepository: CommentRepository.createImplementation({ + * save: (comment) => comment, + * }), + * articleRepository: ArticleRepository.createImplementation({ + * findById: (articleId) => ({ + * id: articleId, + * commentCount: 11, + * }), + * save: (article) => article, + * }), + * }); + * + * const publishedComment = publishComment({ + * articleId: 12, + * content: " New comment ", + * }); + * + * type CheckPublishedComment = ExpectType< + * typeof publishedComment, + * { + * comment: Comment; + * article: Article; + * } | E.Fail, + * "strict" + * >; + * + * const emptyContentResult = publishComment({ + * articleId: 12, + * content: " ", + * }); + * + * type CheckEmptyContentResult = ExpectType< + * typeof emptyContentResult, + * { + * comment: Comment; + * article: Article; + * } | E.Fail, + * "strict" + * >; + * + * const failingPublishComment = PublishCommentUseCase.getUseCase({ + * commentRepository: CommentRepository.createImplementation({ + * save: () => E.fail(), + * }), + * articleRepository: ArticleRepository.createImplementation({ + * findById: (articleId) => ({ + * id: articleId, + * commentCount: 11, + * }), + * save: (article) => article, + * }), + * }); + * + * const repositoryFailureResult = failingPublishComment({ + * articleId: 12, + * content: "New comment", + * }); + * + * type CheckRepositoryFailureResult = ExpectType< + * typeof repositoryFailureResult, + * { + * comment: Comment; + * article: Article; + * } | E.Fail, + * "strict" + * >; + * ``` + * + * @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`. + * + * @see https://utils.duplojs.dev/en/v1/api/clean/chainedFunction + * @see [`C.createUseCase`](https://utils.duplojs.dev/en/v1/api/clean/useCase) + * @see [`E.Left`](https://utils.duplojs.dev/en/v1/api/either/left) + * + * @namespace C + * + */ +export declare function chainedFunction(function1: GenericFunction1, function2: GenericFunction2, ...functions: GenericFunctions): ChainedFunction<[ + GenericFunction1, + GenericFunction2, + ...GenericFunctions +]>; +export {}; diff --git a/docs/public/libs/v1/clean/chainedFunction.mjs b/docs/public/libs/v1/clean/chainedFunction.mjs new file mode 100644 index 00000000..f4e0e105 --- /dev/null +++ b/docs/public/libs/v1/clean/chainedFunction.mjs @@ -0,0 +1,67 @@ +import { createCleanKind } from './kind.mjs'; +import { isLeft } from '../either/left/is.mjs'; + +const chainEndKind = createCleanKind("chain-end"); +function* breakIfLeft(value) { + if (isLeft(value)) { + yield value; + } + return value; +} +const chainedFunctionParams = { breakIfLeft }; +/** + * {@include clean/chainedFunction/index.md} + */ +function chainedFunction(function1, function2, ...functions) { + return (theFunction) => { + const functionChain = [function1, function2, ...functions]; + const createLink = (functionChain) => (theFunction) => { + const [functionName, chainedFunction] = functionChain.shift(); + const result = theFunction({ [functionName]: chainedFunction }); + const nextLink = functionChain.length === 0 + ? (value) => chainEndKind.setTo({}, value) + : createLink(functionChain); + if (result instanceof Promise) { + return (async function* () { + const awaitedResult = await result; + if (isLeft(awaitedResult)) { + yield awaitedResult; + } + return [awaitedResult, nextLink]; + })(); + } + return (function* () { + if (isLeft(result)) { + yield result; + } + return [result, nextLink]; + })(); + }; + const generator = theFunction(createLink(functionChain), chainedFunctionParams); + let result = undefined; + if (Symbol.asyncIterator in generator) { + return (async () => { + try { + result = await generator.next(); + } + finally { + await generator.return(undefined); + } + return (chainEndKind.has(result.value) + ? chainEndKind.getValue(result.value) + : result.value); + })(); + } + try { + result = generator.next(); + } + finally { + generator.return(undefined); + } + return (chainEndKind.has(result.value) + ? chainEndKind.getValue(result.value) + : result.value); + }; +} + +export { chainEndKind, chainedFunction }; diff --git a/docs/public/libs/v1/clean/flag.d.ts b/docs/public/libs/v1/clean/flag.d.ts index 92a80486..f54b7166 100644 --- a/docs/public/libs/v1/clean/flag.d.ts +++ b/docs/public/libs/v1/clean/flag.d.ts @@ -15,7 +15,7 @@ export interface FlagHandler("majorUser"); * export type MajorFlag = C.GetFlag; * * export function isMajor(entity: Entity) { * if (C.greaterThan(entity.age, 18)) { * return E.success( - * MajorFlag.append(entity, entity.age), + * MajorFlag.append(entity, { age: entity.age }), * ); * } * return E.left("not-major"); @@ -94,7 +94,7 @@ export interface Flag | E.Right<"not-thirsty-anymore", undefined> * - * const flagged = User.MajorFlag.append(user, user.age); + * const flagged = User.MajorFlag.append(user, { age: user.age }); * const value = User.MajorFlag.getValue(flagged); * * ``` diff --git a/docs/public/libs/v1/clean/index.cjs b/docs/public/libs/v1/clean/index.cjs index f31c2a1b..45d9967d 100644 --- a/docs/public/libs/v1/clean/index.cjs +++ b/docs/public/libs/v1/clean/index.cjs @@ -8,6 +8,7 @@ var useCase = require('./useCase.cjs'); var flag = require('./flag.cjs'); var maybe = require('./maybe.cjs'); var toMapDataParser = require('./toMapDataParser.cjs'); +var chainedFunction = require('./chainedFunction.cjs'); var property = require('./entity/property.cjs'); var unwrap = require('./entity/unwrap.cjs'); var base = require('./constraint/base.cjs'); @@ -66,6 +67,8 @@ exports.flagKind = flag.flagKind; exports.none = maybe.none; exports.some = maybe.some; exports.toMapDataParser = toMapDataParser.toMapDataParser; +exports.chainEndKind = chainedFunction.chainEndKind; +exports.chainedFunction = chainedFunction.chainedFunction; exports.entityPropertyArrayKind = property.entityPropertyArrayKind; exports.entityPropertyDefinitionToDataParser = property.entityPropertyDefinitionToDataParser; exports.entityPropertyDefinitionTools = property.entityPropertyDefinitionTools; diff --git a/docs/public/libs/v1/clean/index.d.ts b/docs/public/libs/v1/clean/index.d.ts index 51749bb7..7f1b65d3 100644 --- a/docs/public/libs/v1/clean/index.d.ts +++ b/docs/public/libs/v1/clean/index.d.ts @@ -34,3 +34,4 @@ export * from "./useCase"; export * from "./flag"; export * from "./maybe"; export * from "./toMapDataParser"; +export * from "./chainedFunction"; diff --git a/docs/public/libs/v1/clean/index.mjs b/docs/public/libs/v1/clean/index.mjs index 802a0110..58d0f7da 100644 --- a/docs/public/libs/v1/clean/index.mjs +++ b/docs/public/libs/v1/clean/index.mjs @@ -6,6 +6,7 @@ export { createUseCase, useCaseHandlerKind, useCaseInstances } from './useCase.m export { createFlag, flagKind } from './flag.mjs'; export { none, some } from './maybe.mjs'; export { toMapDataParser } from './toMapDataParser.mjs'; +export { chainEndKind, chainedFunction } from './chainedFunction.mjs'; export { entityPropertyArrayKind, entityPropertyDefinitionToDataParser, entityPropertyDefinitionTools, entityPropertyIdentifierKind, entityPropertyNullableKind, entityPropertyStructureKind, entityPropertyUnionKind } from './entity/property.mjs'; export { unwrapEntity, unwrapEntityProperty } from './entity/unwrap.mjs'; export { CreateConstrainedTypeError, constrainedTypeKind, constraintHandlerKind, createConstraint } from './constraint/base.mjs'; diff --git a/docs/public/libs/v1/clean/toMapDataParser.cjs b/docs/public/libs/v1/clean/toMapDataParser.cjs index 20da7ffa..e2b14c50 100644 --- a/docs/public/libs/v1/clean/toMapDataParser.cjs +++ b/docs/public/libs/v1/clean/toMapDataParser.cjs @@ -1,10 +1,11 @@ 'use strict'; var newType = require('./newType.cjs'); +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 hasSomeKinds = require('../common/hasSomeKinds.cjs'); var index = require('../pattern/match/index.cjs'); var transform = require('../dataParser/parsers/transform.cjs'); var index$1 = require('../dataParser/parsers/string/index.cjs'); @@ -18,6 +19,15 @@ var nil = require('../dataParser/parsers/nil.cjs'); var wrapValue = require('../common/wrapValue.cjs'); function toMapDataParser(input, params) { + if (hasSomeKinds.hasSomeKinds(input, [ + property.entityPropertyNullableKind, + property.entityPropertyArrayKind, + property.entityPropertyStructureKind, + property.entityPropertyIdentifierKind, + property.entityPropertyUnionKind, + ])) { + return property.entityPropertyDefinitionToDataParser(input, (newTypeHandler) => toMapDataParser(newTypeHandler, params)); + } const dataParser = (base.primitiveHandlerKind.has(input) ? input.dataParser.clone() : input.internal.dataParser.clone()); diff --git a/docs/public/libs/v1/clean/toMapDataParser.d.ts b/docs/public/libs/v1/clean/toMapDataParser.d.ts index 8ef67f6a..51d9afa7 100644 --- a/docs/public/libs/v1/clean/toMapDataParser.d.ts +++ b/docs/public/libs/v1/clean/toMapDataParser.d.ts @@ -1,9 +1,9 @@ import * as DDataParser from "../dataParser"; import { type ConstraintHandler, type ConstraintsSetHandler, type GetConstraint, type GetConstraints } from "./constraint"; -import { type GetNewType, type NewTypeHandler } from "./newType"; import { type PrimitiveHandler } from "./primitive"; -type ToMapDataParserInput = NewTypeHandler | ConstraintHandler | ConstraintsSetHandler | PrimitiveHandler; -type OutputDataParser = GenericInput extends NewTypeHandler ? GetNewType : GenericInput extends ConstraintHandler ? GetConstraint : GenericInput extends ConstraintsSetHandler ? GetConstraints : GenericInput extends PrimitiveHandler ? ReturnType : never; +import { type EntityPropertyDefinition, type EntityProperty } from "./entity"; +type ToMapDataParserInput = (ConstraintHandler | ConstraintsSetHandler | PrimitiveHandler | EntityPropertyDefinition); +type OutputDataParser = GenericInput extends ConstraintHandler ? GetConstraint : GenericInput extends ConstraintsSetHandler ? GetConstraints : GenericInput extends PrimitiveHandler ? ReturnType : GenericInput extends EntityPropertyDefinition ? EntityProperty : never; interface ToMapDataParserParams { coerce?: boolean; } @@ -41,7 +41,7 @@ interface ToMapDataParserParams { * ``` * * @remarks - * - Supported inputs: `NewTypeHandler`, `ConstraintHandler`, `ConstraintsSetHandler`, and `PrimitiveHandler`. + * - Supported inputs: `NewTypeHandler`, `ConstraintHandler`, `ConstraintsSetHandler`, `PrimitiveHandler`, and `EntityProperty`. * - Use `coerce: true` to allow conversions (e.g. number to string) on compatible parsers. * * @see https://utils.duplojs.dev/en/v1/api/clean/toMapDataParser @@ -49,5 +49,5 @@ interface ToMapDataParserParams { * @namespace C * */ -export declare function toMapDataParser(input: GenericInput, params?: ToMapDataParserParams): DDataParser.Contract, GenericInputDataParser>; +export declare function toMapDataParser>(input: GenericInput, params?: ToMapDataParserParams): DDataParser.Contract, unknown>; export {}; diff --git a/docs/public/libs/v1/clean/toMapDataParser.mjs b/docs/public/libs/v1/clean/toMapDataParser.mjs index 9e841245..03ba64df 100644 --- a/docs/public/libs/v1/clean/toMapDataParser.mjs +++ b/docs/public/libs/v1/clean/toMapDataParser.mjs @@ -1,8 +1,9 @@ import { newTypeHandlerKind, newTypeKind } from './newType.mjs'; +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 { hasSomeKinds } from '../common/hasSomeKinds.mjs'; import { match } from '../pattern/match/index.mjs'; import { transform } from '../dataParser/parsers/transform.mjs'; import { stringKind } from '../dataParser/parsers/string/index.mjs'; @@ -16,6 +17,15 @@ import { nilKind } from '../dataParser/parsers/nil.mjs'; import { keyWrappedValue } from '../common/wrapValue.mjs'; function toMapDataParser(input, params) { + if (hasSomeKinds(input, [ + entityPropertyNullableKind, + entityPropertyArrayKind, + entityPropertyStructureKind, + entityPropertyIdentifierKind, + entityPropertyUnionKind, + ])) { + return entityPropertyDefinitionToDataParser(input, (newTypeHandler) => toMapDataParser(newTypeHandler, params)); + } const dataParser = (primitiveHandlerKind.has(input) ? input.dataParser.clone() : input.internal.dataParser.clone()); diff --git a/docs/public/libs/v1/common/types/DeepReadonly.d.ts b/docs/public/libs/v1/common/types/deepReadonly.d.ts similarity index 100% rename from docs/public/libs/v1/common/types/DeepReadonly.d.ts rename to docs/public/libs/v1/common/types/deepReadonly.d.ts diff --git a/docs/public/libs/v1/common/types/index.d.ts b/docs/public/libs/v1/common/types/index.d.ts index d15eea86..1f6071dc 100644 --- a/docs/public/libs/v1/common/types/index.d.ts +++ b/docs/public/libs/v1/common/types/index.d.ts @@ -43,7 +43,8 @@ export * from "./onlyLiteral"; export * from "./sortType"; export * from "./maybeGetter"; export * from "./falsyValue"; -export * from "./DeepReadonly"; +export * from "./deepReadonly"; export * from "./json"; export * from "./predicate"; export * from "./bivariantFunction"; +export * from "./maybeAsyncGenerator"; diff --git a/docs/public/libs/v1/common/types/maybeAsyncGenerator.d.ts b/docs/public/libs/v1/common/types/maybeAsyncGenerator.d.ts new file mode 100644 index 00000000..258f3d16 --- /dev/null +++ b/docs/public/libs/v1/common/types/maybeAsyncGenerator.d.ts @@ -0,0 +1 @@ +export type MaybeAsyncGenerator = (Generator | AsyncGenerator); diff --git a/docs/public/libs/v1/dataParser/base.cjs b/docs/public/libs/v1/dataParser/base.cjs index fa2de3cc..089fe286 100644 --- a/docs/public/libs/v1/dataParser/base.cjs +++ b/docs/public/libs/v1/dataParser/base.cjs @@ -45,7 +45,7 @@ function dataParserInit(kind, definition, exec, specificOverrideHandler) { if (result !== SDPE && self.definition.checkers.length) { for (const checker of self.definition.checkers) { - const checkerResult = checker.exec(result, error, checker); + const checkerResult = checker.exec(result, error, checker, self); if (checkerResult === SDPE) { return SDPE; } @@ -61,7 +61,7 @@ function dataParserInit(kind, definition, exec, specificOverrideHandler) { if (result !== SDPE && self.definition.checkers.length) { for (const checker of self.definition.checkers) { - const checkerResult = checker.exec(result, error, checker); + const checkerResult = checker.exec(result, error, checker, self); if (checkerResult === SDPE) { return SDPE; } diff --git a/docs/public/libs/v1/dataParser/base.d.ts b/docs/public/libs/v1/dataParser/base.d.ts index 29133be2..140b6c5e 100644 --- a/docs/public/libs/v1/dataParser/base.d.ts +++ b/docs/public/libs/v1/dataParser/base.d.ts @@ -8,7 +8,7 @@ export interface DataParserCheckerDefinition { } export interface DataParserChecker extends Kind { readonly definition: GenericDefinition; - exec(data: GenericInput, error: DataParserError, self: this): GenericOutput | SymbolDataParserError; + exec(data: GenericInput, error: DataParserError, self: this, dataParser: DataParser): GenericOutput | SymbolDataParserError; } export type InputChecker = Parameters[0]; export declare function dataParserCheckerInit(kind: Exclude, typeof checkerKind>, params: NoInfer, "exec">>, exec: (...args: Parameters) => InputChecker | SymbolDataParserError): GenericDataParserChecker; diff --git a/docs/public/libs/v1/dataParser/base.mjs b/docs/public/libs/v1/dataParser/base.mjs index 8f56f5aa..3f862bbd 100644 --- a/docs/public/libs/v1/dataParser/base.mjs +++ b/docs/public/libs/v1/dataParser/base.mjs @@ -43,7 +43,7 @@ function dataParserInit(kind, definition, exec, specificOverrideHandler) { if (result !== SDPE && self.definition.checkers.length) { for (const checker of self.definition.checkers) { - const checkerResult = checker.exec(result, error, checker); + const checkerResult = checker.exec(result, error, checker, self); if (checkerResult === SDPE) { return SDPE; } @@ -59,7 +59,7 @@ function dataParserInit(kind, definition, exec, specificOverrideHandler) { if (result !== SDPE && self.definition.checkers.length) { for (const checker of self.definition.checkers) { - const checkerResult = checker.exec(result, error, checker); + const checkerResult = checker.exec(result, error, checker, self); if (checkerResult === SDPE) { return SDPE; } diff --git a/docs/public/libs/v1/dataParser/parsers/array/checkers/max.cjs b/docs/public/libs/v1/dataParser/parsers/array/checkers/max.cjs index a3b6f807..8b27aba4 100644 --- a/docs/public/libs/v1/dataParser/parsers/array/checkers/max.cjs +++ b/docs/public/libs/v1/dataParser/parsers/array/checkers/max.cjs @@ -11,9 +11,9 @@ function checkerArrayMax(max, definition = {}) { ...definition, max, }, - }, (data, error$1, self) => data.length <= self.definition.max + }, (data, error$1, self, dataParser) => data.length <= self.definition.max ? data - : error.addIssue(error$1, `array.length <= ${self.definition.max}`, data, self.definition.errorMessage)); + : error.addIssue(error$1, `array.length <= ${self.definition.max}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } exports.checkerArrayMax = checkerArrayMax; diff --git a/docs/public/libs/v1/dataParser/parsers/array/checkers/max.mjs b/docs/public/libs/v1/dataParser/parsers/array/checkers/max.mjs index 3957498a..e5535ca8 100644 --- a/docs/public/libs/v1/dataParser/parsers/array/checkers/max.mjs +++ b/docs/public/libs/v1/dataParser/parsers/array/checkers/max.mjs @@ -9,9 +9,9 @@ function checkerArrayMax(max, definition = {}) { ...definition, max, }, - }, (data, error, self) => data.length <= self.definition.max + }, (data, error, self, dataParser) => data.length <= self.definition.max ? data - : addIssue(error, `array.length <= ${self.definition.max}`, data, self.definition.errorMessage)); + : addIssue(error, `array.length <= ${self.definition.max}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } export { checkerArrayMax, checkerArrayMaxKind }; diff --git a/docs/public/libs/v1/dataParser/parsers/array/checkers/min.cjs b/docs/public/libs/v1/dataParser/parsers/array/checkers/min.cjs index be0b35bd..6c3e4d2f 100644 --- a/docs/public/libs/v1/dataParser/parsers/array/checkers/min.cjs +++ b/docs/public/libs/v1/dataParser/parsers/array/checkers/min.cjs @@ -11,9 +11,9 @@ function checkerArrayMin(min, definition = {}) { ...definition, min, }, - }, (data, error$1, self) => data.length >= self.definition.min + }, (data, error$1, self, dataParser) => data.length >= self.definition.min ? data - : error.addIssue(error$1, `array.length >= ${self.definition.min}`, data, self.definition.errorMessage)); + : error.addIssue(error$1, `array.length >= ${self.definition.min}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } exports.checkerArrayMin = checkerArrayMin; diff --git a/docs/public/libs/v1/dataParser/parsers/array/checkers/min.mjs b/docs/public/libs/v1/dataParser/parsers/array/checkers/min.mjs index fa75795e..ee7520f2 100644 --- a/docs/public/libs/v1/dataParser/parsers/array/checkers/min.mjs +++ b/docs/public/libs/v1/dataParser/parsers/array/checkers/min.mjs @@ -9,9 +9,9 @@ function checkerArrayMin(min, definition = {}) { ...definition, min, }, - }, (data, error, self) => data.length >= self.definition.min + }, (data, error, self, dataParser) => data.length >= self.definition.min ? data - : addIssue(error, `array.length >= ${self.definition.min}`, data, self.definition.errorMessage)); + : addIssue(error, `array.length >= ${self.definition.min}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } export { checkerArrayMin, checkerArrayMinKind }; diff --git a/docs/public/libs/v1/dataParser/parsers/bigint/checkers/max.cjs b/docs/public/libs/v1/dataParser/parsers/bigint/checkers/max.cjs index 24d06fc6..3a0c8e43 100644 --- a/docs/public/libs/v1/dataParser/parsers/bigint/checkers/max.cjs +++ b/docs/public/libs/v1/dataParser/parsers/bigint/checkers/max.cjs @@ -11,9 +11,9 @@ function checkerBigIntMax(max, definition = {}) { ...definition, max, }, - }, (value, error$1, self) => { + }, (value, error$1, self, dataParser) => { if (value > self.definition.max) { - return error.addIssue(error$1, `bigint <= ${self.definition.max}n`, value, self.definition.errorMessage); + return error.addIssue(error$1, `bigint <= ${self.definition.max}n`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage); } return value; }); diff --git a/docs/public/libs/v1/dataParser/parsers/bigint/checkers/max.mjs b/docs/public/libs/v1/dataParser/parsers/bigint/checkers/max.mjs index 5cadd5a4..525935e2 100644 --- a/docs/public/libs/v1/dataParser/parsers/bigint/checkers/max.mjs +++ b/docs/public/libs/v1/dataParser/parsers/bigint/checkers/max.mjs @@ -9,9 +9,9 @@ function checkerBigIntMax(max, definition = {}) { ...definition, max, }, - }, (value, error, self) => { + }, (value, error, self, dataParser) => { if (value > self.definition.max) { - return addIssue(error, `bigint <= ${self.definition.max}n`, value, self.definition.errorMessage); + return addIssue(error, `bigint <= ${self.definition.max}n`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage); } return value; }); diff --git a/docs/public/libs/v1/dataParser/parsers/bigint/checkers/min.cjs b/docs/public/libs/v1/dataParser/parsers/bigint/checkers/min.cjs index f27213b5..f2dddd22 100644 --- a/docs/public/libs/v1/dataParser/parsers/bigint/checkers/min.cjs +++ b/docs/public/libs/v1/dataParser/parsers/bigint/checkers/min.cjs @@ -11,9 +11,9 @@ function checkerBigIntMin(min, definition = {}) { ...definition, min, }, - }, (value, error$1, self) => { + }, (value, error$1, self, dataParser) => { if (value < self.definition.min) { - return error.addIssue(error$1, `bigint >= ${self.definition.min}n`, value, self.definition.errorMessage); + return error.addIssue(error$1, `bigint >= ${self.definition.min}n`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage); } return value; }); diff --git a/docs/public/libs/v1/dataParser/parsers/bigint/checkers/min.mjs b/docs/public/libs/v1/dataParser/parsers/bigint/checkers/min.mjs index 51b816d8..3a3a7a44 100644 --- a/docs/public/libs/v1/dataParser/parsers/bigint/checkers/min.mjs +++ b/docs/public/libs/v1/dataParser/parsers/bigint/checkers/min.mjs @@ -9,9 +9,9 @@ function checkerBigIntMin(min, definition = {}) { ...definition, min, }, - }, (value, error, self) => { + }, (value, error, self, dataParser) => { if (value < self.definition.min) { - return addIssue(error, `bigint >= ${self.definition.min}n`, value, self.definition.errorMessage); + return addIssue(error, `bigint >= ${self.definition.min}n`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage); } return value; }); diff --git a/docs/public/libs/v1/dataParser/parsers/number/checkers/int.cjs b/docs/public/libs/v1/dataParser/parsers/number/checkers/int.cjs index 5fa131ca..c832b55c 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/int.cjs +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/int.cjs @@ -9,11 +9,11 @@ const checkerIntKind = kind.createDataParserKind("checker-number-int"); function checkerInt(definition = {}) { return base.dataParserCheckerInit(checkerIntKind, { definition, - }, (data, error$1, self) => { + }, (data, error$1, self, dataParser) => { if (Number.isInteger(data)) { return data; } - return error.addIssue(error$1, "integer", data, self.definition.errorMessage); + return error.addIssue(error$1, "integer", data, self.definition.errorMessage ?? dataParser.definition.errorMessage); }); } function int(definition) { diff --git a/docs/public/libs/v1/dataParser/parsers/number/checkers/int.mjs b/docs/public/libs/v1/dataParser/parsers/number/checkers/int.mjs index 2c7abaef..08a776a5 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/int.mjs +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/int.mjs @@ -7,11 +7,11 @@ const checkerIntKind = createDataParserKind("checker-number-int"); function checkerInt(definition = {}) { return dataParserCheckerInit(checkerIntKind, { definition, - }, (data, error, self) => { + }, (data, error, self, dataParser) => { if (Number.isInteger(data)) { return data; } - return addIssue(error, "integer", data, self.definition.errorMessage); + return addIssue(error, "integer", data, self.definition.errorMessage ?? dataParser.definition.errorMessage); }); } function int(definition) { 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 7d2455aa..fef70252 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/max.cjs +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/max.cjs @@ -11,9 +11,9 @@ function checkerNumberMax(max, definition = {}) { ...definition, max, }, - }, (value, error$1, self) => value <= self.definition.max + }, (value, error$1, self, dataParser) => value <= self.definition.max ? value - : error.addIssue(error$1, `number <= ${self.definition.max}`, value, self.definition.errorMessage)); + : error.addIssue(error$1, `number <= ${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.mjs b/docs/public/libs/v1/dataParser/parsers/number/checkers/max.mjs index 6e908a90..67ee0bfd 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/max.mjs +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/max.mjs @@ -9,9 +9,9 @@ function checkerNumberMax(max, definition = {}) { ...definition, max, }, - }, (value, error, self) => value <= self.definition.max + }, (value, error, self, dataParser) => value <= self.definition.max ? value - : addIssue(error, `number <= ${self.definition.max}`, value, self.definition.errorMessage)); + : addIssue(error, `number <= ${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 59058c68..c160ffa8 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/min.cjs +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/min.cjs @@ -11,9 +11,9 @@ function checkerNumberMin(min, definition = {}) { ...definition, min, }, - }, (value, error$1, self) => value >= self.definition.min + }, (value, error$1, self, dataParser) => value >= self.definition.min ? value - : error.addIssue(error$1, `number >= ${self.definition.min}`, value, self.definition.errorMessage)); + : error.addIssue(error$1, `number >= ${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.mjs b/docs/public/libs/v1/dataParser/parsers/number/checkers/min.mjs index 11138be6..330ab008 100644 --- a/docs/public/libs/v1/dataParser/parsers/number/checkers/min.mjs +++ b/docs/public/libs/v1/dataParser/parsers/number/checkers/min.mjs @@ -9,9 +9,9 @@ function checkerNumberMin(min, definition = {}) { ...definition, min, }, - }, (value, error, self) => value >= self.definition.min + }, (value, error, self, dataParser) => value >= self.definition.min ? value - : addIssue(error, `number >= ${self.definition.min}`, value, self.definition.errorMessage)); + : addIssue(error, `number >= ${self.definition.min}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } export { checkerNumberMin, checkerNumberMinKind }; diff --git a/docs/public/libs/v1/dataParser/parsers/refine.cjs b/docs/public/libs/v1/dataParser/parsers/refine.cjs index 4bf0161d..4adc336d 100644 --- a/docs/public/libs/v1/dataParser/parsers/refine.cjs +++ b/docs/public/libs/v1/dataParser/parsers/refine.cjs @@ -11,9 +11,9 @@ function checkerRefine(theFunction, definition) { ...definition, theFunction, }, - }, (value, error$1, self) => self.definition.theFunction(value) + }, (value, error$1, self, dataParser) => self.definition.theFunction(value) ? value - : error.addIssue(error$1, "value matching refine predicate", value, self.definition.errorMessage)); + : error.addIssue(error$1, "value matching refine predicate", value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } exports.checkerRefine = checkerRefine; diff --git a/docs/public/libs/v1/dataParser/parsers/refine.mjs b/docs/public/libs/v1/dataParser/parsers/refine.mjs index ecdc64ec..9df26766 100644 --- a/docs/public/libs/v1/dataParser/parsers/refine.mjs +++ b/docs/public/libs/v1/dataParser/parsers/refine.mjs @@ -9,9 +9,9 @@ function checkerRefine(theFunction, definition) { ...definition, theFunction, }, - }, (value, error, self) => self.definition.theFunction(value) + }, (value, error, self, dataParser) => self.definition.theFunction(value) ? value - : addIssue(error, "value matching refine predicate", value, self.definition.errorMessage)); + : addIssue(error, "value matching refine predicate", value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } export { checkerRefine, dataParserCheckerRefineKind }; diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/email.cjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/email.cjs index abfeec3e..95a1cafc 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/email.cjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/email.cjs @@ -13,9 +13,9 @@ function checkerEmail(definition = {}) { ...definition, regex: emailRegex, }, - }, (data, error$1, self) => self.definition.regex.test(data) + }, (data, error$1, self, dataParser) => self.definition.regex.test(data) ? data - : error.addIssue(error$1, "email", data, self.definition.errorMessage)); + : error.addIssue(error$1, "email", data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } function email(definition) { return index.string({ diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/email.mjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/email.mjs index 3b6a3cab..026c2c86 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/email.mjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/email.mjs @@ -11,9 +11,9 @@ function checkerEmail(definition = {}) { ...definition, regex: emailRegex, }, - }, (data, error, self) => self.definition.regex.test(data) + }, (data, error, self, dataParser) => self.definition.regex.test(data) ? data - : addIssue(error, "email", data, self.definition.errorMessage)); + : addIssue(error, "email", data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } function email(definition) { return string({ diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/max.cjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/max.cjs index aa59dac1..947e57da 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/max.cjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/max.cjs @@ -11,9 +11,9 @@ function checkerStringMax(max, definition = {}) { ...definition, max, }, - }, (data, error$1, self) => data.length <= self.definition.max + }, (data, error$1, self, dataParser) => data.length <= self.definition.max ? data - : error.addIssue(error$1, `string.length <= ${self.definition.max}`, data, self.definition.errorMessage)); + : error.addIssue(error$1, `string.length <= ${self.definition.max}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } exports.checkerStringMax = checkerStringMax; diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/max.mjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/max.mjs index c42e8992..80833edc 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/max.mjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/max.mjs @@ -9,9 +9,9 @@ function checkerStringMax(max, definition = {}) { ...definition, max, }, - }, (data, error, self) => data.length <= self.definition.max + }, (data, error, self, dataParser) => data.length <= self.definition.max ? data - : addIssue(error, `string.length <= ${self.definition.max}`, data, self.definition.errorMessage)); + : addIssue(error, `string.length <= ${self.definition.max}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } export { checkerStringMax, checkerStringMaxKind }; diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/min.cjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/min.cjs index 46fb6460..0aceb3af 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/min.cjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/min.cjs @@ -11,9 +11,9 @@ function checkerStringMin(min, definition = {}) { ...definition, min, }, - }, (data, error$1, self) => data.length >= self.definition.min + }, (data, error$1, self, dataParser) => data.length >= self.definition.min ? data - : error.addIssue(error$1, `string.length >= ${self.definition.min}`, data, self.definition.errorMessage)); + : error.addIssue(error$1, `string.length >= ${self.definition.min}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } exports.checkerStringMin = checkerStringMin; diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/min.mjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/min.mjs index 2c31566a..0749d348 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/min.mjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/min.mjs @@ -9,9 +9,9 @@ function checkerStringMin(min, definition = {}) { ...definition, min, }, - }, (data, error, self) => data.length >= self.definition.min + }, (data, error, self, dataParser) => data.length >= self.definition.min ? data - : addIssue(error, `string.length >= ${self.definition.min}`, data, self.definition.errorMessage)); + : addIssue(error, `string.length >= ${self.definition.min}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } export { checkerStringMin, checkerStringMinKind }; diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/regex.cjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/regex.cjs index 8ca92aef..129282c7 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/regex.cjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/regex.cjs @@ -11,9 +11,9 @@ function checkerRegex(regex, definition = {}) { ...definition, regex, }, - }, (data, error$1, self) => self.definition.regex.test(data) + }, (data, error$1, self, dataParser) => self.definition.regex.test(data) ? data - : error.addIssue(error$1, `string with pattern ${self.definition.regex.source.toString()}`, data, self.definition.errorMessage)); + : error.addIssue(error$1, `string with pattern ${self.definition.regex.source.toString()}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } exports.checkerRegex = checkerRegex; diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/regex.mjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/regex.mjs index e6fb9cfa..eb22bc62 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/regex.mjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/regex.mjs @@ -9,9 +9,9 @@ function checkerRegex(regex, definition = {}) { ...definition, regex, }, - }, (data, error, self) => self.definition.regex.test(data) + }, (data, error, self, dataParser) => self.definition.regex.test(data) ? data - : addIssue(error, `string with pattern ${self.definition.regex.source.toString()}`, data, self.definition.errorMessage)); + : addIssue(error, `string with pattern ${self.definition.regex.source.toString()}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } export { checkerRegex, checkerRegexKind }; diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/url.cjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/url.cjs index b78c361e..1454c3a3 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/url.cjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/url.cjs @@ -10,19 +10,19 @@ const regexRemoveDote = /:$/; function checkerUrl(definition = {}) { return base.dataParserCheckerInit(checkerUrlKind, { definition: definition, - }, (data, error$1, self) => { + }, (data, error$1, self, dataParser) => { try { const url = new URL(data); if (self.definition.hostname) { self.definition.hostname.lastIndex = 0; if (!self.definition.hostname.test(url.hostname)) { - return error.addIssue(error$1, `URL with hostname matching ${self.definition.hostname.source}`, data, self.definition.errorMessage); + return error.addIssue(error$1, `URL with hostname matching ${self.definition.hostname.source}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage); } } if (self.definition.protocol) { self.definition.protocol.lastIndex = 0; if (!self.definition.protocol.test(url.protocol.replace(regexRemoveDote, ""))) { - return error.addIssue(error$1, `URL with protocol matching ${self.definition.protocol.source}`, data, self.definition.errorMessage); + return error.addIssue(error$1, `URL with protocol matching ${self.definition.protocol.source}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage); } } if (self.definition.normalize) { @@ -33,7 +33,7 @@ function checkerUrl(definition = {}) { } } catch { - return error.addIssue(error$1, "valid URL", data, self.definition.errorMessage); + return error.addIssue(error$1, "valid URL", data, self.definition.errorMessage ?? dataParser.definition.errorMessage); } }); } diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/url.mjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/url.mjs index 4f2d4b73..c0c1b187 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/url.mjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/url.mjs @@ -8,19 +8,19 @@ const regexRemoveDote = /:$/; function checkerUrl(definition = {}) { return dataParserCheckerInit(checkerUrlKind, { definition: definition, - }, (data, error, self) => { + }, (data, error, self, dataParser) => { try { const url = new URL(data); if (self.definition.hostname) { self.definition.hostname.lastIndex = 0; if (!self.definition.hostname.test(url.hostname)) { - return addIssue(error, `URL with hostname matching ${self.definition.hostname.source}`, data, self.definition.errorMessage); + return addIssue(error, `URL with hostname matching ${self.definition.hostname.source}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage); } } if (self.definition.protocol) { self.definition.protocol.lastIndex = 0; if (!self.definition.protocol.test(url.protocol.replace(regexRemoveDote, ""))) { - return addIssue(error, `URL with protocol matching ${self.definition.protocol.source}`, data, self.definition.errorMessage); + return addIssue(error, `URL with protocol matching ${self.definition.protocol.source}`, data, self.definition.errorMessage ?? dataParser.definition.errorMessage); } } if (self.definition.normalize) { @@ -31,7 +31,7 @@ function checkerUrl(definition = {}) { } } catch { - return addIssue(error, "valid URL", data, self.definition.errorMessage); + return addIssue(error, "valid URL", data, self.definition.errorMessage ?? dataParser.definition.errorMessage); } }); } diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/uuid.cjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/uuid.cjs index a3d89d03..a7995ec8 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/uuid.cjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/uuid.cjs @@ -13,9 +13,9 @@ function checkerUuid(definition = {}) { ...definition, regex: uuidRegex, }, - }, (data, error$1, self) => uuidRegex.test(data) + }, (data, error$1, self, dataParser) => uuidRegex.test(data) ? data - : error.addIssue(error$1, "uuid", data, self.definition.errorMessage)); + : error.addIssue(error$1, "uuid", data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } /** * {@include dataParser/classic/uuid/index.md} diff --git a/docs/public/libs/v1/dataParser/parsers/string/checkers/uuid.mjs b/docs/public/libs/v1/dataParser/parsers/string/checkers/uuid.mjs index e2db8be4..363698b7 100644 --- a/docs/public/libs/v1/dataParser/parsers/string/checkers/uuid.mjs +++ b/docs/public/libs/v1/dataParser/parsers/string/checkers/uuid.mjs @@ -11,9 +11,9 @@ function checkerUuid(definition = {}) { ...definition, regex: uuidRegex, }, - }, (data, error, self) => uuidRegex.test(data) + }, (data, error, self, dataParser) => uuidRegex.test(data) ? data - : addIssue(error, "uuid", data, self.definition.errorMessage)); + : addIssue(error, "uuid", data, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } /** * {@include dataParser/classic/uuid/index.md} diff --git a/docs/public/libs/v1/dataParser/parsers/time/checkers/max.cjs b/docs/public/libs/v1/dataParser/parsers/time/checkers/max.cjs index 3b97bf6d..cdbc7341 100644 --- a/docs/public/libs/v1/dataParser/parsers/time/checkers/max.cjs +++ b/docs/public/libs/v1/dataParser/parsers/time/checkers/max.cjs @@ -15,9 +15,9 @@ function checkerTimeMax(max, definition = {}) { ...definition, max, }, - }, (value, error$1, self) => lessTime.lessTime(value, self.definition.max) + }, (value, error$1, self, dataParser) => lessTime.lessTime(value, self.definition.max) ? value - : error.addIssue(error$1, `time <= ${self.definition.max.toString()}`, value, self.definition.errorMessage)); + : error.addIssue(error$1, `time <= ${self.definition.max.toString()}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } exports.checkerTimeMax = checkerTimeMax; diff --git a/docs/public/libs/v1/dataParser/parsers/time/checkers/max.mjs b/docs/public/libs/v1/dataParser/parsers/time/checkers/max.mjs index 4b4416b7..e921578e 100644 --- a/docs/public/libs/v1/dataParser/parsers/time/checkers/max.mjs +++ b/docs/public/libs/v1/dataParser/parsers/time/checkers/max.mjs @@ -13,9 +13,9 @@ function checkerTimeMax(max, definition = {}) { ...definition, max, }, - }, (value, error, self) => lessTime(value, self.definition.max) + }, (value, error, self, dataParser) => lessTime(value, self.definition.max) ? value - : addIssue(error, `time <= ${self.definition.max.toString()}`, value, self.definition.errorMessage)); + : addIssue(error, `time <= ${self.definition.max.toString()}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } export { checkerTimeMax, checkerTimeMaxKind }; diff --git a/docs/public/libs/v1/dataParser/parsers/time/checkers/min.cjs b/docs/public/libs/v1/dataParser/parsers/time/checkers/min.cjs index 3e548c47..883755e0 100644 --- a/docs/public/libs/v1/dataParser/parsers/time/checkers/min.cjs +++ b/docs/public/libs/v1/dataParser/parsers/time/checkers/min.cjs @@ -15,9 +15,9 @@ function checkerTimeMin(min, definition = {}) { ...definition, min, }, - }, (value, error$1, self) => greaterTime.greaterTime(value, self.definition.min) + }, (value, error$1, self, dataParser) => greaterTime.greaterTime(value, self.definition.min) ? value - : error.addIssue(error$1, `time >= ${self.definition.min.toString()}`, value, self.definition.errorMessage)); + : error.addIssue(error$1, `time >= ${self.definition.min.toString()}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } exports.checkerTimeMin = checkerTimeMin; diff --git a/docs/public/libs/v1/dataParser/parsers/time/checkers/min.mjs b/docs/public/libs/v1/dataParser/parsers/time/checkers/min.mjs index 71d0f7d2..a1a60335 100644 --- a/docs/public/libs/v1/dataParser/parsers/time/checkers/min.mjs +++ b/docs/public/libs/v1/dataParser/parsers/time/checkers/min.mjs @@ -13,9 +13,9 @@ function checkerTimeMin(min, definition = {}) { ...definition, min, }, - }, (value, error, self) => greaterTime(value, self.definition.min) + }, (value, error, self, dataParser) => greaterTime(value, self.definition.min) ? value - : addIssue(error, `time >= ${self.definition.min.toString()}`, value, self.definition.errorMessage)); + : addIssue(error, `time >= ${self.definition.min.toString()}`, value, self.definition.errorMessage ?? dataParser.definition.errorMessage)); } export { checkerTimeMin, checkerTimeMinKind }; diff --git a/docs/public/libs/v1/metadata.json b/docs/public/libs/v1/metadata.json index dcde8a5f..3f0382e0 100644 --- a/docs/public/libs/v1/metadata.json +++ b/docs/public/libs/v1/metadata.json @@ -993,6 +993,15 @@ } ] }, + { + "name": "chainedFunction.cjs" + }, + { + "name": "chainedFunction.d.ts" + }, + { + "name": "chainedFunction.mjs" + }, { "name": "flag.cjs" }, @@ -1110,7 +1119,7 @@ "name": "deepPartial.d.ts" }, { - "name": "DeepReadonly.d.ts" + "name": "deepReadonly.d.ts" }, { "name": "deepRemoveReadonly.d.ts" @@ -1157,6 +1166,9 @@ { "name": "maybeArray.d.ts" }, + { + "name": "maybeAsyncGenerator.d.ts" + }, { "name": "maybeGetter.d.ts" }, diff --git a/jsDoc/clean/chainedFunction/example.ts b/jsDoc/clean/chainedFunction/example.ts new file mode 100644 index 00000000..4eee1265 --- /dev/null +++ b/jsDoc/clean/chainedFunction/example.ts @@ -0,0 +1,145 @@ +import { C, E, type ExpectType } from "@scripts"; + +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", + (draft: CommentDraft): Comment | E.Fail => draft.content.trim() + ? { + id: 1, + articleId: draft.articleId, + content: draft.content.trim(), + } + : E.fail(), + ], + [ + "incrementArticleCommentCount", + (article: Article): Article => ({ + ...article, + commentCount: article.commentCount + 1, + }), + ], +); + +const PublishCommentUseCase = C.createUseCase( + { + CommentRepository, + ArticleRepository, + }, + ({ + commentRepository, + articleRepository, + }) => (draft: CommentDraft) => CommentPublicationAggregate(function *(link1, { breakIfLeft }) { + const [comment, link2] = yield *link1(({ createComment }) => createComment(draft)); + + const savedComment = yield *breakIfLeft(commentRepository.save(comment)); + + const article = yield *breakIfLeft(articleRepository.findById(savedComment.articleId)); + + const [updatedArticle, chainEnd] = yield *link2( + ({ incrementArticleCommentCount }) => incrementArticleCommentCount(article), + ); + + const savedArticle = yield *breakIfLeft(articleRepository.save(updatedArticle)); + + return chainEnd({ + comment: savedComment, + article: savedArticle, + }); + }), +); + +const publishComment = PublishCommentUseCase.getUseCase({ + commentRepository: CommentRepository.createImplementation({ + save: (comment) => comment, + }), + articleRepository: ArticleRepository.createImplementation({ + findById: (articleId) => ({ + id: articleId, + commentCount: 11, + }), + save: (article) => article, + }), +}); + +const publishedComment = publishComment({ + articleId: 12, + content: " New comment ", +}); + +type CheckPublishedComment = ExpectType< + typeof publishedComment, + { + comment: Comment; + article: Article; + } | E.Fail, + "strict" +>; + +const emptyContentResult = publishComment({ + articleId: 12, + content: " ", +}); + +type CheckEmptyContentResult = ExpectType< + typeof emptyContentResult, + { + comment: Comment; + article: Article; + } | E.Fail, + "strict" +>; + +const failingPublishComment = PublishCommentUseCase.getUseCase({ + commentRepository: CommentRepository.createImplementation({ + save: () => E.fail(), + }), + articleRepository: ArticleRepository.createImplementation({ + findById: (articleId) => ({ + id: articleId, + commentCount: 11, + }), + save: (article) => article, + }), +}); + +const repositoryFailureResult = failingPublishComment({ + articleId: 12, + content: "New comment", +}); + +type CheckRepositoryFailureResult = ExpectType< + typeof repositoryFailureResult, + { + comment: Comment; + article: Article; + } | E.Fail, + "strict" +>; diff --git a/jsDoc/clean/chainedFunction/index.md b/jsDoc/clean/chainedFunction/index.md new file mode 100644 index 00000000..0e3619db --- /dev/null +++ b/jsDoc/clean/chainedFunction/index.md @@ -0,0 +1,19 @@ +Declares a typed aggregate of pure linked business actions that must run in order. + +**Supported call styles:** +- Classic: `chainedFunction(firstFunction, secondFunction, ...functions)` -> returns an implementation function driven by generator links + +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 +{@include clean/chainedFunction/example.ts[31,145]} +``` + +@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) +@see [`E.Left`](https://utils.duplojs.dev/en/v1/api/either/left) + +@namespace C diff --git a/scripts/clean/chainedFunction.ts b/scripts/clean/chainedFunction.ts new file mode 100644 index 00000000..9c6ee863 --- /dev/null +++ b/scripts/clean/chainedFunction.ts @@ -0,0 +1,247 @@ +import { type AnyFunction, type Kind, type IsEqual, type MaybePromise, type MaybeAsyncGenerator, type GetKindValue } from "@scripts/common"; +import * as EE from "@scripts/either"; +import { createCleanKind } from "./kind"; + +export type FunctionOfChain = [string, AnyFunction]; + +export type FunctionChain = [ + FunctionOfChain, + FunctionOfChain, + ...FunctionOfChain[], +]; + +export const chainEndKind = createCleanKind("chain-end"); + +export interface ChainEnd< + GenericValue extends unknown = unknown, +> extends Kind { + +} + +export interface CreateChainEnd { + (): ChainEnd; + < + GenericValue extends unknown, + >( + value: GenericValue + ): ChainEnd; +} + +export type Link< + GenericFunction extends FunctionOfChain = FunctionOfChain, + GenericNext extends (Link | CreateChainEnd) = any, +> = < + GenericOutput extends ReturnType, +>( + theFunction: ( + theFunction: { [Prop in GenericFunction[0]]: GenericFunction[1] } + ) => GenericOutput +) => ( + | ( + Extract> extends infer InferredPromise + ? IsEqual extends true + ? never + : Awaited extends infer InferredValue extends unknown + ? AsyncGenerator< + Extract, + [Exclude, GenericNext] + > + : never + : never + ) + | ( + Exclude> extends infer InferredValue + ? IsEqual extends true + ? never + : Generator< + Extract, + [Exclude, GenericNext] + > + : never + ) +); + +export type Chain< + GenericFunctionChain extends readonly FunctionOfChain[], +> = GenericFunctionChain extends readonly [] + ? CreateChainEnd + : GenericFunctionChain extends [ + infer InferredFirst extends FunctionOfChain, + ...infer InferredRest extends readonly FunctionOfChain[], + ] + ? Chain extends infer InferredRestResult extends (Link | CreateChainEnd) + ? Link< + InferredFirst, + InferredRestResult + > + : never + : never; + +declare const SymbolError: unique symbol; + +type OutputMustContainChainEnd< + GenericGenerator extends MaybeAsyncGenerator, +> = IsEqual< + GenericGenerator extends MaybeAsyncGenerator + ? InferredReturnValue extends ChainEnd + ? InferredReturnValue + : never + : never, + never +> extends true + ? { [SymbolError]: "Output must contain a chainEnd" } + : unknown; + +type ComputeResult< + GenericGenerator extends MaybeAsyncGenerator, +> = GenericGenerator extends Generator< + infer InferredIterateValue, + infer InferredReturnValue +> + ? ( + | InferredIterateValue + | InferredReturnValue + ) extends infer InferredResult + ? InferredResult extends ChainEnd + ? GetKindValue + : InferredResult + : never + : GenericGenerator extends AsyncGenerator< + infer InferredIterateValue, + infer InferredReturnValue + > + ? Promise< + Awaited< + | InferredIterateValue + | InferredReturnValue + > extends infer InferredResult + ? InferredResult extends ChainEnd + ? GetKindValue + : InferredResult + : never + > + : never; + +function *breakIfLeft< + GenericValue extends unknown, +>( + value: GenericValue, +): Generator< + Extract, + Exclude + > { + if (EE.isLeft(value)) { + yield value; + } + + return value as never; +} + +export interface ChainedFunctionParams { + breakIfLeft: typeof breakIfLeft; +} + +const chainedFunctionParams: ChainedFunctionParams = { breakIfLeft }; + +export type ChainedFunction< + GenericValue extends FunctionChain = FunctionChain, +> = < + GenericGenerator extends MaybeAsyncGenerator< + MaybePromise, + MaybePromise + >, +>( + callback: (firstLink: Chain, params: ChainedFunctionParams) => ( + & GenericGenerator + & OutputMustContainChainEnd< + GenericGenerator + > + ) +) => ComputeResult; + +/** + * {@include clean/chainedFunction/index.md} + */ +export function chainedFunction< + const GenericFunction1 extends FunctionOfChain, + const GenericFunction2 extends FunctionOfChain, + const GenericFunctions extends FunctionOfChain[], +>( + function1: GenericFunction1, + function2: GenericFunction2, + ...functions: GenericFunctions +): ChainedFunction<[ + GenericFunction1, + GenericFunction2, + ...GenericFunctions, + ]> { + return (theFunction) => { + const functionChain: FunctionChain = [function1, function2, ...functions]; + + const createLink = ( + functionChain: FunctionChain, + ): Link => (theFunction) => { + const [functionName, chainedFunction] = functionChain.shift()!; + const result = theFunction({ [functionName]: chainedFunction }); + + const nextLink = functionChain.length === 0 + ? (value: unknown) => chainEndKind.setTo({}, value) + : createLink(functionChain); + + if ((result as any) instanceof Promise) { + return (async function *() { + const awaitedResult = await result; + + if (EE.isLeft(awaitedResult)) { + yield awaitedResult; + } + + return [awaitedResult, nextLink]; + })() as never; + } + + return (function *() { + if (EE.isLeft(result)) { + yield result; + } + + return [result, nextLink]; + })() as never; + }; + + const generator = theFunction( + createLink(functionChain) as never, + chainedFunctionParams, + ); + + let result: undefined | IteratorResult, unknown> = undefined; + + if (Symbol.asyncIterator in generator) { + return (async() => { + try { + result = await generator.next(); + } finally { + await generator.return(undefined as never); + } + + return ( + chainEndKind.has(result.value) + ? chainEndKind.getValue(result.value) + : result.value + ); + })() as never; + } + + try { + result = generator.next(); + } finally { + generator.return(undefined as never); + } + + return ( + chainEndKind.has(result.value) + ? chainEndKind.getValue(result.value) + : result.value + ) as never; + }; +} diff --git a/scripts/clean/index.ts b/scripts/clean/index.ts index 858a4fbd..2382904b 100644 --- a/scripts/clean/index.ts +++ b/scripts/clean/index.ts @@ -13,3 +13,4 @@ export * from "./useCase"; export * from "./flag"; export * from "./maybe"; export * from "./toMapDataParser"; +export * from "./chainedFunction"; diff --git a/scripts/common/types/DeepReadonly.ts b/scripts/common/types/deepReadonly.ts similarity index 100% rename from scripts/common/types/DeepReadonly.ts rename to scripts/common/types/deepReadonly.ts diff --git a/scripts/common/types/index.ts b/scripts/common/types/index.ts index d15eea86..1f6071dc 100644 --- a/scripts/common/types/index.ts +++ b/scripts/common/types/index.ts @@ -43,7 +43,8 @@ export * from "./onlyLiteral"; export * from "./sortType"; export * from "./maybeGetter"; export * from "./falsyValue"; -export * from "./DeepReadonly"; +export * from "./deepReadonly"; export * from "./json"; export * from "./predicate"; export * from "./bivariantFunction"; +export * from "./maybeAsyncGenerator"; diff --git a/scripts/common/types/maybeAsyncGenerator.ts b/scripts/common/types/maybeAsyncGenerator.ts new file mode 100644 index 00000000..89e0337b --- /dev/null +++ b/scripts/common/types/maybeAsyncGenerator.ts @@ -0,0 +1,16 @@ +export type MaybeAsyncGenerator< + GenericIterateValue extends unknown = unknown, + GenericReturnValue extends unknown = unknown, + GenericNext extends unknown = unknown, +> = ( + | Generator< + GenericIterateValue, + GenericReturnValue, + GenericNext + > + | AsyncGenerator< + GenericIterateValue, + GenericReturnValue, + GenericNext + > +); diff --git a/tests/clean/chainedFunction.test.ts b/tests/clean/chainedFunction.test.ts new file mode 100644 index 00000000..77411a1c --- /dev/null +++ b/tests/clean/chainedFunction.test.ts @@ -0,0 +1,239 @@ +import { DClean, DEither, type ExpectType } from "@scripts"; + +describe("chainedFunction", () => { + it("executes links in declaration order and returns the chain end value", () => { + const useCase = DClean.chainedFunction( + ["parseTitle", (input: string) => input.trim()], + ["createSlug", (input: string) => input.toLowerCase().replaceAll(" ", "-")], + ["persistSlug", (input: string) => input.length], + ); + + const calls: string[] = []; + + const result = useCase(function *(link1) { + const [title, link2] = yield *link1(({ parseTitle }) => { + calls.push("parseTitle"); + return parseTitle(" Hello World "); + }); + + const [slug, link3] = yield *link2(({ createSlug }) => { + calls.push("createSlug"); + return createSlug(title); + }); + + const [slugLength, chainEnd] = yield *link3(({ persistSlug }) => { + calls.push("persistSlug"); + return persistSlug(slug); + }); + + return chainEnd(slugLength); + }); + + expect(calls).toStrictEqual(["parseTitle", "createSlug", "persistSlug"]); + expect(result).toBe(11); + + type Check = ExpectType< + typeof result, + number, + "strict" + >; + }); + + it("short-circuits on a synchronous left and does not execute following links", () => { + const error = DEither.error("invalid-title"); + const useCase = DClean.chainedFunction( + ["parseTitle", () => error], + ["persistTitle", (input: string) => input.length], + ); + + const persistTitleSpy = vi.fn((input: string) => input.length); + + const result = useCase(function *(link1) { + const [title, link2] = yield *link1(({ parseTitle }) => parseTitle()); + const [length, chainEnd] = yield *link2(() => persistTitleSpy(title)); + + return chainEnd(length); + }); + + expect(result).toBe(error); + expect(persistTitleSpy).not.toHaveBeenCalled(); + + type Check = ExpectType< + typeof result, + DEither.Error<"invalid-title"> | number, + "strict" + >; + }); + + it("returns the left produced by a chained function call after previous links succeeded", () => { + const error = DEither.error({ reason: "persist-failed" }); + const useCase = DClean.chainedFunction( + ["parseTitle", (input: string) => input.trim()], + ["persistTitle", (input: string) => input.length > 0 ? error : input.length], + ); + + const result = useCase(function *(link1) { + const [title, link2] = yield *link1(({ parseTitle }) => parseTitle("hello")); + const [length, chainEnd] = yield *link2(({ persistTitle }) => persistTitle(title)); + + return chainEnd(length); + }); + + expect(result).toBe(error); + + type Check = ExpectType< + typeof result, + DEither.Error<{ readonly reason: "persist-failed" }> | number, + "strict" + >; + }); + + it("returns the left produced directly by the implementation", () => { + const error = DEither.error({ reason: "implementation-failed" }); + const useCase = DClean.chainedFunction( + ["parseTitle", (input: string) => input.trim()], + ["persistTitle", (input: string) => input.length], + ); + + const result = useCase(function *(link1) { + const [title, link2] = yield *link1(({ parseTitle }) => parseTitle("hello")); + const [length, chainEnd] = yield *link2(({ persistTitle }) => persistTitle(title)); + + return title.length > 0 + ? error + : chainEnd(length); + }); + + expect(result).toBe(error); + + type Check = ExpectType< + typeof result, + DEither.Error<{ readonly reason: "implementation-failed" }> | number, + "strict" + >; + }); + + it("breakIfLeft returns a synchronous value without its left branch", () => { + const error = DEither.error("manual-left"); + const value = "hello" as string | typeof error; + const useCase = DClean.chainedFunction( + ["parseTitle", (input: string) => input.trim()], + ["persistTitle", (input: string) => input.length], + ); + + const result = useCase(function *(link1, { breakIfLeft }) { + const titleInput = yield *breakIfLeft(value); + const [title, link2] = yield *link1(({ parseTitle }) => parseTitle(titleInput)); + const [length, chainEnd] = yield *link2(({ persistTitle }) => persistTitle(title)); + + return chainEnd(length); + }); + + expect(result).toBe(5); + + type Check = ExpectType< + typeof result, + DEither.Error<"manual-left"> | number, + "strict" + >; + }); + + it("breakIfLeft short-circuits on a synchronous left", () => { + const error = DEither.error("manual-left"); + const value = error as string | typeof error; + const useCase = DClean.chainedFunction( + ["parseTitle", (input: string) => input.trim()], + ["persistTitle", (input: string) => input.length], + ); + const persistTitleSpy = vi.fn((input: string) => input.length); + + const result = useCase(function *(link1, { breakIfLeft }) { + const titleInput = yield *breakIfLeft(value); + type check = ExpectType< + typeof titleInput, + string, + "strict" + >; + const [title, link2] = yield *link1(({ parseTitle }) => parseTitle(titleInput)); + const [length, chainEnd] = yield *link2(() => persistTitleSpy(title)); + + return chainEnd(length); + }); + + expect(result).toBe(error); + expect(persistTitleSpy).not.toHaveBeenCalled(); + + type Check = ExpectType< + typeof result, + DEither.Error<"manual-left"> | number, + "strict" + >; + }); + + it("awaits asynchronous links and returns asynchronous chain end values", async() => { + const useCase = DClean.chainedFunction( + ["loadCount", async(input: number) => Promise.resolve(input + 1)], + ["saveCount", async(input: number) => Promise.resolve(input * 2)], + ); + + const result = useCase(async function *(link1) { + const [loadedCount, link2] = yield *link1(({ loadCount }) => loadCount(3)); + const [savedCount, chainEnd] = yield *link2(({ saveCount }) => saveCount(loadedCount)); + + await Promise.resolve(); + + return chainEnd(savedCount); + }); + + await expect(result).resolves.toBe(8); + + type Check = ExpectType< + typeof result, + Promise, + "strict" + >; + }); + + it("short-circuits on an asynchronous left before running the next link", async() => { + const error = DEither.error({ code: "load-failed" }); + const useCase = DClean.chainedFunction( + ["loadCount", async() => Promise.resolve(error)], + ["saveCount", async(input: number) => Promise.resolve(input * 2)], + ); + + const saveCountSpy = vi.fn(async(input: number) => Promise.resolve(input * 2)); + + const result = useCase(async function *(link1) { + const [loadedCount, link2] = yield *link1(({ loadCount }) => loadCount()); + const [savedCount, chainEnd] = yield *link2(() => saveCountSpy(loadedCount)); + + await Promise.resolve(); + + return chainEnd(savedCount); + }); + + await expect(result).resolves.toBe(error); + expect(saveCountSpy).not.toHaveBeenCalled(); + + type Check = ExpectType< + typeof result, + Promise | number>, + "strict" + >; + }); + + it("rejects callbacks that can finish without returning a chain end", () => { + const useCase = DClean.chainedFunction( + ["readValue", () => 1], + ["saveValue", (input: number) => input], + ); + + useCase( + //@ts-expect-error chainedFunction callbacks must return chainEnd on the success path + function *(link1) { + const [value, link2] = yield *link1(({ readValue }) => readValue()); + yield *link2(({ saveValue }) => saveValue(value)); + }, + ); + }); +});