diff --git a/.openapi-generator/VERSION b/.openapi-generator/VERSION index eb1dc6a..368fd8f 100644 --- a/.openapi-generator/VERSION +++ b/.openapi-generator/VERSION @@ -1 +1 @@ -7.13.0 +7.15.0 diff --git a/dev.md b/dev.md index 68d3c98..fd9e0cc 100644 --- a/dev.md +++ b/dev.md @@ -13,16 +13,23 @@ and use next command from the project root. ## Generator configuration Features -1. The generator does not generate long enums, the problem -is solved by adding the edited template modelEnum.mustache. -2. When generating oneOf schemas, the generator creates its +1. Two client generation modes have been added: +strict (for client testing) and lenient (for release). +In strict mode, the client will throw an exception if the +types do not match or the required fields are missing; +in lenient mode, error data will be output as a warning to +the console. The templates for generating these modes +are located in the generator-templates folder. +2. The generator does not generate long enums, the problem +is solved by adding the edited template modelEnum.mustache (for both modes). +3. When generating oneOf schemas, the generator creates its own abstract class, which does not look like it would like. The problem was solved by replacing the abstract generator class with ours using typeMappings in the generator config. -3. The generator sets the discriminator name incorrectly, the problem -is solved by adding and editing the modelGeneric.mustache template. -4. Changes have also been made to modelsGeneric.mustache to exclude -unspecified fields from JSON when serializing a class. +4. The generator sets the discriminator name incorrectly, the problem +is solved by adding and editing the modelGeneric.mustache template (for both modes). +5. Changes have also been made to modelsGeneric.mustache to exclude +unspecified fields from JSON when serializing a class (for both modes). ## Problem solving diff --git a/generator-templates/lenient/ApiClient.mustache b/generator-templates/lenient/ApiClient.mustache new file mode 100644 index 0000000..423e752 --- /dev/null +++ b/generator-templates/lenient/ApiClient.mustache @@ -0,0 +1,801 @@ +{{>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; +{{#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 async Task Deserialize(HttpResponseMessage response) + { + var result = (T) await Deserialize(response, typeof(T)).ConfigureAwait(false); + return result; + } + + /// + /// Deserialize the JSON string into a proper object. + /// + /// The HTTP response. + /// Object type. + /// Object representation of the JSON string. + internal async Task Deserialize(HttpResponseMessage response, Type type) + { + IList headers = new List(); + // process response headers, e.g. Access-Control-Allow-Methods + foreach (var responseHeader in response.Headers) + { + headers.Add(responseHeader.Key + "=" + ClientUtils.ParameterToString(responseHeader.Value)); + } + + // process response content headers, e.g. Content-Type + foreach (var responseHeader in response.Content.Headers) + { + headers.Add(responseHeader.Key + "=" + ClientUtils.ParameterToString(responseHeader.Value)); + } + + // RFC 2183 & RFC 2616 + var fileNameRegex = new Regex(@"Content-Disposition=.*filename=['""]?([^'""\s]+)['""]?$", RegexOptions.IgnoreCase); + if (type == typeof(byte[])) // return byte array + { + return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + } + else if (type == typeof(FileParameter)) + { + if (headers != null) { + foreach (var header in headers) + { + var match = fileNameRegex.Match(header.ToString()); + if (match.Success) + { + string fileName = ClientUtils.SanitizeFilename(match.Groups[1].Value.Replace("\"", "").Replace("'", "")); + return new FileParameter(fileName, await response.Content.ReadAsStreamAsync().ConfigureAwait(false)); + } + } + } + return new FileParameter(await response.Content.ReadAsStreamAsync().ConfigureAwait(false)); + } + + // TODO: ? if (type.IsAssignableFrom(typeof(Stream))) + if (type == typeof(Stream)) + { + var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + if (headers != null) + { + var filePath = string.IsNullOrEmpty(_configuration.TempFolderPath) + ? Path.GetTempPath() + : _configuration.TempFolderPath; + + foreach (var header in headers) + { + var match = fileNameRegex.Match(header.ToString()); + if (match.Success) + { + string fileName = filePath + ClientUtils.SanitizeFilename(match.Groups[1].Value.Replace("\"", "").Replace("'", "")); + File.WriteAllBytes(fileName, bytes); + return new FileStream(fileName, FileMode.Open); + } + } + } + var stream = new MemoryStream(bytes); + return stream; + } + + if (type.Name.StartsWith("System.Nullable`1[[System.DateTime")) // return a datetime object + { + return DateTime.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false), null, System.Globalization.DateTimeStyles.RoundtripKind); + } + + if (type == typeof(string) || type.Name.StartsWith("System.Nullable")) // return primitive type + { + return Convert.ChangeType(await response.Content.ReadAsStringAsync().ConfigureAwait(false), type); + } + + // at this point, it must be a model (json) + try + { + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync().ConfigureAwait(false), type, _serializerSettings); + } + catch (Exception e) + { + Console.WriteLine($"Warning: Failed to deserialize response to type {type.Name}: {e.Message}"); + return type.IsValueType ? Activator.CreateInstance(type) : null; + } + } + + 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; + + private readonly HttpClientHandler _httpClientHandler; + private readonly HttpClient _httpClient; + private readonly bool _disposeClient; + + /// + /// 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 + } + }, + Error = HandleDeserializationError + }; + + private static void HandleDeserializationError(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs args) + { + var currentError = args.ErrorContext.Error.GetBaseException(); + Console.WriteLine($"Warning: JSON Deserialization Error at path '{args.ErrorContext.Path}'. Member: '{args.ErrorContext.Member}'. Error: {currentError.Message}."); + args.ErrorContext.Handled = true; + } + + /// + /// 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"); + + _httpClientHandler = new HttpClientHandler(); + _httpClient = new HttpClient(_httpClientHandler, true); + _disposeClient = true; + _baseUrl = basePath; + } + + /// + /// Initializes a new instance of the , defaulting to the global configurations' base url. + /// + /// An instance of HttpClient. + /// An optional instance of HttpClientHandler that is used by HttpClient. + /// + /// + /// Some configuration settings will not be applied without passing an HttpClientHandler. + /// The features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. + /// + public ApiClient(HttpClient client, HttpClientHandler handler = null) : + this(client, {{packageName}}.Client.GlobalConfiguration.Instance.BasePath, handler) + { + } + + /// + /// Initializes a new instance of the . + /// + /// An instance of HttpClient. + /// The target service's base path in URL format. + /// An optional instance of HttpClientHandler that is used by HttpClient. + /// + /// + /// + /// Some configuration settings will not be applied without passing an HttpClientHandler. + /// The features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. + /// + public ApiClient(HttpClient client, string basePath, HttpClientHandler handler = null) + { + if (client == null) throw new ArgumentNullException("client cannot be null"); + if (string.IsNullOrEmpty(basePath)) throw new ArgumentException("basePath cannot be empty"); + + _httpClientHandler = handler; + _httpClient = client; + _baseUrl = basePath; + } + + /// + /// Disposes resources if they were created by us + /// + public void Dispose() + { + if(_disposeClient) { + _httpClient.Dispose(); + } + } + + /// Prepares multipart/form-data content + {{! TODO: Add handling of improper usage }} + HttpContent PrepareMultipartFormDataContent(RequestOptions options) + { + string boundary = "---------" + Guid.NewGuid().ToString().ToUpperInvariant(); + var multipartContent = new MultipartFormDataContent(boundary); + foreach (var formParameter in options.FormParameters) + { + multipartContent.Add(new StringContent(formParameter.Value), formParameter.Key); + } + + if (options.FileParameters != null && options.FileParameters.Count > 0) + { + foreach (var fileParam in options.FileParameters) + { + foreach (var file in fileParam.Value) + { + var content = new StreamContent(file.Content); + content.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType); + multipartContent.Add(content, fileParam.Key, file.Name); + } + } + } + return multipartContent; + } + + /// + /// 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 HttpRequestMessage NewRequest( + HttpMethod 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); + + HttpRequestMessage request = new HttpRequestMessage(method, builder.GetFullUri()); + + 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); + } + } + } + + List> contentList = new List>(); + + string contentType = null; + if (options.HeaderParameters != null && options.HeaderParameters.ContainsKey("Content-Type")) + { + var contentTypes = options.HeaderParameters["Content-Type"]; + contentType = contentTypes.FirstOrDefault(); + } + + {{!// TODO Add error handling in case of improper usage}} + if (contentType == "multipart/form-data") + { + request.Content = PrepareMultipartFormDataContent(options); + } + else if (contentType == "application/x-www-form-urlencoded") + { + request.Content = new FormUrlEncodedContent(options.FormParameters); + } + else + { + if (options.Data != null) + { + if (options.Data is FileParameter fp) + { + contentType = contentType ?? "application/octet-stream"; + + var streamContent = new StreamContent(fp.Content); + streamContent.Headers.ContentType = new MediaTypeHeaderValue(contentType); + request.Content = streamContent; + } + else + { + var serializer = new CustomJsonCodec(SerializerSettings, configuration); + request.Content = new StringContent(serializer.Serialize(options.Data), new UTF8Encoding(), + "application/json"); + } + } + } + + + + // 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; + } + + {{#useVirtualForHooks}}public virtual{{/useVirtualForHooks}}{{^useVirtualForHooks}}partial{{/useVirtualForHooks}} void InterceptRequest(HttpRequestMessage req){{#useVirtualForHooks}} { }{{/useVirtualForHooks}}{{^useVirtualForHooks}};{{/useVirtualForHooks}} + {{#useVirtualForHooks}}public virtual{{/useVirtualForHooks}}{{^useVirtualForHooks}}partial{{/useVirtualForHooks}} void InterceptResponse(HttpRequestMessage req, HttpResponseMessage response){{#useVirtualForHooks}} { }{{/useVirtualForHooks}}{{^useVirtualForHooks}};{{/useVirtualForHooks}} + + private async Task> ToApiResponse(HttpResponseMessage response, object responseData, Uri uri) + { + T result = (T) responseData; + string rawContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + 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) + { + CancellationTokenSource timeoutTokenSource = null; + CancellationTokenSource finalTokenSource = null; + var deserializer = new CustomJsonCodec(SerializerSettings, configuration); + var finalToken = cancellationToken; + + try + { + if (configuration.Timeout > TimeSpan.Zero) + { + 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, req.RequestUri).ConfigureAwait(false); + } + + object responseData = await deserializer.Deserialize(response).ConfigureAwait(false); + + // 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().ConfigureAwait(false); + } + + InterceptResponse(req, response); + + return await ToApiResponse(response, responseData, req.RequestUri).ConfigureAwait(false); + } + catch (OperationCanceledException original) + { + if (timeoutTokenSource != null && timeoutTokenSource.IsCancellationRequested) + { + throw new TaskCanceledException($"[{req.Method}] {req.RequestUri} was timeout.", + new TimeoutException(original.Message, original)); + } + throw; + } + 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) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest(HttpMethod.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) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest(HttpMethod.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) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest(HttpMethod.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) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest(HttpMethod.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) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest(HttpMethod.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) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest(HttpMethod.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) + { + var config = configuration ?? GlobalConfiguration.Instance; + return ExecAsync(NewRequest(new HttpMethod("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(HttpMethod.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(HttpMethod.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(HttpMethod.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(HttpMethod.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(HttpMethod.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(HttpMethod.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(new HttpMethod("PATCH"), path, options, config), config); + } + #endregion ISynchronousClient + } +} \ No newline at end of file diff --git a/generator-templates/modelEnum.mustache b/generator-templates/lenient/modelEnum.mustache similarity index 92% rename from generator-templates/modelEnum.mustache rename to generator-templates/lenient/modelEnum.mustache index 12744d8..9c8ff95 100644 --- a/generator-templates/modelEnum.mustache +++ b/generator-templates/lenient/modelEnum.mustache @@ -141,7 +141,7 @@ /// public override void Write(Utf8JsonWriter writer, {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} {{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}, JsonSerializerOptions options) { - writer.WriteStringValue({{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}.ToString()); + writer.WriteStringValue({{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}).ToString()); } } @@ -172,14 +172,14 @@ } /// - /// Writes the DateTime to the json writer + /// Writes the {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} to the json writer /// /// /// /// public override void Write(Utf8JsonWriter writer, {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? {{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}, JsonSerializerOptions options) { - writer.WriteStringValue({{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}?.ToString() ?? "null"); + writer.WriteStringValue({{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}.HasValue ? {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}.Value).ToString() : "null"); } } {{/useGenericHost}} diff --git a/generator-templates/modelGeneric.mustache b/generator-templates/lenient/modelGeneric.mustache similarity index 99% rename from generator-templates/modelGeneric.mustache rename to generator-templates/lenient/modelGeneric.mustache index 7ff12a4..14e93a9 100644 --- a/generator-templates/modelGeneric.mustache +++ b/generator-templates/lenient/modelGeneric.mustache @@ -25,12 +25,14 @@ {{#items}} {{^complexType}} {{>modelInnerEnum}} + {{/complexType}} {{/items}} {{/items.isEnum}} {{#isEnum}} {{^complexType}} {{>modelInnerEnum}} + {{/complexType}} {{/isEnum}} {{#isEnum}} @@ -428,5 +430,6 @@ {{#validatable}} {{>validatable}} + {{/validatable}} } diff --git a/generator-templates/strict/modelEnum.mustache b/generator-templates/strict/modelEnum.mustache new file mode 100644 index 0000000..9c8ff95 --- /dev/null +++ b/generator-templates/strict/modelEnum.mustache @@ -0,0 +1,185 @@ + /// + /// {{description}}{{^description}}Defines {{{name}}}{{/description}} + /// + {{#description}} + /// {{.}} + {{/description}} + {{#vendorExtensions.x-cls-compliant}} + [CLSCompliant({{{.}}})] + {{/vendorExtensions.x-cls-compliant}} + {{#vendorExtensions.x-com-visible}} + [ComVisible({{{.}}})] + {{/vendorExtensions.x-com-visible}} + {{#allowableValues}} + {{#enumVars}} + {{#-first}} + {{#isString}} + {{^useGenericHost}} + [JsonConverter(typeof(StringEnumConverter))] + {{/useGenericHost}} + {{/isString}} + {{/-first}} + {{/enumVars}} + {{/allowableValues}} + {{>visibility}} enum {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{#isLong}}: long{{/isLong}}{{/datatypeWithEnum}}{{#vendorExtensions.x-enum-byte}}: byte{{/vendorExtensions.x-enum-byte}} + { + {{#allowableValues}} + {{#enumVars}} + /// + /// Enum {{name}} for value: {{value}} + /// + {{#isString}} + {{^useGenericHost}} + {{! EnumMember not currently supported in System.Text.Json, use a converter instead }} + [EnumMember(Value = "{{{value}}}")] + {{/useGenericHost}} + {{/isString}} + {{name}}{{^isString}} = {{{value}}}{{/isString}}{{#isString}}{{^vendorExtensions.x-zero-based-enum}} = {{-index}}{{/vendorExtensions.x-zero-based-enum}}{{/isString}}{{^-last}},{{/-last}} + {{^-last}} + + {{/-last}} + {{/enumVars}} + {{/allowableValues}} + } + {{#useGenericHost}} + + /// + /// Converts to and from the JSON value + /// + public static class {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}ValueConverter + { + /// + /// Parses a given value to + /// + /// + /// + public static {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} FromString(string value) + { + {{#allowableValues}} + {{#enumVars}} + if (value.Equals({{^isString}}({{{value}}}).ToString(){{/isString}}{{#isString}}"{{{value}}}"{{/isString}})) + return {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.{{name}}; + + {{/enumVars}} + {{/allowableValues}} + throw new NotImplementedException($"Could not convert value to type {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}: '{value}'"); + } + + /// + /// Parses a given value to + /// + /// + /// + public static {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? FromStringOrDefault(string value) + { + {{#allowableValues}} + {{#enumVars}} + if (value.Equals({{^isString}}({{{value}}}).ToString(){{/isString}}{{#isString}}"{{{value}}}"{{/isString}})) + return {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.{{name}}; + + {{/enumVars}} + {{/allowableValues}} + return null; + } + + /// + /// Converts the to the json value + /// + /// + /// + /// + public static {{>EnumValueDataType}} ToJsonValue({{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} value) + { + {{^isString}} + return ({{>EnumValueDataType}}) value; + {{/isString}} + {{#isString}} + {{#allowableValues}} + {{#enumVars}} + if (value == {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.{{name}}) + return {{^isNumeric}}"{{/isNumeric}}{{{value}}}{{^isNumeric}}"{{/isNumeric}}; + + {{/enumVars}} + {{/allowableValues}} + throw new NotImplementedException($"Value could not be handled: '{value}'"); + {{/isString}} + } + } + + /// + /// A Json converter for type + /// + /// + public class {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}JsonConverter : JsonConverter<{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}> + { + /// + /// Returns a {{datatypeWithEnum}} from the Json object + /// + /// + /// + /// + /// + public override {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string{{nrt?}} rawValue = reader.GetString(); + + {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? result = rawValue == null + ? null + : {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}ValueConverter.FromStringOrDefault(rawValue); + + if (result != null) + return result.Value; + + throw new JsonException(); + } + + /// + /// Writes the {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} to the json writer + /// + /// + /// + /// + public override void Write(Utf8JsonWriter writer, {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} {{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}, JsonSerializerOptions options) + { + writer.WriteStringValue({{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}).ToString()); + } + } + + /// + /// A Json converter for type + /// + public class {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}NullableJsonConverter : JsonConverter<{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}?> + { + /// + /// Returns a {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} from the Json object + /// + /// + /// + /// + /// + public override {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string{{nrt?}} rawValue = reader.GetString(); + + {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? result = rawValue == null + ? null + : {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}ValueConverter.FromStringOrDefault(rawValue); + + if (result != null) + return result.Value; + + throw new JsonException(); + } + + /// + /// Writes the {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} to the json writer + /// + /// + /// + /// + public override void Write(Utf8JsonWriter writer, {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}? {{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}, JsonSerializerOptions options) + { + writer.WriteStringValue({{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}.HasValue ? {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}{{/lambda.camelcase_sanitize_param}}.Value).ToString() : "null"); + } + } + {{/useGenericHost}} diff --git a/generator-templates/strict/modelGeneric.mustache b/generator-templates/strict/modelGeneric.mustache new file mode 100644 index 0000000..14e93a9 --- /dev/null +++ b/generator-templates/strict/modelGeneric.mustache @@ -0,0 +1,435 @@ + /// + /// {{description}}{{^description}}{{classname}}{{/description}} + /// + {{#vendorExtensions.x-cls-compliant}} + [CLSCompliant({{{vendorExtensions.x-cls-compliant}}})] + {{/vendorExtensions.x-cls-compliant}} + {{#vendorExtensions.x-com-visible}} + [ComVisible({{{vendorExtensions.x-com-visible}}})] + {{/vendorExtensions.x-com-visible}} + [DataContract(Name = "{{{name}}}")] + {{^useUnityWebRequest}} + {{#discriminator}} + {{#mappedModels.size}} + [JsonConverter(typeof(JsonSubtypes), "{{{discriminator.propertyBaseName}}}")] + {{/mappedModels.size}} + {{#mappedModels}} + [JsonSubtypes.KnownSubType(typeof({{{modelName}}}), "{{^vendorExtensions.x-discriminator-value}}{{{mappingName}}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{.}}}{{/vendorExtensions.x-discriminator-value}}")] + {{/mappedModels}} + {{/discriminator}} + {{/useUnityWebRequest}} + {{>visibility}} partial class {{classname}}{{#lambda.firstDot}}{{#parent}} : .{{/parent}}{{#validatable}} : .{{/validatable}}{{#equatable}} : .{{/equatable}}{{/lambda.firstDot}}{{#lambda.joinWithComma}}{{#parent}}{{{.}}} {{/parent}}{{#equatable}}IEquatable<{{classname}}> {{/equatable}}{{#validatable}}IValidatableObject {{/validatable}}{{/lambda.joinWithComma}} + { + {{#vars}} + {{#items.isEnum}} + {{#items}} + {{^complexType}} +{{>modelInnerEnum}} + + {{/complexType}} + {{/items}} + {{/items.isEnum}} + {{#isEnum}} + {{^complexType}} +{{>modelInnerEnum}} + + {{/complexType}} + {{/isEnum}} + {{#isEnum}} + + /// + /// {{description}}{{^description}}Gets or Sets {{{name}}}{{/description}} + /// + {{#description}} + /// {{.}} + {{/description}} + {{#example}} + /* + {{.}} + */ + {{/example}} + {{^conditionalSerialization}} + [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] + {{#deprecated}} + [Obsolete] + {{/deprecated}} + public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} {{name}} { get; set; } + {{#isReadOnly}} + + /// + /// Returns false as {{name}} should not be serialized given that it's read-only. + /// + /// false (boolean) + public bool ShouldSerialize{{name}}() + { + return false; + } + {{/isReadOnly}} + {{/conditionalSerialization}} + {{#conditionalSerialization}} + {{#isReadOnly}} + [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] + {{#deprecated}} + [Obsolete] + {{/deprecated}} + public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} {{name}} { get; set; } + + + /// + /// Returns false as {{name}} should not be serialized given that it's read-only. + /// + /// false (boolean) + public bool ShouldSerialize{{name}}() + { + return false; + } + {{/isReadOnly}} + + {{^isReadOnly}} + [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] + {{#deprecated}} + [Obsolete] + {{/deprecated}} + public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} {{name}} + { + get{ return _{{name}};} + set + { + _{{name}} = value; + _flag{{name}} = true; + } + } + private {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} _{{name}}; + private bool _flag{{name}}; + + /// + /// Returns false as {{name}} should not be serialized given that it's read-only. + /// + /// false (boolean) + public bool ShouldSerialize{{name}}() + { + return _flag{{name}}; + } + {{/isReadOnly}} + {{/conditionalSerialization}} + {{/isEnum}} + {{/vars}} + {{#hasRequired}} + {{^hasOnlyReadOnly}} + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + {{^isAdditionalPropertiesTrue}} + protected {{classname}}() { } + {{/isAdditionalPropertiesTrue}} + {{#isAdditionalPropertiesTrue}} + protected {{classname}}() + { + this.AdditionalProperties = new Dictionary(); + } + {{/isAdditionalPropertiesTrue}} + {{/hasOnlyReadOnly}} + {{/hasRequired}} + /// + /// Initializes a new instance of the class. + /// + {{#readWriteVars}} + /// {{description}}{{^description}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}{{/description}}{{#required}} (required){{/required}}{{#defaultValue}} (default to {{.}}){{/defaultValue}}. + {{/readWriteVars}} + {{#hasOnlyReadOnly}} + [JsonConstructorAttribute] + {{/hasOnlyReadOnly}} + public {{classname}}({{#readWriteVars}}{{{datatypeWithEnum}}}{{#isEnum}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}}{{/isEnum}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{#defaultValue}}{{^isDateTime}}{{#isString}}{{^isEnum}}@{{/isEnum}}{{/isString}}{{{defaultValue}}}{{/isDateTime}}{{#isDateTime}}default({{{datatypeWithEnum}}}){{/isDateTime}}{{/defaultValue}}{{^defaultValue}}default({{{datatypeWithEnum}}}{{#isEnum}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}}{{/isEnum}}){{/defaultValue}}{{^-last}}, {{/-last}}{{/readWriteVars}}){{#parent}} : base({{#parentVars}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}{{^-last}}, {{/-last}}{{/parentVars}}){{/parent}} + { + {{#vars}} + {{^isInherited}} + {{^isReadOnly}} + {{#required}} + {{^conditionalSerialization}} + {{^vendorExtensions.x-csharp-value-type}} + // to ensure "{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}" is required (not null) + if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} == null) + { + throw new ArgumentNullException("{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} is a required property for {{classname}} and cannot be null"); + } + this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; + {{/vendorExtensions.x-csharp-value-type}} + {{#vendorExtensions.x-csharp-value-type}} + this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; + {{/vendorExtensions.x-csharp-value-type}} + {{/conditionalSerialization}} + {{#conditionalSerialization}} + {{^vendorExtensions.x-csharp-value-type}} + // to ensure "{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}" is required (not null) + if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} == null) + { + throw new ArgumentNullException("{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} is a required property for {{classname}} and cannot be null"); + } + this._{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; + {{/vendorExtensions.x-csharp-value-type}} + {{#vendorExtensions.x-csharp-value-type}} + this._{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; + {{/vendorExtensions.x-csharp-value-type}} + {{/conditionalSerialization}} + {{/required}} + {{/isReadOnly}} + {{/isInherited}} + {{/vars}} + {{#vars}} + {{^isInherited}} + {{^isReadOnly}} + {{^required}} + {{#defaultValue}} + {{^conditionalSerialization}} + {{^vendorExtensions.x-csharp-value-type}} + // use default value if no "{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}" provided + this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} ?? {{#isString}}@{{/isString}}{{{defaultValue}}}; + {{/vendorExtensions.x-csharp-value-type}} + {{#vendorExtensions.x-csharp-value-type}} + this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; + {{/vendorExtensions.x-csharp-value-type}} + {{/conditionalSerialization}} + {{/defaultValue}} + {{^defaultValue}} + {{^conditionalSerialization}} + this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; + {{/conditionalSerialization}} + {{#conditionalSerialization}} + this._{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; + if (this.{{name}} != null) + { + this._flag{{name}} = true; + } + {{/conditionalSerialization}} + {{/defaultValue}} + {{/required}} + {{/isReadOnly}} + {{/isInherited}} + {{/vars}} + {{#isAdditionalPropertiesTrue}} + this.AdditionalProperties = new Dictionary(); + {{/isAdditionalPropertiesTrue}} + } + + {{#vars}} + {{^isInherited}} + {{^isEnum}} + /// + /// {{description}}{{^description}}Gets or Sets {{{name}}}{{/description}} + /// {{#description}} + /// {{.}}{{/description}} + {{#example}} + /* + {{.}} + */ + {{/example}} + {{^conditionalSerialization}} + [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}false{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] + {{#isDate}} + {{^supportsDateOnly}} + [JsonConverter(typeof(OpenAPIDateConverter))] + {{/supportsDateOnly}} + {{/isDate}} + {{#deprecated}} + [Obsolete] + {{/deprecated}} + public {{{dataType}}}{{^required}}?{{/required}} {{name}} { get; {{#isReadOnly}}private {{/isReadOnly}}set; } + + {{#isReadOnly}} + /// + /// Returns false as {{name}} should not be serialized given that it's read-only. + /// + /// false (boolean) + public bool ShouldSerialize{{name}}() + { + return false; + } + {{/isReadOnly}} + {{/conditionalSerialization}} + {{#conditionalSerialization}} + {{#isReadOnly}} + [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] + {{#isDate}} + [JsonConverter(typeof(OpenAPIDateConverter))] + {{/isDate}} + {{#deprecated}} + [Obsolete] + {{/deprecated}} + public {{{dataType}}} {{name}} { get; private set; } + + /// + /// Returns false as {{name}} should not be serialized given that it's read-only. + /// + /// false (boolean) + public bool ShouldSerialize{{name}}() + { + return false; + } + {{/isReadOnly}} + {{^isReadOnly}} + {{#isDate}} + [JsonConverter(typeof(OpenAPIDateConverter))] + {{/isDate}} + [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] + {{#deprecated}} + [Obsolete] + {{/deprecated}} + public {{{dataType}}} {{name}} + { + get{ return _{{name}};} + set + { + _{{name}} = value; + _flag{{name}} = true; + } + } + private {{{dataType}}} _{{name}}; + private bool _flag{{name}}; + + /// + /// Returns false as {{name}} should not be serialized given that it's read-only. + /// + /// false (boolean) + public bool ShouldSerialize{{name}}() + { + return _flag{{name}}; + } + {{/isReadOnly}} + {{/conditionalSerialization}} + {{/isEnum}} + {{/isInherited}} + {{/vars}} + {{#isAdditionalPropertiesTrue}} + /// + /// Gets or Sets additional properties + /// + [JsonExtensionData] + public IDictionary AdditionalProperties { get; set; } + + {{/isAdditionalPropertiesTrue}} + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class {{classname}} {\n"); + {{#parent}} + sb.Append(" ").Append(base.ToString().Replace("\n", "\n ")).Append("\n"); + {{/parent}} + {{#vars}} + sb.Append(" {{name}}: ").Append({{name}}).Append("\n"); + {{/vars}} + {{#isAdditionalPropertiesTrue}} + sb.Append(" AdditionalProperties: ").Append(AdditionalProperties).Append("\n"); + {{/isAdditionalPropertiesTrue}} + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public {{#parent}}{{^isArray}}{{^isMap}}override {{/isMap}}{{/isArray}}{{/parent}}{{^parent}}virtual {{/parent}}string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + {{#equatable}} + + /// + /// Returns true if objects are equal + /// + /// Object to be compared + /// Boolean + public override bool Equals(object input) + { + {{#useCompareNetObjects}} + return OpenAPIClientUtils.compareLogic.Compare(this, input as {{classname}}).AreEqual; + {{/useCompareNetObjects}} + {{^useCompareNetObjects}} + return this.Equals(input as {{classname}}); + {{/useCompareNetObjects}} + } + + /// + /// Returns true if {{classname}} instances are equal + /// + /// Instance of {{classname}} to be compared + /// Boolean + public bool Equals({{classname}} input) + { + {{#useCompareNetObjects}} + return OpenAPIClientUtils.compareLogic.Compare(this, input).AreEqual; + {{/useCompareNetObjects}} + {{^useCompareNetObjects}} + if (input == null) + { + return false; + } + return {{#vars}}{{#parent}}base.Equals(input) && {{/parent}}{{^isContainer}} + ( + this.{{name}} == input.{{name}} || + {{^vendorExtensions.x-is-value-type}} + (this.{{name}} != null && + this.{{name}}.Equals(input.{{name}})) + {{/vendorExtensions.x-is-value-type}} + {{#vendorExtensions.x-is-value-type}} + this.{{name}}.Equals(input.{{name}}) + {{/vendorExtensions.x-is-value-type}} + ){{^-last}} && {{/-last}}{{/isContainer}}{{#isContainer}} + ( + this.{{name}} == input.{{name}} || + {{^vendorExtensions.x-is-value-type}}this.{{name}} != null && + input.{{name}} != null && + {{/vendorExtensions.x-is-value-type}}this.{{name}}.SequenceEqual(input.{{name}}) + ){{^-last}} && {{/-last}}{{/isContainer}}{{/vars}}{{^vars}}{{#parent}}base.Equals(input){{/parent}}{{^parent}}false{{/parent}}{{/vars}}{{^isAdditionalPropertiesTrue}};{{/isAdditionalPropertiesTrue}} + {{#isAdditionalPropertiesTrue}} + && (this.AdditionalProperties.Count == input.AdditionalProperties.Count && !this.AdditionalProperties.Except(input.AdditionalProperties).Any()); + {{/isAdditionalPropertiesTrue}} + {{/useCompareNetObjects}} + } + + /// + /// Gets the hash code + /// + /// Hash code + public override int GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + {{#parent}} + int hashCode = base.GetHashCode(); + {{/parent}} + {{^parent}} + int hashCode = 41; + {{/parent}} + {{#vars}} + {{^vendorExtensions.x-is-value-type}} + if (this.{{name}} != null) + { + hashCode = (hashCode * 59) + this.{{name}}.GetHashCode(); + } + {{/vendorExtensions.x-is-value-type}} + {{#vendorExtensions.x-is-value-type}} + hashCode = (hashCode * 59) + this.{{name}}.GetHashCode(); + {{/vendorExtensions.x-is-value-type}} + {{/vars}} + {{#isAdditionalPropertiesTrue}} + if (this.AdditionalProperties != null) + { + hashCode = (hashCode * 59) + this.AdditionalProperties.GetHashCode(); + } + {{/isAdditionalPropertiesTrue}} + return hashCode; + } + } + {{/equatable}} + +{{#validatable}} +{{>validatable}} + +{{/validatable}} + } diff --git a/src/Regula.DocumentReader.WebClient/Client/ApiClient.cs b/src/Regula.DocumentReader.WebClient/Client/ApiClient.cs index 8281845..1e77851 100644 --- a/src/Regula.DocumentReader.WebClient/Client/ApiClient.cs +++ b/src/Regula.DocumentReader.WebClient/Client/ApiClient.cs @@ -171,7 +171,8 @@ internal async Task Deserialize(HttpResponseMessage response, Type type) } catch (Exception e) { - throw new ApiException(500, e.Message); + Console.WriteLine($"Warning: Failed to deserialize response to type {type.Name}: {e.Message}"); + return type.IsValueType ? Activator.CreateInstance(type) : null; } } @@ -214,9 +215,17 @@ public partial class ApiClient : IDisposable, ISynchronousClient, IAsynchronousC { OverrideSpecifiedNames = false } - } + }, + Error = HandleDeserializationError }; + private static void HandleDeserializationError(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs args) + { + var currentError = args.ErrorContext.Error.GetBaseException(); + Console.WriteLine($"Warning: JSON Deserialization Error at path '{args.ErrorContext.Path}'. Member: '{args.ErrorContext.Member}'. Error: {currentError.Message}."); + args.ErrorContext.Handled = true; + } + /// /// 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. @@ -784,4 +793,4 @@ public ApiResponse Patch(string path, RequestOptions options, IReadableCon } #endregion ISynchronousClient } -} +} \ No newline at end of file diff --git a/src/Regula.DocumentReader.WebClient/Model/EncryptedRCLItem.cs b/src/Regula.DocumentReader.WebClient/Model/EncryptedRCLItem.cs index 93839dd..dfd1942 100644 --- a/src/Regula.DocumentReader.WebClient/Model/EncryptedRCLItem.cs +++ b/src/Regula.DocumentReader.WebClient/Model/EncryptedRCLItem.cs @@ -56,7 +56,7 @@ protected EncryptedRCLItem() { } /// /// Base64 encoded data /* - [B@c230d58 + [B@38efd9b4 */ [DataMember(Name = "EncryptedRCL", IsRequired = true, EmitDefaultValue = true)] public byte[] EncryptedRCL { get; set; } diff --git a/src/Regula.DocumentReader.WebClient/Model/EncryptedRCLResult.cs b/src/Regula.DocumentReader.WebClient/Model/EncryptedRCLResult.cs index 52aa46e..f1b492b 100644 --- a/src/Regula.DocumentReader.WebClient/Model/EncryptedRCLResult.cs +++ b/src/Regula.DocumentReader.WebClient/Model/EncryptedRCLResult.cs @@ -62,7 +62,7 @@ protected EncryptedRCLResult() { } /// /// Base64 encoded data /* - [B@c230d58 + [B@38efd9b4 */ [DataMember(Name = "EncryptedRCL", IsRequired = true, EmitDefaultValue = true)] public byte[] EncryptedRCL { get; set; } diff --git a/src/Regula.DocumentReader.WebClient/Model/LicenseItem.cs b/src/Regula.DocumentReader.WebClient/Model/LicenseItem.cs index cdf3775..e8113dd 100644 --- a/src/Regula.DocumentReader.WebClient/Model/LicenseItem.cs +++ b/src/Regula.DocumentReader.WebClient/Model/LicenseItem.cs @@ -56,7 +56,7 @@ protected LicenseItem() { } /// /// Base64 encoded data /* - [B@4f840c3f + [B@161092b */ [DataMember(Name = "License", IsRequired = true, EmitDefaultValue = true)] public byte[] License { get; set; } diff --git a/src/Regula.DocumentReader.WebClient/Model/LicenseResult.cs b/src/Regula.DocumentReader.WebClient/Model/LicenseResult.cs index c6888d3..e27dedb 100644 --- a/src/Regula.DocumentReader.WebClient/Model/LicenseResult.cs +++ b/src/Regula.DocumentReader.WebClient/Model/LicenseResult.cs @@ -62,7 +62,7 @@ protected LicenseResult() { } /// /// Base64 encoded data /* - [B@4f840c3f + [B@161092b */ [DataMember(Name = "License", IsRequired = true, EmitDefaultValue = true)] public byte[] License { get; set; } diff --git a/update-models.sh b/update-models.sh index f69f92d..97f1048 100755 --- a/update-models.sh +++ b/update-models.sh @@ -1,13 +1,19 @@ #!/bin/sh -DOCS_DEFINITION_FOLDER="${PWD}/../DocumentReader-web-openapi" \ -\ -&& docker run --user "$(id -u):$(id -g)" --rm \ +MODE="$1" +DOCS_DEFINITION_FOLDER="${PWD}/../DocumentReader-web-openapi" +TEMPLATE_PATH="/client/generator-templates/lenient" + +if [ "$MODE" = "strict" ]; then + TEMPLATE_PATH="/client/generator-templates/strict" +fi + +docker run --user "$(id -u):$(id -g)" --rm \ -v "${PWD}:/client" \ -v "${DOCS_DEFINITION_FOLDER}:/definitions" \ -openapitools/openapi-generator-cli:v7.13.0 generate \ +openapitools/openapi-generator-cli:v7.15.0 generate \ -g csharp \ -i /definitions/index.yml \ -o /client/ --openapi-normalizer REF_AS_PARENT_IN_ALLOF=true \ --t /client/generator-templates \ +-t $TEMPLATE_PATH \ -c /client/csharp-generator-config.json || exit 1 \ No newline at end of file