Skip to content

Commit

Permalink
typescript-fetch-client2
Browse files Browse the repository at this point in the history
Resolves #25 #31
  • Loading branch information
karlvr committed Jan 24, 2022
1 parent 802a0fe commit 0b9443a
Show file tree
Hide file tree
Showing 55 changed files with 1,508 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/typescript-fetch-client2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/dist
3 changes: 3 additions & 0 deletions packages/typescript-fetch-client2/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/CHANGELOG.md
/jest.config.js
/src/__tests__
73 changes: 73 additions & 0 deletions packages/typescript-fetch-client2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Strongly-typed TypeScript Fetch Browser API generator for OpenAPI Generator Plus

An [OpenAPI Generator Plus](https://github.com/karlvr/openapi-generator-plus) template for a TypeScript API client using Fetch in a Browser
with support for multiple strongly-typed responses.

This client generator supercedes the [typescript-fetch-client-generator](../typescript-fetch-client)

For an API client to use in Node applications, see [typescript-fetch-node-client-generator](../typescript-fetch-node-client).

## Using

See the [OpenAPI Generator Plus](https://github.com/karlvr/openapi-generator-plus) documentation for how to use
generator templates.

## Config file

The available config file properties are:

### Project layout

|Property|Type|Description|Default|
|--------|----|-----------|-------|
|`relativeSourceOutputPath`|`string`|The path to output generated source code, relative to the output path.|`./` or `./src` if `npm` is specified.|

### Code style

|Property|Type|Description|Default|
|--------|----|-----------|-------|
|`constantStyle`|`"allCapsSnake"|"allCaps"|"camelCase"|"pascalCase"`|The style to use for constant naming.|`"pascalCase"`|
|`dateApproach`|`"native"|"string"|"blind-date"`|Whether to use `string` for date and time and `Date` for date-time, or just `string`, or whether to use [blind-date](https://npmjs.com/blind-date) for dates and times.|`native`|
|`includePolyfills`|`boolean`|Include polyfills for features that browsers might not support or support well.|`true`|

### TypeScript

A `tsconfig.json` file will be output if you specify any of the TypeScript config options.

|Property|Type|Description|Default|
|--------|----|-----------|-------|
|`typescript`|`TypeScriptConfig`|Configuration for the `tsconfig.json` file.|`undefined`|

#### `TypeScriptConfig`

|Property|Type|Description|Default|
|--------|----|-----------|-------|
|`target`|`string`|The ECMAScript target version.|`ES5`|

### Packaging

|Property|Type|Description|Default|
|--------|----|-----------|-------|
|`npm`|`NpmConfig`|Configuration for generating an npm `package.json`|`undefined`|

#### `NpmConfig`

|Property|Type|Description|Default|
|--------|----|-----------|-------|
|`name`|`string`|The package name|`typescript-fetch-api`|
|`version`|`string`|The package version|`0.0.1`|
|`repository`|`string`|The URL to the package repository|`undefined`|

### Overrides

|Property|Type|Description|Default|
|--------|----|-----------|-------|
|`customTemplates`|`string`|The path to a directory containing custom Handlebars templates, relative to the config file. See Customising below.|`undefined`|

## Customising

This generator supports a `customTemplates` config file property to specify a directory containing Handlebars templates that will be used to override built-in templates.

Any custom template will have the original template available as a partial named by prefixing the template name with `original`, and then upper-casing the first letter, e.g. `originalModelEnum`.

Some of the templates in the generator are designed to support overriding for custom requirements. Please inspect the templates in the `templates` directory.
12 changes: 12 additions & 0 deletions packages/typescript-fetch-client2/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* https://jestjs.io/docs/en/configuration */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: [
'/node_modules/',
'/dist/',
],
/* Only run files with test or spec in their filename, so we can have support files in __tests__ */
testRegex: '(\\.|/)(test|spec)\\.[jt]sx?$',
testTimeout: 120000,
}
42 changes: 42 additions & 0 deletions packages/typescript-fetch-client2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@openapi-generator-plus/typescript-fetch-client-generator2",
"version": "0.0.1",
"description": "An OpenAPI Generator Plus template for a TypeScript API client using Fetch",
"keywords": [
"openapi-generator-plus",
"openapi-generator-plus-generator",
"openapi",
"openapi-generator",
"typescript",
"fetch",
"client"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "../../node_modules/.bin/tsc -p tsconfig.prod.json",
"clean": "../../node_modules/.bin/rimraf dist",
"test": "../../node_modules/.bin/jest",
"watch": "../../node_modules/.bin/tsc --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/karlvr/openapi-generator-plus-generators.git"
},
"author": "Karl von Randow",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/karlvr/openapi-generator-plus-generators/issues"
},
"homepage": "https://github.com/karlvr/openapi-generator-plus-generators/tree/master/packages/typescript-fetch-client#readme",
"dependencies": {
"@openapi-generator-plus/generator-common": "^1.0.0",
"@openapi-generator-plus/handlebars-templates": "^1.0.0",
"@openapi-generator-plus/typescript-generator-common": "^1.0.0",
"change-case": "^4.1.2"
},
"publishConfig": {
"access": "public"
},
"gitHead": "4d7336969427733ca316dfb48219f166adb9182a"
}
31 changes: 31 additions & 0 deletions packages/typescript-fetch-client2/src/__tests__/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import path from 'path'
import { CodegenConfig } from '@openapi-generator-plus/types'
import { CodegenResult, createCodegenResult } from '@openapi-generator-plus/testing'
import createGenerator from '..'
import { exec } from 'child_process'

