feat(plugins): change websockets Data field type to []byte for binary support
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -38,7 +38,7 @@ type OnBinaryMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
// OnErrorRequest is the request provided when an error occurs on a WebSocket connection.
|
||||
|
||||
@@ -30,6 +30,7 @@ components:
|
||||
description: ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
data:
|
||||
type: string
|
||||
format: byte
|
||||
description: Data is the binary data received from the WebSocket, encoded as base64.
|
||||
required:
|
||||
- connectionId
|
||||
|
||||
@@ -246,6 +246,12 @@ func buildProperty(field FieldDef, knownTypes map[string]bool) xtpProperty {
|
||||
return prop
|
||||
}
|
||||
|
||||
// Handle primitive types (including []byte which maps to string/byte, not array)
|
||||
if isPrimitiveGoType(goType) {
|
||||
prop.Type, prop.Format = goTypeToXTPTypeAndFormat(goType)
|
||||
return prop
|
||||
}
|
||||
|
||||
// Handle slice types
|
||||
if strings.HasPrefix(goType, "[]") {
|
||||
elemType := goType[2:]
|
||||
@@ -259,7 +265,7 @@ func buildProperty(field FieldDef, knownTypes map[string]bool) xtpProperty {
|
||||
return prop
|
||||
}
|
||||
|
||||
// Handle primitive types
|
||||
// Handle remaining types
|
||||
prop.Type, prop.Format = goTypeToXTPTypeAndFormat(goType)
|
||||
return prop
|
||||
}
|
||||
|
||||
@@ -303,6 +303,45 @@ var _ = Describe("XTP Schema Generation", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Context("capability with []byte field", func() {
|
||||
It("should map []byte to string with byte format, not array", func() {
|
||||
capability := Capability{
|
||||
Name: "byte_test",
|
||||
SourceFile: "byte_test",
|
||||
Methods: []Export{
|
||||
{ExportName: "test", Input: NewParam("input", "Input"), Output: NewParam("output", "Output")},
|
||||
},
|
||||
Structs: []StructDef{
|
||||
{
|
||||
Name: "Input",
|
||||
Fields: []FieldDef{
|
||||
{Name: "Data", Type: "[]byte", JSONTag: "data"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Output",
|
||||
Fields: []FieldDef{
|
||||
{Name: "Value", Type: "string", JSONTag: "value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
schema, err := GenerateSchema(capability)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ValidateXTPSchema(schema)).To(Succeed())
|
||||
|
||||
doc := parseSchema(schema)
|
||||
components := doc["components"].(map[string]any)
|
||||
schemas := components["schemas"].(map[string]any)
|
||||
input := schemas["Input"].(map[string]any)
|
||||
props := input["properties"].(map[string]any)
|
||||
data := props["data"].(map[string]any)
|
||||
Expect(data["type"]).To(Equal("string"))
|
||||
Expect(data["format"]).To(Equal("byte"))
|
||||
Expect(data).NotTo(HaveKey("items"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("capability with nullable ref", func() {
|
||||
It("should mark pointer to enum as nullable with $ref", func() {
|
||||
capability := Capability{
|
||||
|
||||
@@ -2,7 +2,6 @@ package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
@@ -355,7 +354,7 @@ func (s *webSocketServiceImpl) invokeOnTextMessage(ctx context.Context, connecti
|
||||
func (s *webSocketServiceImpl) invokeOnBinaryMessage(ctx context.Context, connectionID string, data []byte) {
|
||||
invokeWebSocketCallback(ctx, s, FuncWebSocketOnBinaryMessage, capabilities.OnBinaryMessageRequest{
|
||||
ConnectionID: connectionID,
|
||||
Data: base64.StdEncoding.EncodeToString(data),
|
||||
Data: data,
|
||||
}, "binary message", connectionID)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ package plugins
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
|
||||
"encoding/hex"
|
||||
"maps"
|
||||
"net/http"
|
||||
@@ -294,11 +294,13 @@ var _ = Describe("WebSocketService", Ordered, func() {
|
||||
var wsServer *httptest.Server
|
||||
var serverConn *websocket.Conn
|
||||
var serverMessages []string
|
||||
var serverBinaryMessages [][]byte
|
||||
var serverMu sync.Mutex
|
||||
|
||||
BeforeEach(func() {
|
||||
serverConn = nil
|
||||
serverMessages = nil
|
||||
serverBinaryMessages = nil
|
||||
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
@@ -314,12 +316,16 @@ var _ = Describe("WebSocketService", Ordered, func() {
|
||||
|
||||
// Read and store messages
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
msgType, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
serverMu.Lock()
|
||||
serverMessages = append(serverMessages, string(msg))
|
||||
if msgType == websocket.BinaryMessage {
|
||||
serverBinaryMessages = append(serverBinaryMessages, msg)
|
||||
} else {
|
||||
serverMessages = append(serverMessages, string(msg))
|
||||
}
|
||||
serverMu.Unlock()
|
||||
}
|
||||
}))
|
||||
@@ -359,13 +365,12 @@ var _ = Describe("WebSocketService", Ordered, func() {
|
||||
serverMu.Unlock()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Plugin echoes binary data back as text prefixed with "binary_echo:"
|
||||
expectedEcho := "binary_echo:" + base64.StdEncoding.EncodeToString(binaryData)
|
||||
Eventually(func() []string {
|
||||
// Plugin echoes binary data back as a binary message
|
||||
Eventually(func() [][]byte {
|
||||
serverMu.Lock()
|
||||
defer serverMu.Unlock()
|
||||
return serverMessages
|
||||
}).Should(ContainElement(expectedEcho))
|
||||
return serverBinaryMessages
|
||||
}).Should(ContainElement(binaryData))
|
||||
})
|
||||
|
||||
It("should invoke OnClose callback when server closes connection", func() {
|
||||
@@ -609,6 +614,3 @@ func findWebSocketService(m *Manager, pluginName string) *webSocketServiceImpl {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure base64 import is used
|
||||
var _ = base64.StdEncoding
|
||||
|
||||
@@ -16,7 +16,7 @@ type OnBinaryMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
// OnCloseRequest is the request provided when a WebSocket connection is closed.
|
||||
|
||||
@@ -13,7 +13,7 @@ type OnBinaryMessageRequest struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
// OnCloseRequest is the request provided when a WebSocket connection is closed.
|
||||
|
||||
@@ -4,6 +4,29 @@
|
||||
// It is intended for use in Navidrome plugins built with extism-pdk.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
|
||||
mod base64_bytes {
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
|
||||
pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&BASE64.encode(bytes))
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
BASE64.decode(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for skip_serializing_if with numeric types
|
||||
#[allow(dead_code)]
|
||||
@@ -27,7 +50,8 @@ pub struct OnBinaryMessageRequest {
|
||||
pub connection_id: String,
|
||||
/// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
#[serde(default)]
|
||||
pub data: String,
|
||||
#[serde(with = "base64_bytes")]
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
/// OnCloseRequest is the request provided when a WebSocket connection is closed.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
|
||||
+5
-4
@@ -3,6 +3,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/host"
|
||||
@@ -45,11 +46,11 @@ func (t *testWebSocket) OnTextMessage(input websocket.OnTextMessageRequest) erro
|
||||
}
|
||||
|
||||
// OnBinaryMessage is called when a binary message is received.
|
||||
// Echoes the data back as a text message prefixed with "binary_echo:" so tests
|
||||
// can observe the callback fired.
|
||||
// Echoes the data back as a binary message so tests can observe the callback fired.
|
||||
func (t *testWebSocket) OnBinaryMessage(input websocket.OnBinaryMessageRequest) error {
|
||||
storeReceivedMessage("binary:" + input.Data)
|
||||
return host.WebSocketSendText(input.ConnectionID, "binary_echo:"+input.Data)
|
||||
encoded := base64.StdEncoding.EncodeToString(input.Data)
|
||||
storeReceivedMessage("binary:" + encoded)
|
||||
return host.WebSocketSendBinary(input.ConnectionID, input.Data)
|
||||
}
|
||||
|
||||
// OnError is called when an error occurs on a WebSocket connection.
|
||||
|
||||
Reference in New Issue
Block a user