Repository: tableauio/tableau
Pull request #34 (2023-02-22 11:53:03)

fix(xml): support incell enum list definition in xml element's text


Patch:
diff --git a/internal/confgen/parser.go b/internal/confgen/parser.go
index 21953aa..cab3788 100644
--- a/internal/confgen/parser.go
+++ b/internal/confgen/parser.go
@@ -1104,8 +1104,19 @@ func (sp *sheetParser) parseUnionField(field *Field, msg protoreflect.Message, r
 	}
 	structValue.Message().Set(unionDesc.Type, typeVal)
 
-	// parse union value
-	valueFD := unionDesc.GetValueByNumber(int32(typeVal.Enum()))
+	// parse value
+	fieldNumber := int32(typeVal.Enum())
+	if fieldNumber == 0 {
+		// default enum value 0, no need to parse.
+		return false, nil
+	}
+	valueFD := unionDesc.GetValueByNumber(fieldNumber)
+	if valueFD == nil{
+		typeValue := unionDesc.Type.Enum().Values().ByNumber(protoreflect.EnumNumber(fieldNumber)).Name()
+		err := xerrors.E2010(typeValue, fieldNumber)
+		kvs := append(rc.CellDebugKV(typeColName), xerrors.KeyPBFieldType, "enum")
+		return false, xerrors.WithMessageKV(err, kvs...) 
+	}
 	fieldValue := structValue.Message().NewField(valueFD)
 	if valueFD.Kind() == protoreflect.MessageKind {
 		// MUST be message type.
diff --git a/internal/confgen/union.go b/internal/confgen/union.go
index 3444d28..2d1dacb 100644
--- a/internal/confgen/union.go
+++ b/internal/confgen/union.go
@@ -48,6 +48,8 @@ type UnionDescriptor struct {
 	Value protoreflect.OneofDescriptor
 }
 
+// TypeName returns the type field name.
+// It returns CameCase style of proto field name if not set explicitly in field extension.
 func (u UnionDescriptor) TypeName() string {
 	opts := proto.GetExtension(u.Type.Options(), tableaupb.E_Field).(*tableaupb.FieldOptions)
 	if opts != nil {
@@ -57,6 +59,8 @@ func (u UnionDescriptor) TypeName() string {
 	return strcase.ToCamel(string(u.Type.Name()))
 }
 
+// ValueFieldName returns the value field name.
+// It returns "Field" if not set explicitly in oneof extension.
 func (u UnionDescriptor) ValueFieldName() string {
 	opts := proto.GetExtension(u.Value.Options(), tableaupb.E_Oneof).(*tableaupb.OneofOptions)
 	if opts != nil {
@@ -66,6 +70,8 @@ func (u UnionDescriptor) ValueFieldName() string {
 	return "Field"
 }
 
+// GetValueByNumber returns the FieldDescriptor for a field numbered n.
+// It returns nil if not found.
 func (u UnionDescriptor) GetValueByNumber(n int32) protoreflect.FieldDescriptor {
 	return GetOneofFieldByNumber(u.Value, n)
 }
diff --git a/internal/importer/xml.go b/internal/importer/xml.go
index 060a7c6..34c10e0 100644
--- a/internal/importer/xml.go
+++ b/internal/importer/xml.go
@@ -26,6 +26,7 @@ type XMLImporter struct {
 }
 
 var attrRegexp *regexp.Regexp
+var tagRegexp *regexp.Regexp
 var scalarListRegexp *regexp.Regexp
 var metasheetRegexp *regexp.Regexp
 
@@ -33,12 +34,13 @@ const (
 	xmlProlog             = `<?xml version='1.0' encoding='UTF-8'?>`
 	atTableauDisplacement = `ATABLEAU`
 	ungreedyPropGroup     = `(\|\{[^\{\}]+\})?`                        // e.g.: |{default:"100"}
-	metasheetItemBlock    = `(\s+<Item(\s+\S+\s*=\s*"\S+")+\s*/>\s+)*` // e.g.: <Item Sheet="XXXConf" Sep="|"/>
-	sheetBlock            = `<%v(>(.*\n)*</%v>|\s*/>)`                 // e.g.: <XXXConf>...</XXXConf>
+	metasheetItemBlock    = `<Item(\s+\S+\s*=\s*("\S+"|'\S+'))+\s*/>` // e.g.: <Item Sheet="XXXConf" Sep="|"/>
+	sheetBlock            = `<%v(>(.*\n)*</%v>|\s*/>)`                 // e.g.: <XXXConf>...</XXXConf>	
 )
 
 func init() {
-	attrRegexp = regexp.MustCompile(`([0-9A-Za-z_]+)\s*=\s*"` + types.TypeGroup + ungreedyPropGroup + `"`) // e.g.: Num = "int32|{range:"1,~"}"
+	attrRegexp = regexp.MustCompile(`\s*=\s*("|')` + types.TypeGroup + ungreedyPropGroup + `("|')`) // e.g.: = "int32|{range:"1,~"}"
+	tagRegexp = regexp.MustCompile(`>` + types.TypeGroup + ungreedyPropGroup + `</`) // e.g.: >int32|{range:"1,~"}</
 	scalarListRegexp = regexp.MustCompile(`([A-Za-z_]+)([0-9]+)`)                                          // e.g.: Para1, Para2, Para3, ...
 
 	// metasheet regexp, e.g.:
@@ -51,7 +53,7 @@ func init() {
 	// 		<Weight Num="map<uint32, Weight>"/>
 	// </Server>
 	// -->
-	metasheetRegexp = regexp.MustCompile(fmt.Sprintf(`<!--\s+(<%v(>`+metasheetItemBlock+`</%v>|\s*/>)(.*\n)+?)-->`, book.MetasheetName, book.MetasheetName))
+	metasheetRegexp = regexp.MustCompile(fmt.Sprintf(`<!--\s+(<%v(>(\s+`+metasheetItemBlock+`\s+)*</%v>|\s*/>)(.*\n)+?)-->`, book.MetasheetName, book.MetasheetName))
 }
 
 // TODO: options
@@ -414,7 +416,14 @@ func escapeAttrs(doc string) string {
 		var typeBuf, propBuf bytes.Buffer
 		xml.EscapeText(&typeBuf, []byte(matches[2]))
 		xml.EscapeText(&propBuf, []byte(matches[3]))
-		return fmt.Sprintf("%s=\"%s%s\"", matches[1], typeBuf.String(), propBuf.String())
+		return fmt.Sprintf("=\"%s%s\"", typeBuf.String(), propBuf.String())
+	})
+	escapedDoc = tagRegexp.ReplaceAllStringFunc(escapedDoc, func(s string) string {
+		matches := matchTag(s)
+		var typeBuf, propBuf bytes.Buffer
+		xml.EscapeText(&typeBuf, []byte(matches[1]))
+		xml.EscapeText(&propBuf, []byte(matches[2]))
+		return fmt.Sprintf(">%s%s</", typeBuf.String(), propBuf.String())
 	})
 	return escapedDoc
 }
@@ -451,6 +460,10 @@ func matchAttr(s string) []string {
 	return attrRegexp.FindStringSubmatch(s)
 }
 
+func matchTag(s string) []string {
+	return tagRegexp.FindStringSubmatch(s)
+}
+
 func matchScalarList(s string) []string {
 	return scalarListRegexp.FindStringSubmatch(s)
 }
diff --git a/internal/localizer/i18n/config/ecode/en.yaml b/internal/localizer/i18n/config/ecode/en.yaml
index 969ffb9..321de00 100644
--- a/internal/localizer/i18n/config/ecode/en.yaml
+++ b/internal/localizer/i18n/config/ecode/en.yaml
@@ -40,4 +40,8 @@ E2009:
   desc: duplicate key exists in different sheets
   text: 'field {{.FieldName}} has duplicate key "{{.Key}}"'
   help: "key must not be duplicate when merge multiple sheets"
+E2010:
+  desc: union type has no corresponding value field
+  text: 'union type "{{.TypeValue}}" has no corresponding value field with number "{{.FieldNumber}}"'
+  help: 'add new union value field and bind it to type "{{.TypeValue}}"'
 # [3000, 3999]: importer error
diff --git a/internal/localizer/i18n/config/ecode/zh.yaml b/internal/localizer/i18n/config/ecode/zh.yaml
index b669fb5..eb18d20 100644
--- a/internal/localizer/i18n/config/ecode/zh.yaml
+++ b/internal/localizer/i18n/config/ecode/zh.yaml
@@ -40,4 +40,8 @@ E2009:
   desc: duplicate key exists in different sheets
   text: '字段 {{.FieldName}} 在配置表合表时，存在重复的键 "{{.Key}}"'
   help: '合表时，不同的表不允许配置重复的键'
+E2010:
+  desc: union type has no corresponding value field
+  text: 'union 类型"{{.TypeValue}}"找不到 field number 为"{{.FieldNumber}}"的对应 value 字段'
+  help: '新增 union value field 并绑定到类型"{{.TypeValue}}"'
 # [3000, 3999]: importer error
diff --git a/xerrors/ecode.go b/xerrors/ecode.go
index 880d159..17cfeec 100644
--- a/xerrors/ecode.go
+++ b/xerrors/ecode.go
@@ -85,3 +85,11 @@ func E2009(key, fieldName interface{}) error {
 		"FieldName": fieldName,
 	})
 }
+
+// E2010 describes union type and value field mismatch.
+func E2010(typeValue, fieldNumber interface{}) error {
+	return renderEcode("E2010", map[string]interface{}{
+		"TypeValue":   typeValue,
+		"FieldNumber": fieldNumber,
+	})
+}


Test patch:
diff --git a/internal/importer/xml_test.go b/internal/importer/xml_test.go
index f1e51d4..196cfab 100644
--- a/internal/importer/xml_test.go
+++ b/internal/importer/xml_test.go
@@ -67,6 +67,36 @@ func Test_escapeAttrs(t *testing.T) {
 <Conf>
 	<Client ID="map&lt;uint32, Client&gt;|{unique:true range:&#34;1,~&#34;}" OpenTime="datetime|{default:&#34;2022-01-23 15:40:00&#34;}"/>
 </Conf>
+`,
+		},
+		{
+			name: "SimpleTag",
+			args: args{
+				doc: `
+<Conf>
+	<ClientID>enum<.ClientIDType></ClientID>
+</Conf>
+`,
+			},
+			want: `
+<Conf>
+	<ClientID>enum&lt;.ClientIDType&gt;</ClientID>
+</Conf>
+`,
+		},
+		{
+			name: "ComplexTag",
+			args: args{
+				doc: `
+<Conf>
+	<ClientID>map<uint32, Client>|{unique:true range:"1,~"}</ClientID>
+</Conf>
+`,
+			},
+			want: `
+<Conf>
+	<ClientID>map&lt;uint32, Client&gt;|{unique:true range:&#34;1,~&#34;}</ClientID>
+</Conf>
 `,
 		},
 	}
@@ -179,7 +209,7 @@ func Test_matchAttr(t *testing.T) {
 				s: `<AAA bb = "bool" cc = "int64" dd = "enum<.EnumType>" >`,
 			},
 			want: []string{
-				`bb = "bool"`, `bb`, `bool`, ``,
+				` = "bool"`, `"`, `bool`, ``, `"`,
 			},
 		},
 		{
@@ -188,8 +218,8 @@ func Test_matchAttr(t *testing.T) {
 				s: `<Client OpenTime="datetime|{default:"2022-01-23 15:40:00"}" CloseTime="datetime|{default:"2022-01-23 15:40:00"}"/>`,
 			},
 			want: []string{
-				`OpenTime="datetime|{default:"2022-01-23 15:40:00"}"`,
-				`OpenTime`, `datetime`, `|{default:"2022-01-23 15:40:00"}`,
+				`="datetime|{default:"2022-01-23 15:40:00"}"`,
+				`"`, `datetime`, `|{default:"2022-01-23 15:40:00"}`, `"`,
 			},
 		},
 	}
@@ -584,13 +614,13 @@ func Test_genMetasheet(t *testing.T) {
 			},
 			want: map[string]map[string]string{
 				"ServerConf": {
-					"Nested": "true",
-					"Sheet":  "ServerConf",
-					"Sep":    "|",
-					"Namerow": "1",
-					"Typerow": "2",
-					"Noterow": "3",
-					"Datarow": "4",
+					"Nested":   "true",
+					"Sheet":    "ServerConf",
+					"Sep":      "|",
+					"Namerow":  "1",
+					"Typerow":  "2",
+					"Noterow":  "3",
+					"Datarow":  "4",
 					"Nameline": "1",
 					"Typeline": "1",
 				},
@@ -983,6 +1013,7 @@ func Test_matchMetasheet(t *testing.T) {
 	<Item Sheet="Server" />
 `,
 				` Sheet="Server"`,
+				`"Server"`,
 				`</Server>
 `,
 			},
@@ -1126,3 +1157,44 @@ func Test_matchSheetBlock(t *testing.T) {
 		})
 	}
 }
