Union(联合体)

本文说明 Excel union 类型的各种特性。

原理

在 protoconf 中,union 类型是指带标签的联合体(tagged union):一种用于保存可以取多种不同但固定类型值的数据结构。任意时刻只有一种类型在使用,且一个 tag 字段明确指示当前使用的是哪种类型。更多详情请参考维基百科 Tagged union

Tagged union 在不同编程语言中的对应:

Tableau 使用 protobuf messageenum 类型和 oneof 类型绑定在一起,以实现 tagged union。默认情况下,每个枚举值(>0)与 oneof 类型中具有相同 tag 编号的字段绑定。

Union 定义

例如,common.proto 中预定义的 union 类型 Target

// Predefined union type.
message Target {
  option (tableau.union) = {name:"Target"};

  Type type = 9999 [(tableau.field) = { name: "Type" }];
  oneof value {
    option (tableau.oneof) = {
      field: "Field"
    };
    Pvp pvp = 1;      // Bound to enum value 1: TYPE_PVP.
    Pve pve = 2;      // Bound to enum value 2: TYPE_PVP.
    Story story = 3;  // Bound to enum value 3: TYPE_STORY.
    Skill skill = 4;  // Bound to enum value 4: TYPE_SKILL.
  }

  enum Type {
    TYPE_NIL = 0;
    TYPE_PVP = 1 [(tableau.evalue) = { name: "PVP" }];
    TYPE_PVE = 2 [(tableau.evalue) = { name: "PVE" }];
    TYPE_STORY = 3 [(tableau.evalue) = { name: "Story" }];
    TYPE_SKILL = 4 [(tableau.evalue) = { name: "Skill" }];
  }
  message Pvp {
    int32 type = 1;                          // scalar
    int64 damage = 2;                        // scalar
    repeated protoconf.FruitType types = 3;  // incell enum list
  }
  message Pve {
    Mission mission = 1;             // incell struct
    repeated int32 heros = 2;        // incell list
    map<int32, int64> dungeons = 3;  // incell map

    message Mission {
      int32 id = 1;
      uint32 level = 2;
      int64 damage = 3;
    }
  }
  message Story {
    protoconf.Item cost = 1;                     // incell predefined struct
    map<int32, protoconf.FruitType> fruits = 2;  // incell map with value as enum type
    map<int32, Flavor> flavors = 3;              // incell map with key as enum type
    message Flavor {
      protoconf.FruitFlavor key = 1 [(tableau.field) = { name: "Key" }];
      int32 value = 2 [(tableau.field) = { name: "Value" }];
    }
  }
  message Skill {
    int32 id = 1;      // scalar
    int64 damage = 2;  // scalar
    // no field tag 3
  }
}

List 中的 predefined union

[!NOTE] 基于 predefined union 类型 Target

HelloWorld.xlsx 中的 worksheet TaskConf

IDTarget1TypeTarget1Field1Target1Field2Target1Field3Target2TypeTarget2Field1Target2Field2Target2Field3
map<int32, Task>[.Target]enum<.Target.Type>unionunionunionenum<.Target.Type>unionunionunion
IDTarget1’s typeTarget1’s field1Target1’s field2Target1’s field3Target2’s typeTarget2’s field1Target2’s field2Target2’s field3
1PVP110Apple,Orange,BananaPVE1,100,9991,2,31:10,2:20,3:30
2Story1001,101:Apple,2:OrangeFragrant:1,Sour:2Skill12

生成结果:

hello_world.proto
// --snip--
import "common.proto";
option (tableau.workbook) = {name:"HelloWorld.xlsx" namerow:1 typerow:2 noterow:3 datarow:4};

