Repository: tableauio/tableau
Pull request #29 (2023-01-09 09:40:27)

feat: add support for incell-map with key/value as enum type
close issue #27

codecov[bot]: # [Codecov](https://codecov.io/gh/tableauio/tableau/pull/29?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) Report
> Merging [#29](https://codecov.io/gh/tableauio/tableau/pull/29?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) (a9c36b8) into [master](https://codecov.io/gh/tableauio/tableau/commit/0955a7f27e97ab664f67ad7afc68059f8bae9715?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) (0955a7f) will **increase** coverage by `0.27%`.
> The diff coverage is `8.88%`.

```diff
@@            Coverage Diff             @@
##           master      #29      +/-   ##
==========================================
+ Coverage   20.34%   20.62%   +0.27%     
==========================================
  Files          22       23       +1     
  Lines        3057     3123      +66     
==========================================
+ Hits          622      644      +22     
- Misses       2392     2436      +44     
  Partials       43       43              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `20.62% <8.88%> (+0.27%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/tableauio/tableau/pull/29?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) | Coverage Δ | |
|---|---|---|
| [internal/protogen/parser.go](https://codecov.io/gh/tableauio/tableau/pull/29?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-aW50ZXJuYWwvcHJvdG9nZW4vcGFyc2VyLmdv) | `0.00% <0.00%> (ø)` | |
| [internal/types/map.go](https://codecov.io/gh/tableauio/tableau/pull/29?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-aW50ZXJuYWwvdHlwZXMvbWFwLmdv) | `0.00% <0.00%> (ø)` | |
| [internal/xproto/build.go](https://codecov.io/gh/tableauio/tableau/pull/29?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-aW50ZXJuYWwveHByb3RvL2J1aWxkLmdv) | `14.75% <0.00%> (-0.51%)` | :arrow_down: |
| [internal/types/types.go](https://codecov.io/gh/tableauio/tableau/pull/29?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-aW50ZXJuYWwvdHlwZXMvdHlwZXMuZ28=) | `68.60% <100.00%> (+5.81%)` | :arrow_up: |
| [internal/confgen/prop/prop.go](https://codecov.io/gh/tableauio/tableau/pull/29?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-aW50ZXJuYWwvY29uZmdlbi9wcm9wL3Byb3AuZ28=) | `13.82% <0.00%> (+13.82%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio)


Patch:
diff --git a/README.md b/README.md
index 726a29a..af1e7bf 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ See official document: [Design](https://tableauio.github.io/docs/design/overview
 
 #### Protobuf
 
-Goto [Protocol Buffers v3.17.3](https://github.com/protocolbuffers/protobuf/releases/tag/v3.17.3), choose and download the correct platform of **protoc**, then install by README.
+Goto [Protocol Buffers v21.12](https://github.com/protocolbuffers/protobuf/releases/tag/v21.12), choose and download the correct platform of **protoc**, then install by README.
 
 #### protoc-gen-go
 
diff --git a/internal/confgen/parser.go b/internal/confgen/parser.go
index d0803b8..ef7083d 100644
--- a/internal/confgen/parser.go
+++ b/internal/confgen/parser.go
@@ -12,6 +12,7 @@ import (
 	"github.com/tableauio/tableau/internal/confgen/prop"
 	"github.com/tableauio/tableau/internal/importer"
 	"github.com/tableauio/tableau/internal/importer/book"
+	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/internal/xproto"
 	"github.com/tableauio/tableau/log"
 	"github.com/tableauio/tableau/options"
@@ -329,12 +330,11 @@ func (sp *sheetParser) parseFieldOptions(msg protoreflect.Message, rc *book.RowC
 			// default processing
 			if fd.IsList() {
 				// truncate suffix `List` (CamelCase) corresponding to `_list` (snake_case)
-				name = strings.TrimSuffix(name, "List")
+				name = strings.TrimSuffix(name, types.DefaultListFieldOptNameSuffix)
 			} else if fd.IsMap() {
 				// truncate suffix `Map` (CamelCase) corresponding to `_map` (snake_case)
-				// name = strings.TrimSuffix(name, "Map")
-				name = ""
-				key = "Key"
+				name = strings.TrimSuffix(name, types.DefaultMapFieldOptNameSuffix)
+				key = types.DefaultMapKeyOptName
 			}
 		}
 		if sep == "" {
@@ -444,8 +444,8 @@ func (sp *sheetParser) parseMapField(field *Field, msg protoreflect.Message, rc
 			reflectMap.Set(newMapKey, newMapValue)
 		} else {
 			// value is scalar type
-			key := "Key"     // default key name
-			value := "Value" // default value name
+			key := types.DefaultMapKeyOptName     // default key name
+			value := types.DefaultMapValueOptName // default value name
 			// key cell
 			keyColName := prefix + field.opts.Name + key
 			cell, err := rc.Cell(keyColName, field.opts.Optional)
@@ -567,45 +567,18 @@ func (sp *sheetParser) parseMapField(field *Field, msg protoreflect.Message, rc
 		}
 
 		if valueFd.Kind() == protoreflect.MessageKind {
-			kvs := append(rc.CellDebugKV(colName), xerrors.KeyPBFieldType, "incell map")
-			return false, xerrors.ErrorKV("map value type is struct, and is not supported", kvs...)
-		}
-
-		if cell.Data != "" {
-			// If s does not contain sep and sep is not empty, Split returns a
-			// slice of length 1 whose only element is s.
-			splits := strings.Split(cell.Data, field.opts.Sep)
-			size := len(splits)
-			for i := 0; i < size; i++ {
-				kv := strings.SplitN(splits[i], field.opts.Subsep, 2)
-				if len(kv) == 1 {
-					// If value is not set, then treated it as default empty string.
-					kv = append(kv, "")
-				}
-				key, value := kv[0], kv[1]
-
-				fieldValue, keyPresent, err := sp.parseFieldValue(keyFd, key)
-				if err != nil {
-					kvs := append(rc.CellDebugKV(colName), xerrors.KeyPBFieldType, "incell map")
-					return false, xerrors.WithMessageKV(err, kvs...)
-				}
-
-				newMapKey := fieldValue.MapKey()
-				fieldValue, valuePresent, err := sp.parseFieldValue(valueFd, value)
-				if err != nil {
-					kvs := append(rc.CellDebugKV(colName), xerrors.KeyPBFieldType, "incell map")
-					return false, xerrors.WithMessageKV(err, kvs...)
-				}
-				newMapValue := reflectMap.NewValue()
-				newMapValue = fieldValue
-
-				if !keyPresent && !valuePresent {
-					// key and value are both not present.
-					// TODO: check the remaining keys all not present, otherwise report error!
-					break
-				}
-
-				reflectMap.Set(newMapKey, newMapValue)
+			if !types.CheckMessageWithOnlyKVFields(reflectMap.NewValue().Message()) {
+				kvs := append(rc.CellDebugKV(colName), xerrors.KeyPBFieldType, "incell map")
+				return false, xerrors.ErrorKV("map value type is not KV struct, and is not supported", kvs...)
+			}
+			err := sp.parseIncellMapWithValueAsSimpleKVMessage(field, reflectMap, cell.Data)
+			if err != nil {
+				return false, xerrors.WithMessageKV(err, rc.CellDebugKV(colName))
+			}
+		} else {
+			err := sp.parseIncellMapWithSimpleKV(field, reflectMap, cell.Data)
+			if err != nil {
+				return false, xerrors.WithMessageKV(err, rc.CellDebugKV(colName))
 			}
 		}
 	}
@@ -619,6 +592,115 @@ func (sp *sheetParser) parseMapField(field *Field, msg protoreflect.Message, rc
 	return present, nil
 }
 
+// parseIncellMapWithSimpleKV parses simple incell map with key as scalar type and value as scalar or enum type.
+// For example:
+//  - map<int32, int32>
+//  - map<int32, EnumType>
+func (sp *sheetParser) parseIncellMapWithSimpleKV(field *Field, reflectMap protoreflect.Map, cellData string) (err error) {
+	if cellData == "" {
+		return nil
+	}
+	// If s does not contain sep and sep is not empty, Split returns a
+	// slice of length 1 whose only element is s.
+	keyFd := field.fd.MapKey()
+	valueFd := field.fd.MapValue()
+	splits := strings.Split(cellData, field.opts.Sep)
+	size := len(splits)
+	for i := 0; i < size; i++ {
+		kv := strings.SplitN(splits[i], field.opts.Subsep, 2)
+		if len(kv) == 1 {
+			// If value is not set, then treated it as default empty string.
+			kv = append(kv, "")
+		}
+		key, value := kv[0], kv[1]
+
+		fieldValue, keyPresent, err := sp.parseFieldValue(keyFd, key)
+		if err != nil {
+			return xerrors.WithMessageKV(err, xerrors.KeyPBFieldType, "incell map")
+		}
+
+		newMapKey := fieldValue.MapKey()
+		fieldValue, valuePresent, err := sp.parseFieldValue(valueFd, value)
+		if err != nil {
+			return xerrors.WithMessageKV(err, xerrors.KeyPBFieldType, "incell map")
+		}
+		newMapValue := fieldValue
+
+		if !keyPresent && !valuePresent {
+			// key and value are both not present.
+			// TODO: check the remaining keys all not present, otherwise report error!
+			break
+		}
+		reflectMap.Set(newMapKey, newMapValue)
+	}
+	return nil
+}
+
+// parseIncellMapWithValueAsSimpleKVMessage parses simple incell map with key as scalar or enum type
+// and value as simple message with only key and value fields. For example:
+//
+//  enum FruitType {
+//    FRUIT_TYPE_UNKNOWN = 0 [(tableau.evalue).name = "Unknown"];
+//    FRUIT_TYPE_APPLE   = 1 [(tableau.evalue).name = "Apple"];
+//    FRUIT_TYPE_ORANGE  = 2 [(tableau.evalue).name = "Orange"];
+//    FRUIT_TYPE_BANANA  = 3 [(tableau.evalue).name = "Banana"];
+//  }
+//  enum FruitFlavor {
+//    FRUIT_FLAVOR_UNKNOWN = 0 [(tableau.evalue).name = "Unknown"];
+//    FRUIT_FLAVOR_FRAGRANT = 1 [(tableau.evalue).name = "Fragrant"];
+//    FRUIT_FLAVOR_SOUR = 2 [(tableau.evalue).name = "Sour"];
+//    FRUIT_FLAVOR_SWEET = 3 [(tableau.evalue).name = "Sweet"];
+//  }
+//
+//  map<int32, Fruit> fruit_map = 1 [(tableau.field) = {name:"Fruit" key:"Key" layout:LAYOUT_INCELL}];
+//  message Fruit {
+//    FruitType key = 1 [(tableau.field) = {name:"Key"}];
+//    int64 value = 2 [(tableau.field) = {name:"Value"}];
+// 	}
+//
+//  map<int32, Item> item_map = 3 [(tableau.field) = {name:"Item" key:"Key" layout:LAYOUT_INCELL}];
+//  message Item {
+//    FruitType key = 1 [(tableau.field) = {name:"Key"}];
+//    FruitFlavor value = 2 [(tableau.field) = {name:"Value"}];
+//  }
+func (sp *sheetParser) parseIncellMapWithValueAsSimpleKVMessage(field *Field, reflectMap protoreflect.Map, cellData string) (err error) {
+	if cellData == "" {
+		return nil
+	}
+	// If s does not contain sep and sep is not empty, Split returns a
+	// slice of length 1 whose only element is s.
+	splits := strings.Split(cellData, field.opts.Sep)
+	size := len(splits)
+	for i := 0; i < size; i++ {
+		mapItemData := splits[i]
+		kv := strings.SplitN(mapItemData, field.opts.Subsep, 2)
+		if len(kv) == 1 {
+			// If value is not set, then treated it as default empty string.
+			kv = append(kv, "")
+		}
+		keyData := kv[0]
+
+		newMapKey, keyPresent, err := sp.parseMapKey(field, reflectMap, keyData)
+		if err != nil {
+			return xerrors.WithMessageKV(err, xerrors.KeyPBFieldType, "incell map")
+		}
+
+		newMapValue := reflectMap.NewValue()
+		valuePresent, err := sp.parseIncellStruct(newMapValue, mapItemData, field.opts.Subsep)
+		if err != nil {
+			return xerrors.WithMessageKV(err, xerrors.KeyPBFieldType, "incell struct list")
+		}
+
+		if !keyPresent && !valuePresent {
+			// key and value are both not present.
+			// TODO: check the remaining keys all not present, otherwise report error!
+			break
+		}
+		reflectMap.Set(newMapKey, newMapValue)
+	}
+	return nil
+}
+
 func (sp *sheetParser) parseMapKey(field *Field, reflectMap protoreflect.Map, cellData string) (mapKey protoreflect.MapKey, present bool, err error) {
 	var keyFd protoreflect.FieldDescriptor
 
diff --git a/internal/protogen/exporter.go b/internal/protogen/exporter.go
index 11247dc..b807b01 100644
--- a/internal/protogen/exporter.go
+++ b/internal/protogen/exporter.go
@@ -49,7 +49,7 @@ func (x *bookExporter) export(checkProtoFileConflicts bool) error {
 	g1.P("")
 	g1.P("package ", x.ProtoPackage, ";")
 	g1.P("")
-	
+
 	// keep the elements ordered by import path
 	set := treeset.NewWithStringComparator()
 	set.Add(tableauProtoPath) // default must be imported path
@@ -130,7 +130,7 @@ type sheetExporter struct {
 	ws          *tableaupb.Worksheet
 	g           *GeneratedBuf
 	isLastSheet bool
-	typeInfos   map[string]*xproto.TypeInfo
+	typeInfos   xproto.TypeInfoMap
 
 	Imports        map[string]bool             // import name -> defined
 	nestedMessages map[string]*tableaupb.Field // type name -> field
diff --git a/internal/protogen/parser.go b/internal/protogen/parser.go
index c14613a..bdc7843 100644
--- a/internal/protogen/parser.go
+++ b/internal/protogen/parser.go
@@ -9,6 +9,7 @@ import (
 	"github.com/tableauio/tableau/internal/fs"
 	"github.com/tableauio/tableau/internal/protogen/parseroptions"
 	"github.com/tableauio/tableau/internal/types"
+	"github.com/tableauio/tableau/internal/xproto"
 	"github.com/tableauio/tableau/log"
 	"github.com/tableauio/tableau/proto/tableaupb"
 	"github.com/tableauio/tableau/xerrors"
@@ -28,9 +29,7 @@ const (
 
 type bookParser struct {
 	gen *Generator
-
-	wb       *tableaupb.Workbook
-	withNote bool
+	wb  *tableaupb.Workbook
 }
 
 func newBookParser(bookName, relSlashPath string, gen *Generator) *bookParser {
@@ -53,7 +52,6 @@ func newBookParser(bookName, relSlashPath string, gen *Generator) *bookParser {
 			Name:       filename,
 			Imports:    make(map[string]int32),
 		},
-		withNote: false,
 	}
 
 	// custom imported proto files
@@ -66,7 +64,6 @@ func newBookParser(bookName, relSlashPath string, gen *Generator) *bookParser {
 func (p *bookParser) parseField(field *tableaupb.Field, header *sheetHeader, cursor int, prefix string, options ...parseroptions.Option) (cur int, parsed bool, err error) {
 	nameCell := header.getValidNameCell(&cursor)
 	typeCell := header.getTypeCell(cursor)
-	noteCell := header.getNoteCell(cursor)
 	// log.Debugf("column: %d, name: %s, type: %s", cursor, nameCell, typeCell)
 	if nameCell == "" || typeCell == "" {
 		log.Debugf("no need to parse column %d, as name(%s) or type(%s) is empty", cursor, nameCell, typeCell)
@@ -95,7 +92,7 @@ func (p *bookParser) parseField(field *tableaupb.Field, header *sheetHeader, cur
 	} else {
 		// scalar or enum type
 		trimmedNameCell := strings.TrimPrefix(nameCell, prefix)
-		scalarField, err := p.parseScalarField(trimmedNameCell, typeCell, noteCell)
+		scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, trimmedNameCell, typeCell)
 		if err != nil {
 			return cursor, false, xerrors.WithMessageKV(err, xerrors.KeyPBFieldType, "scalar/enum", xerrors.KeyTypeCell, typeCell)
 		}
@@ -132,7 +129,6 @@ func (p *bookParser) parseMapField(field *tableaupb.Field, header *sheetHeader,
 	if opts.GetVTypeCell(cursor) != "" {
 		typeCell = opts.GetVTypeCell(cursor)
 	}
-	noteCell := header.getNoteCell(cursor)
 
 	// map syntax pattern
 	matches := types.MatchMap(typeCell)
@@ -145,7 +141,8 @@ func (p *bookParser) parseMapField(field *tableaupb.Field, header *sheetHeader,
 		// NOTE: support enum as map key, convert key type as `int32`.
 		parsedKeyType = "int32"
 	}
-	valueTypeDesc, err := p.parseType(valueType)
+
+	valueTypeDesc, err := parseTypeDescriptor(p.gen.typeInfos, valueType)
 	if err != nil {
 		return cursor, xerrors.WithMessageKV(err,
 			xerrors.KeyPBFieldType, valueType+" (map value)",
@@ -155,7 +152,7 @@ func (p *bookParser) parseMapField(field *tableaupb.Field, header *sheetHeader,
 	mapType := fmt.Sprintf("map<%s, %s>", parsedKeyType, valueTypeDesc.Name)
 	fullMapType := fmt.Sprintf("map<%s, %s>", parsedKeyType, valueTypeDesc.FullName)
 
-	isScalarValue := types.IsScalarType(valueTypeDesc.Name)
+	mapValueKind := valueTypeDesc.Kind
 	trimmedNameCell := strings.TrimPrefix(nameCell, prefix)
 
 	// preprocess: analyze the correct layout of map.
@@ -178,19 +175,19 @@ func (p *bookParser) parseMapField(field *tableaupb.Field, header *sheetHeader,
 				nextTypeCell := header.getTypeCell(nextCursor)
 				if types.IsMap(nextTypeCell) {
 					// The next type cell is also a map type declaration.
-					if isScalarValue {
+					if mapValueKind == types.ScalarKind || mapValueKind == types.EnumKind {
 						layout = tableaupb.Layout_LAYOUT_INCELL // incell map
 					}
 				}
 			} else {
 				// only one map item, treat it as incell map
-				if isScalarValue {
+				if mapValueKind == types.ScalarKind || mapValueKind == types.EnumKind {
 					layout = tableaupb.Layout_LAYOUT_INCELL // incell map
 				}
 			}
 		}
 	} else {
-		if isScalarValue {
+		if mapValueKind == types.ScalarKind || mapValueKind == types.EnumKind {
 			layout = tableaupb.Layout_LAYOUT_INCELL // incell map
 		}
 	}
@@ -232,7 +229,7 @@ func (p *bookParser) parseMapField(field *tableaupb.Field, header *sheetHeader,
 		if opts.Nested {
 			field.Options.Name = valueTypeDesc.Name
 		}
-		scalarField, err := p.parseScalarField(trimmedNameCell, keyType+rawPropText, noteCell)
+		scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, trimmedNameCell, keyType+rawPropText)
 		if err != nil {
 			return cursor, xerrors.WithMessageKV(err,
 				xerrors.KeyPBFieldType, keyType+" (map key)",
@@ -285,7 +282,7 @@ func (p *bookParser) parseMapField(field *tableaupb.Field, header *sheetHeader,
 			Layout: layout,
 			Prop:   ExtractMapFieldProp(prop),
 		}
-		scalarField, err := p.parseScalarField(trimmedNameCell, keyType+rawPropText, noteCell)
+		scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, trimmedNameCell, keyType+rawPropText)
 		if err != nil {
 			return cursor, xerrors.WithMessageKV(err,
 				xerrors.KeyPBFieldType, keyType+" (map key)",
@@ -317,16 +314,37 @@ func (p *bookParser) parseMapField(field *tableaupb.Field, header *sheetHeader,
 	case tableaupb.Layout_LAYOUT_INCELL:
 		// incell map
 		trimmedNameCell := strings.TrimPrefix(nameCell, prefix)
+		valuePredefined := valueTypeDesc.Predefined
+		parsedValueName := valueTypeDesc.Name
+		parsedValueFullName := valueTypeDesc.FullName
+
+		keyTypeDesc, err := parseTypeDescriptor(p.gen.typeInfos, keyType)
+		if err != nil {
+			return cursor, xerrors.WithMessageKV(err,
+				xerrors.KeyPBFieldType, valueType+" (map key)",
+				xerrors.KeyPBFieldOpts, rawPropText)
+		}
+
+		// special process for key as enum type
+		if keyTypeDesc.Kind == types.EnumKind {
+			valuePredefined = false
+			parsedValueName = trimmedNameCell
+			parsedValueFullName = trimmedNameCell
+			mapType = fmt.Sprintf("map<%s, %s>", parsedKeyType, parsedValueName)
+			fullMapType = mapType
+		}
+
 		// auto add suffix "_map".
 		field.Name = strcase.ToSnake(trimmedNameCell) + mapVarSuffix
 		field.Type = mapType
 		field.FullType = fullMapType
-		// For map type, Predefined indicates the ValueType of map has been defined.
-		field.Predefined = valueTypeDesc.Predefined
+		// For map type, Predefined indicates the ValueType of map has already
+		// been defined before.
+		field.Predefined = valuePredefined
 		field.MapEntry = &tableaupb.Field_MapEntry{
 			KeyType:       parsedKeyType,
-			ValueType:     valueTypeDesc.Name,
-			ValueFullType: valueTypeDesc.FullName,
+			ValueType:     parsedValueName,
+			ValueFullType: parsedValueFullName,
 		}
 		prop, err := types.ParseProp(rawPropText)
 		if err != nil {
@@ -340,6 +358,29 @@ func (p *bookParser) parseMapField(field *tableaupb.Field, header *sheetHeader,
 			Layout: layout,
 			Prop:   prop, // for incell scalar map, need whole prop
 		}
+
+		// special process for key as enum type: create a new simple KV message as map value type.
+		if keyTypeDesc.Kind == types.EnumKind {
+			field.Options.Key = types.DefaultMapKeyOptName
+
+			scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, types.DefaultMapKeyOptName, keyType+rawPropText)
+			if err != nil {
+				return cursor, xerrors.WithMessageKV(err,
+					xerrors.KeyPBFieldType, keyType+" (map key)",
+					xerrors.KeyPBFieldOpts, rawPropText,
+					xerrors.KeyTrimmedNameCell, trimmedNameCell)
+			}
+			field.Fields = append(field.Fields, scalarField)
+
+			scalarField, err = parseScalarOrEnumField(p.gen.typeInfos, types.DefaultMapValueOptName, valueType)
+			if err != nil {
+				return cursor, xerrors.WithMessageKV(err,
+					xerrors.KeyPBFieldType, valueType+" (map value)",
+					xerrors.KeyPBFieldOpts, rawPropText,
+					xerrors.KeyTrimmedNameCell, trimmedNameCell)
+			}
+			field.Fields = append(field.Fields, scalarField)
+		}
 	case tableaupb.Layout_LAYOUT_DEFAULT:
 		return cursor, xerrors.Errorf("should not reach default layout: %v", layout)
 	}
@@ -355,7 +396,6 @@ func (p *bookParser) parseListField(field *tableaupb.Field, header *sheetHeader,
 	if opts.GetVTypeCell(cursor) != "" {
 		typeCell = opts.GetVTypeCell(cursor)
 	}
-	noteCell := header.getNoteCell(cursor)
 
 	trimmedNameCell := strings.TrimPrefix(nameCell, prefix)
 
@@ -383,7 +423,7 @@ func (p *bookParser) parseListField(field *tableaupb.Field, header *sheetHeader,
 				// incell predefined struct
 				listElemSpanInnerCell = true
 				elemType = structType
-				typeDesc, err := p.parseType(structType)
+				typeDesc, err := parseTypeDescriptor(p.gen.typeInfos, structType)
 				if err != nil {
 					return cursor, xerrors.WithMessageKV(err,
 						xerrors.KeyPBFieldType, structType,
@@ -435,7 +475,7 @@ func (p *bookParser) parseListField(field *tableaupb.Field, header *sheetHeader,
 	switch layout {
 	case tableaupb.Layout_LAYOUT_VERTICAL:
 		// vertical list: all columns belong to this list after this cursor.
-		scalarField, err := p.parseScalarField(trimmedNameCell, elemType, noteCell)
+		scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, trimmedNameCell, elemType)
 		if err != nil {
 			return cursor, xerrors.WithMessageKV(err,
 				xerrors.KeyPBFieldType, elemType+" (list element)",
@@ -518,7 +558,7 @@ func (p *bookParser) parseListField(field *tableaupb.Field, header *sheetHeader,
 		listName := trimmedNameCell[:firstElemIndex]
 		prefix += listName
 
-		scalarField, err := p.parseScalarField(trimmedNameCell, elemType, noteCell)
+		scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, trimmedNameCell, elemType)
 		if err != nil {
 			return cursor, xerrors.WithMessageKV(err,
 				xerrors.KeyPBFieldType, elemType+" (list element)",
@@ -600,7 +640,7 @@ func (p *bookParser) parseListField(field *tableaupb.Field, header *sheetHeader,
 			key = trimmedNameCell
 		}
 		colTypeWithProp := colType + rawPropText
-		scalarField, err := p.parseScalarField(trimmedNameCell, colTypeWithProp, noteCell)
+		scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, trimmedNameCell, colTypeWithProp)
 		if err != nil {
 			return cursor, xerrors.WithMessageKV(err,
 				xerrors.KeyPBFieldType, colType,
@@ -643,21 +683,20 @@ func (p *bookParser) parseStructField(field *tableaupb.Field, header *sheetHeade
 	if opts.GetVTypeCell(cursor) != "" {
 		typeCell = opts.GetVTypeCell(cursor)
 	}
-	noteCell := header.getNoteCell(cursor)
 
 	trimmedNameCell := strings.TrimPrefix(nameCell, prefix)
 
 	// struct syntax pattern
 	matches := types.MatchStruct(typeCell)
-	elemType := strings.TrimSpace(matches[1])
+	structType := strings.TrimSpace(matches[1])
 	colType := strings.TrimSpace(matches[2])
 	rawPropText := strings.TrimSpace(matches[3])
 	if colType == "" {
 		// incell predefined struct
-		scalarField, err := p.parseScalarField(trimmedNameCell, elemType, noteCell)
+		scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, trimmedNameCell, structType)
 		if err != nil {
 			return cursor, xerrors.WithMessageKV(err,
-				xerrors.KeyPBFieldType, elemType,
+				xerrors.KeyPBFieldType, structType,
 				xerrors.KeyPBFieldOpts, rawPropText,
 				xerrors.KeyTrimmedNameCell, trimmedNameCell)
 		}
@@ -666,12 +705,12 @@ func (p *bookParser) parseStructField(field *tableaupb.Field, header *sheetHeade
 		return cursor, nil
 	}
 
-	fieldPairs, err := ParseIncellStruct(elemType)
+	fieldPairs, err := parseIncellStruct(structType)
 	if err != nil {
 		return cursor, err
 	}
 	if fieldPairs != nil {
-		scalarField, err := p.parseScalarField(trimmedNameCell, colType, noteCell)
+		scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, trimmedNameCell, colType)
 		if err != nil {
 			return cursor, xerrors.WithMessageKV(err,
 				xerrors.KeyPBFieldType, colType,
@@ -684,7 +723,7 @@ func (p *bookParser) parseStructField(field *tableaupb.Field, header *sheetHeade
 		for i := 0; i < len(fieldPairs); i += 2 {
 			fieldType := fieldPairs[i]
 			fieldName := fieldPairs[i+1]
-			scalarField, err := p.parseScalarField(fieldName, fieldType, "")
+			scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, fieldName, fieldType)
 			if err != nil {
 				return cursor, xerrors.WithMessageKV(err,
 					xerrors.KeyPBFieldType, fieldType,
@@ -696,10 +735,10 @@ func (p *bookParser) parseStructField(field *tableaupb.Field, header *sheetHeade
 	} else {
 		// cross cell struct
 		// NOTE(wenchy): treated as nested named struct
-		scalarField, err := p.parseScalarField(trimmedNameCell, elemType, noteCell)
+		scalarField, err := parseScalarOrEnumField(p.gen.typeInfos, trimmedNameCell, structType)
 		if err != nil {
 			return cursor, xerrors.WithMessageKV(err,
-				xerrors.KeyPBFieldType, elemType,
+				xerrors.KeyPBFieldType, structType,
 				xerrors.KeyPBFieldOpts, rawPropText,
 				xerrors.KeyTrimmedNameCell, trimmedNameCell)
 		}
@@ -759,7 +798,7 @@ func (p *bookParser) parseStructField(field *tableaupb.Field, header *sheetHeade
 	return cursor, nil
 }
 
-func (p *bookParser) parseScalarField(name, typ, note string) (*tableaupb.Field, error) {
+func parseScalarOrEnumField(typeInfos xproto.TypeInfoMap, name, typ string) (*tableaupb.Field, error) {
 	rawPropText := ""
 	// enum syntax pattern
 	if matches := types.MatchEnum(typ); len(matches) > 0 {
@@ -774,7 +813,7 @@ func (p *bookParser) parseScalarField(name, typ, note string) (*tableaupb.Field,
 			rawPropText = splits[1]
 		}
 	}
-	typeDesc, err := p.parseType(typ)
+	typeDesc, err := parseTypeDescriptor(typeInfos, typ)
 	if err != nil {
 		return nil, xerrors.WithMessageKV(err,
 			xerrors.KeyPBFieldOpts, rawPropText,
@@ -797,34 +836,28 @@ func (p *bookParser) parseScalarField(name, typ, note string) (*tableaupb.Field,
 		Predefined: typeDesc.Predefined,
 		Options: &tableaupb.FieldOptions{
 			Name: name,
-			Note: p.genNote(note),
+			Note: "", // no need to add note now, maybe will be deprecated in the future.
 			Prop: ExtractScalarFieldProp(prop),
 		},
 	}, nil
 }
 
-func (p *bookParser) genNote(note string) string {
-	if p.withNote {
-		return note
+func parseTypeDescriptor(typeInfos xproto.TypeInfoMap, rawType string) (*types.Descriptor, error) {
+	// enum syntax pattern
+	if matches := types.MatchEnum(rawType); len(matches) > 0 {
+		enumType := strings.TrimSpace(matches[1])
+		rawType = enumType
 	}
-	return ""
-}
-
-type typeDesc struct {
-	Name       string
-	FullName   string
-	Predefined bool
-}
 
-func (p *bookParser) parseType(rawType string) (*typeDesc, error) {
 	if strings.HasPrefix(rawType, ".") {
 		// This messge type is defined in imported proto
 		name := strings.TrimPrefix(rawType, ".")
-		if typeInfo, ok := p.gen.typeInfos[name]; ok {
-			return &typeDesc{
+		if typeInfo, ok := typeInfos[name]; ok {
+			return &types.Descriptor{
 				Name:       name,
 				FullName:   typeInfo.Fullname,
 				Predefined: true,
+				Kind:       typeInfo.Kind,
 			}, nil
 		} else {
 			return nil, xerrors.Errorf("predefined type not found: %s", name)
@@ -832,38 +865,49 @@ func (p *bookParser) parseType(rawType string) (*typeDesc, error) {
 	}
 	switch rawType {
 	case "datetime", "date":
-		return &typeDesc{
+		return &types.Descriptor{
 			Name:       "google.protobuf.Timestamp",
 			FullName:   "google.protobuf.Timestamp",
 			Predefined: true,
+			Kind:       types.ScalarKind,
 		}, nil
 	case "time", "duration":
-		return &typeDesc{
+		return &types.Descriptor{
 			Name:       "google.protobuf.Duration",
 			FullName:   "google.protobuf.Duration",
 			Predefined: true,
+			Kind:       types.ScalarKind,
 		}, nil
 	default:
-		return &typeDesc{
+		desc := &types.Descriptor{
 			Name:       rawType,
 			FullName:   rawType,
 			Predefined: false,
-		}, nil
+		}
+		if types.IsScalarType(desc.Name) {
+			desc.Kind = types.ScalarKind
+		} else {
+			desc.Kind = types.MessageKind
+		}
+		return desc, nil
 	}
 }
 
-func ParseIncellStruct(elemType string) ([]string, error) {
-	fields := strings.Split(elemType, ",")
+// parseIncellStruct parses incell struct type definition. For example:
+//  - int32 ID
+//  - int32 ID, string Name
+func parseIncellStruct(structType string) ([]string, error) {
+	fields := strings.Split(structType, ",")
 	if len(fields) == 1 && len(strings.Split(fields[0], " ")) == 1 {
 		// cross cell struct
 		return nil, nil
 	}
 
 	fieldPairs := make([]string, 0)
-	for _, pair := range strings.Split(elemType, ",") {
+	for _, pair := range strings.Split(structType, ",") {
 		kv := strings.Split(strings.TrimSpace(pair), " ")
 		if len(kv) != 2 {
-			return nil, xerrors.Errorf("illegal type-variable pair: %v in incell struct: %s", pair, elemType)
+			return nil, xerrors.Errorf("illegal type-variable pair: %v in incell struct: %s", pair, structType)
 		}
 		fieldPairs = append(fieldPairs, kv...)
 	}
diff --git a/internal/protogen/protogen.go b/internal/protogen/protogen.go
index da3c01d..6fb6621 100644
--- a/internal/protogen/protogen.go
+++ b/internal/protogen/protogen.go
@@ -39,8 +39,8 @@ type Generator struct {
 	OutputOpt    *options.ProtoOutputOption
 
 	// internal
-	fileDescs []*desc.FileDescriptor      // all parsed imported proto file descriptors.
-	typeInfos map[string]*xproto.TypeInfo // proto full type name -> type info
+	fileDescs []*desc.FileDescriptor // all parsed imported proto file descriptors.
+	typeInfos xproto.TypeInfoMap     // proto full type name -> type info
 }
 
 func NewGenerator(protoPackage, indir, outdir string, setters ...options.Option) *Generator {
diff --git a/internal/types/list.go b/internal/types/list.go
new file mode 100644
index 0000000..7a15f40
--- /dev/null
+++ b/internal/types/list.go
@@ -0,0 +1,3 @@
+package types
+
+const DefaultListFieldOptNameSuffix = "List"
diff --git a/internal/types/map.go b/internal/types/map.go
new file mode 100644
index 0000000..fda7114
--- /dev/null
+++ b/internal/types/map.go
@@ -0,0 +1,27 @@
+package types
+
+import (
+	"github.com/tableauio/tableau/proto/tableaupb"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
+)
+
+const DefaultMapFieldOptNameSuffix = "Map"
+
+const DefaultMapKeyOptName = "Key"
+const DefaultMapValueOptName = "Value"
+
+func CheckMessageWithOnlyKVFields(msg protoreflect.Message) bool {
+	md := msg.Descriptor()
+	if md.Fields().Len() == 2 {
+		keyFd := md.Fields().Get(0)
+		valFd := md.Fields().Get(1)
+		return expectFieldOptName(keyFd, DefaultMapKeyOptName) && expectFieldOptName(valFd, DefaultMapValueOptName)
+	}
+	return false
+}
+
+func expectFieldOptName(fd protoreflect.FieldDescriptor, name string) bool {
+	fdOpts := proto.GetExtension(fd.Options(), tableaupb.E_Field).(*tableaupb.FieldOptions)
+	return fdOpts != nil && fdOpts.Name == name
+}
diff --git a/internal/types/types.go b/internal/types/types.go
index abad481..a971a6a 100644
--- a/internal/types/types.go
+++ b/internal/types/types.go
@@ -29,11 +29,11 @@ const ungreedyTypeGroup = `(` + typeCharSet + `*?)`
 const TypeGroup = ungreedyTypeGroup
 
 func init() {
-	mapRegexp = regexp.MustCompile(`^map<` + typeGroup + `,` + typeGroup + `>` + rawPropGroup)               // e.g.: map<uint32,Type>
-	listRegexp = regexp.MustCompile(`^\[` + ungreedyTypeGroup + `\]` + typeGroup + rawPropGroup)             // e.g.: [Type]uint32
-	keyedListRegexp = regexp.MustCompile(`^\[` + ungreedyTypeGroup + `\]<` + typeGroup + `>` + rawPropGroup) // e.g.: [Type]<uint32>
-	structRegexp = regexp.MustCompile(`^\{` + ungreedyTypeGroup + `\}` + looseTypeGroup + rawPropGroup)      // e.g.: {Type}uint32
-	enumRegexp = regexp.MustCompile(`^enum<` + typeGroup + `>` + rawPropGroup)                               // e.g.: enum<Type>
+	mapRegexp = regexp.MustCompile(`^map<` + typeGroup + `,` + typeGroup + `>` + rawPropGroup)               // e.g.: map<uint32, Type>, map<uint32, .PredefinedType>
+	listRegexp = regexp.MustCompile(`^\[` + ungreedyTypeGroup + `\]` + typeGroup + rawPropGroup)             // e.g.: [Type]uint32, [.PredefinedType]uint32
+	keyedListRegexp = regexp.MustCompile(`^\[` + ungreedyTypeGroup + `\]<` + typeGroup + `>` + rawPropGroup) // e.g.: [Type]<uint32>, [.PredefinedType]<uint32>
+	structRegexp = regexp.MustCompile(`^\{` + ungreedyTypeGroup + `\}` + looseTypeGroup + rawPropGroup)      // e.g.: {Type}uint32, {.PredefinedType}uint32
+	enumRegexp = regexp.MustCompile(`^enum<` + typeGroup + `>` + rawPropGroup)                               // e.g.: enum<Type>, enum<.PredefinedType>
 	propRegexp = regexp.MustCompile(`\|?\{(.+)\}`)                                                           // e.g.: |{range:"1,10" refer:"XXXConf.ID"}
 
 	// trim float to integer after(include) dot, e.g: 0.0, 1.0, 1.00 ...
@@ -41,42 +41,64 @@ func init() {
 	boringIntegerRegexp = regexp.MustCompile(`([-+]?[0-9]+)\.0+$`)
 }
 
+// MatchMap matches the map type patterns. For example:
+//  - map<KeyType, ValueType>
+//  - map<KeyType, .PredefinedValueType>
+//  - map<.PredefinedKeyType, ValueType>
+//  - map<.PredefinedKeyType, .PredefinedValueType>
 func MatchMap(text string) []string {
 	return mapRegexp.FindStringSubmatch(text)
 }
 
+// IsMap checks if text matches the map type patterns.
 func IsMap(text string) bool {
 	return MatchMap(text) != nil
 }
 
+// MatchList matches the list type patterns. For example:
+//  - [ElemType]Type
+//  - [.PredefinedElemType]Type
 func MatchList(text string) []string {
 	return listRegexp.FindStringSubmatch(text)
 }
 
+// IsList checks if text matches the list type patterns.
 func IsList(text string) bool {
 	return MatchList(text) != nil
 }
 
+// MatchKeyedList matches the keyed list type patterns. For example:
+//  - [ElemType]<Type>
+//  - [.PredefinedElemType]<Type>
 func MatchKeyedList(text string) []string {
 	return keyedListRegexp.FindStringSubmatch(text)
 }
 
+// IsKeyedList checks if text matches the keyed list type patterns.
 func IsKeyedList(text string) bool {
 	return MatchKeyedList(text) != nil
 }
 
+// MatchStruct matches the struct type patterns. For example:
+//  - {StructType}Type
+//  - {.PredefinedStructType}Type
 func MatchStruct(text string) []string {
 	return structRegexp.FindStringSubmatch(text)
 }
 
+// IsStruct checks if text matches the struct type patterns.
 func IsStruct(text string) bool {
 	return MatchStruct(text) != nil
 }
 
+// MatchEnum matches the enum type pattern. For example:
+//  - enum<Type>
+//  - enum<.PredefinedType>
 func MatchEnum(text string) []string {
 	return enumRegexp.FindStringSubmatch(text)
 }
 
+// IsEnum checks if text matches the enum type patterns.
 func IsEnum(text string) bool {
 	return MatchEnum(text) != nil
 }
@@ -134,7 +156,6 @@ var typeKindMap map[string]Kind
 func init() {
 	typeKindMap = map[string]Kind{
 		"bool":                      ScalarKind,
-		"enum":                      ScalarKind,
 		"int32":                     ScalarKind,
 		"sint32":                    ScalarKind,
 		"uint32":                    ScalarKind,
@@ -152,8 +173,9 @@ func init() {
 		"google.protobuf.Duration":  ScalarKind,
 		"google.protobuf.Timestamp": ScalarKind,
 
-		"repeated": ListKind,
-		"map":      MapKind,
+		// "enum":     EnumKind,
+		// "repeated": ListKind,
+		// "map":      MapKind,
 	}
 }
 
@@ -163,3 +185,11 @@ func IsScalarType(t string) bool {
 	}
 	return false
 }
+
+// Descriptor describes type metadata.
+type Descriptor struct {
+	Name       string
+	FullName   string
+	Predefined bool
+	Kind       Kind
+}
diff --git a/internal/xproto/build.go b/internal/xproto/build.go
index 51850ea..45c072c 100644
--- a/internal/xproto/build.go
+++ b/internal/xproto/build.go
@@ -7,6 +7,7 @@ import (
 	"github.com/jhump/protoreflect/desc/protoparse"
 	"github.com/pkg/errors"
 	"github.com/tableauio/tableau/internal/fs"
+	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/log"
 	_ "github.com/tableauio/tableau/proto/tableaupb"
 	"google.golang.org/protobuf/reflect/protodesc"
@@ -27,7 +28,7 @@ func ParseProtos(protoPaths []string, protoFiles ...string) ([]*desc.FileDescrip
 
 // NewFiles creates a new protoregistry.Files from the proto paths and proto Gob filenames.
 func NewFiles(protoPaths []string, protoFiles []string, excludeProtoFiles ...string) (*protoregistry.Files, error) {
-	 parsedExcludedProtoFiles := map[string]bool{}
+	parsedExcludedProtoFiles := map[string]bool{}
 	for _, filename := range excludeProtoFiles {
 		matches, err := filepath.Glob(filename)
 		if err != nil {
@@ -91,21 +92,26 @@ func NewFiles(protoPaths []string, protoFiles []string, excludeProtoFiles ...str
 type TypeInfo struct {
 	Fullname       string
 	ParentFilename string
+	Kind           types.Kind
 }
 
-func GetAllTypeInfo(fileDescs []*desc.FileDescriptor) map[string]*TypeInfo {
-	typeInfos := make(map[string]*TypeInfo)
+type TypeInfoMap map[string]*TypeInfo
+
+func GetAllTypeInfo(fileDescs []*desc.FileDescriptor) TypeInfoMap {
+	typeInfos := make(TypeInfoMap)
 	for _, fileDesc := range fileDescs {
 		for _, mt := range fileDesc.GetMessageTypes() {
 			typeInfos[mt.GetName()] = &TypeInfo{
 				Fullname:       mt.GetFullyQualifiedName(),
 				ParentFilename: fileDesc.GetName(),
+				Kind:           types.MessageKind,
 			}
 		}
 		for _, mt := range fileDesc.GetEnumTypes() {
 			typeInfos[mt.GetName()] = &TypeInfo{
 				Fullname:       mt.GetFullyQualifiedName(),
 				ParentFilename: fileDesc.GetName(),
+				Kind:           types.EnumKind,
 			}
 		}
 	}
diff --git a/proto/tableaupb/metabook.pb.go b/proto/tableaupb/metabook.pb.go
index e224885..7542d26 100644
--- a/proto/tableaupb/metabook.pb.go
+++ b/proto/tableaupb/metabook.pb.go
@@ -4,7 +4,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.27.1
-// 	protoc        v3.21.5
+// 	protoc        v3.21.12
 // source: tableau/protobuf/metabook.proto
 
 package tableaupb
diff --git a/proto/tableaupb/tableau.pb.go b/proto/tableaupb/tableau.pb.go
index 28e4479..d219a97 100644
--- a/proto/tableaupb/tableau.pb.go
+++ b/proto/tableaupb/tableau.pb.go
@@ -4,7 +4,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.27.1
-// 	protoc        v3.21.5
+// 	protoc        v3.21.12
 // source: tableau/protobuf/tableau.proto
 
 package tableaupb
diff --git a/proto/tableaupb/workbook.pb.go b/proto/tableaupb/workbook.pb.go
index 9655785..d1fc9f7 100644
--- a/proto/tableaupb/workbook.pb.go
+++ b/proto/tableaupb/workbook.pb.go
@@ -4,7 +4,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.27.1
-// 	protoc        v3.21.5
+// 	protoc        v3.21.12
 // source: tableau/protobuf/workbook.proto
 
 package tableaupb
diff --git a/proto/tableaupb/xmlbook.pb.go b/proto/tableaupb/xmlbook.pb.go
index 6758347..bf93b78 100644
--- a/proto/tableaupb/xmlbook.pb.go
+++ b/proto/tableaupb/xmlbook.pb.go
@@ -4,7 +4,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.27.1
-// 	protoc        v3.21.5
+// 	protoc        v3.21.12
 // source: tableau/protobuf/xmlbook.proto
 
 package tableaupb


Test patch:
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 057375f..d7f9bdf 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -34,6 +34,7 @@ jobs:
         uses: arduino/setup-protoc@v1
         with:
           version: "3.x"
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Installing protoc-gen-go
         run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1
diff --git a/internal/confgen/prop/prop_test.go b/internal/confgen/prop/prop_test.go
new file mode 100644
index 0000000..61f1955
--- /dev/null
+++ b/internal/confgen/prop/prop_test.go
@@ -0,0 +1,134 @@
+package prop
+
+import (
+	"testing"
+
+	"github.com/tableauio/tableau/proto/tableaupb"
+)
+
+func TestCheckKeyUnique(t *testing.T) {
+	type args struct {
+		prop    *tableaupb.FieldProp
+		key     string
+		existed bool
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "unique",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Unique: true,
+				},
+				key:     "100",
+				existed: false,
+			},
+			wantErr: false,
+		},
+		{
+			name: "not unique",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Unique: true,
+				},
+				key:     "100",
+				existed: true,
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := CheckKeyUnique(tt.args.prop, tt.args.key, tt.args.existed); (err != nil) != tt.wantErr {
+				t.Errorf("CheckKeyUnique() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestGetSize(t *testing.T) {
+	type args struct {
+		prop         *tableaupb.FieldProp
+		detectedSize int
+	}
+	tests := []struct {
+		name string
+		args args
+		want int
+	}{
+		{
+			name: "use specified size",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Size: 3,
+				},
+				detectedSize: 5,
+			},
+			want: 3,
+		},
+		{
+			name: "use detected size",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Fixed: true,
+				},
+				detectedSize: 5,
+			},
+			want: 5,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := GetSize(tt.args.prop, tt.args.detectedSize); got != tt.want {
+				t.Errorf("GetSize() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestIsFixed(t *testing.T) {
+	type args struct {
+		prop *tableaupb.FieldProp
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "set fixed prop as true",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Fixed: true,
+				},
+			},
+			want: true,
+		},
+		{
+			name: "set size prop as 3",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Size: 3,
+				},
+			},
+			want: true,
+		},
+		{
+			name: "not fixed",
+			args: args{
+				prop: &tableaupb.FieldProp{},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := IsFixed(tt.args.prop); got != tt.want {
+				t.Errorf("IsFixed() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/internal/protogen/parser_test.go b/internal/protogen/parser_test.go
new file mode 100644
index 0000000..c86be89
--- /dev/null
+++ b/internal/protogen/parser_test.go
@@ -0,0 +1,212 @@
+package protogen
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/tableauio/tableau/internal/types"
+	"github.com/tableauio/tableau/internal/xproto"
+	"github.com/tableauio/tableau/proto/tableaupb"
+)
+
+func Test_parseScalarOrEnumField(t *testing.T) {
+	type args struct {
+		typeInfos xproto.TypeInfoMap
+		name      string
+		typ       string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *tableaupb.Field
+		wantErr bool
+	}{
+		{
+			name: "int32 ID",
+			args: args{
+				typeInfos: xproto.TypeInfoMap{},
+				name:      "ID",
+				typ:       "int32",
+			},
+			want: &tableaupb.Field{
+				Type:     "int32",
+				FullType: "int32",
+				Name:     "id",
+				Options: &tableaupb.FieldOptions{
+					Name: "ID",
+				},
+			},
+		},
+		{
+			name: "predefined enum type: ItemType",
+			args: args{
+				typeInfos: xproto.TypeInfoMap{
+					"ItemType": &xproto.TypeInfo{
+						Fullname:       "protoconf.ItemType",
+						ParentFilename: "common.proto",
+						Kind:           types.EnumKind,
+					},
+				},
+				name: "Type",
+				typ:  "enum<.ItemType>",
+			},
+			want: &tableaupb.Field{
+				Type:       "ItemType",
+				FullType:   "protoconf.ItemType",
+				Name:       "type",
+				Predefined: true,
+				Options: &tableaupb.FieldOptions{
+					Name: "Type",
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseScalarOrEnumField(tt.args.typeInfos, tt.args.name, tt.args.typ)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseScalarOrEnumField() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("parseScalarOrEnumField() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_parseTypeDescriptor(t *testing.T) {
+	type args struct {
+		typeInfos xproto.TypeInfoMap
+		rawType   string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *types.Descriptor
+		wantErr bool
+	}{
+		{
+			name: "scalar: int32",
+			args: args{
+				typeInfos: xproto.TypeInfoMap{},
+				rawType:   "int32",
+			},
+			want: &types.Descriptor{
+				Name:     "int32",
+				FullName: "int32",
+			},
+		},
+		{
+			name: "message: Item",
+			args: args{
+				typeInfos: xproto.TypeInfoMap{},
+				rawType:   "Item",
+			},
+			want: &types.Descriptor{
+				Name:     "Item",
+				FullName: "Item",
+				Kind:     types.MessageKind,
+			},
+		},
+		{
+			name: "predefined enum: ItemType",
+			args: args{
+				typeInfos: xproto.TypeInfoMap{
+					"ItemType": &xproto.TypeInfo{
+						Fullname:       "protoconf.ItemType",
+						ParentFilename: "common.proto",
+						Kind:           types.EnumKind,
+					},
+				},
+				rawType: "enum<.ItemType>",
+			},
+			want: &types.Descriptor{
+				Name:       "ItemType",
+				FullName:   "protoconf.ItemType",
+				Predefined: true,
+				Kind:       types.EnumKind,
+			},
+		},
+		{
+			name: "predefined message: Item",
+			args: args{
+				typeInfos: xproto.TypeInfoMap{
+					"Item": &xproto.TypeInfo{
+						Fullname:       "protoconf.Item",
+						ParentFilename: "common.proto",
+						Kind:           types.MessageKind,
+					},
+				},
+				rawType: ".Item",
+			},
+			want: &types.Descriptor{
+				Name:       "Item",
+				FullName:   "protoconf.Item",
+				Predefined: true,
+				Kind:       types.MessageKind,
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseTypeDescriptor(tt.args.typeInfos, tt.args.rawType)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseTypeDescriptor() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("name: %s, parseTypeDescriptor() = %v, want %v", tt.name, got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_parseIncellStruct(t *testing.T) {
+	type args struct {
+		structType string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    []string
+		wantErr bool
+	}{
+		{
+			name: "one field",
+			args: args{
+
+				structType: "int32 ID",
+			},
+			want: []string{"int32", "ID"},
+		},
+		{
+			name: "one field with space",
+			args: args{
+
+				structType: " int32 ID ",
+			},
+			want: []string{"int32", "ID"},
+		},
+		{
+			name: "two fields with space",
+			args: args{
+
+				structType: "int32 ID, string Name",
+			},
+			want: []string{"int32", "ID", "string", "Name"},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseIncellStruct(tt.args.structType)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseIncellStruct() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("parseIncellStruct() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/internal/types/map_test.go b/internal/types/map_test.go
new file mode 100644
index 0000000..756deef
--- /dev/null
+++ b/internal/types/map_test.go
@@ -0,0 +1,71 @@
+package types
+
+import (
+	"testing"
+
+	"github.com/tableauio/tableau/proto/tableaupb"
+	"google.golang.org/protobuf/reflect/protoreflect"
+)
+
+func TestCheckMessageWithOnlyKVFields(t *testing.T) {
+	type args struct {
+		msg protoreflect.Message
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "Fruit",
+			args: args{
+				msg: (&tableaupb.TestIncellMap_Fruit{}).ProtoReflect(),
+			},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := CheckMessageWithOnlyKVFields(tt.args.msg); got != tt.want {
+				t.Errorf("CheckMessageWithOnlyKVFields() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_expectFieldOptName(t *testing.T) {
+	msg := &tableaupb.TestIncellMap_Fruit{}
+	type args struct {
+		fd   protoreflect.FieldDescriptor
+		name string
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "Key",
+			args: args{
+				fd:   msg.ProtoReflect().Descriptor().Fields().ByNumber(1),
+				name: DefaultMapKeyOptName,
+			},
+			want: true,
+		},
+		{
+			name: "Value",
+			args: args{
+				fd:   msg.ProtoReflect().Descriptor().Fields().ByNumber(2),
+				name: DefaultMapValueOptName,
+			},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := expectFieldOptName(tt.args.fd, tt.args.name); got != tt.want {
+				t.Errorf("expectFieldOptName() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/internal/types/types_test.go b/internal/types/types_test.go
index a2dd054..c3ccfb5 100644
--- a/internal/types/types_test.go
+++ b/internal/types/types_test.go
@@ -255,28 +255,28 @@ func TestMatchStruct(t *testing.T) {
 		{
 			name: "new defined struct",
 			args: args{
-				text:   "{int32 Id, int32 Value}Prop",
+				text: "{int32 Id, int32 Value}Prop",
 			},
 			want: []string{"{int32 Id, int32 Value}Prop", "int32 Id, int32 Value", "Prop", ""},
 		},
 		{
 			name: "predefined cross cell struct",
 			args: args{
-				text:   "{.Item}int32",
+				text: "{.Item}int32",
 			},
 			want: []string{"{.Item}int32", ".Item", "int32", ""},
 		},
 		{
 			name: "predefined incell struct",
 			args: args{
-				text:   "{.Item}",
+				text: "{.Item}",
 			},
 			want: []string{"{.Item}", ".Item", "", ""},
 		},
 		{
 			name: "new defined cross cell struct with prop",
 			args: args{
-				text:   `{Item}int32|{range:"~,20"}`,
+				text: `{Item}int32|{range:"~,20"}`,
 			},
 			want: []string{`{Item}int32|{range:"~,20"}`, "Item", "int32", `|{range:"~,20"}`},
 		},
@@ -289,3 +289,42 @@ func TestMatchStruct(t *testing.T) {
 		})
 	}
 }
+
+func TestIsScalarType(t *testing.T) {
+	type args struct {
+		t string
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "int32",
+			args: args{t: "int32"},
+			want: true,
+		},
+		{
+			name: "google.protobuf.Duration",
+			args: args{t: "google.protobuf.Duration"},
+			want: true,
+		},
+		{
+			name: "google.protobuf.Duration",
+			args: args{t: "google.protobuf.Duration"},
+			want: true,
+		},
+		{
+			name: "MessageType",
+			args: args{t: "MessageType"},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := IsScalarType(tt.args.t); got != tt.want {
+				t.Errorf("IsScalarType() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/proto/tableau/protobuf/unittest.proto b/proto/tableau/protobuf/unittest.proto
new file mode 100644
index 0000000..c81c974
--- /dev/null
+++ b/proto/tableau/protobuf/unittest.proto
@@ -0,0 +1,52 @@
+// Protoconf - Tableau's data interchange format
+// https://tableauio.github.io/
+// A proto file we will use for unit testing.
+
+syntax = "proto3";
+
+package tableau;
+
+option go_package = "github.com/tableauio/tableau/proto/tableaupb";
+
+import "tableau/protobuf/tableau.proto";
+
+enum TestFruitType {
+  FRUIT_TYPE_UNKNOWN = 0 [(tableau.evalue).name = "Unknown"];
+  FRUIT_TYPE_APPLE = 1 [(tableau.evalue).name = "Apple"];
+  FRUIT_TYPE_ORANGE = 3 [(tableau.evalue).name = "Orange"];
+  FRUIT_TYPE_BANANA = 4 [(tableau.evalue).name = "Banana"];
+}
+
+enum TestFruitFlavor {
+  FRUIT_FLAVOR_UNKNOWN = 0 [(tableau.evalue).name = "Unknown"];
+  FRUIT_FLAVOR_FRAGRANT = 1 [(tableau.evalue).name = "Fragrant"];
+  FRUIT_FLAVOR_SOUR = 2 [(tableau.evalue).name = "Sour"];
+  FRUIT_FLAVOR_SWEET = 3 [(tableau.evalue).name = "Sweet"];
+}
+
+message TestItem {
+  int32 id = 1 [(tableau.field).name = "ID"];
+  int32 num = 2 [(tableau.field).name = "Num"];
+}
+
+message TestIncellMap {
+  option (worksheet) = {
+    name: "IncellMap"
+    namerow: 1
+    typerow: 2
+    noterow: 3
+    datarow: 4
+  };
+
+  map<int32, Fruit> fruit_map = 1 [(tableau.field) = { name: "Fruit" key: "Key" layout: LAYOUT_INCELL }];
+  message Fruit {
+    TestFruitType key = 1 [(tableau.field) = { name: "Key" }];
+    int64 value = 2 [(tableau.field) = { name: "Value" }];
+  }
+  map<int64, TestFruitFlavor> flavor_map = 2 [(tableau.field) = { name: "Flavor" layout: LAYOUT_INCELL }];
+  map<int32, Item> item_map = 3 [(tableau.field) = { name: "Item" key: "Key" layout: LAYOUT_INCELL }];
+  message Item {
+    TestFruitType key = 1 [(tableau.field) = { name: "Key" }];
+    TestFruitFlavor value = 2 [(tableau.field) = { name: "Value" }];
+  }
+}
\ No newline at end of file
diff --git a/test/functest/conf/IncellEnumMap.json b/test/functest/conf/IncellEnumMap.json
new file mode 100644
index 0000000..1b66c25
--- /dev/null
+++ b/test/functest/conf/IncellEnumMap.json
@@ -0,0 +1,26 @@
+{
+    "fruitMap": {
+        "1": {
+            "key": "FRUIT_TYPE_APPLE",
+            "value": "1"
+        },
+        "3": {
+            "key": "FRUIT_TYPE_ORANGE",
+            "value": "2"
+        }
+    },
+    "flavorMap": {
+        "1": "FRUIT_FLAVOR_FRAGRANT",
+        "2": "FRUIT_FLAVOR_SWEET"
+    },
+    "itemMap": {
+        "1": {
+            "key": "FRUIT_TYPE_APPLE",
+            "value": "FRUIT_FLAVOR_FRAGRANT"
+        },
+        "3": {
+            "key": "FRUIT_TYPE_ORANGE",
+            "value": "FRUIT_FLAVOR_SOUR"
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/functest/proto/common/common.proto b/test/functest/proto/common/common.proto
index 7068a45..4997675 100644
--- a/test/functest/proto/common/common.proto
+++ b/test/functest/proto/common/common.proto
@@ -1,19 +1,26 @@
-
-
-syntax = "proto3";
-package protoconf;
-option go_package = "github.com/tableauio/tableau/test/functest/protoconf/common";
-
-import "tableau/protobuf/tableau.proto";
-
-enum FruitType {
-  FRUIT_TYPE_UNKNOWN = 0 [(tableau.evalue).name = "Unknown"];
-  FRUIT_TYPE_APPLE   = 1 [(tableau.evalue).name = "Apple"];
-  FRUIT_TYPE_ORANGE  = 3 [(tableau.evalue).name = "Orange"];
-  FRUIT_TYPE_BANANA  = 4 [(tableau.evalue).name = "Banana"];
-}
-
-message Item {
-  int32 id = 1 [(tableau.field).name = "ID"];
-  int32 num = 2 [(tableau.field).name = "Num"];
-}
+
+
+syntax = "proto3";
+package protoconf;
+option go_package = "github.com/tableauio/tableau/test/functest/protoconf/common";
+
+import "tableau/protobuf/tableau.proto";
+
+enum FruitType {
+  FRUIT_TYPE_UNKNOWN = 0 [(tableau.evalue).name = "Unknown"];
+  FRUIT_TYPE_APPLE = 1 [(tableau.evalue).name = "Apple"];
+  FRUIT_TYPE_ORANGE = 3 [(tableau.evalue).name = "Orange"];
+  FRUIT_TYPE_BANANA = 4 [(tableau.evalue).name = "Banana"];
+}
+
+enum FruitFlavor {
+  FRUIT_FLAVOR_UNKNOWN = 0 [(tableau.evalue).name = "Unknown"];
+  FRUIT_FLAVOR_FRAGRANT = 1 [(tableau.evalue).name = "Fragrant"];
+  FRUIT_FLAVOR_SOUR = 2 [(tableau.evalue).name = "Sour"];
+  FRUIT_FLAVOR_SWEET = 3 [(tableau.evalue).name = "Sweet"];
+}
+
+message Item {
+  int32 id = 1 [(tableau.field).name = "ID"];
+  int32 num = 2 [(tableau.field).name = "Num"];
+}
diff --git a/test/functest/proto/excel__map__map.proto b/test/functest/proto/excel__map__map.proto
index 24f5362..06c17e9 100644
--- a/test/functest/proto/excel__map__map.proto
+++ b/test/functest/proto/excel__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";
@@ -51,6 +51,22 @@ message HorizontalImplicitFixedSizeMap {
   }
 }
 
+message IncellEnumMap {
+  option (tableau.worksheet) = {name:"IncellEnumMap" namerow:1 typerow:2 noterow:3 datarow:4};
+
+  map<int32, Fruit> fruit_map = 1 [(tableau.field) = {name:"Fruit" key:"Key" layout:LAYOUT_INCELL}];
+  message Fruit {
+    protoconf.FruitType key = 1 [(tableau.field) = {name:"Key"}];
+    int64 value = 2 [(tableau.field) = {name:"Value"}];
+  }
+  map<int64, protoconf.FruitFlavor> flavor_map = 2 [(tableau.field) = {name:"Flavor" layout:LAYOUT_INCELL}];
+  map<int32, Item> item_map = 3 [(tableau.field) = {name:"Item" key:"Key" layout:LAYOUT_INCELL}];
+  message Item {
+    protoconf.FruitType key = 1 [(tableau.field) = {name:"Key"}];
+    protoconf.FruitFlavor value = 2 [(tableau.field) = {name:"Value"}];
+  }
+}
+
 message IncellMap {
   option (tableau.worksheet) = {name:"IncellMap" namerow:1 typerow:2 noterow:3 datarow:4};
 
diff --git a/test/functest/testdata/excel/map/Map#IncellEnumMap.csv b/test/functest/testdata/excel/map/Map#IncellEnumMap.csv
new file mode 100644
index 0000000..9296516
--- /dev/null
+++ b/test/functest/testdata/excel/map/Map#IncellEnumMap.csv
@@ -0,0 +1,4 @@
+Fruit,Flavor,Item
+"map<enum<.FruitType>, int64>","map<int64, enum<.FruitFlavor>>","map<enum<.FruitType>, enum<.FruitFlavor>>"
+Fruits,Flavors,Items
+"Apple:1,Orange:2","1:Fragrant,2:Sweet","Apple:Fragrant,Orange:Sour"