This is the 8th day of my participation in the August Text Challenge.More challenges in August

  • 🥱 still in hand masturbation interface? Come to the whole interface request generator! (on)
  • 🥱 still in hand masturbation interface? Come to the whole interface request generator! (below)

Data acquisition

We already know how to get the corresponding interface information, so let’s implement it now. We will first determine the service we need to generate. There are two cases: single service and multiple service, that is, we will see if we have multiple independent Swagger API interfaces under the gateway.

export function generateService({ gateway, services, version }) {
    if (services && Object.keys(services).length) {
        // Multi-service mode
        return Object.entries(services).map(([key, service]) = > ({
            key: key,
            name: service,
            url: `${gateway}/${service}/${version}/api-docs`}}))else {
        // Single service mode
        return [{
            key: ' '.name: ' '.url: `${gateway}/${version}/api-docs`}}}]Copy the code

We generated a list of Swagger apis to request from Services and are now ready for the request.

export function generate(service) {
    fetch(service.url, { method: 'GET' })
        .then((res) = > res.json())
        .then(
            ({
                tags,
                paths
            }: {
                tags: any[]
                paths: { [keys: string] :any}}) = > {
                // Controller list
                const controllers: any = []
                // Fill in the controller list
                generateControllers(service, controllers, paths, tags)
                // Generate a controller file
                generateControllerFiles(service, controllers)
                // Generate the service file
                generateServiceFiles(service, controllers)
            }
        )
}

Copy the code

As we mentioned earlier,paths are part of the interface list. We can calculate where the controller and service files need to be generated based on the data in the Paths. The main role of the Generatecontroller is to extract the main data from the Paths.

We can first handle the tasks according to our cognition, which can be divided into controller and Action according to the server. A controller contains multiple actions, and the path of controller is the upper path of action.

export function generateControllers(
    service: { key: string, name: string, url: string },
    controllers: any[],
    paths: { [keys: string] :any },
    tags: any[]
) {
    Object.entries(paths)
        .filter(([key]) = > key.startsWith('/api') || key.startsWith(` /${service.name}`))
        .map(([key, config]: [string, { [keys: string] :any }]) = > ({
            path: key.replace(new RegExp(` ^ \ /${service.name}\ / `), "/"),
            config
        }))
        .forEach(({ path, config }) = > {
            // Interface behavior
            Object.entries(config).forEach(
                ([ method, { summary, tags: currentTag, operationId } ]) = > {
                    const getController = config.getControllerResolver ? config.getControllerResolver : getControllerName
                    const controller = getController(path, currentTag, tags)
                    const action = getActionName(operationId)
                    const filename = controller
                        .replace(/([A-Z])/g.'- $1')
                        .replace(/^-/g.' ')
                        .toLowerCase()

                    // Query and create a controller
                    let target = controllers.find(
                        (x) = > x.controller === filename
                    )

                    // If the controller does not exist, it will be created automatically
                    if(! target) { target = {controller: filename,
                            filename: filename,
                            controllerClass: `${controller}Controller`.serviceClass: `${controller}Service`.actions: []
                        }
                        controllers.push(target)
                    }

                    // Add controller behavior
                    target.actions.push({
                        path,
                        controller,
                        action: (action || method).replace(/-(\w)/g.($, $1) = >
                            $1.toUpperCase()
                        ),
                        defaultAction: !action,
                        method: method.replace(/^\S/.(s) = > s.toUpperCase()),
                        summary
                    })
                }
            )
        })
}
Copy the code

We get the name of the corresponding controller from the path field and the name of the action from the operationId field. The generated filename is named according to the string naming method (kebab-case).

So we end up with a set of data for controllers, a collection of all their controllers, and each controller has a field called Actions for all the actions of that controller, and our goal is that for each controller we’re going to generate a CONT Roller files and Service files,action is the interface that is actually called in each controller file.

Code generation

Once we have the data ready to generate code, we can use Handlebars to write code templates for code generation.

  • The Controller template
