-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SIANXSVC-1222: Add a binary file API to go-e5e
Closes SIANXSVC-1222
- Loading branch information
1 parent
8a159d0
commit dcdfd85
Showing
4 changed files
with
313 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package e5e_test | ||
|
||
import ( | ||
"context" | ||
|
||
"go.anx.io/e5e/v2" | ||
) | ||
|
||
func BinaryInverse(_ context.Context, request e5e.Request[e5e.File, any]) (*e5e.Result, error) { | ||
var outputBinary []byte | ||
|
||
inputBinary := request.Data().Bytes() | ||
for _, inputByte := range inputBinary { | ||
outputBinary = append(outputBinary, inputByte^255) | ||
} | ||
|
||
outputFile := e5e.File{ | ||
Name: "output.blob", | ||
ContentType: "x-my-first-function/blob", | ||
} | ||
outputFile.Write(outputBinary) | ||
|
||
return &e5e.Result{ | ||
Type: e5e.ResultDataTypeBinary, | ||
Data: outputFile, | ||
}, nil | ||
} | ||
|
||
func Example_binaryContent() { | ||
e5e.AddHandlerFunc("MyFunction", BinaryInverse) | ||
e5e.Start(context.Background()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
package e5e | ||
|
||
import ( | ||
"encoding/base64" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
// File contains information about a received or sent file. | ||
// It is commonly used with "mixed" or "binary" requests/responses. | ||
type File struct { | ||
// The contents of the file, encoded in [Charset]. | ||
content []byte | ||
|
||
// The type of this binary, usually just "binary". | ||
Type string `json:"type"` | ||
|
||
// The size of the file in bytes. | ||
// If it cannot be determined reliably, just leave it at the default value. | ||
SizeInBytes int64 `json:"size,omitempty"` | ||
|
||
// The optional filename of the file. | ||
Name string `json:"name,omitempty"` | ||
|
||
// The content type of the file. | ||
// For responses, the Content-Type heaader is set automatically be the E5E engine to this value. | ||
ContentType string `json:"content_type,omitempty"` | ||
|
||
// The charset of the file. Should be set to the recommended value "utf-8". | ||
Charset string `json:"charset,omitempty"` | ||
} | ||
|
||
// SetText sets the content of this file to the encoded version of text. | ||
// It further enforces the content type to "text/plain". The file size and the charset are set, | ||
// if they haven't been set already modified by the user. | ||
func (f *File) SetPlainText(text string) error { | ||
_, err := f.Write([]byte(text)) | ||
if err != nil { | ||
return err | ||
} | ||
f.ContentType = "text/plain" | ||
return nil | ||
} | ||
|
||
// Bytes returns the raw bytes of this file. | ||
func (f File) Bytes() []byte { return f.content } | ||
|
||
// Read implements io.Reader. | ||
func (f File) Read(p []byte) (n int, err error) { return copy(p, f.content), io.EOF } | ||
|
||
// Write implements io.Writer. | ||
// It further sets the content type to the output of [http.DetectContentType], | ||
// the file size and the charset, if none of those properties have been set before. | ||
func (f *File) Write(p []byte) (n int, err error) { | ||
// Set a copy of the slice as the content, so we don't keep | ||
// a reference to the original. | ||
f.content = p[:] | ||
if f.Charset == "" { | ||
f.Charset = "utf-8" | ||
} | ||
if f.ContentType == "" { | ||
// If the content type appends the charset, we remove it. | ||
// This happens for content types like "text/plain; charset=utf-8" | ||
f.ContentType, _, _ = strings.Cut(http.DetectContentType(p), "; ") | ||
} | ||
if f.SizeInBytes == 0 { | ||
f.SizeInBytes = int64(len(p)) | ||
} | ||
return len(p), nil | ||
} | ||
|
||
// rawFile describes the structure that we receive from e5e. | ||
// It is just used for internal decoding. | ||
type rawFile struct { | ||
Base64Encoded string `json:"binary"` | ||
Type string `json:"type"` | ||
FileSizeInBytes int64 `json:"size,omitempty"` | ||
Filename string `json:"name,omitempty"` | ||
ContentType string `json:"content_type,omitempty"` | ||
Charset string `json:"charset,omitempty"` | ||
} | ||
|
||
// MarshalJSON implements json.Marshaler. | ||
func (f File) MarshalJSON() ([]byte, error) { | ||
if f.Type == "" { | ||
f.Type = "binary" | ||
} | ||
|
||
file := rawFile{ | ||
Base64Encoded: base64.StdEncoding.EncodeToString(f.content), | ||
Type: f.Type, | ||
FileSizeInBytes: f.SizeInBytes, | ||
Filename: f.Name, | ||
ContentType: f.ContentType, | ||
Charset: f.Charset, | ||
} | ||
return json.Marshal(file) | ||
} | ||
|
||
// UnmarshalJSON implements json.Unmarshaler. | ||
func (f *File) UnmarshalJSON(data []byte) error { | ||
var file rawFile | ||
if err := json.Unmarshal(data, &file); err != nil { | ||
return err | ||
} | ||
|
||
fileBytes, err := base64.StdEncoding.DecodeString(file.Base64Encoded) | ||
if err != nil { | ||
return fmt.Errorf("%q attribute does not contain a valid base64 string: %w", "binary", err) | ||
} | ||
|
||
f.content = fileBytes | ||
f.Type = file.Type | ||
f.SizeInBytes = file.FileSizeInBytes | ||
f.Name = file.Filename | ||
f.ContentType = file.ContentType | ||
f.Charset = file.Charset | ||
return nil | ||
} | ||
|
||
// compile-time check for certain interfaces | ||
var _ io.Reader = File{} | ||
var _ io.Writer = &File{} | ||
var _ json.Unmarshaler = &File{} | ||
var _ json.Marshaler = File{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package e5e_test | ||
|
||
import ( | ||
_ "embed" | ||
"encoding/json" | ||
"io" | ||
"strings" | ||
"testing" | ||
|
||
"go.anx.io/e5e/v2" | ||
) | ||
|
||
//go:embed testdata/binary_request_with_multiple_files.json | ||
var binaryRequestWithMultipleFiles []byte | ||
|
||
func TestFile(t *testing.T) { | ||
t.Parallel() | ||
t.Run("SetText encodes the content properly", func(t *testing.T) { | ||
t.Parallel() | ||
f := &e5e.File{} | ||
_ = f.SetPlainText("Hello world!") | ||
|
||
Equal(t, "utf-8", f.Charset, "Charset does not match") | ||
Equal(t, 12, int(f.SizeInBytes), "file size does not match") | ||
Equal(t, "text/plain", f.ContentType, "content type does not match") | ||
|
||
var encodedBytes = []byte{72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33} | ||
DeepEqual(t, encodedBytes, f.Bytes(), "bytes do not match") | ||
}) | ||
t.Run("JSON serialization matches expectation", func(t *testing.T) { | ||
t.Parallel() | ||
var expect = `{"binary":"SGVsbG8gd29ybGQh","type":"binary","size":12,"content_type":"text/plain","charset":"utf-8"}` | ||
|
||
f := &e5e.File{} | ||
_, err := f.Write([]byte("Hello world!")) | ||
if err != nil { | ||
t.Errorf("expected no write error, got: %v", err) | ||
} | ||
|
||
// One test with the pointer | ||
actual, err := json.Marshal(f) | ||
if err != nil { | ||
t.Errorf("JSON marshalling failed: %v", err) | ||
} | ||
|
||
Equal(t, expect, string(actual), "JSON does not match") | ||
|
||
// And one without it | ||
actual, err = json.Marshal(*f) | ||
if err != nil { | ||
t.Errorf("JSON marshalling failed: %v", err) | ||
} | ||
|
||
Equal(t, expect, string(actual), "JSON does not match") | ||
}) | ||
t.Run("JSON deserialization works", func(t *testing.T) { | ||
t.Parallel() | ||
var expected = e5e.File{ | ||
Type: "binary", | ||
SizeInBytes: 12, | ||
Name: "my-file-1.name", | ||
ContentType: "application/my-content-type-1", | ||
Charset: "utf-8", | ||
} | ||
expected.SetPlainText("Hello world!") | ||
expected.ContentType = "application/my-content-type-1" | ||
|
||
const input = `{ | ||
"binary": "SGVsbG8gd29ybGQh", | ||
"type": "binary", | ||
"name": "my-file-1.name", | ||
"size": 12, | ||
"content_type": "application/my-content-type-1", | ||
"charset": "utf-8" | ||
}` | ||
var actual e5e.File | ||
if err := json.Unmarshal([]byte(input), &actual); err != nil { | ||
t.Errorf("JSON unmarshaling failed: %v", err) | ||
} | ||
DeepEqual(t, expected, actual, "files do not match") | ||
}) | ||
t.Run("original slice is ignored", func(t *testing.T) { | ||
var original = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9} | ||
var modified = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9} | ||
f := &e5e.File{} | ||
n, _ := f.Write(modified) | ||
Equal(t, 9, n, "written bytes do not match") | ||
modified = append(modified, 10) | ||
DeepEqual(t, f.Bytes(), original, "slice got passed by reference") | ||
}) | ||
t.Run("request can be deserialized", func(t *testing.T) { | ||
request := e5e.Request[[]e5e.File, any]{} | ||
if err := json.Unmarshal(binaryRequestWithMultipleFiles, &request); err != nil { | ||
t.Errorf("JSON unmarshaling failed: %v", err) | ||
} | ||
|
||
Equal(t, 2, len(request.Data()), "expected two files") | ||
for _, file := range request.Data() { | ||
Equal(t, "binary", file.Type, "file type does not match") | ||
Equal(t, 12, file.SizeInBytes, "file size does not match") | ||
Equal(t, "utf-8", file.Charset, "charset does not match") | ||
if !strings.HasPrefix(file.ContentType, "application/my-content-type") { | ||
t.Errorf("invalid content type prefix, got: %s", file.ContentType) | ||
} | ||
if !strings.HasPrefix(file.Name, "my-file-") { | ||
t.Errorf("invalid name prefix, got: %s", file.Name) | ||
} | ||
} | ||
}) | ||
t.Run("file can be read", func(t *testing.T) { | ||
t.Parallel() | ||
file := &e5e.File{} | ||
if err := file.SetPlainText("Hello world!"); err != nil { | ||
t.Errorf("setting file content failed: %v", err) | ||
} | ||
|
||
var buf strings.Builder | ||
n, err := io.Copy(&buf, file) | ||
if err != nil { | ||
t.Errorf("copying failed: %v", err) | ||
} | ||
Equal(t, 12, n, "read bytes do not match") | ||
Equal(t, "Hello world!", buf.String(), "file content does not match") | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"context": { | ||
"type": "integration-test", | ||
"async": true, | ||
"date": "2024-01-01T00:00:00Z" | ||
}, | ||
"event": { | ||
"type": "mixed", | ||
"data": [ | ||
{ | ||
"binary": "SGVsbG8gd29ybGQh", | ||
"type": "binary", | ||
"name": "my-file-1.name", | ||
"size": 12, | ||
"content_type": "application/my-content-type-1", | ||
"charset": "utf-8" | ||
}, | ||
{ | ||
"binary": "SGVsbG8gd29ybGQh", | ||
"type": "binary", | ||
"name": "my-file-2.name", | ||
"size": 12, | ||
"content_type": "application/my-content-type-2", | ||
"charset": "utf-8" | ||
} | ||
] | ||
} | ||
} |