ballerina-platform/ballerina-library

Improve the OpenAPI generated client's method signature to match the HTTP client's method signature

Closed this issue · 0 comments

Description:

Improve the signature of the OpenAPI generated client's method to have clear parameter separation with headers, queries and payload. The HTTP client methods have such separation:

  • map<string|string[]> parameter to represents the headers
  • included record parameter to represents the queries
  • anydata parameter to represent the payload

Describe your problem(s)

Consider the following OAS:

openapi: 3.0.1
info:
  title: Api
  version: 0.1.0
servers:
- url: "{server}:{port}/api"
  variables:
    server:
      default: http://localhost
    port:
      default: "9090"
paths:
  /albums:
    get:
      operationId: getAlbums
      parameters:
      - name: version
        in: header
        required: true
        schema:
          $ref: '#/components/schemas/Version'
      - name: user-id
        in: header
        required: true
        schema:
          type: string
      - name: genre
        in: query
        schema:
          type: string
      responses:
        "200":
          description: Ok
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Album'
    post:
      operationId: postAlbums
      parameters:
      - name: version
        in: header
        required: true
        schema:
          $ref: '#/components/schemas/Version'
      - name: user-id
        in: header
        required: true
        schema:
          type: string
      - name: directory
        in: query
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Album'
        required: true
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Album'
components:
  schemas:
    Album:
      required:
      - artist
      - genre
      - name
      - year
      type: object
      properties:
        name:
          type: string
        artist:
          type: string
        year:
          type: integer
          format: int64
        genre:
          type: string
      additionalProperties: false
    Version:
      type: string
      enum:
      - V2
      - V1

The current generated client:

public isolated client class Client {
    ...

    # Get the albums
    #
    # + genre - The genre
    # + return - Ok 
    resource isolated function get albums(Version version, string user\-id, string? genre = ()) returns Album[]|error {
        string resourcePath = string `/albums`;
        map<anydata> queryParam = {"genre": genre};
        resourcePath = resourcePath + check getPathForQueryParam(queryParam);
        map<any> headerValues = {"version": version, "user-id": user\-id};
        map<string|string[]> httpHeaders = getMapForHeaders(headerValues);
        return self.clientEp->get(resourcePath, httpHeaders);
    }

    # Post an album
    #
    # + directory - The directory
    # + payload - The album 
    # + return - Created 
    resource isolated function post albums(Version version, string user\-id, Album payload, string? directory = ()) returns Album|error {
        string resourcePath = string `/albums`;
        map<anydata> queryParam = {"directory": directory};
        resourcePath = resourcePath + check getPathForQueryParam(queryParam);
        map<any> headerValues = {"version": version, "user-id": user\-id};
        map<string|string[]> httpHeaders = getMapForHeaders(headerValues);
        http:Request request = new;
        json jsonBody = payload.toJson();
        request.setPayload(jsonBody, "application/json");
        return self.clientEp->post(resourcePath, request, httpHeaders);
    }
}

Usage compare to the HTTP client:

Album[] _ = check httpClient->/albums({user\-id: "123", version: "V1"}, genre = "pop");

Album[] _ = check generatedClient->/albums(user\-id = "123", version = "V1", genre = "pop");

Describe your solution(s)

The new generated client:

public isolated client class Client {
    ...

    # + headers - Headers to be sent with the request 
    # + queries - Queries to be sent with the request 
    # + return - Ok 
    resource isolated function get albums(GetAlbumsHeaders headers, *GetAlbumsQueries queries) returns Album[]|error {
        string resourcePath = string `/albums`;
        resourcePath = resourcePath + check getPathForQueryParam(queries);
        map<string|string[]> httpHeaders = getMapForHeaders(headers);
        return self.clientEp->get(resourcePath, httpHeaders);
    }

    # + headers - Headers to be sent with the request 
    # + queries - Queries to be sent with the request 
    # + return - Created 
    resource isolated function post albums(PostAlbumsHeaders headers, Album payload, *PostAlbumsQueries queries) returns Album|error {
        string resourcePath = string `/albums`;
        resourcePath = resourcePath + check getPathForQueryParam(queries);
        map<string|string[]> httpHeaders = getMapForHeaders(headers);
        http:Request request = new;
        json jsonBody = payload.toJson();
        request.setPayload(jsonBody, "application/json");
        return self.clientEp->post(resourcePath, request, httpHeaders);
    }
}

The new header and query records:

# Represents the Headers record for the operation: postAlbums
public type PostAlbumsHeaders record {
    string user\-id;
    Version version;
};

# Represents the Headers record for the operation: getAlbums
public type GetAlbumsHeaders record {
    string user\-id;
    Version version;
};

public type Version "V2"|"V1";

# Represents the Queries record for the operation: postAlbums
public type PostAlbumsQueries record {
    string? directory?;
};

# Represents the Queries record for the operation: getAlbums
public type GetAlbumsQueries record {
    # The genre
    string? genre?;
};