diff --git a/package.json b/package.json index 77e47ed..c618240 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,14 @@ "@types/node": "^18.14.1", "eslint": "^8.35.0", "eslint-config-lxsmnsyc": "^0.5.0", + "fastify": "^4.12.0", "pridepack": "2.4.4", "tslib": "^2.5.0", "typescript": "^4.9.5", "vitest": "^0.28.1" }, "dependencies": { - "fastify": "^4.12.0", + "@modal-sh/oatmeal-core": "file:../core", "fastify-plugin": "^4.5.0" }, "scripts": { diff --git a/src/index.ts b/src/index.ts index 8f28352..4ef7a0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,40 @@ import { fastifyPlugin } from 'fastify-plugin'; +import * as oatmeal from '@modal-sh/oatmeal-core'; +import { FastifyReply, FastifyRequest} from 'fastify'; + +const JSON_TYPES = [ + 'application/json', +] as const + +interface SendX { + (contentType?: string, data?: unknown): void; +} + +const sendX: SendX = function sendX(this: FastifyReply, contentType?: string, data?: unknown) { + if (typeof contentType !== 'undefined' && typeof data !== 'undefined') { + const content = oatmeal.serialize(data, { type: contentType }) + // TODO add error handling when payload can't be serialized + this.type(contentType).send(content); + return; + } + this.send(); +} export const fastifyOatmeal = fastifyPlugin(async (fastify) => { + oatmeal.AVAILABLE_TYPES + .filter((type) => !JSON_TYPES.includes(type as typeof JSON_TYPES[number])) + .forEach((type) => { + fastify.addContentTypeParser(type, { parseAs: 'buffer' }, async (_: FastifyRequest, body: Buffer) => { + return oatmeal.deserialize(body.toString('utf-8'), { type }); + // TODO add error handling when body can't be deserialized + }); + }); + fastify.decorateReply('sendX', sendX); }); + +declare module 'fastify' { + interface FastifyReply { + sendX: SendX; + } +} diff --git a/src/routes.ts b/src/routes.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index e69de29..0000000 diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..2295ab6 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,138 @@ +import {describe, it, expect, beforeEach, afterEach} from 'vitest'; +import {fastify, FastifyInstance} from 'fastify'; +import { fastifyOatmeal } from '../src'; + +describe('oatmeal-fastify', () => { + let server: FastifyInstance; + + beforeEach(() => { + server = fastify(); + server.route({ + url: '/', + method: 'POST', + handler: async (request, reply) => { + return reply.sendX(request.headers['accept'] as string, request.body); + }, + }); + }); + + afterEach(async () => { + await server.close(); + }); + + describe('deserialize', () => { + it('deserializes application/json', async () => { + await server.register(fastifyOatmeal); + const response = await server + .inject() + .post('/') + .body(JSON.stringify({ hello: 'world' })) + .headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }); + + expect(response.body).toBe(JSON.stringify({ hello: 'world' })); + }); + + it('deserializes text/json', async () => { + await server.register(fastifyOatmeal); + const response = await server + .inject() + .post('/') + .body(JSON.stringify({ hello: 'world' })) + .headers({ + 'Accept': 'application/json', + 'Content-Type': 'text/json', + }); + + expect(response.body).toBe(JSON.stringify({ hello: 'world' })); + }); + + it('deserializes application/xml', async () => { + await server.register(fastifyOatmeal); + const response = await server + .inject() + .post('/') + .body(`world`) + .headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/xml', + }); + + expect(response.body).toBe(JSON.stringify({ hello: 'world' })); + }); + + it('deserializes text/xml', async () => { + await server.register(fastifyOatmeal); + const response = await server + .inject() + .post('/') + .body(`world`) + .headers({ + 'Accept': 'application/json', + 'Content-Type': 'text/xml', + }); + + expect(response.body).toBe(JSON.stringify({ hello: 'world' })); + }); + }); + + describe('serialize', () => { + it('serializes application/json', async () => { + await server.register(fastifyOatmeal); + const response = await server + .inject() + .post('/') + .body(JSON.stringify({ hello: 'world' })) + .headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }); + + expect(response.body).toBe(JSON.stringify({ hello: 'world' })); + }); + + it('serializes text/json', async () => { + await server.register(fastifyOatmeal); + const response = await server + .inject() + .post('/') + .body(JSON.stringify({ hello: 'world' })) + .headers({ + 'Accept': 'text/json', + 'Content-Type': 'application/json', + }); + + expect(response.body).toBe(JSON.stringify({ hello: 'world' })); + }); + + it('deserializes application/xml', async () => { + await server.register(fastifyOatmeal); + const response = await server + .inject() + .post('/') + .body(JSON.stringify({ hello: 'world' })) + .headers({ + 'Accept': 'application/xml', + 'Content-Type': 'application/json', + }); + + expect(response.body).toBe('world'); + }); + + it('deserializes application/xml', async () => { + await server.register(fastifyOatmeal); + const response = await server + .inject() + .post('/') + .body(JSON.stringify({ hello: 'world' })) + .headers({ + 'Accept': 'text/xml', + 'Content-Type': 'application/json', + }); + + expect(response.body).toBe('world'); + }); + }); +}); diff --git a/test/index.test.tsx b/test/index.test.tsx deleted file mode 100644 index 77cf2d5..0000000 --- a/test/index.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import SERVER from '../src/server'; -import '../src/routes'; - -describe('Example', () => { - it('should have the expected content', async () => { - const response = await SERVER - .inject() - .get('/') - .headers({ - 'Accept': 'application/json', - }); - expect(response.statusCode).toBe(200); - }); -}); diff --git a/yarn.lock b/yarn.lock index 11c7b88..a938d1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -470,6 +470,11 @@ resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.2.47.tgz#421bed6950958adf9b2aee8386aeac9d92e22045" integrity sha512-6/QvoKeooo3J/WL7i9yjfDtUkqOZW8K6aqdzcw+bz4YdgMBzBQVZU7vZmEdCGOcV5AlsBHZT38mqx/sTrnZMDQ== +"@modal-sh/oatmeal-core@file:../core": + version "0.0.0" + dependencies: + xml-js "^1.6.11" + "@next/eslint-plugin-next@^13.2.4": version "13.2.4" resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.4.tgz#3e124cd10ce24dab5d3448ce04104b4f1f4c6ca7" @@ -3206,6 +3211,11 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + secure-json-parse@^2.5.0: version "2.7.0" resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" @@ -3811,6 +3821,13 @@ xdg-basedir@^4.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== +xml-js@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" + integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== + dependencies: + sax "^1.2.4" + xml-name-validator@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"