Skip to content

Commit

Permalink
refactor: dry out transform of basic properties
Browse files Browse the repository at this point in the history
also fixed some missing use cases
  • Loading branch information
moontai0724 committed Oct 8, 2024
1 parent 710981c commit 5cae68b
Show file tree
Hide file tree
Showing 20 changed files with 643 additions and 661 deletions.
24 changes: 24 additions & 0 deletions src/helpers/is-union-literal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Type } from "@sinclair/typebox";
import { expect, it } from "vitest";

import { isUnionLiteral } from "./is-union-literal";

it("should return true if schema is TUnion of TLiteral", () => {
const schema = Type.Union([
Type.Literal("foo"),
Type.Literal(10),
Type.Literal(true),
]);

const result = isUnionLiteral(schema);

expect(result).toBe(true);
});

it("should return false if schema is not TUnion of TLiteral", () => {
const schema = Type.Union([Type.Literal("foo")]);

const result = isUnionLiteral(schema);

expect(result).toBe(false);
});
12 changes: 12 additions & 0 deletions src/helpers/is-union-literal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {
type TLiteral,
type TSchema,
type TUnion,
TypeGuard,
} from "@sinclair/typebox";

export function isUnionLiteral(schema: TSchema): schema is TUnion<TLiteral[]> {
if (!TypeGuard.IsUnion(schema)) return false;

return schema.anyOf.every(item => TypeGuard.IsLiteral(item));
}
16 changes: 16 additions & 0 deletions src/helpers/t-union-to-tuple.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Type } from "@sinclair/typebox";
import { expect, it } from "vitest";

import { tUnionToTuple } from "./t-union-to-tuple";

it("should transform TUnion to tuple", () => {
const schema = Type.Union([
Type.Literal("foo"),
Type.Literal(10),
Type.Literal(true),
]);

const result = tUnionToTuple(schema);

expect(result).toEqual(["foo", 10, true]);
});
12 changes: 12 additions & 0 deletions src/helpers/t-union-to-tuple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {
type Static,
type TLiteral,
type TUnion,
type UnionToTuple,
} from "@sinclair/typebox";

export function tUnionToTuple<S extends TUnion<TLiteral[]>>(
schema: S,
): UnionToTuple<Static<S>> {
return schema.anyOf.map(item => item.const) as never;
}
163 changes: 68 additions & 95 deletions src/transformers/array.spec.ts
Original file line number Diff line number Diff line change
@@ -1,120 +1,93 @@
import { Type } from "@sinclair/typebox";
import { describe, expect, it } from "vitest";
import { beforeAll, beforeEach, expect, it, vi } from "vitest";
import type { Options } from "yargs";

import { getArrayOption } from "./array";
const isUnionLiteral = vi.fn();
const tUnionToTuple = vi.fn();
const transform = vi.fn();

