Skip to content

Commit

Permalink
Add form parser
Browse files Browse the repository at this point in the history
  • Loading branch information
aquapi committed Jul 7, 2024
1 parent 268be0d commit 09a8f41
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 3 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bit-js/byte",
"version": "1.6.0",
"version": "1.6.1",
"module": "index.js",
"devDependencies": {
"@types/bun": "latest",
Expand All @@ -9,7 +9,7 @@
"type": "module",
"types": "types/index.d.ts",
"dependencies": {
"@bit-js/blitz": "^1.3.5"
"@bit-js/blitz": "^1.3.8"
},
"scripts": {
"build-test": "bun build.ts && bun test && tsc --noEmit -p ./tests/tsconfig.json"
Expand Down
1 change: 1 addition & 0 deletions src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
export * from './server/cors';
export * from './server/csrf';
export * from './server/query';
export * from './server/form';
72 changes: 72 additions & 0 deletions src/plugins/server/form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { $async, type BaseContext } from "../../core";
import { noop } from "../../utils/defaultOptions";

interface TypeMap {
string: string;
number: number;
bool: boolean;
file: File;
}

export interface FormPropertyOptions {
type: keyof TypeMap;
multipleItems?: boolean;
}
export type InferFormPropertyOptions<T extends FormPropertyOptions> =
T['multipleItems'] extends true ? (TypeMap[T['type']])[] : TypeMap[T['type']];

export type FormSchema = Record<string, FormPropertyOptions>;

export type InferFormSchema<Schema extends FormSchema> = {
[K in keyof Schema]: InferFormPropertyOptions<Schema[K]>
}

export const form = {
get<Options extends FormPropertyOptions>(prop: string, { type, multipleItems }: Options): (ctx: BaseContext) => Promise<InferFormPropertyOptions<Options> | null> {
return $async(Function(`const p=(f)=>${type === 'string'
? (multipleItems === true
? `{const v=f.getAll(${JSON.stringify(prop)});return v.every((x)=>typeof x==='string')?v:null;}`
: `{const v=f.get(${JSON.stringify(prop)});return typeof v==='string'?v:null;}`)
: type === 'number'
? (multipleItems === true
? `{const v=f.getAll(${JSON.stringify(prop)}).map((t)=>+t);return v.some(Number.isNaN)?v:null;}`
: `{const v=+f.get(${JSON.stringify(prop)});return Number.isNaN(v)?null:v;}`)
: type === 'file'
? (multipleItems === true
? `{const v=f.getAll(${JSON.stringify(prop)});return v.every((x)=>x instanceof File)?v:null;}`
: `{const v=f.get(${JSON.stringify(prop)});return v instanceof File?v:null;}`)
: `f.has(${JSON.stringify(prop)})`
};return (c)=>c.req.formData().then(p);`)())
},

schema<Schema extends FormSchema>(schema: Schema): (ctx: BaseContext) => Promise<InferFormSchema<Schema> | null> {
const parts: string[] = [''], sets = [];

for (const key in schema) {
const item = schema[key];
const { type } = item;

if (type === 'string') {
parts.push(item.multipleItems === true
? `const ${key}=f.getAll(${JSON.stringify(key)});if(${key}.some((x)=>typeof x!=='string'))return null;`
: `const ${key}=f.get(${JSON.stringify(key)});if(typeof ${key}!=='string')return null;`
);
sets.push(key);
} else if (type === 'number') {
parts.push(item.multipleItems === true
? `const ${key}=f.getAll(${JSON.stringify(key)}).map((t)=>+t);if(${key}.some(Number.isNaN))return null;`
: `const ${key}=+f.get(${JSON.stringify(key)});if(Number.isNaN(${key}))return null;`
);
sets.push(key);
} else if (type === 'file') {
parts.push(item.multipleItems === true
? `const ${key}=f.getAll(${JSON.stringify(key)});if(${key}.some((x)=>!(x instanceof File)))return null;`
: `const ${key}=+f.get(${JSON.stringify(key)});if(!(${key} instanceof File))return null;`
);
} else
sets.push(`${key}:f.has(${JSON.stringify(key)})`);
}

return $async(Function('n', `const p=(f)=>{${parts.join('')}return {${sets}};};return (c)=>c.req.formData().then(p).catch(n);`)(noop));
}
};
2 changes: 2 additions & 0 deletions src/utils/defaultOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export const default404: Fn = (ctx) => {
ctx.status = 404;
return new Response(`Cannot ${ctx.req.method} ${ctx.path}`, ctx as ResponseInit);
}

export const noop = () => null;
3 changes: 2 additions & 1 deletion tests/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Server
import { Byte, cors, csrf, send } from '@bit-js/byte';
import { Byte, cors, csrf, send, form } from '@bit-js/byte';

// Basic responses
export const basicApis = new Byte()
Expand Down Expand Up @@ -34,3 +34,4 @@ export const apiWithDefers = new Byte()
export const apiWithSet = new Byte()
.set('startTime', performance.now)
.get('/', (ctx) => ctx.body(performance.now() - ctx.startTime + ''));

57 changes: 57 additions & 0 deletions tests/utils/form.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { form, Context } from '@bit-js/byte';
import { expect, test } from 'bun:test';

function context(obj: Record<string, any>) {
const body = new FormData();

for (const key in obj) {
const value = obj[key];

if (Array.isArray(value)) {
for (let i = 0, { length } = value; i < length; ++i) {
const item = value[i];

body.append(key, typeof item === 'string' || item instanceof File ? item : item + '');
}
} else if (typeof value === 'boolean') {
if (value)
body.append(key, '');
} else body.append(key, typeof value === 'string' || value instanceof File ? value : value + '');
}

return new Context(new Request('http://localhost:3000', {
method: 'POST', body
}))
}

test('Form getters', async () => {
const parseStr = form.get('name', { type: 'string' });
expect(await parseStr(context({ name: 'a' }))).toBe('a');
expect(await parseStr(context({ age: 16 }))).toBe(null);

const parseNum = form.get('id', { type: 'number' });
expect(await parseNum(context({ id: 0 }))).toBe(0);
expect(await parseNum(context({ id: 'str' }))).toBe(null);


const parseBool = form.get('darkMode', { type: 'bool' });
expect(await parseBool(context({ darkMode: '' }))).toBe(true);
expect(await parseBool(context({ other: '' }))).toBe(false);
});

test('Form schema', async () => {
const parseForm = form.schema({
name: { type: 'string' },
age: { type: 'number' },
darkMode: { type: 'bool' },
ids: { type: 'number', multipleItems: true }
});

const o1 = {
name: 'dave',
age: 18,
darkMode: true,
ids: [5, 6]
};
expect(await parseForm(context(o1))).toEqual(o1);
});

0 comments on commit 09a8f41

Please sign in to comment.