From c6b1efd0d3d3078376193b81c4eb2ea2224ab838 Mon Sep 17 00:00:00 2001 From: Anthony Griffon Date: Thu, 29 Aug 2024 14:41:43 +0200 Subject: [PATCH] feat: add new overloading method for serve --- .changeset/fuzzy-tables-think.md | 5 +++ example/deno-simple-http-page/src/main.ts | 2 +- libs/bunny-sdk/src/net/ip.test.ts | 27 +++++++++++++ libs/bunny-sdk/src/net/ip.ts | 11 +++++ libs/bunny-sdk/src/net/serve.ts | 49 ++++++++++++++++++----- libs/bunny-sdk/src/net/socket/v4.test.ts | 48 ++++++++++++++++++++++ libs/bunny-sdk/src/net/socket/v4.ts | 32 +++++++++++++-- libs/bunny-sdk/src/net/tcp.ts | 12 ++++++ 8 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 .changeset/fuzzy-tables-think.md create mode 100644 libs/bunny-sdk/src/net/ip.test.ts create mode 100644 libs/bunny-sdk/src/net/socket/v4.test.ts diff --git a/.changeset/fuzzy-tables-think.md b/.changeset/fuzzy-tables-think.md new file mode 100644 index 0000000..5cf7169 --- /dev/null +++ b/.changeset/fuzzy-tables-think.md @@ -0,0 +1,5 @@ +--- +"@bunny.net/edgescript-sdk": minor +--- + +New overloading for serve to avoid putting listener diff --git a/example/deno-simple-http-page/src/main.ts b/example/deno-simple-http-page/src/main.ts index bb26b24..f60b3db 100644 --- a/example/deno-simple-http-page/src/main.ts +++ b/example/deno-simple-http-page/src/main.ts @@ -5,7 +5,7 @@ function sleep(ms: number): Promise { } console.log("Starting server..."); -BunnySDK.net.http.serve(async (req) => { +BunnySDK.net.http.serve({}, async (req) => { console.log(`[INFO]: ${req.method} - ${req.url}`); await sleep(1); return new Response("blbl"); diff --git a/libs/bunny-sdk/src/net/ip.test.ts b/libs/bunny-sdk/src/net/ip.test.ts new file mode 100644 index 0000000..c8ebe01 --- /dev/null +++ b/libs/bunny-sdk/src/net/ip.test.ts @@ -0,0 +1,27 @@ +import * as IP from "./ip.ts"; + +test('toString should convert IPv4 tuple to string', () => { + const ip: IP.IPv4 = [192, 168, 1, 1]; + expect(IP.toString(ip)).toBe('192.168.1.1'); +}); + +test('tryParseFromString should parse valid IP string', () => { + const ipString = '192.168.1.1'; + const expected: IP.IPv4 = [192, 168, 1, 1]; + expect(IP.tryParseFromString(ipString)).toEqual(expected); +}); + +test('tryParseFromString should return SyntaxError for invalid IP string', () => { + const invalidIpString = '999.999.999.999'; + expect(IP.tryParseFromString(invalidIpString)).toBeInstanceOf(SyntaxError); +}); + +test('tryParseFromString should return SyntaxError for non-numeric IP string', () => { + const invalidIpString = 'abc.def.ghi.jkl'; + expect(IP.tryParseFromString(invalidIpString)).toBeInstanceOf(SyntaxError); +}); + +test('tryParseFromString should return SyntaxError for incomplete IP string', () => { + const invalidIpString = '192.168.1'; + expect(IP.tryParseFromString(invalidIpString)).toBeInstanceOf(SyntaxError); +}); diff --git a/libs/bunny-sdk/src/net/ip.ts b/libs/bunny-sdk/src/net/ip.ts index bc1e3a5..23d9c89 100644 --- a/libs/bunny-sdk/src/net/ip.ts +++ b/libs/bunny-sdk/src/net/ip.ts @@ -9,3 +9,14 @@ export type IPv4 = [Range<0, 255>, Range<0, 255>, Range<0, 255>, Range<0, 255>]; export function toString(ip: IPv4): string { return `${ip[0]}.${ip[1]}.${ip[2]}.${ip[3]}`; } + +/** + * Try to parse na IP + */ +export function tryParseFromString(ip: string): IPv4 | SyntaxError { + const parts = ip.split('.').map(Number); + if (parts.length !== 4 || parts.some(part => isNaN(part) || part < 0 || part > 255)) { + return new SyntaxError('Invalid IP address'); + } + return [parts[0], parts[1], parts[2], parts[3]] as IPv4; +} diff --git a/libs/bunny-sdk/src/net/serve.ts b/libs/bunny-sdk/src/net/serve.ts index 75a9f2d..f0c5ce0 100644 --- a/libs/bunny-sdk/src/net/serve.ts +++ b/libs/bunny-sdk/src/net/serve.ts @@ -16,24 +16,55 @@ type ServeHandler = {} & unknown; /** * Serves HTTP requests on the given [TcpListener] */ -function serve(handler: ServerHandler, listener: { port: number; hostname: string; }): ServeHandler; -function serve(handler: ServerHandler, listener: TcpListener): ServeHandler; -function serve(handler: ServerHandler, listener?: TcpListener | { port: number; hostname: string; },): ServeHandler { - const platform = internal_getPlatform(); - const listenerUnion = Tcp.isTcpListener(listener) ? listener : Tcp.unstable_new(); +function is_port_and_hostname(value: unknown): value is { port: number; hostname: string; } { + if (typeof value === "object" && value !== null) { + let port = value["port"]; + return port !== undefined && typeof port === "number" && value["hostname"] !== undefined; + } + + return false; +} + +function serve(handler: ServerHandler): ServeHandler; +function serve(listener: { port: number; hostname: string; }, handler: ServerHandler): ServeHandler; +function serve(listener: TcpListener, handler: ServerHandler): ServeHandler; +function serve(listener: ServerHandler | { port: number; hostname: string; } | TcpListener, handler?: ServerHandler): ServeHandler { + let raw_handler: ServerHandler | undefined; + let raw_listener: TcpListener; + + if (is_port_and_hostname(listener)) { + const addr = SocketAddr.v4.tryFromString(`${listener.hostname}:${listener.port}`); + if (addr instanceof Error) { + throw addr; + } + raw_listener = Tcp.bind(addr); + raw_handler = handler; + } else if (Tcp.isTcpListener(listener)) { + raw_handler = handler; + raw_listener = listener; + } else { + raw_handler = listener; + raw_listener = Tcp.unstable_new(); + } + + if (raw_handler === undefined) { + throw new Error("An issue happened."); + } + + const platform = internal_getPlatform(); switch (platform.runtime) { case "bunny": { - Bunny.v1.serve(handler); + Bunny.v1.serve(raw_handler); return {}; } case "node": { - return NodeImpl.node_serve(listenerUnion, handler); + return NodeImpl.node_serve(raw_listener, raw_handler); } case "deno": { - const addr = Tcp.unstable_local_addr(listenerUnion); + const addr = Tcp.unstable_local_addr(raw_listener); if (!SocketAddr.isV4(addr)) { throw new Error("An issue happened with the addr."); @@ -42,7 +73,7 @@ function serve(handler: ServerHandler, listener?: TcpListener | { port: number; const port = SocketAddr.v4.port(addr); const hostname = Ip.toString(SocketAddr.v4.ip(addr)); - Deno.serve({ port, hostname }, handler); + Deno.serve({ port, hostname }, raw_handler); return {}; } case "unknown": { diff --git a/libs/bunny-sdk/src/net/socket/v4.test.ts b/libs/bunny-sdk/src/net/socket/v4.test.ts new file mode 100644 index 0000000..554f03b --- /dev/null +++ b/libs/bunny-sdk/src/net/socket/v4.test.ts @@ -0,0 +1,48 @@ +import { SocketAddrV4, port, tryFromString, ip } from './v4.ts'; + +describe('SocketAddrV4', () => { + describe('port', () => { + it('should return the port number', () => { + const addr: SocketAddrV4 = { _tag: "SocketAddrV4", port: 8080, ip: [127, 0, 0, 1] }; + expect(port(addr)).toBe(8080); + }); + }); + + describe('ip', () => { + it('should return the IP address', () => { + const addr: SocketAddrV4 = { _tag: "SocketAddrV4", port: 8080, ip: [127, 0, 0, 1] }; + expect(ip(addr)).toStrictEqual([127, 0, 0, 1]); + }); + }); + + describe('tryFromString', () => { + it('should parse a valid SocketAddrV4 string', () => { + const result = tryFromString("127.0.0.1:8080"); + expect(result).toEqual({ _tag: "SocketAddrV4", port: 8080, ip: [127, 0, 0, 1] }); + }); + + it('should return a SyntaxError for an invalid IP', () => { + const result = tryFromString("invalid_ip:8080"); + expect(result).toBeInstanceOf(SyntaxError); + expect((result as SyntaxError).message).toBe('Invalid IP address'); + }); + + it('should return a SyntaxError for an invalid port', () => { + const result = tryFromString("127.0.0.1:invalid_port"); + expect(result).toBeInstanceOf(SyntaxError); + expect((result as SyntaxError).message).toBe('Invalid Port'); + }); + + it('should return a SyntaxError for a port out of range', () => { + const result = tryFromString("127.0.0.1:70000"); + expect(result).toBeInstanceOf(SyntaxError); + expect((result as SyntaxError).message).toBe('Invalid Port'); + }); + + it('should return a SyntaxError for an invalid format', () => { + const result = tryFromString("127.0.0.1-8080"); + expect(result).toBeInstanceOf(SyntaxError); + expect((result as SyntaxError).message).toBe('Invalid SocketAddrV4 address'); + }); + }); +}); diff --git a/libs/bunny-sdk/src/net/socket/v4.ts b/libs/bunny-sdk/src/net/socket/v4.ts index 6587137..5d8e7d9 100644 --- a/libs/bunny-sdk/src/net/socket/v4.ts +++ b/libs/bunny-sdk/src/net/socket/v4.ts @@ -1,9 +1,9 @@ -import { IPv4 } from "../ip.ts"; +import * as IP from "../ip.ts"; export type SocketAddrV4 = { readonly _tag: "SocketAddrV4", port: number, - ip: IPv4, + ip: IP.IPv4, }; /** @@ -16,6 +16,32 @@ export function port(addr: SocketAddrV4): number { /** * Returns the IP address associated with this socket address. */ -export function ip(addr: SocketAddrV4): IPv4 { +export function ip(addr: SocketAddrV4): IP.IPv4 { return addr.ip; } + +/** + * Try to parse a SocketAddrV4 + */ +export function tryFromString(value: string): SocketAddrV4 | SyntaxError { + const parts = value.split(':'); + + if (parts.length !== 2) { + return new SyntaxError('Invalid SocketAddrV4 address'); + } + + let ip = IP.tryParseFromString(parts[0]); + if (ip instanceof SyntaxError) { + return ip; + } + const port = Number(parts[1]); + if (isNaN(port) || port < 0 || port > 65535) { + return new SyntaxError('Invalid Port'); + } + + return ({ + _tag: "SocketAddrV4", + port, + ip, + }) +} diff --git a/libs/bunny-sdk/src/net/tcp.ts b/libs/bunny-sdk/src/net/tcp.ts index 451e6ba..e1fce7a 100644 --- a/libs/bunny-sdk/src/net/tcp.ts +++ b/libs/bunny-sdk/src/net/tcp.ts @@ -60,3 +60,15 @@ export function unstable_new(): TcpListener { } } + +/** + * Bind an Addr + */ +export function bind(addr: SocketAddr.v4.SocketAddrV4): TcpListener { + // TODO: Add a proper bind to ensure the port is available. + + return ({ + _tag: 'TcpListener', + addr, + }); +}