it("should transform TArray to yargs option", () => {
const schema = Type.Array(Type.String());
let getArrayOption: typeof import("./array").getArrayOption;

const result = getArrayOption(schema);
beforeAll(async () => {
vi.doMock("@/helpers/is-union-literal", () => ({ isUnionLiteral }));
vi.doMock("@/helpers/t-union-to-tuple", () => ({ tUnionToTuple }));
vi.doMock("./transform", () => ({ transform }));

expect(result).toEqual({
type: "array",
requiresArg: true,
});
getArrayOption = await import("./array").then(m => m.getArrayOption);
});

describe("should transform TArray with choices to yargs option", () => {
it("when item is literal", () => {
const schema = Type.Array(Type.Literal("foo"));

const result = getArrayOption(schema);

expect(result).toEqual({
type: "array",
requiresArg: true,
choices: ["foo"],
});
});

it("when items are union of literal", () => {
const schema = Type.Array(
Type.Union([Type.Literal("foo"), Type.Literal("bar")]),
);

const result = getArrayOption(schema);

expect(result).toEqual({
type: "array",
requiresArg: true,
choices: ["foo", "bar"],
});
});

it("should not transform when items are union of literal and non-literal", () => {
const schema = Type.Array(
Type.Union([Type.Literal("foo"), Type.Literal("bar"), Type.Boolean()]),
);

const result = getArrayOption(schema);

expect(result).toEqual({
type: "array",
requiresArg: true,
});
});
beforeEach(() => {
vi.resetAllMocks();
});

it("should transform TArray with truthy default value to yargs option", () => {
const schema = Type.Array(Type.String(), { default: ["foo"] });
it("should call transform to transform", () => {
const schema = Type.Array(Type.Any());
const expectedResponse = { mocked: true };
transform.mockReturnValue(expectedResponse);

const result = getArrayOption(schema);
const response = getArrayOption(schema);

expect(result).toEqual({
type: "array",
requiresArg: false,
default: ["foo"],
});
expect(transform).toBeCalledWith("array", schema, {});
expect(response).toEqual(expectedResponse);
});

it("should transform TArray with falsy default value to yargs option", () => {
const schema = Type.Array(Type.String(), { default: [] });
it("should take its value as choices if it is literal", () => {
const schema = Type.Array(Type.Union([Type.Literal("foo")]));
const expectedResponse = { mocked: true };
transform.mockReturnValue(expectedResponse);

const result = getArrayOption(schema);
const response = getArrayOption(schema);

expect(result).toEqual({
type: "array",
requiresArg: false,
default: [],
});
});

it("should transform TArray with description to yargs option", () => {
const schema = Type.Array(Type.String(), { description: "foo" });
const expectedOverwrites = {
choices: ["foo"],
} satisfies Options;

const result = getArrayOption(schema);

expect(result).toEqual({
type: "array",
requiresArg: true,
description: "foo",
});
expect(isUnionLiteral).not.toBeCalled();
expect(tUnionToTuple).not.toBeCalled();
expect(transform).toBeCalledWith("array", schema, expectedOverwrites);
expect(response).toEqual(expectedResponse);
});

it("should transform TArray with override to yargs option", () => {
const schema = Type.Array(Type.String());
const overwrite: Options = {
requiresArg: false,
alias: "aliased",
};

const result = getArrayOption(schema, overwrite);
it("should take union of literals to tuple of literals as value of choices then call transform to transform", () => {
const schema = Type.Array(
Type.Union([Type.Literal("foo"), Type.Literal(10), Type.Literal(true)]),
);
const expectedResponse = { mocked: true };
isUnionLiteral.mockReturnValueOnce(true);
tUnionToTuple.mockReturnValueOnce(["foo", 10, true]);
transform.mockReturnValue(expectedResponse);

const response = getArrayOption(schema);

const expectedOverwrites = {
choices: ["foo", 10, true],
} satisfies Options;

expect(isUnionLiteral).toBeCalledWith(schema.items);
expect(tUnionToTuple).toBeCalledWith(schema.items);
expect(transform).toBeCalledWith("array", schema, expectedOverwrites);
expect(response).toEqual(expectedResponse);
});

expect(result).toEqual({
type: "array",
requiresArg: false,
it("should call transform with overwrites", () => {
const schema = Type.Array(
Type.Union([Type.Literal("foo"), Type.Literal(10), Type.Literal(true)]),
);
const overwrites = {
alias: "aliased",
});
});
choices: ["foo", "bar"],
} satisfies Options;
const expectedResponse = { mocked: true, ...overwrites };
isUnionLiteral.mockReturnValueOnce(true);
tUnionToTuple.mockReturnValueOnce(["foo", 10, true]);
transform.mockReturnValue(expectedResponse);

it("should detect if it is optional", () => {
const schema = Type.Optional(Type.Array(Type.String()));
const response = getArrayOption(schema, overwrites);

const result = getArrayOption(schema);
const expectedOverwrites = overwrites satisfies Options;

expect(result).toEqual({
type: "array",
requiresArg: false,
});
expect(isUnionLiteral).toBeCalledWith(schema.items);
expect(tUnionToTuple).toBeCalledWith(schema.items);
expect(transform).toBeCalledWith("array", schema, expectedOverwrites);
expect(response).toEqual(expectedResponse);
});
73 changes: 26 additions & 47 deletions src/transformers/array.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,45 @@
import {
type Static,
type TArray,
type TLiteral,
type TSchema,
type TUnion,
TypeGuard,
type UnionToTuple,
} from "@sinclair/typebox";
import type { Options } from "yargs";

function getLiteralValue(schema: TLiteral): string {
return schema.const.toString();
}

function getValue(schema: TSchema): string | undefined {
if (TypeGuard.IsLiteral(schema)) return getLiteralValue(schema);

return undefined;
}

function getUnionValues(schema: TUnion): string[] | undefined {
const values: string[] = [];

const isAllValid = schema.anyOf.every(item => {
const value = getValue(item);
if (!value) return false;
import { isUnionLiteral } from "@/helpers/is-union-literal";
import { tUnionToTuple } from "@/helpers/t-union-to-tuple";

values.push(value);
import { transform } from "./transform";

return true;
});
type GetChoices<T extends TSchema> = T extends TLiteral
? [Static<T>]
: T extends TUnion<TLiteral[]>
? UnionToTuple<Static<T>>
: never;

if (!isAllValid) return undefined;

return values;
}

function getChoices(schema: TSchema): string[] | undefined {
if (TypeGuard.IsLiteral(schema)) return [getLiteralValue(schema)];
if (TypeGuard.IsUnion(schema)) return getUnionValues(schema);
function getChoices<T extends TSchema>(schema: T): GetChoices<T>;
function getChoices(schema: TSchema) {
if (TypeGuard.IsLiteral(schema)) return [schema.const];
if (isUnionLiteral(schema)) return tUnionToTuple(schema);

return undefined;
}

export function getArrayOption(
schema: TArray,
override: Options = {},
): Options {
const hasDefaultValue = schema.default !== undefined;
const options = {
type: "array" as const,
requiresArg: !TypeGuard.IsOptional(schema) && !hasDefaultValue,
export function getArrayOption<S extends TArray, O extends Options = object>(
schema: S,
overwrites: O = {} as never,
) {
const choices = getChoices(schema.items) as GetChoices<S["items"]>;
const mergedOverwrites = {
// @ts-expect-error We expect this type to be calculated, but seems it's too
// long for ts. If it been proved to be not nessesary, we can remove this
// type inference.
choices,
...overwrites,
};

if (hasDefaultValue)
Object.assign(options, { default: schema.default as unknown[] });
if (schema.description)
Object.assign(options, { description: schema.description });

const choices = getChoices(schema.items);

if (choices) Object.assign(options, { choices });

Object.assign(options, override);

return options;
return transform("array", schema, mergedOverwrites);
}
Loading

0 comments on commit 5cae68b

Please sign in to comment.