Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat dotnet mutlipart #970

Merged
merged 13 commits into from
Sep 17, 2024
20 changes: 15 additions & 5 deletions src/SDK/Language/DotNet.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ public function getTypeName(array $parameter, array $spec = []): string
self::TYPE_NUMBER => 'double',
self::TYPE_STRING => 'string',
self::TYPE_BOOLEAN => 'bool',
self::TYPE_FILE => 'InputFile',
self::TYPE_FILE => 'Payload',
self::TYPE_PAYLOAD => 'Payload',
self::TYPE_ARRAY => (!empty(($parameter['array'] ?? [])['type']) && !\is_array($parameter['array']['type']))
? 'List<' . $this->getTypeName($parameter['array']) . '>'
: 'List<object>',
Expand Down Expand Up @@ -242,8 +243,11 @@ public function getParamExample(array $param): string

if (empty($example) && $example !== 0 && $example !== false) {
switch ($type) {
case self::TYPE_PAYLOAD:
$output .= 'Payload.fromJson(new KeyValuePair<string, string>("x", "y"))';
break;
case self::TYPE_FILE:
$output .= 'InputFile.FromPath("./path-to-files/image.jpg")';
$output .= 'Payload.FromFile("/path/to/file.png")';
break;
case self::TYPE_NUMBER:
case self::TYPE_INTEGER:
Expand Down Expand Up @@ -286,8 +290,14 @@ public function getParamExample(array $param): string
case self::TYPE_BOOLEAN:
$output .= ($example) ? 'true' : 'false';
break;
case self::TYPE_PAYLOAD:
$output .= 'Payload.fromJson(new KeyValuePair<string, string>("x", "y"))';
break;
case self::TYPE_FILE:
$output .= 'Payload.FromFile("/path/to/file.png")';
break;
case self::TYPE_STRING:
$output .= "\"{$example}\"";
$output .= '"{$example}"';
break;
}
}
Expand Down Expand Up @@ -393,8 +403,8 @@ public function getFiles(): array
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}/Models/InputFile.cs',
'template' => 'dotnet/Package/Models/InputFile.cs.twig',
'destination' => '{{ spec.title | caseUcfirst }}/Models/Payload.cs',
'template' => 'dotnet/Package/Models/Payload.cs.twig',
],
[
'scope' => 'default',
Expand Down
8 changes: 4 additions & 4 deletions src/SDK/Language/Go.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,10 @@ public function getParamExample(array $param): string
$output .= '[]interface{}{}';
break;
case self::TYPE_PAYLOAD:
$output .= 'payload.NewPayloadFromString("<BODY>")';
$output .= 'payload.NewPayloadFromJson(map[string]interface{}{ "x": "y" })';
break;
case self::TYPE_FILE:
$output .= 'payload.NewPayloadFromFile("/path/to/file.png", "file.png")';
$output .= 'payload.NewPayloadFromFile("/path/to/file.png")';
break;
}
} else {
Expand Down Expand Up @@ -275,10 +275,10 @@ public function getParamExample(array $param): string
$output .= '"{$example}"';
break;
case self::TYPE_PAYLOAD:
$output .= 'payload.NewPayloadFromString("<BODY>")';
$output .= 'payload.NewPayloadFromJson(map[string]interface{}{ "x": "y" })';
break;
case self::TYPE_FILE:
$output .= 'payload.NewPayloadFromFile("/path/to/file.png", "file.png")';
$output .= 'payload.NewPayloadFromFile("/path/to/file.png")';
break;
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/SDK/Language/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public function getParamExample(array $param): string
break;
case self::TYPE_PAYLOAD:
$output .= 'Payload.fromJson({ x: "y" })';
break;
case self::TYPE_FILE:
$output .= "Payload.fromBinary(fs.readFileSync('/path/to/file.png'), 'file.png')";
break;
Expand All @@ -142,6 +143,7 @@ public function getParamExample(array $param): string
break;
case self::TYPE_PAYLOAD:
$output .= 'Payload.fromJson({ x: "y" })';
break;
case self::TYPE_FILE:
$output .= "Payload.fromBinary(fs.readFileSync('/path/to/file.png'), 'file.png')";
break;
Expand Down
4 changes: 2 additions & 2 deletions src/SDK/Language/PHP.php
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ public function getParamExample(array $param): string
$output .= '[]';
break;
case self::TYPE_PAYLOAD:
$output .= "Payload::fromString('<BODY>')";
$output .= "Payload::fromJson([ 'x' => 'y' ])";
break;
case self::TYPE_FILE:
$output .= "Payload::fromFile('file.png')";
Expand All @@ -375,7 +375,7 @@ public function getParamExample(array $param): string
$output .= "'{$example}'";
break;
case self::TYPE_PAYLOAD:
$output .= "Payload::fromJson([])";
$output .= "Payload::fromJson([ 'x' => 'y' ])";
break;
case self::TYPE_FILE:
$output .= "Payload::fromFile('file.png')";
Expand Down
4 changes: 2 additions & 2 deletions src/SDK/Language/Python.php
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ public function getParamExample(array $param): string
$output .= '{}';
break;
case self::TYPE_PAYLOAD:
$output .= 'Payload.from_string({"x": "y"})';
$output .= 'Payload.from_json({"x": "y"})';
break;
case self::TYPE_FILE:
$output .= "Payload.from_file('/path/to/file.png')";
Expand All @@ -361,7 +361,7 @@ public function getParamExample(array $param): string
$output .= "'{$example}'";
break;
case self::TYPE_PAYLOAD:
$output .= 'Payload.from_string({"x": "y"})';
$output .= 'Payload.from_json({"x": "y"})';
break;
case self::TYPE_FILE:
$output .= "Payload.from_file('/path/to/file.png')";
Expand Down
96 changes: 88 additions & 8 deletions templates/dotnet/Package/Client.cs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using {{ spec.title | caseUcfirst }}.Converters;
using {{ spec.title | caseUcfirst }}.Extensions;
using {{ spec.title | caseUcfirst }}.Models;
Expand Down Expand Up @@ -60,7 +61,7 @@ namespace {{ spec.title | caseUcfirst }}
{
_endpoint = endpoint;
_http = http ?? new HttpClient();

_httpForRedirect = httpForRedirect ?? new HttpClient(
new HttpClientHandler(){
AllowAutoRedirect = false
Expand Down Expand Up @@ -220,7 +221,7 @@ namespace {{ spec.title | caseUcfirst }}

public async Task<String> Redirect(
string method,
string path,
string path,
Dictionary<string, string> headers,
Dictionary<string, object?> parameters)
{
Expand Down Expand Up @@ -269,7 +270,7 @@ namespace {{ spec.title | caseUcfirst }}
var response = await _http.SendAsync(request);
var code = (int)response.StatusCode;

if (response.Headers.TryGetValues("x-{{ spec.title | lower }}-warning", out var warnings))
if (response.Headers.TryGetValues("x-{{ spec.title | lower }}-warning", out var warnings))
{
foreach (var warning in warnings)
{
Expand All @@ -284,6 +285,7 @@ namespace {{ spec.title | caseUcfirst }}
}

var isJson = contentType.Contains("application/json");
var isFormData = contentType.Contains("multipart/form-data");

if (code >= 400) {
var message = await response.Content.ReadAsStringAsync();
Expand All @@ -310,10 +312,12 @@ namespace {{ spec.title | caseUcfirst }}

return (dict as T)!;
}
else
{
return ((await response.Content.ReadAsByteArrayAsync()) as T)!;
}

if (!isFormData) return ((await response.Content.ReadAsByteArrayAsync()) as T)!;

var data = HandleMultipart<Dictionary<string, object>>(await response.Content.ReadAsByteArrayAsync());

return convert != null ? convert(data as Dictionary<string, object>) : data as T;
}

public async Task<T> ChunkedUpload<T>(
Expand All @@ -325,7 +329,7 @@ namespace {{ spec.title | caseUcfirst }}
string? idParamName = null,
Action<UploadProgress>? onProgress = null) where T : class
{
var input = parameters[paramName] as InputFile;
var input = parameters[paramName] as Payload;
var size = 0L;
switch(input.SourceType)
{
Expand Down Expand Up @@ -446,5 +450,81 @@ namespace {{ spec.title | caseUcfirst }}

return converter(result);
}

public static T HandleMultipart<T>(byte[] multipart) where T : class
{
var str = Encoding.UTF8.GetString(multipart);
var data = new Dictionary<string, object>();
var boundarySearch = new Regex(@"(-+\w+)--").Match(str);

if (boundarySearch.Groups.Count != 2)
{
return new object() as T;
}

var boundary = boundarySearch.Groups[1].Value;
var parts = str.Split(new string[] { boundary }, StringSplitOptions.RemoveEmptyEntries);

foreach (var part in parts)
{
var lines = part.Split(new string[]{"\r\n"}, StringSplitOptions.RemoveEmptyEntries);
var nameMatch = new Regex(@"name=""?(\w+)").Match(part);

if (lines.Length <= 1 || nameMatch.Groups.Count != 2)
{
continue;
}
lines = lines.Skip(1).ToArray();
var name = nameMatch.Groups[1].Value;
if (lines[0] == "Content-Type: application/json")
{
lines = lines.Skip(1).ToArray();
data.Add(name, JsonConvert.DeserializeObject<List<dynamic>>(string.Join("\r\n", lines)) ?? new List<dynamic>());
continue;
}
if (name == "responseBody")
{
const string needle = "name=\"responseBody\"\r\n\r\n";
var indexOf = str.IndexOf(needle, StringComparison.Ordinal) + needle.Length;
var endBytes = Encoding.UTF8.GetBytes("\r\n-------");
multipart = multipart.Skip(indexOf).ToArray();

data.Add(name, Payload.FromBinary(multipart.TakeWhile((t, i) => !DidFinishedBinaryData(multipart, endBytes, i)).ToArray()));

continue;
}
var value = string.Join("\r\n", lines);

data.Add(name, value);
}
// Adding to match Execution model
data.Add("$id","");
data.Add("$createdAt","");
data.Add("$updatedAt","");
data.Add("logs","");
data.Add("errors","");
data.Add("scheduledAt","");
data.Add("$permissions",new List<Object>());
return data as T;
}

private static bool DidFinishedBinaryData(byte[] multipart, byte[] endBytes, int i)
{
if (multipart.Length > i + endBytes.Length)
{
for (var j = 0; j < endBytes.Length; j++)
{
if (multipart[i + j] != endBytes[j])
{
break;
}

if (j != endBytes.Length - 1) continue;
return true;
}
}

return false;
}
}
}
20 changes: 0 additions & 20 deletions templates/dotnet/Package/Extensions/Extensions.cs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -603,25 +603,5 @@ namespace {{ spec.title | caseUcfirst }}.Extensions
#endregion

};

public static string GetMimeTypeFromExtension(string extension)
{
if (extension == null)
{
throw new ArgumentNullException("extension");
}

if (!extension.StartsWith("."))
{
extension = "." + extension;
}

return _mappings.TryGetValue(extension, out var mime) ? mime : "application/octet-stream";
}

public static string GetMimeType(this string path)
{
return GetMimeTypeFromExtension(System.IO.Path.GetExtension(path));
}
}
}
41 changes: 0 additions & 41 deletions templates/dotnet/Package/Models/InputFile.cs.twig

This file was deleted.

4 changes: 2 additions & 2 deletions templates/dotnet/Package/Models/Model.cs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ namespace {{ spec.title | caseUcfirst }}.Models

public static {{ definition.name | caseUcfirst | overrideIdentifier}} From(Dictionary<string, object> map) => new {{ definition.name | caseUcfirst | overrideIdentifier}}(
{%~ for property in definition.properties %}
{{ property.name | caseCamel | escapeKeyword | removeDollarSign }}: {% if property.sub_schema %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject<List<Dictionary<string, object>>>().Select(it => {{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: it)).ToList(){% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: ((JObject)map["{{ property.name }}"]).ToObject<Dictionary<string, object>>()!){% endif %}{% else %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject<{{ property | typeName }}>(){% else %}{% if property.type == "integer" or property.type == "number" %}{% if not property.required %}map["{{ property.name }}"] == null ? null : {% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]){% else %}{% if property.type == "boolean" %}({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"]{% else %}map{% if not property.required %}.TryGetValue("{{ property.name }}", out var {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}) ? {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}?.ToString() : null{% else %}["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString(){% endif %}{% endif %}{% endif %}{% endif %}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %}
{{ property.name | caseCamel | escapeKeyword | removeDollarSign }}: {% if property.sub_schema %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject<List<Dictionary<string, object>>>().Select(it => {{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: it)).ToList(){% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: ((JObject)map["{{ property.name }}"]).ToObject<Dictionary<string, object>>()!){% endif %}{% else %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject<{{ property | typeName }}>(){% else %}{% if property.type == "integer" or property.type == "number" %}{% if not property.required %}map["{{ property.name }}"] == null ? null : {% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]){% else %}{% if property.type == "boolean" %}({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"]{% else %}map{% if not property.required %}.TryGetValue("{{ property.name }}", out var {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}) ? {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}{% if property.name == "responseBody" or property.type | caseLower == "file" %} as Payload{% else %}?.ToString(){% endif %} : null{% else %}["{{ property.name }}"]{% if not property.required %}?{% endif %}{% if property.name == "responseBody" or property.type | caseLower == "payload" %} as Payload{% else %}.ToString(){% endif %}{% endif %}{% endif %}{% endif %}{% endif %}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{{ property.name | caseCamel | escapeKeyword | removeDollarSign }}: {% if property.sub_schema %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject<List<Dictionary<string, object>>>().Select(it => {{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: it)).ToList(){% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: ((JObject)map["{{ property.name }}"]).ToObject<Dictionary<string, object>>()!){% endif %}{% else %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject<{{ property | typeName }}>(){% else %}{% if property.type == "integer" or property.type == "number" %}{% if not property.required %}map["{{ property.name }}"] == null ? null : {% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]){% else %}{% if property.type == "boolean" %}({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"]{% else %}map{% if not property.required %}.TryGetValue("{{ property.name }}", out var {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}) ? {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}{% if property.name == "responseBody" or property.type | caseLower == "file" %} as Payload{% else %}?.ToString(){% endif %} : null{% else %}["{{ property.name }}"]{% if not property.required %}?{% endif %}{% if property.name == "responseBody" or property.type | caseLower == "payload" %} as Payload{% else %}.ToString(){% endif %}{% endif %}{% endif %}{% endif %}{% endif %}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %}
{{ property.name | caseCamel | escapeKeyword | removeDollarSign }}:
{%~ if property.sub_schema %}
{%~ if property.type == 'array' %}
((JArray)map["{{ property.name }}"])
.ToObject<List<Dictionary<string, object>>>()
.Select(it => {{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: it))
.ToList()
{%~ else %}
{{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(
map: ((JObject)map["{{ property.name }}"])
.ToObject<Dictionary<string, object>>()!
)
{%~ endif %}
{%~ else %}
{%~ if property.type == 'array' %}
((JArray)map["{{ property.name }}"])
.ToObject<{{ property | typeName }}>()
{%~ else %}
{%~ if property.type == "integer" or property.type == "number" %}
{%~ if not property.required -%}
map["{{ property.name }}"] == null
? null
:
{%~ endif -%}
Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"])
{%~ else %}
{%~ if property.type == "boolean" %}
({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"]
{%~ else %}
map{% if not property.required %}.TryGetValue("{{ property.name }}", out var {{ property.name | caseCamel | escapeKeyword | removeDollarSign }})
? {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}{% if property.name == "responseBody" or property.type | caseLower == "file" %} as Payload{% else %}?.ToString(){% endif %}
: null
{%~ else %}
["{{ property.name }}"]{% if not property.required %}?{% endif %}{% if property.name == "responseBody" or property.type | caseLower == "payload" %} as Payload{% else %}.ToString(){% endif %}
{%~ endif %}
{%~ endif %}
{%~ endif %}
{%~ endif %}
{%~ endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif -%}

Copy link
Contributor

@Meldiron Meldiron Sep 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this but I think it should be done as part of code quality project planned later this week. We have issue for it. There are many more places in sdk-generator that could benefit from this.
Simple linter for max row length can spot them all


{%~ endfor %}
{%~ if definition.additionalProperties %}
Expand Down Expand Up @@ -76,4 +76,4 @@ namespace {{ spec.title | caseUcfirst }}.Models
{%~ endif %}
{%~ endfor %}
}
}
}
Loading
Loading