Skip to content
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

feat(snippets): splitting apart package and variable name configs #816

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
13 changes: 0 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/api/src/lib/suggestedOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@ export async function buildCodeSnippetForOperation(oas: Oas, operation: Operatio
{
api: {
definition: oas.getDefinition(),
identifier: opts.identifier,
packageName: opts.identifier,
registryURI: opts.identifier,
variableName: opts.identifier,
},
},
);
Expand Down
1 change: 0 additions & 1 deletion packages/httpsnippet-client-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"@types/content-type": "^1.1.6",
"@types/stringify-object": "^4.0.3",
"@vitest/coverage-v8": "^0.34.4",
"camelcase": "^8.0.0",
"stringify-object": "^5.0.0",
"typescript": "^5.2.2",
"vitest": "^0.34.5"
Expand Down
66 changes: 55 additions & 11 deletions packages/httpsnippet-client-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { Operation } from 'oas/operation';
import type { HttpMethods, OASDocument } from 'oas/types';

import { CodeBuilder } from '@readme/httpsnippet/helpers/code-builder';
import camelCase from 'camelcase'; // eslint-disable-line import/no-extraneous-dependencies
import contentType from 'content-type';
import Oas from 'oas';
import { matchesMimeType } from 'oas/utils';
Expand Down Expand Up @@ -36,6 +35,42 @@ function stringify(obj: any, opts = {}) {
return stringifyObject(obj, { indent: ' ', ...opts });
}

/**
* Convert a string that might contain spaces or special characters to one that can safely be used
* as a TypeScript interface or enum name.
*
* This function has been adapted and slighty modified from `json-schema-to-typescript`. This
* function also exists in `api` TS codegen.
*
* @license MIT
* @see {@link https://github.com/bcherny/json-schema-to-typescript}
*/
function toSafeString(str: string) {
erunion marked this conversation as resolved.
Show resolved Hide resolved
// identifiers in javaScript/ts:
// First character: a-zA-Z | _ | $
// Rest: a-zA-Z | _ | $ | 0-9

// remove accents, umlauts, ... by their basic latin letters
return (
str
// if the string starts with a number, prefix it with character that typescript can accept
// https://github.com/bcherny/json-schema-to-typescript/issues/489
.replace(/^(\d){1}/, '$$1')
// replace chars which are not valid for typescript identifiers with whitespace
.replace(/(^\s*[^a-zA-Z_$])|([^a-zA-Z_$\d])/g, ' ')
// uppercase leading underscores followed by lowercase
.replace(/^_[a-z]/g, (match: string) => match.toUpperCase())
// remove non-leading underscores followed by lowercase (convert snake_case)
.replace(/_[a-z]/g, (match: string) => match.substr(1, match.length).toUpperCase())
// uppercase letters after digits, dollars
.replace(/([\d$]+[a-zA-Z])/g, (match: string) => match.toUpperCase())
// uppercase first letter after whitespace
.replace(/\s+([a-zA-Z])/g, (match: string) => match.toUpperCase().trim())
// remove remaining whitespace
.replace(/\s/g, '')
);
}

function buildAuthSnippet(sdkVariable: string, authKey: string | string[]) {
// Auth key will be an array for Basic auth cases.
if (Array.isArray(authKey)) {
Expand Down Expand Up @@ -102,18 +137,25 @@ interface APIOptions {

/**
* The string to identify this SDK as. This is used in the `import sdk from '<identifier>'`
* sample as well as the the variable name we attach the SDK to.
* sample.
*
* @example `@api/developers`
*/
identifier?: string;
packageName?: string;

/**
* The URI that is used to download this API definition from `npx api install`.
*
* @example `@developers/v2.0#17273l2glm9fq4l5`
*/
registryURI: string;

/**
* The variable name to use when importing an SDK. If not present we will default to `sdk`.
*
* @example `readme`
*/
variableName?: string;
};
escapeBrackets?: boolean;
indent?: string | false;
Expand Down Expand Up @@ -151,20 +193,22 @@ const client: Client<APIOptions> = {
);
}

let sdkPackageName;
let sdkVariable: string;
if (opts.api.identifier) {
sdkPackageName = opts.api.identifier;
let sdkPackageName: string;
if (opts.api.packageName) {
sdkPackageName = opts.api.packageName;
} else {
const registryUUID = getProjectPrefixFromRegistryUUID(opts.api.registryURI);
sdkPackageName = registryUUID || 'company-name';
}

sdkVariable = camelCase(opts.api.identifier);
let sdkVariable: string = 'sdk';
erunion marked this conversation as resolved.
Show resolved Hide resolved
if (opts.api.variableName) {
sdkVariable = toSafeString(opts.api.variableName);
if (isReservedOrBuiltinsLC(sdkVariable)) {
// If this identifier is a reserved JS word then we should prefix it with an underscore so
// this snippet can be valid code.
sdkVariable = `_${sdkVariable}`;
}
} else {
sdkPackageName = getProjectPrefixFromRegistryUUID(opts.api.registryURI);
sdkVariable = 'sdk';
}

const operationSlugs = foundOperation.url.slugs;
Expand Down
18 changes: 10 additions & 8 deletions packages/httpsnippet-client-api/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,16 @@ describe('httpsnippet-client-api', () => {
const code = await new HTTPSnippet(mock.har).convert('node', 'api', {
api: {
definition: mock.definition,
identifier: 'developers',
packageName: 'developers',
registryURI: '@developers/v2.0#17273l2glm9fq4l5',
variableName: 'developersSDK',
},
});

expect(code).toStrictEqual(`import developers from '@api/developers';
expect(code).toStrictEqual(`import developersSDK from '@api/developers';

developers.auth('123');
developers.findPetsByStatus({status: 'available', accept: 'application/xml'})
developersSDK.auth('123');
developersSDK.findPetsByStatus({status: 'available', accept: 'application/xml'})
.then(({ data }) => console.log(data))
.catch(err => console.error(err));`);
});
Expand All @@ -160,15 +161,16 @@ developers.findPetsByStatus({status: 'available', accept: 'application/xml'})
const code = await new HTTPSnippet(mock.har).convert('node', 'api', {
api: {
definition: mock.definition,
identifier: 'metro-transit',
packageName: 'metro-transit',
registryURI: '@metro-transit/v2.0#17273l2glm9fq4l5',
variableName: 'metro-transit-SDK',
},
});

expect(code).toStrictEqual(`import metroTransit from '@api/metro-transit';
expect(code).toStrictEqual(`import metroTransitSDK from '@api/metro-transit';

metroTransit.auth('123');
metroTransit.findPetsByStatus({status: 'available', accept: 'application/xml'})
metroTransitSDK.auth('123');
metroTransitSDK.findPetsByStatus({status: 'available', accept: 'application/xml'})
.then(({ data }) => console.log(data))
.catch(err => console.error(err));`);
});
Expand Down
1 change: 0 additions & 1 deletion packages/httpsnippet-client-api/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export default defineConfig((options: Options) => ({
// treat them as external dependencies as CJS libraries can't load ESM code that uses `export`.
// `noExternal` will instead treeshake these dependencies down and include them in our compiled
// dists.
'camelcase',
'stringify-object',
],

Expand Down