Full Code of 5alidz/tiny-schema-validator for AI

master e7b8dd074178 cached
21 files
58.9 KB
16.6k tokens
61 symbols
1 requests
Download .txt
Repository: 5alidz/tiny-schema-validator
Branch: master
Commit: e7b8dd074178
Files: 21
Total size: 58.9 KB

Directory structure:
gitextract_l7zhuy_5/

├── .eslintignore
├── .eslintrc.js
├── .github/
│   └── workflows/
│       ├── main.yml
│       └── size.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package.json
├── src/
│   ├── constants.ts
│   ├── createErrors.ts
│   ├── createSchema.ts
│   ├── helpers.ts
│   ├── index.ts
│   ├── type-utils.ts
│   ├── utils.ts
│   └── validatorTypes.ts
├── test/
│   ├── index.test.ts
│   ├── tsconfig.json
│   └── validators.test.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintignore
================================================
dist/

================================================
FILE: .eslintrc.js
================================================
module.exports = {
  rules: {
    eqeqeq: 'off',
    'no-redeclare': 'off',
    '@typescript-eslint/no-redeclare': ['error', { ignoreDeclarationMerge: true }],
  },
};


================================================
FILE: .github/workflows/main.yml
================================================
name: CI
on: [push]
jobs:
  build:
    name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        node: ['10.x', '12.x', '14.x']
        os: [ubuntu-latest, windows-latest, macOS-latest]

    steps:
      - name: Checkout repo
        uses: actions/checkout@v2

      - name: Use Node ${{ matrix.node }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node }}

      - name: Install deps and build (with cache)
        uses: bahmutov/npm-install@v1

      - name: Lint
        run: yarn lint

      - name: Test
        run: yarn test --ci --coverage --maxWorkers=2

      - name: Build
        run: yarn build


================================================
FILE: .github/workflows/size.yml
================================================
name: size
on: [pull_request]
jobs:
  size:
    runs-on: ubuntu-latest
    env:
      CI_JOB_NUMBER: 1
    steps:
      - uses: actions/checkout@v1
      - uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
*.log
.DS_Store
node_modules
dist
coverage
todo

================================================
FILE: CHANGELOG.md
================================================
# Changelog

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.

### [5.0.3](https://github.com/5alidz/tiny-schema-validator/compare/v5.0.2...v5.0.3) (2021-08-31)


### Bug Fixes

