Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
9 changes: 9 additions & 0 deletions .github/workflows/test-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ jobs:
with:
working-directory: 'core'

- name: Run tests
run: |
cd core
go test $(go list ./... | grep -v '/pkg/')
go vet ./...
go mod verify
go mod tidy


6 changes: 3 additions & 3 deletions core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ It provides a rich set of features for Go development, including code completion
There is the code template for Go file in GoLand:

```text
package ${GO_PACKAGE_NAME}

// @Title ${FILE_NAME}
// @Description
// @Description #[[$END$]]#
// @Create cunyue ${DATE} ${TIME}

package ${GO_PACKAGE_NAME}
```

## Linter
Expand Down
4 changes: 4 additions & 0 deletions core/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ module github.com/SwanHubX/SwanLab/core
go 1.24.4

require (
github.com/stretchr/testify v1.10.0
golang.org/x/net v0.38.0
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions core/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
Expand All @@ -8,6 +10,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
Expand All @@ -32,3 +38,7 @@ google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2 changes: 1 addition & 1 deletion core/internal/api/api.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package api

// @Title api.go
// @Description SwanLab API intended for requests to the SwanLab-Server.
// @Description SwanLab API intended for requests to the SwanLab Server.
// @Create cunyue 2025/6/10 13:00
156 changes: 156 additions & 0 deletions core/internal/api/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// @Title send.go
// @Description parse the proto message to json
// @Create cunyue 2025/6/12 18:49

package api

import (
"errors"
"strconv"

"github.com/SwanHubX/SwanLab/core/pkg/pb"
)

// Some constants for the parser.
const (
minRangeLen = 2
)

// Parser provides methods to parse proto messages to JSON sent to the SwanLab Server.
type Parser struct {
}

// convertRange will convert the YRange from a proto message (like ["0", "None"]) to a JSON-compatible format(like [0, null]).
// We expect the input to be a slice of strings with 2 elements, any other length will return an empty slice.
func (p *Parser) convertRange(r []string) []interface{} {
var left, right interface{} = nil, nil
// must be at least 2 elements
if len(r) != minRangeLen {
return []interface{}{}
}
if val, err := strconv.Atoi(r[0]); err == nil {
left = val
}
if val, err := strconv.Atoi(r[1]); err == nil {
right = val
}
return []interface{}{left, right}
}

// validateColumnClass will validate the columnClass.
func (p *Parser) validateColumnClass(record *pb.ColumnRecord) (string, error) {
var class string
switch record.GetColumnClass() {
case pb.ColumnRecord_COL_CUSTOM:
class = "CUSTOM"
case pb.ColumnRecord_COL_SYSTEM:
class = "SYSTEM"
default:
return "", errors.New("invalid column class: " + record.GetColumnClass().String())
}
return class, nil
}

// validateColumnType will validate the columnType.
func (p *Parser) validateColumnType(record *pb.ColumnRecord) (string, error) {
switch record.GetColumnType() {
case pb.ColumnRecord_COL_FLOAT:
return "FLOAT", nil
case pb.ColumnRecord_COL_IMAGE:
return "IMAGE", nil
case pb.ColumnRecord_COL_AUDIO:
return "AUDIO", nil
case pb.ColumnRecord_COL_TEXT:
return "TEXT", nil
case pb.ColumnRecord_COL_OBJECT3D:
return "OBJECT3D", nil
case pb.ColumnRecord_COL_MOLECULE:
return "MOLECULE", nil
case pb.ColumnRecord_COL_ECHARTS:
return "ECHARTS", nil
default:
return "", errors.New("invalid column type: " + record.GetColumnType().String())
}
}

// validateSectionType will validate the sectionType.
func (p *Parser) validateSectionType(record *pb.ColumnRecord) (string, error) {
var sectionType string
switch record.GetSectionType() {
case pb.ColumnRecord_SEC_CUSTOM:
sectionType = "CUSTOM"
case pb.ColumnRecord_SEC_SYSTEM:
sectionType = "SYSTEM"
case pb.ColumnRecord_SEC_PINNED:
sectionType = "PINNED"
case pb.ColumnRecord_SEC_HIDDEN:
sectionType = "HIDDEN"
case pb.ColumnRecord_SEC_PUBLIC:
sectionType = "PUBLIC"
default:
return "", errors.New("invalid section type: " + record.GetSectionType().String())
}
return sectionType, nil
}

type ColumnDTO struct {
Class string `json:"class"`
Type string `json:"type"`
Key string `json:"key"`
Name string `json:"name,omitempty"`
Error interface{} `json:"error,omitempty"`
SectionName string `json:"section_name,omitempty"`
SectionType string `json:"section_type,omitempty"`
// allow [0, 100] or [0, null] for y_range
YRange []interface{} `json:"y_range,omitempty"`
ChartIndex string `json:"chart_index,omitempty"`
ChartName string `json:"chart_name,omitempty"`
MetricName string `json:"metric_name,omitempty"`
MetricColor []string `json:"metric_color,omitempty"`
}

// ParseColumnRecord parses a ColumnRecord proto message into a map[string]interface{} for JSON serialization.
// Attention: y_range should be serialized to [0, null] in JSON, not ["0", "None"].
func (p *Parser) ParseColumnRecord(record *pb.ColumnRecord) (ColumnDTO, error) {
// 1. parse the y_range, if error, yRange will be nil
yRange := p.convertRange(record.GetChartYRange())
// 2. parse the enum type
// 2.1 column class
class, err := p.validateColumnClass(record)
if err != nil {
return ColumnDTO{}, err
}
// 2.2 column type
columnType, err := p.validateColumnType(record)
if err != nil {
return ColumnDTO{}, err
}
// 2.3 section type
sectionType, err := p.validateSectionType(record)
if err != nil {
return ColumnDTO{}, err
}
// 4. check if the key is empty, if so, return an error
key := record.GetColumnKey()
if key == "" {
return ColumnDTO{}, errors.New("column key cannot be EMPTY")
}
// 5. check the metric color length, if it is not empty, it must be 2 elements
if len(record.GetMetricColor()) > 0 && len(record.GetMetricColor()) != 2 {
return ColumnDTO{}, errors.New("metric color must be empty or have exactly 2 elements")
}
return ColumnDTO{
Class: class,
Type: columnType,
Key: record.GetColumnKey(),
Name: record.GetColumnName(),
Error: record.GetColumnError(),
SectionName: record.GetSectionName(),
SectionType: sectionType,
YRange: yRange,
ChartIndex: record.GetChartIndex(),
ChartName: record.GetChartName(),
MetricName: record.GetMetricName(),
MetricColor: record.GetMetricColor(),
}, nil
}
115 changes: 115 additions & 0 deletions core/internal/api/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// @Title parse_test.go
// @Description test the parse function
// @Create cunyue 2025/6/12 18:50

package api_test

import (
"encoding/json"
"math/rand"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/SwanHubX/SwanLab/core/internal/api"
"github.com/SwanHubX/SwanLab/core/pkg/pb"
)

// TestRangeSerialization tests the ParseColumnRecord function of the Parser.
// Just a simple test to ensure that the yRange can be parsed correctly.
func TestRangeSerialization(t *testing.T) {
// Define a test case with a valid yRange
tests := []struct {
name string
input []string
expected []interface{}
yRangeStr string
}{
{
name: "Valid yRange",
input: []string{"1", "2"},
expected: []interface{}{1, 2},
yRangeStr: "[1,2]",
},
{
name: "Too many values in yRange",
input: []string{"1", "2", "3"},
expected: []interface{}{},
yRangeStr: "[]",
},
{
name: "Left is none",
input: []string{"None", "2"},
expected: []interface{}{nil, 2},
yRangeStr: "[null,2]",
},
{
name: "Right is none",
input: []string{"1", "None"},
expected: []interface{}{1, nil},
yRangeStr: "[1,null]",
},
{
name: "Invalid yRange",
input: []string{"x", "invalid"},
expected: []interface{}{nil, nil},
},
}
parser := api.Parser{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
record := &pb.ColumnRecord{
ColumnKey: "test-key",
ColumnClass: pb.ColumnRecord_COL_CUSTOM,
ColumnType: pb.ColumnRecord_COL_ECHARTS,
SectionName: "",
SectionType: pb.ColumnRecord_SEC_PUBLIC,
ChartName: "test-chart-name",
ChartYRange: tt.input,
ChartIndex: "test-chart-index" + string(rune(rand.Intn(1000))),
MetricName: "test-metric-name",
MetricColor: []string{"#FF0000", "#0000"},
}
// Call the ParseColumnRecord function with the test input
result, err := parser.ParseColumnRecord(record)
if err != nil {
t.Errorf("Parse error: %s", err)
return
}
// Check base fields
assert.Equal(t, record.GetColumnKey(), result.Key)
assert.Empty(t, result.Name)
assert.Equal(t, "CUSTOM", result.Class)
assert.Equal(t, "ECHARTS", result.Type)
assert.Empty(t, result.SectionName)
assert.Equal(t, "PUBLIC", result.SectionType)
assert.Equal(t, record.GetChartName(), result.ChartName)
assert.Equal(t, record.GetChartIndex(), result.ChartIndex)
assert.Equal(t, record.GetMetricName(), result.MetricName)
assert.Equal(t, record.GetMetricColor()[0], result.MetricColor[0])
assert.Equal(t, record.GetMetricColor()[1], result.MetricColor[1])
// Check if the yRange is parsed correctly
if len(result.YRange) != len(tt.expected) {
t.Errorf("Expected yRange length %d, got %d", len(tt.expected), len(result.YRange))
}
// check if the yRange values are equal
if len(tt.expected) == 2 {
assert.Equal(t, tt.expected[0], result.YRange[0], "Left value mismatch")
assert.Equal(t, tt.expected[1], result.YRange[1], "Right value mismatch")
} else {
assert.Empty(t, result.YRange, "Expected empty yRange for invalid input")
}
// JSON serialization, check if it can be serialized without error
// Also check the yRange is serialized correctly
jsonData, err := json.Marshal(result)
require.NoError(t, err)
jsonStr := string(jsonData)
if len(tt.expected) == 2 {
require.Contains(t, jsonStr, tt.yRangeStr, "Expected yRange to be serialized correctly")
} else {
require.NotContains(t, "y_range", jsonStr, "Expected yRange to be empty for invalid input")
}
})
}
}
Loading
Loading