diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a0e7df9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Set default behavior to automatically convert line endings +* text=auto eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dfa7fa6..35d66ca 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,5 +9,5 @@ updates: - package-ecosystem: "npm" directory: "/" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b9855f..fd45202 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,11 @@ -name: Continuous Integration +name: CI on: push: + branches: + - main + - next + - 'v*' paths-ignore: - 'docs/**' - '*.md' @@ -10,6 +14,15 @@ on: - 'docs/**' - '*.md' +permissions: + contents: read + jobs: test: - uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3 + permissions: + contents: write + pull-requests: write + uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 + with: + license-check: true + lint: true diff --git a/.gitignore b/.gitignore index cab290e..2b6aed4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* +.pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json @@ -41,8 +42,8 @@ build/Release node_modules/ jspm_packages/ -# TypeScript v1 declaration files -typings/ +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ # TypeScript cache *.tsbuildinfo @@ -53,6 +54,9 @@ typings/ # Optional eslint cache .eslintcache +# Optional stylelint cache +.stylelintcache + # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ @@ -68,15 +72,20 @@ typings/ # Yarn Integrity file .yarn-integrity -# dotenv environment variables file +# dotenv environment variable files .env -.env.test +.env.development.local +.env.test.local +.env.production.local +.env.local # parcel-bundler cache (https://parceljs.org/) .cache +.parcel-cache # Next.js build output .next +out # Nuxt.js build / generate output .nuxt @@ -84,13 +93,20 @@ dist # Gatsby files .cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js +# Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + # Serverless directories .serverless/ @@ -103,4 +119,34 @@ dist # TernJS port file .tern-port +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Vim swap files +*.swp + +# macOS files +.DS_Store + +# Clinic +.clinic + +# lock files +bun.lockb package-lock.json +pnpm-lock.yaml +yarn.lock + +# editor files +.vscode +.idea + +#tap files +.tap/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/LICENSE b/LICENSE index 5559f4f..1d7c1ee 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,8 @@ MIT License -Copyright (c) 2022 Fastify +Copyright (c) 2022-present The Fastify team + +The Fastify team members are listed at https://github.com/fastify/fastify#team. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9958b64..dcc1ecd 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # @fastify/fast-json-stringify-compiler -Build and manage the [`fast-json-stringify`](https://www.npmjs.com/package/fast-json-stringify) instances for the fastify framework. -This package is responsible for compiling the application's `response` JSON schemas into optimized functions to speed up the response time. -[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) -[![Continuous Integration](https://github.com/fastify/fast-json-stringify-compiler/workflows/Continuous%20Integration/badge.svg)](https://github.com/fastify/fast-json-stringify-compiler/actions/workflows/ci.yml) +[![CI](https://github.com/fastify/fast-json-stringify-compiler/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fast-json-stringify-compiler/actions/workflows/ci.yml) +[![NPM version](https://img.shields.io/npm/v/@fastify/fast-json-stringify-compiler.svg?style=flat)](https://www.npmjs.com/package/@fastify/fast-json-stringify-compiler) +[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) +Build and manage the [`fast-json-stringify`](https://www.npmjs.com/package/fast-json-stringify) instances for the Fastify framework. +This package is responsible for compiling the application's `response` JSON schemas into optimized functions to speed up the response time. ## Versions @@ -13,21 +14,114 @@ This package is responsible for compiling the application's `response` JSON sche | v1.x | v3.x | ^3.x | | v2.x | v3.x | ^4.x | | v3.x | v4.x | ^4.x | +| v4.x | v5.x | ^5.x | ### fast-json-stringify Configuration -The `fast-json-stringify` configuration is the default one. You can check it the default settings in the [`fast-json-stringify` option](https://github.com/fastify/fast-json-stringify/#options) documentation. +The `fast-json-stringify` configuration is the default one. You can check the default settings in the [`fast-json-stringify` option](https://github.com/fastify/fast-json-stringify/#options) documentation. -You can also override the default configuration by passing the [`serializerOpts`](https://www.fastify.io/docs/latest/Reference/Server/#serializeropts) configuration to the Fastify instance. +You can also override the default configuration by passing the [`serializerOpts`](https://fastify.dev/docs/latest/Reference/Server/#serializeropts) configuration to the Fastify instance. ## Usage This module is already used as default by Fastify. -If you need to provide to your server instance a different version, refer to [the official doc](https://www.fastify.io/docs/latest/Reference/Server/#schemacontroller). +If you need to provide to your server instance a different version, refer to [the official doc](https://fastify.dev/docs/latest/Reference/Server/#schemacontroller). + +### fast-json-stringify Standalone + +`fast-json-stringify@v4.1.0` introduces the [standalone feature](https://github.com/fastify/fast-json-stringify#standalone) that lets you pre-compile your schemas and use them in your application for a faster startup. + +To use this feature, you must be aware of the following: + +1. You must generate and save the application's compiled schemas. +2. Read the compiled schemas from the file and provide them back to your Fastify application. + + +#### Generate and save the compiled schemas + +Fastify helps you to generate the serialization schemas functions and it is your choice to save them where you want. +To accomplish this, you must use a new compiler: `@fastify/fast-json-stringify-compiler/standalone`. + +You must provide 2 parameters to this compiler: + +- `readMode: false`: a boolean to indicate that you want to generate the schemas functions string. +- `storeFunction`" a sync function that must store the source code of the schemas functions. You may provide an async function too, but you must manage errors. + +When `readMode: false`, **the compiler is meant to be used in development ONLY**. + + +```js +const { StandaloneSerializer } = require('@fastify/fast-json-stringify-compiler') + +const factory = StandaloneSerializer({ + readMode: false, + storeFunction (routeOpts, schemaSerializationCode) { + // routeOpts is like: { schema, method, url, httpStatus } + // schemaSerializationCode is a string source code that is the compiled schema function + const fileName = generateFileName(routeOpts) + fs.writeFileSync(path.join(__dirname, fileName), schemaSerializationCode) + } +}) + +const app = fastify({ + jsonShorthand: false, + schemaController: { + compilersFactory: { + buildSerializer: factory + } + } +}) + +// ... add all your routes with schemas ... + +app.ready().then(() => { + // at this stage all your schemas are compiled and stored in the file system + // now it is important to turn off the readMode +}) +``` + +#### Read the compiled schemas functions + +At this stage, you should have a file for every route's schema. +To use them, you must use the `@fastify/fast-json-stringify-compiler/standalone` with the parameters: + +- `readMode: true`: a boolean to indicate that you want to read and use the schemas functions string. +- `restoreFunction`" a sync function that must return a function to serialize the route's payload. + +Important keep away before you continue reading the documentation: + +- when you use the `readMode: true`, the application schemas are not compiled (they are ignored). So, if you change your schemas, you must recompile them! +- as you can see, you must relate the route's schema to the file name using the `routeOpts` object. You may use the `routeOpts.schema.$id` field to do so, it is up to you to define a unique schema identifier. + +```js +const { StandaloneSerializer } = require('@fastify/fast-json-stringify-compiler') + +const factory = StandaloneSerializer({ + readMode: true, + restoreFunction (routeOpts) { + // routeOpts is like: { schema, method, url, httpStatus } + const fileName = generateFileName(routeOpts) + return require(path.join(__dirname, fileName)) + } +}) + +const app = fastify({ + jsonShorthand: false, + schemaController: { + compilersFactory: { + buildSerializer: factory + } + } +}) + +// ... add all your routes with schemas as before... + +app.listen({ port: 3000 }) +``` ### How it works -This module provide a factory function to produce [Serializer Compilers](https://www.fastify.io/docs/latest/Reference/Server/#serializercompiler) functions. +This module provides a factory function to produce [Serializer Compilers](https://fastify.dev/docs/latest/Reference/Server/#serializercompiler) functions. ## License diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..89fd678 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,6 @@ +'use strict' + +module.exports = require('neostandard')({ + ignores: require('neostandard').resolveIgnoresFromGitignore(), + ts: true +}) diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index 31506ad..0000000 --- a/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Options as FJSOptions } from 'fast-json-stringify' - -export type SerializerCompiler = ( - externalSchemas: unknown, - options: FJSOptions -) => (doc: any) => string; - -export declare function SerializerSelector(): SerializerCompiler; - -export type { Options } from 'fast-json-stringify' - -export default SerializerSelector; \ No newline at end of file diff --git a/index.js b/index.js index 2f14dd0..9323c97 100644 --- a/index.js +++ b/index.js @@ -1,16 +1,8 @@ 'use strict' -const fastJsonStringify = require('fast-json-stringify') - -function SerializerSelector () { - return function buildSerializerFactory (externalSchemas, serializerOpts) { - const fjsOpts = Object.assign({}, serializerOpts, { schema: externalSchemas }) - return responseSchemaCompiler.bind(null, fjsOpts) - } -} - -function responseSchemaCompiler (fjsOpts, { schema /* method, url, httpStatus */ }) { - return fastJsonStringify(schema, fjsOpts) -} +const { SerializerSelector, StandaloneSerializer } = require('./standalone') module.exports = SerializerSelector +module.exports.default = SerializerSelector +module.exports.SerializerSelector = SerializerSelector +module.exports.StandaloneSerializer = StandaloneSerializer diff --git a/package.json b/package.json index cce55f4..0215c45 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "name": "@fastify/fast-json-stringify-compiler", "description": "Build and manage the fast-json-stringify instances for the fastify framework", - "version": "3.0.0", + "version": "5.0.3", "main": "index.js", - "types": "index.d.ts", + "type": "commonjs", + "types": "types/index.d.ts", "scripts": { - "lint:fix": "standard --fix", - "unit": "tap --100 test/**/*.test.js", - "test": "standard && npm run unit && npm run test:typescript", + "lint": "eslint", + "lint:fix": "eslint --fix", + "unit": "c8 --100 node --test", + "test": "npm run unit && npm run test:typescript", "test:typescript": "tsd" }, "repository": { @@ -16,21 +18,54 @@ }, "keywords": [], "author": "Manuel Spigolon (https://github.com/Eomm)", + "contributors": [ + { + "name": "Matteo Collina", + "email": "hello@matteocollina.com" + }, + { + "name": "Aras Abbasi", + "email": "aras.abbasi@gmail.com" + }, + { + "name": "James Sumners", + "url": "https://james.sumners.info" + }, + { + "name": "Frazer Smith", + "email": "frazer.dev@icloud.com", + "url": "https://github.com/fdawgs" + } + ], "license": "MIT", "bugs": { "url": "https://github.com/fastify/fast-json-stringify-compiler/issues" }, "homepage": "https://github.com/fastify/fast-json-stringify-compiler#readme", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "devDependencies": { - "fastify": "^4.0.0-rc.2", - "standard": "^17.0.0", - "tap": "^16.0.0", - "tsd": "^0.20.0" + "@fastify/pre-commit": "^2.1.0", + "c8": "^10.1.3", + "eslint": "^9.17.0", + "fastify": "^5.0.0", + "neostandard": "^0.12.0", + "sanitize-filename": "^1.6.3", + "tsd": "^0.32.0" }, + "pre-commit": [ + "lint", + "test" + ], "dependencies": { - "fast-json-stringify": "^4.0.0" - }, - "tsd": { - "directory": "test/types" + "fast-json-stringify": "^6.0.0" } } diff --git a/standalone.js b/standalone.js new file mode 100644 index 0000000..7f0d40a --- /dev/null +++ b/standalone.js @@ -0,0 +1,58 @@ +'use strict' + +const fastJsonStringify = require('fast-json-stringify') + +function SerializerSelector () { + return function buildSerializerFactory (externalSchemas, serializerOpts) { + const fjsOpts = Object.assign({}, serializerOpts, { schema: externalSchemas }) + return responseSchemaCompiler.bind(null, fjsOpts) + } +} + +function responseSchemaCompiler (fjsOpts, { schema /* method, url, httpStatus */ }) { + if (fjsOpts.schema && schema.$id && fjsOpts.schema[schema.$id]) { + fjsOpts.schema = { ...fjsOpts.schema } + delete fjsOpts.schema[schema.$id] + } + return fastJsonStringify(schema, fjsOpts) +} + +function StandaloneSerializer (options = { readMode: true }) { + if (options.readMode === true && typeof options.restoreFunction !== 'function') { + throw new Error('You must provide a function for the restoreFunction-option when readMode ON') + } + + if (options.readMode !== true && typeof options.storeFunction !== 'function') { + throw new Error('You must provide a function for the storeFunction-option when readMode OFF') + } + + if (options.readMode === true) { + // READ MODE: it behalf only in the restore function provided by the user + return function wrapper () { + return function (opts) { + return options.restoreFunction(opts) + } + } + } + + // WRITE MODE: it behalf on the default SerializerSelector, wrapping the API to run the Ajv Standalone code generation + const factory = SerializerSelector() + return function wrapper (externalSchemas, serializerOpts = {}) { + // to generate the serialization source code, this option is mandatory + serializerOpts.mode = 'standalone' + + const compiler = factory(externalSchemas, serializerOpts) + return function (opts) { // { schema/*, method, url, httpPart */ } + const serializeFuncCode = compiler(opts) + + options.storeFunction(opts, serializeFuncCode) + + // eslint-disable-next-line no-new-func + return new Function(serializeFuncCode) + } + } +} + +module.exports.SerializerSelector = SerializerSelector +module.exports.StandaloneSerializer = StandaloneSerializer +module.exports.default = StandaloneSerializer diff --git a/test/duplicate-schema.test.js b/test/duplicate-schema.test.js new file mode 100644 index 0000000..2acaa24 --- /dev/null +++ b/test/duplicate-schema.test.js @@ -0,0 +1,26 @@ +'use strict' + +const { test } = require('node:test') +const FjsCompiler = require('../index') + +test('Use input schema duplicate in the externalSchemas', async t => { + t.plan(1) + const externalSchemas = { + schema1: { + $id: 'schema1', + type: 'number' + }, + schema2: { + $id: 'schema2', + type: 'string' + } + } + + const factory = FjsCompiler() + const compiler = factory(externalSchemas) + + compiler({ schema: externalSchemas.schema1 }) + compiler({ schema: externalSchemas.schema2 }) + + t.assert.ok(true) +}) diff --git a/test/plugin.test.js b/test/plugin.test.js index d467720..26ca0f5 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -1,10 +1,10 @@ 'use strict' -const t = require('tap') +const { test } = require('node:test') const fastify = require('fastify') const FjsCompiler = require('../index') -const echo = async (req, reply) => { return req.body } +const echo = async (req) => { return req.body } const sampleSchema = Object.freeze({ $id: 'example1', @@ -25,20 +25,18 @@ const externalSchemas2 = Object.freeze({ } }) -const fastifyFjsOptionsDefault = Object.freeze({ - customOptions: {} -}) +const fastifyFjsOptionsDefault = Object.freeze({}) -t.test('basic usage', t => { +test('basic usage', t => { t.plan(1) const factory = FjsCompiler() const compiler = factory(externalSchemas1, fastifyFjsOptionsDefault) const serializeFunc = compiler({ schema: sampleSchema }) const result = serializeFunc({ name: 'hello' }) - t.equal(result, '{"name":"hello"}') + t.assert.equal(result, '{"name":"hello"}') }) -t.test('fastify integration', async t => { +test('fastify integration', async t => { const factory = FjsCompiler() const app = fastify({ @@ -75,6 +73,6 @@ t.test('fastify integration', async t => { } }) - t.equal(res.statusCode, 200) - t.same(res.json(), { name: 'serialize me' }) + t.assert.equal(res.statusCode, 200) + t.assert.deepStrictEqual(res.json(), { name: 'serialize me' }) }) diff --git a/test/standalone.test.js b/test/standalone.test.js new file mode 100644 index 0000000..6ecdc17 --- /dev/null +++ b/test/standalone.test.js @@ -0,0 +1,230 @@ +'use strict' + +const fs = require('node:fs') +const path = require('node:path') +const { test } = require('node:test') +const fastify = require('fastify') +const sanitize = require('sanitize-filename') + +const { StandaloneSerializer: FjsStandaloneCompiler } = require('../') + +const generatedFileNames = [] + +function generateFileName (routeOpts) { + const fileName = `/fjs-generated-${sanitize(routeOpts.schema.$id)}-${routeOpts.method}-${routeOpts.httpPart}-${sanitize(routeOpts.url)}.js` + generatedFileNames.push(fileName) + return fileName +} + +test('standalone', async t => { + t.plan(5) + + t.after(async () => { + for (const fileName of generatedFileNames) { + try { + await fs.promises.unlink(path.join(__dirname, fileName)) + } catch {} + } + }) + + t.test('errors', t => { + t.plan(2) + t.assert.throws(() => { + FjsStandaloneCompiler() + }, 'missing restoreFunction') + t.assert.throws(() => { + FjsStandaloneCompiler({ readMode: false }) + }, 'missing storeFunction') + }) + + t.test('generate standalone code', t => { + t.plan(5) + + const base = { + $id: 'urn:schema:base', + definitions: { + hello: { type: 'string' } + }, + type: 'object', + properties: { + hello: { $ref: '#/definitions/hello' } + } + } + + const refSchema = { + $id: 'urn:schema:ref', + type: 'object', + properties: { + hello: { $ref: 'urn:schema:base#/definitions/hello' } + } + } + + const endpointSchema = { + schema: { + $id: 'urn:schema:endpoint', + $ref: 'urn:schema:ref' + } + } + + const schemaMap = { + [base.$id]: base, + [refSchema.$id]: refSchema + } + + const factory = FjsStandaloneCompiler({ + readMode: false, + storeFunction (routeOpts, schemaSerializerCode) { + t.assert.deepStrictEqual(routeOpts, endpointSchema) + t.assert.ok(typeof schemaSerializerCode === 'string') + fs.writeFileSync(path.join(__dirname, '/fjs-generated.js'), schemaSerializerCode) + generatedFileNames.push('/fjs-generated.js') + t.assert.ok('stored the serializer function') + } + }) + + const compiler = factory(schemaMap) + compiler(endpointSchema) + t.assert.ok('compiled the endpoint schema') + + t.test('usage standalone code', t => { + t.plan(3) + const standaloneSerializer = require('./fjs-generated') + t.assert.ok(standaloneSerializer) + + const valid = standaloneSerializer({ hello: 'world' }) + t.assert.deepStrictEqual(valid, JSON.stringify({ hello: 'world' })) + + const invalid = standaloneSerializer({ hello: [] }) + t.assert.deepStrictEqual(invalid, '{"hello":""}') + }) + }) + + t.test('fastify integration - writeMode', async t => { + t.plan(4) + + const factory = FjsStandaloneCompiler({ + readMode: false, + storeFunction (routeOpts, schemaSerializationCode) { + const fileName = generateFileName(routeOpts) + t.assert.ok(routeOpts) + fs.writeFileSync(path.join(__dirname, fileName), schemaSerializationCode) + t.assert.ok(`stored the serializer function ${fileName}`) + }, + restoreFunction () { + t.fail('write mode ON') + } + }) + + const app = buildApp(factory) + await app.ready() + }) + + await t.test('fastify integration - writeMode forces standalone', async t => { + t.plan(4) + + const factory = FjsStandaloneCompiler({ + readMode: false, + storeFunction (routeOpts, schemaSerializationCode) { + const fileName = generateFileName(routeOpts) + t.assert.ok(routeOpts) + fs.writeFileSync(path.join(__dirname, fileName), schemaSerializationCode) + t.assert.ok(`stored the serializer function ${fileName}`) + }, + restoreFunction () { + t.fail('write mode ON') + } + }) + + const app = buildApp(factory, { + mode: 'not-standalone', + rounding: 'ceil' + }) + + await app.ready() + }) + + await t.test('fastify integration - readMode', async t => { + t.plan(6) + + const factory = FjsStandaloneCompiler({ + readMode: true, + storeFunction () { + t.fail('read mode ON') + }, + restoreFunction (routeOpts) { + const fileName = generateFileName(routeOpts) + t.assert.ok(`restore the serializer function ${fileName}}`) + return require(path.join(__dirname, fileName)) + } + }) + + const app = buildApp(factory) + await app.ready() + + let res = await app.inject({ + url: '/foo', + method: 'POST' + }) + t.assert.equal(res.statusCode, 200) + t.assert.equal(res.payload, JSON.stringify({ hello: 'world' })) + + res = await app.inject({ + url: '/bar?lang=it', + method: 'GET' + }) + t.assert.equal(res.statusCode, 200) + t.assert.equal(res.payload, JSON.stringify({ lang: 'en' })) + }) + + function buildApp (factory, serializerOpts) { + const app = fastify({ + exposeHeadRoutes: false, + jsonShorthand: false, + schemaController: { + compilersFactory: { + buildSerializer: factory + } + }, + serializerOpts + }) + + app.addSchema({ + $id: 'urn:schema:foo', + type: 'object', + properties: { + name: { type: 'string' }, + id: { type: 'integer' } + } + }) + + app.post('/foo', { + schema: { + response: { + 200: { + $id: 'urn:schema:response', + type: 'object', + properties: { + hello: { $ref: 'urn:schema:foo#/properties/name' } + } + } + } + } + }, () => { return { hello: 'world' } }) + + app.get('/bar', { + schema: { + response: { + 200: { + $id: 'urn:schema:response:bar', + type: 'object', + properties: { + lang: { type: 'string', enum: ['it', 'en'] } + } + } + } + } + }, () => { return { lang: 'en' } }) + + return app + } +}) diff --git a/test/types/index.test.ts b/test/types/index.test.ts deleted file mode 100644 index 67caf0d..0000000 --- a/test/types/index.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expectType } from "tsd"; -import SerializerSelector, { SerializerCompiler } from "../.."; - -const compiler = SerializerSelector(); - -expectType(compiler); \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..0546135 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,41 @@ +import { Options } from 'fast-json-stringify' + +type FastJsonStringifyFactory = () => SerializerSelector.SerializerFactory + +declare namespace SerializerSelector { + export type SerializerFactory = ( + externalSchemas?: unknown, + options?: Options + ) => SerializerCompiler + + export type SerializerCompiler = (routeDef: RouteDefinition) => Serializer + export type Serializer = (doc: any) => string + + export type RouteDefinition = { + method: string; + url: string; + httpStatus: string; + schema?: unknown; + } + + export type StandaloneOptions = StandaloneOptionsReadModeOn | StandaloneOptionsReadModeOff + + export type StandaloneOptionsReadModeOn = { + readMode: true; + restoreFunction?(opts: RouteDefinition): Serializer; + } + + export type StandaloneOptionsReadModeOff = { + readMode?: false | undefined; + storeFunction?(opts: RouteDefinition, schemaSerializationCode: string): void; + } + + export type { Options } + export const SerializerSelector: FastJsonStringifyFactory + export function StandaloneSerializer (options: StandaloneOptions): SerializerFactory + + export { SerializerSelector as default } +} + +declare function SerializerSelector (...params: Parameters): ReturnType +export = SerializerSelector diff --git a/types/index.test-d.ts b/types/index.test-d.ts new file mode 100644 index 0000000..018759a --- /dev/null +++ b/types/index.test-d.ts @@ -0,0 +1,142 @@ +import { expectAssignable, expectError, expectType } from 'tsd' +import SerializerSelector, { + RouteDefinition, + Serializer, + SerializerCompiler, + SerializerFactory, + SerializerSelector as SerializerSelectorNamed, + StandaloneSerializer, +} from '..' + +/** + * SerializerSelector + */ + +{ + const compiler = SerializerSelector() + expectType(compiler) +} + +{ + const compiler = SerializerSelectorNamed() + expectType(compiler) +} + +{ + const sampleSchema = { + $id: 'example1', + type: 'object', + properties: { + name: { type: 'string' } + } + } + + const externalSchemas1 = {} + + const factory = SerializerSelector() + expectType(factory) + const compiler = factory(externalSchemas1, {}) + expectType(compiler) + const serializeFunc = compiler({ schema: sampleSchema, method: '', url: '', httpStatus: '' }) + expectType(serializeFunc) + + expectType(serializeFunc({ name: 'hello' })) +} + +/** + * StandaloneSerializer + */ + +const reader = StandaloneSerializer({ + readMode: true, + restoreFunction: (route: RouteDefinition) => { + expectAssignable(route) + return {} as Serializer + }, +}) +expectType(reader) + +const writer = StandaloneSerializer({ + readMode: false, + storeFunction: (route: RouteDefinition, code: string) => { + expectAssignable(route) + expectAssignable(code) + }, +}) +expectType(writer) + +{ + const base = { + $id: 'urn:schema:base', + definitions: { + hello: { type: 'string' } + }, + type: 'object', + properties: { + hello: { $ref: '#/definitions/hello' } + } + } + + const refSchema = { + $id: 'urn:schema:ref', + type: 'object', + properties: { + hello: { $ref: 'urn:schema:base#/definitions/hello' } + } + } + + const endpointSchema = { + method: '', + url: '', + httpStatus: '', + schema: { + $id: 'urn:schema:endpoint', + $ref: 'urn:schema:ref' + } + } + + const schemaMap = { + [base.$id]: base, + [refSchema.$id]: refSchema + } + + expectError(StandaloneSerializer({ + readMode: true, + storeFunction () { } + })) + expectError(StandaloneSerializer({ + readMode: false, + restoreFunction () {} + })) + expectError(StandaloneSerializer({ + restoreFunction () {} + })) + + expectType(StandaloneSerializer({ + storeFunction (routeOpts, schemaSerializerCode) { + expectType(routeOpts) + expectType(schemaSerializerCode) + } + })) + + expectType(StandaloneSerializer({ + readMode: true, + restoreFunction (routeOpts) { + expectType(routeOpts) + return {} as Serializer + } + })) + + const factory = StandaloneSerializer({ + readMode: false, + storeFunction (routeOpts, schemaSerializerCode) { + expectType(routeOpts) + expectType(schemaSerializerCode) + } + }) + expectType(factory) + + const compiler = factory(schemaMap) + expectType(compiler) + expectType(compiler(endpointSchema)) +}