diff --git a/plugin/wshub/server.go b/plugin/wshub/server.go index 9ec3f08..853655c 100644 --- a/plugin/wshub/server.go +++ b/plugin/wshub/server.go @@ -120,7 +120,7 @@ func (s *wsServer) handleWsInfo(w http.ResponseWriter, r *http.Request) { if data.EventID == "" { continue } - eventCacheData, _ := json.Marshal(data) + eventCacheData, _ := toCapitalizedJSON(data) err := client.conn.WriteMessage(websocket.TextMessage, eventCacheData) if err != nil { s.log.Warn("write message failed", err) diff --git a/plugin/wshub/utils.go b/plugin/wshub/utils.go new file mode 100644 index 0000000..5e85d5e --- /dev/null +++ b/plugin/wshub/utils.go @@ -0,0 +1,70 @@ +package wshub + +import ( + "encoding/json" + "fmt" + "unicode" +) + +// capitalize is a helper function to safely capitalize the first letter of a string. +// It's robust against empty strings. +func capitalize(s string) string { + if s == "" { + return "" + } + r := []rune(s) + r[0] = unicode.ToUpper(r[0]) + return string(r) +} + +// capitalizeKeys recursively traverses an interface{} and capitalizes the keys of any maps it finds. +func capitalizeKeys(data interface{}) interface{} { + // Use a type switch to handle the different types of data we might encounter. + switch value := data.(type) { + // If it's a map, we iterate over its keys and values. + case map[string]interface{}: + newMap := make(map[string]interface{}) + for k, v := range value { + // Capitalize the key and recursively process the value. + newMap[capitalize(k)] = capitalizeKeys(v) + } + return newMap + + // If it's a slice, we iterate over its elements. + case []interface{}: + // The slice itself doesn't have keys, but its elements might. + newSlice := make([]interface{}, len(value)) + for i, v := range value { + // Recursively process each element in the slice. + newSlice[i] = capitalizeKeys(v) + } + return newSlice + + // For any other type (string, int, bool, etc.), return it as is. + default: + return data + } +} + +// toCapitalizedJSON marshals any data structure (including structs) to a JSON string +// with all keys having their first letter capitalized. +func toCapitalizedJSON(payload interface{}) ([]byte, error) { + // Step 1: Marshal the data to JSON. This respects the `json` tags on any structs. + tempJSON, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to perform initial marshal: %w", err) + } + + // Step 2: Unmarshal the JSON into a generic interface{}. + // This converts all JSON objects into map[string]interface{}, regardless of the original type. + var genericData interface{} + if err := json.Unmarshal(tempJSON, &genericData); err != nil { + return nil, fmt.Errorf("failed to unmarshal into generic interface: %w", err) + } + + // Step 3: Recursively capitalize the keys of the generic data structure. + capitalizedData := capitalizeKeys(genericData) + + // Step 4: Marshal the final, capitalized data structure back to JSON. + return json.MarshalIndent(capitalizedData, "", " ") +} diff --git a/plugin/wshub/utils_test.go b/plugin/wshub/utils_test.go new file mode 100644 index 0000000..11b7135 --- /dev/null +++ b/plugin/wshub/utils_test.go @@ -0,0 +1,164 @@ +package wshub + +import ( + "encoding/json" + "reflect" + "testing" +) + +// --- Example struct that might be used in the Data field --- +type UserDetails struct { + UserIdentifier int `json:"userIdentifier"` + EmailAddress string `json:"emailAddress"` + IsActive bool `json:"isActive"` + Metadata map[string]interface{} `json:"metadata"` + Tags []string `json:"tags"` +} + +func TestToCapitalizedJSON(t *testing.T) { + // Define a struct for our table-driven tests + testCases := []struct { + name string // Name of the test case + input interface{} // Input to the function + expectedJSON string // The expected JSON output string + expectError bool // Whether we expect an error + }{ + { + name: "Simple Struct", + input: UserDetails{ + UserIdentifier: 101, + EmailAddress: "test@example.com", + IsActive: true, + }, + expectedJSON: `{ + "UserIdentifier": 101, + "EmailAddress": "test@example.com", + "IsActive": true, + "Metadata": null, + "Tags": null + }`, + }, + { + name: "Struct with Nested Map", + input: UserDetails{ + UserIdentifier: 102, + EmailAddress: "another@example.com", + IsActive: false, + Metadata: map[string]interface{}{ + "lastLogin": "2024-01-01T12:00:00Z", + "loginCount": 5, + }, + Tags: []string{"beta", "tester"}, + }, + expectedJSON: `{ + "UserIdentifier": 102, + "EmailAddress": "another@example.com", + "IsActive": false, + "Metadata": { + "LastLogin": "2024-01-01T12:00:00Z", + "LoginCount": 5 + }, + "Tags": ["beta", "tester"] + }`, + }, + { + name: "Simple Map", + input: map[string]interface{}{ + "firstName": "John", + "lastName": "Doe", + }, + expectedJSON: `{ + "FirstName": "John", + "LastName": "Doe" + }`, + }, + { + name: "Nested Map and Slice", + input: map[string]interface{}{ + "event": "user.created", + "payload": map[string]interface{}{ + "userName": "jdoe", + "roles": []interface{}{ + "editor", + map[string]interface{}{"permissionLevel": 4}, + }, + }, + }, + expectedJSON: `{ + "Event": "user.created", + "Payload": { + "UserName": "jdoe", + "Roles": [ + "editor", + { + "PermissionLevel": 4 + } + ] + } + }`, + }, + { + name: "Top-level Slice with Structs", + input: []UserDetails{ + {UserIdentifier: 201, EmailAddress: "user1@test.com"}, + {UserIdentifier: 202, EmailAddress: "user2@test.com"}, + }, + expectedJSON: `[ + { + "UserIdentifier": 201, "EmailAddress": "user1@test.com", "IsActive": false, "Metadata": null, "Tags": null + }, + { + "UserIdentifier": 202, "EmailAddress": "user2@test.com", "IsActive": false, "Metadata": null, "Tags": null + } + ]`, + }, + { + name: "Nil Input", + input: nil, + expectedJSON: `null`, + }, + { + name: "Empty map", + input: map[string]interface{}{}, + expectedJSON: `{}`, + }, + } + + // --- Test Runner --- + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Call the function we are testing + actualBytes, err := toCapitalizedJSON(tc.input) + + // Check for an unexpected error + if !tc.expectError && err != nil { + t.Fatalf("ToCapitalizedJSON() returned an unexpected error: %v", err) + } + // Check for an expected error that did not occur + if tc.expectError && err == nil { + t.Fatalf("ToCapitalizedJSON() was expected to return an error, but it did not") + } + + // To reliably compare JSON, we unmarshal both the actual and expected + // results into a generic interface{} and use reflect.DeepEqual. + // This avoids issues with whitespace and key ordering. + var actualResult interface{} + if err := json.Unmarshal(actualBytes, &actualResult); err != nil { + t.Fatalf("Failed to unmarshal actual result: %v", err) + } + + var expectedResult interface{} + if err := json.Unmarshal([]byte(tc.expectedJSON), &expectedResult); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %v", err) + } + + // Compare the results + if !reflect.DeepEqual(actualResult, expectedResult) { + // Use MarshalIndent to get a pretty-printed version for easier comparison + prettyActual, _ := json.MarshalIndent(actualResult, "", " ") + prettyExpected, _ := json.MarshalIndent(expectedResult, "", " ") + t.Errorf("Result does not match expected.\nGot:\n%s\n\nWant:\n%s", string(prettyActual), string(prettyExpected)) + } + }) + } +} diff --git a/plugin/wshub/wshub.go b/plugin/wshub/wshub.go index cada6d9..a8a0e21 100644 --- a/plugin/wshub/wshub.go +++ b/plugin/wshub/wshub.go @@ -10,7 +10,6 @@ import ( "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" "AynaLivePlayer/pkg/logger" - "encoding/json" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" @@ -183,7 +182,7 @@ func (w *WsHub) registerEvents() { EventID: e.Id, Data: e.Data, } - val, err := json.Marshal(ed) + val, err := toCapitalizedJSON(ed) if err != nil { w.log.Errorf("failed to marshal event data %v", err) return diff --git a/todo.txt b/todo.txt index 1af3ce9..c55f4a5 100644 --- a/todo.txt +++ b/todo.txt @@ -16,6 +16,7 @@ ---- Finished +- 2024.07.24 : 修复网易云,修复wshub大小写问题 - 2024.07.07 : QQ音乐 - 2024.06.30 : 添加vlc核心,修复若干bug,gui框架更新,修复点歌限制为1时可能出现的无法点歌的问题 - 2024.05.27 : 修复web弹幕获取到0个host的时候闪退的问题