feat: Add sheet name validation to Excel module

- Implement validateSheetName function to check for invalid characters, empty names, and length restrictions for sheet names.
- Integrate validation into CreateSheet, UpdateSheet, and CopySheet methods to ensure compliance before proceeding with operations.
- Enhance WriteAll method to create a new sheet if it doesn't exist, improving robustness in sheet handling.
This commit is contained in:
Max 2025-03-29 17:10:34 +08:00
parent 6e4b7cd59e
commit efb588bbd1
3 changed files with 221 additions and 4 deletions

View file

@ -2,6 +2,7 @@ package excel
import (
"fmt"
"strings"
"github.com/xuri/excelize/v2"
)
@ -18,9 +19,31 @@ func New() (*Excel, error) {
}, nil
}
// validateSheetName checks if the sheet name contains invalid characters
func (excel *Excel) validateSheetName(name string) error {
invalidChars := []string{":", "\\", "/", "?", "*", "[", "]"}
for _, char := range invalidChars {
if strings.Contains(name, char) {
return fmt.Errorf("sheet name cannot contain any of these characters: :/?*[\\]")
}
}
if len(name) == 0 {
return fmt.Errorf("sheet name cannot be empty")
}
if len(name) > 31 {
return fmt.Errorf("sheet name cannot be longer than 31 characters")
}
return nil
}
// CreateSheet creates a new sheet with the given name
// Returns the index of the new sheet and any error encountered
func (excel *Excel) CreateSheet(name string) (int, error) {
// Validate sheet name
if err := excel.validateSheetName(name); err != nil {
return 0, err
}
// Check if sheet already exists
if idx, _ := excel.GetSheetIndex(name); idx != -1 {
return 0, fmt.Errorf("sheet %s already exists", name)
@ -56,6 +79,11 @@ func (excel *Excel) ReadSheet(name string) ([][]interface{}, error) {
// UpdateSheet updates an existing sheet with new data
// If the sheet doesn't exist, it will be created
func (excel *Excel) UpdateSheet(name string, data [][]interface{}) error {
// Validate sheet name
if err := excel.validateSheetName(name); err != nil {
return err
}
// Ensure sheet exists
_, err := excel.SetSheet(name)
if err != nil {
@ -95,6 +123,11 @@ func (excel *Excel) ListSheets() []string {
// CopySheet copies a sheet to a new name
func (excel *Excel) CopySheet(source, destination string) error {
// Validate destination sheet name
if err := excel.validateSheetName(destination); err != nil {
return err
}
// Check if source exists
if idx, _ := excel.GetSheetIndex(source); idx == -1 {
return fmt.Errorf("source sheet %s does not exist", source)

View file

@ -38,18 +38,38 @@ func (excel *Excel) WriteColumn(sheet string, cell string, value []interface{})
// WriteAll write all the sheet
func (excel *Excel) WriteAll(sheet string, cell string, rows [][]interface{}) error {
_, err := excel.SetSheet(sheet)
// Check if sheet exists
idx, err := excel.GetSheetIndex(sheet)
if err != nil {
return err
}
if idx == -1 {
// Create new sheet if it doesn't exist
idx, err = excel.NewSheet(sheet)
if err != nil {
return err
}
}
// If no data to write, return
if len(rows) == 0 {
return nil
}
// Write each row
currentCell := cell
for _, row := range rows {
if err := excel.SetSheetRow(sheet, cell, &row); err != nil {
if err := excel.SetSheetRow(sheet, currentCell, &row); err != nil {
return err
}
colIndex, rowIndex, err := excelize.CellNameToCoordinates(cell)
cell, err = excelize.CoordinatesToCellName(colIndex, rowIndex+1)
// Move to next row
colIndex, rowIndex, err := excelize.CellNameToCoordinates(currentCell)
if err != nil {
return err
}
currentCell, err = excelize.CoordinatesToCellName(colIndex, rowIndex+1)
if err != nil {
return err
}

164
excel/write_test.go Normal file
View file

@ -0,0 +1,164 @@
package excel
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/xuri/excelize/v2"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
)
func TestWriteAll(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// Create a new Excel file
xls := excelize.NewFile()
defer func() {
if err := xls.Close(); err != nil {
t.Error(err)
}
}()
// Create Excel instance
excel := &Excel{
File: xls,
abs: "test.xlsx",
}
t.Run("Write to default sheet", func(t *testing.T) {
data := [][]interface{}{
{"Header1", "Header2", "Header3"},
{1, "Data1", true},
{2, "Data2", false},
}
err := excel.WriteAll("Sheet1", "A1", data)
assert.NoError(t, err)
// Verify data was written
rows, err := excel.GetRows("Sheet1")
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(rows), 3)
assert.Equal(t, "Header1", rows[0][0])
assert.Equal(t, "Header2", rows[0][1])
assert.Equal(t, "Header3", rows[0][2])
})
t.Run("Write to new sheet", func(t *testing.T) {
data := [][]interface{}{
{"Name", "Age", "Active"},
{"John", 30, true},
{"Jane", 25, false},
}
// Verify sheet doesn't exist before writing
sheets := excel.ListSheets()
assert.NotContains(t, sheets, "NewSheet")
err := excel.WriteAll("NewSheet", "B2", data)
assert.NoError(t, err)
// Verify sheet was created
sheets = excel.ListSheets()
assert.Contains(t, sheets, "NewSheet")
// Verify data was written
rows, err := excel.GetRows("NewSheet")
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(rows), 4) // Account for B2 start position
assert.Equal(t, "Name", rows[1][1]) // B2 position
assert.Equal(t, "Age", rows[1][2])
assert.Equal(t, "Active", rows[1][3])
})
t.Run("Write empty data to new sheet", func(t *testing.T) {
var data [][]interface{}
// Verify sheet doesn't exist before writing
sheets := excel.ListSheets()
assert.NotContains(t, sheets, "EmptySheet")
err := excel.WriteAll("EmptySheet", "A1", data)
assert.NoError(t, err)
// Verify sheet was created but is empty
sheets = excel.ListSheets()
assert.Contains(t, sheets, "EmptySheet")
rows, err := excel.GetRows("EmptySheet")
assert.NoError(t, err)
assert.Empty(t, rows)
})
t.Run("Write empty data to existing sheet", func(t *testing.T) {
// First write some data
data := [][]interface{}{
{"Test"},
}
err := excel.WriteAll("ExistingSheet", "A1", data)
assert.NoError(t, err)
// Then write empty data
var emptyData [][]interface{}
err = excel.WriteAll("ExistingSheet", "A1", emptyData)
assert.NoError(t, err)
// Verify original data remains
rows, err := excel.GetRows("ExistingSheet")
assert.NoError(t, err)
assert.NotEmpty(t, rows)
assert.Equal(t, "Test", rows[0][0])
})
t.Run("Write with invalid cell reference", func(t *testing.T) {
data := [][]interface{}{
{"Test"},
}
err := excel.WriteAll("InvalidCell", "INVALID", data)
assert.Error(t, err)
// Verify sheet was still created despite error
sheets := excel.ListSheets()
assert.Contains(t, sheets, "InvalidCell")
})
t.Run("Write to sheet with special characters", func(t *testing.T) {
// Valid sheet name with allowed special characters
data := [][]interface{}{
{"Special"},
}
err := excel.WriteAll("Sheet-123_中文", "A1", data)
assert.NoError(t, err)
// Verify sheet was created and data written
sheets := excel.ListSheets()
assert.Contains(t, sheets, "Sheet-123_中文")
rows, err := excel.GetRows("Sheet-123_中文")
assert.NoError(t, err)
assert.Equal(t, "Special", rows[0][0])
// Invalid sheet names
invalidNames := []string{
"Sheet:1",
"Sheet/2",
"Sheet\\3",
"Sheet?4",
"Sheet*5",
"Sheet[6]",
"", // Empty name
"ThisSheetNameIsWayTooLongAndShouldFailBecauseExcelHasALimitOf31Characters", // Too long
}
for _, name := range invalidNames {
err := excel.WriteAll(name, "A1", data)
assert.Error(t, err, "Should fail for invalid sheet name: %s", name)
}
})
// Optional: Save the file for manual inspection
// err := excel.SaveAs("test_output.xlsx")
// assert.NoError(t, err)
}