* remove unused import ([8c1247a](https://github.com/5alidz/tiny-schema-validator/commit/8c1247a5be26a40209e584c7ea856db85a9896fd))
* report unknown keys found in data on validation ([#2](https://github.com/5alidz/tiny-schema-validator/issues/2)) ([08a4e5c](https://github.com/5alidz/tiny-schema-validator/commit/08a4e5cda8a01710d8964db9ab199fc94d6e23e1))
* test and fix unknown keys ([aff85e1](https://github.com/5alidz/tiny-schema-validator/commit/aff85e1d3177a2db7a9c1c5947820db0a3521329))

### [5.0.2](https://github.com/5alidz/tiny-schema-validator/compare/v5.0.1...v5.0.2) (2021-08-28)

### [5.0.1](https://github.com/5alidz/tiny-schema-validator/compare/v5.0.0...v5.0.1) (2021-08-28)

## [5.0.0](https://github.com/5alidz/tiny-schema-validator/compare/v4.0.0...v5.0.0) (2021-08-23)


### ⚠ BREAKING CHANGES

* - replace "traverse" with "source" so you can parse the schema however you want,
also to add "atomic" validations

* remove traverse, and replace it with direct validation ([513c682](https://github.com/5alidz/tiny-schema-validator/commit/513c682acd3d1845acbc73c44f52ed766e5039a5))

## [4.0.0](https://github.com/5alidz/tiny-schema-validator/compare/v3.1.0...v4.0.0) (2021-08-21)


### ⚠ BREAKING CHANGES

* when traversing the schema and the returned value is null, the result will contain
'invalid-type' message

### Features

* add constant and union validators ([7859392](https://github.com/5alidz/tiny-schema-validator/commit/7859392dffdda3bc7adeaa6fc5f6df6085b2d5a1))
* include the schema source on the schema api ([14c1ecf](https://github.com/5alidz/tiny-schema-validator/commit/14c1ecf2e8bf0e7e9429bb34c6c58e88199bb4c2))

## [3.1.0](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.5...v3.1.0) (2021-07-18)


### Features

* add latest version of typescript ([2e8a339](https://github.com/5alidz/tiny-schema-validator/commit/2e8a339d392bb229d52f79ded5f563e1953e2dc6))

### [3.0.5](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.4...v3.0.5) (2021-04-05)


### Bug Fixes

* narrow down types in case of {} ([5cf59cf](https://github.com/5alidz/tiny-schema-validator/commit/5cf59cfaf5b6b851e0adbce055dd6fb364ccc8c6))

### [3.0.4](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.3...v3.0.4) (2021-04-05)


### Bug Fixes

* fix shape validators recursion even when optional ([994cef6](https://github.com/5alidz/tiny-schema-validator/commit/994cef6229b2accc49c46972e94ef5b41fe8d275))
* move data checking outside the loop ([2d40ec1](https://github.com/5alidz/tiny-schema-validator/commit/2d40ec1a10633d5a20c738e1f44f2da9acbcd7a9))
* optimize types for strict traverse ([4fd3d26](https://github.com/5alidz/tiny-schema-validator/commit/4fd3d26f7e6380a2d9a23ffcbb9804a1d166075b))
* throw TypeError on produce invalid-data ([0462755](https://github.com/5alidz/tiny-schema-validator/commit/04627558e7007ac39622aa085b3380f54997a750))

### [3.0.3](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.1...v3.0.3) (2021-04-02)


### Bug Fixes

* **types:** fix infered schema.embed ([ade04e0](https://github.com/5alidz/tiny-schema-validator/commit/ade04e0684dd0d4cdd1887a32c74ce6542913e90))

### [3.0.2](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.1...v3.0.2) (2021-04-02)


### Bug Fixes

* **types:** fix infered schema.embed ([ade04e0](https://github.com/5alidz/tiny-schema-validator/commit/ade04e0684dd0d4cdd1887a32c74ce6542913e90))

### [3.0.1](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.0...v3.0.1) (2021-03-28)

## [3.0.0](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.0-alpha.0...v3.0.0) (2021-03-26)


### Bug Fixes

* helper better type support & insure primitives is required ([ab37494](https://github.com/5alidz/tiny-schema-validator/commit/ab374945dd1b439701161f38b183b7c1d4fecd7a))

## [3.0.0-alpha.0](https://github.com/5alidz/tiny-schema-validator/compare/v2.1.0-alpha.3...v3.0.0-alpha.0) (2021-03-26)


### ⚠ BREAKING CHANGES

* infers data type automatically for both JS & TS

### Features

* implement better type inference | less work for the user ([a827591](https://github.com/5alidz/tiny-schema-validator/commit/a827591a8ce525b8f32d08e99ffdb8f8f9657485))


### Bug Fixes

* fix validator circular refernce ([f922048](https://github.com/5alidz/tiny-schema-validator/commit/f922048af6faca4389e7d0abfd5c35097946e916))

## [2.1.0-alpha.3](https://github.com/5alidz/tiny-schema-validator/compare/v2.1.0-alpha.2...v2.1.0-alpha.3) (2021-03-19)

## [2.1.0-alpha.2](https://github.com/5alidz/tiny-schema-validator/compare/v2.1.0-alpha.1...v2.1.0-alpha.2) (2021-03-18)


### Bug Fixes

* expose validatorTypes with index.d.ts ([796990d](https://github.com/5alidz/tiny-schema-validator/commit/796990d543de176332973ef198b33e5d8a48ea1d))

## [2.1.0-alpha.1](https://github.com/5alidz/tiny-schema-validator/compare/v2.1.0-alpha.0...v2.1.0-alpha.1) (2021-03-11)


### Bug Fixes

* expose path as array of strings ([f304909](https://github.com/5alidz/tiny-schema-validator/commit/f304909c9d06bf118cf9d33bc0bfa2043f8ff424))
* remove repeated parent path ([ecd7efa](https://github.com/5alidz/tiny-schema-validator/commit/ecd7efa427156c5e56c5a225975451bf467699cc))

## [2.1.0-alpha.0](https://github.com/5alidz/tiny-schema-validator/compare/v2.0.1-alpha.3...v2.1.0-alpha.0) (2021-03-10)


### Features

* expose correct path ([2fa69ae](https://github.com/5alidz/tiny-schema-validator/commit/2fa69ae08c6c95ee76afe60c07da1c060a726208))

### [2.0.1-alpha.3](https://github.com/5alidz/tiny-schema-validator/compare/v2.0.1-alpha.2...v2.0.1-alpha.3) (2021-03-10)


### Bug Fixes

* expose parentkey ([d6d4302](https://github.com/5alidz/tiny-schema-validator/commit/d6d43028f858983b4f74cfeb2908693c56465ded))

### [2.0.1-alpha.2](https://github.com/5alidz/tiny-schema-validator/compare/v2.0.1-alpha.1...v2.0.1-alpha.2) (2021-03-10)


### Bug Fixes

* prettier not supporting export * as ([0bc2a29](https://github.com/5alidz/tiny-schema-validator/commit/0bc2a2960cdee7c135a1fd57245c1892e2b7293d))
* recordof traverse using index instead of key ([9606738](https://github.com/5alidz/tiny-schema-validator/commit/96067381a3115d087f2633dfa7291d238eb01243))

### [2.0.1-alpha.1](https://github.com/5alidz/tiny-schema-validator/compare/v2.0.1-alpha.0...v2.0.1-alpha.1) (2021-03-06)


### Bug Fixes

* better type inference ([a233541](https://github.com/5alidz/tiny-schema-validator/commit/a233541bd1337fc289427046bac02d5b804e15a8))

### [2.0.1-alpha.0](https://github.com/5alidz/tiny-schema-validator/compare/v2.0.0...v2.0.1-alpha.0) (2021-03-06)


### Bug Fixes

* infer errors types ([6ea7d1f](https://github.com/5alidz/tiny-schema-validator/commit/6ea7d1f9ac62aabff78e627d1133c947f10e0d95))

## [2.0.0](https://github.com/5alidz/tiny-schema-validator/compare/v1.0.5...v2.0.0) (2021-03-04)


### ⚠ BREAKING CHANGES

* renamed recordOf -> recordof

* change helpers names to match docs ([65a1f29](https://github.com/5alidz/tiny-schema-validator/commit/65a1f298323d397d7399933252b2022bdacc784a))

### [1.0.5](https://github.com/5alidz/tiny-schema-validator/compare/v1.0.4...v1.0.5) (2021-03-02)


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 khaled

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
# Tiny Schema Validator

JSON schema validator with excellent type inference for JavaScript and TypeScript.

[![GitHub license](https://img.shields.io/github/license/5alidz/tiny-schema-validator)](https://github.com/5alidz/tiny-schema-validator/blob/master/LICENSE) ![Minzipped size](https://img.shields.io/bundlephobia/minzip/tiny-schema-validator.svg)

## Installation

```sh
npm install tiny-schema-validator
# or
yarn add tiny-schema-validator
```

## Usage

### Creating a schema

```js
import { createSchema, _ } from 'tiny-schema-validator';

export const User = createSchema({
  metadata: _.record({
    date_created: _.number(),
    id: _.string(),
  }),
  profile: _.record({
    name: _.string({
      maxLength: [100, 'too-long'],
      minLength: [2, 'too-short'],
    }),
    age: _.number({
      max: [150, 'too-old'],
      min: [13, 'too-young'],
    }),
    email: _.string({
      pattern: [/^[^@]+@[^@]+\.[^@]+$/, 'invalid-email'],
    }),
  }),
  payment_status: _.union(
    _.constant('pending'),
    _.constant('failed'),
    _.constant('success'),
    _.constant('canceled')
  ),
});
```

and in TypeScript, everything is the same, but to get the data type inferred from the schema, you can do this:

```ts
/*
  UserType {
    metadata: {
      date_created: number;
      id: string;
    };
    profile: {
      name: string;
      age: number;
      email: string;
    };
    payment_status: 'pending' | 'failed' | 'success' | 'canceled';
  }
*/
export type UserType = ReturnType<typeof User.produce>;
```

### Using the schema

When you create a schema, you will get a nice API to handle multiple use-cases in the client and the server.

- `is(data: any): boolean` check if the data is valid (eager evaluation)
- `validate(data: any): Errors` errors returned has the same shape as the schema you defined (does not throw)
- `produce(data: any): data` throws an error when the data is invalid. otherwise, it returns data
- `embed(config?: { optional: boolean })` embeds the schema in other schemas
- `source` the schema itself in a parsable format

example usage:

```js
const Person = createSchema({
  name: _.string(),
  age: _.number(),
  email: _.string(),
});

const john = { name: 'john', age: 42, email: 'john@gmail.com' };
Person.is({}); // false
Person.is(john); // true

Person.validate({}); // { name: 'invalid-type', age: 'invalid-type', email: 'invalid-type' }
Person.validate(john); // null

try {
  Person.produce(undefined);
} catch (e) {
  console.log(e instanceof TypeError); // true
  console.log(e.message); // "invalid-data"
}

// embedding the person schema
const GroupOfPeople = createSchema({
  // ...
  people: _.listof(Person.embed()),
  // ...
});
```

## Validators

All validators are required by default.
All validators are accessible with the `_` (underscore) namespace; The reason for using `_` instead of a good name like `validators` is developer experience, and you can alias it to whatever you want.

```js
import { _ as validators } from 'tiny-schema-validator';
```

Example of all validators and corresponding Typescript types:

<!-- prettier-ignore -->
```js
import { _ } from 'tiny-schema-validator';

// NOTE: when you call a validator you just create an object 
// containing { type: '<type of validator>', ...options }
// this is just a shorthand for that.

// simple validators.
_.string(); // string
_.number(); // number
_.boolean(); // boolean
_.constant(42); // 42

// complex validators (types that accepts other types as paramater)
_.union(
  _.record({ id: _.string() }),
  _.constant(1),
  _.constant(2),
  _.constant(3)
); // { id: string; } | 1 | 2 | 3

_.list([
  _.number(),
  _.string(),
]); // [number, number]
_.record({
  timestamp: _.number(),
  id: _.string(),
}); // { timestamp: number; id: string; }

_.listof(_.string()); // string[]
_.recordof(_.string()); // Record<string, string>
```

Check out the full validators API below:

| validator | signature                       | props                                                          |
| :-------- | ------------------------------- | :------------------------------------------------------------- |
|           |                                 |                                                                |
| constant  | `constant(value)`               | value: `string \| number \| boolean`                           |
|           |                                 |                                                                |
| string    | `string(options?)`              | options (optional): Object                                     |
|           |                                 | - `optional : boolean` defaults to false                       |
|           |                                 | - `maxLength: [length: number, error: string]`                 |
|           |                                 | - `minLength: [length: number, error: string]`                 |
|           |                                 | - `pattern : [pattern: RegExp, error: string]`                 |
|           |                                 |                                                                |
| number    | `number(options?)`              | options(optional): Object                                      |
|           |                                 | - `optional: boolean` default to false                         |
|           |                                 | - `min: [number, error: string]`                               |
|           |                                 | - `max: [number, error: string]`                               |
|           |                                 | - `is : ['integer' \| 'float', error: string]` default is both |
|           |                                 |                                                                |
| boolean   | `boolean(options?)`             | options(optional): Object                                      |
|           |                                 | - `optional: boolean` default to false                         |
|           |                                 |                                                                |
| union     | `union(...validators)`          | validators: Array of validators as paramaters                  |
|           |                                 |                                                                |
| list      | `list(validators[], options?)`  | validators: Array of validators                                |
|           |                                 | options(optional): Object                                      |
|           |                                 | - `optional: boolean` default to false                         |
|           |                                 |                                                                |
| listof    | `listof(validator, options?)`   | validator: Validator                                           |
|           |                                 | options(optional): Object                                      |
|           |                                 | - `optional: boolean` default to false                         |
|           |                                 |                                                                |
| record    | `record(shape, options?)`       | shape: `Object { [key: string]: Validator }`                   |
|           |                                 | options(optional): Object                                      |
|           |                                 | - `optional: boolean` default to false                         |
|           |                                 |                                                                |
| recordof  | `recordof(validator, options?)` | validator: `Validator`                                         |
|           |                                 | options(optional): Object                                      |
|           |                                 | - `optional: boolean` default to false                         |

### Custom validators

To create custom validators that do not break type inference:

- use validators from `_` as building blocks for your custom validator.
- your custom validator should define `optional` and `required` functions.

Example of creating custom validators:

```js
const alphaNumeric = (() => {
  const config = {
    pattern: [/^[a-zA-Z0-9]*$/, 'only-letters-and-number'],
  };
  return {
    required: additional => _.string({ ...additional, ...config, optional: false }), // inferred as Required
    optional: additional => _.string({ ...additional, ...config, optional: true }), // inferred as Optional
  };
})();

const Person = createSchema({
  // ...
  username: alphaNumeric.required({ maxLength: [20, 'username-too-long'] }),
  // ...
});
```

## Built-in Errors

```js
// when typeof value does not match the validator infered type
const TYPEERR = 'invalid-type';

// when "schema" in createSchema(schema) is not plain object
const SCHEMAERR = 'invalid-schema';

// when produce(data) and "data" failed to match the schema
// always accompanied be TypeError, so make sure to catch it
const DATAERR = 'invalid-data';

// when an unknown key is found in data while using record | list
// and any keys that exists on data and not present in the schema
const UNKOWN_KEY_ERR = 'unknown-key';
```

## Caveats

- When using the `recordof | listof | list` validators, the optional property of the validator is ignored, example:

```js
_.recordof(_.string({ optional: true /* THIS IS IGNORED */ }));
_.list([_.number({ optional: true /* THIS IS IGNORED */ }), _.number()]);
```

- You might expect errors returned from a `list | listof` validators to be an array but it is actually an object, example:

```js
const list = createSchema({ list: _.listof(_.string()) });
list.validate({ list: ['string', 42, 'string'] }); // { list: { 1: 'invalid-type' } }
```

## Recursive types

Currently, there's no easy way to create recursive types. if you think you could help, PRs are welcome

## Errors while wrapping schemas

if you try to wrap your schema, you will encounter this error (Type instantiation is excessively deep and possibly infinite)
to fix it, you should unwrap your schema and re-create it inside your abstraction.
let's take the following example:

```ts
const User = createSchema({
  name: _.string(),
  age: _.number(),
});

// your abstraction
function schemaWrapper<T>(schema: T) {
  //...
}

const wrappedUser = schemaWrapper(User); // ERROR: Type instantiation is excessively deep and possibly infinite
```

The fix:

```ts
import { Schema, R, RecordOptions } from 'tiny-schema-validator';
/*
optionally, to infer data from the embedded schema, you do DataFrom<T>

import { DataFrom } from 'tiny-schema-validator/dist/type-utils';
*/

// extract schema with Schema.embed()
function schemaWrapper<T extends Schema>(schema: R<RecordOptions<T>>) {
  const newSchema = createSchema(schema.shape); // you can add/remove/modify passed schema here
  // ...
}

const wrappedUser = schemaWrapper(User.embed()); // all good no errors
```


================================================
FILE: package.json
================================================
{
  "version": "5.0.3",
  "license": "MIT",
  "main": "dist/index.js",
  "typings": "dist/index.d.ts",
  "files": [
    "dist",
    "src"
  ],
  "engines": {
    "node": ">=10"
  },
  "scripts": {
    "start": "tsdx watch",
    "build": "tsdx build",
    "test": "tsdx test",
    "lint": "tsdx lint",
    "prepare": "tsdx build",
    "size": "size-limit",
    "analyze": "size-limit --why",
    "release": "standard-version && git push --follow-tags origin master",
    "release:alpha": "yarn release -- --prerelease alpha"
  },
  "peerDependencies": {},
  "husky": {
    "hooks": {
      "pre-commit": "tsdx lint"
    }
  },
  "prettier": {
    "printWidth": 100,
    "semi": true,
    "singleQuote": true,
    "trailingComma": "es5"
  },
  "name": "tiny-schema-validator",
  "author": "khaled",
  "repository": {
    "url": "https://github.com/5alidz/tiny-schema-validator"
  },
  "module": "dist/tiny-schema-validator.esm.js",
  "size-limit": [
    {
      "path": "dist/tiny-schema-validator.cjs.production.min.js",
      "limit": "10 KB"
    },
    {
      "path": "dist/tiny-schema-validator.esm.js",
      "limit": "10 KB"
    }
  ],
  "resolutions": {
    "**/typescript": "^4.0.5",
    "**/@typescript-eslint/eslint-plugin": "^4.6.1",
    "**/@typescript-eslint/parser": "^4.6.1"
  },
  "jest": {
    "coverageReporters": [
      "json-summary",
      "text",
      "lcov"
    ]
  },
  "devDependencies": {
    "@size-limit/preset-small-lib": "^4.9.0",
    "@typescript-eslint/eslint-plugin": "^4.8.2",
    "@typescript-eslint/parser": "^4.8.2",
    "cz-conventional-changelog": "3.3.0",
    "husky": "^4.3.0",
    "size-limit": "^4.9.0",
    "standard-version": "^9.1.1",
    "tsdx": "^0.14.1",
    "tslib": "^2.3.1",
    "typescript": "^4.3.5"
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  },
  "dependencies": {
    "tiny-invariant": "^1.1.0"
  }
}


================================================
FILE: src/constants.ts
================================================
export const $string = 'string';
export const $number = 'number';
export const $boolean = 'boolean';
export const $list = 'list';
export const $listof = 'listof';
export const $record = 'record';
export const $recordof = 'recordof';
export const $constant = 'constant';
export const $union = 'union';

export const TYPEERR = 'invalid-type';
export const SCHEMAERR = 'invalid-schema';
export const DATAERR = 'invalid-data';
export const UNKOWN_KEY_ERR = 'unknown-key';


================================================
FILE: src/createErrors.ts
================================================
import { isPlainObject, isNumber, isString, isBool, ObjectKeys, toObj, isArray } from './utils';
import {
  BooleanValidator,
  ConstantValidator,
  ListofValidator,
  ListValidator,
  NumberValidator,
  RecordofValidator,
  RecordValidator,
  Schema,
  StringValidator,
  UnionValidator,
  Validator,
} from './validatorTypes';
import { InferResult, InferCallbackResult } from './type-utils';
import { TYPEERR, UNKOWN_KEY_ERR } from './constants';
import invariant from 'tiny-invariant';

function shouldAddToResult(res: unknown) {
  if (
    res == null ||
    (isPlainObject(res) && ObjectKeys(res).length < 1) ||
    (Array.isArray(res) && res.length < 1)
  ) {
    return false;
  }
  return true;
}

function shouldSkipValidation(value: unknown, validator: Validator) {
  return value == null && Boolean(validator.optional);
}

function normalizeResult<T extends Record<string, any>>(result: T) {
  return ObjectKeys(result).length <= 0 ? null : result;
}

function enterNode(validator: Validator, value: unknown, eager = false) {
  const fn = validators[validator.type] as any;
  invariant(typeof fn == 'function', 'invalid-validator-type');
  return fn(validator, value, eager);
}

function parseShapeValidator(
  validator: RecordValidator<any> | ListValidator<any[]>,
  value: unknown,
  eager = false
) {
  const shape = toObj(validator).shape;
  const keys = ObjectKeys(shape);
  const values = toObj(value);
  const result: Record<string, any> = {};
  const dataKeys = ObjectKeys(values);

  for (let i = 0; i < dataKeys.length; i++) {
    const key = dataKeys[i];
    if (!keys.includes(key)) {
      // we gonna sneak this one in the result without typescript knowning about it.
      // can be fixed if we require "data" to be also infered
      // (instead of using any in e.g. schema.validate(data))
      result[key] = UNKOWN_KEY_ERR as any;
      if (eager) return result;
    }
  }

  for (let i = 0; i < keys.length; i++) {
    const currentResult = enterNode(shape[keys[i]], values[keys[i]]);
    if (shouldAddToResult(currentResult)) {
      result[keys[i]] = currentResult;
      if (eager) return result;
    }
  }
  return normalizeResult(result);
}

function parseOfValidator(
  validator: RecordofValidator<any> | ListofValidator<any>,
  value: unknown,
  eager = false
) {
  const values = toObj(value);
  const keys = ObjectKeys(values);
  const result: Record<string, any> = {};
  for (let i = 0; i < keys.length; i++) {
    const currentResult = enterNode(validator.of, values[keys[i]]);
    if (shouldAddToResult(currentResult)) {
      result[keys[i]] = currentResult;
      if (eager) return result;
    }
  }
  return normalizeResult(result);
}

const validators = {
  string(validator: StringValidator, value: unknown) {
    if (shouldSkipValidation(value, validator)) return null;
    if (!isString(value)) return TYPEERR;

    const [minLength, minLengthErrMsg] = validator.minLength ? validator.minLength : [];
    if (minLength && minLengthErrMsg && isNumber(minLength) && value.length < minLength)
      return minLengthErrMsg;

    const [maxLength, maxLengthErrMsg] = validator.maxLength ? validator.maxLength : [];
    if (maxLength && maxLengthErrMsg && isNumber(maxLength) && value.length > maxLength)
      return maxLengthErrMsg;

    const [pattern, patterErrMsg] = validator.pattern ? validator.pattern : [];
    if (pattern && patterErrMsg && pattern.test(value) == false) return patterErrMsg;

    return null;
  },
  number(validator: NumberValidator, value: unknown) {
    if (shouldSkipValidation(value, validator)) return null;

    if (!isNumber(value)) return TYPEERR;

    const [min, minErrMsg] = validator.min ? validator.min : [];
    if (isNumber(min) && value < min && minErrMsg) return minErrMsg;

    const [max, maxErrMsg] = validator.max ? validator.max : [];
    if (isNumber(max) && value > max && maxErrMsg) return maxErrMsg;

    const [is, isErrMsg] = validator.is ? validator.is : [];
    if (isString(is) && isErrMsg) {
      const isInt = Number.isInteger(value);
      if ((isInt && is == 'float') || (!isInt && is == 'integer')) return isErrMsg;
    }

    return null;
  },
  boolean(validator: BooleanValidator, value: unknown) {
    if (shouldSkipValidation(value, validator)) return null;
    if (!isBool(value)) return TYPEERR;
    return null;
  },
  constant<T extends string | number | boolean>(validator: ConstantValidator<T>, value: unknown) {
    if (shouldSkipValidation(value, validator)) return null;
    if (value === validator.value) return null;
    return TYPEERR;
  },
  union<T extends Validator[]>(validator: UnionValidator<T>, value: unknown) {
    if (shouldSkipValidation(value, validator)) return null;
    const unionTypes = validator.of;
    let currentResult = null;
    for (let i = 0; i < unionTypes.length; i++) {
      currentResult = enterNode(unionTypes[i], value);
      if (currentResult == null) return null;
    }
    return TYPEERR;
  },
  list<T extends Validator[]>(validator: ListValidator<T>, value: unknown, eager = false) {
    if (shouldSkipValidation(value, validator)) return null;
    if (!isArray(value)) return TYPEERR;
    return parseShapeValidator(validator, value, eager);
  },
  listof<T extends Validator>(validator: ListofValidator<T>, value: unknown, eager = false) {
    if (shouldSkipValidation(value, validator)) return null;
    if (!isArray(value)) return TYPEERR;
    return parseOfValidator(validator, value, eager);
  },
  record<T extends Schema>(validator: RecordValidator<T>, value: unknown, eager = false) {
    if (shouldSkipValidation(value, validator)) return null;
    if (!isPlainObject(value)) return TYPEERR;
    return parseShapeValidator(validator, value, eager);
  },
  recordof<T extends Validator>(validator: RecordofValidator<T>, value: unknown, eager = false) {
    if (shouldSkipValidation(value, validator)) return null;
    if (!isPlainObject(value)) return TYPEERR;
    return parseOfValidator(validator, value, eager);
  },
};

export function createErrors<T extends Schema>(
  schema: T,
  _data: any,
  eager = false
): null | InferResult<T> {
  const data = isPlainObject(_data) ? _data : {};
  const result: InferResult<T> = {};
  const schemaKeys = ObjectKeys(schema) as (keyof T)[];
  const dataKeys = ObjectKeys(data);

  // get unknown keys
  for (let i = 0; i < dataKeys.length; i++) {
    const key = dataKeys[i];
    if (!schemaKeys.includes(key)) {
      // we gonna sneak this one in the result without typescript knowning about it.
      // can be fixed if we require "data" to be also infered
      // (instead of using any in e.g. schema.validate(data: any))
      result[key as keyof T] = UNKOWN_KEY_ERR as any;
      if (eager) return result;
    }
  }

  for (let i = 0; i < schemaKeys.length; i++) {
    const schemaKey = schemaKeys[i];
    const validator = schema[schemaKey];
    const value = data[schemaKey as string];
    let _result = enterNode(validator, value, eager);
    if (shouldAddToResult(_result)) {
      result[schemaKey] = _result as InferCallbackResult<typeof validator>;
      if (eager) return result;
    }
  }
  return normalizeResult(result);
}


================================================
FILE: src/createSchema.ts
================================================
import { isPlainObject } from './utils';
import { createErrors } from './createErrors';
import { DATAERR, $record, SCHEMAERR } from './constants';
import invariant from 'tiny-invariant';
import { RecordValidator, Schema, R, O, RecordOptions } from './validatorTypes';
import { DataFrom } from './type-utils';

export function createSchema<T extends Schema>(_schema: T) {
  invariant(isPlainObject(_schema), SCHEMAERR);

  type Data = DataFrom<T>;
  const source = Object.freeze({ ..._schema });

  function validate(data: any, eager = false) {
    return createErrors(source, data, eager);
  }

  function is(data: any): data is Data {
    if (!isPlainObject(data)) return false;
    return validate(data, true) == null;
  }

  function embed(): R<RecordOptions<T>>;
  function embed(config: { optional: false }): R<RecordOptions<T>>;
  function embed(config: { optional: true }): O<RecordOptions<T>>;
  function embed(config = { optional: false }): RecordValidator<T> {
    return { type: $record, shape: source, ...config };
  }

  function produce(data: any): Data {
    if (!is(data)) throw new TypeError(DATAERR);
    return data;
  }

  return {
    source,
    validate,
    embed,
    produce,
    is,
  };
}


================================================
FILE: src/helpers.ts
================================================
import {
  O,
  R,
  BooleanValidator,
  ListValidator,
  ListofValidator,
  NumberValidator,
  RecordValidator,
  RecordofValidator,
  Schema,
  StringValidator,
  Validator,
  BooleanOptions,
  ListOptions,
  ListofOptions,
  NumberOptions,
  RecordOptions,
  RecordofOptions,
  StringOptions,
  ConstantOptions,
  UnionOptions,
} from './validatorTypes';
import {
  $boolean,
  $constant,
  $list,
  $listof,
  $number,
  $record,
  $recordof,
  $string,
  $union,
} from './constants';

export function string(): R<StringOptions>;
export function string(config: Omit<StringOptions, 'type'>): R<StringOptions>;
export function string(config: { optional: false } & Omit<StringOptions, 'type'>): R<StringOptions>;
export function string(config: { optional: true } & Omit<StringOptions, 'type'>): O<StringOptions>;

export function string(
  config?: { optional?: boolean } & Omit<StringOptions, 'type'>
): StringValidator {
  return {
    type: $string,
    optional: !!config?.optional,
    ...config,
  };
}

export function number(): R<NumberOptions>;
export function number(config: Omit<NumberOptions, 'type'>): R<NumberOptions>;
export function number(config: { optional: true } & Omit<NumberOptions, 'type'>): O<NumberOptions>;
export function number(config: { optional: false } & Omit<NumberOptions, 'type'>): R<NumberOptions>;

export function number(
  config?: { optional?: boolean } & Omit<NumberOptions, 'type'>
): NumberValidator {
  return {
    type: $number,
    optional: !!config?.optional,
    ...config,
  };
}

export function boolean(): R<BooleanOptions>;
export function boolean(config: { optional: true }): O<BooleanOptions>;
export function boolean(config: { optional: false }): R<BooleanOptions>;

export function boolean(config?: { optional: boolean }): BooleanValidator {
  return {
    type: $boolean,
    optional: !!config?.optional,
  };
}

export function list<T extends R<Validator>[]>(list: T): R<ListOptions<T>>;
export function list<T extends R<Validator>[]>(
  list: T,
  config: { optional: false }
): R<ListOptions<T>>;
export function list<T extends R<Validator>[]>(
  list: T,
  config: { optional: true }
): O<ListOptions<T>>;

export function list<T extends R<Validator>[]>(
  list: T,
  config?: { optional: boolean }
): ListValidator<T> {
  return {
    type: $list,
    optional: !!config?.optional,
    shape: list.map(v => ({ ...v, optional: false })) as T,
  };
}

export function listof<T extends R<Validator>>(v: T): R<ListofOptions<T>>;
export function listof<T extends R<Validator>>(
  v: T,
  config: { optional: false }
): R<ListofOptions<T>>;
export function listof<T extends R<Validator>>(
  v: T,
  config: { optional: true }
): O<ListofOptions<T>>;

export function listof<T extends R<Validator>>(
  v: T,
  config?: { optional: boolean }
): ListofValidator<T> {
  return {
    type: $listof,
    optional: !!config?.optional,
    of: { ...v, optional: false },
  };
}

export function record<T extends Schema>(s: T): R<RecordOptions<T>>;
export function record<T extends Schema>(s: T, config: { optional: false }): R<RecordOptions<T>>;
export function record<T extends Schema>(s: T, config: { optional: true }): O<RecordOptions<T>>;

export function record<T extends Schema>(s: T, config?: { optional: boolean }): RecordValidator<T> {
  return {
    type: $record,
    optional: !!config?.optional,
    shape: s,
  };
}

export function recordof<T extends R<Validator>>(v: T): R<RecordofOptions<T>>;
export function recordof<T extends R<Validator>>(
  v: T,
  config: { optional: false }
): R<RecordofOptions<T>>;
export function recordof<T extends R<Validator>>(
  v: T,
  config: { optional: true }
): O<RecordofOptions<T>>;

export function recordof<T extends R<Validator>>(
  v: T,
  config?: { optional: boolean }
): RecordofValidator<T> {
  return {
    type: $recordof,
    of: { ...v, optional: false },
    optional: !!config?.optional,
  };
}

export function constant<T extends string | number | boolean>(v: T): R<ConstantOptions<T>> {
  return {
    type: $constant,
    optional: false,
    value: v,
  };
}

export function union<T extends R<Validator>[]>(...types: T): R<UnionOptions<T>> {
  return {
    type: $union,
    optional: false,
    of: types,
  };
}


================================================
FILE: src/index.ts
================================================
import * as helpers from './helpers';

export * from './validatorTypes';
export * from './createSchema';
export const _ = helpers;


================================================
FILE: src/type-utils.ts
================================================
import {
  O,
  BooleanValidator,
  ListValidator,
  ListofValidator,
  NumberValidator,
  RecordValidator,
  RecordofValidator,
  StringValidator,
  Schema,
  ConstantValidator,
  UnionValidator,
  Validator,
} from './validatorTypes';

type InferTypeWithOptional<T, U> = T extends O<T> ? U | undefined : U;

type ArrayElement<T> = T extends readonly unknown[]
  ? T extends readonly (infer ElementType)[]
    ? ElementType
    : never
  : never;

type InferDataType<T> = T extends UnionValidator<infer U>
  ? ArrayElement<InferTypeWithOptional<T, { [K in keyof U]: InferDataType<U[K]> }>>
  : T extends ConstantValidator<infer U>
  ? U
  : T extends StringValidator
  ? InferTypeWithOptional<T, string>
  : T extends NumberValidator
  ? InferTypeWithOptional<T, number>
  : T extends BooleanValidator
  ? InferTypeWithOptional<T, boolean>
  : T extends ListValidator<infer U>
  ? InferTypeWithOptional<T, { [K in keyof U]: InferDataType<U[K]> }>
  : T extends ListofValidator<infer V>
  ? InferTypeWithOptional<T, InferDataType<V>[]>
  : T extends RecordValidator<infer S>
  ? InferTypeWithOptional<T, { [K in keyof S]: InferDataType<S[K]> }>
  : T extends RecordofValidator<infer V>
  ? InferTypeWithOptional<T, { [key: string]: InferDataType<V> }>
  : never;

export type DataFrom<S extends Schema> = {
  [K in keyof S]: InferDataType<S[K]>;
};

export type InferCallbackResult<V extends Validator> = V extends
  | StringValidator
  | NumberValidator
  | BooleanValidator
  | ConstantValidator<any>
  | UnionValidator<any>
  ? string
  : V extends ListValidator<infer U>
  ? { [key in number]?: InferCallbackResult<U[key]> }
  : V extends ListofValidator<infer U>
  ? { [key: number]: InferCallbackResult<U> | undefined }
  : V extends RecordValidator<infer U>
  ? { [key in keyof U]?: InferCallbackResult<U[key]> }
  : V extends RecordofValidator<infer U>
  ? { [key: string]: InferCallbackResult<U> | undefined }
  : never;

export type InferResult<S extends Schema> = {
  [key in keyof S]?: InferCallbackResult<S[key]>;
};


================================================
FILE: src/utils.ts
================================================
export const ObjectKeys = Object.keys.bind(Object);
export const isArray = (value: unknown): value is any[] => Array.isArray(value);
export const isBool = (value: unknown): value is boolean => typeof value == 'boolean';
export const isString = (value: unknown): value is string => typeof value == 'string';
export const isNumber = (value: unknown): value is number =>
  typeof value == 'number' && Number.isFinite(value);

export function isPlainObject(maybeObject: any): maybeObject is Record<string, any> {
  return (
    typeof maybeObject == 'object' &&
    maybeObject != null &&
    Object.prototype.toString.call(maybeObject) == '[object Object]'
  );
}

export function toObj(value: any) {
  return isArray(value) ? { ...value } : isPlainObject(value) ? value : ({} as Record<string, any>);
}


================================================
FILE: src/validatorTypes.ts
================================================
export type O<V> = V & { optional: true };
export type R<V> = V & { optional: false };
export type V<T> = O<T> | R<T>;

export interface StringOptions {
  type: 'string';
  maxLength?: [number, string];
  minLength?: [number, string];
  pattern?: [RegExp, string];
}

export interface NumberOptions {
  type: 'number';
  max?: [number, string];
  min?: [number, string];
  is?: ['integer' | 'float', string];
}

export interface BooleanOptions {
  type: 'boolean';
}

export interface ListOptions<T> {
  type: 'list';
  shape: T;
}

export interface ListofOptions<T> {
  type: 'listof';
  of: T;
}

export interface RecordOptions<T> {
  type: 'record';
  shape: T;
}

export interface RecordofOptions<T> {
  type: 'recordof';
  of: T;
}

export interface ConstantOptions<T> {
  type: 'constant';
  value: T;
}

export interface UnionOptions<T> {
  type: 'union';
  of: T;
}

export type BooleanValidator = V<BooleanOptions>;
export type StringValidator = V<StringOptions>;
export type NumberValidator = V<NumberOptions>;
export type ListValidator<T extends Validator[]> = V<ListOptions<T>>;
export type ListofValidator<T extends Validator> = V<ListofOptions<T>>;
export type RecordValidator<T extends Schema> = V<RecordOptions<T>>;
export type RecordofValidator<T extends Validator> = V<RecordofOptions<T>>;
export type ConstantValidator<T extends string | number | boolean> = V<ConstantOptions<T>>;
export type UnionValidator<T extends Validator[]> = V<UnionOptions<T>>;

export type Validator =
  | UnionValidator<any[]>
  | ConstantValidator<any>
  | StringValidator
  | NumberValidator
  | BooleanValidator
  | ListofValidator<any>
  | ListValidator<any[]>
  | RecordValidator<any>
  | RecordofValidator<any>;

export interface Schema {
  [key: string]: Validator;
}


================================================
FILE: test/index.test.ts
================================================
import { createSchema, _ } from '../src/index';
import { DATAERR, TYPEERR } from '../src/constants';

describe('createSchema throws when', () => {
  test('passed invalid schema', () => {
    // @ts-expect-error
    expect(() => createSchema(null)).toThrow();
    // @ts-expect-error
    expect(() => createSchema(undefined)).toThrow();
    // @ts-expect-error
    expect(() => createSchema([])).toThrow();
  });
});

const Person = createSchema({
  is_premium: _.boolean({ optional: true }),
  is_verified: _.boolean(),
  name: _.string({
    maxLength: [24, 'too-long'],
    minLength: [2, 'too-short'],
    pattern: [/[a-zA-Z ]/g, 'contains-symbols'],
  }),
  age: _.number({
    max: [150, 'too-old'],
    min: [13, 'too-young'],
  }),
  email: _.string({
    pattern: [/^[^@]+@[^@]+\.[^@]+$/, 'invalid-email'],
  }),
  tags: _.listof(_.string(), { optional: true }),
  friends: _.recordof(_.record({ name: _.string(), id: _.string() }), { optional: true }),
  nested_list: _.list([_.string(), _.list([_.list([_.number()])])], { optional: true }),
  four_tags: _.list([_.string(), _.string(), _.string(), _.string(), _.list([_.number()])], {
    optional: true,
  }),
  meta: _.record({
    id: _.string({ minLength: [1, 'invalid-id'], maxLength: [1000, 'invalid-id'] }),
    created: _.number({ is: ['integer', 'timestamp-should-be-intger'] }),
    updated: _.number({ optional: true }),
    nested: _.record(
      {
        propA: _.number(),
        propB: _.boolean(),
        propC: _.string(),
      },
      { optional: true }
    ),
  }),
  payment_status: _.union(
    _.constant('pending'),
    _.constant('canceled'),
    _.constant('processed'),
    _.constant('failed')
  ),
});

// type IPerson = ReturnType<typeof Person['produce']>;

describe('validate', () => {
  test('ignores optional properties when not found', () => {
    const errors = Person.validate({
      is_verified: true,
      name: 'abc',
      age: 42,
      email: 'abc@gmail.com',
      payment_status: 'pending',
      meta: {
        id: '123',
        created: Date.now(),
      },
    });

    expect(errors).toBe(null);
  });

  test('validates optional properties when found', () => {
    const errors = Person.validate({
      is_premium: 'hello world',
      is_verified: true,
      name: 'abc',
      age: 42,
      email: 'abc@gmail.com',
      tags: {},
      meta: {
        id: '123',
        created: Date.now(),
        updated: new Date().toISOString(),
      },
      payment_status: 'pending',
    });
    expect(errors).toStrictEqual({
      is_premium: 'invalid-type',
      tags: 'invalid-type',
      meta: {
        updated: 'invalid-type',
      },
    });
  });

  test('emits correct error messages', () => {
    const errors = Person.validate(
      {
        is_premium: 42,
      },
      true
    );
    expect(errors).toStrictEqual({ is_premium: 'invalid-type' });
  });
  // test('handles eager validation correctly', () => {
  //   expect(Person.validate({}, true)).toStrictEqual({ name: TYPEERR });
  // });
});

describe('produce', () => {
  const Person = createSchema({
    name: _.string(),
    age: _.number(),
    email: _.string(),
  });

  test('throws on first error', () => {
    expect(() => Person.produce(null)).toThrow(new TypeError(DATAERR));
    expect(() => Person.produce(undefined)).toThrow(new TypeError(DATAERR));
    expect(() => Person.produce(34)).toThrow(new TypeError(DATAERR));
    expect(() => Person.produce('hello world')).toThrow(new TypeError(DATAERR));
    expect(() => {
      return Person.produce({ name: 2, age: 42, email: 'email@example.com' });
    }).toThrow(new TypeError(DATAERR));
  });

  test('let data throw if it matches the schema', () => {
    const p = { name: 'john', age: 42, email: 'john@gmail.com' };
    expect(Person.produce(p)).toStrictEqual(p);
  });
});

describe('is', () => {
  const s = createSchema({
    a: _.record({
      b: _.string({ optional: true }),
      c: _.record({ d: _.number(), e: _.number({ optional: true }) }),
    }),
  });

  test('returns false when passed incorrect data type', () => {
    const s = createSchema({});
    expect(s.is(undefined)).toBe(false);
    expect(s.is(null)).toBe(false);
    expect(s.is([])).toBe(false);

    expect(s.is({})).toBe(true);
  });

  test('return correct boolean based on data', () => {
    expect(s.is({ a: { c: { d: 42 } } })).toBe(true);
    expect(s.is({ a: { b: 'hello', c: { e: 120, d: 42 } } })).toBe(true);

    expect(s.is({ a: { b: true, c: { e: 120, d: 42 } } })).toBe(false);
    expect(s.is({ a: { c: { d: 'hello' } } })).toBe(false);
  });
});

describe('eager validation', () => {
  const s = createSchema({
    a: _.record({
      b: _.string({ optional: true }),
      c: _.record({ d: _.number(), e: _.number({ optional: true }) }),
    }),
  });

  test('test 1', () => {
    const errors = s.validate({ a: { b: 42, c: false } }, true);
    expect(errors).toStrictEqual({ a: { b: TYPEERR } });
  });
});

describe('reports unknown keys', () => {
  describe('is', () => {
    const schema = createSchema({
      hello: _.string(),
    });
    test('exits when it finds an unknown-key', () => {
      expect(schema.is({ goodbye: 42, hello: 'im valid' })).toBe(false);
    });
  });

  describe('validate', () => {
    const schema = createSchema({
      o: _.record({ a: _.string() }),
    });

    test('errors contain unknown-keys error message', () => {
      expect(schema.validate({ o: { a: '' }, x: 'reported', y: 'reported' })).toStrictEqual({
        x: 'unknown-key',
        y: 'unknown-key',
      });
      const errors = schema.validate({ o: { a: '', b: 42 }, x: 'reported' });
      expect(errors).toStrictEqual({
        x: 'unknown-key',
        o: {
          b: 'unknown-key',
        },
      });
    });
    test('handles eager validation', () => {
      expect(schema.validate({ o: { a: '' }, x: 'reported', y: 'reported' }, true)).toStrictEqual({
        x: 'unknown-key',
      });
      const errors = schema.validate({ o: { a: '', b: 42 }, x: 'reported' }, true);
      expect(errors).toStrictEqual({
        x: 'unknown-key',
      });
    });
  });

  test('is', () => {
    const schema = createSchema({
      opt: _.record({}, { optional: true }),
      list: _.list([_.string(), _.number()], { optional: true }),
      metadata: _.record({
        propA: _.string(),
      }),
    });
    expect(schema.is({ metadata: { propA: 'hello world' }, unknownKey: 42 })).toBe(false);
    expect(schema.is({ metadata: { propA: 'hello world', extra: 42 } })).toBe(false);
    expect(schema.is({ metadata: { propA: 'hello world' }, list: ['hello', 42], opt: {} })).toBe(
      true
    );
    expect(
      schema.is({ metadata: { propA: 'hello world' }, list: ['hello', 42], opt: { newKey: 42 } })
    ).toBe(false);
    expect(schema.is({ metadata: { propA: 'hello world' }, list: ['hello', 42, undefined] })).toBe(
      false
    );
  });
});


================================================
FILE: test/tsconfig.json
================================================
{
  "extends": "../tsconfig.json",
  "include": ["."]
}


================================================
FILE: test/validators.test.ts
================================================
import { createSchema, _ } from '../src/index';
import { TYPEERR } from '../src/constants';

const createString = (length: number, char?: string) => {
  let s = '';
  for (let i = 0; i < length; i++) s += char || ' ';
  return s;
};

describe('string validator', () => {
  const name = (() => {
    const config = {
      maxLength: [100, 'too-long'] as [number, string],
      minLength: [10, 'too-short'] as [number, string],
      pattern: [/[a-zA-Z]/g, 'invalid-pattern'] as [RegExp, string],
    };
    return {
      optional: () => _.string({ ...config, optional: true }),
      required: () => _.string(config),
    };
  })();
  const Person1 = createSchema({
    name: name.optional(),
  });
  const Person2 = createSchema({
    name: name.required(),
  });

  test('tests type', () => {
    expect(Person1.is({ name: 0 })).toBe(false);
    expect(Person1.is({ name: 42 })).toBe(false);
    expect(Person1.is({ name: {} })).toBe(false);
    expect(Person1.is({ name: [] })).toBe(false);
    expect(Person1.is({ name: true })).toBe(false);

    expect(Person2.is({ name: 0 })).toBe(false);
    expect(Person2.is({ name: 42 })).toBe(false);
    expect(Person2.is({ name: {} })).toBe(false);
    expect(Person2.is({ name: [] })).toBe(false);
    expect(Person2.is({ name: true })).toBe(false);
  });

  test('optional', () => {
    expect(Person1.is({ name: undefined })).toBe(true);
    expect(Person1.is({ name: null })).toBe(true);

    expect(Person1.is({ name: 0 })).toBe(false);
    expect(Person1.is({ name: '' })).toBe(false);

    expect(Person2.is({ name: undefined })).toBe(false);
    expect(Person2.is({ name: null })).toBe(false);

    expect(Person2.is({ name: 0 })).toBe(false);
    expect(Person2.is({ name: '' })).toBe(false);
  });

  test('pattern', () => {
    expect(Person1.is({ name: '0123456789' })).toBe(false);
    expect(Person1.is({ name: '----------' })).toBe(false);
    expect(Person1.is({ name: '__________' })).toBe(false);
    expect(Person1.is({ name: 'abcdefghij' })).toBe(true);

    expect(Person2.is({ name: '0123456789' })).toBe(false);
    expect(Person2.is({ name: '----------' })).toBe(false);
    expect(Person2.is({ name: '__________' })).toBe(false);
    expect(Person2.is({ name: 'abcdefghij' })).toBe(true);
  });

  test('maxLength', () => {
    expect(Person1.is({ name: '' })).toBe(false);
    expect(Person1.is({ name: createString(100, 'a') })).toBe(true);
    expect(Person1.is({ name: createString(101, 'a') })).toBe(false);
  });

  test('minLength', () => {
    expect(Person1.is({ name: '' })).toBe(false);
    expect(Person1.is({ name: createString(9, 'a') })).toBe(false);
    expect(Person1.is({ name: createString(10, 'a') })).toBe(true);
  });

  test('emits correct error message', () => {
    expect(Person1.validate({ name: '' })).toStrictEqual({ name: 'too-short' });
    expect(Person1.validate({ name: createString(101, 'a') })).toStrictEqual({ name: 'too-long' });
    expect(Person1.validate({ name: createString(11, '0') })).toStrictEqual({
      name: 'invalid-pattern',
    });
  });
});

describe('number validator', () => {
  const age = (() => {
    const config = {
      max: [10, 'too-large'] as [number, string],
      min: [1, 'too-small'] as [number, string],
      is: ['integer', 'wrong-type-of-number'] as ['integer', string],
    };
    return {
      optional: () => _.number({ ...config, optional: true }),
      required: () => _.number(config),
    };
  })();

  const Person1 = createSchema({
    age: age.optional(),
    n: _.number({ optional: true }),
  });
  const Person2 = createSchema({
    age: age.required(),
    n: _.number({ optional: true }),
  });

  test('tests type', () => {
    expect(Person1.is({ age: '' })).toBe(false);
    expect(Person1.is({ age: {} })).toBe(false);
    expect(Person1.is({ age: [] })).toBe(false);
    expect(Person1.is({ age: true })).toBe(false);
    expect(Person1.is({ age: Infinity, n: Infinity })).toBe(false);
    expect(Person1.is({ age: -Infinity, n: -Infinity })).toBe(false);
    expect(Person1.is({ age: NaN, n: NaN })).toBe(false);

    expect(Person2.is({ age: '' })).toBe(false);
    expect(Person2.is({ age: {} })).toBe(false);
    expect(Person2.is({ age: [] })).toBe(false);
    expect(Person2.is({ age: true })).toBe(false);
    expect(Person1.is({ age: Infinity, n: Infinity })).toBe(false);
    expect(Person1.is({ age: -Infinity, n: -Infinity })).toBe(false);
    expect(Person1.is({ age: NaN, n: NaN })).toBe(false);
  });

  test('optional', () => {
    expect(Person1.is({ age: undefined })).toBe(true);
    expect(Person1.is({ age: null })).toBe(true);
    expect(Person1.is({ age: false })).toBe(false);

    expect(Person2.is({ age: undefined })).toBe(false);
    expect(Person2.is({ age: null })).toBe(false);
    expect(Person1.is({ age: false })).toBe(false);
  });

  test('max', () => {
    expect(Person1.is({ age: 10 })).toBe(true);
    expect(Person1.is({ age: 11 })).toBe(false);
  });

  test('min', () => {
    expect(Person1.is({ age: 0 })).toBe(false);
    expect(Person1.is({ age: 10 })).toBe(true);
  });

  test('is', () => {
    expect(Person1.is({ age: 9.4 })).toBe(false);
    expect(Person1.is({ age: 10 })).toBe(true);
  });

  test('emits correct error message', () => {
    expect(Person1.validate({ age: 11 })).toStrictEqual({ age: 'too-large' });
    expect(Person1.validate({ age: -1 })).toStrictEqual({ age: 'too-small' });
    expect(Person1.validate({ age: 9.4 })).toStrictEqual({ age: 'wrong-type-of-number' });
  });
});

describe('boolean validator', () => {
  const Person1 = createSchema({
    is: _.boolean({ optional: true }),
  });
  const Person2 = createSchema({
    is: _.boolean(),
  });

  test('tests type', () => {
    expect(Person1.is({ is: 0 })).toBe(false);
    expect(Person1.is({ is: '' })).toBe(false);
    expect(Person1.is({ is: {} })).toBe(false);
    expect(Person1.is({ is: [] })).toBe(false);

    expect(Person2.is({ is: 0 })).toBe(false);
    expect(Person2.is({ is: '' })).toBe(false);
    expect(Person2.is({ is: {} })).toBe(false);
    expect(Person2.is({ is: [] })).toBe(false);

    expect(Person1.is({ is: false })).toBe(true);
    expect(Person1.is({ is: true })).toBe(true);
    expect(Person2.is({ is: false })).toBe(true);
    expect(Person2.is({ is: true })).toBe(true);
  });

  test('optional', () => {
    expect(Person1.is({ is: undefined })).toBe(true);
    expect(Person1.is({ is: null })).toBe(true);

    expect(Person2.is({ is: undefined })).toBe(false);
    expect(Person2.is({ is: null })).toBe(false);
  });
});

describe('listof validator', () => {
  const Person = createSchema({
    friends: _.listof(_.string({ minLength: [2, 'too-short'] })),
  });

  test('emits correct error messages', () => {
    expect(Person.validate({ friends: [] })).toStrictEqual(null);
    expect(Person.validate({ friends: {} })).toStrictEqual({ friends: TYPEERR });
    expect(Person.validate({ friends: [1, 'john'] })).toStrictEqual({ friends: { 0: TYPEERR } });
  });
});

describe('list validator', () => {
  const Person = createSchema({
    friends: _.list([_.string(), _.number()]),
    otherList: _.list([_.string({ minLength: [4, 'lower-bound'] })], { optional: true }),
  });

  test('handles optional property', () => {
    expect(Person.validate({ friends: ['John', 42] })).toBe(null);
    expect(Person.validate({ friends: ['John', 42], otherList: [] })).toStrictEqual({
      otherList: { 0: TYPEERR },
    });
    expect(Person.validate({ friends: ['John', 42], otherList: ['hel'] })).toStrictEqual({
      otherList: { 0: 'lower-bound' },
    });
    expect(Person.validate({ friends: ['John', 42], otherList: ['hell'] })).toStrictEqual(null);
  });

  test('emits correct error messages', () => {
    expect(Person.validate({ friends: [] })).toStrictEqual({ friends: { 0: TYPEERR, 1: TYPEERR } });
    expect(Person.validate({ friends: {} })).toStrictEqual({ friends: TYPEERR });
    expect(Person.validate({ friends: [1, 'john'] })).toStrictEqual({
      friends: { 0: TYPEERR, 1: TYPEERR },
    });
    expect(Person.validate({ friends: ['john', 0] })).toStrictEqual(null);
  });
});

describe('record validator', () => {
  const Person = createSchema({
    meta: _.record({
      id: _.string(),
      date_created: _.number(),
      is_verified: _.boolean(),
    }),
    tags: _.listof(_.string(), { optional: true }),
  });

  test('emits correct error messages', () => {
    expect(Person.validate({})).toStrictEqual({ meta: TYPEERR });
    expect(Person.validate({ meta: {} })).toStrictEqual({
      meta: {
        id: TYPEERR,
        date_created: TYPEERR,
        is_verified: TYPEERR,
      },
    });
    expect(
      Person.validate({ meta: { id: 123, date_created: true, is_verified: '' } })
    ).toStrictEqual({
      meta: {
        id: TYPEERR,
        date_created: TYPEERR,
        is_verified: TYPEERR,
      },
    });
    expect(
      Person.validate({ meta: { id: null, date_created: 123, is_verified: false } })
    ).toStrictEqual({ meta: { id: TYPEERR } });
  });

  test('handle recursive records', () => {
    const s = createSchema({ o: _.record({ o: _.record({ x: _.record({ y: _.number() }) }) }) });

    expect(s.validate({ o: { o: { x: { y: 'hello' } } } })).toStrictEqual({
      o: { o: { x: { y: TYPEERR } } },
    });
  });
});

describe('constant validator', () => {
  const schema = createSchema({
    version: _.constant('v2'),
  });

  test('emits correct error message', () => {
    expect(schema.validate({ version: 'v2' })).toStrictEqual(null);
    expect(schema.validate({ version: 'something else' })).toStrictEqual({ version: TYPEERR });
  });
});

describe('union validator', () => {
  test('emits correct error message', () => {
    const schema = createSchema({
      state: _.union(_.constant('on'), _.constant('off'), _.constant('unknown')),
    });
    expect(schema.validate({ state: 'on' })).toStrictEqual(null);
    expect(schema.validate({ state: 'off' })).toStrictEqual(null);
    expect(schema.validate({ state: 'unknown' })).toStrictEqual(null);
    expect(schema.validate({ state: 'should-error' })).toStrictEqual({ state: TYPEERR });
  });

  test('performs deep checks', () => {
    const schema = createSchema({
      state: _.union(
        _.record({ prop: _.number() }),
        _.record({ x: _.record({ nested: _.constant(42) }) })
      ),
    });
    expect(schema.validate({ state: { prop: 100 } })).toStrictEqual(null);
    expect(schema.validate({ state: { x: { nested: 42 } } })).toStrictEqual(null);
    expect(schema.validate({ state: { x: { nested: 33 } } })).toStrictEqual({ state: TYPEERR });
    expect(schema.validate({ state: 'should-error' })).toStrictEqual({ state: TYPEERR });
  });
});

describe('recordof validator', () => {
  const Group = createSchema({
    people: _.recordof(
      _.record({
        name: _.string(),
        age: _.number(),
      })
    ),
  });

  test('emits correct error messages', () => {
    expect(Group.validate({ people: { john: { name: 'john', age: 42 } } })).toStrictEqual(null);
    expect(
      Group.validate({
        people: {
          john: { name: 'john', age: 42 },
          sarah: { name: 'sarah', age: true },
        },
      })
    ).toStrictEqual({ people: { sarah: { age: TYPEERR } } });
  });
});


================================================
FILE: tsconfig.json
================================================
{
  // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
  "include": ["src", "types"],
  "compilerOptions": {
    "module": "esnext",
    "lib": ["dom", "esnext"],
    "importHelpers": true,
    // output .d.ts declaration files for consumers
    "declaration": true,
    // output .js.map sourcemap files for consumers
    "sourceMap": true,
    // match output dir to input dir. e.g. dist/index instead of dist/src/index
    "rootDir": "./src",
    // stricter type-checking for stronger correctness. Recommended by TS
    "strict": true,
    // linter checks for common issues
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    // use Node's module resolution algorithm, instead of the legacy TS one
    "moduleResolution": "node",
    // transpile JSX to React.createElement
    "jsx": "react",
    // interop between ESM and CJS modules. Recommended by TS
    "esModuleInterop": true,
    // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
    "skipLibCheck": true,
    // error out if import and file system have a casing mismatch. Recommended by TS
    "forceConsistentCasingInFileNames": true,
    // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
    "noEmit": true
  }
}
Download .txt
gitextract_l7zhuy_5/

├── .eslintignore
├── .eslintrc.js
├── .github/
│   └── workflows/
│       ├── main.yml
│       └── size.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package.json
├── src/
│   ├── constants.ts
│   ├── createErrors.ts
│   ├── createSchema.ts
│   ├── helpers.ts
│   ├── index.ts
│   ├── type-utils.ts
│   ├── utils.ts
│   └── validatorTypes.ts
├── test/
│   ├── index.test.ts
│   ├── tsconfig.json
│   └── validators.test.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (61 symbols across 7 files)

FILE: src/constants.ts
  constant TYPEERR (line 11) | const TYPEERR = 'invalid-type';
  constant SCHEMAERR (line 12) | const SCHEMAERR = 'invalid-schema';
  constant DATAERR (line 13) | const DATAERR = 'invalid-data';
  constant UNKOWN_KEY_ERR (line 14) | const UNKOWN_KEY_ERR = 'unknown-key';

FILE: src/createErrors.ts
  function shouldAddToResult (line 19) | function shouldAddToResult(res: unknown) {
  function shouldSkipValidation (line 30) | function shouldSkipValidation(value: unknown, validator: Validator) {
  function normalizeResult (line 34) | function normalizeResult<T extends Record<string, any>>(result: T) {
  function enterNode (line 38) | function enterNode(validator: Validator, value: unknown, eager = false) {
  function parseShapeValidator (line 44) | function parseShapeValidator(
  function parseOfValidator (line 76) | function parseOfValidator(
  method string (line 95) | string(validator: StringValidator, value: unknown) {
  method number (line 112) | number(validator: NumberValidator, value: unknown) {
  method boolean (line 131) | boolean(validator: BooleanValidator, value: unknown) {
  method constant (line 136) | constant<T extends string | number | boolean>(validator: ConstantValidat...
  method union (line 141) | union<T extends Validator[]>(validator: UnionValidator<T>, value: unknow...
  method list (line 151) | list<T extends Validator[]>(validator: ListValidator<T>, value: unknown,...
  method listof (line 156) | listof<T extends Validator>(validator: ListofValidator<T>, value: unknow...
  method record (line 161) | record<T extends Schema>(validator: RecordValidator<T>, value: unknown, ...
  method recordof (line 166) | recordof<T extends Validator>(validator: RecordofValidator<T>, value: un...
  function createErrors (line 173) | function createErrors<T extends Schema>(

FILE: src/createSchema.ts
  function createSchema (line 8) | function createSchema<T extends Schema>(_schema: T) {

FILE: src/helpers.ts
  function string (line 40) | function string(
  function number (line 55) | function number(
  function boolean (line 69) | function boolean(config?: { optional: boolean }): BooleanValidator {
  function list (line 86) | function list<T extends R<Validator>[]>(
  function listof (line 107) | function listof<T extends R<Validator>>(
  function record (line 122) | function record<T extends Schema>(s: T, config?: { optional: boolean }):...
  function recordof (line 140) | function recordof<T extends R<Validator>>(
  function constant (line 151) | function constant<T extends string | number | boolean>(v: T): R<Constant...
  function union (line 159) | function union<T extends R<Validator>[]>(...types: T): R<UnionOptions<T>> {

FILE: src/type-utils.ts
  type InferTypeWithOptional (line 16) | type InferTypeWithOptional<T, U> = T extends O<T> ? U | undefined : U;
  type ArrayElement (line 18) | type ArrayElement<T> = T extends readonly unknown[]
  type InferDataType (line 24) | type InferDataType<T> = T extends UnionValidator<infer U>
  type DataFrom (line 44) | type DataFrom<S extends Schema> = {
  type InferCallbackResult (line 48) | type InferCallbackResult<V extends Validator> = V extends
  type InferResult (line 65) | type InferResult<S extends Schema> = {

FILE: src/utils.ts
  function isPlainObject (line 8) | function isPlainObject(maybeObject: any): maybeObject is Record<string, ...
  function toObj (line 16) | function toObj(value: any) {

FILE: src/validatorTypes.ts
  type O (line 1) | type O<V> = V & { optional: true };
  type R (line 2) | type R<V> = V & { optional: false };
  type V (line 3) | type V<T> = O<T> | R<T>;
  type StringOptions (line 5) | interface StringOptions {
  type NumberOptions (line 12) | interface NumberOptions {
  type BooleanOptions (line 19) | interface BooleanOptions {
  type ListOptions (line 23) | interface ListOptions<T> {
  type ListofOptions (line 28) | interface ListofOptions<T> {
  type RecordOptions (line 33) | interface RecordOptions<T> {
  type RecordofOptions (line 38) | interface RecordofOptions<T> {
  type ConstantOptions (line 43) | interface ConstantOptions<T> {
  type UnionOptions (line 48) | interface UnionOptions<T> {
  type BooleanValidator (line 53) | type BooleanValidator = V<BooleanOptions>;
  type StringValidator (line 54) | type StringValidator = V<StringOptions>;
  type NumberValidator (line 55) | type NumberValidator = V<NumberOptions>;
  type ListValidator (line 56) | type ListValidator<T extends Validator[]> = V<ListOptions<T>>;
  type ListofValidator (line 57) | type ListofValidator<T extends Validator> = V<ListofOptions<T>>;
  type RecordValidator (line 58) | type RecordValidator<T extends Schema> = V<RecordOptions<T>>;
  type RecordofValidator (line 59) | type RecordofValidator<T extends Validator> = V<RecordofOptions<T>>;
  type ConstantValidator (line 60) | type ConstantValidator<T extends string | number | boolean> = V<Constant...
  type UnionValidator (line 61) | type UnionValidator<T extends Validator[]> = V<UnionOptions<T>>;
  type Validator (line 63) | type Validator =
  type Schema (line 74) | interface Schema {
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (63K chars).
[
  {
    "path": ".eslintignore",
    "chars": 5,
    "preview": "dist/"
  },
  {
    "path": ".eslintrc.js",
    "chars": 168,
    "preview": "module.exports = {\n  rules: {\n    eqeqeq: 'off',\n    'no-redeclare': 'off',\n    '@typescript-eslint/no-redeclare': ['err"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 732,
    "preview": "name: CI\non: [push]\njobs:\n  build:\n    name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}\n\n    "
  },
  {
    "path": ".github/workflows/size.yml",
    "chars": 258,
    "preview": "name: size\non: [pull_request]\njobs:\n  size:\n    runs-on: ubuntu-latest\n    env:\n      CI_JOB_NUMBER: 1\n    steps:\n      "
  },
  {
    "path": ".gitignore",
    "chars": 47,
    "preview": "*.log\n.DS_Store\nnode_modules\ndist\ncoverage\ntodo"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 7393,
    "preview": "# Changelog\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github."
  },
  {
    "path": "LICENSE",
    "chars": 1062,
    "preview": "MIT License\n\nCopyright (c) 2020 khaled\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
  },
  {
    "path": "README.md",
    "chars": 11168,
    "preview": "# Tiny Schema Validator\n\nJSON schema validator with excellent type inference for JavaScript and TypeScript.\n\n[![GitHub l"
  },
  {
    "path": "package.json",
    "chars": 1919,
    "preview": "{\n  \"version\": \"5.0.3\",\n  \"license\": \"MIT\",\n  \"main\": \"dist/index.js\",\n  \"typings\": \"dist/index.d.ts\",\n  \"files\": [\n    "
  },
  {
    "path": "src/constants.ts",
    "chars": 468,
    "preview": "export const $string = 'string';\nexport const $number = 'number';\nexport const $boolean = 'boolean';\nexport const $list "
  },
  {
    "path": "src/createErrors.ts",
    "chars": 7159,
    "preview": "import { isPlainObject, isNumber, isString, isBool, ObjectKeys, toObj, isArray } from './utils';\nimport {\n  BooleanValid"
  },
  {
    "path": "src/createSchema.ts",
    "chars": 1217,
    "preview": "import { isPlainObject } from './utils';\nimport { createErrors } from './createErrors';\nimport { DATAERR, $record, SCHEM"
  },
  {
    "path": "src/helpers.ts",
    "chars": 4239,
    "preview": "import {\n  O,\n  R,\n  BooleanValidator,\n  ListValidator,\n  ListofValidator,\n  NumberValidator,\n  RecordValidator,\n  Recor"
  },
  {
    "path": "src/index.ts",
    "chars": 131,
    "preview": "import * as helpers from './helpers';\n\nexport * from './validatorTypes';\nexport * from './createSchema';\nexport const _ "
  },
  {
    "path": "src/type-utils.ts",
    "chars": 2030,
    "preview": "import {\n  O,\n  BooleanValidator,\n  ListValidator,\n  ListofValidator,\n  NumberValidator,\n  RecordValidator,\n  RecordofVa"
  },
  {
    "path": "src/utils.ts",
    "chars": 801,
    "preview": "export const ObjectKeys = Object.keys.bind(Object);\nexport const isArray = (value: unknown): value is any[] => Array.isA"
  },
  {
    "path": "src/validatorTypes.ts",
    "chars": 1771,
    "preview": "export type O<V> = V & { optional: true };\nexport type R<V> = V & { optional: false };\nexport type V<T> = O<T> | R<T>;\n\n"
  },
  {
    "path": "test/index.test.ts",
    "chars": 6930,
    "preview": "import { createSchema, _ } from '../src/index';\nimport { DATAERR, TYPEERR } from '../src/constants';\n\ndescribe('createSc"
  },
  {
    "path": "test/tsconfig.json",
    "chars": 56,
    "preview": "{\n  \"extends\": \"../tsconfig.json\",\n  \"include\": [\".\"]\n}\n"
  },
  {
    "path": "test/validators.test.ts",
    "chars": 11292,
    "preview": "import { createSchema, _ } from '../src/index';\nimport { TYPEERR } from '../src/constants';\n\nconst createString = (lengt"
  },
  {
    "path": "tsconfig.json",
    "chars": 1501,
    "preview": "{\n  // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs\n  \"include\": [\"src\", \"types\"],\n  \"comp"
  }
]

About this extraction

This page contains the full source code of the 5alidz/tiny-schema-validator GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (58.9 KB), approximately 16.6k tokens, and a symbol index with 61 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!