Skip to content

Commit

Permalink
feat(oas): creation of a new friendlyCase option for `.getOperation…
Browse files Browse the repository at this point in the history
…Id()` (#879)

| 🚥 Resolves #869 |
| :------------------- |

## 🧰 Changes

This resurrects the work I originally did in
#851, but unfortunately had to
revert, to create a new `friendlyCase` option on
`Operation.getOperationId()` for generating friendlier camelCase
operation IDs that we can use in our upcoming Git storage system for API
references.

## 🧬 QA & Testing

I reorganized a bunch of the tests for `camelCase` to retain them all
along with this new option.
  • Loading branch information
erunion authored Jun 12, 2024
1 parent bcc7e99 commit 411e9b2
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 136 deletions.
58 changes: 49 additions & 9 deletions packages/oas/src/operation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,17 +350,38 @@ export class Operation {
* a hash of the path and method will be returned instead.
*
*/
getOperationId(opts?: {
/**
* Generate a JS method-friendly operation ID when one isn't present.
*/
camelCase: boolean;
}): string {
getOperationId(
opts: {
/**
* Generate a JS method-friendly operation ID when one isn't present.
*
* For backwards compatiblity reasons this option will be indefinitely supported however we
* recommend using `friendlyCase` instead as it's a heavily improved version of this option.
*
* @see {opts.friendlyCase}
* @deprecated
*/
camelCase?: boolean;

/**
* Generate a human-friendly, but still camelCase, operation ID when one isn't present. The
* difference between this and `camelCase` is that this also ensure that consecutive words are
* not present in the resulting ID. For example, for the endpoint `/candidate/{candidate}` will
* return `getCandidateCandidate` for `camelCase` however `friendlyCase` will return
* `getCandidate`.
*
* The reason this friendliness is just not a part of the `camelCase` option is because we have
* a number of consumers of the old operation ID style and making that change there would a
* breaking change that we don't have any easy way to resolve.
*/
friendlyCase?: boolean;
} = {},
): string {
function sanitize(id: string) {
// We aren't sanitizing underscores here by default in order to preserve operation IDs that
// were already generated with this method in the past.
return id
.replace(opts?.camelCase ? /[^a-zA-Z0-9_]/g : /[^a-zA-Z0-9]/g, '-') // Remove weird characters
.replace(opts?.camelCase || opts?.friendlyCase ? /[^a-zA-Z0-9_]/g : /[^a-zA-Z0-9]/g, '-') // Remove weird characters
.replace(/--+/g, '-') // Remove double --'s
.replace(/^-|-$/g, ''); // Don't start or end with -
}
Expand All @@ -373,7 +394,26 @@ export class Operation {
}

const method = this.method.toLowerCase();
if (opts?.camelCase) {
if (opts?.camelCase || opts?.friendlyCase) {
if (opts?.friendlyCase) {
// In order to generate friendlier operation IDs we should swap out underscores with spaces
// so the end result will be _slightly_ more camelCase.
operationId = operationId.replaceAll('_', ' ');

if (!this.hasOperationId()) {
// In another effort to generate friendly operation IDs we should prevent words from
// appearing in consecutive order (eg. `/candidate/{candidate}` should generate
// `getCandidate` not `getCandidateCandidate`). However we only want to do this if we're
// generating the operation ID as if they intentionally added a consecutive word into the
// operation ID then we should respect that.
operationId = operationId
.replace(/[^a-zA-Z0-9_]+(.)/g, (_, chr) => ` ${chr}`)
.split(' ')
.filter((word, i, arr) => word !== arr[i - 1])
.join(' ');
}
}

operationId = operationId.replace(/[^a-zA-Z0-9_]+(.)/g, (_, chr) => chr.toUpperCase());
if (this.hasOperationId()) {
operationId = sanitize(operationId);
Expand All @@ -398,7 +438,7 @@ export class Operation {
}

// Because we're merging the `operationId` into an HTTP method we need to reset the first
// character of it back to lowercase so end up with `getBuster`, not `getbuster`.
// character of it back to lowercase so we end up with `getBuster`, not `getbuster`.
operationId = operationId.charAt(0).toUpperCase() + operationId.slice(1);
return `${method}${operationId}`;
} else if (this.hasOperationId()) {
Expand Down
22 changes: 21 additions & 1 deletion packages/oas/test/__fixtures__/create-oas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Oas from '../../src/index.js';
* @param operation Operation to create a fake API definition and Oas instance for.
* @param components Schema components to add into the fake API definition.
*/
export default function createOas(operation: RMOAS.OperationObject, components?: RMOAS.ComponentsObject): Oas {
export function createOasForOperation(operation: RMOAS.OperationObject, components?: RMOAS.ComponentsObject): Oas {
const schema = {
openapi: '3.0.3',
info: { title: 'testing', version: '1.0.0' },
Expand All @@ -23,3 +23,23 @@ export default function createOas(operation: RMOAS.OperationObject, components?:

return new Oas(schema);
}

/**
* @param paths Path objects to create a fake API definition and Oas instance for.
* @param components Schema components to add into the fake API definition.
*/
export function createOasForPaths(paths: RMOAS.PathsObject, components?: RMOAS.ComponentsObject): Oas {
const schema = {
openapi: '3.0.3',
info: { title: 'testing', version: '1.0.0' },
paths: {
...paths,
},
} as RMOAS.OASDocument;

if (components) {
schema.components = components;
}

return new Oas(schema);
}
4 changes: 2 additions & 2 deletions packages/oas/test/lib/openapi-to-json-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { beforeAll, expect, describe, it } from 'vitest';

import Oas from '../../src/index.js';
import { toJSONSchema } from '../../src/lib/openapi-to-json-schema.js';
import createOas from '../__fixtures__/create-oas.js';
import { createOasForOperation } from '../__fixtures__/create-oas.js';
import generateJSONSchemaFixture from '../__fixtures__/json-schema.js';

let petstore: Oas;
Expand Down Expand Up @@ -857,7 +857,7 @@ describe('`description` support', () => {
});

it('should add defaults for enums if default is present', () => {
const oas = createOas({
const oas = createOasForOperation({
requestBody: {
content: {
'application/json': {
Expand Down
Loading

0 comments on commit 411e9b2

Please sign in to comment.