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: go multipart #942

Merged
merged 15 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ jobs:
DotNet80,
FlutterStable,
FlutterBeta,
Go112,
Go118,
Go122,
KotlinJava8,
KotlinJava11,
KotlinJava17,
Expand Down
24 changes: 18 additions & 6 deletions src/SDK/Language/Go.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ public function getFiles(): array
],
[
'scope' => 'default',
'destination' => 'file/inputFile.go',
'template' => 'go/inputFile.go.twig',
'destination' => 'payload/payload.go',
'template' => 'go/payload.go.twig',
],
[
'scope' => 'default',
Expand Down Expand Up @@ -141,10 +141,12 @@ public function getTypeName(array $parameter, array $spec = []): string
if (str_contains($parameter['description'] ?? '', 'Collection attributes') || str_contains($parameter['description'] ?? '', 'List of attributes')) {
return '[]map[string]any';
}

return match ($parameter['type']) {
self::TYPE_INTEGER => 'int',
self::TYPE_NUMBER => 'float64',
self::TYPE_FILE => 'file.InputFile',
self::TYPE_PAYLOAD,
self::TYPE_FILE => '*payload.Payload',
self::TYPE_STRING => 'string',
self::TYPE_BOOLEAN => 'bool',
self::TYPE_OBJECT => 'interface{}',
Expand Down Expand Up @@ -241,8 +243,11 @@ public function getParamExample(array $param): string
case self::TYPE_ARRAY:
$output .= '[]interface{}{}';
break;
case self::TYPE_PAYLOAD:
$output .= 'payload.NewPayloadFromString("<BODY>")';
break;
case self::TYPE_FILE:
$output .= 'file.NewInputFile("/path/to/file.png", "file.png")';
$output .= 'payload.NewPayloadFromPath("/path/to/file.png", "file.png")';
break;
}
} else {
Expand All @@ -267,10 +272,13 @@ public function getParamExample(array $param): string
$output .= ($example) ? 'true' : 'false';
break;
case self::TYPE_STRING:
$output .= "\"{$example}\"";
$output .= '"{$example}"';
break;
case self::TYPE_PAYLOAD:
$output .= 'payload.NewPayloadFromString("<BODY>")';
break;
case self::TYPE_FILE:
$output .= 'file.NewInputFile("/path/to/file.png", "file.png")';
$output .= 'payload.NewPayloadFromPath("/path/to/file.png", "file.png")';
break;
}
}
Expand Down Expand Up @@ -304,6 +312,10 @@ public function getFilters(): array

protected function getPropertyType(array $property, array $spec, string $generic = 'map[string]interface{}'): string
{

if (strpos($property['description'], 'HTTP response body. This will return empty unless execution') !== false) {
return '*payload.Payload';
}
if (\array_key_exists('sub_schema', $property)) {
$type = $this->toPascalCase($property['sub_schema']);

Expand Down
14 changes: 7 additions & 7 deletions templates/go/README.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ go get github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName }}
* Then inject these environment variables:

```bash
export YOUR_ENDPOINT=https://{{ sdk.gitUserName|url_encode }}.io/v1
export YOUR_PROJECT_ID=6…8
export YOUR_KEY="7055781…cd95"
export COLLECTION_ID=616a095b20180
export YOUR_ENDPOINT=https://{{ sdk.gitUserName|url_encode }}.io/v1
export YOUR_PROJECT_ID=6…8
export YOUR_KEY="7055781…cd95"
export COLLECTION_ID=616a095b20180
```

Create `main.go` file with:
Expand All @@ -63,7 +63,7 @@ func main() {
appwrite.WithKey(os.Getenv("YOUR_KEY")),
)

databases := appwrite.NewDatabase(client)
databases := appwrite.NewDatabases(client)

