diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7e8acac --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,14 @@ +# These are supported funding model platforms + +github: [lambdalisue] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/jsr.yml b/.github/workflows/jsr.yml new file mode 100644 index 0000000..17910a3 --- /dev/null +++ b/.github/workflows/jsr.yml @@ -0,0 +1,27 @@ +name: jsr + +env: + DENO_VERSION: 1.x + +on: + push: + tags: + - "v*" + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: denoland/setup-deno@v1 + with: + deno-version: ${{ env.DENO_VERSION }} + - name: Publish + run: | + deno run -A jsr:@david/publish-on-tag@^0.1.4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a7af027 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,57 @@ +name: Test + +env: + DENO_VERSION: 1.x + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v1 + with: + deno-version: ${{ env.DENO_VERSION }} + - name: Format + run: | + deno fmt --check + - name: Lint + run: deno lint + - name: Type check + run: deno task check + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v1 + with: + deno-version: ${{ env.DENO_VERSION }} + - name: Test + run: | + deno task test:coverage + timeout-minutes: 5 + - run: | + deno task coverage --lcov > coverage.lcov + - uses: codecov/codecov-action@v4 + with: + os: ${{ runner.os }} + files: ./coverage.lcov + token: ${{ secrets.CODECOV_TOKEN }} + + jsr-publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v1 + with: + deno-version: ${{ env.DENO_VERSION }} + - name: Publish (dry-run) + run: | + deno publish --dry-run diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e4b189 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/docs +deno.lock +.coverage diff --git a/.gitmessage b/.gitmessage new file mode 100644 index 0000000..71ddb20 --- /dev/null +++ b/.gitmessage @@ -0,0 +1,21 @@ + +# **Conventional Commits** +# +# [optional scope]: +# +# feat: feature (minor) +# deps: dependencies (minor/patch) +# fix: bug fix (patch) +# refactor: refactoring code +# test: test fix; no code change +# docs: documentation fix; no code change +# style: formatting, missing semi colons, etc; no code change +# chore: updating build tasks, package manager configs, etc; no code change +# +# **Install** +# +# git config commit.template .gitmessage +# +# **Reference** +# +# - https://www.conventionalcommits.org/en/v1.0.0/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c4b69db --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright 2024 jsr-core + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..00a4720 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# pipeline + +[![jsr](https://jsr.io/badges/@core/pipeline)](https://jsr.io/@core/pipeline) +[![test](https://github.com/jsr-core/pipeline/workflows/Test/badge.svg)](https://github.com/jsr-core/pipeline/actions?query=workflow%3ATest) +[![codecov](https://codecov.io/github/jsr-core/pipeline/graph/badge.svg?token=pfbLRGU5AM)](https://codecov.io/github/jsr-core/pipeline) + +## Usage + +### pipe + +Pipe a value through a series of operatorfunctions. + +```ts +import { pipe } from "@core/pipeline"; + +const result = pipe( + 1, + (v) => v + 1, + (v) => v * 2, + (v) => v.toString(), +); +console.log(result); // "4" +``` + +## License + +The code follows MIT license written in [LICENSE](./LICENSE). Contributors need +to agree that any modifications sent in this repository follow the license. diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..483b280 --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,37 @@ +{ + "name": "@core/pipeline", + "version": "0.0.0", + "exports": { + ".": "./mod.ts" + }, + "exclude": [ + ".coverage/**" + ], + "publish": { + "include": [ + "**/*.ts", + "README.md", + "LICENSE" + ], + "exclude": [ + "**/*_test.ts", + "**/*_bench.ts", + ".*" + ] + }, + "imports": { + "@core/pipeline": "./mod.ts", + "@std/assert": "jsr:@std/assert@^1.0.2", + "@std/jsonc": "jsr:@std/jsonc@^1.0.0", + "@std/path": "jsr:@std/path@^1.0.2", + "@std/testing": "jsr:@std/testing@^1.0.0" + }, + "tasks": { + "check": "deno check **/*.ts", + "test": "deno test -A --doc --parallel --shuffle", + "test:coverage": "deno task test --coverage=.coverage", + "coverage": "deno coverage .coverage", + "update": "deno run --allow-env --allow-read --allow-write=. --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli ./*.ts", + "update:commit": "deno task -q update --commit --prefix deps: --pre-commit=fmt,lint" + } +} diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..3801208 --- /dev/null +++ b/mod.ts @@ -0,0 +1,470 @@ +export type Operator = (v: A) => B; + +type LastOperatorReturn[]> = T extends + [...Operator[], Operator] ? R + : never; + +/** + * Pipe a value through a series of operatorfunctions. + * + * @param value - The value to pipe through the operators. + * @param operators - The operators to apply to the value. + * @returns The value after it has been piped through all the operators. + * + * @example + * ```ts + * import { pipe } from "@core/pipeline"; + * + * const result = pipe( + * 1, + * (v) => v + 1, + * (v) => v * 2, + * (v) => v.toString(), + * ); + * console.log(result); // "4" + * ``` + */ +export function pipe(value: V): V; +export function pipe(value: V, o01: Operator): T01; +export function pipe( + value: V, + o01: Operator, + o02: Operator, +): T02; +export function pipe( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, +): T03; +export function pipe( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, +): T04; +export function pipe( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, +): T05; +export function pipe( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, +): T06; +export function pipe( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, +): T07; +export function pipe( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, +): T08; +export function pipe( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, +): T09; +export function pipe( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, +): T10; +export function pipe( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, + o11: Operator, +): T11; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, +>( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, + o11: Operator, + o12: Operator, +): T12; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, +>( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, + o11: Operator, + o12: Operator, + o13: Operator, +): T13; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, +>( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, + o11: Operator, + o12: Operator, + o13: Operator, + o14: Operator, +): T14; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, +>( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, + o11: Operator, + o12: Operator, + o13: Operator, + o14: Operator, + o15: Operator, +): T15; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, + T16, +>( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, + o11: Operator, + o12: Operator, + o13: Operator, + o14: Operator, + o15: Operator, + o16: Operator, +): T16; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, + T16, + T17, +>( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, + o11: Operator, + o12: Operator, + o13: Operator, + o14: Operator, + o15: Operator, + o16: Operator, + o17: Operator, +): T17; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, + T16, + T17, + T18, +>( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, + o11: Operator, + o12: Operator, + o13: Operator, + o14: Operator, + o15: Operator, + o16: Operator, + o17: Operator, + o18: Operator, +): T18; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, + T16, + T17, + T18, + T19, +>( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, + o11: Operator, + o12: Operator, + o13: Operator, + o14: Operator, + o15: Operator, + o16: Operator, + o17: Operator, + o18: Operator, + o19: Operator, +): T19; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, + T16, + T17, + T18, + T19, + T20, +>( + value: V, + o01: Operator, + o02: Operator, + o03: Operator, + o04: Operator, + o05: Operator, + o06: Operator, + o07: Operator, + o08: Operator, + o09: Operator, + o10: Operator, + o11: Operator, + o12: Operator, + o13: Operator, + o14: Operator, + o15: Operator, + o16: Operator, + o17: Operator, + o18: Operator, + o19: Operator, + o20: Operator, +): T20; + +// deno-lint-ignore no-explicit-any +export function pipe[]>( + value: V, + ...operators: Operators +): LastOperatorReturn; + +// deno-lint-ignore no-explicit-any +export function pipe(value: V, ...operators: Operator[]) { + return operators.reduce( + (result, next) => next(result), + value, + ); +} diff --git a/mod_test.ts b/mod_test.ts new file mode 100644 index 0000000..25ee95a --- /dev/null +++ b/mod_test.ts @@ -0,0 +1,151 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { pipe } from "./mod.ts"; + +Deno.test("pipe", async (t) => { + await t.step("with no operators", async (t) => { + await t.step("should return the input", () => { + assertEquals(pipe(1), 1); + }); + }); + + await t.step("with one operator", async (t) => { + await t.step("should return operator applied value", () => { + assertEquals(pipe(1, (v) => v * 2), 2); + }); + + await t.step("should resolve the type properly", () => { + pipe(1, (v) => { + assertType>(true); + return v.toString(); + }); + }); + }); + + await t.step("with two operators", async (t) => { + await t.step("should return operator applied value", () => { + assertEquals(pipe(1, (v) => v * 2, (v) => v * 2), 4); + }); + + await t.step("should resolve the type properly", () => { + pipe(1, (v) => { + assertType>(true); + return v.toString(); + }, (v) => { + assertType>(true); + return v.length; + }); + }); + }); + + await t.step("with three operators", async (t) => { + await t.step("should return operator applied value", () => { + assertEquals(pipe(1, (v) => v * 2, (v) => v * 2, (v) => v * 2), 8); + }); + + await t.step("should resolve the type properly", () => { + pipe(1, (v) => { + assertType>(true); + return v.toString(); + }, (v) => { + assertType>(true); + return v.length; + }, (v) => { + assertType>(true); + return v.toString(); + }); + }); + }); + + await t.step(`with twenty operators`, async (t) => { + await t.step("should return operator applied value", () => { + assertEquals(pipe(1, ...Array(20).fill((v: number) => v * 2)), 2 ** 20); + }); + + await t.step("should resolve the type properly", () => { + pipe( + 1, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + ); + }); + }); +});