message TaskConf {
  option (tableau.worksheet) = {name:"TaskConf"};

  map<int32, Task> task_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL}];
  message Task {
    int32 id = 1 [(tableau.field) = {name:"ID"}];
    repeated protoconf.Target target_list = 2 [(tableau.field) = {name:"Target" layout:LAYOUT_HORIZONTAL}];
  }
}
TaskConf.json
{
    "taskMap": {
        "1": {
            "id": 1,
            "targetList": [
                {
                    "type": "TYPE_PVP",
                    "pvp": {
                        "type": 1,
                        "damage": "10",
                        "types": [
                            "FRUIT_TYPE_APPLE",
                            "FRUIT_TYPE_ORANGE",
                            "FRUIT_TYPE_BANANA"
                        ]
                    }
                },
                {
                    "type": "TYPE_PVE",
                    "pve": {
                        "mission": {
                            "id": 1,
                            "level": 100,
                            "damage": "999"
                        },
                        "heros": [
                            1,
                            2,
                            3
                        ],
                        "dungeons": {
                            "1": "10",
                            "2": "20",
                            "3": "30"
                        }
                    }
                }
            ]
        },
        "2": {
            "id": 2,
            "targetList": [
                {
                    "type": "TYPE_STORY",
                    "story": {
                        "cost": {
                            "id": 1001,
                            "num": 10
                        },
                        "fruits": {
                            "1": "FRUIT_TYPE_APPLE",
                            "2": "FRUIT_TYPE_ORANGE"
                        },
                        "flavors": {
                            "1": {
                                "key": "FRUIT_FLAVOR_FRAGRANT",
                                "value": 1
                            },
                            "2": {
                                "key": "FRUIT_FLAVOR_SOUR",
                                "value": 2
                            }
                        }
                    }
                },
                {
                    "type": "TYPE_SKILL",
                    "skill": {
                        "id": 1,
                        "damage": "2"
                    }
                }
            ]
        }
    }
}
TaskConf.txt
task_map: {
  key: 1
  value: {
    id: 1
    target_list: {
      type: TYPE_PVP
      pvp: {
        type: 1
        damage: 10
        types: FRUIT_TYPE_APPLE
        types: FRUIT_TYPE_ORANGE
        types: FRUIT_TYPE_BANANA
      }
    }
    target_list: {
      type: TYPE_PVE
      pve: {
        mission: {
          id: 1
          level: 100
          damage: 999
        }
        heros: 1
        heros: 2
        heros: 3
        dungeons: {
          key: 1
          value: 10
        }
        dungeons: {
          key: 2
          value: 20
        }
        dungeons: {
          key: 3
          value: 30
        }
      }
    }
  }
}
task_map: {
  key: 2
  value: {
    id: 2
    target_list: {
      type: TYPE_STORY
      story: {
        cost: {
          id: 1001
          num: 10
        }
        fruits: {
          key: 1
          value: FRUIT_TYPE_APPLE
        }
        fruits: {
          key: 2
          value: FRUIT_TYPE_ORANGE
        }
        flavors: {
          key: 1
          value: {
            key: FRUIT_FLAVOR_FRAGRANT
            value: 1
          }
        }
        flavors: {
          key: 2
          value: {
            key: FRUIT_FLAVOR_SOUR
            value: 2
          }
        }
      }
    }
    target_list: {
      type: TYPE_SKILL
      skill: {
        id: 1
        damage: 2
      }
    }
  }
}

Map 中的 predefined union

[!NOTE] 基于 predefined union 类型 Target

HelloWorld.xlsx 中的 worksheet TaskConf

IDTargetTypeTargetField1TargetField2TargetField3Progress
map<int32, Task>{.Target}enum<.Target.Type>unionunionunionint32
IDTarget’s typeTarget’s field1Target’s field2Target’s field3Progress
1PVP110Apple,Orange,Banana3
2PVE1,100,9991,2,31:10,2:20,3:3010
3Story1001,101:Apple,2:OrangeFragrant:1,Sour:210
4Skill128

生成结果:

hello_world.proto
// --snip--
import "common.proto";
option (tableau.workbook) = {name:"HelloWorld.xlsx" namerow:1 typerow:2 noterow:3 datarow:4};

