Skip to content

Commit 16d2521

Browse files
committed
Implement IHttpServer interface and HonoHttpServer class for HTTP server capabilities
1 parent 88b09b9 commit 16d2521

File tree

5 files changed

+303
-67
lines changed

5 files changed

+303
-67
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Export IHttpServer from spec/api
2+
export * from '@objectstack/spec/api';
3+
4+
import {
5+
IHttpServer,
6+
RouteHandler,
7+
Middleware
8+
} from '@objectstack/spec/api';
9+
import { Hono } from 'hono';
10+
import { serve } from '@hono/node-server';
11+
import { serveStatic } from '@hono/node-server/serve-static';
12+
13+
/**
14+
* Hono Implementation of IHttpServer
15+
*/
16+
export class HonoHttpServer implements IHttpServer {
17+
private app: Hono;
18+
private server: any;
19+
20+
constructor(
21+
private port: number = 3000,
22+
private staticRoot?: string
23+
) {
24+
this.app = new Hono();
25+
}
26+
27+
// internal helper to convert standard handler to Hono handler
28+
private wrap(handler: RouteHandler) {
29+
return async (c: any) => {
30+
const req = {
31+
params: c.req.param(),
32+
query: c.req.query(),
33+
body: await c.req.parseBody().catch(() => {}), // fallback
34+
headers: c.req.header(),
35+
method: c.req.method,
36+
path: c.req.path
37+
};
38+
39+
// Try to parse JSON body if possible
40+
if (c.req.header('content-type')?.includes('application/json')) {
41+
try { req.body = await c.req.json(); } catch(e) {}
42+
}
43+
44+
let capturedResponse: any;
45+
46+
const res = {
47+
json: (data: any) => { capturedResponse = c.json(data); },
48+
send: (data: string) => { capturedResponse = c.html(data); },
49+
status: (code: number) => { c.status(code); return res; },
50+
header: (name: string, value: string) => { c.header(name, value); return res; }
51+
};
52+
53+
await handler(req as any, res as any);
54+
return capturedResponse;
55+
};
56+
}
57+
58+
get(path: string, handler: RouteHandler) {
59+
this.app.get(path, this.wrap(handler));
60+
}
61+
post(path: string, handler: RouteHandler) {
62+
this.app.post(path, this.wrap(handler));
63+
}
64+
put(path: string, handler: RouteHandler) {
65+
this.app.put(path, this.wrap(handler));
66+
}
67+
delete(path: string, handler: RouteHandler) {
68+
this.app.delete(path, this.wrap(handler));
69+
}
70+
patch(path: string, handler: RouteHandler) {
71+
this.app.patch(path, this.wrap(handler));
72+
}
73+
74+
use(pathOrHandler: string | Middleware, handler?: Middleware) {
75+
if (typeof pathOrHandler === 'string' && handler) {
76+
// Path based middleware
77+
// Hono middleware signature is different (c, next) => ...
78+
this.app.use(pathOrHandler, async (c, next) => {
79+
// Simplistic conversion
80+
await handler({} as any, {} as any, next);
81+
});
82+
} else if (typeof pathOrHandler === 'function') {
83+
// Global middleware
84+
this.app.use('*', async (c, next) => {
85+
await pathOrHandler({} as any, {} as any, next);
86+
});
87+
}
88+
}
89+
90+
async listen(port: number) {
91+
return new Promise<void>((resolve) => {
92+
if (this.staticRoot) {
93+
this.app.get('/*', serveStatic({ root: this.staticRoot }));
94+
}
95+
96+
this.server = serve({
97+
fetch: this.app.fetch,
98+
port: port || this.port
99+
}, (info) => {
100+
resolve();
101+
});
102+
});
103+
}
104+
105+
// Expose raw app for scenarios where standard interface is not enough
106+
getRawApp() {
107+
return this.app;
108+
}
109+
110+
async close() {
111+
if (this.server && typeof this.server.close === 'function') {
112+
this.server.close();
113+
}
114+
}
115+
116+
117+
}
Lines changed: 41 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { serve } from '@hono/node-server';
2-
import { serveStatic } from '@hono/node-server/serve-static';
3-
import { Hono } from 'hono';
4-
import { cors } from 'hono/cors';
5-
import { logger } from 'hono/logger';
61
import {
72
Plugin,
83
PluginContext,
94
ObjectStackRuntimeProtocol
105
} from '@objectstack/runtime';
6+
// Use the new IHttpServer interface
7+
import { IHttpServer } from '@objectstack/spec/api';
8+
import { HonoHttpServer } from './adapter';
119

