From 4a3b618091b4afb40c888165bd86e1cabcb43da3 Mon Sep 17 00:00:00 2001 From: Antoine Monnet Date: Thu, 30 Jan 2025 10:49:15 +0100 Subject: [PATCH] feat(url): new loadFromUrl(url, {drivers}) function --- build.config.ts | 12 +++++++++++ package.json | 5 +++++ src/index.ts | 1 + src/loader/_utils.ts | 19 +++++++++++++++++ src/loader/index.ts | 38 +++++++++++++++++++++++++++++++++ test/loader.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+) create mode 100644 src/loader/_utils.ts create mode 100644 src/loader/index.ts create mode 100644 test/loader.test.ts diff --git a/build.config.ts b/build.config.ts index c6a9e7d5..4d8bcfd5 100644 --- a/build.config.ts +++ b/build.config.ts @@ -20,6 +20,18 @@ export default defineBuildConfig({ ext: "cjs", declaration: false, }, + { + input: "src/loader/", + outDir: "dist/loader", + format: "esm", + }, + { + input: "src/loader/", + outDir: "dist/loader", + format: "cjs", + ext: "cjs", + declaration: false, + }, ], externals: ["mongodb", "unstorage", /unstorage\/drivers\//], }); diff --git a/package.json b/package.json index bdc4f0aa..8ac8bb9c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,11 @@ "types": "./dist/server.d.ts", "import": "./dist/server.mjs", "require": "./dist/server.cjs" + }, + "./loader/*": { + "types": "./dist/loader/*.d.ts", + "import": "./dist/loader/*.mjs", + "require": "./dist/loader/*.cjs" } }, "main": "./dist/index.cjs", diff --git a/src/index.ts b/src/index.ts index d8df1533..f0c547f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from "./storage"; export * from "./types"; export * from "./utils"; +export * from "./loader"; export { defineDriver } from "./drivers/utils"; diff --git a/src/loader/_utils.ts b/src/loader/_utils.ts new file mode 100644 index 00000000..ea8239ac --- /dev/null +++ b/src/loader/_utils.ts @@ -0,0 +1,19 @@ +export function coerceQuery(query: Record) { + return Object.fromEntries( + Object.entries(query).map(([key, value]: [string, string | string[]]) => { + return [key, coerceValue(value)]; + }) + ); +} + +function coerceValue(value: string | string[]): any { + if (Array.isArray(value)) return value.map((v) => coerceValue(v)); + else if (["true", "false"].includes(value.toLowerCase())) + return Boolean(value); + else if (value != "" && !Number.isNaN(Number(value))) return Number(value); + try { + return JSON.parse(value); + } catch { + return value; + } +} diff --git a/src/loader/index.ts b/src/loader/index.ts new file mode 100644 index 00000000..ddd227ef --- /dev/null +++ b/src/loader/index.ts @@ -0,0 +1,38 @@ +import { parseQuery } from "ufo"; +import { createError } from "../drivers/utils"; +import { coerceQuery } from "./_utils"; +import type { Driver, DriverFactory, AsyncDriverFactory } from "../types"; + +const RE = /^(?[^:]+):(?[^?]*)?(\?(?.*))?$/; + +export async function loadFromUrl( + url: string, + factories: Record< + string, + DriverFactory | AsyncDriverFactory + > +): Promise> { + const match = url.match(RE); + if (!match?.groups) throw createError("load-from-url", `invalid url ${url}`); + + const { scheme, base, query } = match.groups as { + scheme: string; + base?: string; + query?: string; + }; + + const factory = factories[scheme]; + if (!factory) + throw createError( + "load-from-url", + `no driver handle scheme for url ${url}` + ); + + const opts = { + base, + scheme, + ...coerceQuery(parseQuery(query)), + }; + + return await factory(opts); +} diff --git a/test/loader.test.ts b/test/loader.test.ts new file mode 100644 index 00000000..e44fc942 --- /dev/null +++ b/test/loader.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { defineDriver } from "../src"; +import { loadFromUrl } from "../src/loader"; + +interface Options { + scheme: string; + base?: string; + string?: string; + number?: number; + boolean?: boolean; + array?: string[]; + object?: Record; +} + +const test = defineDriver((options: Options) => ({ + name: "test", + options: options, + hasItem: () => false, + getItem: () => null, + getKeys: () => [], +})); + +describe("loader", () => { + it("invalid url", () => { + expect(async () => + loadFromUrl("not-a-url", { proto: test }) + ).rejects.toThrowError("invalid url"); + }); + + it("missing driver", () => { + expect(async () => + loadFromUrl("no:", { proto: test }) + ).rejects.toThrowError("no driver handle scheme for url"); + }); + + it("load driver", async () => { + const driver = await loadFromUrl( + 'proto:abc?string=def&number=1&boolean=true&array=[2,3,4]&object={"h":5,"i":6,"j":"7"}', + { proto: test } + ); + expect(driver.name).toBe("test"); + expect(driver.options.scheme).toBe("proto"); + expect(driver.options.base).toBe("abc"); + expect(driver.options.string).toBe("def"); + expect(driver.options.number).toBe(1); + expect(driver.options.boolean).toBe(true); + expect(driver.options.array).toStrictEqual([2, 3, 4]); + expect(driver.options.object).toStrictEqual({ h: 5, i: 6, j: "7" }); + }); +});