From 7359b6b91a33059b32d18e4a92cde985c29901fe Mon Sep 17 00:00:00 2001 From: gewfy Date: Mon, 10 Oct 2022 11:11:20 +0200 Subject: [PATCH] Capitalize output (#7) * Add toCapitalized helper and tests * Add capitalize option to transformType * Add capitalize option to generateTypes * Add capitalize option to cli * Reverted deleted test * Correct order --- src/__snapshots__/generate.spec.ts.snap | 194 ++++++++++++++++++++++++ src/cli.ts | 10 +- src/generate.spec.ts | 6 + src/generate.ts | 2 +- src/types/options.ts | 1 + src/widget/transform.spec.ts | 75 ++++++++- src/widget/transform.ts | 43 ++++-- 7 files changed, 311 insertions(+), 20 deletions(-) diff --git a/src/__snapshots__/generate.spec.ts.snap b/src/__snapshots__/generate.spec.ts.snap index e2c57d1..6bdd5a0 100644 --- a/src/__snapshots__/generate.spec.ts.snap +++ b/src/__snapshots__/generate.spec.ts.snap @@ -387,3 +387,197 @@ export interface KitchenSink { } " `; + +exports[`Output testing should support capitalization of type names 1`] = ` +"/* eslint-disable */ +/* tslint:disable */ + +export interface Posts { + title: string; + draft: boolean; + date: string; + image?: string; +} + +export interface Faq { + title: string; +} + +export interface General_Posts { + front_limit: string; + author: string; + thumb?: string; +} + +export interface General { + site_title: string; + posts: General_Posts; +} + +export interface Authors_Authors { + name: string; + description: string; +} + +export interface Authors { + authors: Authors_Authors[]; +} + +export type KitchenSink_Select_Options = \\"a\\" | \\"b\\" | \\"c\\"; + +export type KitchenSink_Select_Multiple_Options = \\"a\\" | \\"b\\" | \\"c\\"; + +export type KitchenSink_Select_Numeric_Options = 1 | 2 | 3; + +export type KitchenSink_Object_Select_Options = \\"a\\" | \\"b\\" | \\"c\\"; + +export interface KitchenSink_Object { + post: any; + string: string; + boolean: boolean; + text: string; + number: string; + markdown: string; + datetime: string; + date: string; + image: string; + file: any; + select: KitchenSink_Object_Select_Options; +} + +export type KitchenSink_List_Select_Options = \\"a\\" | \\"b\\" | \\"c\\"; + +export type KitchenSink_List_Object_Select_Options = \\"a\\" | \\"b\\" | \\"c\\"; + +export type KitchenSink_List_Object_List_Select_Options = \\"a\\" | \\"b\\" | \\"c\\"; + +export type KitchenSink_List_Object_List_Object_Select_Options = \\"a\\" | \\"b\\" | \\"c\\"; + +export interface KitchenSink_List_Object_List_Object { + string: string; + boolean: boolean; + text: string; + number: string; + markdown: string; + datetime: string; + date: string; + image: string; + file: any; + select: KitchenSink_List_Object_List_Object_Select_Options; +} + +export interface KitchenSink_List_Object_List { + post: any; + string: string; + boolean: boolean; + text: string; + number: string; + markdown: string; + datetime: string; + date: string; + image: string; + file: any; + select: KitchenSink_List_Object_List_Select_Options; + hidden: any; + object: KitchenSink_List_Object_List_Object; +} + +export interface KitchenSink_List_Object { + string: string; + boolean: boolean; + text: string; + number: string; + markdown: string; + datetime: string; + date: string; + image: string; + file: any; + select: KitchenSink_List_Object_Select_Options; + list: KitchenSink_List_Object_List[]; +} + +export interface KitchenSink_List { + string: string; + boolean: boolean; + text: string; + number: string; + markdown: string; + datetime: string; + date: string; + image: string; + file: any; + select: KitchenSink_List_Select_Options; + object: KitchenSink_List_Object; +} + +export interface KitchenSink_Typed_List_Type_2_Object_Nested { + number: string; +} + +export type KitchenSink_Typed_List_Type_2_Object_Select_Options = \\"a\\" | \\"b\\" | \\"c\\"; + +export interface KitchenSink_Typed_List_Type_1_Object { + type: \\"type_1_object\\"; + string: string; + boolean: boolean; + text: string; +} + +export interface KitchenSink_Typed_List_Type_2_Object { + type: \\"type_2_object\\"; + number: string; + nested: KitchenSink_Typed_List_Type_2_Object_Nested; + select: KitchenSink_Typed_List_Type_2_Object_Select_Options; + datetime: string; + markdown: string; +} + +export interface KitchenSink_Typed_List_Type_3_Object { + type: \\"type_3_object\\"; + date: string; + image: string; + file: any; +} + +export interface KitchenSink_Typed_List_Type_4_Object { + type: \\"type_4_object\\"; + name: string; +} + +export interface KitchenSink_Code { + code: string; + lang: string; +} + +export interface KitchenSink_Code_Alt { + alt: string; + typescript: string; +} + +export interface KitchenSink { + post: any; + title: string; + boolean: boolean; + map: string; + text: string; + number: string; + markdown: string; + datetime: string; + date: string; + color: string; + colorEditable: string; + image: string; + file: any; + select: KitchenSink_Select_Options; + select_multiple: KitchenSink_Select_Multiple_Options[]; + select_numeric: KitchenSink_Select_Numeric_Options; + hidden: any; + object: KitchenSink_Object; + list: KitchenSink_List[]; + typed_list: (KitchenSink_Typed_List_Type_1_Object | KitchenSink_Typed_List_Type_2_Object | KitchenSink_Typed_List_Type_3_Object | KitchenSink_Typed_List_Type_4_Object)[]; + code: KitchenSink_Code; + code_alt: KitchenSink_Code_Alt; + snippet: string; +} +" +`; diff --git a/src/cli.ts b/src/cli.ts index 48a443c..8a2cf5d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -18,13 +18,19 @@ const args = yargs default: true, describe: `use 'label_singular' or 'label' as interface name`, type: "boolean", + }) + .option("capitalize", { + demandOption: false, + default: false, + describe: "capitalize type names", + type: "boolean", }).argv; let spinner: ora.Ora; export const run = async (): Promise => { try { - const { input, output = OUTPUT_FILENAME, label } = await args; + const { input, output = OUTPUT_FILENAME, label, capitalize } = await args; spinner = ora("Loading config").start(); @@ -32,7 +38,7 @@ export const run = async (): Promise => { spinner.succeed().start("Generating types"); - const types = generateTypes(collections, { label }); + const types = generateTypes(collections, { label, capitalize }); spinner.succeed().start("Saving file"); diff --git a/src/generate.spec.ts b/src/generate.spec.ts index 32368f7..7f249c3 100644 --- a/src/generate.spec.ts +++ b/src/generate.spec.ts @@ -13,4 +13,10 @@ describe("Output testing", () => { expect(generateTypes(collections, { label: true })).toMatchSnapshot(); }); + + it("should support capitalization of type names", () => { + const collections = loadConfig("kitchen-sink.yml"); + + expect(generateTypes(collections, { capitalize: true })).toMatchSnapshot(); + }); }); diff --git a/src/generate.ts b/src/generate.ts index 87ef90c..01827d6 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -10,7 +10,7 @@ export const generateTypes = ( return collections .flatMap(pullCollection) .map(resolveWidget) - .reduce(transformType({ label: !!options.label }), [[], []]) + .reduce(transformType({ label: !!options.label, capitalize: !!options.capitalize }), [[], []]) .flat() .map(resolveRelations) .map(formatType) diff --git a/src/types/options.ts b/src/types/options.ts index 77d3692..c84d7ee 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -1,3 +1,4 @@ export interface NetlifyTsOptions { label?: boolean; + capitalize?: boolean; } diff --git a/src/widget/transform.spec.ts b/src/widget/transform.spec.ts index 06b0310..46da9fe 100644 --- a/src/widget/transform.spec.ts +++ b/src/widget/transform.spec.ts @@ -6,6 +6,7 @@ import { pullType, sortTypes, TransformState, + toCapitalized, } from "./transform"; describe("Widget transformation", () => { @@ -546,6 +547,50 @@ describe("Widget transformation", () => { }); }); + describe("'capitalize' option", () => { + it("should capitalize names", () => { + expect( + parse( + { + name: "users", + required: true, + multiple: true, + singularLabel: "User", + type: [ + { name: "name", type: "string", required: true, multiple: false }, + { name: "active", type: "boolean", required: true, multiple: false }, + ], + }, + { prefix: "parent", capitalize: true }, + ), + ).toEqual([ + ["users: Parent_Users[];"], + ["interface Parent_Users { name: string; active: boolean; }"], + ]); + }); + + it("should work together with label option", () => { + expect( + parse( + { + name: "users", + required: true, + multiple: true, + singularLabel: "user", + type: [ + { name: "name", type: "string", required: true, multiple: false }, + { name: "active", type: "boolean", required: true, multiple: false }, + ], + }, + { prefix: "parent", capitalize: true, label: true }, + ), + ).toEqual([ + ["users: Parent_User[];"], + ["interface Parent_User { name: string; active: boolean; }"], + ]); + }); + }); + it("should parse typed lists with labels", () => { expect( parse( @@ -578,7 +623,13 @@ describe("Widget transformation", () => { required: true, multiple: false, type: [ - { name: "id", singularLabel: "my_id", type: "number", required: true, multiple: false }, + { + name: "id", + singularLabel: "my_id", + type: "number", + required: true, + multiple: false, + }, ], }, ], @@ -648,3 +699,25 @@ describe("Pull typename", () => { expect(pullType("one: parent_list_one;")).toEqual("parent_list_one"); }); }); + +describe("Capitalization", () => { + it("should capitalize words separated by spaces", () => { + const str = "space separated string"; + expect(toCapitalized(str)).toBe("Space Separated String"); + }); + + it("should capitalize words separated by dashes", () => { + const str = "dash-separated-string"; + expect(toCapitalized(str)).toBe("Dash-Separated-String"); + }); + + it("should capitalize words separated by underscores", () => { + const str = "underscore_separated_string"; + expect(toCapitalized(str)).toBe("Underscore_Separated_String"); + }); + + it("should handle mixed separators", () => { + const str = "string with-mixed_separators"; + expect(toCapitalized(str)).toBe("String With-Mixed_Separators"); + }); +}); diff --git a/src/widget/transform.ts b/src/widget/transform.ts index 99023e9..87faba1 100644 --- a/src/widget/transform.ts +++ b/src/widget/transform.ts @@ -1,12 +1,21 @@ import type { Widget } from "../types"; -export const getWidgetName = (widget: Widget, useLabel: boolean): string => - useLabel - ? (widget.singularLabel || widget.label || widget.name).replace(/\s./gi, toCamelCase) - : widget.name; +export const getName = (name: string, capitalize: boolean) => + capitalize ? toCapitalized(name) : name; + +export const getWidgetName = (widget: Widget, useLabel: boolean, capitalize: boolean): string => + getName( + useLabel + ? (widget.singularLabel || widget.label || widget.name).replace(/\s./gi, toCamelCase) + : widget.name, + capitalize, + ); export const toCamelCase = (str: string): string => str.slice(1).toUpperCase(); +export const toCapitalized = (str: string) => + str.replace(/(^|[\s-_])(\w)/g, (match, separator, char) => `${separator}${char.toUpperCase()}`); + export const wrapEnum = (item: number | string): string => typeof item === "number" ? `${item}` : `"${item}"`; @@ -67,17 +76,18 @@ const empty: TypeArray = [[], []]; export interface TransformState { prefix?: string; label?: boolean; + capitalize?: boolean; } export const transformType = - ({ prefix = "", label = false }: TransformState = {}) => + ({ prefix = "", label = false, capitalize = false }: TransformState = {}) => (types: TypeArray, widget: Widget): TypeArray => { const required = !widget.required ? "?" : ""; const multiple = widget.multiple ? "[]" : ""; - const widgetName = getWidgetName(widget, label); + const widgetName = getWidgetName(widget, label, capitalize); - const name = prefix ? `${prefix}_${widgetName}` : widgetName; + const name = getName(prefix ? `${prefix}_${widgetName}` : widgetName, capitalize); if (!Array.isArray(widget.type)) { // if widget name is `body` @@ -97,7 +107,7 @@ export const transformType = // single field list logic - const child = transformType({ prefix: name, label })(empty, { + const child = transformType({ prefix: name, label, capitalize })(empty, { ...widget.type, multiple: widget.multiple, required: widget.required, @@ -129,13 +139,12 @@ export const transformType = // check if enum list if (typeof iterator[0] === "string" || typeof iterator[0] === "number") { + const enumName = getName(`${name}_options`, capitalize); return [ - [...types[0], `${widget.name}${required}: ${name}_options${multiple};`], + [...types[0], `${widget.name}${required}: ${enumName}${multiple};`], [ ...types[1], - `type ${name}_options = ${(iterator as (string | number)[]) - .map(wrapEnum) - .join(" | ")};`, + `type ${enumName} = ${(iterator as (string | number)[]).map(wrapEnum).join(" | ")};`, ], ]; } @@ -144,12 +153,14 @@ export const transformType = if (Array.isArray(iterator[0])) { const [typeKey, ...objects] = iterator[0]; const [names, interfaces] = (objects as Widget[]).reduce( - transformType({ prefix: name, label }), + transformType({ prefix: name, label, capitalize }), empty, ); const pattern = new RegExp( - `(${(objects as Widget[]).map((w) => `${name}_${getWidgetName(w, label)}`).join("|")}) {`, + `(${(objects as Widget[]) + .map((w) => `${name}_${getWidgetName(w, label, capitalize)}`) + .join("|")}) {`, ); // sort typed lists last and splice rest @@ -169,7 +180,7 @@ export const transformType = // root level collection if (!prefix) { const [fields, interfaces] = (iterator as Widget[]).reduce( - transformType({ prefix: widgetName, label }), + transformType({ prefix: widgetName, label, capitalize }), empty, ); @@ -181,7 +192,7 @@ export const transformType = // object field const [fields, interfaces] = (iterator as Widget[]).reduce( - transformType({ prefix: name, label }), + transformType({ prefix: name, label, capitalize }), [[], []], );