Skip to content

Commit

Permalink
Merge pull request #1715 from ballerina-platform/status-code-creation
Browse files Browse the repository at this point in the history
Fix status code response creation without body and headers
  • Loading branch information
TharmiganK authored Aug 1, 2024
2 parents 4d76108 + b4d1fcb commit 7cc6376
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1144,7 +1144,7 @@ private static String getNewStatusCodeRecordName(String path, boolean isRequest)
public static TypeDescriptorNode getUnionTypeDescriptorNodeFromTypeDescNodes(HashMap<String, TypeDescriptorNode>
typeDescNodes) {
if (typeDescNodes.isEmpty()) {
return getSimpleNameReferenceNode(GeneratorConstants.ANYDATA);
return null;
}
List<TypeDescriptorNode> qualifiedNodeList = typeDescNodes.values().stream().toList();
TypeDescriptorNode unionTypeDescriptorNode = qualifiedNodeList.get(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
import io.ballerina.compiler.syntax.tree.ImportDeclarationNode;
import io.ballerina.compiler.syntax.tree.ModuleMemberDeclarationNode;
import io.ballerina.compiler.syntax.tree.ModulePartNode;
import io.ballerina.compiler.syntax.tree.NameReferenceNode;
import io.ballerina.compiler.syntax.tree.Node;
import io.ballerina.compiler.syntax.tree.NodeFactory;
import io.ballerina.compiler.syntax.tree.NodeList;
import io.ballerina.compiler.syntax.tree.NodeParser;
import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode;
import io.ballerina.compiler.syntax.tree.RecordFieldNode;
import io.ballerina.compiler.syntax.tree.RecordTypeDescriptorNode;
import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode;
import io.ballerina.compiler.syntax.tree.SyntaxKind;
import io.ballerina.compiler.syntax.tree.SyntaxTree;
import io.ballerina.compiler.syntax.tree.Token;
Expand Down Expand Up @@ -198,11 +198,16 @@ public TypeDescriptorNode generateHeaderType(Schema headersSchema) {
return getTypeNodeFromOASSchema(headersSchema).orElse(null);
}

public SimpleNameReferenceNode createTypeInclusionRecord(String statusCode, TypeDescriptorNode bodyType,
TypeDescriptorNode headersType, String method) {
public NameReferenceNode createTypeInclusionRecord(String statusCode, TypeDescriptorNode bodyType,
TypeDescriptorNode headersType, String method) {
String recordName;
String statusCodeName = statusCode.equals(DEFAULT_STATUS_CODE_RESPONSE) ? DEFAULT_STATUS : statusCode;
if (bodyType != null) {

if (Objects.isNull(bodyType) && Objects.isNull(headersType)) {
return GeneratorUtils.getQualifiedNameReferenceNode(GeneratorConstants.HTTP, statusCode);
}

if (Objects.nonNull(bodyType)) {
String bodyTypeStr = bodyType.toString().replaceAll("[\\[\\\\]]", "Array");
recordName = GeneratorUtils.getValidName(bodyTypeStr, true) + statusCodeName;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,27 @@ public void serviceAndClientWithDefaultStatusCodeBinding() throws IOException, I
compareFiles(projectGenPath, "utils.bal", projectExpectedPath, "utils_all_with_default.bal");
}

@Test(description = "`--status-code-binding` option with responses with optional header or body schema")
public void optionalSchemaWithStatusCodeBinding() throws IOException, InterruptedException {
String openapiFilePath = "openapi.yaml";
List<String> buildArgs = new LinkedList<>();
buildArgs.add("-i");
buildArgs.add(openapiFilePath);
buildArgs.add("--status-code-binding");
buildArgs.add("--mode");
buildArgs.add("client");
Path projectGenPath = Paths.get(TEST_RESOURCE + "/project-10");
Path projectExpectedPath = Paths.get(EXPECTED_RESOURCE + "/expected/project-10");
boolean successful = TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, projectGenPath, buildArgs);
Assert.assertTrue(Files.exists(projectGenPath.resolve("Ballerina.toml")));
Assert.assertTrue(Files.exists(projectGenPath.resolve("client.bal")));
compareFiles(projectGenPath, "client.bal", projectExpectedPath, "client.bal");
Assert.assertTrue(Files.exists(projectGenPath.resolve("types.bal")));
compareFiles(projectGenPath, "types.bal", projectExpectedPath, "types.bal");
Assert.assertTrue(Files.exists(projectGenPath.resolve("utils.bal")));
compareFiles(projectGenPath, "utils.bal", projectExpectedPath, "utils.bal");
}

/**
* Compare two files.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// AUTO-GENERATED FILE. DO NOT MODIFY.
// This file is auto-generated by the Ballerina OpenAPI tool.

import ballerina/http;
import ballerina/jballerina.java;

function setModule() = @java:Method {'class: "io.ballerina.openapi.client.ModuleUtils"} external;

function init() {
setModule();
}

type ClientMethodImpl record {|
string name;
|};

annotation ClientMethodImpl MethodImpl on function;

type ClientMethodInvocationError http:ClientError;

public isolated client class Client {
final http:StatusCodeClient clientEp;
# Gets invoked to initialize the `connector`.
#
# + config - The configurations to be used when initializing the `connector`
# + serviceUrl - URL of the target service
# + return - An error if connector initialization failed
public isolated function init(ConnectionConfig config = {}, string serviceUrl = "http://localhost:8080/socialMedia") returns error? {
http:ClientConfiguration httpClientConfig = {httpVersion: config.httpVersion, timeout: config.timeout, forwarded: config.forwarded, poolConfig: config.poolConfig, compression: config.compression, circuitBreaker: config.circuitBreaker, retryConfig: config.retryConfig, validation: config.validation};
do {
if config.http1Settings is ClientHttp1Settings {
ClientHttp1Settings settings = check config.http1Settings.ensureType(ClientHttp1Settings);
httpClientConfig.http1Settings = {...settings};
}
if config.http2Settings is http:ClientHttp2Settings {
httpClientConfig.http2Settings = check config.http2Settings.ensureType(http:ClientHttp2Settings);
}
if config.cache is http:CacheConfig {
httpClientConfig.cache = check config.cache.ensureType(http:CacheConfig);
}
if config.responseLimits is http:ResponseLimitConfigs {
httpClientConfig.responseLimits = check config.responseLimits.ensureType(http:ResponseLimitConfigs);
}
if config.secureSocket is http:ClientSecureSocket {
httpClientConfig.secureSocket = check config.secureSocket.ensureType(http:ClientSecureSocket);
}
if config.proxy is http:ProxyConfig {
httpClientConfig.proxy = check config.proxy.ensureType(http:ProxyConfig);
}
}
http:StatusCodeClient httpEp = check new (serviceUrl, httpClientConfig);
self.clientEp = httpEp;
return;
}

# Get all products
#
# + headers - Headers to be sent with the request
# + return - Ok
@MethodImpl {name: "getProductsImpl"}
resource isolated function get products(map<string|string[]> headers = {}, typedesc<Ok> targetType = <>) returns targetType|error = @java:Method {'class: "io.ballerina.openapi.client.GeneratedClient", name: "invokeResourceWithoutPath"} external;

# Get user by id
#
# + headers - Headers to be sent with the request
# + return - Ok
@MethodImpl {name: "getUserByIdImpl"}
resource isolated function get users/[int id](map<string|string[]> headers = {}, typedesc<UserOk> targetType = <>) returns targetType|error = @java:Method {'class: "io.ballerina.openapi.client.GeneratedClient", name: "invokeResource"} external;

# Get all users
#
# + headers - Headers to be sent with the request
# + return - Ok
@MethodImpl {name: "getUsersImpl"}
resource isolated function get users(map<string|string[]> headers = {}, typedesc<http:Ok> targetType = <>) returns targetType|error = @java:Method {'class: "io.ballerina.openapi.client.GeneratedClient", name: "invokeResourceWithoutPath"} external;

private isolated function getProductsImpl(map<string|string[]> headers, typedesc<Ok> targetType) returns http:StatusCodeResponse|error {
string resourcePath = string `/products`;
return self.clientEp->get(resourcePath, headers, targetType = targetType);
}

private isolated function getUserByIdImpl(int id, map<string|string[]> headers, typedesc<UserOk> targetType) returns http:StatusCodeResponse|error {
string resourcePath = string `/users/${getEncodedUri(id)}`;
return self.clientEp->get(resourcePath, headers, targetType = targetType);
}

private isolated function getUsersImpl(map<string|string[]> headers, typedesc<http:Ok> targetType) returns http:StatusCodeResponse|error {
string resourcePath = string `/users`;
return self.clientEp->get(resourcePath, headers, targetType = targetType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// AUTO-GENERATED FILE. DO NOT MODIFY.
// This file is auto-generated by the Ballerina OpenAPI tool.

import ballerina/http;

public type User record {
int id?;
string name?;
string email?;
string password?;
};

# Provides settings related to HTTP/1.x protocol.
public type ClientHttp1Settings record {|
# Specifies whether to reuse a connection for multiple requests
http:KeepAlive keepAlive = http:KEEPALIVE_AUTO;
# The chunking behaviour of the request
http:Chunking chunking = http:CHUNKING_AUTO;
# Proxy server related options
ProxyConfig proxy?;
|};

public type UserOk record {|
*http:Ok;
User body;
|};

# Proxy server configurations to be used with the HTTP client endpoint.
public type ProxyConfig record {|
# Host name of the proxy server
string host = "";
# Proxy server port
int port = 0;
# Proxy server username
string userName = "";
# Proxy server password
@display {label: "", kind: "password"}
string password = "";
|};

public type Ok record {|
*http:Ok;
record {|int:Signed32 X\-Rate\-Limit?;|} headers;
|};

# Provides a set of configurations for controlling the behaviours when communicating with a remote HTTP endpoint.
@display {label: "Connection Config"}
public type ConnectionConfig record {|
# The HTTP version understood by the client
http:HttpVersion httpVersion = http:HTTP_2_0;
# Configurations related to HTTP/1.x protocol
ClientHttp1Settings http1Settings?;
# Configurations related to HTTP/2 protocol
http:ClientHttp2Settings http2Settings?;
# The maximum time to wait (in seconds) for a response before closing the connection
decimal timeout = 60;
# The choice of setting `forwarded`/`x-forwarded` header
string forwarded = "disable";
# Configurations associated with request pooling
http:PoolConfiguration poolConfig?;
# HTTP caching related configurations
http:CacheConfig cache?;
# Specifies the way of handling compression (`accept-encoding`) header
http:Compression compression = http:COMPRESSION_AUTO;
# Configurations associated with the behaviour of the Circuit Breaker
http:CircuitBreakerConfig circuitBreaker?;
# Configurations associated with retrying
http:RetryConfig retryConfig?;
# Configurations associated with inbound response size limits
http:ResponseLimitConfigs responseLimits?;
# SSL/TLS-related options
http:ClientSecureSocket secureSocket?;
# Proxy server related options
http:ProxyConfig proxy?;
# Enables the inbound payload validation functionality which provided by the constraint package. Enabled by default
boolean validation = true;
|};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// AUTO-GENERATED FILE. DO NOT MODIFY.
// This file is auto-generated by the Ballerina OpenAPI tool.

import ballerina/url;

# Get Encoded URI for a given value.
#
# + value - Value to be encoded
# + return - Encoded string
isolated function getEncodedUri(anydata value) returns string {
string|error encoded = url:encode(value.toString(), "UTF8");
if encoded is string {
return encoded;
} else {
return value.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
org = "openapi_client_test"
name = "project"
version = "0.1.0"

[build-options]
observabilityIncluded = true

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
openapi: 3.0.1
info:
title: SocialMedia
version: 0.1.0
servers:
- url: "{server}:{port}/socialMedia"
variables:
server:
default: http://localhost
port:
default: "8080"
paths:
/users:
get:
summary: Get all users
operationId: getUsers
responses:
"200":
description: Ok
/products:
get:
summary: Get all products
operationId: getProducts
responses:
"200":
description: Ok
headers:
X-Rate-Limit:
description: Rate limit
schema:
type: integer
format: int32
/users/{id}:
get:
summary: Get user by id
operationId: getUserById
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/User"
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
password:
type: string

0 comments on commit 7cc6376

Please sign in to comment.