From 897da492184121ed4da3484be7eac084f334d4b9 Mon Sep 17 00:00:00 2001 From: Justin Larrabee Date: Wed, 2 Nov 2022 09:02:31 -0600 Subject: [PATCH 1/3] Hacking up the csharp netcore generator to use UnityWebRequest so we can support all platforms (xbox doesn't allow built-in c# libs that don't use their api under the hood) --- .../languages/CSharpNetCoreClientCodegen.java | 13 + .../libraries/unity/ApiClient.mustache | 701 ++++++++++++++++++ .../libraries/unity/RequestOptions.mustache | 60 ++ .../libraries/unity/api.mustache | 674 +++++++++++++++++ .../libraries/unity/model.mustache | 51 ++ 5 files changed, 1499 insertions(+) create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/RequestOptions.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/api.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/model.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java index 5275022545e3..90be1b0ed6a2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java @@ -61,6 +61,7 @@ public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen { protected static final String RESTSHARP = "restsharp"; protected static final String HTTPCLIENT = "httpclient"; protected static final String GENERICHOST = "generichost"; + protected static final String UNITY = "unity"; // Project Variable, determined from target framework. Not intended to be user-settable. protected static final String TARGET_FRAMEWORK_IDENTIFIER = "targetFrameworkIdentifier"; @@ -325,6 +326,8 @@ public CSharpNetCoreClientCodegen() { + "(Experimental. Subject to breaking changes without notice.)"); supportedLibraries.put(HTTPCLIENT, "HttpClient (https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) " + "(Experimental. Subject to breaking changes without notice.)"); + supportedLibraries.put(UNITY, "UnityWebRequest (...) " + + "(Experimental. Subject to breaking changes without notice.)"); supportedLibraries.put(RESTSHARP, "RestSharp (https://github.com/restsharp/RestSharp)"); CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "HTTP library template (sub-template) to use"); @@ -678,6 +681,11 @@ public void processOpts() { setLibrary(HTTPCLIENT); additionalProperties.put("useHttpClient", true); needsUriBuilder = true; + } else if (UNITY.equals(getLibrary())) { + setLibrary(UNITY); + additionalProperties.put("useUnityClient", true); + needsUriBuilder = true; + } else { throw new RuntimeException("Invalid HTTP library " + getLibrary() + ". Only restsharp, httpclient, and generichost are supported."); } @@ -794,6 +802,11 @@ public void processOpts() { addGenericHostSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir); additionalProperties.put("apiDocPath", apiDocPath + File.separatorChar + "apis"); additionalProperties.put("modelDocPath", modelDocPath + File.separatorChar + "models"); + } else if (UNITY.equals(getLibrary())) { + setSupportsRetry(false); + setSupportsAsync(true); + + addSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir, authPackageDir); } else { //restsharp addSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir, authPackageDir); additionalProperties.put("apiDocPath", apiDocPath); diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache new file mode 100644 index 000000000000..c9a6af52be1f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache @@ -0,0 +1,701 @@ +{{>partial_header}} + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters; +using System.Text; +using System.Threading; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +{{^netStandard}} +using System.Web; +{{/netStandard}} +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; +using System.Net.Http; +using System.Net.Http.Headers; +using UnityEngine.Networking; +using UnityEngine; +{{#supportsRetry}} +using Polly; +{{/supportsRetry}} + +namespace {{packageName}}.Client +{ + /// + /// To Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON. + /// + internal class CustomJsonCodec + { + private readonly IReadableConfiguration _configuration; + private static readonly string _contentType = "application/json"; + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings + { + // OpenAPI generated types generally hide default constructors. + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + OverrideSpecifiedNames = false + } + } + }; + + public CustomJsonCodec(IReadableConfiguration configuration) + { + _configuration = configuration; + } + + public CustomJsonCodec(JsonSerializerSettings serializerSettings, IReadableConfiguration configuration) + { + _serializerSettings = serializerSettings; + _configuration = configuration; + } + + /// + /// Serialize the object into a JSON string. + /// + /// Object to be serialized. + /// A JSON string. + public string Serialize(object obj) + { + if (obj != null && obj is {{{packageName}}}.{{modelPackage}}.AbstractOpenAPISchema) + { + // the object to be serialized is an oneOf/anyOf schema + return (({{{packageName}}}.{{modelPackage}}.AbstractOpenAPISchema)obj).ToJson(); + } + else + { + return JsonConvert.SerializeObject(obj, _serializerSettings); + } + } + + public T Deserialize(UnityWebRequest request) + { + var result = (T) Deserialize(response, typeof(T)); + return result; + } + + /// + /// Deserialize the JSON string into a proper object. + /// + /// The HTTP response. + /// Object type. + /// Object representation of the JSON string. + internal object Deserialize(UnityWebRequest request, Type type) + { + IList headers = response.Headers.Select(x => x.Key + "=" + x.Value).ToList(); + + if (type == typeof(byte[])) // return byte array + { + return request.downloadHandler.data; + } + + // TODO: ? if (type.IsAssignableFrom(typeof(Stream))) + if (type == typeof(Stream)) + { + var bytes = await response.Content.ReadAsByteArrayAsync(); + // NOTE: Ignoring Content-Disposition filename support, since not all platforms + // have a location on disk to write arbitrary data (tvOS, consoles). + return new MemoryStream(request.downloadHandler.data); + } + + if (type.Name.StartsWith("System.Nullable`1[[System.DateTime")) // return a datetime object + { + return DateTime.Parse(request.downloadHandler.text, null, System.Globalization.DateTimeStyles.RoundtripKind); + } + + if (type == typeof(string) || type.Name.StartsWith("System.Nullable")) // return primitive type + { + return Convert.ChangeType(request.downloadHandler.text, type); + } + + // at this point, it must be a model (json) + try + { + return JsonConvert.DeserializeObject(request.downloadHandler.text, type, _serializerSettings); + } + catch (Exception e) + { + throw new ApiException(500, e.Message); + } + } + + public string RootElement { get; set; } + public string Namespace { get; set; } + public string DateFormat { get; set; } + + public string ContentType + { + get { return _contentType; } + set { throw new InvalidOperationException("Not allowed to set content type."); } + } + } + /// + /// Provides a default implementation of an Api client (both synchronous and asynchronous implementations), + /// encapsulating general REST accessor use cases. + /// + /// + /// The Dispose method will manage the HttpClient lifecycle when not passed by constructor. + /// + {{>visibility}} partial class ApiClient : IDisposable, ISynchronousClient{{#supportsAsync}}, IAsynchronousClient{{/supportsAsync}} + { + private readonly string _baseUrl; + + /// + /// Specifies the settings on a object. + /// These settings can be adjusted to accommodate custom serialization rules. + /// + public JsonSerializerSettings SerializerSettings { get; set; } = new JsonSerializerSettings + { + // OpenAPI generated types generally hide default constructors. + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + OverrideSpecifiedNames = false + } + } + }; + + /// + /// Initializes a new instance of the , defaulting to the global configurations' base url. + /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. + /// It's better to reuse the HttpClient and HttpClientHandler. + /// + public ApiClient() : + this({{packageName}}.Client.GlobalConfiguration.Instance.BasePath) + { + } + + /// + /// Initializes a new instance of the . + /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. + /// It's better to reuse the HttpClient and HttpClientHandler. + /// + /// The target service's base path in URL format. + /// + public ApiClient(string basePath) + { + if (string.IsNullOrEmpty(basePath)) throw new ArgumentException("basePath cannot be empty"); + + _baseUrl = basePath; + } + + /// + /// Disposes resources if they were created by us + /// + public void Dispose() + { + } + + /// + /// Provides all logic for constructing a new HttpRequestMessage. + /// At this point, all information for querying the service is known. Here, it is simply + /// mapped into the a HttpRequestMessage. + /// + /// The http verb. + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// [private] A new HttpRequestMessage instance. + /// + private UnityWebRequest NewRequest( + string method, + string path, + RequestOptions options, + IReadableConfiguration configuration) + { + if (path == null) throw new ArgumentNullException("path"); + if (options == null) throw new ArgumentNullException("options"); + if (configuration == null) throw new ArgumentNullException("configuration"); + + WebRequestPathBuilder builder = new WebRequestPathBuilder(_baseUrl, path); + + builder.AddPathParameters(options.PathParameters); + + builder.AddQueryParameters(options.QueryParameters); + + string contentType = null; + if (options.HeaderParameters != null && options.HeaderParameters.ContainsKey("Content-Type")) + { + var contentTypes = options.HeaderParameters["Content-Type"]; + contentType = contentTypes.FirstOrDefault(); + } + + UnityWebRequest request = new UnityWebRequest(builder.GetFullUri(), method); + var setContentTypeHeader = false; + + if (contentType == "multipart/form-data") + { + var formData = new List(); + foreach (var formParameter in options.FormParameters) + { + formData.Add(new MultipartFormDataSection(formParameter.Key, formParameter.Value)); + } + + var boundary = UnityWebRequest.GenerateBoundary(); + var data = UnityWebRequest.SerializeFormSections(formData, boundary); + request.uploadHandler = new UploadHandlerRaw(data); + request.uploadHandler.contentType = "multipart/form-data; boundary=" + System.Text.Encoding.UTF8.GetString(boundary, 0, boundary.Length); + + setContentTypeHeader = true; + } + /* + else if (contentType == "application/x-www-form-urlencoded") + { + request.Content = new FormUrlEncodedContent(options.FormParameters); + } + else + { + if (options.Data != null) + { + var serializer = new CustomJsonCodec(SerializerSettings, configuration); + request.Content = new StringContent(serializer.Serialize(options.Data), new UTF8Encoding(), + "application/json"); + } + } + + if (!string.IsNullOrEmpty(contentType) && !setContentTypeHeader) + { + request.SetRequestHeader("Content-Type", contentType); + } + + + switch (method) { + case "GET": + { + request = UnityWebRequest.Get(uri); + break; + } + + case "POST": + { + if (contentType == "application/json") { + // Making a post body application/json encoded is whack with UnityWebRequest. + // See: https://stackoverflow.com/questions/68156230/unitywebrequest-post-not-sending-body + request = UnityWebRequest.Put(uri, serializedJsonData); + request.method = "POST"; + } + else + { + request = UnityWeb + } + break; + } + + } + + if (configuration.UserAgent != null) + { + request.Headers.TryAddWithoutValidation("User-Agent", configuration.UserAgent); + } + + if (configuration.DefaultHeaders != null) + { + foreach (var headerParam in configuration.DefaultHeaders) + { + request.Headers.Add(headerParam.Key, headerParam.Value); + } + } + + if (options.HeaderParameters != null) + { + foreach (var headerParam in options.HeaderParameters) + { + foreach (var value in headerParam.Value) + { + // Todo make content headers actually content headers + request.Headers.TryAddWithoutValidation(headerParam.Key, value); + } + } + } + + // TODO provide an alternative that allows cookies per request instead of per API client + if (options.Cookies != null && options.Cookies.Count > 0) + { + request.Properties["CookieContainer"] = options.Cookies; + } + */ + + return request; + } + + partial void InterceptRequest(HttpRequestMessage req); + partial void InterceptResponse(HttpRequestMessage req, HttpResponseMessage response); + + /* + private async Task> ToApiResponse(HttpResponseMessage response, object responseData, Uri uri) + { + T result = (T) responseData; + string rawContent = await response.Content.ReadAsStringAsync(); + + var transformed = new ApiResponse(response.StatusCode, new Multimap({{#caseInsensitiveResponseHeaders}}StringComparer.OrdinalIgnoreCase{{/caseInsensitiveResponseHeaders}}), result, rawContent) + { + ErrorText = response.ReasonPhrase, + Cookies = new List() + }; + + // process response headers, e.g. Access-Control-Allow-Methods + if (response.Headers != null) + { + foreach (var responseHeader in response.Headers) + { + transformed.Headers.Add(responseHeader.Key, ClientUtils.ParameterToString(responseHeader.Value)); + } + } + + // process response content headers, e.g. Content-Type + if (response.Content.Headers != null) + { + foreach (var responseHeader in response.Content.Headers) + { + transformed.Headers.Add(responseHeader.Key, ClientUtils.ParameterToString(responseHeader.Value)); + } + } + + if (_httpClientHandler != null && response != null) + { + try { + foreach (Cookie cookie in _httpClientHandler.CookieContainer.GetCookies(uri)) + { + transformed.Cookies.Add(cookie); + } + } + catch (PlatformNotSupportedException) {} + } + + return transformed; + } + */ + + private ApiResponse Exec(HttpRequestMessage req, IReadableConfiguration configuration) + { + return ExecAsync(req, configuration).GetAwaiter().GetResult(); + } + + private async Task> ExecAsync(HttpRequestMessage req, + IReadableConfiguration configuration, + System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + await Task.Delay(1000); + /* + CancellationTokenSource timeoutTokenSource = null; + CancellationTokenSource finalTokenSource = null; + var deserializer = new CustomJsonCodec(SerializerSettings, configuration); + var finalToken = cancellationToken; + + try + { + if (configuration.Timeout > 0) + { + timeoutTokenSource = new CancellationTokenSource(configuration.Timeout); + finalTokenSource = CancellationTokenSource.CreateLinkedTokenSource(finalToken, timeoutTokenSource.Token); + finalToken = finalTokenSource.Token; + } + + if (configuration.Proxy != null) + { + if(_httpClientHandler == null) throw new InvalidOperationException("Configuration `Proxy` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor."); + _httpClientHandler.Proxy = configuration.Proxy; + } + + if (configuration.ClientCertificates != null) + { + if(_httpClientHandler == null) throw new InvalidOperationException("Configuration `ClientCertificates` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor."); + _httpClientHandler.ClientCertificates.AddRange(configuration.ClientCertificates); + } + + var cookieContainer = req.Properties.ContainsKey("CookieContainer") ? req.Properties["CookieContainer"] as List : null; + + if (cookieContainer != null) + { + if(_httpClientHandler == null) throw new InvalidOperationException("Request property `CookieContainer` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor."); + foreach (var cookie in cookieContainer) + { + _httpClientHandler.CookieContainer.Add(cookie); + } + } + + InterceptRequest(req); + + HttpResponseMessage response; + {{#supportsRetry}} + if (RetryConfiguration.AsyncRetryPolicy != null) + { + var policy = RetryConfiguration.AsyncRetryPolicy; + var policyResult = await policy + .ExecuteAndCaptureAsync(() => _httpClient.SendAsync(req, finalToken)) + .ConfigureAwait(false); + response = (policyResult.Outcome == OutcomeType.Successful) ? + policyResult.Result : new HttpResponseMessage() + { + ReasonPhrase = policyResult.FinalException.ToString(), + RequestMessage = req + }; + } + else + { + {{/supportsRetry}} + response = await _httpClient.SendAsync(req, finalToken).ConfigureAwait(false); + {{#supportsRetry}} + } + {{/supportsRetry}} + + if (!response.IsSuccessStatusCode) + { + return await ToApiResponse(response, default(T), req.RequestUri); + } + + object responseData = await deserializer.Deserialize(response); + + // if the response type is oneOf/anyOf, call FromJSON to deserialize the data + if (typeof({{{packageName}}}.{{modelPackage}}.AbstractOpenAPISchema).IsAssignableFrom(typeof(T))) + { + responseData = (T) typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content }); + } + else if (typeof(T).Name == "Stream") // for binary response + { + responseData = (T) (object) await response.Content.ReadAsStreamAsync(); + } + + InterceptResponse(req, response); + + return await ToApiResponse(response, responseData, req.RequestUri); + } + finally + { + if (timeoutTokenSource != null) + { + timeoutTokenSource.Dispose(); + } + + if (finalTokenSource != null) + { + finalTokenSource.Dispose(); + } + } + */ + } + + {{#supportsAsync}} + #region IAsynchronousClient + /// + /// Make a HTTP GET request (async). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// Token that enables callers to cancel the request. + /// A Task containing ApiResponse + public Task> GetAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest("GET", path, options, config), config, cancellationToken); + } + + /// + /// Make a HTTP POST request (async). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// Token that enables callers to cancel the request. + /// A Task containing ApiResponse + public Task> PostAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest("POST", path, options, config), config, cancellationToken); + } + + /// + /// Make a HTTP PUT request (async). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// Token that enables callers to cancel the request. + /// A Task containing ApiResponse + public Task> PutAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest("PUT", path, options, config), config, cancellationToken); + } + + /// + /// Make a HTTP DELETE request (async). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// Token that enables callers to cancel the request. + /// A Task containing ApiResponse + public Task> DeleteAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest("DELETE", path, options, config), config, cancellationToken); + } + + /// + /// Make a HTTP HEAD request (async). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// Token that enables callers to cancel the request. + /// A Task containing ApiResponse + public Task> HeadAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest("HEAD", path, options, config), config, cancellationToken); + } + + /// + /// Make a HTTP OPTION request (async). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// Token that enables callers to cancel the request. + /// A Task containing ApiResponse + public Task> OptionsAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest("OPTIONS", path, options, config), config, cancellationToken); + } + + /// + /// Make a HTTP PATCH request (async). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// Token that enables callers to cancel the request. + /// A Task containing ApiResponse + public Task> PatchAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest("PATCH", path, options, config), config, cancellationToken); + } + #endregion IAsynchronousClient + {{/supportsAsync}} + + #region ISynchronousClient + /// + /// Make a HTTP GET request (synchronous). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// A Task containing ApiResponse + public ApiResponse Get(string path, RequestOptions options, IReadableConfiguration configuration = null) + { + var config = configuration ?? GlobalConfiguration.Instance; + return Exec(NewRequest("GET", path, options, config), config); + } + + /// + /// Make a HTTP POST request (synchronous). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// A Task containing ApiResponse + public ApiResponse Post(string path, RequestOptions options, IReadableConfiguration configuration = null) + { + var config = configuration ?? GlobalConfiguration.Instance; + return Exec(NewRequest("POST", path, options, config), config); + } + + /// + /// Make a HTTP PUT request (synchronous). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// A Task containing ApiResponse + public ApiResponse Put(string path, RequestOptions options, IReadableConfiguration configuration = null) + { + var config = configuration ?? GlobalConfiguration.Instance; + return Exec(NewRequest("PUT", path, options, config), config); + } + + /// + /// Make a HTTP DELETE request (synchronous). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// A Task containing ApiResponse + public ApiResponse Delete(string path, RequestOptions options, IReadableConfiguration configuration = null) + { + var config = configuration ?? GlobalConfiguration.Instance; + return Exec(NewRequest("DELETE", path, options, config), config); + } + + /// + /// Make a HTTP HEAD request (synchronous). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// A Task containing ApiResponse + public ApiResponse Head(string path, RequestOptions options, IReadableConfiguration configuration = null) + { + var config = configuration ?? GlobalConfiguration.Instance; + return Exec(NewRequest("HEAD", path, options, config), config); + } + + /// + /// Make a HTTP OPTION request (synchronous). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// A Task containing ApiResponse + public ApiResponse Options(string path, RequestOptions options, IReadableConfiguration configuration = null) + { + var config = configuration ?? GlobalConfiguration.Instance; + return Exec(NewRequest("OPTIONS", path, options, config), config); + } + + /// + /// Make a HTTP PATCH request (synchronous). + /// + /// The target path (or resource). + /// The additional request options. + /// A per-request configuration object. It is assumed that any merge with + /// GlobalConfiguration has been done before calling this method. + /// A Task containing ApiResponse + public ApiResponse Patch(string path, RequestOptions options, IReadableConfiguration configuration = null) + { + var config = configuration ?? GlobalConfiguration.Instance; + return Exec(NewRequest("PATCH", path, options, config), config); + } + #endregion ISynchronousClient + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/RequestOptions.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/RequestOptions.mustache new file mode 100644 index 000000000000..0dd18c4ed609 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/RequestOptions.mustache @@ -0,0 +1,60 @@ +{{>partial_header}} + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; + +namespace {{packageName}}.Client +{ + /// + /// A container for generalized request inputs. This type allows consumers to extend the request functionality + /// by abstracting away from the default (built-in) request framework (e.g. RestSharp). + /// + public class RequestOptions + { + /// + /// Parameters to be bound to path parts of the Request's URL + /// + public Dictionary PathParameters { get; set; } + + /// + /// Query parameters to be applied to the request. + /// Keys may have 1 or more values associated. + /// + public Multimap QueryParameters { get; set; } + + /// + /// Header parameters to be applied to to the request. + /// Keys may have 1 or more values associated. + /// + public Multimap HeaderParameters { get; set; } + + /// + /// Form parameters to be sent along with the request. + /// + public Dictionary FormParameters { get; set; } + + /// + /// Cookies to be sent along with the request. + /// + public List Cookies { get; set; } + + /// + /// Any data associated with a request body. + /// + public Object Data { get; set; } + + /// + /// Constructs a new instance of + /// + public RequestOptions() + { + PathParameters = new Dictionary(); + QueryParameters = new Multimap(); + HeaderParameters = new Multimap(); + FormParameters = new Dictionary(); + Cookies = new List(); + } + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/api.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/api.mustache new file mode 100644 index 000000000000..f1c3392d785c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/api.mustache @@ -0,0 +1,674 @@ +{{>partial_header}} + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; +using System.Net.Mime; +using {{packageName}}.Client; +{{#hasImport}}using {{packageName}}.{{modelPackage}}; +{{/hasImport}} + +namespace {{packageName}}.{{apiPackage}} +{ + {{#operations}} + + /// + /// Represents a collection of functions to interact with the API endpoints + /// + {{>visibility}} interface {{interfacePrefix}}{{classname}}Sync : IApiAccessor + { + #region Synchronous Operations + {{#operation}} + /// + /// {{summary}} + /// + {{#notes}} + /// + /// {{.}} + /// + {{/notes}} + /// Thrown when fails to make API call + {{#allParams}}/// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} + {{/allParams}}/// {{returnType}} + {{#isDeprecated}} + [Obsolete] + {{/isDeprecated}} + {{{returnType}}}{{^returnType}}void{{/returnType}} {{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}); + + /// + /// {{summary}} + /// + /// + /// {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}}/// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} + {{/allParams}}/// ApiResponse of {{returnType}}{{^returnType}}Object(void){{/returnType}} + {{#isDeprecated}} + [Obsolete] + {{/isDeprecated}} + ApiResponse<{{{returnType}}}{{^returnType}}Object{{/returnType}}> {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}); + {{/operation}} + #endregion Synchronous Operations + } + + {{#supportsAsync}} + /// + /// Represents a collection of functions to interact with the API endpoints + /// + {{>visibility}} interface {{interfacePrefix}}{{classname}}Async : IApiAccessor + { + #region Asynchronous Operations + {{#operation}} + /// + /// {{summary}} + /// + /// + /// {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// Task of {{returnType}}{{^returnType}}void{{/returnType}} + {{#isDeprecated}} + [Obsolete] + {{/isDeprecated}} + {{#returnType}}System.Threading.Tasks.Task<{{{.}}}>{{/returnType}}{{^returnType}}System.Threading.Tasks.Task{{/returnType}} {{operationId}}Async({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// + /// {{summary}} + /// + /// + /// {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// Task of ApiResponse{{#returnType}} ({{.}}){{/returnType}} + {{#isDeprecated}} + [Obsolete] + {{/isDeprecated}} + System.Threading.Tasks.Task> {{operationId}}WithHttpInfoAsync({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + {{/operation}} + #endregion Asynchronous Operations + } + {{/supportsAsync}} + + /// + /// Represents a collection of functions to interact with the API endpoints + /// + {{>visibility}} interface {{interfacePrefix}}{{classname}} : {{interfacePrefix}}{{classname}}Sync{{#supportsAsync}}, {{interfacePrefix}}{{classname}}Async{{/supportsAsync}} + { + + } + + /// + /// Represents a collection of functions to interact with the API endpoints + /// + {{>visibility}} partial class {{classname}} : IDisposable, {{interfacePrefix}}{{classname}} + { + private {{packageName}}.Client.ExceptionFactory _exceptionFactory = (name, response) => null; + + /// + /// Initializes a new instance of the class. + /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. + /// It's better to reuse the HttpClient and HttpClientHandler. + /// + /// + public {{classname}}() : this((string)null) + { + } + + /// + /// Initializes a new instance of the class. + /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. + /// It's better to reuse the HttpClient and HttpClientHandler. + /// + /// The target service's base path in URL format. + /// + /// + public {{classname}}(string basePath) + { + this.Configuration = {{packageName}}.Client.Configuration.MergeConfigurations( + {{packageName}}.Client.GlobalConfiguration.Instance, + new {{packageName}}.Client.Configuration { BasePath = basePath } + ); + this.ApiClient = new {{packageName}}.Client.ApiClient(this.Configuration.BasePath); + this.Client = this.ApiClient; + {{#supportsAsync}} + this.AsynchronousClient = this.ApiClient; + {{/supportsAsync}} + this.ExceptionFactory = {{packageName}}.Client.Configuration.DefaultExceptionFactory; + } + + /// + /// Initializes a new instance of the class using Configuration object. + /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. + /// It's better to reuse the HttpClient and HttpClientHandler. + /// + /// An instance of Configuration. + /// + /// + public {{classname}}({{packageName}}.Client.Configuration configuration) + { + if (configuration == null) throw new ArgumentNullException("configuration"); + + this.Configuration = {{packageName}}.Client.Configuration.MergeConfigurations( + {{packageName}}.Client.GlobalConfiguration.Instance, + configuration + ); + this.ApiClient = new {{packageName}}.Client.ApiClient(this.Configuration.BasePath); + this.Client = this.ApiClient; + {{#supportsAsync}} + this.AsynchronousClient = this.ApiClient; + {{/supportsAsync}} + ExceptionFactory = {{packageName}}.Client.Configuration.DefaultExceptionFactory; + } + + /// + /// Initializes a new instance of the class + /// using a Configuration object and client instance. + /// + /// The client interface for synchronous API access.{{#supportsAsync}} + /// The client interface for asynchronous API access.{{/supportsAsync}} + /// The configuration object. + /// + public {{classname}}({{packageName}}.Client.ISynchronousClient client, {{#supportsAsync}}{{packageName}}.Client.IAsynchronousClient asyncClient, {{/supportsAsync}}{{packageName}}.Client.IReadableConfiguration configuration) + { + if (client == null) throw new ArgumentNullException("client"); + {{#supportsAsync}} + if (asyncClient == null) throw new ArgumentNullException("asyncClient"); + {{/supportsAsync}} + if (configuration == null) throw new ArgumentNullException("configuration"); + + this.Client = client; + {{#supportsAsync}} + this.AsynchronousClient = asyncClient; + {{/supportsAsync}} + this.Configuration = configuration; + this.ExceptionFactory = {{packageName}}.Client.Configuration.DefaultExceptionFactory; + } + + /// + /// Disposes resources if they were created by us + /// + public void Dispose() + { + this.ApiClient?.Dispose(); + } + + /// + /// Holds the ApiClient if created + /// + public {{packageName}}.Client.ApiClient ApiClient { get; set; } = null; + + {{#supportsAsync}} + /// + /// The client for accessing this underlying API asynchronously. + /// + public {{packageName}}.Client.IAsynchronousClient AsynchronousClient { get; set; } + {{/supportsAsync}} + + /// + /// The client for accessing this underlying API synchronously. + /// + public {{packageName}}.Client.ISynchronousClient Client { get; set; } + + /// + /// Gets the base path of the API client. + /// + /// The base path + public string GetBasePath() + { + return this.Configuration.BasePath; + } + + /// + /// Gets or sets the configuration object + /// + /// An instance of the Configuration + public {{packageName}}.Client.IReadableConfiguration Configuration { get; set; } + + /// + /// Provides a factory method hook for the creation of exceptions. + /// + public {{packageName}}.Client.ExceptionFactory ExceptionFactory + { + get + { + if (_exceptionFactory != null && _exceptionFactory.GetInvocationList().Length > 1) + { + throw new InvalidOperationException("Multicast delegate for ExceptionFactory is unsupported."); + } + return _exceptionFactory; + } + set { _exceptionFactory = value; } + } + + {{#operation}} + /// + /// {{summary}} {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}}/// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} + {{/allParams}}/// {{returnType}} + {{#isDeprecated}} + [Obsolete] + {{/isDeprecated}} + public {{{returnType}}}{{^returnType}}void{{/returnType}} {{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) + { + {{#returnType}}{{packageName}}.Client.ApiResponse<{{{returnType}}}> localVarResponse = {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + return localVarResponse.Data;{{/returnType}}{{^returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});{{/returnType}} + } + + /// + /// {{summary}} {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}}/// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} + {{/allParams}}/// ApiResponse of {{returnType}}{{^returnType}}Object(void){{/returnType}} + {{#isDeprecated}} + [Obsolete] + {{/isDeprecated}} + public {{packageName}}.Client.ApiResponse<{{{returnType}}}{{^returnType}}Object{{/returnType}}> {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) + { + {{#allParams}} + {{#required}} + {{^vendorExtensions.x-csharp-value-type}} + // verify the required parameter '{{paramName}}' is set + if ({{paramName}} == null) + throw new {{packageName}}.Client.ApiException(400, "Missing required parameter '{{paramName}}' when calling {{classname}}->{{operationId}}"); + + {{/vendorExtensions.x-csharp-value-type}} + {{/required}} + {{/allParams}} + {{packageName}}.Client.RequestOptions localVarRequestOptions = new {{packageName}}.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + {{#consumes}} + "{{{mediaType}}}"{{^-last}},{{/-last}} + {{/consumes}} + }; + + // to determine the Accept header + string[] _accepts = new string[] { + {{#produces}} + "{{{mediaType}}}"{{^-last}},{{/-last}} + {{/produces}} + }; + + var localVarContentType = {{packageName}}.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = {{packageName}}.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + {{#pathParams}} + {{#required}} + localVarRequestOptions.PathParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // path parameter + {{/required}} + {{^required}} + if ({{paramName}} != null) + { + localVarRequestOptions.PathParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // path parameter + } + {{/required}} + {{/pathParams}} + {{#queryParams}} + {{#required}} + {{#isDeepObject}} + {{#items.vars}} + localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}}.{{name}})); + {{/items.vars}} + {{^items}} + localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("deepObject", "{{baseName}}", {{paramName}})); + {{/items}} + {{/isDeepObject}} + {{^isDeepObject}} + localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}})); + {{/isDeepObject}} + {{/required}} + {{^required}} + if ({{paramName}} != null) + { + {{#isDeepObject}} + {{#items.vars}} + if ({{paramName}}.{{name}} != null) + { + localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}}.{{name}})); + } + {{/items.vars}} + {{^items}} + localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("deepObject", "{{baseName}}", {{paramName}})); + {{/items}} + {{/isDeepObject}} + {{^isDeepObject}} + localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}})); + {{/isDeepObject}} + } + {{/required}} + {{/queryParams}} + {{#headerParams}} + {{#required}} + localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter + {{/required}} + {{^required}} + if ({{paramName}} != null) + { + localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter + } + {{/required}} + {{/headerParams}} + {{#formParams}} + {{#required}} + {{#isFile}} + localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); + {{/isFile}} + {{^isFile}} + localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter + {{/isFile}} + {{/required}} + {{^required}} + if ({{paramName}} != null) + { + {{#isFile}} + localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); + {{/isFile}} + {{^isFile}} + localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter + {{/isFile}} + } + {{/required}} + {{/formParams}} + {{#bodyParam}} + localVarRequestOptions.Data = {{paramName}}; + {{/bodyParam}} + + {{#authMethods}} + // authentication ({{name}}) required + {{#isApiKey}} + {{#isKeyInCookie}} + // cookie parameter support + if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) + { + localVarRequestOptions.Cookies.Add(new Cookie("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))); + } + {{/isKeyInCookie}} + {{#isKeyInHeader}} + if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) + { + localVarRequestOptions.HeaderParameters.Add("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}")); + } + {{/isKeyInHeader}} + {{#isKeyInQuery}} + if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) + { + localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("", "{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))); + } + {{/isKeyInQuery}} + {{/isApiKey}} + {{#isBasicBasic}} + // http basic authentication required + if (!string.IsNullOrEmpty(this.Configuration.Username) || !string.IsNullOrEmpty(this.Configuration.Password) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Basic " + {{packageName}}.Client.ClientUtils.Base64Encode(this.Configuration.Username + ":" + this.Configuration.Password)); + } + {{/isBasicBasic}} + {{#isBasicBearer}} + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + {{/isBasicBearer}} + {{#isOAuth}} + // oauth required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + {{/isOAuth}} + {{#isHttpSignature}} + if (this.Configuration.HttpSigningConfiguration != null) + { + var HttpSigningHeaders = this.Configuration.HttpSigningConfiguration.GetHttpSignedHeader(this.Configuration.BasePath, "{{{httpMethod}}}", "{{{path}}}", localVarRequestOptions); + foreach (var headerItem in HttpSigningHeaders) + { + if (localVarRequestOptions.HeaderParameters.ContainsKey(headerItem.Key)) + { + localVarRequestOptions.HeaderParameters[headerItem.Key] = new List() { headerItem.Value }; + } + else + { + localVarRequestOptions.HeaderParameters.Add(headerItem.Key, headerItem.Value); + } + } + } + {{/isHttpSignature}} + {{/authMethods}} + + // make the HTTP request + var localVarResponse = this.Client.{{#lambda.titlecase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.titlecase}}<{{{returnType}}}{{^returnType}}Object{{/returnType}}>("{{{path}}}", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("{{operationId}}", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + {{#supportsAsync}} + /// + /// {{summary}} {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// Task of {{returnType}}{{^returnType}}void{{/returnType}} + {{#isDeprecated}} + [Obsolete] + {{/isDeprecated}} + {{#returnType}}public async System.Threading.Tasks.Task<{{{.}}}>{{/returnType}}{{^returnType}}public async System.Threading.Tasks.Task{{/returnType}} {{operationId}}Async({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + {{#returnType}}{{packageName}}.Client.ApiResponse<{{{returnType}}}> localVarResponse = await {{operationId}}WithHttpInfoAsync({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}cancellationToken).ConfigureAwait(false); + return localVarResponse.Data;{{/returnType}}{{^returnType}}await {{operationId}}WithHttpInfoAsync({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}cancellationToken).ConfigureAwait(false);{{/returnType}} + } + + /// + /// {{summary}} {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// Task of ApiResponse{{#returnType}} ({{.}}){{/returnType}} + {{#isDeprecated}} + [Obsolete] + {{/isDeprecated}} + public async System.Threading.Tasks.Task<{{packageName}}.Client.ApiResponse<{{{returnType}}}{{^returnType}}Object{{/returnType}}>> {{operationId}}WithHttpInfoAsync({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + {{#allParams}} + {{#required}} + {{^vendorExtensions.x-csharp-value-type}} + // verify the required parameter '{{paramName}}' is set + if ({{paramName}} == null) + throw new {{packageName}}.Client.ApiException(400, "Missing required parameter '{{paramName}}' when calling {{classname}}->{{operationId}}"); + + {{/vendorExtensions.x-csharp-value-type}} + {{/required}} + {{/allParams}} + + {{packageName}}.Client.RequestOptions localVarRequestOptions = new {{packageName}}.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + {{#consumes}} + "{{{mediaType}}}"{{^-last}}, {{/-last}} + {{/consumes}} + }; + + // to determine the Accept header + string[] _accepts = new string[] { + {{#produces}} + "{{{mediaType}}}"{{^-last}},{{/-last}} + {{/produces}} + }; + + + var localVarContentType = {{packageName}}.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = {{packageName}}.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + {{#pathParams}} + {{#required}} + localVarRequestOptions.PathParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // path parameter + {{/required}} + {{^required}} + if ({{paramName}} != null) + { + localVarRequestOptions.PathParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // path parameter + } + {{/required}} + {{/pathParams}} + {{#queryParams}} + {{#required}} + localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}})); + {{/required}} + {{^required}} + if ({{paramName}} != null) + { + localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}})); + } + {{/required}} + {{/queryParams}} + {{#headerParams}} + {{#required}} + localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter + {{/required}} + {{^required}} + if ({{paramName}} != null) + { + localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter + } + {{/required}} + {{/headerParams}} + {{#formParams}} + {{#required}} + {{#isFile}} + localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); + {{/isFile}} + {{^isFile}} + localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter + {{/isFile}} + {{/required}} + {{^required}} + if ({{paramName}} != null) + { + {{#isFile}} + localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); + {{/isFile}} + {{^isFile}} + localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter + {{/isFile}} + } + {{/required}} + {{/formParams}} + {{#bodyParam}} + localVarRequestOptions.Data = {{paramName}}; + {{/bodyParam}} + + {{#authMethods}} + // authentication ({{name}}) required + {{#isApiKey}} + {{#isKeyInCookie}} + // cookie parameter support + if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) + { + localVarRequestOptions.Cookies.Add(new Cookie("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))); + } + {{/isKeyInCookie}} + {{#isKeyInHeader}} + if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) + { + localVarRequestOptions.HeaderParameters.Add("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}")); + } + {{/isKeyInHeader}} + {{#isKeyInQuery}} + if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) + { + localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("", "{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))); + } + {{/isKeyInQuery}} + {{/isApiKey}} + {{#isBasic}} + {{#isBasicBasic}} + // http basic authentication required + if (!string.IsNullOrEmpty(this.Configuration.Username) || !string.IsNullOrEmpty(this.Configuration.Password) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Basic " + {{packageName}}.Client.ClientUtils.Base64Encode(this.Configuration.Username + ":" + this.Configuration.Password)); + } + {{/isBasicBasic}} + {{#isBasicBearer}} + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + {{/isBasicBearer}} + {{/isBasic}} + {{#isOAuth}} + // oauth required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + {{/isOAuth}} + {{#isHttpSignature}} + if (this.Configuration.HttpSigningConfiguration != null) + { + var HttpSigningHeaders = this.Configuration.HttpSigningConfiguration.GetHttpSignedHeader(this.Configuration.BasePath, "{{{httpMethod}}}", "{{{path}}}", localVarRequestOptions); + foreach (var headerItem in HttpSigningHeaders) + { + if (localVarRequestOptions.HeaderParameters.ContainsKey(headerItem.Key)) + { + localVarRequestOptions.HeaderParameters[headerItem.Key] = new List() { headerItem.Value }; + } + else + { + localVarRequestOptions.HeaderParameters.Add(headerItem.Key, headerItem.Value); + } + } + } + {{/isHttpSignature}} + {{/authMethods}} + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.{{#lambda.titlecase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.titlecase}}Async<{{{returnType}}}{{^returnType}}Object{{/returnType}}>("{{{path}}}", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("{{operationId}}", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + {{/supportsAsync}} + {{/operation}} + } + {{/operations}} +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/model.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/model.mustache new file mode 100644 index 000000000000..f84de7f64327 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/model.mustache @@ -0,0 +1,51 @@ +{{>partial_header}} + +{{#models}} +{{#model}} +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +{{#vendorExtensions.x-com-visible}} +using System.Runtime.InteropServices; +{{/vendorExtensions.x-com-visible}} +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +{{#discriminator}} +using JsonSubTypes; +{{/discriminator}} +{{/model}} +{{/models}} +{{#validatable}} +using System.ComponentModel.DataAnnotations; +{{/validatable}} +using FileParameter = {{packageName}}.Client.FileParameter; +using OpenAPIDateConverter = {{packageName}}.Client.OpenAPIDateConverter; +{{#useCompareNetObjects}} +using OpenAPIClientUtils = {{packageName}}.Client.ClientUtils; +{{/useCompareNetObjects}} +{{#models}} +{{#model}} +{{#oneOf}} +{{#-first}} +using System.Reflection; +{{/-first}} +{{/oneOf}} +{{#anyOf}} +{{#-first}} +using System.Reflection; +{{/-first}} +{{/anyOf}} + +namespace {{packageName}}.{{modelPackage}} +{ +{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{^isEnum}}{{#oneOf}}{{#-first}}{{>modelOneOf}}{{/-first}}{{/oneOf}}{{#anyOf}}{{#-first}}{{>modelAnyOf}}{{/-first}}{{/anyOf}}{{^oneOf}}{{^anyOf}}{{>modelGeneric}}{{/anyOf}}{{/oneOf}}{{/isEnum}} +{{/model}} +{{/models}} +} From c65d9188ebd753d91070c107ed1ac803d8291a93 Mon Sep 17 00:00:00 2001 From: Justin Larrabee Date: Wed, 2 Nov 2022 12:36:03 -0600 Subject: [PATCH 2/3] csharp-netcore client generator supports UnityWebRequest library --- .../languages/CSharpNetCoreClientCodegen.java | 9 + .../csharp-netcore/RequestOptions.mustache | 4 + .../resources/csharp-netcore/api.mustache | 16 + .../libraries/unity/ApiClient.mustache | 294 ++++++++---------- .../unity/ConnectionException.mustache | 21 ++ .../UnexpectedResponseException.mustache | 26 ++ .../libraries/unity/api.mustache | 4 - .../libraries/unity/model.mustache | 1 - 8 files changed, 203 insertions(+), 172 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ConnectionException.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/UnexpectedResponseException.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java index 90be1b0ed6a2..702c757b204a 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java @@ -100,6 +100,7 @@ public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen { protected boolean supportsRetry = Boolean.TRUE; protected boolean supportsAsync = Boolean.TRUE; + protected boolean supportsFileParameters = Boolean.TRUE; protected boolean netStandard = Boolean.FALSE; protected boolean validatable = Boolean.TRUE; @@ -805,8 +806,12 @@ public void processOpts() { } else if (UNITY.equals(getLibrary())) { setSupportsRetry(false); setSupportsAsync(true); + setSupportsFileParameters(false); addSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir, authPackageDir); + + supportingFiles.add(new SupportingFile("ConnectionException.mustache", clientPackageDir, "ConnectionException.cs")); + supportingFiles.add(new SupportingFile("UnexpectedResponseException.mustache", clientPackageDir, "UnexpectedResponseException.cs")); } else { //restsharp addSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir, authPackageDir); additionalProperties.put("apiDocPath", apiDocPath); @@ -1036,6 +1041,10 @@ public void setSupportsAsync(Boolean supportsAsync) { this.supportsAsync = supportsAsync; } + public void setSupportsFileParameters(Boolean supportsFileParameters) { + this.supportsFileParameters = supportsFileParameters; + } + public void setSupportsRetry(Boolean supportsRetry) { this.supportsRetry = supportsRetry; } diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/RequestOptions.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/RequestOptions.mustache index 40436b965771..2c39f79c8dbe 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/RequestOptions.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/RequestOptions.mustache @@ -35,10 +35,12 @@ namespace {{packageName}}.Client /// public Dictionary FormParameters { get; set; } + {{#supportsFileParameters}} /// /// File parameters to be sent along with the request. /// public Multimap FileParameters { get; set; } + {{/supportsFileParameters}} /// /// Cookies to be sent along with the request. @@ -76,7 +78,9 @@ namespace {{packageName}}.Client QueryParameters = new Multimap(); HeaderParameters = new Multimap(); FormParameters = new Dictionary(); + {{#supportsFileParameters}} FileParameters = new Multimap(); + {{/supportsFileParameters}} Cookies = new List(); } } diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/api.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/api.mustache index 99d24d2aaa04..1a0c8143f4fb 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/api.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/api.mustache @@ -361,13 +361,17 @@ namespace {{packageName}}.{{apiPackage}} {{#required}} {{#isFile}} {{#isArray}} + {{#supportsFileParameters}} foreach (var file in {{paramName}}) { localVarRequestOptions.FileParameters.Add("{{baseName}}", file); } + {{/supportsFileParameters}} {{/isArray}} {{^isArray}} + {{#supportsFileParameters}} localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); + {{/supportsFileParameters}} {{/isArray}} {{/isFile}} {{^isFile}} @@ -379,13 +383,17 @@ namespace {{packageName}}.{{apiPackage}} { {{#isFile}} {{#isArray}} + {{#supportsFileParameters}} foreach (var file in {{paramName}}) { localVarRequestOptions.FileParameters.Add("{{baseName}}", file); } + {{/supportsFileParameters}} {{/isArray}} {{^isArray}} + {{#supportsFileParameters}} localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); + {{/supportsFileParameters}} {{/isArray}} {{/isFile}} {{^isFile}} @@ -602,13 +610,17 @@ namespace {{packageName}}.{{apiPackage}} {{#required}} {{#isFile}} {{#isArray}} + {{#supportsFileParameters}} foreach (var file in {{paramName}}) { localVarRequestOptions.FileParameters.Add("{{baseName}}", file); } + {{/supportsFileParameters}} {{/isArray}} {{^isArray}} + {{#supportsFileParameters}} localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); + {{/supportsFileParameters}} {{/isArray}} {{/isFile}} {{^isFile}} @@ -620,13 +632,17 @@ namespace {{packageName}}.{{apiPackage}} { {{#isFile}} {{#isArray}} + {{#supportsFileParameters}} foreach (var file in {{paramName}}) { localVarRequestOptions.FileParameters.Add("{{baseName}}", file); } + {{/supportsFileParameters}} {{/isArray}} {{^isArray}} + {{#supportsFileParameters}} localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); + {{/supportsFileParameters}} {{/isArray}} {{/isFile}} {{^isFile}} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache index c9a6af52be1f..cb2988c1344a 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache @@ -14,9 +14,6 @@ using System.Text; using System.Threading; using System.Text.RegularExpressions; using System.Threading.Tasks; -{{^netStandard}} -using System.Web; -{{/netStandard}} using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; @@ -24,9 +21,6 @@ using System.Net.Http; using System.Net.Http.Headers; using UnityEngine.Networking; using UnityEngine; -{{#supportsRetry}} -using Polly; -{{/supportsRetry}} namespace {{packageName}}.Client { @@ -81,20 +75,18 @@ namespace {{packageName}}.Client public T Deserialize(UnityWebRequest request) { - var result = (T) Deserialize(response, typeof(T)); + var result = (T) Deserialize(request, typeof(T)); return result; } /// /// Deserialize the JSON string into a proper object. /// - /// The HTTP response. + /// The UnityWebRequest after it has a response. /// Object type. /// Object representation of the JSON string. internal object Deserialize(UnityWebRequest request, Type type) { - IList headers = response.Headers.Select(x => x.Key + "=" + x.Value).ToList(); - if (type == typeof(byte[])) // return byte array { return request.downloadHandler.data; @@ -103,7 +95,6 @@ namespace {{packageName}}.Client // TODO: ? if (type.IsAssignableFrom(typeof(Stream))) if (type == typeof(Stream)) { - var bytes = await response.Content.ReadAsByteArrayAsync(); // NOTE: Ignoring Content-Disposition filename support, since not all platforms // have a location on disk to write arbitrary data (tvOS, consoles). return new MemoryStream(request.downloadHandler.data); @@ -119,15 +110,36 @@ namespace {{packageName}}.Client return Convert.ChangeType(request.downloadHandler.text, type); } - // at this point, it must be a model (json) - try + var contentType = request.GetResponseHeader("Content-Type"); + + if (!string.IsNullOrEmpty(contentType) && contentType.Contains("application/json")) { - return JsonConvert.DeserializeObject(request.downloadHandler.text, type, _serializerSettings); + var text = request.downloadHandler?.text; + + // Generated APIs that don't expect a return value provide System.Object as the type + if (type == typeof(System.Object) && (string.IsNullOrEmpty(text) || text.Trim() == "null")) + { + return null; + } + + // Deserialize as a model + try + { + return JsonConvert.DeserializeObject(text, type, _serializerSettings); + } + catch (Exception e) + { + throw new UnexpectedResponseException(request, type, e.ToString()); + } } - catch (Exception e) + + if (type != typeof(System.Object) && request.responseCode >= 200 && request.responseCode < 300) { - throw new ApiException(500, e.Message); + throw new UnexpectedResponseException(request, type); } + + return null; + } public string RootElement { get; set; } @@ -200,18 +212,18 @@ namespace {{packageName}}.Client } /// - /// Provides all logic for constructing a new HttpRequestMessage. + /// Provides all logic for constructing a new UnityWebRequest. /// At this point, all information for querying the service is known. Here, it is simply - /// mapped into the a HttpRequestMessage. + /// mapped into the UnityWebRequest. /// /// The http verb. /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. - /// [private] A new HttpRequestMessage instance. + /// [private] A new UnityWebRequest instance. /// - private UnityWebRequest NewRequest( + private UnityWebRequest NewRequest( string method, string path, RequestOptions options, @@ -234,8 +246,9 @@ namespace {{packageName}}.Client contentType = contentTypes.FirstOrDefault(); } - UnityWebRequest request = new UnityWebRequest(builder.GetFullUri(), method); - var setContentTypeHeader = false; + var uri = builder.GetFullUri(); + UploadHandler uploadHandler = null; + UnityWebRequest request = null; if (contentType == "multipart/form-data") { @@ -245,68 +258,51 @@ namespace {{packageName}}.Client formData.Add(new MultipartFormDataSection(formParameter.Key, formParameter.Value)); } - var boundary = UnityWebRequest.GenerateBoundary(); - var data = UnityWebRequest.SerializeFormSections(formData, boundary); - request.uploadHandler = new UploadHandlerRaw(data); - request.uploadHandler.contentType = "multipart/form-data; boundary=" + System.Text.Encoding.UTF8.GetString(boundary, 0, boundary.Length); - - setContentTypeHeader = true; + request = UnityWebRequest.Post(uri, formData); + request.method = method; } - /* else if (contentType == "application/x-www-form-urlencoded") { - request.Content = new FormUrlEncodedContent(options.FormParameters); - } - else - { - if (options.Data != null) + var form = new WWWForm(); + foreach (var kvp in options.FormParameters) { - var serializer = new CustomJsonCodec(SerializerSettings, configuration); - request.Content = new StringContent(serializer.Serialize(options.Data), new UTF8Encoding(), - "application/json"); + form.AddField(kvp.Key, kvp.Value); } - } - if (!string.IsNullOrEmpty(contentType) && !setContentTypeHeader) + request = UnityWebRequest.Post(uri, form); + request.method = method; + } + else if (options.Data != null) { - request.SetRequestHeader("Content-Type", contentType); + var serializer = new CustomJsonCodec(SerializerSettings, configuration); + var jsonData = serializer.Serialize(options.Data); + + // Making a post body application/json encoded is whack with UnityWebRequest. + // See: https://stackoverflow.com/questions/68156230/unitywebrequest-post-not-sending-body + request = UnityWebRequest.Put(uri, jsonData); + request.method = method; + request.SetRequestHeader("Content-Type", "application/json"); + } + else + { + request = new UnityWebRequest(builder.GetFullUri(), method); } - - - switch (method) { - case "GET": - { - request = UnityWebRequest.Get(uri); - break; - } - - case "POST": - { - if (contentType == "application/json") { - // Making a post body application/json encoded is whack with UnityWebRequest. - // See: https://stackoverflow.com/questions/68156230/unitywebrequest-post-not-sending-body - request = UnityWebRequest.Put(uri, serializedJsonData); - request.method = "POST"; - } - else - { - request = UnityWeb - } - break; - } + if (request.downloadHandler == null && typeof(T) != typeof(System.Object)) + { + request.downloadHandler = new DownloadHandlerBuffer(); } if (configuration.UserAgent != null) { - request.Headers.TryAddWithoutValidation("User-Agent", configuration.UserAgent); + request.SetRequestHeader("User-Agent", configuration.UserAgent); } if (configuration.DefaultHeaders != null) { foreach (var headerParam in configuration.DefaultHeaders) { - request.Headers.Add(headerParam.Key, headerParam.Value); + request.SetRequestHeader(headerParam.Key, headerParam.Value); } } @@ -317,85 +313,66 @@ namespace {{packageName}}.Client foreach (var value in headerParam.Value) { // Todo make content headers actually content headers - request.Headers.TryAddWithoutValidation(headerParam.Key, value); + request.SetRequestHeader(headerParam.Key, value); } } } - // TODO provide an alternative that allows cookies per request instead of per API client if (options.Cookies != null && options.Cookies.Count > 0) { - request.Properties["CookieContainer"] = options.Cookies; + #if UNITY_WEBGL + throw new System.InvalidOperationException("UnityWebRequest does not support setting cookies in WebGL"); + #else + if (options.Cookies.Count != 1) + { + UnityEngine.Debug.LogError("Only one cookie supported, ignoring others"); + } + + request.SetRequestHeader("Cookie", options.Cookies[0].ToString()); + #endif } - */ return request; + } - partial void InterceptRequest(HttpRequestMessage req); - partial void InterceptResponse(HttpRequestMessage req, HttpResponseMessage response); + partial void InterceptRequest(UnityWebRequest req); + partial void InterceptResponse(UnityWebRequest req); - /* - private async Task> ToApiResponse(HttpResponseMessage response, object responseData, Uri uri) + private ApiResponse ToApiResponse(UnityWebRequest request, object responseData) { T result = (T) responseData; - string rawContent = await response.Content.ReadAsStringAsync(); - var transformed = new ApiResponse(response.StatusCode, new Multimap({{#caseInsensitiveResponseHeaders}}StringComparer.OrdinalIgnoreCase{{/caseInsensitiveResponseHeaders}}), result, rawContent) + var transformed = new ApiResponse((HttpStatusCode)request.responseCode, new Multimap({{#caseInsensitiveResponseHeaders}}StringComparer.OrdinalIgnoreCase{{/caseInsensitiveResponseHeaders}}), result, request.downloadHandler?.text ?? "") { - ErrorText = response.ReasonPhrase, + ErrorText = request.error, Cookies = new List() }; // process response headers, e.g. Access-Control-Allow-Methods - if (response.Headers != null) - { - foreach (var responseHeader in response.Headers) - { - transformed.Headers.Add(responseHeader.Key, ClientUtils.ParameterToString(responseHeader.Value)); - } - } - - // process response content headers, e.g. Content-Type - if (response.Content.Headers != null) + var responseHeaders = request.GetResponseHeaders(); + if (responseHeaders != null) { - foreach (var responseHeader in response.Content.Headers) + foreach (var responseHeader in request.GetResponseHeaders()) { transformed.Headers.Add(responseHeader.Key, ClientUtils.ParameterToString(responseHeader.Value)); } } - if (_httpClientHandler != null && response != null) - { - try { - foreach (Cookie cookie in _httpClientHandler.CookieContainer.GetCookies(uri)) - { - transformed.Cookies.Add(cookie); - } - } - catch (PlatformNotSupportedException) {} - } - return transformed; } - */ - - private ApiResponse Exec(HttpRequestMessage req, IReadableConfiguration configuration) - { - return ExecAsync(req, configuration).GetAwaiter().GetResult(); - } - private async Task> ExecAsync(HttpRequestMessage req, + private async Task> ExecAsync( + UnityWebRequest request, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { - await Task.Delay(1000); - /* CancellationTokenSource timeoutTokenSource = null; CancellationTokenSource finalTokenSource = null; var deserializer = new CustomJsonCodec(SerializerSettings, configuration); var finalToken = cancellationToken; + using (request) try { if (configuration.Timeout > 0) @@ -407,72 +384,63 @@ namespace {{packageName}}.Client if (configuration.Proxy != null) { - if(_httpClientHandler == null) throw new InvalidOperationException("Configuration `Proxy` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor."); - _httpClientHandler.Proxy = configuration.Proxy; + throw new InvalidOperationException("Configuration `Proxy` not supported by UnityWebRequest"); } if (configuration.ClientCertificates != null) { - if(_httpClientHandler == null) throw new InvalidOperationException("Configuration `ClientCertificates` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor."); - _httpClientHandler.ClientCertificates.AddRange(configuration.ClientCertificates); + // Only Android/iOS/tvOS/Standalone players can support certificates, and this + // implementation is intended to work on all platforms. + // + // TODO: Could optionally allow support for this on these platforms. + // + // See: https://docs.unity3d.com/ScriptReference/Networking.CertificateHandler.html + throw new InvalidOperationException("Configuration `ClientCertificates` not supported by UnityWebRequest on all platforms"); } - var cookieContainer = req.Properties.ContainsKey("CookieContainer") ? req.Properties["CookieContainer"] as List : null; + InterceptRequest(request); - if (cookieContainer != null) + var asyncOp = request.SendWebRequest(); + + TaskCompletionSource tsc = new TaskCompletionSource(); + asyncOp.completed += (_) => tsc.TrySetResult(request.result); + + if (asyncOp.isDone) { - if(_httpClientHandler == null) throw new InvalidOperationException("Request property `CookieContainer` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor."); - foreach (var cookie in cookieContainer) - { - _httpClientHandler.CookieContainer.Add(cookie); - } + tsc.TrySetResult(asyncOp.webRequest.result); } - InterceptRequest(req); - - HttpResponseMessage response; - {{#supportsRetry}} - if (RetryConfiguration.AsyncRetryPolicy != null) + try { - var policy = RetryConfiguration.AsyncRetryPolicy; - var policyResult = await policy - .ExecuteAndCaptureAsync(() => _httpClient.SendAsync(req, finalToken)) - .ConfigureAwait(false); - response = (policyResult.Outcome == OutcomeType.Successful) ? - policyResult.Result : new HttpResponseMessage() - { - ReasonPhrase = policyResult.FinalException.ToString(), - RequestMessage = req - }; + await Task.WhenAny(tsc.Task, Task.Delay(Timeout.Infinite, finalToken)).ConfigureAwait(false); } - else + catch (OperationCanceledException) { - {{/supportsRetry}} - response = await _httpClient.SendAsync(req, finalToken).ConfigureAwait(false); - {{#supportsRetry}} + request.Abort(); + throw; } - {{/supportsRetry}} - if (!response.IsSuccessStatusCode) + if (request.result == UnityWebRequest.Result.ConnectionError || + request.result == UnityWebRequest.Result.DataProcessingError) { - return await ToApiResponse(response, default(T), req.RequestUri); + throw new ConnectionException(request); } - object responseData = await deserializer.Deserialize(response); + object responseData = deserializer.Deserialize(request); // if the response type is oneOf/anyOf, call FromJSON to deserialize the data if (typeof({{{packageName}}}.{{modelPackage}}.AbstractOpenAPISchema).IsAssignableFrom(typeof(T))) { - responseData = (T) typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content }); + responseData = (T) typeof(T).GetMethod("FromJson").Invoke(null, new object[] { new ByteArrayContent(request.downloadHandler.data) }); } else if (typeof(T).Name == "Stream") // for binary response { - responseData = (T) (object) await response.Content.ReadAsStreamAsync(); + responseData = (T) (object) new MemoryStream(request.downloadHandler.data); } - InterceptResponse(req, response); + InterceptResponse(request); - return await ToApiResponse(response, responseData, req.RequestUri); + return ToApiResponse(request, responseData); } finally { @@ -486,7 +454,6 @@ namespace {{packageName}}.Client finalTokenSource.Dispose(); } } - */ } {{#supportsAsync}} @@ -503,7 +470,7 @@ namespace {{packageName}}.Client public Task> GetAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; - return ExecAsync(NewRequest("GET", path, options, config), config, cancellationToken); + return ExecAsync(NewRequest("GET", path, options, config), config, cancellationToken); } /// @@ -518,7 +485,7 @@ namespace {{packageName}}.Client public Task> PostAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; - return ExecAsync(NewRequest("POST", path, options, config), config, cancellationToken); + return ExecAsync(NewRequest("POST", path, options, config), config, cancellationToken); } /// @@ -533,7 +500,7 @@ namespace {{packageName}}.Client public Task> PutAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; - return ExecAsync(NewRequest("PUT", path, options, config), config, cancellationToken); + return ExecAsync(NewRequest("PUT", path, options, config), config, cancellationToken); } /// @@ -548,7 +515,7 @@ namespace {{packageName}}.Client public Task> DeleteAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; - return ExecAsync(NewRequest("DELETE", path, options, config), config, cancellationToken); + return ExecAsync(NewRequest("DELETE", path, options, config), config, cancellationToken); } /// @@ -563,7 +530,7 @@ namespace {{packageName}}.Client public Task> HeadAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; - return ExecAsync(NewRequest("HEAD", path, options, config), config, cancellationToken); + return ExecAsync(NewRequest("HEAD", path, options, config), config, cancellationToken); } /// @@ -578,7 +545,7 @@ namespace {{packageName}}.Client public Task> OptionsAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; - return ExecAsync(NewRequest("OPTIONS", path, options, config), config, cancellationToken); + return ExecAsync(NewRequest("OPTIONS", path, options, config), config, cancellationToken); } /// @@ -593,7 +560,7 @@ namespace {{packageName}}.Client public Task> PatchAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; - return ExecAsync(NewRequest("PATCH", path, options, config), config, cancellationToken); + return ExecAsync(NewRequest("PATCH", path, options, config), config, cancellationToken); } #endregion IAsynchronousClient {{/supportsAsync}} @@ -609,8 +576,7 @@ namespace {{packageName}}.Client /// A Task containing ApiResponse public ApiResponse Get(string path, RequestOptions options, IReadableConfiguration configuration = null) { - var config = configuration ?? GlobalConfiguration.Instance; - return Exec(NewRequest("GET", path, options, config), config); + throw new System.NotImplementedException("UnityWebRequest does not support synchronous operation"); } /// @@ -623,8 +589,7 @@ namespace {{packageName}}.Client /// A Task containing ApiResponse public ApiResponse Post(string path, RequestOptions options, IReadableConfiguration configuration = null) { - var config = configuration ?? GlobalConfiguration.Instance; - return Exec(NewRequest("POST", path, options, config), config); + throw new System.NotImplementedException("UnityWebRequest does not support synchronous operation"); } /// @@ -637,8 +602,7 @@ namespace {{packageName}}.Client /// A Task containing ApiResponse public ApiResponse Put(string path, RequestOptions options, IReadableConfiguration configuration = null) { - var config = configuration ?? GlobalConfiguration.Instance; - return Exec(NewRequest("PUT", path, options, config), config); + throw new System.NotImplementedException("UnityWebRequest does not support synchronous operation"); } /// @@ -651,8 +615,7 @@ namespace {{packageName}}.Client /// A Task containing ApiResponse public ApiResponse Delete(string path, RequestOptions options, IReadableConfiguration configuration = null) { - var config = configuration ?? GlobalConfiguration.Instance; - return Exec(NewRequest("DELETE", path, options, config), config); + throw new System.NotImplementedException("UnityWebRequest does not support synchronous operation"); } /// @@ -665,8 +628,7 @@ namespace {{packageName}}.Client /// A Task containing ApiResponse public ApiResponse Head(string path, RequestOptions options, IReadableConfiguration configuration = null) { - var config = configuration ?? GlobalConfiguration.Instance; - return Exec(NewRequest("HEAD", path, options, config), config); + throw new System.NotImplementedException("UnityWebRequest does not support synchronous operation"); } /// @@ -679,8 +641,7 @@ namespace {{packageName}}.Client /// A Task containing ApiResponse public ApiResponse Options(string path, RequestOptions options, IReadableConfiguration configuration = null) { - var config = configuration ?? GlobalConfiguration.Instance; - return Exec(NewRequest("OPTIONS", path, options, config), config); + throw new System.NotImplementedException("UnityWebRequest does not support synchronous operation"); } /// @@ -693,8 +654,7 @@ namespace {{packageName}}.Client /// A Task containing ApiResponse public ApiResponse Patch(string path, RequestOptions options, IReadableConfiguration configuration = null) { - var config = configuration ?? GlobalConfiguration.Instance; - return Exec(NewRequest("PATCH", path, options, config), config); + throw new System.NotImplementedException("UnityWebRequest does not support synchronous operation"); } #endregion ISynchronousClient } diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ConnectionException.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ConnectionException.mustache new file mode 100644 index 000000000000..108ea3bf567b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ConnectionException.mustache @@ -0,0 +1,21 @@ +{{>partial_header}} + +using System; +using UnityEngine.Networking; + +namespace {{packageName}}.Client +{ + public class ConnectionException : Exception + { + public UnityWebRequest.Result Result { get; private set; } + public string Error { get; private set; } + + // NOTE: Cannot keep reference to the request since it will be disposed. + public ConnectionException(UnityWebRequest request) + : base($"result={request.result} error={request.error}") + { + Result = request.result; + Error = request.error ?? ""; + } + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/UnexpectedResponseException.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/UnexpectedResponseException.mustache new file mode 100644 index 000000000000..a976b2a76be8 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/UnexpectedResponseException.mustache @@ -0,0 +1,26 @@ +{{>partial_header}} + +using System; +using UnityEngine.Networking; + +namespace {{packageName}}.Client +{ + // Thrown when a backend doesn't return an expected response based on the expected type + // of the response data. + public class UnexpectedResponseException : Exception + { + public int ErrorCode { get; private set; } + + // NOTE: Cannot keep reference to the request since it will be disposed. + public UnexpectedResponseException(UnityWebRequest request, System.Type type, string extra = "") + : base(CreateMessage(request, type, extra)) + { + ErrorCode = (int)request.responseCode; + } + + private static string CreateMessage(UnityWebRequest request, System.Type type, string extra) + { + return $"httpcode={request.responseCode}, expected {type.Name} but got data: {extra}"; + } + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/api.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/api.mustache index f1c3392d785c..91a37fbf99b6 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/api.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/api.mustache @@ -368,7 +368,6 @@ namespace {{packageName}}.{{apiPackage}} {{#formParams}} {{#required}} {{#isFile}} - localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); {{/isFile}} {{^isFile}} localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter @@ -378,7 +377,6 @@ namespace {{packageName}}.{{apiPackage}} if ({{paramName}} != null) { {{#isFile}} - localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); {{/isFile}} {{^isFile}} localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter @@ -567,7 +565,6 @@ namespace {{packageName}}.{{apiPackage}} {{#formParams}} {{#required}} {{#isFile}} - localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); {{/isFile}} {{^isFile}} localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter @@ -577,7 +574,6 @@ namespace {{packageName}}.{{apiPackage}} if ({{paramName}} != null) { {{#isFile}} - localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); {{/isFile}} {{^isFile}} localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/model.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/model.mustache index f84de7f64327..0d0c0b683541 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/model.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/model.mustache @@ -25,7 +25,6 @@ using JsonSubTypes; {{#validatable}} using System.ComponentModel.DataAnnotations; {{/validatable}} -using FileParameter = {{packageName}}.Client.FileParameter; using OpenAPIDateConverter = {{packageName}}.Client.OpenAPIDateConverter; {{#useCompareNetObjects}} using OpenAPIClientUtils = {{packageName}}.Client.ClientUtils; From df21cfc240bd916c2cbef4e6a27afa2354dd8728 Mon Sep 17 00:00:00 2001 From: Justin Larrabee Date: Wed, 2 Nov 2022 12:39:28 -0600 Subject: [PATCH 3/3] Remove ConfigureAwait(false) --- .../resources/csharp-netcore/libraries/unity/ApiClient.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache index cb2988c1344a..2b22d82abada 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/unity/ApiClient.mustache @@ -412,7 +412,7 @@ namespace {{packageName}}.Client try { - await Task.WhenAny(tsc.Task, Task.Delay(Timeout.Infinite, finalToken)).ConfigureAwait(false); + await Task.WhenAny(tsc.Task, Task.Delay(Timeout.Infinite, finalToken)); } catch (OperationCanceledException) {