Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/_template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class ApiClient {
}
}

// apigen:modules
// apigen:clientMembers
}

// apigen:types
8 changes: 8 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type Config = {
source: string
output: string | null
name: string
namespacing: boolean
parseDates: boolean
inlineEnums: boolean
resolveName?: (ctx: Context, op: OpConfig, proposal: OpName) => OpName | undefined
Expand All @@ -24,6 +25,7 @@ export const initCtx = (config?: Partial<Context>): Context => {
output: "",
name: "ApiClient",
doc: { openapi: "3.1.0" },
namespacing: true,
parseDates: false,
inlineEnums: false,
headers: {},
Expand Down Expand Up @@ -54,6 +56,11 @@ export const getCliConfig = () => {
description: "API class name to export",
default: "ApiClient",
},
noNamespacing: {
type: Boolean,
description: "disable namespacing of generated methods based on the first tag",
default: false,
},
parseDates: {
type: Boolean,
description: "Parse dates as Date objects",
Expand All @@ -78,6 +85,7 @@ export const getCliConfig = () => {
source: argv._.source,
output: argv._.output ?? null,
name: argv.flags.name,
namespacing: !argv.flags.noNamespacing,
parseDates: argv.flags.parseDates,
inlineEnums: argv.flags.inlineEnums,
headers: parseHeaders(argv.flags.header),
Expand Down
32 changes: 21 additions & 11 deletions src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ const normalizeOpName = (val: string) => {
}

export const getOpName = (ctx: Context, op: OpConfig) => {
let ns = normalizeOpName(filterEmpty(op.tags ?? [])[0] ?? "general")
let ns
if (ctx.namespacing) ns = normalizeOpName(filterEmpty(op.tags ?? [])[0] ?? "general")
else ns = ""
let fn = op.operationId ?? null

// if not opId, try to make it from path
Expand Down Expand Up @@ -78,7 +80,7 @@ export const prepareUrl = (url: string, rename: Record<string, string>) => {
)
}

const prepareOp = (ctx: Context, cfg: OpConfig, opName: string) => {
const prepareOp = (ctx: Context, cfg: OpConfig, opName: string): [ts.Identifier, ts.Expression] => {
cfg.parameters = cfg.parameters ?? []

const reqSchema = getReqSchema(ctx, cfg)
Expand Down Expand Up @@ -126,7 +128,7 @@ const prepareOp = (ctx: Context, cfg: OpConfig, opName: string) => {
: undefined,
])

return f.createPropertyAssignment(
return [
f.createIdentifier(normalizeIdentifier(opName)),
f.createArrowFunction(
undefined,
Expand All @@ -148,21 +150,23 @@ const prepareOp = (ctx: Context, cfg: OpConfig, opName: string) => {
),
]),
),
)
]
}

const prepareNs = (ctx: Context, name: string, handlers: ts.PropertyAssignment[]) => {
const prepareNs = (ctx: Context, name: string, handlers: [ts.Identifier, ts.Expression][]) => {
return f.createPropertyDeclaration(
undefined,
normalizeIdentifier(name),
undefined,
undefined,
f.createObjectLiteralExpression(handlers),
f.createObjectLiteralExpression(
handlers.map(([name, expr]) => f.createPropertyAssignment(name, expr)),
),
)
}

const prepareRoutes = async (ctx: Context) => {
const routes: Record<string, ts.PropertyAssignment[]> = {}
const routes: Record<string, [ts.Identifier, ts.Expression][]> = {}

for (const [path, pathConfig] of Object.entries(ctx.doc.paths ?? {})) {
ctx.logTag = `${"[ALL]".toUpperCase().padEnd(6, " ")} ${path}`
Expand Down Expand Up @@ -223,12 +227,18 @@ const prepareTypes = async (ctx: Context) => {
export const generateAst = async (ctx: Context) => {
const types = await prepareTypes(ctx)
const routes = await prepareRoutes(ctx)
const modules: ts.PropertyDeclaration[] = []
for (const [k, v] of Object.entries(routes)) {
modules.push(prepareNs(ctx, k, v))
const clientMembers: ts.Node[] = []
if (ctx.namespacing) {
for (const [k, v] of Object.entries(routes)) {
clientMembers.push(prepareNs(ctx, k, v))
}
} else {
for (const [name, expr] of routes[""]) {
clientMembers.push(f.createPropertyDeclaration(undefined, name, undefined, undefined, expr))
}
}

return { modules, types }
return { clientMembers, types }
}

export const loadSchema = async ({
Expand Down
6 changes: 2 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import fs from "fs/promises"
import { dirname, join } from "path"
import ts from "typescript"
import { fileURLToPath } from "url"
import { Config, initCtx } from "./config"
import { generateAst, loadSchema } from "./generator"
Expand All @@ -9,7 +8,7 @@ import { formatCode, printCode } from "./printer"
export const apigen = async (config: Partial<Config> & Pick<Config, "source" | "output">) => {
const doc = await loadSchema({ url: config.source, headers: config.headers })
const ctx = initCtx({ ...config, doc })
const { modules, types } = await generateAst(ctx)
const { clientMembers, types } = await generateAst(ctx)

const filepath = join(dirname(fileURLToPath(import.meta.url)), "_template.ts")
const file = await fs.readFile(filepath, "utf-8")
Expand All @@ -27,8 +26,7 @@ export const apigen = async (config: Partial<Config> & Pick<Config, "source" | "
code = code.replace(/this.PopulateDates\((.+)\)/, "$1")
}

// broken type by design, need to keep original formatting (ts still can print it)
code = code.replace("// apigen:modules", printCode(modules as unknown as ts.Statement[]))
code = code.replace("// apigen:clientMembers", printCode(clientMembers))
code = code.replace("// apigen:types", printCode(types))
code = code.replace("class ApiClient", `class ${ctx.name}`)

Expand Down
32 changes: 12 additions & 20 deletions src/printer.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
import path from "node:path"
import ts from "typescript"

const f = ts.factory
export const printCode = (nodes: ts.Node[]) => {
const printer = ts.createPrinter()
const sourceFile = ts.createSourceFile(
"temp.ts",
"",
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TS,
)

const addNewLines = <T extends ts.Node>(nodes: T[]) => {
const result: T[] = []
for (const node of nodes) {
result.push(node)
result.push(f.createIdentifier("\n") as unknown as T)
}
return result
}

export const printCode = (nodes: ts.Statement[]) => {
return ts
.createPrinter()
.printFile(
f.createSourceFile(
addNewLines(nodes),
f.createToken(ts.SyntaxKind.EndOfFileToken),
ts.NodeFlags.None,
),
)
return nodes
.map((node) => printer.printNode(ts.EmitHint.Unspecified, node, sourceFile))
.join("\n\n")
.replaceAll("}, ", "},\n\n")
}

Expand Down
5 changes: 2 additions & 3 deletions test/type-gen.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Oas3Schema } from "@redocly/openapi-core"
import ts from "typescript"
import { test } from "uvu"
import { equal } from "uvu/assert"
import { initCtx } from "../src/config"
Expand All @@ -16,7 +15,7 @@ test("type inline", async () => {
const t = (l: OAS3, r: string, cfg?: Cfg) => {
const ctx = initCtx({ ...cfg })
const res = makeType(ctx, l)
const txt = printCode([res as unknown as ts.Statement])
const txt = printCode([res])
.replace(/"(\w+)"(\??):/g, "$1$2:")
.replaceAll("\n", " ")
.replace(/ +/g, " ")
Expand Down Expand Up @@ -147,7 +146,7 @@ test("type alias", async () => {
const t = (l: Oas3Schema & { name?: string }, r: string, cfg?: Cfg) => {
const ctx = initCtx({ ...cfg })
const res = makeTypeAlias(ctx, l.name ?? "t", l)
const txt = printCode([res as unknown as ts.Statement])
const txt = printCode([res])
.replace(";", "")
.replace("export ", "")
.replaceAll("\n", " ")
Expand Down
3 changes: 1 addition & 2 deletions test/url-gen.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { trim } from "lodash-es"
import ts from "typescript"
import { test } from "uvu"
import { equal } from "uvu/assert"
import { prepareUrl } from "../src/generator"
import { printCode } from "../src/printer"

test("url template", async () => {
const t = async (url: string, replacements: Record<string, string>) => {
const code = printCode([prepareUrl(url, replacements) as unknown as ts.Statement])
const code = printCode([prepareUrl(url, replacements)])
return trim(trim(code.trim(), ";"), '`"')
}

Expand Down