Skip to content
Merged
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
103 changes: 103 additions & 0 deletions app/api/routes-f/__tests__/binary-to-text.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// @ts-nocheck
/**
* @jest-environment node
*/
import { POST, toBinary, fromBinary } from "../binary-to-text/route";
import { NextRequest } from "next/server";

function makeReq(body: unknown) {
return new NextRequest("http://localhost/api/routes-f/binary-to-text", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}

describe("/api/routes-f/binary-to-text", () => {
// --- unit helpers ---
describe("toBinary", () => {
it("encodes ASCII correctly", () => {
expect(toBinary("A", 8)).toBe("01000001");
expect(toBinary("Hi", 8)).toBe("01001000 01101001");
});

it("round-trips ASCII with fromBinary", () => {
const bin = toBinary("Hello, World!", 8);
expect(fromBinary(bin, 8)).toBe("Hello, World!");
});

it("round-trips emoji (multibyte UTF-8)", () => {
const bin = toBinary("😊", 8);
expect(fromBinary(bin, 8)).toBe("😊");
});

it("round-trips mixed ASCII + emoji", () => {
const input = "hi 🌍";
expect(fromBinary(toBinary(input, 8), 8)).toBe(input);
});
});

describe("fromBinary", () => {
it("rejects non-binary characters", () => {
expect(() => fromBinary("01000001 0100GG01", 8)).toThrow();
});

it("rejects tokens with wrong bit length", () => {
expect(() => fromBinary("0100000", 8)).toThrow(); // 7 bits
});
});

// --- POST handler ---
it("to_binary returns correct result for ASCII", async () => {
const res = await POST(makeReq({ input: "A", mode: "to_binary" }));
expect(res.status).toBe(200);
const { result } = await res.json();
expect(result).toBe("01000001");
});

it("from_binary decodes back to original ASCII", async () => {
const res = await POST(makeReq({ input: "01000001", mode: "from_binary" }));
expect(res.status).toBe(200);
const { result } = await res.json();
expect(result).toBe("A");
});

it("round-trips emoji via POST", async () => {
const encRes = await POST(makeReq({ input: "😊", mode: "to_binary" }));
const { result: bin } = await encRes.json();

const decRes = await POST(makeReq({ input: bin, mode: "from_binary" }));
expect(decRes.status).toBe(200);
const { result } = await decRes.json();
expect(result).toBe("😊");
});

it("returns 400 for malformed binary on decode", async () => {
const res = await POST(makeReq({ input: "0100GG01", mode: "from_binary" }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toBeDefined();
});

it("returns 400 for wrong bit-length token", async () => {
const res = await POST(makeReq({ input: "0100000", mode: "from_binary" }));
expect(res.status).toBe(400);
});

it("returns 400 for missing mode", async () => {
const res = await POST(makeReq({ input: "hello" }));
expect(res.status).toBe(400);
});

it("returns 400 for missing input", async () => {
const res = await POST(makeReq({ mode: "to_binary" }));
expect(res.status).toBe(400);
});

it("handles empty string to_binary", async () => {
const res = await POST(makeReq({ input: "", mode: "to_binary" }));
expect(res.status).toBe(200);
const { result } = await res.json();
expect(result).toBe("");
});
});
144 changes: 144 additions & 0 deletions app/api/routes-f/__tests__/deep-merge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// @ts-nocheck
/**
* @jest-environment node
*/
import { POST, deepMerge } from "../deep-merge/route";
import { NextRequest } from "next/server";

function makeReq(body: unknown) {
return new NextRequest("http://localhost/api/routes-f/deep-merge", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}

describe("/api/routes-f/deep-merge", () => {
describe("deepMerge helper", () => {
it("merges two flat objects", () => {
const result = deepMerge([{ a: 1 }, { b: 2 }], "replace");
expect(result).toEqual({ a: 1, b: 2 });
});

it("overwrites primitive values", () => {
const result = deepMerge([{ a: 1 }, { a: 2 }], "replace");
expect(result).toEqual({ a: 2 });
});

it("deep merges nested objects", () => {
const result = deepMerge(
[{ user: { name: "Alice", age: 30 } }, { user: { age: 31, city: "NYC" } }],
"replace"
);
expect(result).toEqual({ user: { name: "Alice", age: 31, city: "NYC" } });
});

it("replace strategy replaces arrays", () => {
const result = deepMerge([{ arr: [1, 2] }, { arr: [3, 4] }], "replace");
expect(result).toEqual({ arr: [3, 4] });
});

it("concat strategy concatenates arrays", () => {
const result = deepMerge([{ arr: [1, 2] }, { arr: [3, 4] }], "concat");
expect(result).toEqual({ arr: [1, 2, 3, 4] });
});

it("union strategy deduplicates arrays", () => {
const result = deepMerge([{ arr: [1, 2, 2] }, { arr: [2, 3] }], "union");
expect(result).toEqual({ arr: [1, 2, 3] });
});

it("handles three objects", () => {
const result = deepMerge([{ a: 1 }, { b: 2 }, { c: 3 }], "replace");
expect(result).toEqual({ a: 1, b: 2, c: 3 });
});

it("deeply nested merge", () => {
const result = deepMerge(
[
{ config: { db: { host: "localhost" } } },
{ config: { db: { port: 5432 }, cache: true } },
],
"replace"
);
expect(result).toEqual({
config: { db: { host: "localhost", port: 5432 }, cache: true },
});
});
});

describe("POST handler", () => {
it("merges two objects with default strategy", async () => {
const res = await POST(makeReq({ objects: [{ a: 1 }, { b: 2 }] }));
expect(res.status).toBe(200);
const { merged } = await res.json();
expect(merged).toEqual({ a: 1, b: 2 });
});

it("uses replace strategy by default for arrays", async () => {
const res = await POST(makeReq({ objects: [{ arr: [1] }, { arr: [2] }] }));
const { merged } = await res.json();
expect(merged.arr).toEqual([2]);
});

it("concat strategy works", async () => {
const res = await POST(
makeReq({ objects: [{ arr: [1] }, { arr: [2] }], array_strategy: "concat" })
);
const { merged } = await res.json();
expect(merged.arr).toEqual([1, 2]);
});

it("union strategy works", async () => {
const res = await POST(
makeReq({ objects: [{ arr: [1, 2] }, { arr: [2, 3] }], array_strategy: "union" })
);
const { merged } = await res.json();
expect(merged.arr).toEqual([1, 2, 3]);
});

it("deep nested merge via POST", async () => {
const res = await POST(
makeReq({
objects: [
{ user: { name: "Bob", settings: { theme: "dark" } } },
{ user: { settings: { lang: "en" } } },
],
})
);
const { merged } = await res.json();
expect(merged.user.settings).toEqual({ theme: "dark", lang: "en" });
});

it("returns 400 for empty objects array", async () => {
const res = await POST(makeReq({ objects: [] }));
expect(res.status).toBe(400);
});

it("returns 400 for missing objects", async () => {
const res = await POST(makeReq({}));
expect(res.status).toBe(400);
});

it("returns 400 for invalid array_strategy", async () => {
const res = await POST(
makeReq({ objects: [{ a: 1 }], array_strategy: "invalid" })
);
expect(res.status).toBe(400);
});

it("handles single object", async () => {
const res = await POST(makeReq({ objects: [{ a: 1, b: 2 }] }));
const { merged } = await res.json();
expect(merged).toEqual({ a: 1, b: 2 });
});

it("rejects body exceeding 2MB", async () => {
const large = { objects: [{ data: "x".repeat(3 * 1024 * 1024) }] };
const res = await POST(makeReq(large));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toContain("exceeds");
});
});
});
51 changes: 51 additions & 0 deletions app/api/routes-f/binary-to-text/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { validateBody } from "@/app/api/routes-f/_lib/validate";

const schema = z.object({
input: z.string(),
mode: z.enum(["to_binary", "from_binary"]),
bits: z.number().int().positive().optional().default(8),
});

/** Encode a UTF-8 string to space-separated binary groups. */
export function toBinary(input: string, bits: number): string {
const bytes = new TextEncoder().encode(input);
return Array.from(bytes)
.map((b) => b.toString(2).padStart(bits, "0"))
.join(" ");
}

/** Decode space-separated binary groups back to a UTF-8 string. */
export function fromBinary(input: string, bits: number): string {
const groups = input.trim().split(/\s+/);

for (const g of groups) {
if (!/^[01]+$/.test(g)) {
throw new Error(`Invalid binary token: "${g}"`);
}
if (g.length !== bits) {
throw new Error(`Token "${g}" has ${g.length} bits, expected ${bits}`);
}
}

const bytes = new Uint8Array(groups.map((g) => parseInt(g, 2)));
return new TextDecoder().decode(bytes);
}

export async function POST(request: Request): Promise<NextResponse> {
const result = await validateBody(request, schema);
if (result instanceof NextResponse) return result;

const { input, mode, bits } = result.data;

try {
const output = mode === "to_binary" ? toBinary(input, bits) : fromBinary(input, bits);
return NextResponse.json({ result: output });
} catch (err) {
return NextResponse.json(
{ error: err instanceof Error ? err.message : "Conversion failed" },
{ status: 400 }
);
}
}
86 changes: 86 additions & 0 deletions app/api/routes-f/deep-merge/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { validateBody } from "@/app/api/routes-f/_lib/validate";

type ArrayStrategy = "replace" | "concat" | "union";

const schema = z.object({
objects: z.array(z.record(z.unknown())).min(1),
array_strategy: z.enum(["replace", "concat", "union"]).optional().default("replace"),
});

const MAX_SIZE = 2 * 1024 * 1024; // 2MB

function isObject(val: unknown): val is Record<string, unknown> {
return typeof val === "object" && val !== null && !Array.isArray(val);
}

export function deepMerge(
objects: Record<string, unknown>[],
arrayStrategy: ArrayStrategy
): Record<string, unknown> {
if (objects.length === 0) return {};
if (objects.length === 1) return objects[0];

const result: Record<string, unknown> = {};

for (const obj of objects) {
for (const key in obj) {
const val = obj[key];

if (!(key in result)) {
result[key] = val;
continue;
}

const existing = result[key];

if (Array.isArray(existing) && Array.isArray(val)) {
if (arrayStrategy === "replace") {
result[key] = val;
} else if (arrayStrategy === "concat") {
result[key] = existing.concat(val);
} else if (arrayStrategy === "union") {
result[key] = Array.from(new Set([...existing, ...val]));
}
} else if (isObject(existing) && isObject(val)) {
result[key] = deepMerge([existing, val], arrayStrategy);
} else {
result[key] = val;
}
}
}

return result;
}

export async function POST(request: Request): Promise<NextResponse> {
const bodyText = await request.text();

if (bodyText.length > MAX_SIZE) {
return NextResponse.json(
{ error: `Request body exceeds ${MAX_SIZE / 1024 / 1024}MB limit` },
{ status: 400 }
);
}

let body: unknown;
try {
body = JSON.parse(bodyText);
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}

const parsed = schema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid request body", details: parsed.error.flatten() },
{ status: 400 }
);
}

const { objects, array_strategy } = parsed.data;
const merged = deepMerge(objects, array_strategy);

return NextResponse.json({ merged });
}
Loading