Repository: tableauio/tableau (go)

Issue:
feat: support advanced incell message
## Design

At some situations, we want to configure any complex message in one cell, which is called **advanced incell message**. And tableau (confgen) should support two kinds of protobuf serialized formats: [text format](https://developers.google.com/protocol-buffers/docs/text-format-spec), and [JSON format](https://developers.google.com/protocol-buffers/docs/proto3#json).

Add new field prop **mode** to *tableau.proto*:
```protobuf
message FieldProp {
  ...
  // Specify cell's data form for parsing.
  Form form = 8;
}

enum Form {
  FORM_DEFAULT = 0;  // Default form which confgen parser defines.
  FORM_TEXT = 1;     // Refer: https://developers.google.com/protocol-buffers/docs/text-format-spec
  FORM_JSON = 2;     // Refer: https://developers.google.com/protocol-buffers/docs/proto3#json
}
```

## Example

Predefined message `Transform`:
```protobuf
message Transform {
  Vector3 position = 1;
  Vector3 rotation = 2;
  Vector3 scale = 3;
}

message Vector3 {
  float x = 1;
  float y = 2;
  float z = 3;
}
```

### 1. message text format
refer: https://developers.google.com/protocol-buffers/docs/text-format-spec

| Transform |
|:---|
| {.Transform}\|{form:FORM_TEXT} |
| Box's transform |
| position:{x:1 y:2 z:3} rotation:{x:4 y:5 z:6} scale:{x:7 y:8 z:9} |

### 2. message JSON format
refer: https://developers.google.com/protocol-buffers/docs/proto3#json

| Transform |
|:---|
| {.Transform}\|{form:FORM_JSON} |
| Box's transform |
| {"position":{"x":1, "y":2, "z":3}, "rotation":{"x":4, "y":5, "z":6}, "scale":{"x":7, "y":8, "z":9}} |

Pull request:
feat: support multiple incell-struct forms
close #30 

Patch:
diff --git a/.gitignore b/.gitignore
index ce60650..93a5aec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,5 @@ test/dev/testpb/
 test/parallel/proto/
 test/parallel/protoconf/
 .vscode/
-*.prof
\ No newline at end of file
+*.prof
+test/functest/testdata/xml/*/*.csv
\ No newline at end of file
diff --git a/internal/confgen/parser.go b/internal/confgen/parser.go
index 4c85578..a2f6b2f 100644
--- a/internal/confgen/parser.go
+++ b/internal/confgen/parser.go
@@ -19,6 +19,8 @@ import (
 	"github.com/tableauio/tableau/proto/tableaupb"
 	"github.com/tableauio/tableau/xerrors"
 	"golang.org/x/sync/errgroup"
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/encoding/prototext"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
 	"google.golang.org/protobuf/types/descriptorpb"
@@ -674,7 +676,7 @@ func (sp *sheetParser) parseIncellMapWithValueAsSimpleKVMessage(field *Field, re
 		}
 
 		newMapValue := reflectMap.NewValue()
-		valuePresent, err := sp.parseIncellStruct(newMapValue, mapItemData, field.opts.Subsep)
+		valuePresent, err := sp.parseIncellStruct(newMapValue, mapItemData, field.opts.GetProp().GetForm(), field.opts.Subsep)
 		if err != nil {
 			return xerrors.WithMessageKV(err, xerrors.KeyPBFieldType, "incell struct list")
 		}
@@ -797,7 +799,7 @@ func (sp *sheetParser) parseListField(field *Field, msg protoreflect.Message, rc
 						kvs := append(rc.CellDebugKV(colName), xerrors.KeyPBFieldType, "incell struct list")
 						return false, xerrors.WithMessageKV(err, kvs...)
 					}
-					if elemPresent, err = sp.parseIncellStruct(newListValue, cell.Data, field.opts.Sep); err != nil {
+					if elemPresent, err = sp.parseIncellStruct(newListValue, cell.Data, field.opts.GetProp().GetForm(), field.opts.Sep); err != nil {
 						kvs := append(rc.CellDebugKV(colName), xerrors.KeyPBFieldType, "incell struct list")
 						return false, xerrors.WithMessageKV(err, kvs...)
 					}
@@ -857,7 +859,7 @@ func (sp *sheetParser) parseListField(field *Field, msg protoreflect.Message, rc
 							return false, xerrors.WithMessageKV(err, kvs...)
 						}
 					} else {
-						if elemPresent, err = sp.parseIncellStruct(newListValue, cell.Data, field.opts.Sep); err != nil {
+						if elemPresent, err = sp.parseIncellStruct(newListValue, cell.Data, field.opts.GetProp().GetForm(), field.opts.Sep); err != nil {
 							kvs := append(rc.CellDebugKV(colName), xerrors.KeyPBFieldType, "horizontal incell-struct list")
 							return false, xerrors.WithMessageKV(err, kvs...)
 						}
@@ -1048,7 +1050,7 @@ func (sp *sheetParser) parseStructField(field *Field, msg protoreflect.Message,
 			return false, xerrors.WithMessageKV(err, kvs...)
 		}
 
-		if present, err = sp.parseIncellStruct(structValue, cell.Data, field.opts.Sep); err != nil {
+		if present, err = sp.parseIncellStruct(structValue, cell.Data, field.opts.GetProp().GetForm(), field.opts.Sep); err != nil {
 			kvs := append(rc.CellDebugKV(colName), xerrors.KeyPBFieldType, "incell struct")
 			return false, xerrors.WithMessageKV(err, kvs...)
 		}
@@ -1095,30 +1097,43 @@ func (sp *sheetParser) parseStructField(field *Field, msg protoreflect.Message,
 	}
 }
 
-func (sp *sheetParser) parseIncellStruct(structValue protoreflect.Value, cellData, sep string) (present bool, err error) {
+func (sp *sheetParser) parseIncellStruct(structValue protoreflect.Value, cellData string, form tableaupb.Form, sep string) (present bool, err error) {
 	if cellData == "" {
 		return false, 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, sep)
-	subMd := structValue.Message().Descriptor()
-	for i := 0; i < subMd.Fields().Len() && i < len(splits); i++ {
-		fd := subMd.Fields().Get(i)
-		// log.Debugf("fd.FullName().Name(): ", fd.FullName().Name())
-		incell := splits[i]
-		value, fieldPresent, err := sp.parseFieldValue(fd, incell)
-		if err != nil {
+	switch form {
+	case tableaupb.Form_FORM_TEXT:
+		if err := prototext.Unmarshal([]byte(cellData), structValue.Message().Interface()); err != nil {
+			return false, xerrors.WithMessageKV(err, xerrors.KeyPBFieldType, "incell struct")
+		}
+		return true, nil
+	case tableaupb.Form_FORM_JSON:
+		if err := protojson.Unmarshal([]byte(cellData), structValue.Message().Interface()); err != nil {
 			return false, xerrors.WithMessageKV(err, xerrors.KeyPBFieldType, "incell struct")
 		}
-		structValue.Message().Set(fd, value)
-		if fieldPresent {
-			// The struct is treated as present only if one field is present.
-			present = true
+		return true, nil
+	default:
+		// 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, sep)
+		md := structValue.Message().Descriptor()
+		for i := 0; i < md.Fields().Len() && i < len(splits); i++ {
+			fd := md.Fields().Get(i)
+			// log.Debugf("fd.FullName().Name(): ", fd.FullName().Name())
+			incell := splits[i]
+			value, fieldPresent, err := sp.parseFieldValue(fd, incell)
+			if err != nil {
+				return false, xerrors.WithMessageKV(err, xerrors.KeyPBFieldType, "incell struct")
+			}
 			structValue.Message().Set(fd, value)
+			if fieldPresent {
+				// The struct is treated as present only if one field is present.
+				present = true
+				structValue.Message().Set(fd, value)
+			}
 		}
+		return present, nil
 	}
-	return present, nil
 }
 
 func (sp *sheetParser) parseUnionField(field *Field, msg protoreflect.Message, rc *book.RowCells, prefix string) (present bool, err error) {
@@ -1228,7 +1243,7 @@ func (sp *sheetParser) parseUnionValueField(field *Field, msg protoreflect.Messa
 	} else if field.fd.Kind() == protoreflect.MessageKind {
 		// incell struct
 		value := msg.NewField(field.fd)
-		present, err := sp.parseIncellStruct(value, cellData, field.opts.Sep)
+		present, err := sp.parseIncellStruct(value, cellData, field.opts.GetProp().GetForm(), field.opts.Sep)
 		if err != nil {
 			return xerrors.WithMessageKV(err, xerrors.KeyPBFieldType, "incell struct")
 		}
diff --git a/internal/protogen/field_prop.go b/internal/protogen/field_prop.go
index 1d495c7..e9ddd15 100644
--- a/internal/protogen/field_prop.go
+++ b/internal/protogen/field_prop.go
@@ -7,6 +7,7 @@ import (
 
 var emptyFieldProp = &tableaupb.FieldProp{}
 
+// ExtractMapFieldProp extracts the specified props which the map field recognizes.
 func ExtractMapFieldProp(prop *tableaupb.FieldProp) *tableaupb.FieldProp {
 	if prop == nil {
 		return nil
@@ -23,6 +24,7 @@ func ExtractMapFieldProp(prop *tableaupb.FieldProp) *tableaupb.FieldProp {
 	return p
 }
 
+// ExtractListFieldProp extracts the specified props which the list field recognizes.
 func ExtractListFieldProp(prop *tableaupb.FieldProp) *tableaupb.FieldProp {
 	if prop == nil {
 		return nil
@@ -39,6 +41,21 @@ func ExtractListFieldProp(prop *tableaupb.FieldProp) *tableaupb.FieldProp {
 	return p
 }
 
+// ExtractStructFieldProp extracts the specified props which the struct field recognizes.
+func ExtractStructFieldProp(prop *tableaupb.FieldProp) *tableaupb.FieldProp {
+	if prop == nil {
+		return nil
+	}
+	p := &tableaupb.FieldProp{
+		Form: prop.Form,
+	}
+	if proto.Equal(emptyFieldProp, p) {
+		return nil
+	}
+	return p
+}
+
+// ExtractScalarFieldProp extracts the specified props which the scalar field recognizes.
 func ExtractScalarFieldProp(prop *tableaupb.FieldProp) *tableaupb.FieldProp {
 	if prop == nil {
 		return nil
diff --git a/internal/protogen/parser.go b/internal/protogen/parser.go
index 25559d6..1ffb9e5 100644
--- a/internal/protogen/parser.go
+++ b/internal/protogen/parser.go
@@ -680,6 +680,14 @@ func (p *bookParser) parseStructField(field *tableaupb.Field, header *sheetHeade
 	trimmedNameCell := strings.TrimPrefix(nameCell, prefix)
 	// struct syntax pattern
 	desc := types.MatchStruct(typeCell)
+	prop, err := desc.Prop.FieldProp()
+	if err != nil {
+		return cursor, xerrors.WithMessageKV(err,
+			xerrors.KeyPBFieldType, desc.StructType,
+			xerrors.KeyPBFieldOpts, desc.Prop.Text,
+			xerrors.KeyTrimmedNameCell, trimmedNameCell)
+	}
+
 	if desc.ColumnType == "" {
 		// incell predefined struct
 		structField, err := parseField(p.gen.typeInfos, trimmedNameCell, desc.StructType)
@@ -691,6 +699,7 @@ func (p *bookParser) parseStructField(field *tableaupb.Field, header *sheetHeade
 		}
 		proto.Merge(field, structField)
 		field.Options.Span = tableaupb.Span_SPAN_INNER_CELL
+		field.Options.Prop = ExtractStructFieldProp(prop)
 		return cursor, nil
 	}
 
@@ -708,6 +717,7 @@ func (p *bookParser) parseStructField(field *tableaupb.Field, header *sheetHeade
 		}
 		proto.Merge(field, scalarField)
 		field.Options.Span = tableaupb.Span_SPAN_INNER_CELL
+		field.Options.Prop = ExtractStructFieldProp(prop)
 
 		for i := 0; i < len(fieldPairs); i += 2 {
 			fieldType := fieldPairs[i]
@@ -773,6 +783,7 @@ func (p *bookParser) parseStructField(field *tableaupb.Field, header *sheetHeade
 		field.Name = strcase.ToSnake(structName)
 		field.Options = &tableaupb.FieldOptions{
 			Name: structName,
+			Prop: ExtractStructFieldProp(prop),
 		}
 		prefix += structName
 		firstFieldOptions := append(options, parseroptions.VTypeCell(cursor, desc.ColumnType+desc.Prop.RawProp()))
diff --git a/proto/tableau/protobuf/tableau.proto b/proto/tableau/protobuf/tableau.proto
index 329377a..af12a05 100644
--- a/proto/tableau/protobuf/tableau.proto
+++ b/proto/tableau/protobuf/tableau.proto
@@ -183,9 +183,9 @@ message FieldProp {
   //  - list size is detected by size of the max present list elements in name row.
   //  - map size is detected by size of the max present map items in name row.
   bool fixed = 6;
-  // Explicitly specify fixed size of horizontal list or map.
+  // Specify fixed size of horizontal list or map.
   uint32 size = 7;
-  // Cell's data form for parsing.
+  // Specify cell data form for parsing.
   Form form = 8;
 }
 
@@ -224,6 +224,7 @@ enum Mode {
   MODE_UNION_TYPE = 4;  // Union type definition in sheet.
 }
 
+// Cell data form.
 enum Form {
   FORM_DEFAULT = 0;  // Default form which confgen parser defines.
   FORM_TEXT = 1;     // Refer: https://developers.google.com/protocol-buffers/docs/text-format-spec
diff --git a/proto/tableaupb/tableau.pb.go b/proto/tableaupb/tableau.pb.go
index ed1a068..f662a77 100644
--- a/proto/tableaupb/tableau.pb.go
+++ b/proto/tableaupb/tableau.pb.go
@@ -194,6 +194,7 @@ func (Mode) EnumDescriptor() ([]byte, []int) {
 	return file_tableau_protobuf_tableau_proto_rawDescGZIP(), []int{2}
 }
 
+// Cell data form.
 type Form int32
 
 const (
@@ -839,9 +840,9 @@ type FieldProp struct {
 	//  - list size is detected by size of the max present list elements in name row.
 	//  - map size is detected by size of the max present map items in name row.
 	Fixed bool `protobuf:"varint,6,opt,name=fixed,proto3" json:"fixed,omitempty"`
-	// Explicitly specify fixed size of horizontal list or map.
+	// Specify fixed size of horizontal list or map.
 	Size uint32 `protobuf:"varint,7,opt,name=size,proto3" json:"size,omitempty"`
-	// Cell's data form for parsing.
+	// Specify cell data form for parsing.
 	Form Form `protobuf:"varint,8,opt,name=form,proto3,enum=tableau.Form" json:"form,omitempty"`
 }
 


Test patch:
diff --git a/internal/protogen/field_prop_test.go b/internal/protogen/field_prop_test.go
index d374d4f..497bbd8 100644
--- a/internal/protogen/field_prop_test.go
+++ b/internal/protogen/field_prop_test.go
@@ -7,6 +7,47 @@ import (
 	"google.golang.org/protobuf/proto"
 )
 
+func TestExtractMapFieldProp(t *testing.T) {
+	type args struct {
+		prop *tableaupb.FieldProp
+	}
+	tests := []struct {
+		name string
+		args args
+		want *tableaupb.FieldProp
+	}{
+		{
+			name: "emptyMapFieldProp",
+			args: args{
+				prop: &tableaupb.FieldProp{},
+			},
+			want: nil,
+		},
+		{
+			name: "noneEmptyMapFieldProp",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Unique:   true,
+					Sequence: proto.Int64(1),
+					Size:     2,
+				},
+			},
+			want: &tableaupb.FieldProp{
+				Unique:   true,
+				Sequence: proto.Int64(1),
+				Size:     2,
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ExtractMapFieldProp(tt.args.prop); !proto.Equal(got, tt.want) {
+				t.Errorf("ExtractMapFieldProp() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
 func TestExtractListFieldProp(t *testing.T) {
 	type args struct {
 		prop *tableaupb.FieldProp
@@ -16,7 +57,6 @@ func TestExtractListFieldProp(t *testing.T) {
 		args args
 		want *tableaupb.FieldProp
 	}{
-		// TODO: Add test cases.
 		{
 			name: "emptyListFieldProp",
 			args: args{
@@ -44,3 +84,83 @@ func TestExtractListFieldProp(t *testing.T) {
 		})
 	}
 }
+
+func TestExtractStructFieldProp(t *testing.T) {
+	type args struct {
+		prop *tableaupb.FieldProp
+	}
+	tests := []struct {
+		name string
+		args args
+		want *tableaupb.FieldProp
+	}{
+		{
+			name: "emptyStructFieldProp",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Unique: true,
+				},
+			},
+			want: nil,
+		},
+		{
+			name: "noneEmptyStructFieldProp",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Unique: true,
+					Form:   tableaupb.Form_FORM_JSON,
+				},
+			},
+			want: &tableaupb.FieldProp{
+				Form: tableaupb.Form_FORM_JSON,
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ExtractStructFieldProp(tt.args.prop); !proto.Equal(got, tt.want) {
+				t.Errorf("ExtractStructFieldProp() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestExtractScalarFieldProp(t *testing.T) {
+	type args struct {
+		prop *tableaupb.FieldProp
+	}
+	tests := []struct {
+		name string
+		args args
+		want *tableaupb.FieldProp
+	}{
+		{
+			name: "emptyScalarFieldProp",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Unique: true,
+				},
+			},
+			want: nil,
+		},
+		{
+			name: "noneEmptyStructFieldProp",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Unique: true,
+					Range:  "1~10",
+				},
+			},
+			want: &tableaupb.FieldProp{
+				Range: "1~10",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ExtractScalarFieldProp(tt.args.prop); !proto.Equal(got, tt.want) {
+				t.Errorf("ExtractScalarFieldProp() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/internal/types/types_test.go b/internal/types/types_test.go
index e4a0007..434cc86 100644
--- a/internal/types/types_test.go
+++ b/internal/types/types_test.go
@@ -232,6 +232,17 @@ func TestMatchStruct(t *testing.T) {
 				ColumnType: "",
 			},
 		},
+		{
+			name: "predefined-incell-struct-with-prop",
+			args: args{
+				text: `{.Item}|{range:"1,10"}`,
+			},
+			want: &StructDescriptor{
+				StructType: ".Item",
+				ColumnType: "",
+				Prop:       PropDescriptor{Text: `range:"1,10"`},
+			},
+		},
 		{
 			name: "new-defined-cross-cell-struct-with-prop",
 			args: args{
diff --git a/test/functest/proto/common/common.proto b/test/functest/proto/common/common.proto
index 247c851..9e5c83d 100644
--- a/test/functest/proto/common/common.proto
+++ b/test/functest/proto/common/common.proto
@@ -84,3 +84,15 @@ message Target {
     // no field tag 3
   }
 }
+
+message Vector3 {
+  float x = 1;
+  float y = 2;
+  float z = 3;
+}
+
+message Transform {
+  Vector3 position = 1;
+  Vector3 rotation = 2;
+  Vector3 scale = 3;
+}
diff --git a/test/functest/proto/excel__fieldprop__field_prop.proto b/test/functest/proto/excel__fieldprop__field_prop.proto
index 705e422..b34c32a 100644
--- a/test/functest/proto/excel__fieldprop__field_prop.proto
+++ b/test/functest/proto/excel__fieldprop__field_prop.proto
@@ -1,15 +1,23 @@
-// 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";
 option (tableau.workbook) = {name:"excel/fieldprop/FieldProp#*.csv"};
 
+message FieldPropForm {
+  option (tableau.worksheet) = {name:"FieldPropForm" namerow:1 typerow:2 noterow:3 datarow:4};
+
+  protoconf.Transform transform_1 = 1 [(tableau.field) = {name:"Transform1" span:SPAN_INNER_CELL prop:{form:FORM_TEXT}}];
+  protoconf.Transform transform_2 = 2 [(tableau.field) = {name:"Transform2" span:SPAN_INNER_CELL prop:{form:FORM_JSON}}];
+}
+
 message ItemConf {
   option (tableau.worksheet) = {name:"ItemConf" namerow:1 typerow:2 noterow:3 datarow:4};
 
diff --git a/test/functest/testdata/excel/fieldprop/FieldProp#FieldPropForm.csv b/test/functest/testdata/excel/fieldprop/FieldProp#FieldPropForm.csv
new file mode 100644
index 0000000..3e02ef9
--- /dev/null
+++ b/test/functest/testdata/excel/fieldprop/FieldProp#FieldPropForm.csv
@@ -0,0 +1,4 @@
+Transform1,Transform2
+{.Transform}|{form:FORM_TEXT},{.Transform}|{form:FORM_JSON}
+Box's transform1,Box's transform2
+position:{x:1 y:2 z:3} rotation:{x:4 y:5 z:6} scale:{x:7 y:8 z:9},"{""position"":{""x"":1, ""y"":2, ""z"":3}, ""rotation"":{""x"":4, ""y"":5, ""z"":6}, ""scale"":{""x"":7, ""y"":8, ""z"":9}}"