export const DEFAULT_CONFIG: CodegenConfig = {
npm: {},
}

export async function prepare(spec: string, config?: CodegenConfig): Promise<CodegenResult> {
return createCodegenResult(path.resolve(__dirname, spec), config || DEFAULT_CONFIG, createGenerator)
}

export async function compile(basePath: string): Promise<void> {
return new Promise(function(resolve, reject) {
exec(
'pnpm install',
{
cwd: basePath,
},
function(error, stdout, stderr) {
if (error) {
reject(new Error(`${error.cmd || '<unknown>'} exited with code ${error.code || 'unknown'}:\n ${stdout || stderr}`))
} else {
resolve()
}
}
)
})
}
19 changes: 19 additions & 0 deletions packages/typescript-fetch-client2/src/__tests__/compile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { testGenerate } from '@openapi-generator-plus/generator-common/dist/testing'
import { compile, prepare, DEFAULT_CONFIG } from './common'
import fs from 'fs'
import path from 'path'

describe('compile test cases', () => {
const basePath = path.join(__dirname, '..', '..', '..', '..', '__tests__', 'specs')
const files = fs.readdirSync(basePath)

for (const file of files) {
test(file, async() => {
const result = await prepare(path.join(basePath, file), {
...DEFAULT_CONFIG,
includeTests: true,
})
await testGenerate(result, { postProcess: compile, testName: file })
})
}
})
18 changes: 18 additions & 0 deletions packages/typescript-fetch-client2/src/__tests__/date.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
test('date-time parsing', () => {
expect(new Date('2020-01-03T11:23:45Z').toISOString()).toEqual('2020-01-03T11:23:45.000Z')
})

test('date parsing', () => {
expect(new Date('2020-01-03').toISOString()).toEqual('2020-01-03T00:00:00.000Z')
expect(new Date('2020-01-03').getFullYear()).toEqual(2020)
expect(new Date('2020-01-03').getMonth()).toEqual(0)
expect(new Date('2020-05-03').getMonth()).toEqual(4)
expect(new Date('2020-01-03').getDate()).toEqual(3)
})

test('time parsing', () => {
expect(new Date('1970-01-01T11:23:45Z').toISOString()).toEqual('1970-01-01T11:23:45.000Z')
expect(new Date('1970-01-01T11:23:45').getHours()).toEqual(11)
expect(new Date('1970-01-01T11:23:45').getMinutes()).toEqual(23)
expect(new Date('1970-01-01T11:23:45').getSeconds()).toEqual(45)
})
10 changes: 10 additions & 0 deletions packages/typescript-fetch-client2/src/__tests__/one-of.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { testGenerate } from '@openapi-generator-plus/generator-common/dist/testing'
import { compile, DEFAULT_CONFIG, prepare } from './common'

test('one of no discriminator', async() => {
const result = await prepare('one-of/one-of-no-discriminator.yml', {
...DEFAULT_CONFIG,
})

await testGenerate(result, { postProcess: compile, testName: 'one-of' })
}, 20000)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
openapi: '3.0.3'
info:
version: 1.0.1
title: Example
paths: {}
components:
schemas:
ColorRgb:
type: object
properties:
r:
type: integer
g:
type: integer
b:
type: integer
required:
- r
- g
- b
ColorHs:
type: object
properties:
h:
type: number
s:
type: number
required:
- h
- s
SomeObject:
type: object
properties:
color:
oneOf:
- $ref: "#/components/schemas/ColorRgb"
- $ref: "#/components/schemas/ColorHs"
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createCodegenResult } from '@openapi-generator-plus/testing'
import { testGenerate } from '@openapi-generator-plus/generator-common/dist/testing'
import path from 'path'
import createGenerator from '..'
import { compile, DEFAULT_CONFIG } from './common'