message TaskConf {
  option (tableau.worksheet) = {name:"TaskConf"};

  map<int32, Task> task_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL}];
  message Task {
    int32 id = 1 [(tableau.field) = {name:"ID"}];
    protoconf.Target target = 2 [(tableau.field) = {name:"Target"}];
    int32 progress = 3 [(tableau.field) = {name:"Progress"}];
  }
}
TaskConf.json
{
    "taskMap":  {
        "1":  {
            "id":  1,
            "target":  {
                "type":  "TYPE_PVP",
                "pvp":  {
                    "type":  1,
                    "damage":  "10",
                    "types":  [
                        "FRUIT_TYPE_APPLE",
                        "FRUIT_TYPE_ORANGE",
                        "FRUIT_TYPE_BANANA"
                    ]
                }
            },
            "progress":  3
        },
        "2":  {
            "id":  2,
            "target":  {
                "type":  "TYPE_PVE",
                "pve":  {
                    "mission":  {
                        "id":  1,
                        "level":  100,
                        "damage":  "999"
                    },
                    "heros":  [
                        1,
                        2,
                        3
                    ],
                    "dungeons":  {
                        "1":  "10",
                        "2":  "20",
                        "3":  "30"
                    }
                }
            },
            "progress":  10
        },
        "3":  {
            "id":  3,
            "target":  {
                "type":  "TYPE_STORY",
                "story":  {
                    "cost":  {
                        "id":  1001,
                        "num":  10
                    },
                    "fruits":  {
                        "1":  "FRUIT_TYPE_APPLE",
                        "2":  "FRUIT_TYPE_ORANGE"
                    },
                    "flavors":  {
                        "1":  {
                            "key":  "FRUIT_FLAVOR_FRAGRANT",
                            "value":  1
                        },
                        "2":  {
                            "key":  "FRUIT_FLAVOR_SOUR",
                            "value":  2
                        }
                    }
                }
            },
            "progress":  10
        },
        "4":  {
            "id":  4,
            "target":  {
                "type":  "TYPE_SKILL",
                "skill":  {
                    "id":  1,
                    "damage":  "2"
                }
            },
            "progress":  8
        }
    }
}
TaskConf.txt
task_map:  {
    key:  1
    value:  {
        id:  1
        target:  {
            type:  TYPE_PVP
            pvp:  {
                type:  1
                damage:  10
                types:  FRUIT_TYPE_APPLE
                types:  FRUIT_TYPE_ORANGE
                types:  FRUIT_TYPE_BANANA
            }
        }
        progress:  3
    }
}
task_map:  {
    key:  2
    value:  {
        id:  2
        target:  {
            type:  TYPE_PVE
            pve:  {
                mission:  {
                    id:  1
                    level:  100
                    damage:  999
                }
                heros:  1
                heros:  2
                heros:  3
                dungeons:  {
                    key:  1
                    value:  10
                }
                dungeons:  {
                    key:  2
                    value:  20
                }
                dungeons:  {
                    key:  3
                    value:  30
                }
            }
        }
        progress:  10
    }
}
task_map:  {
    key:  3
    value:  {
        id:  3
        target:  {
            type:  TYPE_STORY
            story:  {
                cost:  {
                    id:  1001
                    num:  10
                }
                fruits:  {
                    key:  1
                    value:  FRUIT_TYPE_APPLE
                }
                fruits:  {
                    key:  2
                    value:  FRUIT_TYPE_ORANGE
                }
                flavors:  {
                    key:  1
                    value:  {
                        key:  FRUIT_FLAVOR_FRAGRANT
                        value:  1
                    }
                }
                flavors:  {
                    key:  2
                    value:  {
                        key:  FRUIT_FLAVOR_SOUR
                        value:  2
                    }
                }
            }
        }
        progress:  10
    }
}
task_map:  {
    key:  4
    value:  {
        id:  4
        target:  {
            type:  TYPE_SKILL
            skill:  {
                id:  1
                damage:  2
            }
        }
        progress:  8
    }
}

Map 中的 predefined incell union

[!NOTE] 基于 predefined union 类型 Target

HelloWorld.xlsx 中的 worksheet TaskConf

IDTarget1Target2Progress
map<int32, Task>{.Target}|{form:FORM_TEXT}{.Target}|{form:FORM_JSON}int32
IDTarget1Target2Progress
1type:TYPE_PVP pvp:{type:1 damage:10 types:FRUIT_TYPE_APPLE types:FRUIT_TYPE_ORANGE types:FRUIT_TYPE_BANANA}{“type”:“TYPE_PVP”,“pvp”:{“type”:1,“damage”:“10”,“types”:[“FRUIT_TYPE_APPLE”,“FRUIT_TYPE_ORANGE”,“FRUIT_TYPE_BANANA”]}}3
2type:TYPE_PVE pve:{mission:{id:1 level:100 damage:999} heros:1 heros:2 heros:3 dungeons:{key:1 value:10} dungeons:{key:2 value:20} dungeons:{key:3 value:30}}{“type”:“TYPE_PVE”,“pve”:{“mission”:{“id”:1,“level”:100,“damage”:“999”},“heros”:[1,2,3],“dungeons”:{“1”:“10”,“2”:“20”,“3”:“30”}}}10
3type:TYPE_STORY story:{cost:{id:1001 num:10} fruits:{key:1 value:FRUIT_TYPE_APPLE} fruits:{key:2 value:FRUIT_TYPE_ORANGE} flavors:{key:1 value:{key:FRUIT_FLAVOR_FRAGRANT value:1}} flavors:{key:2 value:{key:FRUIT_FLAVOR_SOUR value:2}}}{“type”:“TYPE_STORY”,“story”:{“cost”:{“id”:1001,“num”:10},“fruits”:{“1”:“FRUIT_TYPE_APPLE”,“2”:“FRUIT_TYPE_ORANGE”},“flavors”:{“1”:{“key”:“FRUIT_FLAVOR_FRAGRANT”,“value”:1},“2”:{“key”:“FRUIT_FLAVOR_SOUR”,“value”:2}}}}10
4type:TYPE_SKILL skill:{id:1 damage:2}{“type”:“TYPE_SKILL”,“skill”:{“id”:1,“damage”:“2”}}8

