Skip to content

Commit

Permalink
feat: choice transformer
Browse files Browse the repository at this point in the history
  • Loading branch information
moontai0724 committed Oct 7, 2024
1 parent 432082d commit 6d1b0ee
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 1 deletion.
101 changes: 101 additions & 0 deletions src/transformers/choice.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { type TLiteral, type TUnion, Type } from "@sinclair/typebox";
import { expect, it } from "vitest";
import type { Options } from "yargs";

import { getChoiceOption } from "./choice";

it("should transform TUnion to yargs option", () => {
const schema = Type.Union([Type.Literal("foo"), Type.Literal("bar")]);

const result = getChoiceOption(schema);

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

it("should transform TUnion with truthy default value to yargs option", () => {
const schema = Type.Union([Type.Literal("foo"), Type.Literal("bar")], {
default: "foo",
});

const result = getChoiceOption(schema);

expect(result).toEqual({
type: "string",
requiresArg: false,
choices: ["foo", "bar"],
default: "foo",
});
});

it("should transform TUnion with falsy default value to yargs option", () => {
const schema = Type.Union([Type.Literal("foo"), Type.Literal("bar")], {
default: "",
});

const result = getChoiceOption(schema);

expect(result).toEqual({
type: "string",
requiresArg: false,
choices: ["foo", "bar"],
default: "",
});
});

it("should transform TUnion with description to yargs option", () => {
const schema = Type.Union([Type.Literal("foo"), Type.Literal("bar")], {
description: "foo",
});

const result = getChoiceOption(schema);

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

it("should transform TUnion with override to yargs option", () => {
const schema = Type.Union([Type.Literal("foo"), Type.Literal("bar")]);
const overwrite: Options = {
requiresArg: false,
alias: "aliased",
};

const result = getChoiceOption(schema, overwrite);

expect(result).toEqual({
type: "string",
requiresArg: false,
choices: ["foo", "bar"],
alias: "aliased",
});
});

it("should detect if it is optional", () => {
const schema = Type.Optional(
Type.Union([Type.Literal("foo"), Type.Literal("bar")]),
);

const result = getChoiceOption(schema);

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

it("should throw error when union contains non-literal", () => {
const schema = Type.Union([Type.String(), Type.Number()]);

expect(() =>
getChoiceOption(schema as unknown as TUnion<TLiteral[]>),
).toThrowError("Choices must contain only literal values");
});
29 changes: 29 additions & 0 deletions src/transformers/choice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { type TLiteral, type TUnion, TypeGuard } from "@sinclair/typebox";
import type { Options } from "yargs";

export function getChoiceOption(
schema: TUnion<TLiteral[]>,
override: Options = {},
): Options {
const hasDefaultValue = schema.default !== undefined;
const options = {
type: "string" as const,
requiresArg: !TypeGuard.IsOptional(schema) && !hasDefaultValue,
choices: schema.anyOf.map(item => {
if (!TypeGuard.IsLiteral(item)) {
throw new Error("Choices must contain only literal values");
}

return item.const.toString();
}),
};

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

Object.assign(options, override);

return options;
}
19 changes: 19 additions & 0 deletions src/transformers/get-option.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Options } from "yargs";

const getArrayOption = vi.fn().mockReturnValue({ type: "mocked" });
const getBooleanOption = vi.fn().mockReturnValue({ type: "mocked" });
const getChoiceOption = vi.fn().mockReturnValue({ type: "mocked" });
const getNumberOption = vi.fn().mockReturnValue({ type: "mocked" });
const getStringOption = vi.fn().mockReturnValue({ type: "mocked" });

Expand All @@ -12,6 +13,7 @@ let component: typeof import("./get-option");
beforeAll(async () => {
vi.doMock("./array", () => ({ getArrayOption }));
vi.doMock("./boolean", () => ({ getBooleanOption }));
vi.doMock("./choice", () => ({ getChoiceOption }));
vi.doMock("./number", () => ({ getNumberOption }));
vi.doMock("./string", () => ({ getStringOption }));
component = await import("./get-option");
Expand Down Expand Up @@ -89,6 +91,23 @@ describe("string", () => {
});
});

describe("union", () => {
it("should pass schema to union transformer", () => {
const schema = Type.Union([Type.Literal("foo"), Type.Literal("bar")]);
const result = component.getOption(schema);
expect(getChoiceOption).toBeCalledWith(schema, {});
expect(result).toEqual({ type: "mocked" });
});

it("should pass override to union transformer", () => {
const schema = Type.Union([Type.Literal("foo"), Type.Literal("bar")]);
const override: Options = { alias: "aliased" };
const result = component.getOption(schema, override);
expect(getChoiceOption).toBeCalledWith(schema, override);
expect(result).toEqual({ type: "mocked", alias: "aliased" });
});
});

describe("unaccepted type", () => {
it("should return empty object when type is not supported", () => {
const schema = Type.BigInt();
Expand Down
10 changes: 9 additions & 1 deletion src/transformers/get-option.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { type TSchema, TypeGuard } from "@sinclair/typebox";
import {
type TLiteral,
type TSchema,
type TUnion,
TypeGuard,
} from "@sinclair/typebox";
import type { Options } from "yargs";

import { getArrayOption } from "./array";
import { getBooleanOption } from "./boolean";
import { getChoiceOption } from "./choice";
import { getNumberOption } from "./number";
import { getStringOption } from "./string";

Expand All @@ -11,6 +17,8 @@ export function getOption(schema: TSchema, override: Options = {}) {
if (TypeGuard.IsNumber(schema)) return getNumberOption(schema, override);
if (TypeGuard.IsString(schema)) return getStringOption(schema, override);
if (TypeGuard.IsBoolean(schema)) return getBooleanOption(schema, override);
if (TypeGuard.IsUnion(schema))
return getChoiceOption(schema as TUnion<TLiteral[]>, override);
if (TypeGuard.IsArray(schema)) return getArrayOption(schema, override);

return {};
Expand Down
1 change: 1 addition & 0 deletions src/transformers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./array";
export * from "./boolean";
export * from "./choice";
export * from "./get-option";
export * from "./number";
export * from "./string";
24 changes: 24 additions & 0 deletions tests/get-option.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ describe("string", () => {
});
});

describe("union (choices)", () => {
it("should pass schema to union transformer", () => {
const schema = Type.Union([Type.Literal("foo"), Type.Literal("bar")]);
const result = component.getOption(schema);
expect(result).toEqual({
type: "string",
requiresArg: true,
choices: ["foo", "bar"],
});
});

it("should pass override to union transformer", () => {
const schema = Type.Union([Type.Literal("foo"), Type.Literal("bar")]);
const override: Options = { alias: "aliased" };
const result = component.getOption(schema, override);
expect(result).toEqual({
type: "string",
requiresArg: true,
choices: ["foo", "bar"],
alias: "aliased",
});
});
});

describe("unaccepted type", () => {
it("should return empty object when type is not supported", () => {
const schema = Type.BigInt();
Expand Down

0 comments on commit 6d1b0ee

Please sign in to comment.