Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 41 additions & 83 deletions __tests__/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { generateAPISchema } from '../src/interface/schema';
import { generateAPISchema, GET, POST, PUT, DELETE, PATCH } from '../src/interface/schema';
import { Number, Record, String, Array } from 'runtypes';

describe('generateAPISchema', () => {
it('should generate schema from single endpoint definition', () => {
const schema = generateAPISchema({
fields: {
'GET /users': {
fields: {
request: {},
responses: {
200: Record({ users: Array(String) }),
},
},
'users': GET({
responses: {
200: { users: Array(String) },
},
},
}),
});

assert.ok(schema, 'Schema should be generated');
Expand All @@ -24,31 +19,20 @@ describe('generateAPISchema', () => {

it('should merge multiple schemas with intersectees', () => {
const schema1 = {
fields: {
'GET /users': {
fields: {
request: {},
responses: {
200: Record({ users: Array(String) }),
},
},
'users': GET({
responses: {
200: { users: Array(String) },
},
},
}),
};

const schema2 = {
fields: {
'POST /users': {
fields: {
request: {
body: Record({ name: String }),
},
responses: {
201: Record({ id: Number }),
},
},
'users': POST({
body: { name: String },
responses: {
201: { id: Number },
},
},
}),
};

const merged = generateAPISchema({
Expand All @@ -61,9 +45,7 @@ describe('generateAPISchema', () => {
});

it('should handle empty schema gracefully', () => {
const schema = generateAPISchema({
fields: {},
});
const schema = generateAPISchema({});

assert.ok(schema, 'Empty schema should be generated');
assert.deepStrictEqual(Object.keys(schema), [], 'Empty schema should have no keys');
Expand All @@ -73,19 +55,13 @@ describe('generateAPISchema', () => {
describe('Schema structure validation', () => {
it('should accept schema with path parameters', () => {
const schema = {
fields: {
'GET /users/{id}': {
fields: {
request: {
path: { id: Number },
},
responses: {
200: Record({ name: String }),
404: Record({ error: String }),
},
},
'users/{id}': GET({
path: { id: Number },
responses: {
200: { name: String },
404: { error: String },
},
},
}),
};

const result = generateAPISchema(schema);
Expand All @@ -95,19 +71,13 @@ describe('Schema structure validation', () => {

it('should accept schema with request body', () => {
const schema = {
fields: {
'POST /users': {
fields: {
request: {
body: Record({ name: String, email: String }),
},
responses: {
201: Record({ id: Number }),
400: Record({ error: String }),
},
},
'/users': POST({
body: { name: String, email: String },
responses: {
201: { id: Number },
400: { error: String },
},
},
}),
};

const result = generateAPISchema(schema);
Expand All @@ -117,18 +87,12 @@ describe('Schema structure validation', () => {

it('should accept schema with query parameters', () => {
const schema = {
fields: {
'GET /users': {
fields: {
request: {
query: { page: Number, limit: Number },
},
responses: {
200: Record({ users: Array(String) }),
},
},
'users': GET({
query: { page: Number, limit: Number },
responses: {
200: { users: Array(String) },
},
},
}),
};

const result = generateAPISchema(schema);
Expand All @@ -138,22 +102,16 @@ describe('Schema structure validation', () => {

it('should accept schema with multiple status codes', () => {
const schema = {
fields: {
'POST /users': {
fields: {
request: {
body: Record({ name: String }),
},
responses: {
200: Record({ success: String }),
201: Record({ id: Number }),
400: Record({ error: String }),
401: Record({ message: String }),
500: Record({ error: String }),
},
},
'users': POST({
body: { name: String },
responses: {
200: { success: String },
201: { id: Number },
400: { error: String },
401: { message: String },
500: { error: String },
},
},
}),
};

const result = generateAPISchema(schema);
Expand Down
44 changes: 19 additions & 25 deletions __tests__/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
* These tests validate that types are correctly inferred at compile-time
*/

// Import the schema types
// Import the schema types and helpers
import type { APISchema } from '../src/interface/schema';
import { GET, POST, generateAPISchema } from '../src/interface/schema';
import type { Runtype } from 'runtypes';
import { String } from 'runtypes';

// Type alias for compatibility
type RuntypeBase<T> = Runtype.Core<T>;
Expand Down Expand Up @@ -83,33 +85,25 @@ type Test4_Invalid = 670;
// =============================================================================

// Test that APISchema accepts the new structure
type TestAPISchema = {
'POST /users/{id}': {
fields: {
request: {
path: { id: RuntypeBase<number | string> };
body: RuntypeBase<{ name: string }>;
};
responses: {
200: RuntypeBase<{ success: boolean }>;
404: RuntypeBase<{ error: string }>;
};
};
};
'GET /users': {
fields: {
request: {
query?: { page: RuntypeBase<string> };
};
responses: {
200: RuntypeBase<{ users: unknown[] }>;
};
};
};
const typedSchema = {
'users/{id}': POST({
path: { id: String },
body: { name: String },
responses: {
200: { success: String },
404: { error: String },
},
}),
'/users': GET({
query: { page: String },
responses: {
200: { users: String },
},
}),
};

// Verify this compiles as valid APISchema
type TestSchemaIsValid = TestAPISchema extends APISchema ? true : false;
type TestSchemaIsValid = ReturnType<typeof generateAPISchema<typeof typedSchema>> extends APISchema ? true : false;

// Export a marker to indicate all type tests passed
export const TYPE_TESTS_PASSED = true;
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@
"lint": "eslint {src, __tests__}/**/*.ts --fix",
"build": "tsx build.ts",
"type-check": "tsc --noEmit",
"test": "tsx --test __tests__/**/*.test.ts"
"test": "tsx --test __tests__/*.test.ts"
}
}
}
8 changes: 6 additions & 2 deletions src/entry/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@ export class TypedHttpAPIServer<APISchemaType extends APISchema, Raw = undefined
processor: option => async request => {
const payload = request.body;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if(!(v.io.fields.request as any).guard(payload)) return option.incorrectTypeMessage;
if (v.io?.request?.body && !(v.io.request.body as any).guard(payload)) {
return option.incorrectTypeMessage;
}
return HttpAPIResponse.unpack(await v.processor(new HttpAPIRequest(request), payload));
},
}));
const shortage = Object.entries(this.schema).map(v => v[0]).filter(v => types.find(e => `${e.endpoint.method} ${e.endpoint.uri}` === v) === undefined);
const shortage = Object.entries(this.schema)
.map(([key]) => key)
.filter(key => types.find(e => `${e.endpoint.method} ${e.endpoint.uri}` === key) === undefined);
if(summary) generateSummary({
apiCount: HTTP_REQUEST_METHODS.map(v => ({ method: v, count: types.filter(e => e.endpoint.method === v).length })),
doublingEndpoints: detectDuplicate(types.map(v => `${v.endpoint.method} ${v.endpoint.uri}`)),
Expand Down
2 changes: 1 addition & 1 deletion src/interface/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export type GetSchema<
APISchemaType extends APISchema,
EndPoint extends (keyof APISchemaType & APIEndPoint),
Type extends 'request' | 'responses'
> = NonNullable<APISchemaType[EndPoint]>['fields'][Type];
> = NonNullable<APISchemaType[EndPoint]>[Type];

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
export type GetStaticSchema<
Expand Down
Loading