test('shadowed model', async() => {
const result = await createCodegenResult(path.resolve(__dirname, 'shadowed-model-v2.yml'), DEFAULT_CONFIG, createGenerator)

await testGenerate(result, { postProcess: compile, testName: 'shadowed-model' })
}, 20000)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
swagger: '2.0'
info:
title: Test
version: '1.0'
paths: {}
definitions:
Outer:
type: object
properties:
name:
type: string
Container:
type: object
properties:
outer:
allOf:
- $ref: '#/definitions/Outer'
- type: object
properties:
inner:
type: string
98 changes: 98 additions & 0 deletions packages/typescript-fetch-client2/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { CodegenGeneratorConstructor, CodegenGeneratorType, CodegenSchemaType, isCodegenAnyOfSchema, isCodegenEnumSchema, isCodegenInterfaceSchema, isCodegenObjectSchema, isCodegenOneOfSchema } from '@openapi-generator-plus/types'
import path from 'path'
import { loadTemplates, emit } from '@openapi-generator-plus/handlebars-templates'
import typescriptGenerator, { options as typescriptCommonOptions, TypeScriptGeneratorContext, chainTypeScriptGeneratorContext } from '@openapi-generator-plus/typescript-generator-common'
import { CodegenOptionsTypeScriptFetchClient } from './types'
import * as idx from '@openapi-generator-plus/indexed-type'

const createGenerator: CodegenGeneratorConstructor = (config, context) => {
const myContext: TypeScriptGeneratorContext = chainTypeScriptGeneratorContext(context, {
loadAdditionalTemplates: async(hbs) => {
await loadTemplates(path.resolve(__dirname, '../templates'), hbs)
},
additionalWatchPaths: () => {
return [path.resolve(__dirname, '../templates')]
},
defaultNpmOptions: () => ({
name: 'typescript-fetch-api',
version: '0.0.1',
private: true,
repository: null,
}),
defaultTypeScriptOptions: () => ({
target: 'ES5',
libs: ['$target', 'DOM'],
}),
})

const generatorOptions: CodegenOptionsTypeScriptFetchClient = {
...typescriptCommonOptions(config, myContext),
includePolyfills: config.includePolyfills !== undefined ? !!config.includePolyfills : true,
}

myContext.additionalExportTemplates = async(outputPath, doc, hbs, rootContext) => {
const relativeSourceOutputPath = generatorOptions.relativeSourceOutputPath
await emit('api', path.join(outputPath, relativeSourceOutputPath, 'api.ts'), { ...rootContext, ...doc }, true, hbs)
await emit('models', path.join(outputPath, relativeSourceOutputPath, 'models.ts'), {
...rootContext,
...doc,
schemas: idx.filter(doc.schemas, schema => isCodegenObjectSchema(schema) || isCodegenEnumSchema(schema) || isCodegenOneOfSchema(schema) || isCodegenAnyOfSchema(schema) || isCodegenInterfaceSchema(schema)),
}, true, hbs)
await emit('runtime', path.join(outputPath, relativeSourceOutputPath, 'runtime.ts'), { ...rootContext, ...doc }, true, hbs)
await emit('configuration', path.join(outputPath, relativeSourceOutputPath, 'configuration.ts'), { ...rootContext, ...doc }, true, hbs)
await emit('index', path.join(outputPath, relativeSourceOutputPath, 'index.ts'), { ...rootContext, ...doc }, true, hbs)
await emit('README', path.join(outputPath, 'README.md'), { ...rootContext, ...doc }, true, hbs)
}

const base = typescriptGenerator(config, myContext)

return {
...base,
templateRootContext: () => {
return {
...base.templateRootContext(),
...generatorOptions,
generatorClass: '@openapi-generator-plus/typescript-fetch-client-generator',
}
},
generatorType: () => CodegenGeneratorType.CLIENT,
toNativeType: function(options) {
const { schemaType } = options
if (schemaType === CodegenSchemaType.BINARY) {
/* We support string and Blob, which is what FormData supports. It also enables us to handle literal binary values
created from strings... which is hopefully a good idea.
*/
return new context.NativeType('string | Blob')
} else {
return base.toNativeType(options)
}
},
postProcessDocument: (doc) => {
for (const group of doc.groups) {
for (const op of group.operations) {
if (op.parameters) {
op.parameters = idx.filter(op.parameters, param => param.in !== 'header' || !isForbiddenHeaderName(param.name))
}
if (op.headerParams) {
op.headerParams = idx.filter(op.headerParams, param => !isForbiddenHeaderName(param.name))
}
}
}
},
}
}

/** See https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name */
function isForbiddenHeaderName(name: string): boolean {
if (name.toLowerCase().startsWith('proxy-') || name.toLowerCase().startsWith('sec-')) {
return true
}
return [
'Accept-Charset', 'Accept-Encoding', 'Access-Control-Request-Headers', 'Access-Control-Request-Method',
'Connection', 'Content-Length', 'Cookie', 'Cookie2', 'Date', 'DNT', 'Expect', 'Feature-Policy', 'Host',
'Keep-Alive', 'Origin', 'Referer', 'TE', 'Trailer', 'Transfer-Encoding', 'Upgrade', 'Via',
].map(h => h.toLowerCase()).includes(name.toLowerCase())
}

export default createGenerator

Loading

0 comments on commit 0b9443a

Please sign in to comment.