/** * This file is auto generated. * Do not edit. */ import { RequestMethod } from '@gopowerteam/http-request' const controller = '{{controller}}'
const service = '{{service}}' export const{{controllerClass}} = {
{{#each actions}}
    // {{summary}}
    {{action}}: { service, controller, path: '{{path}}',
        {{#unless defaultAction}}
        action: '{{action}}',
        {{/unless}}
        type: RequestMethod.{{method}}
    }{{#unless @last}}.{{/unless}}
{{/each}}
}
Copy the code
  • The Service template
/** * This file is auto generated. * Do not edit. */ import { Request, RequestParams } from '@gopowerteam/http-request' import type { Observable } from 'rxjs' import {{{controllerClass}} } from '{{controllerDir}}/{{controller}}.controller' export class{{serviceClass}} {
{{#each actions}}/ * * *{{summary}}*/ @Request({ server:{{../controllerClass}}.{{action}}}) public{{action}}(requestParams: RequestParams): Observable<any> {
        return requestParams.request()
    }
{{/each}}
}
Copy the code

The generated Contrller file is used to configure the interface information, and the service file is used to facilitate the invocation.

We need to deal with the fact that some interfaces may not have action names, so we need to generate default action names, for example

@Controller('/api')
export class AppController {
 @Get(a)async get(){... }@Post(a)async post(){... }}Copy the code

This case, we can put the method (get) | post |… Call it the name of the action.

Now we can use the prepared data to generate the corresponding template file.

let templateSource = readFileSync(serviceTemplatePath, ENCODING)
let template = compile(templateSource)
let serviceFileContent = template(
    Object.assign(controller, { service: service.key, controllerDir: [loadConfig().controllerAlias, service.key].filter(x= > x).join('/') })
)

writeServiceFile(service.key, controller, serviceFileContent).then(
    (filename) = > {
        log(`Service File Generate`, filename)
    }
)
Copy the code

The generated files are generated in the configured controllerDir and serviceDir directories.

Now we can easily automatically generate the configuration file of the interface. When the interface needs to be updated, we can execute the command of updating the interface to automatically generate the corresponding interface call file. We can add the command of generating code to the scripts of package.json for easy invocation.

Vite plug-in support

This is what we would normally write when we perform a request as we assumed before.

import {UserService} from '... '

const userService = new UserService()

userService.login(new RequestParams()).subscribe({
    next: data= > {
        console.log(data)
    }
})
Copy the code

I need to import UserService first, then instantiate and call it. If there are multiple calls in a page, I need to import multiple corresponding services. Is it possible to optimize a little bit?

One way to make it easier for me to write is through the virtual import feature of Vite. I wrote an article about the virtual import feature of the Vite plugin in order to implement this feature.

Using the virtual import method of the Vite plugin, we can dynamically generate a function that imports all the services and then returns an instantiation object based on the selected service.

After modification, you can write like this:

import { useRequest } from 'virtual:http-request'

const posterService = useRequest(
  services= > services.UserService
)

userService.login(new RequestParams()).subscribe({
    next: data= > {
        console.log(data)
    }
})
Copy the code

The advantage is that we no longer need to import the corresponding Service, when selecting services can also do automatic prompt support.

Implementation is also not troublesome, first to generate the import code

function generateImportCode(
  services: {
    name: string;
    path: string;
  }[],
  placeholder = ""
) {
  return services
    .map((service) = > `import { ${service.name} } from '${service.path}'`)
    .join(`\r\n${placeholder}`);
}
Copy the code

Generate a list of all services

function generateServiceCode(
  services: {
    name: string;
    path: string;
  }[],
  placeholder = ""
) {
  return `const serviceList = {
    ${services.map((service) => service.name).join(`,\r\n${placeholder}`)}} `;
}
Copy the code

Then we generate the code for virtual file export

function generateCode(services: service[]) {
  const importCode = generateImportCode(services);
  const serviceCode = generateServiceCode(services);

  return `
${importCode}
${serviceCode}

export function useRequest(
  select
) {
  const service = select(serviceList)
  return new service()
}
  `;
}
Copy the code

As you can see, we generate a useRequest function, which takes a select function from a serviceList, and then instantiates the selected Service.

However, there is no support for TypeScript type hints. We are typing services=>services. There is no code prompt because services is still of type any.

So we need a declaration file to implement automatic prompt support.

function generateDeclaration(services: service[], serviceDeclaration: string) {
  const importCode = generateImportCode(services, "");
  const serviceCode = generateServiceCode(services, "");

  const declaration = `declare module '${MODULE_ID}'{${importCode}
  ${serviceCode}

  export function useRequest<T>(
    select: (services: typeof serviceList) => { new (): T }
  ): T
}
`; . }Copy the code

We first generate a Declare Module using the generateDeclaration function… Type definition file, which specifies the typeof the select function. We get TypeScript type support by specifying the services type as typeof serviceList in the select function argument.

~ and flower 🎉 🎉 🎉

Source address: Github

If you feel this article is helpful to your words might as well 🍉 attention + like + favorites + comments + forward 🍉 support yo ~~😛