├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ ├── automate-PRs.yml
│ ├── close-stale-issues-and-PRs.yml
│ ├── gha-publish-to-npm.yml
│ └── test.yml
├── .gitignore
├── .husky
├── .gitignore
├── pre-commit
└── pre-push
├── .npmignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── ci
└── ci-test-commands.sh
├── config
├── commitlint.config.js
├── global.d.ts
├── helpers.js
├── jest.config.js
├── setup-tests.js
├── tsconfig.json
└── types.d.ts
├── jest.config.js
├── package.json
├── scripts
├── build.js
├── copy.js
├── file-size.js
└── tsconfig.json
├── src
├── fail.ts
├── index.ts
├── internal
│ └── utils.ts
├── rule.ts
├── rules
│ ├── get-prop.ts
│ ├── greater-than.ts
│ ├── has-prop.ts
│ ├── index.ts
│ ├── is-array.ts
│ ├── is-equal-to.ts
│ ├── is-non-empty-array.ts
│ ├── is-non-empty-object.ts
│ ├── is-non-empty-string.ts
│ ├── is-number.ts
│ ├── is-object.ts
│ ├── is-string.ts
│ ├── is-valid-email.ts
│ ├── less-than.ts
│ ├── matches-regex.ts
│ ├── max-length.ts
│ └── min-length.ts
├── success.ts
├── types
│ └── index.ts
├── utils
│ ├── compose.ts
│ ├── index.ts
│ └── pipe.ts
└── validation.ts
├── test
├── Builtins.test.ts
└── Validation.test.ts
├── tsconfig.dev.json
├── tsconfig.json
├── tsup.config.ts
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | # all files
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | max_line_length = 80
13 |
14 | [*.{js,ts}]
15 | quote_type = single
16 | curly_bracket_next_line = false
17 | spaces_around_brackets = inside
18 | indent_brace_style = BSD KNF
19 |
20 | # HTML
21 | [*.html]
22 | quote_type = double
23 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | scripts
4 | config
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line unicorn/prefer-module
2 | module.exports = {
3 | parser: `@typescript-eslint/parser`,
4 | parserOptions: {
5 | ecmaVersion: 2018,
6 | sourceType: `module`,
7 | project: [`./tsconfig.json`, `./tsconfig.dev.json`],
8 | extraFileExtensions: [`.md`],
9 | },
10 | plugins: [`@typescript-eslint`],
11 | env: {
12 | browser: true,
13 | node: true,
14 | es6: true,
15 | jest: true,
16 | },
17 | extends: [`adjunct`],
18 | rules: {
19 | 'comma-spacing': `off`,
20 | 'no-prototype-builtins': 0,
21 | '@typescript-eslint/no-unsafe-assignment': 0,
22 | '@typescript-eslint/no-unsafe-member-access': 0,
23 | 'unicorn/no-array-reduce': 0,
24 | 'sonarjs/no-nested-template-literals': 0,
25 | '@typescript-eslint/comma-spacing': [`error`],
26 | '@typescript-eslint/quotes': [`error`, `backtick`],
27 | '@typescript-eslint/no-explicit-any': 0,
28 | '@typescript-eslint/explicit-function-return-type': 0,
29 | '@typescript-eslint/prefer-regexp-exec': 0,
30 | '@typescript-eslint/no-non-null-assertion': 0,
31 | '@typescript-eslint/triple-slash-reference': 0,
32 | '@typescript-eslint/no-var-requires': 0,
33 | '@typescript-eslint/semi': [`error`, `never`],
34 | '@typescript-eslint/member-delimiter-style': [
35 | 2,
36 | {
37 | multiline: {
38 | delimiter: `none`,
39 | },
40 | singleline: {
41 | delimiter: `semi`,
42 | requireLast: false,
43 | },
44 | },
45 | ],
46 | 'object-curly-spacing': [`error`, `always`],
47 | indent: [`error`, 2],
48 | 'padding-line-between-statements': [
49 | `error`,
50 | {
51 | blankLine: `always`,
52 | prev: `*`,
53 | next: `return`,
54 | },
55 | {
56 | blankLine: `never`,
57 | prev: `return`,
58 | next: `*`,
59 | },
60 | {
61 | blankLine: `always`,
62 | prev: [`const`, `let`, `var`],
63 | next: `*`,
64 | },
65 | {
66 | blankLine: `any`,
67 | prev: [`const`, `let`, `var`],
68 | next: [`const`, `let`, `var`],
69 | },
70 | {
71 | blankLine: `always`,
72 | prev: [`block-like`],
73 | next: `*`,
74 | },
75 | {
76 | blankLine: `always`,
77 | prev: [`*`],
78 | next: `block-like`,
79 | },
80 | {
81 | blankLine: `always`,
82 | prev: [`import`],
83 | next: `*`,
84 | },
85 | {
86 | blankLine: `any`,
87 | prev: [`import`],
88 | next: `import`,
89 | },
90 | ],
91 | 'no-multiple-empty-lines': [
92 | `error`,
93 | {
94 | max: 1,
95 | maxEOF: 1,
96 | maxBOF: 0,
97 | },
98 | ],
99 | },
100 | }
101 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | First of all, thanks for your interest in contributing to the codeparticle-formal! 🎉
4 |
5 | PRs are the preferred way to spike ideas and address issues, if you have time. If you plan on contributing frequently, please feel free to ask to become a maintainer; the more the merrier. 🤙
6 |
7 | ## Technical overview
8 |
9 | This library uses following libraries for development:
10 |
11 | - [typescript](http://www.typescriptlang.org/) for typed JavaScript and transpilation
12 | - [jest](https://jestjs.io/) for unit testing
13 | - run `yarn test:watch` during development
14 | - [rollup](https://rollupjs.org/guide/en) for creating UMD bundles
15 | - [yarn](https://yarnpkg.com/lang/en/) for package management
16 | - [npm scripts](https://docs.npmjs.com/misc/scripts) for executing tasks
17 |
18 | ### 🧪 Tests
19 |
20 | Test are written and run via Jest 💪
21 |
22 | ```sh
23 | # Run whole test suite once
24 | yarn test
25 | # Run test in watch mode
26 | yarn test:watch
27 | # Ged code coverage
28 | yarn test:coverage
29 | ```
30 |
31 | ### 💅 Style guides
32 |
33 | Style guides are enforced by robots _(I meant prettier and tslint of course 🤖 )_, so they'll let you know if you screwed something, but most of the time, they'll autofix things for you. Magic right ?
34 |
35 | Lint and format codebase via npm-script:
36 |
37 | ```sh
38 | #Format and fix lint errors
39 | yarn ts:style:fix
40 | ```
41 |
42 | #### Commit conventions (via commitizen)
43 |
44 | - this is preferred way how to create conventional-changelog valid commits
45 | - if you prefer your custom tool we provide a commit hook linter which will error out, it you provide invalid commit message
46 | - if you are in rush and just wanna skip commit message validation just prefix your message with `WIP: something done` ( if you do this please squash your work when you're done with proper commit message so standard-version can create Changelog and bump version of your library appropriately )
47 |
48 | ```sh
49 | # invoke [commitizen CLI](https://github.com/commitizen/cz-cli)
50 | yarn commit
51 | ```
52 |
53 | ### 📖 Documentation
54 |
55 | ```sh
56 | yarn docs
57 | ```
58 |
59 | ## Getting started
60 |
61 | ### Creating a Pull Request
62 |
63 | If you've never submitted a Pull request before please visit http://makeapullrequest.com/ to learn everything you need to know.
64 |
65 | #### Setup
66 |
67 | 1. Fork the repo.
68 | 1. `git clone` your fork.
69 | 1. Make a `git checkout -b branch-name` branch for your change.
70 | 1. Run `yarn install --ignore-scripts` (make sure you have node and yarn installed first)
71 | Updates
72 |
73 | 1. Make sure to add unit tests
74 | 1. If there is a `*.spec.ts` file, update it to include a test for your change, if needed. If this file doesn't exist, please create it.
75 | 1. Run `yarn test` or `yarn test:watch` to make sure all tests are working, regardless if a test was added.
76 |
77 | ---
78 |
79 | ## 🚀 Publishing
80 |
81 | > releases are handled by awesome [standard-version](https://github.com/conventional-changelog/standard-version)
82 |
83 | > #### NOTE:
84 | >
85 | > you have to create npm account and register token on your machine
86 | > 👉 `npm adduser`
87 | >
88 | > If you are using scope (you definitely should 👌) don't forget to [`--scope`](https://docs.npmjs.com/cli/adduser#scope)
89 |
90 | Execute `yarn release` which will handle following tasks:
91 |
92 | - bump package version and git tag
93 | - update/(create if it doesn't exist) CHANGELOG.md
94 | - push to github master branch + push tags
95 | - publish build packages to npm
96 |
97 | > releases are handled by awesome [standard-version](https://github.com/conventional-changelog/standard-version)
98 |
99 | ### Initial Release (no package.json version bump):
100 |
101 | ```sh
102 | yarn release --first-release
103 | ```
104 |
105 | ### Pre-release
106 |
107 | - To get from `1.1.2` to `1.1.2-0`:
108 |
109 | `yarn release --prerelease`
110 |
111 | - **Alpha**: To get from `1.1.2` to `1.1.2-alpha.0`:
112 |
113 | `yarn release --prerelease alpha`
114 |
115 | - **Beta**: To get from `1.1.2` to `1.1.2-beta.0`:
116 |
117 | `yarn release --prerelease beta`
118 |
119 | ### Dry run
120 |
121 | #### version bump + changelog
122 |
123 | ```sh
124 | # See what next release version would be with updated changelog
125 | yarn standard-version --dry-run
126 | ```
127 |
128 | #### npm publish
129 |
130 | ```sh
131 | # check what files are gonna be published to npm
132 | yarn release:preflight
133 | ```
134 |
135 | ## License
136 |
137 | By contributing your code to the codeparticle-formal GitHub Repository, you agree to license your contribution under the MIT license.
138 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ## Bug report
6 |
7 | - @codeparticle/codeparticle-formal version: _x.x.x_ ()
8 | - Affected browsers (and versions): _IE 10_
9 |
10 | ### Current behaviour
11 |
12 |
13 |
14 | ```ts
15 | // put code here
16 | ```
17 |
18 |
19 |
20 | [issue demo](https://codesandbox.io/)
21 |
22 | ### Expected behaviour
23 |
24 | _Please explain how you'd expect it to behave._
25 |
26 |
27 |
28 |
29 |
30 | ## Feature request
31 |
32 | ### Use case(s)
33 |
34 | _Explain the rationale for this feature._
35 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | _If there is a linked issue, mention it here._
2 |
3 | - [ ] Bug
4 | - [ ] Feature
5 |
6 | ## Requirements
7 |
8 | - [ ] Read the [contribution guidelines](./.github/CONTRIBUTING.md).
9 | - [ ] Wrote tests.
10 | - [ ] Updated docs and upgrade instructions, if necessary.
11 |
12 | ## Rationale
13 |
14 | _Why is this PR necessary?_
15 |
16 | ## Implementation
17 |
18 | _Why have you implemented it this way? Did you try any other methods?_
19 |
20 | ## Open questions
21 |
22 | _Are there any open questions about this implementation that need answers?_
23 |
24 | ## Other
25 |
26 | _Is there anything else we should know? Delete this section if you don't need it._
27 |
28 | ## Tasks
29 |
30 | _List any tasks you need to do here, if any. Delete this section if you don't need it._
31 |
32 | - [ ] _Example task._
33 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: '/'
5 | schedule:
6 | interval: weekly
7 | time: '13:00'
8 | open-pull-requests-limit: 10
9 | ignore:
10 | - dependency-name: y18n
11 | versions:
12 | - 4.0.1
13 | - dependency-name: husky
14 | versions:
15 | - 4.3.8
16 | - 5.0.9
17 | - 5.1.0
18 | - 5.1.3
19 | - 5.2.0
20 | - dependency-name: '@commitlint/cli'
21 | versions:
22 | - 11.0.0
23 | - 12.0.1
24 | - dependency-name: '@commitlint/config-conventional'
25 | versions:
26 | - 11.0.0
27 | - 12.0.1
28 | - dependency-name: '@types/prettier'
29 | versions:
30 | - 2.1.6
31 | - 2.2.0
32 | - 2.2.1
33 | - 2.2.2
34 | - dependency-name: tslib
35 | versions:
36 | - 2.1.0
37 |
--------------------------------------------------------------------------------
/.github/workflows/automate-PRs.yml:
--------------------------------------------------------------------------------
1 | name: automate-PRs
2 | # The script auto merges PRs
3 |
4 | on:
5 | pull_request:
6 | branches:
7 | - master
8 | types:
9 | - opened
10 | - synchronize
11 |
12 | jobs:
13 | automate-PRs:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node-version: [14.x]
18 | os: [ubuntu-latest, windows-latest, macos-latest]
19 | steps:
20 | - uses: actions/checkout@v2
21 | - uses: actions/setup-node@v2
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 |
25 | # Specific for the project
26 | - run: chmod 755 ./ci/ci-test-commands.sh
27 | - run: ./ci/ci-test-commands.sh
28 |
29 | automate-PRs-merge:
30 | needs: automate-PRs
31 | runs-on: ubuntu-latest
32 | permissions:
33 | pull-requests: write
34 | contents: write
35 | steps:
36 | - uses: fastify/github-action-merge-dependabot@v3
37 | with:
38 | github-token: ${{ secrets.GITHUB_TOKEN }}
39 | target: minor
40 |
--------------------------------------------------------------------------------
/.github/workflows/close-stale-issues-and-PRs.yml:
--------------------------------------------------------------------------------
1 | name: close-stale-issues-and-PRs
2 | # The script closes stale issues and PRs
3 |
4 | on:
5 | schedule:
6 | - cron: '0 1 * * *' # Every day at 1:00
7 |
8 | jobs:
9 | close-stale-issues-and-PRs:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/stale@v4
13 | with:
14 | stale-issue-message: 'Inactive issue'
15 | stale-pr-message: 'Inactive PR'
16 | days-before-stale: 2
17 | days-before-close: 1
18 | only-labels: 'dependencies'
19 | delete-branch: true
20 |
--------------------------------------------------------------------------------
/.github/workflows/gha-publish-to-npm.yml:
--------------------------------------------------------------------------------
1 | name: gha-publish-to-npm
2 | # The script publishes to NPM
3 |
4 | on:
5 | schedule:
6 | - cron: '0 1 15 * *' # On 15th, every month at 1:00
7 |
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - run: git config --global user.name 'gha-publish-to-npm'
14 | - run: git config --global user.email 'gha-publish-to-npm@users.noreply.github.com'
15 | - run: npm version patch
16 |
17 | # Specific for the project
18 | - run: chmod 755 ./ci/ci-test-commands.sh
19 | - run: ./ci/ci-test-commands.sh
20 | - run: git push
21 | - uses: JS-DevTools/npm-publish@v1
22 | with:
23 | token: ${{ secrets.NPM_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | # The script runs tests Specific for the project
3 |
4 | on:
5 | push:
6 | branches:
7 | - master
8 |
9 | jobs:
10 | test:
11 | name: test
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node-version: [14.x]
16 | os: [ubuntu-latest, windows-latest, macos-latest]
17 | steps:
18 | - uses: actions/checkout@v2
19 | - uses: actions/setup-node@v2
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 |
23 | # Specific for the project
24 | - run: chmod 755 ./ci/ci-test-commands.sh
25 | - run: ./ci/ci-test-commands.sh
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .idea
3 | .DS_Store
4 | .cache
5 | node_modules
6 |
7 | coverage
8 | lib
9 | esm5
10 | lib-esm
11 | esm2015
12 | lib-fesm
13 | fesm
14 | umd
15 | bundles
16 | typings
17 | docs
18 | dist
19 | .history
20 |
21 | ## this is generated by `npm pack`
22 | *.tgz
23 | package
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn lint-staged
5 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn style && yarn test -- --bail --onlyChanged
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 | *.log
3 |
4 | # tools configs
5 | **/tsconfig.json
6 | tsconfig.*.json
7 | **/webpack.config.js
8 | **/jest.config.js
9 | **/prettier.config.js
10 |
11 | # build scripts
12 | config/
13 | scripts/
14 |
15 | # Test files
16 | **/*.spec.js
17 | **/*.test.js
18 | **/*.test.d.ts
19 | **/*.spec.d.ts
20 | __tests__
21 | coverage
22 |
23 | # Sources
24 | node_modules
25 | src
26 | docs
27 | examples
28 |
29 | ## this is generated by `npm pack`
30 | *.tgz
31 | package
32 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "arrowParens": "always",
4 | "semi": false,
5 | "bracketSpacing": true,
6 | "trailingComma": "es5",
7 | "printWidth": 80
8 | }
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 'lts/*'
4 | cache: yarn
5 | notifications:
6 | email: false
7 | env:
8 | - CI=true
9 | before_install:
10 | - npm config set scripts-prepend-node-path true
11 | - npm i -g yarn
12 | install:
13 | - yarn
14 | script:
15 | - yarn build
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [2.0.1](https://github.com/codeparticle/formal/compare/v2.0.0...v2.0.1) (2022-04-13)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * **v2.0.1:** fix exports in package.json ([56e1fcb](https://github.com/codeparticle/formal/commit/56e1fcb36e778caf9e0fec470866c4d6f91591f4))
11 |
12 | ## [2.0.0](https://github.com/codeparticle/formal/compare/v1.0.5...v2.0.0) (2022-04-13)
13 |
14 |
15 | ### Features
16 |
17 | * **update-build:** update build to use tsup, rm rollup, update eslint ([8f24093](https://github.com/codeparticle/formal/commit/8f24093c62285c0f7c384d6a51826627f09b050d))
18 |
19 |
20 | ### Bug Fixes
21 |
22 | * **github:** fix builds due to missing jest types ([70c5eac](https://github.com/codeparticle/formal/commit/70c5eacdfe3af4b138437565507cc9a070035c3b))
23 | * **github:** revert back to yarn from pnpm ([c3a173f](https://github.com/codeparticle/formal/commit/c3a173f46da604727946b0486b2a262a7d75221c))
24 |
25 | ### [1.0.5](https://github.com/codeparticle/formal/compare/v1.0.4...v1.0.5) (2022-01-20)
26 |
27 | ### [1.0.4](https://github.com/codeparticle/formal/compare/v1.0.3...v1.0.4) (2021-12-10)
28 |
29 | ### Bug Fixes
30 |
31 | - **deps:** remove git add from lint-staged script ([d03e998](https://github.com/codeparticle/formal/commit/d03e998194f07799d5b5a5495b17f9f7b12dbb1b))
32 | - **rules:** fix object rules throwing when objects are null ([2b16deb](https://github.com/codeparticle/formal/commit/2b16deb9ab3be5cae6c00c4b19d65d91f4519c6d))
33 |
34 | ### [1.0.3](https://github.com/codeparticle/formal/compare/v1.0.2...v1.0.3) (2021-06-30)
35 |
36 | ### Bug Fixes
37 |
38 | - **scripts:** fix npm publish script ([3c598c9](https://github.com/codeparticle/formal/commit/3c598c933c0813b6f1f1e77d6bb8a026e9821506))
39 |
40 | ### [1.0.2](https://github.com/codeparticle/formal/compare/v1.0.1...v1.0.2) (2021-06-23)
41 |
42 | ### [1.0.1](https://github.com/codeparticle/formal/compare/v1.0.0...v1.0.1) (2021-05-24)
43 |
44 | ### [0.4.7](https://github.com/codeparticle/formal/compare/v0.4.2...v0.4.7) (2021-05-24)
45 |
46 | ### Features
47 |
48 | - **deps:** update deps ([8382dc9](https://github.com/codeparticle/formal/commit/8382dc99570e3405b3cd0482d0de8f853098e523))
49 |
50 | ### Bug Fixes
51 |
52 | - **errors:** fix errors accumulation ([724b424](https://github.com/codeparticle/formal/commit/724b4241ba9111080273e30e3638fb91c7363e3d))
53 | - **fail:** do not map transformations over values in Fail ([433f29d](https://github.com/codeparticle/formal/commit/433f29d4035f901bf229a8e452c893b86a0d28a0))
54 | - **rules:** update Rules with new interface, fix withMessage overwriting functions ([6ef04c6](https://github.com/codeparticle/formal/commit/6ef04c6d1772e486e7918a6cbb0dce1dd9c66ea4))
55 |
56 | ### [0.4.2](https://github.com/codeparticle/formal/compare/v0.4.1...v0.4.2) (2020-05-10)
57 |
58 | ## [0.4.0](https://github.com/codeparticle/formal/compare/v0.3.0...v0.4.0) (2020-02-25)
59 |
60 | ### Bug Fixes
61 |
62 | - **linter:** add markdown file ext to parser opts ([359a768](https://github.com/codeparticle/formal/commit/359a768))
63 | - **linter:** don't try to lint non-js ([f420c46](https://github.com/codeparticle/formal/commit/f420c46))
64 | - **tests:** rm dupe utils file, fix tests ([963a11c](https://github.com/codeparticle/formal/commit/963a11c))
65 | - **tests:** temp disable type checking for tests ([2625d95](https://github.com/codeparticle/formal/commit/2625d95))
66 | - **tests:** temp disable type checks in tests as they are incorrect ([fe5ef34](https://github.com/codeparticle/formal/commit/fe5ef34))
67 |
68 | ### Features
69 |
70 | - **build:** move tests and remove them from builds ([7972fa1](https://github.com/codeparticle/formal/commit/7972fa1))
71 | - **docs:** update docs ([d3c192b](https://github.com/codeparticle/formal/commit/d3c192b))
72 | - **rules:** add is-equal-to rule ([d66b82c](https://github.com/codeparticle/formal/commit/d66b82c))
73 | - **rules:** make getProp and hasProp variadic. ([20c0255](https://github.com/codeparticle/formal/commit/20c0255))
74 |
75 |
76 |
77 | # [0.3.0](https://github.com/codeparticle/formal/compare/v0.2.1...v0.3.0) (2019-07-23)
78 |
79 | ### Bug Fixes
80 |
81 | - **rules:** minor bugfixes, message normalization across rules. ([5b332ef](https://github.com/codeparticle/formal/commit/5b332ef))
82 |
83 | ### Features
84 |
85 | - **docs:** update the docs to include an example of validateObject ([7056a05](https://github.com/codeparticle/formal/commit/7056a05))
86 | - **validateObject:** added validateObject util. ([e9b413f](https://github.com/codeparticle/formal/commit/e9b413f))
87 |
88 |
89 |
90 | ## [0.2.1](https://github.com/codeparticle/formal/compare/v0.2.0...v0.2.1) (2019-06-10)
91 |
92 |
93 |
94 | # [0.2.0](https://github.com/codeparticle/formal/compare/v0.1.0...v0.2.0) (2019-06-10)
95 |
96 | ### Bug Fixes
97 |
98 | - **builtins:** clarity and edge-case fixes ([f3e6a2c](https://github.com/codeparticle/formal/commit/f3e6a2c))
99 |
100 | ### Features
101 |
102 | - **builtins:** added isNonEmptyString, matchesRegex, and isValidEmail. ([bb0447a](https://github.com/codeparticle/formal/commit/bb0447a))
103 |
104 |
105 |
106 | # [0.1.0](https://github.com/codeparticle/formal/compare/v0.0.6...v0.1.0) (2019-06-08)
107 |
108 | ### Features
109 |
110 | - **rules:** updated createRule to accept a transform prop to change the value of the output validation ([e09ce49](https://github.com/codeparticle/formal/commit/e09ce49))
111 | - **tests:** updated tests to include new Rule class. ([67dfac2](https://github.com/codeparticle/formal/commit/67dfac2))
112 | - **withMessage:** added withMessage function to overwrite messages in built-in checks. ([7e49a92](https://github.com/codeparticle/formal/commit/7e49a92))
113 | - updated README ([b799586](https://github.com/codeparticle/formal/commit/b799586))
114 |
115 |
116 |
117 | ## [0.0.7](https://github.com/codeparticle/formal/compare/v0.0.6...v0.0.7) (2019-06-08)
118 |
119 |
120 |
121 | ## [0.0.6](https://github.com/codeparticle/formal/compare/v0.0.5...v0.0.6) (2019-06-04)
122 |
123 |
124 |
125 | ## [0.0.5](https://github.com/codeparticle/formal/compare/v0.0.4...v0.0.5) (2019-06-04)
126 |
127 |
128 |
129 | ## [0.0.4](https://github.com/codeparticle/formal/compare/v0.0.3...v0.0.4) (2019-06-04)
130 |
131 |
132 |
133 | ## [0.0.3](https://github.com/codeparticle/formal/compare/v0.0.2...v0.0.3) (2019-06-03)
134 |
135 |
136 |
137 | ## [0.0.2](https://github.com/codeparticle/formal/compare/v0.0.1...v0.0.2) (2019-06-03)
138 |
139 |
140 |
141 | ## 0.0.1 (2019-06-03)
142 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Nick Krause
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @codeparticle/formal
2 |
3 | > A simple data and form validation library with a wide range of possibilities.
4 |
5 | ### 🔧 Installation
6 |
7 | ```sh
8 | yarn add @codeparticle/formal
9 | ```
10 |
11 | ## Usage
12 |
13 | Formal makes it simple to validate arbitrary data with whatever rules you'd like. It also guarantees that when things fail, you know why, and have a plan B in place.
14 |
15 | Here's a playful example:
16 |
17 | ```ts
18 | import { Validator } from '@codeparticle/formal'
19 | import { isString, minLength } from '@codeparticle/lib/rules'
20 |
21 | const techPitchValidator = Validator.of(isString, minLength(32), maxLength(256))
22 |
23 | const validPitch = "It's like your favorite social network, but for dogs"
24 | const invalidPitch = "It's an AI to rule us all"
25 |
26 | const validResult = techPitchValidator
27 | .validate(validPitch)
28 | .map((str) => str.toUpperCase())
29 | // returns Success("IT'S LIKE YOUR FAVORITE SOCIAL NETWORK, BUT FOR DOGS") - maps can change successfully checked values
30 |
31 | const invalidResult = techPitchValidator
32 | .validate(invalidPitch)
33 | .map((str) => str.toUpperCase())
34 | // returns Fail('Must be at least 32 characters') - maps have no effect on failures
35 |
36 | validResult.then({
37 | onSuccess: () => alert("We love it kid. Here's 5 million."),
38 | onFail: (errs) => console.log(errs), // can also simply be console.log
39 | })
40 |
41 | invalidResult.then({
42 | onSuccess: () => alert("We love it kid. Here's 5 million."),
43 | onFail: (errs) => errs.map(console.log), // 'Must be at least 32 characters'
44 | })
45 | ```
46 |
47 | ### validateObject
48 |
49 | For validating forms or large API responses, formal exposes a `validateObject` utility. Here's an example
50 | of a form with a required name and email field.
51 |
52 | ```ts
53 | import { rules, validateObject } from '@codeparticle/formal'
54 |
55 | const { isNonEmptyString, isValidEmail } = rules
56 |
57 | const validateForm = validateObject({
58 | name: [isNonEmptyString],
59 | email: [isNonEmptyString, isValidEmail],
60 | })
61 |
62 | const formValues = {
63 | name: 'hello',
64 | email: '',
65 | }
66 |
67 | validateForm(formValues)
68 | ```
69 |
70 | calling this will return a schema like this:
71 |
72 | ```ts
73 | {
74 | hasErrors: true,
75 | errors: {
76 | email: ['Value must not be empty.', 'Must be a valid email']
77 | },
78 | values: {
79 | name: 'hello',
80 | email: ''
81 | }
82 | }
83 | ```
84 |
85 | formal is flexible to your style, and exposes a `pipeValidators` function for writing validations in a more functional way. It condenses multiple checks into a function that encloses a value into a `Success` or `Fail` container.
86 |
87 | Once run, these validation containers are supplied with an `isSuccess` property for use in filters,with the ability to reach for the internally held `value`. While not recommended for control flow, it's useful in cases where you're running validation over a long list of items, as well as in writing test cases.
88 |
89 | ```ts
90 | import { pipeValidators, rules } from '@codeparticle/formal'
91 | const { isString, minLength } = rules
92 |
93 | // ...
94 | const isLongString = pipeValidators([isString, minLength(50)])
95 |
96 | values
97 | .filter((val) => isLongString(val).isSuccess)
98 | .map((container) => container.value)
99 | .map((str) => console.log(str))
100 |
101 | // this technique can make testing a breeze.
102 |
103 | // here, we want all of our objects to have a common property; maybe a required prop in a react component.
104 |
105 | // while a bit contrived here, this method makes tests over complex, nested objects
106 | // or other data simple to do.
107 |
108 | const testObjects = [
109 | { required: 'present' },
110 | { required: 'present' },
111 | { required: 'wrong' },
112 | ]
113 |
114 | const check = pipeValidators([isObject, hasProp('required')])
115 |
116 | for (const test of testObjects) {
117 | expect(check(test).isSuccess).toBe(true) // passes
118 |
119 | expect(check(test).value).toBe('present') // fails
120 | }
121 | ```
122 |
123 | ### Built-in Validators
124 |
125 | Formal has a small set of useful checks built in to validate simple data.
126 |
127 | ```ts
128 | import {
129 | // Basic validations that take no arguments when used in Validator.of //
130 | isString,
131 | isNumber,
132 | isObject,
133 | isArray,
134 | isNonEmptyString,
135 | isNonEmptyObject,
136 | isNonEmptyArray,
137 | isValidEmail, // Only validates format, not ownership.
138 |
139 | /**
140 | * checks if something is equal to another value.
141 | * if you're using this in validateObject to check that two fields match,
142 | * use it as [values => isEqualTo(values['otherFormField'])]
143 | */
144 | isEqualTo,
145 |
146 | // Validations that take arguments before being supplied to Validator or pipeValidators() //
147 |
148 |
149 | // Check that a value matches a given regex. matchesRegex(/[A-Z]) || matchesRegex(RegExp('[A-Z]'))
150 | matchesRegex,
151 | // Check that a string is x-characters long. Takes a value for the minimum. `minLength(10)`
152 | minLength,
153 | // Check that a string is no more than x-characters long. Takes a value for the maximum. `maxLength(50)`
154 | maxLength,
155 | // Check that a number is less than a certain amount. Takes a value for the minimum. `lessThan(50)`
156 | lessThan,
157 | // Check that a number is less than a certain amount. Takes a value for the maximum. `greaterThan(50)`
158 | greaterThan,
159 | // check that an object has a certain property. Takes a drilldown path supplied as strings. `hasProp('fieldName', 'subfield')`
160 | hasProp,
161 | // check that an object has a property at the given drilldown path, then return a Success object with its value. `getProp('fieldName', 'subfield')`
162 | getProp
163 | } from '@codeparticle/formal';
164 | ...
165 | ```
166 |
167 | ### Customizing built-in or existing checks
168 |
169 | Sometimes, the messages included with built-in or existing checks need to be modified after the fact. Formal supports this via the `withMessage` function.
170 |
171 | `withMessage` creates a new copy of the rule, so don't worry about accidentally overwriting something important when using it. Like `createRule`, you can supply a string, or a function that returns a string.
172 |
173 | ```ts
174 | import { withMessage, rules } from '@codeparticle/formal'
175 |
176 | const withAdminFormErrorMessage = withMessage(
177 | `Admins must enter an administrator ID.`
178 | )
179 | const withUserFormErrorMessage = withMessage(
180 | `Users must enter their first and last name to sign up`
181 | )
182 |
183 | const withInternationalizedErrorMessage = withMessage(
184 | intl.formatMessage('form.error.message')
185 | )
186 |
187 | const withNewMessageFn = withMessage(
188 | (badValue) => `${badValue} is invalid for this field.`
189 | )
190 |
191 | const adminFormFieldCheck = withAdminFormErrorMessage(rules.isNonEmptyString)
192 |
193 | const userFormFieldCheck = withUserFormErrorMessage(rules.isNonEmptyString)
194 |
195 | const internationalizedFieldCheck = withInternationalizedErrorMessage(
196 | rules.isString
197 | )
198 |
199 | const customMessageFunctionFieldCheck = withNewMessageFn(rules.isNonEmptyString)
200 | ```
201 |
202 | ## Creating your own validators
203 |
204 | Formal gives you the ability to create your own rules to supply to `Validator`. There are two ways to do so.
205 |
206 | Using `createRule` is a quick way to check things that _don't change the value that comes out_.
207 |
208 | `createRule` takes two (required) options - a condition function, and a message. The message can be a string, or it can be a function that returns a string, in case you'd like to tailor your messages.
209 |
210 | ```ts
211 | import { createRule } from '@codeparticle/formal'
212 | export const containsMatchingString = (match) =>
213 | createRule({
214 | condition: (str) => str.includes(match),
215 | message: (failedStr) => `Value ${failedStr} does not include today's date`,
216 | })
217 | ```
218 |
219 | createRule also allows more customized checks through an optional parameter called `transform`
220 | that allows for a transformation of the value _before_ it's handed off to the next validation check.
221 |
222 | ```ts
223 | import { createRule } from '../rule'
224 | import { hasProp } from './has-prop'
225 |
226 | // below is the actual source code of the built-in getProp function.
227 | export const getProp = (property) =>
228 | createRule({
229 | condition: (obj) => hasProp(property).check(obj).isSuccess,
230 | message: `Property '${property}' does not exist`,
231 | // transform the object to the value of the successfully found property
232 | // before handing off to the next check / function.
233 | transform: (obj) => obj[property],
234 | })
235 | ```
236 |
237 | ## Usage with Typescript
238 |
239 | This package exports TS type definitions in the `/types` folder.
240 |
241 | ```ts
242 | import {
243 | // interfaces for the Success and Fail classes
244 | Success,
245 | Fail,
246 | // used to specify the argument structure to `createValidator`
247 | CustomValidationOptions,
248 | // options supplied to Validator.then
249 | ValidationActions,
250 | // alias for `Success | Fail` in cases where both are needed.
251 | ValidationM,
252 | // aliases for a single or array of validation rules provided to Validator.of
253 | ValidationRule,
254 | // alias for a function that returns a Success or Failure.
255 | ValidationCheck,
256 | ValidationRuleSet,
257 | // interface for the Validator class
258 | Validator,
259 | } from '@codeparticle/formal/types'
260 | ```
261 |
262 | [](https://travis-ci.org/codeparticle/codeparticle-formal)
263 | [](https://www.npmjs.com/package/@codeparticle/codeparticle-formal)
264 | 
265 | [](https://github.com/conventional-changelog/standard-version)
266 | [](https://github.com/prettier/prettier)
267 | [](https://conventionalcommits.org)
268 |
269 | ---
270 |
271 | ### 🥂 License
272 |
273 | [MIT](./LICENSE.md) as always
274 |
--------------------------------------------------------------------------------
/ci/ci-test-commands.sh:
--------------------------------------------------------------------------------
1 | # The script commands Specific for the project
2 | yarn install
3 | yarn run lint
4 | yarn run build
5 | yarn run test
6 | yarn run test:ci
7 | yarn run verify
--------------------------------------------------------------------------------
/config/commitlint.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@commitlint/core').Config}
3 | */
4 | const config = { extends: ['@commitlint/config-conventional'] }
5 |
6 | module.exports = config
7 |
--------------------------------------------------------------------------------
/config/global.d.ts:
--------------------------------------------------------------------------------
1 | // ============================
2 | // extend existing types
3 | // ============================
4 | declare namespace NodeJS {
5 | interface ProcessEnv {
6 | CI: 'true' | 'false'
7 | }
8 | }
9 |
10 | // ============================
11 | // Rollup plugins without types
12 | // ============================
13 | type RollupPluginImpl =
14 | import('rollup').PluginImpl
15 |
16 | declare module 'rollup-plugin-json' {
17 | export interface Options {
18 | /**
19 | * All JSON files will be parsed by default, but you can also specifically include/exclude files
20 | */
21 | include?: string | string[]
22 | exclude?: string | string[]
23 | /**
24 | * for tree-shaking, properties will be declared as variables, using either `var` or `const`
25 | * @default false
26 | */
27 | preferConst?: boolean
28 | /**
29 | * specify indentation for the generated default export — defaults to '\t'
30 | * @default '\t'
31 | */
32 | indent?: string
33 | }
34 | const plugin: RollupPluginImpl
35 | export default plugin
36 | }
37 | declare module 'rollup-plugin-sourcemaps' {
38 | const plugin: RollupPluginImpl
39 | export default plugin
40 | }
41 | declare module 'rollup-plugin-node-resolve' {
42 | const plugin: RollupPluginImpl
43 | export default plugin
44 | }
45 | declare module 'rollup-plugin-commonjs' {
46 | const plugin: RollupPluginImpl
47 | export default plugin
48 | }
49 | declare module 'rollup-plugin-replace' {
50 | const plugin: RollupPluginImpl
51 | export default plugin
52 | }
53 | declare module 'rollup-plugin-uglify' {
54 | const uglify: RollupPluginImpl
55 | export { uglify }
56 | }
57 | declare module 'rollup-plugin-terser' {
58 | const terser: RollupPluginImpl
59 | export { terser }
60 | }
61 |
62 | // =====================∫
63 | // missing library types
64 | // =====================
65 |
66 | // ts-jest types require 'babel__core'
67 | declare module 'babel__core' {
68 | interface TransformOptions {}
69 | }
70 |
71 | declare module '@commitlint/core' {
72 | interface Config {
73 | extends: string[]
74 | }
75 | }
76 | declare module 'sort-object-keys' {
77 | const sortPackageJson: (
78 | object: T,
79 | sortWith?: (...args: any[]) => any
80 | ) => T
81 | export = sortPackageJson
82 | }
83 |
84 | declare module 'replace-in-file' {
85 | interface Options {
86 | files: string | string[]
87 | from: Array
88 | to: string | string[]
89 | ignore: string | string[]
90 | dry: boolean
91 | encoding: string
92 | disableGlobs: boolean
93 | allowEmptyPaths: boolean
94 | }
95 |
96 | interface API {
97 | (options: Partial): string[]
98 | sync(options: Partial): string[]
99 | }
100 |
101 | const api: API
102 | export = api
103 | }
104 |
105 | declare module 'gzip-size' {
106 | type Options = import('zlib').ZlibOptions
107 | type Input = string | Buffer
108 |
109 | function gzipSize(input: Input, options?: Options): Promise
110 | namespace gzipSize {
111 | function sync(input: Input, options?: Options): number
112 | function stream(options?: Options): import('stream').PassThrough
113 | function file(path: string, options?: Options): Promise
114 | function fileSync(path: string, options?: Options): number
115 | }
116 |
117 | export = gzipSize
118 | }
119 |
120 | declare module 'brotli-size' {
121 | type Input = string | Buffer
122 |
123 | namespace brotliSize {
124 | function sync(input: Input): number
125 | function stream(): import('stream').PassThrough
126 | }
127 |
128 | function brotliSize(input: Input): Promise
129 |
130 | export = brotliSize
131 | }
132 |
133 | declare module 'pretty-bytes' {
134 | type Options = {
135 | /**
136 | * @default false
137 | */
138 | signed: boolean
139 | /**
140 | * @default false
141 | */
142 | locale: string | boolean
143 | }
144 |
145 | function prettyBytes(input: number, options?: Partial): string
146 |
147 | export = prettyBytes
148 | }
149 |
--------------------------------------------------------------------------------
/config/helpers.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | camelCaseToDash,
3 | dashToCamelCase,
4 | toUpperCase,
5 | pascalCase,
6 | normalizePackageName,
7 | getOutputFileName,
8 | }
9 |
10 | /**
11 | *
12 | * @param {string} myStr
13 | */
14 | function camelCaseToDash(myStr) {
15 | return myStr.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
16 | }
17 |
18 | /**
19 | *
20 | * @param {string} myStr
21 | */
22 | function dashToCamelCase(myStr) {
23 | return myStr.replace(/-([a-z])/g, (g) => g[1].toUpperCase())
24 | }
25 |
26 | /**
27 | *
28 | * @param {string} myStr
29 | */
30 | function toUpperCase(myStr) {
31 | return `${myStr.charAt(0).toUpperCase()}${myStr.substr(1)}`
32 | }
33 |
34 | /**
35 | *
36 | * @param {string} myStr
37 | */
38 | function pascalCase(myStr) {
39 | return toUpperCase(dashToCamelCase(myStr))
40 | }
41 |
42 | /**
43 | *
44 | * @param {string} rawPackageName
45 | */
46 | function normalizePackageName(rawPackageName) {
47 | const scopeEnd = rawPackageName.indexOf('/') + 1
48 |
49 | return rawPackageName.substring(scopeEnd)
50 | }
51 |
52 | /**
53 | *
54 | * @param {string} fileName
55 | * @param {boolean?} isProd
56 | */
57 | function getOutputFileName(fileName, isProd = false) {
58 | return isProd ? fileName.replace(/\.js$/, '.min.js') : fileName
59 | }
60 |
--------------------------------------------------------------------------------
/config/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | preset: 'ts-jest',
3 | rootDir: '..',
4 | testMatch: [
5 | '/src/**/__tests__/**/*.ts?(x)',
6 | '/src/**/?(*.)+(spec|test).ts?(x)',
7 | ],
8 | testPathIgnorePatterns: ['dist'],
9 | coverageThreshold: {
10 | global: {
11 | branches: 80,
12 | functions: 80,
13 | lines: 80,
14 | statements: 80,
15 | },
16 | },
17 | setupFiles: ['/config/setup-tests.js'],
18 | watchPlugins: [
19 | 'jest-watch-typeahead/filename',
20 | 'jest-watch-typeahead/testname',
21 | ],
22 | }
23 |
24 | module.exports = config
25 |
--------------------------------------------------------------------------------
/config/setup-tests.js:
--------------------------------------------------------------------------------
1 | // add here any code that you wanna execute before tests like
2 | // - polyfills
3 | // - some custom code
4 | // for more docs check see https://jestjs.io/docs/en/configuration.html#setupfiles-array
5 |
--------------------------------------------------------------------------------
/config/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2017",
5 | "module": "commonjs",
6 | "allowJs": true,
7 | "checkJs": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "noEmit": true,
11 | "importHelpers": false
12 | },
13 | "include": [".", "../jest.config.js"]
14 | }
15 |
--------------------------------------------------------------------------------
/config/types.d.ts:
--------------------------------------------------------------------------------
1 | // ===== JEST ====
2 | export type TsJestConfig = import('ts-jest/dist/types').TsJestConfig
3 | export type JestConfig = Partial
4 |
5 | // ==== PRETTIER ====
6 | export type PrettierConfig = import('prettier').Options
7 |
8 | // ==== ROLLUP ====
9 | export type RollupConfig = import('rollup').InputOptions & {
10 | output:
11 | | import('rollup').OutputOptions
12 | | Array
13 | }
14 | export type RollupPlugin = import('rollup').Plugin
15 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | preset: `ts-jest`,
3 | rootDir: `.`,
4 | testMatch: [`/test/**/*.ts?(x)`],
5 | testPathIgnorePatterns: [`dist`],
6 | coverageThreshold: {
7 | global: {
8 | branches: 80,
9 | functions: 80,
10 | lines: 80,
11 | statements: 80,
12 | },
13 | },
14 | setupFiles: [`/config/setup-tests.js`],
15 | watchPlugins: [
16 | `jest-watch-typeahead/filename`,
17 | `jest-watch-typeahead/testname`,
18 | ],
19 | }
20 |
21 | // eslint-disable-next-line unicorn/prefer-module
22 | module.exports = config
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@codeparticle/formal",
3 | "version": "2.0.40",
4 | "description": "A <2kb library for validating data of any kind",
5 | "keywords": [
6 | "validation",
7 | "formal",
8 | "form",
9 | "prop-types",
10 | "codeparticle"
11 | ],
12 | "main": "./index.js",
13 | "module": "./index.mjs",
14 | "es2015": "./index.mjs",
15 | "typings": "./index.d.ts",
16 | "sideEffects": false,
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/codeparticle/formal"
20 | },
21 | "author": "Nick Krause ",
22 | "license": "MIT",
23 | "engines": {
24 | "node": ">=12.0.0"
25 | },
26 | "scripts": {
27 | "cleanup": "shx rm -rf",
28 | "prebuild": "yarn cleanup && yarn verify",
29 | "build": "tsup",
30 | "postbuild": "node scripts/copy && yarn size",
31 | "docs": "typedoc -p . --theme minimal --target 'es6' --excludeNotExported --excludePrivate --ignoreCompilerErrors --exclude \"**/src/**/__tests__/*.*\" --out docs src/",
32 | "test": "jest -c ./jest.config.js",
33 | "test:watch": "yarn test -- --watch",
34 | "test:coverage": "yarn test -- --coverage",
35 | "test:ci": "yarn test -- --ci",
36 | "validate-js": "tsc -p ./config && tsc -p ./scripts",
37 | "verify": "yarn validate-js && yarn style && yarn test:ci",
38 | "commit": "git-cz",
39 | "lint": "eslint 'src/**/*.ts' 'test/**/*.ts'",
40 | "style": "yarn lint -- --fix",
41 | "prerelease": "yarn build",
42 | "release": "standard-version",
43 | "postrelease": "node scripts/copy && node scripts/build && yarn release:github && npm run release:npm",
44 | "release:github": "git push --no-verify --follow-tags origin master",
45 | "release:npm": "npm publish ./dist --access public",
46 | "release:preflight": "npm pack ./dist --dry-run",
47 | "size": "node scripts/file-size ./dist/index.js"
48 | },
49 | "config": {
50 | "commitizen": {
51 | "path": "cz-conventional-changelog"
52 | }
53 | },
54 | "lint-staged": {
55 | "**/*.{ts,tsx,js,jsx}": [
56 | "yarn style"
57 | ]
58 | },
59 | "dependencies": {
60 | "case": "^1.6.3"
61 | },
62 | "devDependencies": {
63 | "@commitlint/cli": "^17.0.0",
64 | "@commitlint/config-conventional": "^17.0.0",
65 | "@types/jest": "27.5.0",
66 | "@types/node": "^18.0.0",
67 | "@types/prettier": "^2.6.0",
68 | "@types/webpack-config-utils": "2.3.4",
69 | "@typescript-eslint/eslint-plugin": "^5.19.0",
70 | "@typescript-eslint/parser": "5.62.0",
71 | "brotli-size": "^4.0.0",
72 | "commitizen": "^4.2.4",
73 | "cz-conventional-changelog": "^3.3.0",
74 | "eslint": "^8.13.0",
75 | "eslint-config-adjunct": "^4.11.1",
76 | "eslint-config-prettier": "^8.5.0",
77 | "eslint-plugin-array-func": "^3.1.7",
78 | "eslint-plugin-eslint-comments": "^3.2.0",
79 | "eslint-plugin-html": "^7.1.0",
80 | "eslint-plugin-jest": "^27.0.1",
81 | "eslint-plugin-jest-async": "^1.0.3",
82 | "eslint-plugin-json": "^3.1.0",
83 | "eslint-plugin-markdown": "^3.0.0",
84 | "eslint-plugin-no-constructor-bind": "^2.0.4",
85 | "eslint-plugin-no-secrets": "^0.9.1",
86 | "eslint-plugin-no-unsanitized": "^4.0.1",
87 | "eslint-plugin-no-use-extend-native": "^0.5.0",
88 | "eslint-plugin-optimize-regex": "^1.2.1",
89 | "eslint-plugin-promise": "^6.0.0",
90 | "eslint-plugin-scanjs-rules": "^0.2.1",
91 | "eslint-plugin-security": "^1.4.0",
92 | "eslint-plugin-simple-import-sort": "^8.0.0",
93 | "eslint-plugin-sonarjs": "^0.23.0",
94 | "eslint-plugin-switch-case": "^1.1.2",
95 | "eslint-plugin-unicorn": "^45.0.1",
96 | "gzip-size": "^6.0.0",
97 | "husky": "^8.0.2",
98 | "jest": "^27.5.1",
99 | "jest-watch-typeahead": "^2.0.0",
100 | "kleur": "^4.1.4",
101 | "lint-staged": "^13.0.0",
102 | "prettier": "^2.6.2",
103 | "pretty-bytes": "^5.6.0",
104 | "shx": "^0.3.4",
105 | "standard-version": "^9.3.2",
106 | "ts-jest": "^27.1.4",
107 | "tslib": "^2.3.1",
108 | "tsup": "^6.1.0",
109 | "typedoc": "^0.24.1",
110 | "typescript": "^4.6.3",
111 | "webpack-config-utils": "^2.3.1"
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file only purpose is to execute any build related tasks
3 | */
4 |
5 | const { resolve, normalize } = require(`path`)
6 | const { readFileSync, writeFileSync } = require(`fs`)
7 | const pkg = require(`../package.json`)
8 |
9 | const ROOT = resolve(__dirname, `..`)
10 | const DIST = resolve(ROOT, `dist`)
11 | const TYPES_ROOT_FILE = resolve(DIST, normalize(pkg.typings))
12 |
13 | main()
14 |
15 | function main() {
16 | writeDtsHeader()
17 | }
18 |
19 | function writeDtsHeader() {
20 | const dtsHeader = getDtsHeader(
21 | pkg.name,
22 | pkg.version,
23 | pkg.author,
24 | pkg.repository.url,
25 | pkg.devDependencies.typescript
26 | )
27 |
28 | prependFileSync(TYPES_ROOT_FILE, dtsHeader)
29 | }
30 |
31 | /**
32 | *
33 | * @param {string} pkgName
34 | * @param {string} version
35 | * @param {string} author
36 | * @param {string} repoUrl
37 | * @param {string} tsVersion
38 | */
39 | function getDtsHeader(pkgName, version, author, repoUrl, tsVersion) {
40 | const extractUserName = repoUrl.match(/\.com\/([\w-]+)\/\w+/i)
41 | const githubUserUrl = extractUserName ? extractUserName[1] : `Unknown`
42 |
43 | return `
44 | // Type definitions for ${pkgName} ${version}
45 | // Project: ${repoUrl}
46 | // Definitions by: ${author}
47 | // Definitions: ${repoUrl}
48 | // TypeScript Version: ${tsVersion}
49 | `.replace(/^\s+/gm, ``)
50 | }
51 |
52 | /**
53 | *
54 | * @param {string} path
55 | * @param {string | Blob} data
56 | */
57 | function prependFileSync(path, data) {
58 | const existingFileContent = readFileSync(path, {
59 | encoding: `utf8`,
60 | })
61 | const newFileContent = [data, existingFileContent].join(`\n`)
62 |
63 | writeFileSync(path, newFileContent, {
64 | flag: `w+`,
65 | encoding: `utf8`,
66 | })
67 | }
68 |
--------------------------------------------------------------------------------
/scripts/copy.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file only purpose is to copy files before npm publish and strip churn/security sensitive metadata from package.json
3 | *
4 | * **NOTE:**
5 | * 👉 This file should not use any 3rd party dependency
6 | */
7 | const { writeFileSync, copyFileSync, statSync } = require(`fs`)
8 | const { resolve, basename } = require(`path`)
9 | const packageJson = require(`../package.json`)
10 |
11 | main()
12 |
13 | function main() {
14 | const projectRoot = resolve(__dirname, `..`)
15 | const distPath = resolve(projectRoot, `dist`)
16 | const distPackageJson = createDistPackageJson(packageJson)
17 |
18 | const cpFiles = [`README.md`, `CHANGELOG.md`, `LICENSE.md`, `.npmignore`].map(
19 | (file) => resolve(projectRoot, file)
20 | )
21 |
22 | cp(cpFiles, distPath)
23 |
24 | writeFileSync(resolve(distPath, `package.json`), distPackageJson)
25 | }
26 |
27 | /**
28 | *
29 | * @param {string[]|string} source
30 | * @param {string} target
31 | */
32 | function cp(source, target) {
33 | const isDir = statSync(target).isDirectory()
34 |
35 | if (isDir) {
36 | if (!Array.isArray(source)) {
37 | throw new Error(
38 | `if is directory you need to provide source as an array`
39 | )
40 | }
41 |
42 | source.forEach((file) =>
43 | copyFileSync(file, resolve(target, basename(file)))
44 | )
45 |
46 | return
47 | }
48 |
49 | copyFileSync(/** @type {string} */ (source), target)
50 | }
51 |
52 | /**
53 | * @param {typeof packageJson} packageConfig
54 | * @return {string}
55 | */
56 | function createDistPackageJson(packageConfig) {
57 | const {
58 | devDependencies,
59 | scripts,
60 | engines,
61 | config,
62 | 'lint-staged': lintStaged,
63 | ...distPackageJson
64 | } = packageConfig
65 |
66 | return JSON.stringify(distPackageJson, null, 2)
67 | }
68 |
--------------------------------------------------------------------------------
/scripts/file-size.js:
--------------------------------------------------------------------------------
1 | const { basename, normalize } = require(`path`)
2 | const { readFile: readFileCb } = require(`fs`)
3 | const { promisify } = require(`util`)
4 | const readFile = promisify(readFileCb)
5 |
6 | const kolor = require(`kleur`)
7 | const prettyBytes = require(`pretty-bytes`)
8 | const brotliSize = require(`brotli-size`)
9 | const gzipSize = require(`gzip-size`)
10 | const { log } = console
11 | const pkg = require(`../package.json`)
12 |
13 | main()
14 |
15 | async function main() {
16 | const args = process.argv.splice(2)
17 | const filePaths = [...args.map(normalize)]
18 | const fileMetadata = await Promise.all(
19 | filePaths.map(async (filePath) => {
20 | return {
21 | path: filePath,
22 | blob: await readFile(filePath, {
23 | encoding: `utf8`,
24 | }),
25 | }
26 | })
27 | )
28 |
29 | const output = await Promise.all(
30 | fileMetadata.map((metadata) => getSizeInfo(metadata.blob, metadata.path))
31 | )
32 |
33 | log(getFormatedOutput(pkg.name, output))
34 | }
35 |
36 | /**
37 | *
38 | * @param {string} pkgName
39 | * @param {string[]} filesOutput
40 | */
41 | function getFormatedOutput(pkgName, filesOutput) {
42 | const MAGIC_INDENTATION = 3
43 | const WHITE_SPACE = ` `.repeat(MAGIC_INDENTATION)
44 |
45 | return (
46 | kolor.blue(`${pkgName} bundle sizes: 📦`) +
47 | `\n${WHITE_SPACE}` +
48 | readFile.name +
49 | filesOutput.join(`\n${WHITE_SPACE}`)
50 | )
51 | }
52 |
53 | /**
54 | *
55 | * @param {number} size
56 | * @param {string} filename
57 | * @param {'br' | 'gz'} type
58 | * @param {boolean} raw
59 | */
60 | function formatSize(size, filename, type, raw) {
61 | const pretty = raw ? `${size} B` : prettyBytes(size)
62 | const color = size < 5000 ? `green` : size > 40000 ? `red` : `yellow`
63 | const MAGIC_INDENTATION = type === `br` ? 13 : 10
64 |
65 | return `${` `.repeat(MAGIC_INDENTATION - pretty.length)}${kolor[color](
66 | pretty
67 | )}: ${kolor.white(basename(filename))}.${type}`
68 | }
69 |
70 | /**
71 | *
72 | * @param {string} code
73 | * @param {string} filename
74 | * @param {boolean} [raw=false]
75 | */
76 | async function getSizeInfo(code, filename, raw = false) {
77 | const isRaw = raw || code.length < 5000
78 | const gzip = formatSize(await gzipSize(code), filename, `gz`, isRaw)
79 | const brotli = formatSize(brotliSize.sync(code), filename, `br`, isRaw)
80 |
81 | return gzip + `\n` + brotli
82 | }
83 |
--------------------------------------------------------------------------------
/scripts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../config/tsconfig.json",
3 | "compilerOptions": {},
4 | "include": [".", "../config/global.d.ts"]
5 | }
6 |
--------------------------------------------------------------------------------
/src/fail.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file class to describe validation failures.
3 | * @name Failure.js
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 | import { checkIsValidationM } from './internal/utils'
8 | import { ValidationActions, ValidationM } from './types'
9 |
10 | class Fail implements Fail {
11 | static of(value: any, errors: string[] = []) {
12 | return new Fail(value, errors)
13 | }
14 |
15 | value: any = null
16 | errors: string[] = []
17 | isSuccess = false
18 |
19 | /**
20 | * This constructor allows us to coalesce errors from multiple failed checks into
21 | * one, used when we fold out of this context.
22 | *
23 | */
24 | constructor(value: any, errors: string[] = []) {
25 | this.value = value
26 | this.errors = errors
27 | }
28 |
29 | /**
30 | * The map function for Fail preserves the value without mapping anything over it, and accumulates errors on the side.
31 | */
32 | map() {
33 | return new Fail(this.value, this.errors)
34 | }
35 |
36 | /**
37 | * The .chain() for Fail takes another Success or Fail, then combines the results.
38 | */
39 | chain(validationM: (v: any, e?: string[]) => ValidationM): ValidationM {
40 | try {
41 | const result = validationM(this.value, this.errors)
42 |
43 | checkIsValidationM(result)
44 |
45 | return new Fail(result.value, [...this.errors, ...(result?.errors ?? [])])
46 | } catch (error) {
47 | console.error(error.message)
48 | console.error(error.stack)
49 | }
50 | }
51 |
52 | /**
53 | * After checks are done, this method is used to extract the value
54 | * with a function that operates on it.
55 | */
56 | fold({ onFail }: ValidationActions) {
57 | return onFail(this.errors)
58 | }
59 | }
60 |
61 | export { Fail }
62 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Fail } from './fail'
2 | export { pipeValidators, validateObject } from './internal/utils'
3 | export { createRule, withMessage } from './rule'
4 | export * from './rules'
5 | export * as rules from './rules'
6 | export { Success } from './success'
7 | export * from './types'
8 | export * from './utils'
9 | export { Validator } from './validation'
--------------------------------------------------------------------------------
/src/internal/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Util functions for validators
3 | * @name utils.js
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 | import { ValidationCheck, ValidationM, ValidationRuleset } from '../types'
8 |
9 | class ValidationError extends Error {}
10 |
11 | /**
12 | * id function - returns whatever its input is.
13 | * @param {Any} x - Any param supplied to this function.
14 | * @returns {Any} The param supplied to this function.
15 | */
16 | const id = (x: any) => x
17 |
18 | const pipeValidators: (
19 | fns: ValidationRuleset,
20 | values?: any
21 | ) => ValidationCheck = (fns, values) => (value: any) => {
22 | const [first, ...rest] = fns
23 |
24 | const firstCheck = (
25 | typeof first === `function` ? first(values) : first
26 | ).check(value)
27 |
28 | // starting with the first function that returns a monad,
29 | // we chain through the rest of the functions
30 | // in order to combine them all into a single check.
31 | return rest.reduce(
32 | (prevM, nextM) =>
33 | // eslint-disable-next-line @typescript-eslint/unbound-method
34 | prevM.chain(
35 | typeof nextM === `function` ? nextM(values)[`check`] : nextM.check,
36 | ),
37 | firstCheck,
38 | )
39 | }
40 |
41 | /**
42 | * returnConstant is a convenience method that can be used as an argument to functions that require a function,
43 | * but ultimately do nothing but return a primitive value.
44 | * @param {Any} x - Primitive value to be returned.
45 | */
46 | function returnConstant(x: T): () => T {
47 | return () => x
48 | }
49 |
50 | /**
51 | * a customized error message to catch incorrect types in Validator.using
52 | * @param {Function} fn - The incorrect function supplied to using().
53 | * @returns {String} - A tailored error message that attempts to pinpoint the error.
54 | */
55 | const validationErrorMessage = (fn: (v: any) => any): string => {
56 | return `
57 | Chaining validation only works if every function has a .chain() method that takes a Success or Fail object.
58 | Check the type of ${fn.constructor.name} - was it written using createRule?
59 | `
60 | }
61 |
62 | /**
63 | * Function that checks whether the supplied validator is, itself, valid.
64 | * @param {Function} validator - Parameter description.
65 | * @throws {Exception Type} Exception description.
66 | */
67 | const checkIsValidationM = (validator: ValidationM): void => {
68 | if (
69 | !(
70 | validator.hasOwnProperty(`value`) && validator.hasOwnProperty(`isSuccess`)
71 | )
72 | ) {
73 | throw new ValidationError(validationErrorMessage(validator as any))
74 | }
75 | }
76 |
77 | /**
78 | * Function that takes an object with field names and rules, then applies those rules to the fields
79 | * of another object with the same field names. Great for validation of entire objects at once, like
80 | * forms or API responses.
81 | */
82 |
83 | const validateObject =
84 | >(fieldRules: Rules) =>
85 | >(
86 | values: Vals,
87 | ): {
88 | values: Vals
89 | hasErrors: boolean
90 | errors: Partial>
91 | } => {
92 | const errors = Object.keys(fieldRules).reduce((errs, fieldName) => {
93 | if (!(fieldName in values)) {
94 | throw new Error(
95 | `Field ${fieldName} is not in the object being validated`,
96 | )
97 | }
98 |
99 | const applyFieldChecks: ValidationCheck = pipeValidators(
100 | fieldRules[fieldName],
101 | values,
102 | )
103 | const checkResults: ValidationM = applyFieldChecks(
104 | values[fieldName],
105 | values,
106 | )
107 |
108 | if (!checkResults.isSuccess) {
109 | errs[fieldName] = checkResults.errors
110 | }
111 |
112 | return errs
113 | }, {})
114 |
115 | return {
116 | values,
117 | hasErrors: Object.keys(errors).length > 0,
118 | errors,
119 | }
120 | }
121 |
122 | export {
123 | checkIsValidationM,
124 | id,
125 | pipeValidators,
126 | returnConstant,
127 | validateObject,
128 | ValidationError,
129 | }
130 |
--------------------------------------------------------------------------------
/src/rule.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Rule class for use with createRule and withMessage
3 | * @author Nick Krause
4 | */
5 | import { Fail } from './fail'
6 | import { Success } from './success'
7 | import {
8 | CustomValidatorOptions,
9 | ValidationCheck,
10 | ValidationRule,
11 | } from './types'
12 |
13 | class Rule implements ValidationRule {
14 | message: string | ((v: any) => string)
15 | opts: CustomValidatorOptions
16 |
17 | constructor(opts: CustomValidatorOptions) {
18 | this.opts = opts
19 | }
20 |
21 | check: ValidationCheck = (value) => {
22 | if (this.opts.condition(value)) {
23 | // Rule and createRule accept an optional function (delayed) parameter
24 | // called transform that allows us to change the value
25 | // before it is passed onto the next check.
26 | if (this.opts.transform) {
27 | return Success.of(this.opts.transform(value))
28 | }
29 |
30 | return Success.of(value)
31 | } else {
32 | return typeof this.opts.message === `function`
33 | ? Fail.of(value, [this.opts.message(value)].flat())
34 | : Fail.of(value, [this.opts.message].flat())
35 | }
36 | }
37 | }
38 |
39 | /**
40 | * Exposed interface to create a rule
41 | */
42 | const createRule: (opts: CustomValidatorOptions) => ValidationRule = (opts) => {
43 | return new Rule(opts)
44 | }
45 |
46 | /**
47 | * Method to overwrite the message of a rule.
48 | * Useful if you'd like to use a built-in, but want to change the ultimate message
49 | * that comes out of a failed check.
50 | * @param message
51 | */
52 | const withMessage =
53 | (message: string | ((v?: any) => string)) => (rule: ValidationRule) =>
54 | new Rule({ ...rule.opts, message })
55 |
56 | export { createRule, Rule, withMessage }
57 |
--------------------------------------------------------------------------------
/src/rules/get-prop.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Check to see if an object has a property, then return the value of that property.
3 | * @name getProp.js
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 |
8 | import { createRule } from '../rule'
9 |
10 | export const getProp = (...properties: string[]) => {
11 | let prop = ``
12 | let currentObjectPath = {}
13 |
14 | return createRule({
15 | condition: (obj) => {
16 | currentObjectPath = obj
17 |
18 | for (const property of properties) {
19 | if (currentObjectPath.hasOwnProperty(property)) {
20 | prop += `.${property}`
21 | currentObjectPath = currentObjectPath[property]
22 | } else {
23 | return false
24 | }
25 | }
26 |
27 | return true
28 | },
29 | message: () => {
30 | const path = properties.join(`.`)
31 |
32 | return `Object does not include property ${path.slice(
33 | prop.length - 1,
34 | )} at path .${path}`
35 | },
36 |
37 | transform: (obj) => properties.reduce((acc, key) => acc[key], obj),
38 | })
39 | }
40 |
--------------------------------------------------------------------------------
/src/rules/greater-than.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Check to ensure that a number is greater than a supplied amount.
3 | * @name greater-than.js
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 |
8 | import { createRule } from '../rule'
9 |
10 | export const greaterThan = (min) =>
11 | createRule({
12 | condition: (num) => num > min,
13 | message: (num) => `${num} must be greater than ${min}`,
14 | })
15 |
--------------------------------------------------------------------------------
/src/rules/has-prop.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Checks an object to make sure that it has a certain property.
3 | * @name hasProp.js
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 | import { createRule } from '../rule'
8 |
9 | export const hasProp = (...properties: string[]) => {
10 | let prop = ``
11 | let currentObjectPath = {}
12 |
13 | return createRule({
14 | condition: (obj) => {
15 | currentObjectPath = obj
16 |
17 | for (const property of properties) {
18 | if (currentObjectPath.hasOwnProperty(property)) {
19 | prop = property
20 | currentObjectPath = currentObjectPath[property]
21 | } else {
22 | return false
23 | }
24 | }
25 |
26 | return true
27 | },
28 | message: (obj) => {
29 | // list out the keys that we have
30 | // to help us spot where things may have gone wrong prior
31 | const keys = Object.keys(obj).toString().replace(`,`, `,\n`)
32 |
33 | return `Object containing properties ${keys} does not include ${prop}${
34 | properties.length > 1 ? ` at path ${properties.join(`.`)}` : ``
35 | }`
36 | },
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/src/rules/index.ts:
--------------------------------------------------------------------------------
1 | export { getProp } from './get-prop'
2 | export { greaterThan } from './greater-than'
3 | export { hasProp } from './has-prop'
4 | export { isArray } from './is-array'
5 | export { isEqualTo } from './is-equal-to'
6 | export { isNonEmptyArray } from './is-non-empty-array'
7 | export { isNonEmptyObject } from './is-non-empty-object'
8 | export { isNonEmptyString } from './is-non-empty-string'
9 | export { isNumber } from './is-number'
10 | export { isObject } from './is-object'
11 | export { isString } from './is-string'
12 | export { isValidEmail } from './is-valid-email'
13 | export { lessThan } from './less-than'
14 | export { matchesRegex } from './matches-regex'
15 | export { maxLength } from './max-length'
16 | export { minLength } from './min-length'
17 |
--------------------------------------------------------------------------------
/src/rules/is-array.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Check to see if a value is an array
3 | * @name isArray.js
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 | import { createRule } from '../rule'
8 |
9 | export const isArray = createRule({
10 | condition: (maybeArr) => Array.isArray(maybeArr),
11 | message: (notArray) =>
12 | `Value must be an array, but has type ${typeof notArray}`,
13 | })
14 |
--------------------------------------------------------------------------------
/src/rules/is-equal-to.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Validator to ensure that this value is equivalent to another that may or may not be available
3 | * at time of definition.
4 | */
5 |
6 | import { createRule } from '../rule'
7 |
8 | const isEqualTo = (value) =>
9 | createRule({
10 | condition: (val) =>
11 | typeof value === `function` ? val === value() : val === value,
12 | message: () => `Values must be equal`,
13 | })
14 |
15 | export { isEqualTo }
16 |
--------------------------------------------------------------------------------
/src/rules/is-non-empty-array.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Check if an array is actually an array, and also not empty.
3 | * @author Nick Krause
4 | * @license MIT
5 | */
6 | import { createRule } from '../rule'
7 |
8 | export const isNonEmptyArray = createRule({
9 | condition: (arr) => Array.isArray(arr) && arr.length > 0,
10 | message: (val) =>
11 | Array.isArray(val)
12 | ? `Array must not be empty`
13 | : `No array values found in ${val}`,
14 | })
15 |
--------------------------------------------------------------------------------
/src/rules/is-non-empty-object.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file check that an object has keys
3 | * @author Nick Krause
4 | * @license MIT
5 | */
6 | import { createRule } from '../rule'
7 |
8 | export const isNonEmptyObject = createRule({
9 | // Reflect.ownKeys is used because
10 | // this check should not fail if we only have
11 | // properties that are non-enumerable
12 | // like 'Symbol' or properties defined by Object.defineProperty where
13 | // 'enumerable' is set to false.
14 | condition: (obj) =>
15 | !!obj && typeof obj === `object` && Reflect.ownKeys(obj).length > 0,
16 | message: (obj) =>
17 | typeof obj === `object`
18 | ? `Object must not be empty`
19 | : `Value ${obj} is not an object`,
20 | })
21 |
--------------------------------------------------------------------------------
/src/rules/is-non-empty-string.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file ensures that a value is a non-empty string.
3 | * @author Nick Krause
4 | */
5 |
6 | import { createRule } from '../rule'
7 |
8 | export const isNonEmptyString = createRule({
9 | condition: (maybeStr) =>
10 | typeof maybeStr === `string` && maybeStr.length > 0,
11 | message: `Value must be a non-empty string`,
12 | })
13 |
--------------------------------------------------------------------------------
/src/rules/is-number.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Check that something is a valid number
3 | * @name isNumber.js
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 |
8 | import { createRule } from '../rule'
9 |
10 | export const isNumber = createRule({
11 | condition: (maybeNum) =>
12 | typeof maybeNum === `number` && !Number.isNaN(maybeNum),
13 | message: (notNum) => `Value ${notNum} is not a number`,
14 | })
15 |
--------------------------------------------------------------------------------
/src/rules/is-object.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Check to ensure that the supplied value is an object.
3 | * @name isObject.js
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 | import { createRule } from '../rule'
8 |
9 | export const isObject = createRule({
10 | condition: (obj) =>
11 | !!obj &&
12 | typeof obj === `object` &&
13 | !Array.isArray(obj) &&
14 | obj === Object(obj),
15 | message: (notObj) => `Value must be an object, but has type ${typeof notObj}`,
16 | })
17 |
--------------------------------------------------------------------------------
/src/rules/is-string.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Check to see if a value is typeof string
3 | * @author Nick Krause
4 | * @license MIT
5 | */
6 |
7 | import { createRule } from '../rule'
8 |
9 | export const isString = createRule({
10 | condition: (maybeStr) => typeof maybeStr === `string`,
11 | message: () => `Value is not a string`,
12 | })
13 |
--------------------------------------------------------------------------------
/src/rules/is-valid-email.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file checks to make sure that an email address has a valid format
3 | * @author Nick Krause
4 | */
5 |
6 | import { createRule } from '../rule'
7 |
8 | /**
9 | * Check to ensure that an email address is in a valid format.
10 | * Does NOT check that the email is a valid, in-use address.
11 | */
12 | export const isValidEmail = createRule({
13 | // credit to https://tylermcginnis.com/validate-email-address-javascript/ for this regex
14 | condition: (str) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str),
15 | message: `Must be a valid email address`,
16 | })
17 |
--------------------------------------------------------------------------------
/src/rules/less-than.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | * @name lessThan.js
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 | import { createRule } from '../rule'
8 |
9 | /**
10 | * Rule to validate a number that must be less than some amount.
11 | * @param {Number} max - Maximum value.
12 | */
13 | export const lessThan = (max) =>
14 | createRule({
15 | condition: (num) => num < max,
16 | message: (num) => `${num} must be less than ${max}`,
17 | })
18 |
--------------------------------------------------------------------------------
/src/rules/matches-regex.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file rule to verify that a given string matches a certain regex
3 | * @author Nick Krause
4 | */
5 |
6 | import { createRule } from '../rule'
7 |
8 | /**
9 | * Check to see that a value matches a given regex.
10 | * Will fail for non-string values.
11 | * @param regex {RegExp} validation regex
12 | */
13 | export const matchesRegex = (regex: RegExp) =>
14 | createRule({
15 | condition: (maybeStr: string) => regex.test(maybeStr),
16 | message: (val) => `Value ${val} does not match regular expression ${regex}`,
17 | })
18 |
--------------------------------------------------------------------------------
/src/rules/max-length.ts:
--------------------------------------------------------------------------------
1 | import { createRule } from '../rule'
2 |
3 | /**
4 | * Function to ensure that a string is below or equal to a certain length.
5 | * @param {Number} maxLength - Max length of the string.
6 | */
7 | export const maxLength = (max) =>
8 | createRule({
9 | condition: (str) => Boolean(str) && str.length <= max,
10 | message: `Must be shorter than ${max + 1} characters`,
11 | })
12 |
--------------------------------------------------------------------------------
/src/rules/min-length.ts:
--------------------------------------------------------------------------------
1 | import { createRule } from '../rule'
2 |
3 | export const minLength = (length) =>
4 | createRule({
5 | condition: (str) => str.length >= length,
6 | message: `Must be at least ${length} characters`,
7 | })
8 |
--------------------------------------------------------------------------------
/src/success.ts:
--------------------------------------------------------------------------------
1 | import { checkIsValidationM } from './internal/utils'
2 | import { ValidationActions, ValidationM } from './types'
3 |
4 | class Success implements Success {
5 | static of(value: any): Success {
6 | return new Success(value)
7 | }
8 |
9 | isSuccess = true
10 | errors = []
11 | value: any = null
12 |
13 | constructor(value: any) {
14 | this.value = value
15 | this.errors = []
16 | }
17 |
18 | map(fn: (v: any) => any) {
19 | return new Success(fn(this.value))
20 | }
21 |
22 | chain(validationFn: (v: any) => ValidationM): ValidationM {
23 | try {
24 | const result = validationFn(this.value)
25 |
26 | checkIsValidationM(result)
27 |
28 | return result
29 | } catch (error) {
30 | console.error(error.message)
31 | console.error(error.stack)
32 | }
33 | }
34 |
35 | fold({ onSuccess }: ValidationActions) {
36 | return onSuccess(this.value)
37 | }
38 | }
39 |
40 | export { Success }
41 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface Validator {
2 | rules: ValidationRuleset
3 | result: ValidationM | null
4 | of(rules: ValidationRuleset): Validator
5 | validate(value: any): Validator
6 | then(opts: ValidationActions): any
7 | }
8 |
9 | export interface ValidationRule {
10 | message: string | ((v?: any) => string)
11 | opts: CustomValidatorOptions
12 | check(v: any): ValidationM
13 | }
14 |
15 | export interface ValidationActions {
16 | onSuccess: (val: any) => any
17 | onFail: (errs: string[]) => any
18 | }
19 |
20 | export interface CustomValidatorOptions {
21 | condition: (v: any) => boolean
22 | message: string | ((v: any) => string)
23 | // function to ensure delayed execution
24 | // so that we don't get any accidental
25 | // access errors in the middle of
26 | // a long list of checks.
27 | // value works the same way as condition,
28 | // using the value being checked as an argument.
29 | transform?: (v: any) => any
30 | }
31 |
32 | export type ValidationM = Success | Fail
33 |
34 | export interface Fail {
35 | value: any
36 | errors: string[]
37 | isSuccess: boolean
38 | map(fn: (value: any) => any): Fail
39 | chain(fn: (value: any, errors) => ValidationM): Fail
40 | fold(opts: ValidationActions): any
41 | }
42 |
43 | export interface Success {
44 | value: any
45 | // ghost type - errors will never exist on a Success
46 | errors: string[]
47 | isSuccess: boolean
48 | map(fn: (value: any) => any): Success
49 | chain(fn: (value: any) => ValidationM): ValidationM
50 | fold(opts: ValidationActions): any
51 | }
52 |
53 | export type ValidationCheck = (v: any, vs?: any) => ValidationM
54 | export type ValidationRuleset = (
55 | | ValidationRule
56 | | ((v: any) => ValidationRule)
57 | )[]
58 | export type ValidationErrorMessage = (fn: (v: any) => any) => string
59 |
--------------------------------------------------------------------------------
/src/utils/compose.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * compose a set of functions over an argument
3 | *
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 |
8 | const compose =
9 | (...fns: Array<(v: T) => any>) =>
10 | (x: T) =>
11 | fns.reduceRight((v, f) => f(v), x)
12 |
13 | export { compose }
14 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { compose } from './compose'
2 | export { pipe } from './pipe'
3 |
--------------------------------------------------------------------------------
/src/utils/pipe.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Pipe a set of functions over an argument, starting at the leftmost argument
3 | *
4 | * @author Nick Krause
5 | * @license MIT
6 | */
7 |
8 | const pipe =
9 | (...fns: Array<(v: T) => any>) =>
10 | (x: T) =>
11 | fns.reduce((v, f) => f(v), x)
12 |
13 | export { pipe }
14 |
--------------------------------------------------------------------------------
/src/validation.ts:
--------------------------------------------------------------------------------
1 | import { pipeValidators, ValidationError } from './internal/utils'
2 | import { ValidationActions, ValidationM, ValidationRuleset } from './types'
3 |
4 | /**
5 | * convenience method to generate a Success or Fail with a custom message (or message function),
6 | * based on the passing of a supplied condition
7 | *
8 | */
9 |
10 | class Validator implements Validator {
11 | static of(...rules: ValidationRuleset) {
12 | return new Validator(...rules)
13 | }
14 |
15 | rules: ValidationRuleset = []
16 | result: ValidationM | null = null
17 |
18 | constructor(...rules: ValidationRuleset) {
19 | this.rules = rules.flat()
20 | }
21 |
22 | /**
23 | * Run all of the supplied validators against the value that Validator() was supplied with,
24 | * then operate on the results based on whether this succeeded or failed.
25 | * @param {Object} opts
26 | * @property {Function} onSuccess
27 | * @property {Function} onFail
28 | *
29 | * @returns {Any} result
30 | */
31 | validate(value: any): Validator {
32 | this.result = pipeValidators(this.rules)(value)
33 |
34 | return this
35 | }
36 |
37 | // eslint-disable-next-line unicorn/no-thenable
38 | then(opts: ValidationActions) {
39 | if (this.result) {
40 | return this.result.fold(opts)
41 | } else {
42 | throw new ValidationError(
43 | `Validator failed to run - did you supply rules and use validate() first?`
44 | )
45 | }
46 | }
47 | }
48 |
49 | export { Validator }
50 |
--------------------------------------------------------------------------------
/test/Builtins.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Tests for built-in validations.
3 | */
4 |
5 | import {
6 | getProp,
7 | greaterThan,
8 | hasProp,
9 | isArray,
10 | isEqualTo,
11 | isNonEmptyArray,
12 | isNonEmptyObject,
13 | isNonEmptyString,
14 | isNumber,
15 | isObject,
16 | isString,
17 | isValidEmail,
18 | lessThan,
19 | matchesRegex,
20 | maxLength,
21 | minLength,
22 | } from '../src'
23 | import { pipeValidators } from '../src/internal/utils'
24 |
25 | describe(`@codeparticle/formal built-in validations`, () => {
26 | const testStrings = [
27 | ``,
28 | `0`,
29 | `1`,
30 | `str`,
31 | `100`,
32 | `@@%/&&`,
33 | `Templated with a fancy ${2}`,
34 | `"escaped"`,
35 | ]
36 | const testNumbers = [
37 | NaN,
38 | 0,
39 | 1,
40 | 1492,
41 | 1776,
42 | Infinity,
43 | // float
44 | 0.112_358,
45 | // scientific
46 | 1.3e9,
47 | // binary
48 | 0b001,
49 | // octal
50 | 0o012,
51 | // hex
52 | 0xff_ff_ff_ff,
53 | ]
54 |
55 | const emptyArray = []
56 | const emptyObject = {}
57 |
58 | const [goodEmail, badEmail] = [
59 | `here.therebee.dragons@wowmail.org`,
60 | `who@what@why.com`,
61 | ]
62 |
63 | const regexes = {
64 | testCase: /\d/g,
65 | good: `0123459678`,
66 | bad: `what did I do?`,
67 | }
68 |
69 | const testObject = {
70 | deeply: {
71 | nested: {
72 | value: `value`,
73 | },
74 | },
75 | }
76 | const testArray = [...testStrings, ...testNumbers, emptyArray, emptyObject]
77 |
78 | describe(`getProp`, () => {
79 | it(`drills down into nested objects`, () => {
80 | const testCase = pipeValidators([
81 | getProp(`deeply`),
82 | getProp(`nested`),
83 | getProp(`value`),
84 | ])(testObject)
85 |
86 | expect(testCase.isSuccess).toBe(true)
87 | expect(testCase.value).toBe(`value`)
88 | })
89 | it(`fails with bad values`, () => {
90 | const testCase = pipeValidators([
91 | getProp(`strangely`),
92 | getProp(`nested`),
93 | getProp(`corndogs`),
94 | ])(testObject)
95 |
96 | expect(testCase.isSuccess).toBe(false)
97 | })
98 | })
99 |
100 | describe(`greaterThan`, () => {
101 | const validation = greaterThan(5)
102 |
103 | it(`passes with good values`, () => {
104 | expect(validation.check(10).isSuccess).toBe(true)
105 | })
106 | it(`fails with bad values`, () => {
107 | expect(validation.check(5).isSuccess).toBe(false)
108 | })
109 | })
110 |
111 | describe(`hasProp`, () => {
112 | const validation = hasProp(`deeply`, `nested`, `value`)
113 |
114 | it(`passes with good values`, () => {
115 | expect(validation.check(testObject).isSuccess).toBe(true)
116 | })
117 | it(`fails with bad values`, () => {
118 | expect(validation.check(emptyObject).isSuccess).toBe(false)
119 | })
120 | it(`does not mutate the object`, () => {
121 | expect(testObject.deeply).toBeDefined()
122 | })
123 | })
124 |
125 | describe(`isArray`, () => {
126 | it(`passes with good values`, () => {
127 | expect(isArray.check(testArray).isSuccess).toBe(true)
128 | expect(isArray.check(emptyArray).isSuccess).toBe(true)
129 | })
130 | it(`fails with bad values`, () => {
131 | expect(isArray.check(10).isSuccess).toBe(false)
132 | })
133 | })
134 |
135 | describe(`isNonEmptyArray`, () => {
136 | it(`passes with good values`, () => {
137 | expect(isNonEmptyArray.check(testArray).isSuccess).toBe(true)
138 | })
139 | it(`fails with bad values`, () => {
140 | expect(isNonEmptyArray.check(emptyArray).isSuccess).toBe(false)
141 | })
142 | })
143 |
144 | describe(`isNonEmptyObject`, () => {
145 | it(`passes with good values`, () => {
146 | expect(isNonEmptyObject.check(testObject).isSuccess).toBe(true)
147 | })
148 | it(`fails with bad values`, () => {
149 | expect(isNonEmptyObject.check(emptyObject).isSuccess).toBe(false)
150 | })
151 | })
152 |
153 | describe(`isNonEmptyString`, () => {
154 | it(`passes with good values`, () => {
155 | expect(isNonEmptyString.check(`wow`).isSuccess).toBe(true)
156 | })
157 | it(`fails with bad values`, () => {
158 | expect(isNonEmptyString.check(``).isSuccess).toBe(false)
159 | })
160 | })
161 |
162 | describe(`isNumber`, () => {
163 | const [nan, ...others] = testNumbers
164 |
165 | it(`passes with good values`, () => {
166 | for (const num of others) {
167 | expect(isNumber.check(num).isSuccess).toBe(true)
168 | }
169 | })
170 | it(`fails with bad values`, () => {
171 | expect(isNumber.check(nan).isSuccess).toBe(false)
172 |
173 | for (const str of testStrings) {
174 | expect(isNumber.check(str).isSuccess).toBe(false)
175 | }
176 | })
177 | })
178 |
179 | describe(`isObject`, () => {
180 | it(`passes with good values`, () => {
181 | expect(isObject.check(emptyObject).isSuccess).toBe(true)
182 | expect(isObject.check(testObject).isSuccess).toBe(true)
183 | })
184 | it(`fails with bad values`, () => {
185 | expect(isObject.check([]).isSuccess).toBe(false)
186 | })
187 | })
188 |
189 | describe(`isString`, () => {
190 | it(`passes with good values`, () => {
191 | for (const str of testStrings) {
192 | expect(isString.check(str).isSuccess).toBe(true)
193 | }
194 | })
195 | it(`fails with bad values`, () => {
196 | for (const num of testNumbers) {
197 | expect(isString.check(num).isSuccess).toBe(false)
198 | }
199 |
200 | expect(isString.check(testObject).isSuccess).toBe(false)
201 | expect(isString.check(testArray).isSuccess).toBe(false)
202 | })
203 | })
204 |
205 | describe(`isValidEmail`, () => {
206 | it(`passes with good values`, () => {
207 | expect(isValidEmail.check(goodEmail).isSuccess).toBe(true)
208 | })
209 | it(`fails with bad values`, () => {
210 | expect(isValidEmail.check(badEmail).isSuccess).toBe(false)
211 | })
212 | })
213 |
214 | describe(`lessThan`, () => {
215 | it(`passes with good values`, () => {
216 | expect(lessThan(10).check(9).isSuccess).toBe(true)
217 | })
218 | it(`fails with bad values`, () => {
219 | expect(lessThan(10).check(11).isSuccess).toBe(false)
220 | })
221 | })
222 |
223 | describe(`matchesRegex`, () => {
224 | it(`passes with good values`, () => {
225 | expect(matchesRegex(regexes.testCase).check(regexes.good).isSuccess).toBe(
226 | true,
227 | )
228 | })
229 | it(`fails with bad values`, () => {
230 | expect(matchesRegex(regexes.testCase).check(regexes.bad).isSuccess).toBe(
231 | false,
232 | )
233 | })
234 | })
235 |
236 | describe(`maxLength`, () => {
237 | it(`passes with good values`, () => {
238 | expect(maxLength(5).check(`wow`).isSuccess).toBe(true)
239 | })
240 | it(`fails with bad values`, () => {
241 | expect(maxLength(1).check(`wow`).isSuccess).toBe(false)
242 | })
243 | })
244 |
245 | describe(`minLength`, () => {
246 | it(`passes with good values`, () => {
247 | expect(minLength(5).check(`wow cool`).isSuccess).toBe(true)
248 | })
249 | it(`fails with bad values`, () => {
250 | expect(minLength(5).check(`wow`).isSuccess).toBe(false)
251 | })
252 | })
253 |
254 | describe(`isEqualTo`, () => {
255 | it(`passes with good values`, () => {
256 | expect(isEqualTo(5).check(5).isSuccess).toBe(true)
257 | })
258 | it(`fails with bad values`, () => {
259 | expect(isEqualTo(0).check(5).isSuccess).toBe(false)
260 | })
261 | })
262 | })
263 |
--------------------------------------------------------------------------------
/test/Validation.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Unit tests for @codeparticle/formal
3 | */
4 | import { id, validateObject } from '../src/internal/utils'
5 | import { createRule, withMessage } from '../src/rule'
6 | import {
7 | getProp,
8 | isEqualTo,
9 | isNonEmptyString,
10 | isNumber,
11 | isObject,
12 | isString,
13 | isValidEmail,
14 | } from '../src/rules'
15 | import { Validator } from '../src/validation'
16 |
17 | describe(`Validation`, () => {
18 | const testObject = {
19 | nested: {
20 | property: {
21 | num: 2,
22 | },
23 | },
24 | }
25 |
26 | const testOpts = {
27 | onSuccess: id,
28 | onFail: id,
29 | }
30 |
31 | it(`Takes a list of validators made with createValidator and checks a value accordingly`, () => {
32 | const testValue = Validator.of(
33 | isObject,
34 | getProp(`nested`, `property`),
35 | getProp(`num`),
36 | isNumber
37 | ).validate(testObject)
38 |
39 | const folded = testValue.then({
40 | onSuccess: id,
41 | onFail: id,
42 | })
43 |
44 | expect(testValue?.result?.isSuccess).toBe(true)
45 | expect(folded).toBe(2)
46 | })
47 |
48 | it(`Collects errors when multiple conditions fail`, () => {
49 | const failedObject = Validator.of(
50 | isObject,
51 | getProp(`nested`, `property`, `fail`)
52 | ).validate(testObject)
53 |
54 | const failedString = Validator.of(isString).validate(2)
55 | const failedNumber = Validator.of(isNumber).validate(`wow`)
56 |
57 | expect(failedObject?.result?.isSuccess).toBe(false)
58 | expect(failedString?.result?.isSuccess).toBe(false)
59 | expect(failedNumber?.result?.isSuccess).toBe(false)
60 |
61 | expect(failedObject.then(testOpts)).toMatchObject([
62 | `Object does not include property .fail at path .nested.property.fail`,
63 | ])
64 |
65 | expect(failedString.then(testOpts)).toMatchObject([`Value is not a string`])
66 | expect(failedNumber.then(testOpts)).toMatchObject([
67 | `Value wow is not a number`,
68 | ])
69 | })
70 | })
71 |
72 | describe(`Rule`, () => {
73 | const customRule = createRule({
74 | condition: (val) => val > 3,
75 | message: `value must be greater than 3`,
76 | })
77 |
78 | it(`allows for initialization through createRule`, () => {
79 | expect(customRule.check(3).isSuccess).toBe(false)
80 | expect(customRule.check(5).isSuccess).toBe(true)
81 | expect(customRule.check(3).errors[0]).toBe(`value must be greater than 3`)
82 | })
83 |
84 | it(`allows for overwriting the message of an existing rule using withMessage`, () => {
85 | const overwriteText = withMessage(`Whoa`)
86 | const newRule = overwriteText(customRule)
87 | const overwriteFn = withMessage((d) => `Man, that's a crooked ${d}`)
88 |
89 | expect(newRule.check(2).isSuccess).toBe(false)
90 | expect(overwriteFn(newRule).check(2).errors[0]).toBe(
91 | `Man, that's a crooked 2`
92 | )
93 | expect(newRule.check(2).errors[0]).toBe(`Whoa`)
94 | })
95 | })
96 |
97 | describe(`validateObject`, () => {
98 | it(`can validate over an object, returning the original with an attached error object containing messages`, () => {
99 | const hasExactlyTwoNameObjects = createRule({
100 | condition: (arr) => arr.length === 2,
101 | message: `Exactly two names must exist`,
102 | })
103 |
104 | const daveIsSpelledBackwardsSomewhere = createRule({
105 | condition: (arr) => arr.some((o) => o.name === `evad`),
106 | message: `Dave is not spelled backwards somewhere`,
107 | })
108 |
109 | const validate = validateObject({
110 | // required fields
111 | firstName: [isNonEmptyString],
112 | lastName: [isNonEmptyString],
113 | email: [
114 | isNonEmptyString,
115 | isValidEmail,
116 | (values) => isEqualTo(values[`confirmationEmail`]),
117 | ],
118 | confirmationEmail: [
119 | (values) => isEqualTo(values[`email`]),
120 | isNonEmptyString,
121 | isValidEmail,
122 | ],
123 | // non-required fields
124 | city: [isString],
125 | names: [hasExactlyTwoNameObjects, daveIsSpelledBackwardsSomewhere],
126 | })
127 |
128 | const testFormFields = {
129 | empty: {},
130 | bad: {
131 | firstName: ``,
132 | lastName: 5,
133 | email: `notbad@email.com`,
134 | confirmationEmail: `bad`,
135 | city: 5,
136 | names: [
137 | {
138 | name: `what?`,
139 | },
140 | ],
141 | },
142 | good: {
143 | firstName: `captain`,
144 | lastName: `wow`,
145 | email: `savesday@everyday.net`,
146 | confirmationEmail: `savesday@everyday.net`,
147 | city: ``,
148 | names: [
149 | {
150 | name: `dave`,
151 | },
152 | {
153 | name: `evad`,
154 | },
155 | ],
156 | },
157 | }
158 |
159 | const invalid = validate(testFormFields.bad)
160 | const valid = validate(testFormFields.good)
161 |
162 | expect(invalid.hasErrors).toBe(true)
163 | expect(valid.hasErrors).toBe(false)
164 |
165 | expect(invalid.errors).toMatchObject({
166 | firstName: [`Value must be a non-empty string`],
167 | lastName: [`Value must be a non-empty string`],
168 | email: [`Values must be equal`],
169 | confirmationEmail: [
170 | `Values must be equal`,
171 | `Must be a valid email address`,
172 | ],
173 | city: [`Value is not a string`],
174 | names: [
175 | `Exactly two names must exist`,
176 | `Dave is not spelled backwards somewhere`,
177 | ],
178 | })
179 | })
180 | })
181 |
--------------------------------------------------------------------------------
/tsconfig.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true
6 | },
7 | "include": ["test", "./jest.config.js", "scripts", "./tsup.config.ts", "./.eslintrc.js"],
8 | "exclude": ["src"]
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "allowSyntheticDefaultImports": true,
5 | "baseUrl": ".",
6 | "declaration": true,
7 | "declarationDir": "dist/types",
8 | "declarationMap": true,
9 | "downlevelIteration": true,
10 | "esModuleInterop": true,
11 | "lib": ["dom", "esnext"],
12 | "module": "ESNext",
13 | "moduleResolution": "node",
14 | "noImplicitAny": false,
15 | "outDir": "dist/esm5",
16 | "resolveJsonModule": true,
17 | "sourceMap": true,
18 | "strict": false,
19 | "strictNullChecks": false,
20 | "stripInternal": true,
21 | "target": "ES2015"
22 | },
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 |
3 | const baseConfig: object = {
4 | bundle: true,
5 | clean: true,
6 | entry: [`src/**/*`],
7 | format: [`cjs`, `esm`],
8 | minify: false,
9 | name: `build`,
10 | platform: `browser`,
11 | skipNodeModulesBundle: false,
12 | sourcemap: true,
13 | splitting: true,
14 | silent: true,
15 | tsconfig: `./tsconfig.json`,
16 | dts: {
17 | resolve: true,
18 | },
19 | }
20 |
21 | export default defineConfig(baseConfig)
22 |
--------------------------------------------------------------------------------