+
+func Test_matchTag(t *testing.T) {
+	type args struct {
+		s string
+	}
+	tests := []struct {
+		name string
+		args args
+		want []string
+	}{
+		{
+			name: "SimpleTag",
+			args: args{
+				s: `<ClientID>enum<.ClientIDType></ClientID>`,
+			},
+			want: []string{
+				`>enum<.ClientIDType></`,
+				`enum<.ClientIDType>`,
+				``,
+			},
+		},
+		{
+			name: "ComplexTag",
+			args: args{
+				s: `<ClientID>map<uint32, Client>|{unique:true range:"1,~"}</ClientID>`,
+			},
+			want: []string{
+				`>map<uint32, Client>|{unique:true range:"1,~"}</`,
+				`map<uint32, Client>`,
+				`|{unique:true range:"1,~"}`,
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := matchTag(tt.args.s); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("matchTag() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/test/functest/conf/ListConf2.json b/test/functest/conf/ListConf2.json
index 3d33a06..b1a9435 100644
--- a/test/functest/conf/ListConf2.json
+++ b/test/functest/conf/ListConf2.json
@@ -1,6 +1,11 @@
 {
     "parent": {
         "parent2": {
+            "enumIncellList": [
+                "FRUIT_TYPE_APPLE",
+                "FRUIT_TYPE_ORANGE",
+                "FRUIT_TYPE_BANANA"
+            ],
             "incell": {
                 "valueList": [
                     1,
diff --git a/test/functest/conf/MapConf.json b/test/functest/conf/MapConf.json
index 7aeb2f2..0f741bc 100644
--- a/test/functest/conf/MapConf.json
+++ b/test/functest/conf/MapConf.json
@@ -1,17 +1,25 @@
 {
-    "open":  true,
-    "entryMap":  {
-        "1":  {
-            "id":  1,
-            "value":  1
+    "open": true,
+    "preEntryMap": {
+        "1": {
+            "name": "1"
         },
-        "2":  {
-            "id":  2,
-            "value":  2
+        "2": {
+            "name": "2"
+        }
+    },
+    "entryMap": {
+        "1": {
+            "id": 1,
+            "value": 1
+        },
+        "2": {
+            "id": 2,
+            "value": 2
         },
-        "3":  {
-            "id":  3,
-            "value":  3
+        "3": {
+            "id": 3,
+            "value": 3
         }
     }
 }
\ No newline at end of file
diff --git a/test/functest/proto/xml__list__list.proto b/test/functest/proto/xml__list__list.proto
index 31fabe5..14329eb 100644
--- a/test/functest/proto/xml__list__list.proto
+++ b/test/functest/proto/xml__list__list.proto
@@ -1,10 +1,11 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.4.3). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
 
 package protoconf;
 
+import "common/common.proto";
 import "tableau/protobuf/tableau.proto";
 
 option go_package = "github.com/tableauio/tableau/test/functest/protoconf";
@@ -33,7 +34,8 @@ message ListConf2 {
   message Parent {
     Parent2 parent_2 = 1 [(tableau.field) = {name:"Parent2"}];
     message Parent2 {
-      Incell incell = 1 [(tableau.field) = {name:"Incell"}];
+      repeated protoconf.FruitType enum_incell_list = 1 [(tableau.field) = {name:"EnumIncell" layout:LAYOUT_INCELL}];
+      Incell incell = 2 [(tableau.field) = {name:"Incell"}];
       message Incell {
         repeated int32 value_list = 1 [(tableau.field) = {name:"Value" layout:LAYOUT_INCELL}];
       }
diff --git a/test/functest/proto/xml__map__map.proto b/test/functest/proto/xml__map__map.proto
index d41f87a..5e6dc95 100644
--- a/test/functest/proto/xml__map__map.proto
+++ b/test/functest/proto/xml__map__map.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.4.3). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
@@ -14,7 +14,11 @@ message MapConf {
   option (tableau.worksheet) = {name:"MapConf" namerow:1 typerow:2 noterow:3 datarow:4 nameline:1 typeline:1 nested:true};
 
   bool open = 1 [(tableau.field) = {name:"Open"}];
-  map<uint32, Entry> entry_map = 2 [(tableau.field) = {name:"Entry" key:"Id" layout:LAYOUT_VERTICAL}];
+  map<string, PreEntry> pre_entry_map = 2 [(tableau.field) = {name:"PreEntry" key:"Name" layout:LAYOUT_VERTICAL}];
+  message PreEntry {
+    string name = 1 [(tableau.field) = {name:"Name"}];
+  }
+  map<uint32, Entry> entry_map = 3 [(tableau.field) = {name:"Entry" key:"Id" layout:LAYOUT_VERTICAL}];
   message Entry {
     uint32 id = 1 [(tableau.field) = {name:"Id"}];
     int32 value = 2 [(tableau.field) = {name:"Value"}];
diff --git a/test/functest/testdata/xml/list/List.xml b/test/functest/testdata/xml/list/List.xml
index 96d60c7..695a70a 100644
--- a/test/functest/testdata/xml/list/List.xml
+++ b/test/functest/testdata/xml/list/List.xml
@@ -17,6 +17,7 @@
     <Parent>
         <Parent2>
             <Incell Value="[]int32" />
+            <EnumIncell>[]enum<.FruitType></EnumIncell>
         </Parent2>
     </Parent>
 </ListConf2>
@@ -37,6 +38,7 @@
     <Parent>
         <Parent2>
             <Incell Value="1,2,3,1" />
+            <EnumIncell>Apple, Orange, Banana</EnumIncell>
         </Parent2>
     </Parent>
 </ListConf2>
diff --git a/test/functest/testdata/xml/map/Map.xml b/test/functest/testdata/xml/map/Map.xml
index f278b48..552f67c 100644
--- a/test/functest/testdata/xml/map/Map.xml
+++ b/test/functest/testdata/xml/map/Map.xml
@@ -5,11 +5,14 @@
 </@TABLEAU>
 
 <MapConf Open="bool">
+    <PreEntry Name="map<string, PreEntry>" />
     <Entry Id="map<uint32, Entry>" Value="int32" />
 </MapConf>
 -->
 
 <MapConf Open="true">
+    <PreEntry Name="1" />
+    <PreEntry Name="2" />
     <Entry Id="1" Value="1" />
     <Entry Id="2" Value="2" />
     <Entry Id="3" Value="3" />