Repository: tableauio/tableau (go)

Issue:
protogen: support multiple struct type definitions in a sheet

Pull request:
protogen: support multiple struct type definitions in a sheet
- close #173

Patch:
diff --git a/internal/importer/book/table.go b/internal/importer/book/table.go
index 6a1db5c..6c1abb7 100644
--- a/internal/importer/book/table.go
+++ b/internal/importer/book/table.go
@@ -61,7 +61,8 @@ func (t *Table) IsRowEmpty(row int) bool {
 // it will just return the start row. Otherwise, it will return the last
 // none-empty row.
 //
-// NOTE: A block is a series of contiguous none-empty rows.
+// NOTE: A block is a series of contiguous none-empty rows. So different blocks
+// are seperated by one or more empty rows.
 func (t *Table) FindBlockEndRow(startRow int) int {
 	for row := startRow; row <= len(t.Rows); row++ {
 		if t.IsRowEmpty(row) {
@@ -76,7 +77,8 @@ func (t *Table) FindBlockEndRow(startRow int) int {
 
 // ExtractBlock extracts a block of rows.
 //
-// NOTE: A block is a series of contiguous none-empty rows.
+// NOTE: A block is a series of contiguous none-empty rows. So different blocks
+// are seperated by one or more empty rows.
 func (t *Table) ExtractBlock(startRow int) (rows [][]string, endRow int) {
 	endRow = t.FindBlockEndRow(startRow)
 	rows = t.Rows[startRow : endRow+1]
diff --git a/internal/protogen/exporter.go b/internal/protogen/exporter.go
index 24f00b9..fc22395 100644
--- a/internal/protogen/exporter.go
+++ b/internal/protogen/exporter.go
@@ -151,7 +151,7 @@ func (x *sheetExporter) export() error {
 		return x.exportMessager()
 	case tableaupb.Mode_MODE_ENUM_TYPE, tableaupb.Mode_MODE_ENUM_TYPE_MULTI:
 		return x.exportEnum()
-	case tableaupb.Mode_MODE_STRUCT_TYPE:
+	case tableaupb.Mode_MODE_STRUCT_TYPE, tableaupb.Mode_MODE_STRUCT_TYPE_MULTI:
 		return x.exportStruct()
 	case tableaupb.Mode_MODE_UNION_TYPE:
 		return x.exportUnion()
@@ -186,6 +186,13 @@ func (x *sheetExporter) exportEnum() error {
 func (x *sheetExporter) exportStruct() error {
 	x.g.P("// Generated from sheet: ", x.ws.GetOptions().GetName(), ".")
 	x.g.P("message ", x.ws.Name, " {")
+	// 	TODO: support worksheet options, but should not be treated as a
+	//  standard config message, as this is a predefined message.
+	// if x.ws.Alias != "" {
+	// 	opts := &tableaupb.WorkbookOptions{Name: x.ws.Alias}
+	// 	x.g.P("  option (tableau.worksheet) = {", marshalToText(opts), "};")
+	// 	x.g.P("")
+	// }
 	// generate the fields
 	depth := 1
 	for i, field := range x.ws.Fields {
diff --git a/internal/protogen/protogen.go b/internal/protogen/protogen.go
index b3ee79a..bde5f06 100644
--- a/internal/protogen/protogen.go
+++ b/internal/protogen/protogen.go
@@ -519,14 +519,14 @@ func (gen *Generator) extractTypeInfoFromSpecialSheetMode(mode tableaupb.Mode, s
 	case tableaupb.Mode_MODE_ENUM_TYPE_MULTI:
 		for row := 0; row <= sheet.Table.MaxRow; row++ {
 			cols := sheet.Table.GetRow(row)
-			if isEnumTypeDefinitionBlockHeader(cols) {
+			if isEnumTypeBlockHeader(cols) {
 				if row < 1 {
 					continue
 				}
 				typeRow := sheet.Table.GetRow(row - 1)
 				typeName, _, _, err := extractEnumTypeRow(typeRow)
 				if err != nil {
-					return xerrors.Wrapf(err, "failed to parse enum type block at row: %d, sheet: %s", row, sheet.Name)
+					return xerrors.Wrapf(err, "failed to parse enum type block, sheet: %s, row: %d", sheet.Name, row)
 				}
 				// add type info
 				info := &xproto.TypeInfo{
@@ -554,7 +554,40 @@ func (gen *Generator) extractTypeInfoFromSpecialSheetMode(mode tableaupb.Mode, s
 			FirstFieldOptionName: firstFieldOptionName,
 		}
 		gen.typeInfos.Put(info)
-
+	case tableaupb.Mode_MODE_STRUCT_TYPE_MULTI:
+		for row := 0; row <= sheet.Table.MaxRow; row++ {
+			cols := sheet.Table.GetRow(row)
+			if isStructTypeBlockHeader(cols) {
+				if row < 1 {
+					continue
+				}
+				typeRow := sheet.Table.GetRow(row - 1)
+				typeName, _, _, err := extractStructTypeRow(typeRow)
+				if err != nil {
+					return xerrors.Wrapf(err, "failed to parse struct type block at row: %d, sheet: %s", row, sheet.Name)
+				}
+				blockBeginRow := row
+				block, blockEndRow := sheet.Table.ExtractBlock(blockBeginRow)
+				row = blockEndRow // skip row to next block
+				subSheet := book.NewTableSheet(sheet.Name, block)
+				desc := &internalpb.StructDescriptor{}
+				if err := parser.Parse(desc, subSheet); err != nil {
+					return xerrors.Wrapf(err, "failed to parse struct type block, sheet: %s, row: %d", sheet.Name, row)
+				}
+				firstFieldOptionName := ""
+				if len(desc.Fields) != 0 {
+					firstFieldOptionName = desc.Fields[0].Name
+				}
+				// add type info
+				info := &xproto.TypeInfo{
+					FullName:             protoreflect.FullName(gen.ProtoPackage + "." + typeName),
+					ParentFilename:       parentFilename,
+					Kind:                 types.MessageKind,
+					FirstFieldOptionName: firstFieldOptionName,
+				}
+				gen.typeInfos.Put(info)
+			}
+		}
 	case tableaupb.Mode_MODE_UNION_TYPE:
 		// add union self type info
 		info := &xproto.TypeInfo{
@@ -618,7 +651,7 @@ func (gen *Generator) parseSpecialSheetMode(mode tableaupb.Mode, ws *internalpb.
 		var worksheets []*internalpb.Worksheet
 		for row := 0; row <= sheet.Table.MaxRow; row++ {
 			cols := sheet.Table.GetRow(row)
-			if isEnumTypeDefinitionBlockHeader(cols) {
+			if isEnumTypeBlockHeader(cols) {
 				if row < 1 {
 					continue
 				}
@@ -628,11 +661,11 @@ func (gen *Generator) parseSpecialSheetMode(mode tableaupb.Mode, ws *internalpb.
 				subWs := proto.Clone(ws).(*internalpb.Worksheet)
 				subWs.Name, subWs.Alias, subWs.Note, err = extractEnumTypeRow(typeRow)
 				if err != nil {
-					return nil, xerrors.Wrapf(err, "failed to parse enum type block at row: %d, sheet: %s", row, sheet.Name)
+					return nil, xerrors.Wrapf(err, "failed to extract enum type block at row: %d, sheet: %s", row, sheet.Name)
 				}
 				block, blockEndRow := sheet.Table.ExtractBlock(blockBeginRow)
 				row = blockEndRow // skip row to next block
-				subSheet := book.NewTableSheet(sheet.Name, block)
+				subSheet := book.NewTableSheet(subWs.Name, block)
 				if err := parseEnumTypeValues(subWs, subSheet, parser); err != nil {
 					return nil, err
 				}
@@ -641,36 +674,36 @@ func (gen *Generator) parseSpecialSheetMode(mode tableaupb.Mode, ws *internalpb.
 		}
 		return worksheets, nil
 	case tableaupb.Mode_MODE_STRUCT_TYPE:
-		desc := &internalpb.StructDescriptor{}
-		if err := parser.Parse(desc, sheet); err != nil {
-			return nil, xerrors.Wrapf(err, "failed to parse struct type sheet: %s", sheet.Name)
-		}
-		bp := newBookParser("struct", "", "", gen)
-		shHeader := &tableHeader{
-			meta: &tableaupb.WorksheetOptions{
-				Namerow: 1,
-				Typerow: 2,
-			},
-			validNames: map[string]int{},
-		}
-		for _, field := range desc.Fields {
-			shHeader.namerow = append(shHeader.namerow, field.Name)
-			shHeader.typerow = append(shHeader.typerow, field.Type)
-			shHeader.noterow = append(shHeader.noterow, "")
+		if err := parseStructTypeValues(ws, sheet, parser, gen, debugBookName, debugSheetName); err != nil {
+			return nil, err
 		}
-		var parsed bool
-		var err error
-		for cursor := 0; cursor < len(shHeader.namerow); cursor++ {
-			subField := &internalpb.Field{}
-			cursor, parsed, err = bp.parseField(subField, shHeader, cursor, "")
-			if err != nil {
-				return nil, wrapDebugErr(err, debugBookName, debugSheetName, shHeader, cursor)
-			}
-			if parsed {
-				ws.Fields = append(ws.Fields, subField)
+		return []*internalpb.Worksheet{ws}, nil
+	case tableaupb.Mode_MODE_STRUCT_TYPE_MULTI:
+		var worksheets []*internalpb.Worksheet
+		for row := 0; row <= sheet.Table.MaxRow; row++ {
+			cols := sheet.Table.GetRow(row)
+			if isStructTypeBlockHeader(cols) {
+				if row < 1 {
+					continue
+				}
+				blockBeginRow := row
+				typeRow := sheet.Table.GetRow(row - 1)
+				var err error
+				subWs := proto.Clone(ws).(*internalpb.Worksheet)
+				subWs.Name, subWs.Alias, subWs.Note, err = extractStructTypeRow(typeRow)
+				if err != nil {
+					return nil, xerrors.Wrapf(err, "failed to extract struct type block at row: %d, sheet: %s", row, sheet.Name)
+				}
+				block, blockEndRow := sheet.Table.ExtractBlock(blockBeginRow)
+				row = blockEndRow // skip row to next block
+				subSheet := book.NewTableSheet(subWs.Name, block)
+				if err := parseStructTypeValues(subWs, subSheet, parser, gen, debugBookName, debugSheetName); err != nil {
+					return nil, err
+				}
+				worksheets = append(worksheets, subWs)
 			}
 		}
-		return []*internalpb.Worksheet{ws}, nil
+		return worksheets, nil
 	case tableaupb.Mode_MODE_UNION_TYPE:
 		desc := &internalpb.UnionDescriptor{}
 		if err := parser.Parse(desc, sheet); err != nil {
diff --git a/internal/protogen/sheet_mode.go b/internal/protogen/sheet_mode.go
new file mode 100644
index 0000000..9ec6547
--- /dev/null
+++ b/internal/protogen/sheet_mode.go
@@ -0,0 +1,122 @@
+package protogen
+
+import (
+	"fmt"
+
+	"github.com/tableauio/tableau/internal/importer/book"
+	"github.com/tableauio/tableau/proto/tableaupb"
+	"github.com/tableauio/tableau/proto/tableaupb/internalpb"
+	"github.com/tableauio/tableau/xerrors"
+)
+
+const (
+	colNumber = "Number" // name of column "Number"
+	colName   = "Name"   // name of column "Name"
+	colType   = "Type"   // name of column "Type"
+	colAlias  = "Alias"  // name of column "Alias"
+)
+
+func isEnumTypeBlockHeader(cols []string) bool {
+	if len(cols) < 3 {
+		return false
+	}
+	return cols[0] == colNumber && cols[1] == colName && cols[2] == colAlias
+}
+
+// extractEnumTypeRow find the first none-empty column as "name", and then
+// the two subsequent columns as "alias" and "note" if provided.
+func extractEnumTypeRow(cols []string) (name, alias, note string, err error) {
+	for i, cell := range cols {
+		if cell != "" {
+			name = cell
+			if i+1 < len(cols) {
+				alias = cols[i+1]
+			}
+			if i+2 < len(cols) {
+				note = cols[i+2]
+			}
+			break
+		}
+	}
+	if name == "" {
+		err = fmt.Errorf("name cell not found in enum type name row")
+	}
+	return
+}
+
+func parseEnumTypeValues(ws *internalpb.Worksheet, sheet *book.Sheet, parser book.SheetParser) error {
+	desc := &internalpb.EnumDescriptor{}
+	if err := parser.Parse(desc, sheet); err != nil {
+		return xerrors.Wrapf(err, "failed to parse enum type sheet (block): %s", sheet.Name)
+	}
+	for i, value := range desc.Values {
+		number := int32(i + 1)
+		if value.Number != nil {
+			number = *value.Number
+		}
+		field := &internalpb.Field{
+			Number: number,
+			Name:   value.Name,
+			Alias:  value.Alias,
+		}
+		ws.Fields = append(ws.Fields, field)
+	}
+	return nil
+}
+
+func isStructTypeBlockHeader(cols []string) bool {
+	if len(cols) < 2 {
+		return false
+	}
+	return cols[0] == colName && cols[1] == colType
+}
+
+// extractStructTypeRow find the first none-empty column as "name", and then
+// the two subsequent columns as "alias" and "note" if provided.
+func extractStructTypeRow(cols []string) (name, alias, note string, err error) {
+	if len(cols) == 0 || cols[0] == "" {
+		err = fmt.Errorf("name cell not found in struct type name row")
+		return
+	}
+	name = cols[0]
+	if len(cols) >= 2 {
+		alias = cols[1]
+	}
+	if len(cols) >= 3 {
+		note = cols[2]
+	}
+	return
+}
+
+func parseStructTypeValues(ws *internalpb.Worksheet, sheet *book.Sheet, parser book.SheetParser, gen *Generator, debugBookName, debugSheetName string) error {
+	desc := &internalpb.StructDescriptor{}
+	if err := parser.Parse(desc, sheet); err != nil {
+		return xerrors.Wrapf(err, "failed to parse struct type sheet (block): %s", sheet.Name)
+	}
+	bp := newBookParser("struct", "", "", gen)
+	shHeader := &tableHeader{
+		meta: &tableaupb.WorksheetOptions{
+			Namerow: 1,
+			Typerow: 2,
+		},
+		validNames: map[string]int{},
+	}
+	for _, field := range desc.Fields {
+		shHeader.namerow = append(shHeader.namerow, field.Name)
+		shHeader.typerow = append(shHeader.typerow, field.Type)
+		shHeader.noterow = append(shHeader.noterow, "")
+	}
+	var parsed bool
+	var err error
+	for cursor := 0; cursor < len(shHeader.namerow); cursor++ {
+		subField := &internalpb.Field{}
+		cursor, parsed, err = bp.parseField(subField, shHeader, cursor, "")
+		if err != nil {
+			return wrapDebugErr(err, debugBookName, debugSheetName, shHeader, cursor)
+		}
+		if parsed {
+			ws.Fields = append(ws.Fields, subField)
+		}
+	}
+	return nil
+}
diff --git a/internal/protogen/util.go b/internal/protogen/util.go
index b3e5bf7..04e2b72 100644
--- a/internal/protogen/util.go
+++ b/internal/protogen/util.go
@@ -13,7 +13,6 @@ import (
 	"github.com/tableauio/tableau/internal/x/xfs"
 	"github.com/tableauio/tableau/options"
 	"github.com/tableauio/tableau/proto/tableaupb"
-	"github.com/tableauio/tableau/proto/tableaupb/internalpb"
 	"github.com/tableauio/tableau/xerrors"
 )
 
@@ -24,60 +23,6 @@ const (
 	secondPass                  // generate config messagers from sheets
 )
 
-const (
-	colNumber = "Number" // name of column "Number"
-	colName   = "Name"   // name of column "Name"
-	colAlias  = "Alias"  // name of column "Alias"
-)
-
-func isEnumTypeDefinitionBlockHeader(cols []string) bool {
-	if len(cols) < 3 {
-		return false
-	}
-	return cols[0] == colNumber && cols[1] == colName && cols[2] == colAlias
-}
-
-// extractEnumTypeRow find the first none-empty column as "name", and then
-// the two subsequent columns as "alias" and "note" if provided.
-func extractEnumTypeRow(cols []string) (name, alias, note string, err error) {
-	for i, cell := range cols {
-		if cell != "" {
-			name = cell
-			if i+1 < len(cols) {
-				alias = cols[i+1]
-			}
-			if i+2 < len(cols) {
-				note = cols[i+2]
-			}
-			break
-		}
-	}
-	if name == "" {
-		err = fmt.Errorf("name cell not found in enum type row")
-	}
-	return
-}
-
-func parseEnumTypeValues(ws *internalpb.Worksheet, sheet *book.Sheet, parser book.SheetParser) error {
-	desc := &internalpb.EnumDescriptor{}
-	if err := parser.Parse(desc, sheet); err != nil {
-		return xerrors.Wrapf(err, "failed to parse enum type sheet: %s", sheet.Name)
-	}
-	for i, value := range desc.Values {
-		number := int32(i + 1)
-		if value.Number != nil {
-			number = *value.Number
-		}
-		field := &internalpb.Field{
-			Number: number,
-			Name:   value.Name,
-			Alias:  value.Alias,
-		}
-		ws.Fields = append(ws.Fields, field)
-	}
-	return nil
-}
-
 func prepareOutdir(outdir string, importFiles []string, delExisted bool) error {
 	existed, err := xfs.Exists(outdir)
 	if err != nil {


Test patch:
diff --git a/test/functest/conf/StructTypeReuseInBook.json b/test/functest/conf/StructTypeReuseInBook.json
index 3935edb..ae84678 100644
--- a/test/functest/conf/StructTypeReuseInBook.json
+++ b/test/functest/conf/StructTypeReuseInBook.json
@@ -1,42 +1,69 @@
 {
-    "taskRewardMap": {
+    "rewardMap": {
         "1": {
             "id": 1,
-            "num": 100,
-            "fruitType": "FRUIT_TYPE_APPLE",
-            "featureList": [
-                1,
-                2
-            ],
-            "propMap": {
-                "1": "Power",
-                "2": "Skill"
+            "pet": {
+                "kind": 1,
+                "tipList": [
+                    "lovely",
+                    "small"
+                ]
             },
-            "detail": {
-                "type": "ITEM_TYPE_EQUIP",
-                "name": "Sword",
-                "desc": "Sword's Desc"
+            "taskReward": {
+                "id": 1,
+                "num": 100,
+                "fruitType": "FRUIT_TYPE_APPLE",
+                "featureList": [
+                    1,
+                    2
+                ],
+                "propMap": {
+                    "1": "Power",
+                    "2": "Skill"
+                },
+                "detail": {
+                    "type": "ITEM_TYPE_EQUIP",
+                    "name": "Sword",
+                    "desc": "Sword's Desc"
+                }
             }
         },
         "2": {
             "id": 2,
-            "num": 200,
-            "fruitType": "FRUIT_TYPE_BANANA",
-            "featureList": [
-                1
-            ],
-            "propMap": {
-                "1": "Power"
+            "pet": {
+                "kind": 2,
+                "tipList": [
+                    "beautiful",
+                    "big"
+                ]
             },
-            "detail": null
+            "taskReward": {
+                "id": 2,
+                "num": 200,
+                "fruitType": "FRUIT_TYPE_BANANA",
+                "featureList": [
+                    1
+                ],
+                "propMap": {
+                    "1": "Power"
+                },
+                "detail": null
+            }
         },
         "3": {
             "id": 3,
-            "num": 300,
-            "fruitType": "FRUIT_TYPE_UNKNOWN",
-            "featureList": [],
-            "propMap": {},
-            "detail": null
+            "pet": {
+                "kind": 3,
+                "tipList": []
+            },
+            "taskReward": {
+                "id": 3,
+                "num": 300,
+                "fruitType": "FRUIT_TYPE_UNKNOWN",
+                "featureList": [],
+                "propMap": {},
+                "detail": null
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/test/functest/proto/excel__metasheet__sheet_mode.proto b/test/functest/proto/excel__metasheet__sheet_mode.proto
index 9bf2d00..cd0a20d 100644
--- a/test/functest/proto/excel__metasheet__sheet_mode.proto
+++ b/test/functest/proto/excel__metasheet__sheet_mode.proto
@@ -65,7 +65,7 @@ enum ItemType {
 
 // Generated from sheet: StructTaskReward.
 message TaskReward {
-  uint32 id = 1 [(tableau.field) = {name:"ID"}];
+  uint32 id = 1 [(tableau.field) = {name:"ID" prop:{range:"1,10"}}];
   int32 num = 2 [(tableau.field) = {name:"Num"}];
   protoconf.FruitType fruit_type = 3 [(tableau.field) = {name:"FruitType"}];
   repeated int32 feature_list = 4 [(tableau.field) = {name:"Feature" layout:LAYOUT_INCELL}];
@@ -78,10 +78,33 @@ message TaskReward {
   }
 }
 
+// Generated from sheet: StructType.
+message Tree {
+  uint32 id = 1 [(tableau.field) = {name:"ID"}];
+  int32 num = 2 [(tableau.field) = {name:"Num"}];
+}
+
+// Generated from sheet: StructType.
+message Pet {
+  int32 kind = 1 [(tableau.field) = {name:"Kind"}];
+  repeated string tip_list = 2 [(tableau.field) = {name:"Tip" layout:LAYOUT_INCELL}];
+}
+
+// Generated from sheet: StructType.
+message FruitShop {
+  protoconf.FruitType fruit_type = 1 [(tableau.field) = {name:"FruitType"}];
+  map<int32, string> prop_map = 2 [(tableau.field) = {name:"Prop" layout:LAYOUT_INCELL}];
+}
+
 message StructTypeReuseInBook {
   option (tableau.worksheet) = {name:"StructTypeReuseInBook" namerow:1 typerow:2 noterow:3 datarow:4};
 
-  map<uint32, protoconf.TaskReward> task_reward_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL}];
+  map<uint32, Reward> reward_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL}];
+  message Reward {
+    uint32 id = 1 [(tableau.field) = {name:"ID"}];
+    protoconf.Pet pet = 2 [(tableau.field) = {name:"Pet"}];
+    protoconf.TaskReward task_reward = 3 [(tableau.field) = {name:"TaskReward"}];
+  }
 }
 
 // Generated from sheet: UnionSimpleTarget.
diff --git a/test/functest/testdata/excel/metasheet/SheetMode#@TABLEAU.csv b/test/functest/testdata/excel/metasheet/SheetMode#@TABLEAU.csv
index 1968ad0..a340dbd 100644
--- a/test/functest/testdata/excel/metasheet/SheetMode#@TABLEAU.csv
+++ b/test/functest/testdata/excel/metasheet/SheetMode#@TABLEAU.csv
@@ -7,3 +7,4 @@ UnionSimpleTarget,SimpleTarget,MODE_UNION_TYPE
 EnumTypeReuseInBook,,
 StructTypeReuseInBook,,
 UnionTypeReuseInBook,,
+StructType,,MODE_STRUCT_TYPE_MULTI
diff --git a/test/functest/testdata/excel/metasheet/SheetMode#StructTaskReward.csv b/test/functest/testdata/excel/metasheet/SheetMode#StructTaskReward.csv
index 863cb47..0e2bb41 100644
--- a/test/functest/testdata/excel/metasheet/SheetMode#StructTaskReward.csv
+++ b/test/functest/testdata/excel/metasheet/SheetMode#StructTaskReward.csv
@@ -1,5 +1,5 @@
 Name,Type
-ID,uint32
+ID,"uint32|{range: ""1,10""}"
 Num,int32
 FruitType,enum<.FruitType>
 Feature,[]int32
diff --git a/test/functest/testdata/excel/metasheet/SheetMode#StructType.csv b/test/functest/testdata/excel/metasheet/SheetMode#StructType.csv
new file mode 100644
index 0000000..6a99e30
--- /dev/null
+++ b/test/functest/testdata/excel/metasheet/SheetMode#StructType.csv
@@ -0,0 +1,16 @@
+Tree,TreeAlias
+Name,Type
+ID,uint32
+Num,int32
+,
+Pet,PetAlias
+Name,Type
+Kind,int32
+Tip,[]string
+,
+,
+,
+FruitShop,FruitShopAlias
+Name,Type
+FruitType,enum<.FruitType>
+Prop,"map<int32, string>"
diff --git a/test/functest/testdata/excel/metasheet/SheetMode#StructTypeReuseInBook.csv b/test/functest/testdata/excel/metasheet/SheetMode#StructTypeReuseInBook.csv
index 5060a1c..984d6ed 100644
--- a/test/functest/testdata/excel/metasheet/SheetMode#StructTypeReuseInBook.csv
+++ b/test/functest/testdata/excel/metasheet/SheetMode#StructTypeReuseInBook.csv
@@ -1,6 +1,6 @@
-ID,Num,FruitType,Feature,Prop,Detail
-"map<uint32, .TaskReward>",int32,enum<.FruitType>,[]int32,"map<int32, string>","{enum<.ItemType> Type, string Name, string Desc}Detail"
-Reward’s ID,Reward’s num,Reward’s fruit type,Reward’s feature,Reward’s Prop,Reward’s Detail
-1,100,Apple,"1,2","1:Power,2:Skill","Equip,Sword,Sword's Desc"
-2,200,Banana,1,1:Power,
-3,300,,,,
+ID,PetKind,PetTip,TaskRewardID,TaskRewardNum,TaskRewardFruitType,TaskRewardFeature,TaskRewardProp,TaskRewardDetail
+"map<uint32, Reward>",{.Pet}int32,[]string,{.TaskReward}uint32,int32,enum<.FruitType>,[]int32,"map<int32, string>","{enum<.ItemType> Type, string Name, string Desc}Detail"
+Reward’s ID,Pet’s kind,Pet's tip,Reward’s ID,Reward’s num,Reward’s fruit type,Reward’s feature,Reward’s Prop,Reward’s Detail
+1,1,"lovely,small",1,100,Apple,"1,2","1:Power,2:Skill","Equip,Sword,Sword's Desc"
+2,2,"beautiful,big",2,200,Banana,1,1:Power,
+3,3,,3,300,,,,