Completed library conversion.

This commit is contained in:
Jay
2025-10-24 14:44:25 -04:00
parent eec1b352ed
commit 6726526a48
8 changed files with 744 additions and 17 deletions

483
src/filter_json.test.ts Normal file
View File

@@ -0,0 +1,483 @@
import { describe, expect, test } from "vitest";
import { FilterJSON } from "./filter_json";
import type { FilterData } from "./types";
interface FilterMarshalTestCase {
name: string;
filter: FilterData;
expected: string;
}
interface FilterUnmarshalTestCase {
name: string;
input: string;
expected: FilterData;
}
interface FilterRoundTripTestCase {
name: string;
filter: FilterData;
}
const marshalTestCases: FilterMarshalTestCase[] = [
{
name: "empty filter",
filter: {},
expected: "{}",
},
{
name: "undefined IDs",
filter: { ids: undefined },
expected: "{}",
},
{
name: "null IDs",
filter: { ids: null },
expected: '{"ids": null}',
},
{
name: "empty IDs",
filter: { ids: [] },
expected: '{"ids":[]}',
},
{
name: "populated IDs",
filter: { ids: ["abc", "123"] },
expected: '{"ids":["abc","123"]}',
},
{
name: "undefined Authors",
filter: { authors: undefined },
expected: "{}",
},
{
name: "null Authors",
filter: { authors: null },
expected: '{"authors": null}',
},
{
name: "empty Authors",
filter: { authors: [] },
expected: '{"authors":[]}',
},
{
name: "populated Authors",
filter: { authors: ["abc", "123"] },
expected: '{"authors":["abc","123"]}',
},
{
name: "undefined Kinds",
filter: { kinds: undefined },
expected: "{}",
},
{
name: "null Kinds",
filter: { kinds: null },
expected: '{"kinds": null}',
},
{
name: "empty Kinds",
filter: { kinds: [] },
expected: '{"kinds":[]}',
},
{
name: "populated Kinds",
filter: { kinds: [1, 20001] },
expected: '{"kinds":[1,20001]}',
},
{
name: "undefined Since",
filter: { since: undefined },
expected: "{}",
},
{
name: "null Since",
filter: { since: null },
expected: '{"since": null}',
},
{
name: "populated Since",
filter: { since: 1000 },
expected: '{"since":1000}',
},
{
name: "undefined Until",
filter: { until: undefined },
expected: "{}",
},
{
name: "null Until",
filter: { until: null },
expected: '{"until": null}',
},
{
name: "populated Until",
filter: { until: 1000 },
expected: '{"until":1000}',
},
{
name: "undefined Limit",
filter: { limit: undefined },
expected: "{}",
},
{
name: "null Limit",
filter: { limit: null },
expected: '{"limit": null}',
},
{
name: "populated Limit",
filter: { limit: 100 },
expected: '{"limit":100}',
},
{
name: "all standard fields",
filter: {
ids: ["abc", "123"],
authors: ["def", "456"],
kinds: [1, 200, 3000],
since: 1000,
until: 2000,
limit: 100,
},
expected:
'{"ids":["abc","123"],"authors":["def","456"],"kinds":[1,200,3000],"since":1000,"until":2000,"limit":100}',
},
{
name: "mixed fields",
filter: { ids: undefined, authors: [], kinds: [1] },
expected: '{"authors":[],"kinds":[1]}',
},
{
name: "undefined tags map",
filter: { tags: undefined },
expected: "{}",
},
{
name: "null tags map",
filter: { tags: null },
expected: "{}",
},
{
name: "single-letter tag",
filter: { tags: { e: ["event1"] } },
expected: '{"#e":["event1"]}',
},
{
name: "multi-letter tag",
filter: { tags: { emoji: ["🔥", "💧"] } },
expected: '{"#emoji":["🔥","💧"]}',
},
{
name: "null tag array",
filter: { tags: { p: null } },
expected: '{"#p":null}',
},
{
name: "empty tag array",
filter: { tags: { p: [] } },
expected: '{"#p":[]}',
},
{
name: "multiple tags",
filter: {
tags: {
e: ["event1", "event2"],
p: ["pubkey1", "pubkey2"],
},
},
expected: '{"#e":["event1","event2"],"#p":["pubkey1","pubkey2"]}',
},
{
name: "simple extension",
filter: { extensions: { search: "query" } },
expected: '{"search":"query"}',
},
{
name: "extension with nested object",
filter: { extensions: { meta: { author: "alice", score: 99 } } },
expected: '{"meta":{"author":"alice","score":99}}',
},
{
name: "extension with nested array",
filter: { extensions: { items: [1, 2, 3] } },
expected: '{"items":[1,2,3]}',
},
{
name: "extension with complex nested structure",
filter: { extensions: { data: { users: [{ id: 1 }], count: 5 } } },
expected: '{"data":{"users":[{"id":1}],"count":5}}',
},
{
name: "multiple extensions",
filter: { extensions: { search: "x", depth: 3 } },
expected: '{"search":"x","depth":3}',
},
{
name: "extension collides with standard field - IDs",
filter: { ids: ["real"], extensions: { ids: ["fake"] } },
expected: '{"ids":["real"]}',
},
{
name: "extension collides with standard field - Since",
filter: { since: 100, extensions: { since: 999 } },
expected: '{"since":100}',
},
{
name: "extension collides with multiple standard fields",
filter: {
authors: ["a"],
kinds: [1],
extensions: { authors: ["b"], kinds: [2] },
},
expected: '{"authors":["a"],"kinds":[1]}',
},
{
name: "extension collides with tag field - #e",
filter: { extensions: { "#e": ["fakeevent"] } },
expected: "{}",
},
{
name: "extension collides with standard and tag fields",
filter: {
authors: ["realauthor"],
tags: { e: ["realevent"] },
extensions: { authors: ["fakeauthor"], "#e": ["fakeevent"] },
},
expected: '{"authors":["realauthor"],"#e":["realevent"]}',
},
{
name: "filter with all field types",
filter: {
ids: ["x"],
since: 100,
tags: { e: ["y"] },
extensions: { search: "z", ids: ["fakeid"] },
},
expected: '{"ids":["x"],"since":100,"#e":["y"],"search":"z"}',
},
];
const unmarshalTestCases: FilterUnmarshalTestCase[] = [
{
name: "empty object",
input: "{}",
expected: {},
},
{
name: "null IDs",
input: '{"ids": null}',
expected: { ids: null },
},
{
name: "empty IDs",
input: '{"ids": []}',
expected: { ids: [] },
},
{
name: "populated IDs",
input: '{"ids": ["abc","123"]}',
expected: { ids: ["abc", "123"] },
},
{
name: "null Authors",
input: '{"authors": null}',
expected: { authors: null },
},
{
name: "empty Authors",
input: '{"authors": []}',
expected: { authors: [] },
},
{
name: "populated Authors",
input: '{"authors": ["abc","123"]}',
expected: { authors: ["abc", "123"] },
},
{
name: "null Kinds",
input: '{"kinds": null}',
expected: { kinds: null },
},
{
name: "empty Kinds",
input: '{"kinds": []}',
expected: { kinds: [] },
},
{
name: "populated Kinds",
input: '{"kinds": [1,2,3]}',
expected: { kinds: [1, 2, 3] },
},
{
name: "null Since",
input: '{"since": null}',
expected: { since: undefined },
},
{
name: "populated Since",
input: '{"since": 1000}',
expected: { since: 1000 },
},
{
name: "null Until",
input: '{"until": null}',
expected: { until: undefined },
},
{
name: "populated Until",
input: '{"until": 1000}',
expected: { until: 1000 },
},
{
name: "null Limit",
input: '{"limit": null}',
expected: { limit: undefined },
},
{
name: "populated Limit",
input: '{"limit": 1000}',
expected: { limit: 1000 },
},
{
name: "all standard fields",
input:
'{"ids":["abc","123"],"authors":["def","456"],"kinds":[1,200,3000],"since":1000,"until":2000,"limit":100}',
expected: {
ids: ["abc", "123"],
authors: ["def", "456"],
kinds: [1, 200, 3000],
since: 1000,
until: 2000,
limit: 100,
},
},
{
name: "mixed fields",
input: '{"ids": null, "authors": [], "kinds": [1]}',
expected: { ids: null, authors: [], kinds: [1] },
},
{
name: "zero int pointers",
input: '{"since": 0, "until": 0, "limit": 0}',
expected: { since: 0, until: 0, limit: 0 },
},
{
name: "single-letter tag",
input: '{"#e":["event1"]}',
expected: { tags: { e: ["event1"] } },
},
{
name: "multi-letter tag",
input: '{"#emoji":["🔥","💧"]}',
expected: { tags: { emoji: ["🔥", "💧"] } },
},
{
name: "empty tag array",
input: '{"#p":[]}',
expected: { tags: { p: [] } },
},
{
name: "multiple tags",
input: '{"#p":["pubkey1","pubkey2"],"#e":["event1","event2"]}',
expected: {
tags: {
p: ["pubkey1", "pubkey2"],
e: ["event1", "event2"],
},
},
},
{
name: "null tag",
input: '{"#p":null}',
expected: { tags: { p: null } },
},
{
name: "simple extension",
input: '{"search":"query"}',
expected: { extensions: { search: "query" } },
},
{
name: "extension with nested object",
input: '{"meta":{"author":"alice","score":99}}',
expected: { extensions: { meta: { author: "alice", score: 99 } } },
},
{
name: "extension with nested array",
input: '{"items":[1,2,3]}',
expected: { extensions: { items: [1, 2, 3] } },
},
{
name: "extension with complex nested structure",
input: '{"data":{"level1":{"level2":[{"id":1}]}}}',
expected: { extensions: { data: { level1: { level2: [{ id: 1 }] } } } },
},
{
name: "multiple extensions",
input: '{"search":"x","custom":true,"depth":3}',
expected: { extensions: { search: "x", custom: true, depth: 3 } },
},
{
name: "extension with null value",
input: '{"optional":null}',
expected: { extensions: { optional: null } },
},
{
name: "kitchen sink",
input: '{"ids":["x"],"since":100,"#e":["y"],"search":"z"}',
expected: {
ids: ["x"],
since: 100,
tags: { e: ["y"] },
extensions: { search: "z" },
},
},
];
const roundTripTestCases: FilterRoundTripTestCase[] = [
{
name: "fully populated filter",
filter: {
ids: ["x"],
since: 100,
tags: { e: ["y"] },
extensions: { search: "z" },
},
},
];
describe("FilterJSON.toJSON", () => {
test.each(marshalTestCases)("$name", ({ filter, expected }) => {
const result = JSON.stringify(FilterJSON.toJSON(filter));
const expectedObj = JSON.parse(expected);
const actualObj = JSON.parse(result);
expect(actualObj).toEqual(expectedObj);
});
});
describe("FilterJSON.fromJSON", () => {
test.each(unmarshalTestCases)("$name", ({ input, expected }) => {
const result = FilterJSON.fromJSON(JSON.parse(input));
expectEqualFilters(result, expected);
});
});
describe("FilterJSON round trip", () => {
test.each(roundTripTestCases)("$name", ({ filter }) => {
const jsonBytes = JSON.stringify(FilterJSON.toJSON(filter));
const result = FilterJSON.fromJSON(JSON.parse(jsonBytes));
expectEqualFilters(result, filter);
});
});
function expectEqualFilters(got: FilterData, want: FilterData): void {
expect(got.ids).toEqual(want.ids);
expect(got.authors).toEqual(want.authors);
expect(got.kinds).toEqual(want.kinds);
expect(got.since).toEqual(want.since);
expect(got.until).toEqual(want.until);
expect(got.limit).toEqual(want.limit);
expect(got.tags).toEqual(want.tags);
expect(got.extensions).toEqual(want.extensions);
}