生成结果:

hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx" namerow:1 typerow:2 noterow:3 datarow:4};

message TaskConf {
  option (tableau.worksheet) = {name:"TaskConf"};

  map<int32, Task> task_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL}];
  message Task {
    int32 id = 1 [(tableau.field) = {name:"ID"}];
    protoconf.Target target_1 = 2 [(tableau.field) = {name:"Target1" span:SPAN_INNER_CELL prop:{form:FORM_TEXT}}];
    protoconf.Target target_2 = 3 [(tableau.field) = {name:"Target2" span:SPAN_INNER_CELL prop:{form:FORM_JSON}}];
    int32 progress = 4 [(tableau.field) = {name:"Progress"}];
  }
}
TaskConf.json
{
    "taskMap":  {
        "1":  {
            "id":  1,
            "target1":  {
                "type":  "TYPE_PVP",
                "pvp":  {
                    "type":  1,
                    "damage":  "10",
                    "types":  [
                        "FRUIT_TYPE_APPLE",
                        "FRUIT_TYPE_ORANGE",
                        "FRUIT_TYPE_BANANA"
                    ]
                }
            },
            "target2":  {
                "type":  "TYPE_PVP",
                "pvp":  {
                    "type":  1,
                    "damage":  "10",
                    "types":  [
                        "FRUIT_TYPE_APPLE",
                        "FRUIT_TYPE_ORANGE",
                        "FRUIT_TYPE_BANANA"
                    ]
                }
            },
            "progress":  3
        },
        "2":  {
            "id":  2,
            "target1":  {
                "type":  "TYPE_PVE",
                "pve":  {
                    "mission":  {
                        "id":  1,
                        "level":  100,
                        "damage":  "999"
                    },
                    "heros":  [
                        1,
                        2,
                        3
                    ],
                    "dungeons":  {
                        "1":  "10",
                        "2":  "20",
                        "3":  "30"
                    }
                }
            },
            "target2":  {
                "type":  "TYPE_PVE",
                "pve":  {
                    "mission":  {
                        "id":  1,
                        "level":  100,
                        "damage":  "999"
                    },
                    "heros":  [
                        1,
                        2,
                        3
                    ],
                    "dungeons":  {
                        "1":  "10",
                        "2":  "20",
                        "3":  "30"
                    }
                }
            },
            "progress":  10
        },
        "3":  {
            "id":  3,
            "target1":  {
                "type":  "TYPE_STORY",
                "story":  {
                    "cost":  {
                        "id":  1001,
                        "num":  10
                    },
                    "fruits":  {
                        "1":  "FRUIT_TYPE_APPLE",
                        "2":  "FRUIT_TYPE_ORANGE"
                    },
                    "flavors":  {
                        "1":  {
                            "key":  "FRUIT_FLAVOR_FRAGRANT",
                            "value":  1
                        },
                        "2":  {
                            "key":  "FRUIT_FLAVOR_SOUR",
                            "value":  2
                        }
                    }
                }
            },
            "target2":  {
                "type":  "TYPE_STORY",
                "story":  {
                    "cost":  {
                        "id":  1001,
                        "num":  10
                    },
                    "fruits":  {
                        "1":  "FRUIT_TYPE_APPLE",
                        "2":  "FRUIT_TYPE_ORANGE"
                    },
                    "flavors":  {
                        "1":  {
                            "key":  "FRUIT_FLAVOR_FRAGRANT",
                            "value":  1
                        },
                        "2":  {
                            "key":  "FRUIT_FLAVOR_SOUR",
                            "value":  2
                        }
                    }
                }
            },
            "progress":  10
        },
        "4":  {
            "id":  4,
            "target1":  {
                "type":  "TYPE_SKILL",
                "skill":  {
                    "id":  1,
                    "damage":  "2"
                }
            },
            "target2":  {
                "type":  "TYPE_SKILL",
                "skill":  {
                    "id":  1,
                    "damage":  "2"
                }
            },
            "progress":  8
        }
    }
}

