Repository navigation
Generate API commands from the OpenAPI spec #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
368082f
Generate API commands from the OpenAPI spec
dennis-chen-vanta b186f97
Match action suffixes without a dynamic RegExp
dennis-chen-vanta 5f630eb
Simplify the command generator without changing its output
dennis-chen-vanta ed01717
Derive linking and download command names in the generator
dennis-chen-vanta 7ba52b5
Split the command generator into modules and format its output with o…
dennis-chen-vanta 5e03283
Organize the generator into five files
dennis-chen-vanta 048de1e
Move placeholder vocabulary and word exceptions to config.ts
dennis-chen-vanta 64372c5
Move shared types to types.ts
dennis-chen-vanta 094ce56
Replace pluralize and change-case with small helpers
dennis-chen-vanta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| // Which commands exist and what they're called: each operation is placed in | ||
| // a group and named `<verb>-<noun>` (`vendors list-findings`). See AGENTS.md | ||
| // "Command structure". | ||
| import * as config from "./config.js"; | ||
| import type { Group, Operation, Placement, Segment } from "./types.js"; | ||
|
|
||
| // =========================================================================== | ||
| // Words | ||
| // =========================================================================== | ||
|
|
||
| export const lowerFirst = (s: string) => | ||
| s.replace(/^./, (c) => c.toLowerCase()); | ||
| export const upperFirst = (s: string) => | ||
| s.replace(/^./, (c) => c.toUpperCase()); | ||
| // `statusMatchesAny` → `status-matches-any`, and back with `camel`. | ||
| export const kebab = (s: string) => | ||
| s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); | ||
| export const camel = (s: string) => | ||
| s.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase()); | ||
| export const pascal = (s: string) => upperFirst(camel(s)); | ||
| // `faqCategories` and `faq-categories` → "faq categories". | ||
| export const humanize = (s: string) => kebab(s).replace(/-/g, " "); | ||
|
|
||
| // "a vendor", "an integration", "a user". | ||
| export const a = (noun: string) => | ||
| `${/^[aeiou]/i.test(noun) && !/^(us|uni|eu)/i.test(noun) ? "an" : "a"} ${noun}`; | ||
|
|
||
| // Singularizes the last word of a kebab-case segment: `security-reviews`. | ||
| export function singular(segment: string): string { | ||
| const word = segment.split("-").at(-1)!; | ||
| const one = | ||
| config.irregularNouns[word] ?? | ||
| (word.endsWith("ies") | ||
| ? `${word.slice(0, -3)}y` | ||
| : word.endsWith("sses") | ||
| ? word.slice(0, -2) | ||
| : word.endsWith("s") && !/(ss|us)$/.test(word) | ||
| ? word.slice(0, -1) | ||
| : word); | ||
| return segment.slice(0, -word.length) + one; | ||
| } | ||
| export const isPlural = (segment: string) => singular(segment) !== segment; | ||
|
|
||
| // Display names from `config.nouns`, keyed by "<group> <segment>" or by | ||
| // segment, else from the segment itself. | ||
| export const nounFor = (segment: string, group?: string) => | ||
| (group && config.nouns[`${group} ${segment}`]) || config.nouns[segment]; | ||
| export const displaySingular = (segment: string, group?: string) => | ||
| nounFor(segment, group)?.singular ?? humanize(singular(segment)); | ||
| export const displayPlural = (segment: string, group?: string) => | ||
| nounFor(segment, group)?.plural ?? humanize(segment); | ||
|
|
||
| // =========================================================================== | ||
| // Groups and commands | ||
| // =========================================================================== | ||
|
|
||
| export function newGroup(name: string): Group { | ||
| const noun = config.nouns[name]; | ||
| const plural = noun?.plural ?? displayPlural(name); | ||
| return { | ||
| name, | ||
| description: `Manage ${plural}`, | ||
| singular: noun?.singular ?? displaySingular(name), | ||
| plural, | ||
| commands: [], | ||
| }; | ||
| } | ||
|
|
||
| export function place(op: Operation): Placement { | ||
| const [groupSegment, ...rest] = op.segments as [Segment, ...Segment[]]; | ||
| const last = rest.at(-1); | ||
| const isAction = | ||
| last && | ||
| !last.param && | ||
| config.actionVerbs.includes(last.name.split("-")[0]!); | ||
| return { | ||
| groupSegment, | ||
| resources: isAction ? rest.slice(0, -1) : rest, | ||
| action: isAction ? last.name : undefined, | ||
| }; | ||
| } | ||
|
|
||
| // `<verb>-<noun>`, unless `config.operations` names the command. | ||
| export function commandName(op: Operation, placement: Placement): string { | ||
| const override = config.operations[op.id]?.name; | ||
| if (override) return override; | ||
| const atItem = op.segments.at(-1)!.param !== undefined; | ||
| let verb: string; | ||
| let rest: string[] = []; | ||
| // Operations on a whole collection keep the last noun plural. | ||
| let plural = !atItem; | ||
| const link = config.links.includes(op.id); | ||
| if (placement.action) { | ||
| [verb, ...rest] = placement.action.split("-") as [string, ...string[]]; | ||
| plural = false; | ||
| } else if (op.method === "get") { | ||
| verb = op.returnsFile ? "download" : atItem ? "get" : "list"; | ||
| plural = verb === "list"; | ||
| } else if (op.method === "post") { | ||
| verb = link ? "add" : op.body === "multipart" ? "upload" : "create"; | ||
| // `create` and `add` make one item, unless the API adds many at once. | ||
| plural = op.id.startsWith("Bulk"); | ||
| } else if (op.method === "delete" && link) { | ||
| verb = "remove"; | ||
| } else { | ||
| verb = { patch: "update", put: "set", delete: "delete" }[op.method]; | ||
| } | ||
| const noun = placement.resources.map((s, i) => | ||
| plural && i === placement.resources.length - 1 | ||
| ? (config.nouns[s.name]?.commandPlural ?? s.name) | ||
| : (config.nouns[s.name]?.commandSingular ?? singular(s.name)), | ||
| ); | ||
| return [verb, ...noun, ...rest].join("-"); | ||
| } | ||
|
|
||
| export function checkUniqueNames(groups: Group[]): void { | ||
| for (const group of groups) { | ||
| const seen = new Map<string, string>(); | ||
| for (const { name, op } of group.commands) { | ||
| if (seen.has(name)) { | ||
| throw new Error( | ||
| `duplicate command "${group.name} ${name}" for ${seen.get(name)} and ${op.id}; add a name override in scripts/codegen/config.ts`, | ||
| ); | ||
| } | ||
| seen.set(name, op.id); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chose this over introducing small deps like the
pluralizenpm package, mostly because I figured that we're reviewing any generated outputs anyway and probably don't want a bunch of tiny deps.