1210
export interface HonoPluginOptions {
1311
port?: number;
@@ -19,41 +17,28 @@ export interface HonoPluginOptions {
1917
*
2018
* Provides HTTP server capabilities using Hono framework.
2119
* Registers routes for ObjectStack Runtime Protocol.
22-
*
23-
* Dependencies: None (can work standalone)
24-
* Services:
25-
* - 'http-server': Hono app instance
26-
*
27-
* @example
28-
* const server = new HonoServerPlugin({ port: 3000 });
29-
* kernel.use(server);
3020
*/
3121
export class HonoServerPlugin implements Plugin {
3222
name = 'com.objectstack.server.hono';
3323
version = '1.0.0';
3424

3525
private options: HonoPluginOptions;
36-
private app: Hono;
37-
private server: any;
26+
private server: HonoHttpServer;
3827

3928
constructor(options: HonoPluginOptions = {}) {
4029
this.options = {
4130
port: 3000,
4231
...options
4332
};
44-
this.app = new Hono();
33+
this.server = new HonoHttpServer(this.options.port, this.options.staticRoot);
4534
}
4635

4736
/**
4837
* Init phase - Setup HTTP server and register as service
4938
*/
5039
async init(ctx: PluginContext) {
51-
// Middleware
52-
this.app.use('*', logger());
53-
this.app.use('*', cors());
54-
55-
// Register HTTP server service
56-
ctx.registerService('http-server', this.app);
40+
// Register HTTP server service as IHttpServer
41+
ctx.registerService('http-server', this.server);
5742
ctx.logger.log('[HonoServerPlugin] HTTP server service registered');
5843
}
5944

@@ -73,7 +58,8 @@ export class HonoServerPlugin implements Plugin {
7358
getService: (name: string) => {
7459
if (name === 'objectql') return objectql;
7560
throw new Error(`[HonoPlugin] Service ${name} not found`);
76-
}
61+
},
62+
getAllPlugins: () => [] // Mock
7763
} as any);
7864

7965
} catch (e) {
@@ -83,80 +69,68 @@ export class HonoServerPlugin implements Plugin {
8369
// Register protocol routes if available
8470
if (protocol) {
8571
const p = protocol!;
86-
this.app.get('/api/v1', (c) => c.json(p.getDiscovery()));
72+
this.server.get('/api/v1', (req, res) => res.json(p.getDiscovery()));
8773

8874
// Meta Protocol
89-
this.app.get('/api/v1/meta', (c) => c.json(p.getMetaTypes()));
90-
this.app.get('/api/v1/meta/:type', (c) => c.json(p.getMetaItems(c.req.param('type'))));
91-
this.app.get('/api/v1/meta/:type/:name', (c) => {
75+
this.server.get('/api/v1/meta', (req, res) => res.json(p.getMetaTypes()));
76+
this.server.get('/api/v1/meta/:type', (req, res) => res.json(p.getMetaItems(req.params.type)));
77+
this.server.get('/api/v1/meta/:type/:name', (req, res) => {
9278
try {
93-
return c.json(p.getMetaItem(c.req.param('type'), c.req.param('name')));
79+
res.json(p.getMetaItem(req.params.type, req.params.name));
9480
} catch(e:any) {
95-
return c.json({error: e.message}, 404);
81+
res.status(404).json({error: e.message});
9682
}
9783
});
9884

9985
// Data Protocol
100-
this.app.get('/api/v1/data/:object', async (c) => {
101-
try { return c.json(await p.findData(c.req.param('object'), c.req.query())); }
102-
catch(e:any) { return c.json({error:e.message}, 400); }
86+
this.server.get('/api/v1/data/:object', async (req, res) => {
87+
try { res.json(await p.findData(req.params.object, req.query)); }
88+
catch(e:any) { res.status(400).json({error:e.message}); }
10389
});
104-
this.app.get('/api/v1/data/:object/:id', async (c) => {
105-
try { return c.json(await p.getData(c.req.param('object'), c.req.param('id'))); }
106-
catch(e:any) { return c.json({error:e.message}, 404); }
90+
this.server.get('/api/v1/data/:object/:id', async (req, res) => {
91+
try { res.json(await p.getData(req.params.object, req.params.id)); }
92+
catch(e:any) { res.status(404).json({error:e.message}); }
10793
});
108-
this.app.post('/api/v1/data/:object', async (c) => {
109-
try { return c.json(await p.createData(c.req.param('object'), await c.req.json()), 201); }
110-
catch(e:any) { return c.json({error:e.message}, 400); }
94+
this.server.post('/api/v1/data/:object', async (req, res) => {
95+
try { res.status(201).json(await p.createData(req.params.object, req.body)); }
96+
catch(e:any) { res.status(400).json({error:e.message}); }
11197
});
112-
this.app.patch('/api/v1/data/:object/:id', async (c) => {
113-
try { return c.json(await p.updateData(c.req.param('object'), c.req.param('id'), await c.req.json())); }
114-
catch(e:any) { return c.json({error:e.message}, 400); }
98+
this.server.patch('/api/v1/data/:object/:id', async (req, res) => {
99+
try { res.json(await p.updateData(req.params.object, req.params.id, req.body)); }
100+
catch(e:any) { res.status(400).json({error:e.message}); }
115101
});
116-
this.app.delete('/api/v1/data/:object/:id', async (c) => {
117-
try { return c.json(await p.deleteData(c.req.param('object'), c.req.param('id'))); }
118-
catch(e:any) { return c.json({error:e.message}, 400); }
102+
this.server.delete('/api/v1/data/:object/:id', async (req, res) => {
103+
try { res.json(await p.deleteData(req.params.object, req.params.id)); }
104+
catch(e:any) { res.status(400).json({error:e.message}); }
119105
});
120106

121107
// UI Protocol
122108
// @ts-ignore
123-
this.app.get('/api/v1/ui/view/:object', (c) => {
109+
this.server.get('/api/v1/ui/view/:object', (req, res) => {
124110
try {
125-
const viewType = (c.req.query('type') as 'list' | 'form') || 'list';
126-
return c.json(p.getUiView(c.req.param('object'), viewType));
111+
const viewType = (req.query.type) || 'list';
112+
const qt = Array.isArray(viewType) ? viewType[0] : viewType;
113+
res.json(p.getUiView(req.params.object, qt as any));
127114
}
128-
catch(e:any) { return c.json({error:e.message}, 404); }
115+
catch(e:any) { res.status(404).json({error:e.message}); }
129116
});
130117
}
131118

132-
// Static files
133-
if (this.options.staticRoot) {
134-
this.app.get('/', serveStatic({ root: this.options.staticRoot, path: 'index.html' }));
135-
this.app.get('/*', serveStatic({ root: this.options.staticRoot }));
136-
}
137-
138119
// Start server on kernel:ready hook
139-
ctx.hook('kernel:ready', () => {
140-
const port = this.options.port;
120+
ctx.hook('kernel:ready', async () => {
121+
const port = this.options.port || 3000;
141122
ctx.logger.log('[HonoServerPlugin] Starting server...');
142-
ctx.logger.log(`✅ Server is running on http://localhost:${port}`);
143123

144-
this.server = serve({
145-
fetch: this.app.fetch,
146-
port
147-
});
124+
await this.server.listen(port);
125+
ctx.logger.log(`✅ Server is running on http://localhost:${port}`);
148126
});
149127
}
150128

151129
/**
152130
* Destroy phase - Stop server
153131
*/
154132
async destroy() {
155-
// Note: Hono's serve function may not return a server with close method
156-
// This is a best-effort cleanup
157-
if (this.server && typeof this.server.close === 'function') {
158-
this.server.close();
159-
console.log('[HonoServerPlugin] Server stopped');
160-
}
133+
this.server.close();
134+
console.log('[HonoServerPlugin] Server stopped');
161135
}
162136
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export * from './hono-plugin';
2+
export * from './adapter';
3+

0 commit comments

Comments
 (0)