在 sheet 中定义 union 类型

在 metasheet @TABLEAU 中有两种 Mode 可用于在 sheet 中定义 union 类型:

  • MODE_UNION_TYPE:在一个 sheet 中定义单个 union 类型。
  • MODE_UNION_TYPE_MULTI:在一个 sheet 中定义多个 union 类型。

每个 union 字段可以使用以下类型定义:

单个 union 类型

[!Note] Number 列是可选的,用于指定字段编号及对应的枚举值编号。省略时从 1 开始自动递增。

需要在 metasheet @TABLEAU 中将 Mode 选项设置为 MODE_UNION_TYPE

例如,HelloWorld.xlsx 中的 worksheet Target

NumberNameAliasField1Field2Field3
1PVPPVP战斗ID
uint32
战斗ID
Damage
int64
伤害
Type
enum<.FruitType>
水果类型
2PVEPVE战斗Hero
[]uint32
英雄列表
Dungeon
map<int32, int64>
副本映射
3Skill技能StartTime
datetime
开始时间
Duration
duration
持续时间
SheetMode
TargetMODE_UNION_TYPE

生成结果:

hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx"};

// Generated from sheet: Target.
message Target {
  option (tableau.union) = {name:"Target"};

  Type type = 9999 [(tableau.field) = { name: "Type" }];
  oneof value {
    option (tableau.oneof) = {field: "Field"};

    PVP pvp = 1; // Bound to enum value: TYPE_PVP.
    PVE pve = 2; // Bound to enum value: TYPE_PVE.
    Skill skill = 3; // Bound to enum value: TYPE_SKILL.
  }
  enum Type {
    TYPE_INVALID = 0;
    TYPE_PVP = 1 [(tableau.evalue).name = "PVP战斗"]; // PVP战斗
    TYPE_PVE = 2 [(tableau.evalue).name = "PVE战斗"]; // PVE战斗
    TYPE_SKILL = 3 [(tableau.evalue).name = "技能"]; // 技能
  }

  message PVP {
    uint32 id = 1 [(tableau.field) = {name:"ID"}]; // 战斗ID
    int64 damage = 2 [(tableau.field) = {name:"Damage"}]; // 伤害
    protoconf.FruitType type = 3 [(tableau.field) = {name:"Type"}]; // 水果类型
  }
  message PVE {
    repeated uint32 hero_list = 1 [(tableau.field) = {name:"Hero" layout:LAYOUT_INCELL}]; // 英雄列表
    map<int32, int64> dungeon_map = 2 [(tableau.field) = {name:"Dungeon" layout:LAYOUT_INCELL}]; // 副本映射
  }
  message Skill {
    google.protobuf.Timestamp start_time = 1 [(tableau.field) = {name:"StartTime"}]; // 开始时间
    google.protobuf.Duration duration = 2 [(tableau.field) = {name:"Duration"}]; // 持续时间
  }
}

多个 union 类型

[!IMPORTANT] 每个 union 类型由一个 block 定义,即一系列连续的非空行。不同的 block 之间由一行或多行空行分隔。

需要在 metasheet @TABLEAU 中将 Mode 选项设置为 MODE_UNION_TYPE_MULTI

例如,HelloWorld.xlsx 中的 worksheet Union

HeroTarget英雄目标
NumberNameAliasField1Field2Field3
1StarUp升星ID
uint32
英雄ID
Star
int32星级
2LevelUp升级ID
uint32
英雄ID
Level
int32
等级
Super
bool
是否绝品
BattleTarget战斗目标
NumberNameAliasField1Field2Field3
1PVPPVP战斗BattleID
int32
战斗ID
Damage
int64
伤害
2PVEPVE战斗HeroID
[]int32
英雄ID列表
Dungeon
map<int32, int64>
副本映射
Boss
{uint32 ID, int64 Damage}Boss
特定Boss伤害
SheetMode
UnionMODE_UNION_TYPE_MULTI

生成结果:

hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx"};

