From ccce25b6a039cf2e5c1a774c1ab022f0946ca8d5 Mon Sep 17 00:00:00 2001 From: Alex / KATT Date: Sat, 30 Sep 2023 20:04:24 +0200 Subject: [PATCH] feat: initial version (#1) --- .all-contributorsrc | 2 +- .eslintrc.cjs | 12 +- .github/CONTRIBUTING.md | 10 +- .github/DEVELOPMENT.md | 2 +- .github/FUNDING.yml | 1 + .github/ISSUE_TEMPLATE.md | 2 +- .github/ISSUE_TEMPLATE/01-bug.yml | 2 +- .github/ISSUE_TEMPLATE/02-documentation.yml | 2 +- .github/ISSUE_TEMPLATE/03-feature.yml | 2 +- .github/ISSUE_TEMPLATE/04-tooling.yml | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 4 +- .github/workflows/post-release.yml | 2 +- .github/workflows/release.yml | 4 +- .github/workflows/test.yml | 25 ++ .husky/.gitignore | 1 - .husky/pre-commit | 3 - README.md | 20 +- cspell.json | 6 +- package.json | 16 +- pnpm-lock.yaml | 118 +------ src/greet.test.ts | 44 --- src/greet.ts | 13 - src/handlers.ts | 83 +++++ src/index.ts | 8 +- src/tson.test.ts | 349 ++++++++++++++++++++ src/tson.ts | 177 ++++++++++ src/types.ts | 126 ++++++- src/utils.ts | 15 + 28 files changed, 825 insertions(+), 226 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/test.yml delete mode 100644 .husky/.gitignore delete mode 100755 .husky/pre-commit delete mode 100644 src/greet.test.ts delete mode 100644 src/greet.ts create mode 100644 src/handlers.ts create mode 100644 src/tson.test.ts create mode 100644 src/tson.ts create mode 100644 src/utils.ts diff --git a/.all-contributorsrc b/.all-contributorsrc index 7d8e4a8c..1c9f2973 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -35,7 +35,7 @@ "README.md" ], "imageSize": 100, - "projectName": "tson", + "projectName": "tupleson", "projectOwner": "KATT", "repoHost": "https://github.com", "repoType": "github" diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9a4e033e..b40f945a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -118,20 +118,22 @@ module.exports = { root: true, rules: { // These off/less-strict-by-default rules work well for this repo and we like them on. + "@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "all" }], "no-only-tests/no-only-tests": "error", // These on-by-default rules don't work well for this repo and we like them off. + "@typescript-eslint/padding-line-between-statements": [ + "error", + { blankLine: "always", next: "*", prev: "block-like" }, + ], "no-case-declarations": "off", "no-constant-condition": "off", "no-inner-declarations": "off", - "no-mixed-spaces-and-tabs": "off", // Stylistic concerns that don't interfere with Prettier - "@typescript-eslint/padding-line-between-statements": [ - "error", - { blankLine: "always", next: "*", prev: "block-like" }, - ], + + "no-mixed-spaces-and-tabs": "off", "perfectionist/sort-objects": [ "error", { diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f677d733..aa5c642a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Thanks for your interest in contributing to `tson`! 💖 +Thanks for your interest in contributing to `tupleson`! 💖 > After this page, see [DEVELOPMENT.md](./DEVELOPMENT.md) for local development instructions. @@ -10,7 +10,7 @@ This project contains a [Contributor Covenant code of conduct](./CODE_OF_CONDUCT ## Reporting Issues -Please do [report an issue on the issue tracker](https://github.com/KATT/tson/issues/new/choose) if there's any bugfix, documentation improvement, or general enhancement you'd like to see in the repository! Please fully fill out all required fields in the most appropriate issue form. +Please do [report an issue on the issue tracker](https://github.com/KATT/tupleson/issues/new/choose) if there's any bugfix, documentation improvement, or general enhancement you'd like to see in the repository! Please fully fill out all required fields in the most appropriate issue form. ## Sending Contributions @@ -22,8 +22,8 @@ There are two steps involved: ### Finding an Issue -With the exception of very small typos, all changes to this repository generally need to correspond to an [open issue marked as `accepting prs` on the issue tracker](https://github.com/KATT/tson/issues?q=is%3Aopen+is%3Aissue+label%3A%22accepting+prs%22). -If this is your first time contributing, consider searching for [unassigned issues that also have the `good first issue` label](https://github.com/KATT/tson/issues?q=is%3Aopen+is%3Aissue+label%3A%22accepting+prs%22+label%3A%22good+first+issue%22+no%3Aassignee). +With the exception of very small typos, all changes to this repository generally need to correspond to an [open issue marked as `accepting prs` on the issue tracker](https://github.com/KATT/tupleson/issues?q=is%3Aopen+is%3Aissue+label%3A%22accepting+prs%22). +If this is your first time contributing, consider searching for [unassigned issues that also have the `good first issue` label](https://github.com/KATT/tupleson/issues?q=is%3Aopen+is%3Aissue+label%3A%22accepting+prs%22+label%3A%22good+first+issue%22+no%3Aassignee). If the issue you'd like to fix isn't found on the issue, see [Reporting Issues](#reporting-issues) for filing your own (please do!). #### Issue Claiming @@ -42,7 +42,7 @@ Be sure to fill out the pull request template's requested information -- otherwi PRs are also expected to have a title that adheres to [commitlint](https://github.com/conventional-changelog/commitlint). Only PR titles need to be in that format, not individual commits. Don't worry if you get this wrong: you can always change the PR title after sending it. -Check [previously merged PRs](https://github.com/KATT/tson/pulls?q=is%3Apr+is%3Amerged+-label%3Adependencies+) for reference. +Check [previously merged PRs](https://github.com/KATT/tupleson/pulls?q=is%3Apr+is%3Amerged+-label%3Adependencies+) for reference. #### Draft PRs diff --git a/.github/DEVELOPMENT.md b/.github/DEVELOPMENT.md index 0aad1c3e..ad988cbe 100644 --- a/.github/DEVELOPMENT.md +++ b/.github/DEVELOPMENT.md @@ -3,7 +3,7 @@ After [forking the repo from GitHub](https://help.github.com/articles/fork-a-repo) and [installing pnpm](https://pnpm.io/installation): ```shell -git clone https://github.com//tson +git clone https://github.com/KATT/tupleson cd tson pnpm install ``` diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ea615fbd --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: KATT diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 2dde42c9..7ffc9588 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,5 +1,5 @@ - + diff --git a/.github/ISSUE_TEMPLATE/01-bug.yml b/.github/ISSUE_TEMPLATE/01-bug.yml index fac6bf41..27e61551 100644 --- a/.github/ISSUE_TEMPLATE/01-bug.yml +++ b/.github/ISSUE_TEMPLATE/01-bug.yml @@ -7,7 +7,7 @@ body: required: true - label: I have pulled the latest `main` branch of the repository. required: true - - label: I have [searched for related issues](https://github.com/KATT/tson/issues?q=is%3Aissue) and found none that matched my issue. + - label: I have [searched for related issues](https://github.com/KATT/tupleson/issues?q=is%3Aissue) and found none that matched my issue. required: true type: checkboxes - attributes: diff --git a/.github/ISSUE_TEMPLATE/02-documentation.yml b/.github/ISSUE_TEMPLATE/02-documentation.yml index 46cb3ff9..a4d1de57 100644 --- a/.github/ISSUE_TEMPLATE/02-documentation.yml +++ b/.github/ISSUE_TEMPLATE/02-documentation.yml @@ -5,7 +5,7 @@ body: options: - label: I have pulled the latest `main` branch of the repository. required: true - - label: I have [searched for related issues](https://github.com/KATT/tson/issues?q=is%3Aissue) and found none that matched my issue. + - label: I have [searched for related issues](https://github.com/KATT/tupleson/issues?q=is%3Aissue) and found none that matched my issue. required: true type: checkboxes - attributes: diff --git a/.github/ISSUE_TEMPLATE/03-feature.yml b/.github/ISSUE_TEMPLATE/03-feature.yml index 1f946583..ea4d1bd4 100644 --- a/.github/ISSUE_TEMPLATE/03-feature.yml +++ b/.github/ISSUE_TEMPLATE/03-feature.yml @@ -7,7 +7,7 @@ body: required: true - label: I have pulled the latest `main` branch of the repository. required: true - - label: I have [searched for related issues](https://github.com/KATT/tson/issues?q=is%3Aissue) and found none that matched my issue. + - label: I have [searched for related issues](https://github.com/KATT/tupleson/issues?q=is%3Aissue) and found none that matched my issue. required: true type: checkboxes - attributes: diff --git a/.github/ISSUE_TEMPLATE/04-tooling.yml b/.github/ISSUE_TEMPLATE/04-tooling.yml index 670af8dc..acd169ef 100644 --- a/.github/ISSUE_TEMPLATE/04-tooling.yml +++ b/.github/ISSUE_TEMPLATE/04-tooling.yml @@ -7,7 +7,7 @@ body: required: true - label: I have pulled the latest `main` branch of the repository. required: true - - label: I have [searched for related issues](https://github.com/KATT/tson/issues?q=is%3Aissue) and found none that matched my issue. + - label: I have [searched for related issues](https://github.com/KATT/tupleson/issues?q=is%3Aissue) and found none that matched my issue. required: true type: checkboxes - attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5308a423..1c81fc25 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,8 +5,8 @@ Otherwise we may not be able to review your PR. --> ## PR Checklist - [ ] Addresses an existing open issue: fixes #000 -- [ ] That issue was marked as [`status: accepting prs`](https://github.com/KATT/tson/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) -- [ ] Steps in [CONTRIBUTING.md](https://github.com/KATT/tson/blob/main/.github/CONTRIBUTING.md) were taken +- [ ] That issue was marked as [`status: accepting prs`](https://github.com/KATT/tupleson/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) +- [ ] Steps in [CONTRIBUTING.md](https://github.com/KATT/tupleson/blob/main/.github/CONTRIBUTING.md) were taken ## Overview diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index 8aa33a51..29cc6849 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -14,7 +14,7 @@ jobs: The release is available on: - * [GitHub releases](https://github.com/KATT/tson/releases/tag/{release_tag}) + * [GitHub releases](https://github.com/KATT/tupleson/releases/tag/{release_tag}) * [npm package (@latest dist-tag)](https://www.npmjs.com/package/tson/v/${{ env.npm_version }}) Cheers! 📦🚀 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1a9b3db..7dca0a44 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: script: | try { await github.request( - `DELETE /repos/KATT/tson/branches/main/protection`, + `DELETE /repos/KATT/tupleson/branches/main/protection`, ); } catch (error) { if (!error.message?.includes?.("Branch not protected")) { @@ -43,7 +43,7 @@ jobs: github-token: ${{ secrets.ACCESS_TOKEN }} script: | github.request( - `PUT /repos/KATT/tson/branches/main/protection`, + `PUT /repos/KATT/tupleson/branches/main/protection`, { allow_deletions: false, allow_force_pushes: true, diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..48af8220 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + - run: pnpm run test --coverage + - name: Codecov + uses: codecov/codecov-action@v3 + with: + flags: unit + - if: always() + name: Archive code coverage results + uses: actions/upload-artifact@v3 + with: + name: code-coverage-report + path: coverage + +name: Test + +on: + pull_request: ~ + push: + branches: + - main diff --git a/.husky/.gitignore b/.husky/.gitignore deleted file mode 100644 index 31354ec1..00000000 --- a/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 0ccfe480..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" -npx lint-staged diff --git a/README.md b/README.md index 9ab05b92..c40d9ce9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -

TSON

+

tupleSON

-

A hackable JSON (se|de)rializer

+

A hackable JSON serializer/deserializer

@@ -10,19 +10,23 @@ - - Codecov Test Coverage + + Codecov Test Coverage - + Contributor Covenant - - License: MIT + + License: MIT Style: Prettier TypeScript: Strict

+## Introduction + +Not much to see yet, but you can spy on the tests. + ## Contributors @@ -32,7 +36,7 @@ - + diff --git a/cspell.json b/cspell.json index 494dd2ec..eb20ddd3 100644 --- a/cspell.json +++ b/cspell.json @@ -22,6 +22,10 @@ "packagejson", "quickstart", "tsup", - "wontfix" + "wontfix", + "tson", + "stringifier", + "KATT", + "tupleson" ] } diff --git a/package.json b/package.json index 8b9949ba..2c4d8efe 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,13 @@ { - "name": "tson", + "name": "tupleson", "version": "0.0.0", - "description": "A hackable JSON (se|de)rializer", + "description": "A hackable JSON serializer/deserializer", "repository": { "type": "git", - "url": "https://github.com/KATT/tson" + "url": "https://github.com/KATT/tupleson" }, "license": "MIT", - "author": { - "email": "alexander@n1s.se" - }, + "author": "KATT", "type": "module", "main": "./lib/index.js", "files": [ @@ -28,14 +26,10 @@ "lint:package-json": "npmPkgJsonLint .", "lint:packages": "pnpm dedupe --check", "lint:spelling": "cspell \"**\" \".github/**/*\"", - "prepare": "husky install", "should-semantic-release": "should-semantic-release --verbose", "test": "vitest", "tsc": "tsc" }, - "lint-staged": { - "*": "prettier --ignore-unknown --write" - }, "devDependencies": { "@release-it/conventional-changelog": "^7.0.2", "@types/eslint": "^8.44.3", @@ -56,10 +50,8 @@ "eslint-plugin-regexp": "^1.15.0", "eslint-plugin-vitest": "^0.3.1", "eslint-plugin-yml": "^1.9.0", - "husky": "^8.0.3", "jsonc-eslint-parser": "^2.3.0", "knip": "^2.30.0", - "lint-staged": "^14.0.1", "markdownlint": "^0.31.1", "markdownlint-cli": "^0.37.0", "npm-package-json-lint": "^7.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 172e7f42..ae13b783 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,18 +62,12 @@ devDependencies: eslint-plugin-yml: specifier: ^1.9.0 version: 1.9.0(eslint@8.50.0) - husky: - specifier: ^8.0.3 - version: 8.0.3 jsonc-eslint-parser: specifier: ^2.3.0 version: 2.3.0 knip: specifier: ^2.30.0 version: 2.30.0 - lint-staged: - specifier: ^14.0.1 - version: 14.0.1 markdownlint: specifier: ^0.31.1 version: 0.31.1 @@ -1457,13 +1451,6 @@ packages: type-fest: 0.21.3 dev: true - /ansi-escapes@5.0.0: - resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} - engines: {node: '>=12'} - dependencies: - type-fest: 1.4.0 - dev: true - /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1917,14 +1904,6 @@ packages: engines: {node: '>=6'} dev: true - /cli-truncate@3.1.0: - resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - slice-ansi: 5.0.0 - string-width: 5.1.2 - dev: true - /cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1933,6 +1912,7 @@ packages: /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + requiresBuild: true dev: true /color-convert@1.9.3: @@ -1956,10 +1936,6 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true - /colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - dev: true - /commander@11.0.0: resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} engines: {node: '>=16'} @@ -2429,6 +2405,7 @@ packages: /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + requiresBuild: true dependencies: clone: 1.0.4 dev: true @@ -2980,10 +2957,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - dev: true - /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -3601,12 +3574,6 @@ packages: engines: {node: '>=14.18.0'} dev: true - /husky@8.0.3: - resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} - engines: {node: '>=14'} - hasBin: true - dev: true - /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -3837,11 +3804,6 @@ packages: engines: {node: '>=8'} dev: true - /is-fullwidth-code-point@4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - dev: true - /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -4287,43 +4249,6 @@ packages: uc.micro: 1.0.6 dev: true - /lint-staged@14.0.1: - resolution: {integrity: sha512-Mw0cL6HXnHN1ag0mN/Dg4g6sr8uf8sn98w2Oc1ECtFto9tvRF7nkXGJRbx8gPlHyoR0pLyBr2lQHbWwmUHe1Sw==} - engines: {node: ^16.14.0 || >=18.0.0} - hasBin: true - dependencies: - chalk: 5.3.0 - commander: 11.0.0 - debug: 4.3.4 - execa: 7.2.0 - lilconfig: 2.1.0 - listr2: 6.6.1 - micromatch: 4.0.5 - pidtree: 0.6.0 - string-argv: 0.3.2 - yaml: 2.3.1 - transitivePeerDependencies: - - enquirer - - supports-color - dev: true - - /listr2@6.6.1: - resolution: {integrity: sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==} - engines: {node: '>=16.0.0'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true - dependencies: - cli-truncate: 3.1.0 - colorette: 2.0.20 - eventemitter3: 5.0.1 - log-update: 5.0.1 - rfdc: 1.3.0 - wrap-ansi: 8.1.0 - dev: true - /load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4407,17 +4332,6 @@ packages: is-unicode-supported: 1.3.0 dev: true - /log-update@5.0.1: - resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - ansi-escapes: 5.0.0 - cli-cursor: 4.0.0 - slice-ansi: 5.0.0 - strip-ansi: 7.1.0 - wrap-ansi: 8.1.0 - dev: true - /loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: @@ -5208,12 +5122,6 @@ packages: engines: {node: '>=8.6'} dev: true - /pidtree@0.6.0: - resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} - engines: {node: '>=0.10'} - hasBin: true - dev: true - /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -5614,10 +5522,6 @@ packages: resolution: {integrity: sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ==} dev: true - /rfdc@1.3.0: - resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} - dev: true - /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -5802,14 +5706,6 @@ packages: engines: {node: '>=12'} dev: true - /slice-ansi@5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 4.0.0 - dev: true - /smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -5927,11 +5823,6 @@ packages: internal-slot: 1.0.5 dev: true - /string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - dev: true - /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -6792,11 +6683,6 @@ packages: yaml: 2.3.2 dev: true - /yaml@2.3.1: - resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} - engines: {node: '>= 14'} - dev: true - /yaml@2.3.2: resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==} engines: {node: '>= 14'} diff --git a/src/greet.test.ts b/src/greet.test.ts deleted file mode 100644 index f729115f..00000000 --- a/src/greet.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { greet } from "./greet.js"; - -const message = "Yay, testing!"; - -describe("greet", () => { - it("logs to the console once when message is provided as a string", () => { - const logger = vi.spyOn(console, "log").mockImplementation(() => undefined); - - greet(message); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(1); - }); - - it("logs to the console once when message is provided as an object", () => { - const logger = vi.spyOn(console, "log").mockImplementation(() => undefined); - - greet({ message }); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(1); - }); - - it("logs once when times is not provided in an object", () => { - const logger = vi.fn(); - - greet({ logger, message }); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(1); - }); - - it("logs a specified number of times when times is provided", () => { - const logger = vi.fn(); - const times = 7; - - greet({ logger, message, times }); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(7); - }); -}); diff --git a/src/greet.ts b/src/greet.ts deleted file mode 100644 index a0d3b4c6..00000000 --- a/src/greet.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { GreetOptions } from "./types.js"; - -export function greet(options: GreetOptions | string) { - const { - logger = console.log.bind(console), - message, - times = 1, - } = typeof options === "string" ? { message: options } : options; - - for (let i = 0; i < times; i += 1) { - logger(message); - } -} diff --git a/src/handlers.ts b/src/handlers.ts new file mode 100644 index 00000000..c28832f6 --- /dev/null +++ b/src/handlers.ts @@ -0,0 +1,83 @@ +import { TsonType } from "./types.js"; +import { isPlainObject } from "./utils.js"; + +export const tsonMap: TsonType, [unknown, unknown][]> = { + deserialize: (v) => new Map(v), + serialize: (v) => Array.from(v.entries()), + test: (v) => v instanceof Map, +}; + +export const tsonSet: TsonType, unknown[]> = { + deserialize: (v) => new Set(v), + serialize: (v) => Array.from(v), + test: (v) => v instanceof Set, +}; + +export const tsonBigint: TsonType = { + deserialize: (v) => BigInt(v), + primitive: "bigint", + serialize: (v) => v.toString(), +}; + +/** + * Prevents `NaN` and `Infinity` from being serialized + */ +export const tsonNumber: TsonType = { + primitive: "number", + test: (v) => { + const value = v as number; + if (isNaN(value)) { + throw new Error("Encountered NaN"); + } + + if (!isFinite(value)) { + throw new Error("Encountered Infinity"); + } + + return false; + }, + transform: false, +}; + +export const tsonUndefined: TsonType = { + deserialize: () => undefined, + primitive: "undefined", + serialize: () => 0, +}; + +export const tsonDate: TsonType = { + deserialize: (value) => new Date(value), + serialize: (value) => value.toJSON(), + test: (value) => value instanceof Date, +}; + +export class UnknownObjectGuardError extends Error { + public readonly value; + + constructor(value: unknown) { + super(`Unknown object found`); + this.name = this.constructor.name; + this.value = value; + } +} + +export const tsonUnknown: TsonType = { + test: (v) => { + if (v && typeof v === "object" && !Array.isArray(v) && !isPlainObject(v)) { + throw new UnknownObjectGuardError(v); + } + + return false; + }, + transform: false, +}; + +export const tsonRegExp: TsonType = { + deserialize: (str) => { + const body = str.slice(1, str.lastIndexOf("/")); + const flags = str.slice(str.lastIndexOf("/") + 1); + return new RegExp(body, flags); + }, + serialize: (value) => "" + value, + test: (value) => value instanceof RegExp, +}; diff --git a/src/index.ts b/src/index.ts index a39b40fa..498706e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,6 @@ -export * from "./greet.js"; -export * from "./types.js"; +export { + tsonDeserializer, + tsonParser, + tsonSerializer, + tsonStringifier, +} from "./tson.js"; diff --git a/src/tson.test.ts b/src/tson.test.ts new file mode 100644 index 00000000..f68e5c96 --- /dev/null +++ b/src/tson.test.ts @@ -0,0 +1,349 @@ +import { assert, expect, expectTypeOf, test } from "vitest"; + +import { + UnknownObjectGuardError, + tsonBigint, + tsonDate, + tsonMap, + tsonNumber, + tsonRegExp, + tsonSet, + tsonUndefined, + tsonUnknown, +} from "./handlers.js"; +import { + tsonDeserializer, + tsonParser, + tsonSerializer, + tsonStringifier, +} from "./tson.js"; +import { TsonOptions } from "./types.js"; + +const expectError = (fn: () => unknown) => { + let err: unknown; + try { + fn(); + } catch (_err) { + err = _err; + } + + expect(err).toBeDefined(); + expect(err).toBeInstanceOf(Error); + return err as Error; +}; + +function setup(opts: TsonOptions) { + const nonce: TsonOptions["nonce"] = () => "__tson"; + const withDefaults: TsonOptions = { + nonce, + ...opts, + }; + return { + deserialize: tsonDeserializer(withDefaults), + parse: tsonParser(withDefaults), + serializer: tsonSerializer(withDefaults), + stringify: tsonStringifier(withDefaults), + }; +} + +test("Date", () => { + const ctx = setup({ + types: { + Date: tsonDate, + }, + }); + + const date = new Date(); + + const stringified = ctx.stringify(date); + const deserialized = ctx.parse(stringified); + expect(deserialized).toEqual(date); +}); + +test("number", () => { + const t = setup({ + types: { + number: tsonNumber, + }, + }); + + const bad = [ + // + NaN, + Infinity, + -Infinity, + ]; + const good = [1, 0, -1, 1.1, -1.1]; + + const errors: unknown[] = []; + + for (const n of bad) { + const err = expectError(() => t.parse(t.stringify(n))); + errors.push(err); + } + + expect(errors).toMatchInlineSnapshot(` + [ + [Error: Encountered NaN], + [Error: Encountered Infinity], + [Error: Encountered Infinity], + ] + `); + + for (const n of good) { + const deserialized = t.parse(t.stringify(n)); + expect(deserialized).toEqual(n); + } +}); + +test("undefined", () => { + const ctx = setup({ + types: { + undefined: tsonUndefined, + }, + }); + + const expected = { + foo: [1, undefined, 2], + } as const; + const stringified = ctx.stringify(expected); + const deserialized = ctx.parse(stringified); + + expect(deserialized).toEqual(expected); +}); + +test("Map", () => { + const t = setup({ + types: { + Map: tsonMap, + }, + }); + + const expected = new Map([["a", "b"]]); + + const stringified = t.stringify(expected); + const deserialized = t.parse(stringified); + expect(deserialized).toEqual(expected); +}); + +test("Set", () => { + const t = setup({ + types: { + Set: tsonSet, + }, + }); + + const expected = new Set(["a", "b"]); + + const stringified = t.stringify(expected); + const deserialized = t.parse(stringified); + expect(deserialized).toEqual(expected); +}); + +test("bigint", () => { + const t = setup({ + types: { + Map: tsonMap, + Set: tsonSet, + bigint: tsonBigint, + }, + }); + + { + // bigint + const expected = 1n; + + const stringified = t.stringify(expected); + const deserialized = t.parse(stringified); + + expect(deserialized).toEqual(expected); + + { + // set of BigInt + const expected = new Set([1n]); + + const stringified = t.stringify(expected); + const deserialized = t.parse(stringified); + + expect(deserialized).toEqual(expected); + } + + { + // set of a map of bigint + const expected = new Set([new Map([["a", 1n]])]); + + const stringified = t.stringify(expected); + const deserialized = t.parse(stringified); + + expect(deserialized).toEqual(expected); + } + } +}); + +test("guard unwanted objects", () => { + // Sets are okay, but not Maps + const t = setup({ + types: { + Set: tsonSet, + // defined last so it runs last + unknownObjectGuard: tsonUnknown, + }, + }); + + { + // sets are okay + + const expected = new Set([1]); + + const stringified = t.stringify(expected); + const deserialized = t.parse(stringified); + + expect(deserialized).toEqual(expected); + } + + { + // plain objects are okay + const expected = { a: 1 }; + const stringified = t.stringify(expected); + const deserialized = t.parse(stringified); + expect(deserialized).toEqual(expected); + } + + { + // maps are not okay + + const expected = new Map([["a", 1]]); + + const err = expectError(() => t.parse(t.stringify(expected))); + assert(err instanceof UnknownObjectGuardError); + + expect(err).toMatchInlineSnapshot( + "[UnknownObjectGuardError: Unknown object found]", + ); + expect(err.value).toEqual(expected); + } +}); + +test("regex", () => { + const t = setup({ + types: { + RegExp: tsonRegExp, + }, + }); + + const expected = /foo/g; + + const stringified = t.stringify(expected, 2); + + expect(stringified).toMatchInlineSnapshot( + ` + "{ + \\"json\\": [ + \\"RegExp\\", + \\"/foo/g\\", + \\"__tson\\" + ], + \\"nonce\\": \\"__tson\\" + }" + `, + ); + + const deserialized = t.parse(stringified); + + expect(deserialized).toBeInstanceOf(RegExp); + expect(deserialized).toMatchInlineSnapshot("/foo/g"); + expect(deserialized + "").toEqual(expected + ""); +}); + +test("lets have a look at the stringified output", () => { + const t = setup({ + types: { + Map: tsonMap, + Set: tsonSet, + bigint: tsonBigint, + undefined: tsonUndefined, + }, + }); + + const expected = new Set([ + // + 1, + "string", + undefined, + null, + true, + false, + 1n, + new Map([["foo", "bar"]]), + ]); + + const stringified = t.stringify(expected, 2); + + expect(stringified).toMatchInlineSnapshot(` + "{ + \\"json\\": [ + \\"Set\\", + [ + 1, + \\"string\\", + [ + \\"undefined\\", + 0, + \\"__tson\\" + ], + null, + true, + false, + [ + \\"bigint\\", + \\"1\\", + \\"__tson\\" + ], + [ + \\"Map\\", + [ + [ + \\"foo\\", + \\"bar\\" + ] + ], + \\"__tson\\" + ] + ], + \\"__tson\\" + ], + \\"nonce\\": \\"__tson\\" + }" + `); + + const deserialized = t.parse(stringified); + + expect(deserialized).toEqual(expected); +}); + +test("types", () => { + const t = setup({ + types: { + bigint: tsonBigint, + }, + }); + + const expected = 1n; + { + const stringified = t.stringify(expected); + // ^? + const parsed = t.parse(stringified); + // ^? + + expectTypeOf(parsed).toEqualTypeOf(expected); + } + + { + const serialized = t.serializer(expected); + // ^? + const deserialized = t.deserialize(serialized); + // ^? + + expectTypeOf(deserialized).toEqualTypeOf(expected); + } +}); diff --git a/src/tson.ts b/src/tson.ts new file mode 100644 index 00000000..0cc941e6 --- /dev/null +++ b/src/tson.ts @@ -0,0 +1,177 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + TsonAllTypes, + TsonDeserializeFn, + TsonNonce, + TsonOptions, + TsonParseFn, + TsonSerializeFn, + TsonSerialized, + TsonSerializedValue, + TsonStringifyFn, + TsonTransformerSerializeDeserialize, + TsonTuple, + TsonTypeHandlerKey, + TsonTypeTesterCustom, + TsonTypeTesterPrimitive, +} from "./types.js"; +import { isPlainObject } from "./utils.js"; + +function isTsonTuple(v: unknown, nonce: string): v is TsonTuple { + return Array.isArray(v) && v.length === 3 && v[2] === nonce; +} + +type WalkFn = (value: unknown) => unknown; +type WalkerFactory = (nonce: TsonNonce) => WalkFn; + +export function tsonDeserializer(opts: TsonOptions): TsonDeserializeFn { + const walker: WalkerFactory = (nonce) => { + const walk: WalkFn = (value) => { + if (isTsonTuple(value, nonce)) { + const [type, serializedValue] = value; + const transformer = opts.types[ + type + ] as TsonTransformerSerializeDeserialize; + return transformer.deserialize(walk(serializedValue)); + } + + return mapOrReturn(value, walk); + }; + + return walk; + }; + + return ((obj: TsonSerialized) => + walker(obj.nonce)(obj.json)) as TsonDeserializeFn; +} + +export function tsonParser(opts: TsonOptions): TsonParseFn { + const deserializer = tsonDeserializer(opts); + + return ((str: string) => + deserializer(JSON.parse(str) as TsonSerialized)) as TsonParseFn; +} + +export function tsonStringifier(opts: TsonOptions): TsonStringifyFn { + const serializer = tsonSerializer(opts); + + return ((obj: unknown, space?: number | string) => + JSON.stringify(serializer(obj), null, space)) as TsonStringifyFn; +} + +export function tsonSerializer(opts: TsonOptions): TsonSerializeFn { + const handlers = (() => { + // warmup the type handlers + const types = Object.entries(opts.types).map(([_key, handler]) => { + const key = _key as TsonTypeHandlerKey; + const serialize = handler.serialize; + + type Serializer = ( + value: unknown, + nonce: TsonNonce, + walk: WalkFn, + ) => TsonSerializedValue; + + const $serialize: Serializer = serialize + ? (value, nonce, walk): TsonTuple => [ + key, + walk(serialize(value)), + nonce, + ] + : (value, _nonce, walk) => walk(value); + return { + ...handler, + $serialize, + }; + }); + type Handler = (typeof types)[number]; + + const handlerPerPrimitive: Partial< + Record> + > = {}; + const customTypeHandlers: Extract[] = []; + + for (const handler of types) { + if (handler.primitive) { + if (handlerPerPrimitive[handler.primitive]) { + throw new Error( + `Multiple handlers for primitive ${handler.primitive} found`, + ); + } + + handlerPerPrimitive[handler.primitive] = handler; + } else { + customTypeHandlers.push(handler); + } + } + + return { + custom: customTypeHandlers, + primitive: handlerPerPrimitive, + }; + })(); + const maybeNonce = opts.nonce; + + const walker: WalkerFactory = (nonce) => { + const walk: WalkFn = (value) => { + const type = typeof value; + + const primitiveHandler = handlers.primitive[type]; + if ( + primitiveHandler && + (!primitiveHandler.test || primitiveHandler.test(value)) + ) { + return primitiveHandler.$serialize(value, nonce, walk); + } + + for (const handler of handlers.custom) { + if (handler.test(value)) { + return handler.$serialize(value, nonce, walk); + } + } + + return mapOrReturn(value, walk); + }; + + return walk; + }; + + return ((obj): TsonSerialized => { + const nonce: TsonNonce = + typeof maybeNonce === "function" + ? (maybeNonce() as TsonNonce) + : ("__tson" as TsonNonce); + + const json = walker(nonce)(obj); + + return { + json, + nonce, + } as TsonSerialized; + }) as TsonSerializeFn; +} + +/** + * Maps over an object or array, returning a new object or array with the same keys. + * If the input is not an object or array, the input is returned. + */ +function mapOrReturn( + input: unknown, + fn: (val: unknown, key: number | string) => unknown, +): unknown { + if (Array.isArray(input)) { + return input.map(fn); + } + + if (isPlainObject(input)) { + const output: typeof input = {}; + for (const [key, value] of Object.entries(input)) { + output[key] = fn(value, key); + } + + return output; + } + + return input; +} diff --git a/src/types.ts b/src/types.ts index 4f16ae39..c5412fad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,123 @@ -export interface GreetOptions { - logger?: (message: string) => void; - message: string; - times?: number; +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable @typescript-eslint/no-explicit-any */ +const brand = Symbol("branded"); + +export type TsonBranded = TType & { [brand]: TBrand }; + +export type TsonNonce = TsonBranded; +export type TsonTypeHandlerKey = TsonBranded; +export type TsonSerializedValue = unknown; + +export type TsonTuple = [TsonTypeHandlerKey, TsonSerializedValue, TsonNonce]; + +// there's probably a better way of getting this type +export type TsonAllTypes = + | "bigint" + | "boolean" + | "function" + | "number" + | "object" + | "string" + | "symbol" + | "undefined"; + +type SerializedType = + | Record + | boolean + | number + | string + | unknown[]; + +export interface TsonTransformerNone { + deserialize?: never; + + serialize?: never; + /** + * Won't be deserialized nor serialized + */ + transform: false; } +export interface TsonTransformerSerializeDeserialize< + TValue, + TSerializedType extends SerializedType, +> { + /** + * From JSON-serializable value + */ + deserialize: (v: TSerializedType) => TValue; + + /** + * JSON-serializable value + */ + serialize: (v: TValue) => TSerializedType; + /** + * Use a transformer to serialize and deserialize the value? + */ + transform?: true; +} + +export type TsonTransformer = + | TsonTransformerNone + | TsonTransformerSerializeDeserialize; + +export interface TsonTypeTesterPrimitive { + /** + * The type of the primitive + */ + primitive: TsonAllTypes; + /** + * Test if the value is of this type + */ + test?: (v: unknown) => boolean; +} +export interface TsonTypeTesterCustom { + /** + * The type of the primitive + */ + primitive?: never; + /** + * Test if the value is of this type + */ + test: (v: unknown) => boolean; +} + +type TsonTypeTester = TsonTypeTesterCustom | TsonTypeTesterPrimitive; + +export type TsonType< + /** + * The type of the value + */ + TValue, + /** + * JSON-serializable value how it's stored after it's serialized + */ + TSerializedType extends SerializedType, +> = TsonTypeTester & TsonTransformer; + +export interface TsonOptions { + nonce?: () => string; + types: Record | TsonType>; +} + +const serialized = Symbol("serialized"); + +export interface TsonSerialized { + json: TsonSerializedValue; + nonce: TsonNonce; + [serialized]: TValue; +} + +export type TsonSerializeFn = (obj: TValue) => TsonSerialized; + +export type TsonDeserializeFn = ( + data: TsonSerialized, +) => TValue; + +type TsonStringified = string & { [serialized]: TValue }; + +export type TsonStringifyFn = ( + obj: TValue, + space?: number | string, +) => TsonStringified; + +export type TsonParseFn = (string: TsonStringified) => TValue; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..8f47a397 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,15 @@ +export const isPlainObject = (obj: unknown): obj is Record => { + if (!obj || typeof obj !== "object") { + return false; + } + + if (obj === Object.prototype) { + return false; + } + + if (Object.getPrototypeOf(obj) === null) { + return true; + } + + return Object.getPrototypeOf(obj) === Object.prototype; +};
Alex / KATT
Alex / KATT

💻 🖋 📖 🤔 🚇 🚧 📆 🔧
Alex / KATT
Alex / KATT

💻 🖋 📖 🤔 🚇 🚧 📆 🔧
Josh Goldberg ✨
Josh Goldberg ✨

🔧