astahmer / openapi-zod-client

Generate a zodios (typescript http client with zod validation) from an OpenAPI spec (json/yaml)
openapi-zod-client.vercel.app
737 stars 82 forks source link

Can't figure out how to escape angled brackets `<>` w/ Handlebars #300

Open andenacitelli opened 1 month ago

andenacitelli commented 1 month ago

Describe the bug

I'm seeing angled brackets <> that are not being used in a HTML context still be stripped, and have been unable to figure out how to properly escape them.

Minimal reproduction

I'm running this command (the OpenAPI spec is a public link):

openapi-zod-client https://raw.githubusercontent.com/aacitelli/credit-card-bonuses-api/main/src/api.yaml -t schemas-only.hbs --output src/.generated/open-api-zod.ts --export-schemas

With this Handlebars template:

import { z } from "zod";

{{#each schemas}}
export const {{@key}} = {{{this}}};
export type {{@key}} = z.infer<typeof {{@key}}>;
{{/each}}

export const schemas = {
{{#each schemas}}
    {{@key}},
{{/each}}
};

But I'm seeing the <typeof ...> part seemingly get treated as HTML-related and wiped by the Handlebars process. This is what it looks like:

import { z } from "zod";

export const IssuersEnum = z.enum([
  "AMERICAN_EXPRESS",
  "BANK_OF_AMERICA",
  "BARCLAYS",
  "BREX",
  "CHASE",
  "CAPITAL_ONE",
  "CITI",
  "FIRST",
  "FNBO",
  "PENFED",
  "PNC",
  "SYNCHRONY",
  "US_BANK",
  "WELLS_FARGO",
]);
export type IssuersEnum = z.infer;
export const NetworksEnum = z.enum([
  "VISA",
  "MASTERCARD",
  "AMERICAN_EXPRESS",
  "DISCOVER",
]);
export type NetworksEnum = z.infer;
export const CurrenciesEnum = z.enum([
  "BEST_WESTERN",
  "BREEZE",
  "HILTON",
  "HYATT",
  "IHG",
  "MARRIOTT",
  "RADISSON",
  "WYNDHAM",
  "CHOICE",
  "AEROPLAN",
  "ALASKA",
  "AMERICAN",
  "ANA",
  "AVIANCA",
  "AVIOS",
  "CATHAY_PACIFIC",
  "DELTA",
  "EMIRATES",
  "FRONTIER",
  "FLYING_BLUE",
  "HAWAIIAN",
  "JETBLUE",
  "KOREAN",
  "LATAM",
  "LUFTHANSA",
  "SOUTHWEST",
  "SPIRIT",
  "UNITED",
  "VIRGIN",
  "AMERICAN_EXPRESS",
  "BANK_OF_AMERICA",
  "BARCLAYS",
  "BILT",
  "BREX",
  "CHASE",
  "CITI",
  "CAPITAL_ONE",
  "DISCOVER",
  "US_BANK",
  "WELLS_FARGO",
  "CARNIVAL",
  "AMTRAK",
  "PENFED",
  "USD",
]);
export type CurrenciesEnum = z.infer;
export const Credit = z
  .object({
    description: z.string().min(1),
    value: z.number(),
    weight: z.number().gte(0).lte(1),
    currency: CurrenciesEnum.optional(),
  })
  .passthrough();
export type Credit = z.infer;
export const OfferAmount = z
  .object({ amount: z.number(), currency: CurrenciesEnum.optional() })
  .passthrough();
export type OfferAmount = z.infer;
export const Offer = z
  .object({
    spend: z.number().gte(0),
    amount: z.array(OfferAmount),
    days: z.number().gte(30),
    expiration: z.string().optional(),
    isPublic: z.boolean().optional(),
    credits: z.array(Credit),
    details: z.string().optional(),
    url: z.string().optional(),
    referralUrl: z.string().optional(),
  })
  .passthrough();
export type Offer = z.infer;
export const CreditCard = z
  .object({
    name: z.string().min(1),
    issuer: IssuersEnum,
    network: NetworksEnum,
    currency: CurrenciesEnum,
    countsTowards524: z.boolean().optional(),
    details: z.string().optional(),
    isBusiness: z.boolean(),
    annualFee: z.number(),
    isAnnualFeeWaived: z.boolean(),
    universalCashbackPercent: z.number().gte(1).lte(100),
    url: z.string(),
    imageUrl: z.string(),
    credits: z.array(Credit),
    offers: z.array(Offer),
    historicalOffers: z.array(Offer),
  })
  .passthrough();
export type CreditCard = z.infer;

export const schemas = {
  IssuersEnum,
  NetworksEnum,
  CurrenciesEnum,
  Credit,
  OfferAmount,
  Offer,
  CreditCard,
};

Expected behavior

I expect the z.infer to have the <typeof xyz_schema_name> present.

Additional context

This started happening a few months back and I haven't been able to nail down why, as it used to work.

codemariner commented 3 weeks ago

i'm just now also running into this same problem. There's something wonky about how it processes the handlebars template. it's like it sees it as an html tag no matter what I do. I can put some \ and it will output it, but it doesn't treat it like an escape character (also prints \). Sometimes I really hate handlebars for how confining it is.

codemariner commented 3 weeks ago

so, it looks like it has to do with the prettier config. It seems to pick up whatever you have locally. When I removed mine (which works just fine for me) it started working. Here's my prettier config.

// ./.prettierrc.cjs
module.exports = {
  singleQuote: true,
  semi: false,
  plugins: ['@trivago/prettier-plugin-sort-imports'],
  importOrder: ['<THIRD_PARTY_MODULES>', '^@banno/(.*)$', '^[./]'], // for @trivago/prettier-plugin-sort-imports
  importOrderSeparation: true, // for @trivago/prettier-plugin-sort-imports
  importOrderSortSpecifiers: true, // for @trivago/prettier-plugin-sort-imports
}

if I delete it, the generated output is fine. (for me) there's something about having this particular plugin there. Things work if I remove it. It seems to pick up the prettier config if it exists at all. The option to pass a prettier config is just overrides (i think) as it seems to keep using my default one). The code seems to make heavy use of pretty formatting everything. I may need to rename it to a non-default one.