message HeroTarget {
  option (tableau.union) = {name:"UnionType" note:"英雄目标"};

  Type type = 9999 [(tableau.field) = {name:"Type"}];
  oneof value {
    option (tableau.oneof) = {note:"英雄目标" field:"Field"};

    StarUp star_up = 1; // Bound to enum value: TYPE_STAR_UP.
    LevelUp level_up = 2; // Bound to enum value: TYPE_LEVEL_UP.
  }

  enum Type {
    TYPE_INVALID = 0;
    TYPE_STAR_UP = 1 [(tableau.evalue).name = "升星"]; // 升星
    TYPE_LEVEL_UP = 2 [(tableau.evalue).name = "升级"]; // 升级
  }

  message StarUp {
    uint32 id = 1 [(tableau.field) = {name:"ID"}]; // 英雄ID
    int32 star = 2 [(tableau.field) = {name:"Star"}]; // 星级
  }
  message LevelUp {
    uint32 id = 1 [(tableau.field) = {name:"ID"}]; // 英雄ID
    int32 level = 2 [(tableau.field) = {name:"Level"}]; // 等级
    bool super = 3 [(tableau.field) = {name:"Super"}]; // 是否绝品
  }
}

message BattleTarget {
  option (tableau.union) = {name:"UnionType" note:"战斗目标"};

  Type type = 9999 [(tableau.field) = {name:"Type"}];
  oneof value {
    option (tableau.oneof) = {note:"战斗目标" field:"Field"};

    PVP pvp = 1; // Bound to enum value: TYPE_PVP.
    PVE pve = 2; // Bound to enum value: TYPE_PVE.
  }

  enum Type {
    TYPE_INVALID = 0;
    TYPE_PVP = 1 [(tableau.evalue).name = "PVP战斗"]; // PVP战斗
    TYPE_PVE = 2 [(tableau.evalue).name = "PVP战斗"]; // PVE战斗
  }

  message PVP {
    int32 battle_id = 1 [(tableau.field) = {name:"BattleID"}]; // 战斗ID
    int64 damage = 2 [(tableau.field) = {name:"Damage"}]; // 伤害
  }
  message PVE {
    repeated int32 hero_id_list = 1 [(tableau.field) = {name:"HeroID" layout:LAYOUT_INCELL}]; // 英雄ID列表
    map<int32, int64> dungeon_map = 2 [(tableau.field) = {name:"Dungeon" layout:LAYOUT_INCELL}]; // 副本映射
    Boss boss = 3 [(tableau.field) = {name:"Boss" span:SPAN_INNER_CELL}]; // 特定Boss伤害
    message Boss {
      uint32 id = 1 [(tableau.field) = {name:"ID"}];
      int64 damage = 2 [(tableau.field) = {name:"Damage"}];
    }
  }
}

指定 Type 列

默认情况下,每个 union 的 oneof 字段是一个以 Name 列指定名称的 message 类型。 现在,你可以添加 Type 列并指定自定义的 oneof 字段类型:

  • scalar
  • enum
  • 全局 predefined struct
  • 自定义命名 struct
  • 同级的本地 predefined struct

例如,HelloWorld.xlsx 中的 worksheet Target

NameAliasTypeField1Field2#Note
FruitFruitenum<.FruitType>Bound to enum
PointPointint32Bound to scalar
ItemItem.ItemBound to global predefined struct
PlayerPlayerID
uint32
Name
string
Bound to local defined struct
FriendFriendPlayerBound to local predefined in the same level
MonsterMonsterCustomMonsterHealth
uint32
Attack
int32
Bound to local defined struct with custom type name
BossBossCustomMonsterBound to local predefined struct in the same level
SheetMode
TargetMODE_UNION_TYPE

生成结果:

hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx"};

// Generated from sheet: Target.
message Target {
  option (tableau.union) = {name:"Target"};

  Type type = 9999 [(tableau.field) = {name:"Type"}];
  oneof value {
    option (tableau.oneof) = {field:"Field"};

    protoconf.FruitType fruit = 1; // Bound to enum value: TYPE_FRUIT.
    int32 point = 2;               // Bound to enum value: TYPE_POINT.
    protoconf.Item item = 3;       // Bound to enum value: TYPE_ITEM.
    Player player = 4;             // Bound to enum value: TYPE_PLAYER.
    Player friend = 5;             // Bound to enum value: TYPE_FRIEND.
    CustomMonster monster = 6;     // Bound to enum value: TYPE_MONSTER.
    CustomMonster boss = 7;        // Bound to enum value: TYPE_BOSS.
  }

  enum Type {
    TYPE_INVALID = 0;
    TYPE_FRUIT = 1 [(tableau.evalue).name = "Fruit"];
    TYPE_POINT = 2 [(tableau.evalue).name = "Point"];
    TYPE_ITEM = 3 [(tableau.evalue).name = "Item"];
    TYPE_PLAYER = 4 [(tableau.evalue).name = "Player"];
    TYPE_FRIEND = 5 [(tableau.evalue).name = "Friend"];
    TYPE_MONSTER = 6 [(tableau.evalue).name = "Monster"];
    TYPE_BOSS = 7 [(tableau.evalue).name = "Boss"];
  }

  message Player {
    uint32 id = 1 [(tableau.field) = {name:"ID"}];
    string name = 2 [(tableau.field) = {name:"Name"}];
  }
  message CustomMonster {
    int32 health = 1 [(tableau.field) = {name:"Health"}];
    int32 attack = 2 [(tableau.field) = {name:"Attack"}];
  }
}