data := map[string]string{
"hello": "world",
Expand All @@ -84,8 +84,8 @@ func main() {

* After that, run the following

> % go run main.go
> 2021/10/16 03:41:17 Created document: map[$collection:616a095b20180 $id:616a2dbd4df16 $permissions:map[read:[] write:[]] hello:world]
> % go run main.go
> 2021/10/16 03:41:17 Created document: map[$collection:616a095b20180 $id:616a2dbd4df16 $permissions:map[read:[] write:[]] hello:world]

{% if sdk.gettingStarted %}

Expand Down
4 changes: 4 additions & 0 deletions templates/go/base/params.twig
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
params["{{ parameter.name }}"] = {{ parameter.name | caseUcfirst }}
{% else %}
if options.enabledSetters["{{ parameter.name | caseUcfirst}}"] {
{%~ if parameter.type == "payload" %}
params["{{ parameter.name }}"] = string(options.{{ parameter.name | caseUcfirst }}.Data)
{%~ else %}
params["{{ parameter.name }}"] = options.{{ parameter.name | caseUcfirst }}
{%~ endif %}
}
{% endif %}
{% endfor %}
Expand Down
11 changes: 7 additions & 4 deletions templates/go/base/requests/api.twig
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
return nil, err
}
if strings.HasPrefix(resp.Type, "application/json") {
bytes := []byte(resp.Result.(string))
bytesData := []byte(resp.Result.(string))

{%~ if method | returnType(spec, spec.title | caseLower) != 'interface{}' and method | returnType(spec, spec.title | caseLower) != '[]byte' and method | returnType(spec, spec.title | caseLower) != 'bool' %}
parsed := {{ method | returnType(spec, spec.title | caseLower) }}{}.New(bytes)
parsed := {{ method | returnType(spec, spec.title | caseLower) }}{}.New(bytesData)

err = json.Unmarshal(bytes, parsed)
err = json.Unmarshal(bytesData, parsed)
if err != nil {
return nil, err
}
Expand All @@ -17,13 +17,16 @@
{%~ else %}
var parsed {{ method | returnType(spec, spec.title | caseLower) }}

err = json.Unmarshal(bytes, &parsed)
err = json.Unmarshal(bytesData, &parsed)
if err != nil {
return nil, err
}
return &parsed, nil
{%~ endif %}
}
{% if 'multipart/form-data' in method.consumes and method.type != "upload" %}
{{ include('go/base/requests/execution.twig') }}
{%~ endif %}
var parsed {{ method | returnType(spec, spec.title | caseLower) }}
parsed, ok := resp.Result.({{ method | returnType(spec, spec.title | caseLower) }})
if !ok {
Expand Down
92 changes: 92 additions & 0 deletions templates/go/base/requests/execution.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
if strings.Contains(resp.Type, "multipart/form-data") {
bytesData, ok := resp.Result.([]byte)

if !ok {
return nil, errors.New("unexpected response type")
}
responseData := string(bytesData)

matches := regexp.MustCompile("(-+\\w+)--").FindStringSubmatch(responseData)

if len(matches) != 2 {
return nil, errors.New("unexpected response type")
}

parts := strings.Split(responseData, matches[1])

if len(parts) == 0 {
return nil, errors.New("unexpected response type")
}
execution := make(map[string]string, 10)

for _, part := range parts {
cleanPart := strings.TrimSpace(part)
partName := regexp.MustCompile("name=\"?(\\w+)").FindStringSubmatch(cleanPart)

if len(partName) != 2 {
continue
}

name := strings.TrimSpace(partName[1])
lines := strings.Split(strings.ReplaceAll(cleanPart, "\r\n", "\n"), "\n")

Inner:
for i, line := range lines[1:] {
if line == "" {
continue
}

if line == "Content-Type: application/json" {
for _, line := range lines[i:] {
if line == "" {
continue
}

execution[name] = line
}
continue Inner
}
execution[name] += line + "\r\n"
}
execution[name] = strings.TrimSuffix(execution[name],"\r\n")
}

statusCode, err := strconv.Atoi(execution["responseStatusCode"])
if err != nil {
statusCode = 0
}

duration, err := strconv.ParseFloat(execution["duration"], 64)
if err != nil {
duration = 0.0
}

var requestHeaders []models.Headers
var responseHeaders []models.Headers

buffer := bytes.NewBuffer([]byte(execution["requestHeaders"]))
decoder := json.NewDecoder(buffer)
_ = decoder.Decode(&requestHeaders)

buffer = bytes.NewBuffer([]byte(execution["responseHeaders"]))
decoder = json.NewDecoder(buffer)
_ = decoder.Decode(&responseHeaders)

results := models.Execution{
FunctionId: execution["functionId"],
Trigger: execution["trigger"],
Status: execution["status"],
RequestMethod: execution["requestMethod"],
RequestPath: execution["requestPath"],
RequestHeaders: requestHeaders,
ResponseStatusCode: statusCode,
ResponseBody: payload.NewPayloadFromString(execution["responseBody"]),
ResponseHeaders: responseHeaders,
Logs: execution["logs"],
Errors: execution["errors"],
Duration: duration,
ScheduledAt: execution["scheduledAt"],
}

return &results, nil
}
31 changes: 16 additions & 15 deletions templates/go/client.go.twig
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"time"
"runtime"

"github.com/{{sdk.gitUserName}}/sdk-for-go/file"
"github.com/{{sdk.gitUserName}}/sdk-for-go/payload"
)

const (
Expand Down Expand Up @@ -122,7 +122,7 @@ func (client *Client) AddHeader(key string, value string) {
client.Headers[key] = value
}

func isFileUpload(headers map[string]interface{}) bool {
func isMultipart(headers map[string]interface{}) bool {
contentType, ok := headers["content-type"].(string)
if ok {
return strings.Contains(strings.ToLower(contentType), "multipart/form-data")
Expand All @@ -131,13 +131,13 @@ func isFileUpload(headers map[string]interface{}) bool {
}

func (client *Client) FileUpload(url string, headers map[string]interface{}, params map[string]interface{}, paramName string, uploadId string) (*ClientResponse, error) {
inputFile, ok := params[paramName].(file.InputFile)
payload, ok := params[paramName].(*payload.Payload)
if !ok {
msg := fmt.Sprintf("invalid input file. params[%s] must be of type file.InputFile", paramName)
msg := fmt.Sprintf("invalid input file. params[%s] must be of type payload.Payload", paramName)
return nil, errors.New(msg)
}

file, err := os.Open(inputFile.Path)
file, err := os.Open(payload.Path)
if err != nil {
return nil, err
}
Expand All @@ -148,7 +148,7 @@ func (client *Client) FileUpload(url string, headers map[string]interface{}, par
return nil, err
}

inputFile.Data = make([]byte, client.ChunkSize)
payload.Data = make([]byte, client.ChunkSize)

var result *ClientResponse

Expand All @@ -168,12 +168,12 @@ func (client *Client) FileUpload(url string, headers map[string]interface{}, par
if uploadId != "" && uploadId != "unique()" {
headers["x-appwrite-id"] = uploadId
}
inputFile.Data = make([]byte, fileInfo.Size())
_, err := file.Read(inputFile.Data)
payload.Data = make([]byte, fileInfo.Size())
_, err := file.Read(payload.Data)
if err != nil && err != io.EOF {
return nil, err
}
params[paramName] = inputFile
params[paramName] = payload

result, err = client.Call("POST", url, headers, params)
if err != nil {
Expand All @@ -196,13 +196,13 @@ func (client *Client) FileUpload(url string, headers map[string]interface{}, par
offset := int64(i) * chunkSize
if i == numChunks-1 {
chunkSize = fileInfo.Size() - offset
inputFile.Data = make([]byte, chunkSize)
payload.Data = make([]byte, chunkSize)
}
_, err := file.ReadAt(inputFile.Data, offset)
_, err := file.ReadAt(payload.Data, offset)
if err != nil && err != io.EOF {
return nil, err
}
params[paramName] = inputFile
params[paramName] = payload
if uploadId != "" && uploadId != "unique()" {
headers["x-appwrite-id"] = uploadId
}
Expand Down Expand Up @@ -248,18 +248,19 @@ func (client *Client) Call(method string, path string, headers map[string]interf
isGet := strings.ToUpper(method) == "GET"
isPost := strings.ToUpper(method) == "POST"
isJsonRequest := headers["content-type"] == "application/json"
isFileUpload := isFileUpload(headers)
isMultipart := isMultipart(headers)

var req *http.Request
var err error
if isFileUpload {
if isMultipart {
headers["accept"] = "multipart/form-data"
if !isPost {
return nil, errors.New("fileupload needs POST Request")
}
var body bytes.Buffer
writer := multipart.NewWriter(&body)
for key, val := range params {
if file, ok := val.(file.InputFile); ok {
if file, ok := val.(*payload.Payload); ok {
fileName := file.Name
fileData := file.Data
fw, err := writer.CreateFormFile(key, fileName)
Expand Down
19 changes: 9 additions & 10 deletions templates/go/docs/example.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,26 @@ package main

import (
"fmt"
"github.com/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}/client"
"github.com/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}/{{ service.name | caseLower }}"
{% if requireFilesPkg %}
"github.com/{{sdk.gitUserName}}/sdk-for-go/file"
"github.com/{{sdk.gitUserName}}/sdk-for-go/appwrite"
{% if requireFilesPkg or method.name | caseLower == "createexecution" %}
"github.com/{{sdk.gitUserName}}/sdk-for-go/payload"
{% endif %}
)

func main() {
client := client.NewClient()

client := appwrite.NewClient(
{% if method.auth|length > 0 %}
client.SetEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
appwrite.WithEndpoint("https://cloud.appwrite.io/v1"), // Your API Endpoint
{% for node in method.auth %}
{% for key,header in node|keys %}
client.Set{{header}}("{{node[header]['x-appwrite']['demo'] | raw }}") // {{node[header].description}}
appwrite.With{{header}}("{{node[header]['x-{{ spec.title | caseLower }}']['demo']}}"), // {{node[header].description}}
{% endfor %}
{% endfor %}
)

{% endif %}
service := {{ service.name | caseLower }}.New{{ service.name | caseUcfirst }}(client)
response, error := service.{{ method.name | caseUcfirst }}(
{{service.name}} := appwrite.New{{ service.name | caseUcfirst }}(client)
response, error := {{service.name}}.{{ method.name | caseUcfirst }}(
{% for parameter in method.parameters.all %}
{% if parameter.required %}
{{ parameter | paramExample }},
Expand Down
Loading
Loading