复杂 union 类型

例如,HelloWorld.xlsx 中的两个 worksheet TargetTaskConf

NameAliasField1Field2Field3
PVPAliasPVPID
uint32
Note
Damage
int64
Note
Type
[]enum<.FruitType>
Note
PVEAliasPVEMission
{uint32 ID, enum<.ItemType> Type}Mission
Note
Hero
[]uint32
Note
Dungeon
map<int32, int64>
Note
StoryAliasStoryCost
{.Item}
Note
Fruit
map<int32, enum<.FruitType»
Note
Flavor
map<enum<.FruitFlavor>, enum<.FruitType»
Note
HobbyAliasHobbyFlavor
map<enum<.FruitFlavor>, enum<.FruitType»
Note
StartTime
datetime
Note
Duration
duration
Note
SkillAliasSkillID
uint32
Note
Damage
int64
Note
EmptyAliasEmpty
IDTargetTypeTargetField1TargetField2TargetField3Progress
map<int32, Task>{.Target}enum<.Target.Type>unionunionunionint32
IDTarget’s typeTarget’s field1Target’s field2Target’s field3Progress
1AliasPVP110Apple,Orange,Banana3
2AliasPVE1,Equip1,2,31:10,2:20,3:3010
3AliasStory1001,101:Apple,2:OrangeFragrant:Apple,Sour:Orange10
4AliasHobbyFragrant:Apple,Sour:Orange2023-06-01 10:00:0022s12
5AliasSkill12008
6AliasEmpty
SheetMode
TargetMODE_UNION_TYPE
Task

生成结果:

hello_world.proto
// --snip--
option (tableau.workbook) = {name:"HelloWorld.xlsx" namerow:1 typerow:2 noterow:3 datarow:4};

// Generated from sheet: Target.
message Target {
  option (tableau.union) = {name:"Target"};

  Type type = 9999 [(tableau.field) = { name: "Type" }];
  oneof value {
    option (tableau.oneof) = {field: "Field"};

    PVP pvp = 1; // Bound to enum value: TYPE_PVP.
    PVE pve = 2; // Bound to enum value: TYPE_PVE.
    Story story = 3; // Bound to enum value: TYPE_STORY.
    Hobby hobby = 4; // Bound to enum value: TYPE_HOBBY.
    Skill skill = 5; // Bound to enum value: TYPE_SKILL.
    Empty empty = 6; // Bound to enum value: TYPE_EMPTY.
  }
  enum Type {
    TYPE_INVALID = 0;
    TYPE_PVP = 1 [(tableau.evalue).name = "AliasPVP"];
    TYPE_PVE = 2 [(tableau.evalue).name = "AliasPVE"];
    TYPE_STORY = 3 [(tableau.evalue).name = "AliasStory"];
    TYPE_HOBBY = 4 [(tableau.evalue).name = "AliasHobby"];
    TYPE_SKILL = 5 [(tableau.evalue).name = "AliasSkill"];
    TYPE_EMPTY = 6 [(tableau.evalue).name = "AliasEmpty"];
  }

  message PVP {
    uint32 id = 1 [(tableau.field) = {name:"ID"}];
    int64 damage = 2 [(tableau.field) = {name:"Damage"}];
    repeated protoconf.FruitType type_list = 3 [(tableau.field) = {name:"Type" layout:LAYOUT_INCELL}];
  }
  message PVE {
    Mission mission = 1 [(tableau.field) = {name:"Mission" span:SPAN_INNER_CELL}];
    message Mission {
      uint32 id = 1 [(tableau.field) = {name:"ID"}];
      protoconf.ItemType type = 2 [(tableau.field) = {name:"Type"}];
    }
    repeated uint32 hero_list = 2 [(tableau.field) = {name:"Hero" layout:LAYOUT_INCELL}];
    map<int32, int64> dungeon_map = 3 [(tableau.field) = {name:"Dungeon" layout:LAYOUT_INCELL}];
  }
  message Story {
    protoconf.Item cost = 1 [(tableau.field) = {name:"Cost" span:SPAN_INNER_CELL}];
    map<int32, protoconf.FruitType> fruit_map = 2 [(tableau.field) = {name:"Fruit" layout:LAYOUT_INCELL}];
    map<int32, Flavor> flavor_map = 3 [(tableau.field) = {name:"Flavor" key:"Key" layout:LAYOUT_INCELL}];
    message Flavor {
      protoconf.FruitFlavor key = 1 [(tableau.field) = {name:"Key"}];
      protoconf.FruitType value = 2 [(tableau.field) = {name:"Value"}];
    }
  }
  message Hobby {
    map<int32, Flavor> flavor_map = 1 [(tableau.field) = {name:"Flavor" key:"Key" layout:LAYOUT_INCELL}];
    message Flavor {
      protoconf.FruitFlavor key = 1 [(tableau.field) = {name:"Key"}];
      protoconf.FruitType value = 2 [(tableau.field) = {name:"Value"}];
    }
    google.protobuf.Timestamp start_time = 2 [(tableau.field) = {name:"StartTime"}];
    google.protobuf.Duration duration = 3 [(tableau.field) = {name:"Duration"}];
  }
  message Skill {
    uint32 id = 1 [(tableau.field) = {name:"ID"}];
    int64 damage = 2 [(tableau.field) = {name:"Damage"}];
  }
  message Empty {
  }
}

message TaskConf {
  option (tableau.worksheet) = {name:"TaskConf"};

  map<int32, Task> task_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL}];
  message Task {
    int32 id = 1 [(tableau.field) = {name:"ID"}];
    protoconf.Target target = 2 [(tableau.field) = {name:"Target"}];
    int32 progress = 3 [(tableau.field) = {name:"Progress"}];
  }
}
TaskConf.json
{
    "taskMap": {
        "1": {
            "id": 1,
            "target": {
                "type": "TYPE_PVP",
                "pvp": {
                    "id": 1,
                    "damage": "10",
                    "typeList": [
                        "FRUIT_TYPE_APPLE",
                        "FRUIT_TYPE_ORANGE",
                        "FRUIT_TYPE_BANANA"
                    ]
                }
            },
            "progress": 3
        },
        "2": {
            "id": 2,
            "target": {
                "type": "TYPE_PVE",
                "pve": {
                    "mission": {
                        "id": 1,
                        "type": "ITEM_TYPE_EQUIP"
                    },
                    "heroList": [
                        1,
                        2,
                        3
                    ],
                    "dungeonMap": {
                        "1": "10",
                        "2": "20",
                        "3": "30"
                    }
                }
            },
            "progress": 10
        },
        "3": {
            "id": 3,
            "target": {
                "type": "TYPE_STORY",
                "story": {
                    "cost": {
                        "id": 1001,
                        "num": 10
                    },
                    "fruitMap": {
                        "1": "FRUIT_TYPE_APPLE",
                        "2": "FRUIT_TYPE_ORANGE"
                    },
                    "flavorMap": {
                        "1": {
                            "key": "FRUIT_FLAVOR_FRAGRANT",
                            "value": "FRUIT_TYPE_APPLE"
                        },
                        "2": {
                            "key": "FRUIT_FLAVOR_SOUR",
                            "value": "FRUIT_TYPE_ORANGE"
                        }
                    }
                }
            },
            "progress": 10
        },
        "4": {
            "id": 4,
            "target": {
                "type": "TYPE_HOBBY",
                "hobby": {
                    "flavorMap": {
                        "1": {
                            "key": "FRUIT_FLAVOR_FRAGRANT",
                            "value": "FRUIT_TYPE_APPLE"
                        },
                        "2": {
                            "key": "FRUIT_FLAVOR_SOUR",
                            "value": "FRUIT_TYPE_ORANGE"
                        }
                    },
                    "startTime": "2023-06-01T02:00:00Z",
                    "duration": "22s"
                }
            },
            "progress": 12
        },
        "5": {
            "id": 5,
            "target": {
                "type": "TYPE_SKILL",
                "skill": {
                    "id": 1,
                    "damage": "200"
                }
            },
            "progress": 8
        },
        "6": {
            "id": 6,
            "target": {
                "type": "TYPE_EMPTY",
                "empty": {}
            },
            "progress": 0
        }
    }
}