0.6.6 • Published 8 years ago

ds-lang v0.6.6

Weekly downloads
2
License
MIT
Repository
github
Last release
8 years ago

ds-lang

数据建模语言。

主要用于我们自己的工作流。

工作流

  • 建模语言 ds-lang - 建模后,可以生成sql脚本、excel配置文件、protobuf文件。
  • excel配置文件导出工具 xlsx2csv - 填好excel配置文件后,我们可以将excel文件导出到csv,配合生成的代码,实现excel文件自动展开到内存中。

why

对于程序员来说,数据建模,最方便的还是写代码,定义一个struct,是最简单直接的了,而写sql语句之类的,甚至操作phpmyadmin,都是一件很麻烦的事情,更何况,操作完phpmyadmin以后,还是要写一遍struct,甚至还要写中间代码,这部分工作几乎是重复劳动。

为什么不能写一个struct,就把数据库建好,然后把各种代码自动生成出来呢?

不管是mysql,还是redis,甚至是既有mysql持久化又有redis缓存,其实数据建模本身几乎是无差异的。

上面就是这个建模语言的初衷。

其实有了建模数据以后,我们还可以用这个数据做很多自动化编码的工作,省时省事而且降低人为编码的错误率。

进一步来说,我们还会有一系列的工具,做更多的自动化可视化流程,可以影响到策划、美术、测试、运营等等。

我们需要的只是一些更好读的中间数据,而ds-lang做的仅仅是更方便的产生这一批好读的中间数据。

更新说明

  • 0.6.6
  • 增加 float 的支持。

  • 0.6.3

  • 增加 --ver 参数,查看版本号。
  • 增加对文件是否存在的检查。

  • 0.6.2

  • 增加 VER 的宏,必须要声明,采用 1512280 的格式,其中前面6位是年月日,最后一位是今日的版本号,也就是一天最多只能有10个有效版本。
  • excel导出 时,会兼容老版本。
  • 增加 DELETE 关键字,前置删除属性用。

  • 0.6.1

  • 导出的excel表格里,会在tab页里加入枚举表,方便编辑查阅。

  • 0.6.0

  • 将内核部分独立成一个新项目 dsl-core

  • 0.5.2

  • 使用handlebars重构代码生成部分。
  • 支持crystal导出NodeJS服务端代码。
  • 支持枚举直接做默认值。
  • 修正展开子结构时没有考虑下划线的bug。

  • 0.5.1

  • 代码生成时,对齐代码。
  • 调整了代码生成器插件的部分接口。
  • 支持crystal框架的代码生成。
  • 代码生成插件支持多文件的生成。
  • 支持crystal导出C++客户端代码。

  • 0.5.0

  • 加入代码生成功能。
  • 规范了后续代码生成插件的制作方式。

  • 0.3.2

  • 对重名做了更严格的检查,下划线开头不能算一个单独的名字。
  • 增加 map 关键字,主要用于数据管理层。

  • 0.3.1

  • 对消息协议进一步支持。
  • 支持导出protobuf
  • 增加关键字bytes,表示二进制数据流类型。
  • 增加关键字bool,表示bool类型。
  • 增加关键字truefalse

  • 0.3.0

  • 增加message关键字,用来处理消息协议。
  • 增加struct名字前加下划线,不单独为数据库建表的约定。
  • 增加对普通结构体成员expand的语法支持,就是直接展开子结构体,而少一层查找关系,一定程度上可以用来实现类继承的设计。
  • 增加设置AUTOINC的初值。

    		primary PlayerID pid = AUTOINC(10000);		// 角色唯一标识,10000开始
* **0.2.5**
 * 调整解析顺序,改成和输入顺序一致。
 * 修正输出excel文件时重复创建目录失败的bug。
 * 增加对静态表枚举展开(**expand**)的支持(暂不支持动态表的枚举展开)。
 * 增加结构内数组主索引字段的语法支持。

repeated PlayerGameInfo(pid) _gameinfo; // 游戏信息

 * 修正小bug若干。

* **0.2.4**
 * 增强语法检验。
 * 增加命令行参数**--sql**,能直接输出sql语句。
 * 增加命令行参数**--excel**,能将**static**表导出成excel文件。

* **0.2.3**
 * 修正npm发布项目的bug,**package.json**文件需要**latest**属性。

* **0.2.2**
 * 修正命令行工具的bug。

* **0.2.1**
 * 增加 NULL 关键字,用于结构属性,表示该属性可以没有值。
 * 明确了基本默认值。

* **0.2.0**
 * 增加对结构声明的语法支持。

struct MAGInfo; // MAG磁力下载站

 * 完善注释的解析,去掉了前后无意义字符(whitespace)。
 * 增加命名规范的支持。
 * 增加 unique 关键字,用于唯一且不是主键的索引。
 * 增加 AUTOINC 关键字,用于结构中,主键的默认值。
 * 增加 NOW 关键字,用于结构中,时间属性的默认值。

* **0.1.0**
 * 制定基本的语法规则。
 * 解析器。

语法
---
第一次构建一种语言,非常的没有经验,基本上是按照类C语法做的,部分参考google的protobuf,在使用方面,更多的借鉴动态语言,譬如结构默认值就是直接写在结构里了。

原则上,是**强类型**、**强注释**、**强编码规范**的,语法符合大部分人的编码习惯,不反人类。

* **强类型** - 每个对象或变量都是先给定类型的,也鼓励大家用typedef定义新的类型,譬如把ID和int分开(其实本质上ID也就是int)。

typedef int ID; // 定义ID类型,实际上是int

* **强注释** - 必须有注释,否则编译不过。因为这是一个建模语言,基本上每个结构每个属性都应该有注释,一定要让人看明白。

* **强编码规范** - 在语言层面强化编码规范,譬如golang这样的,我个人是认同这个方案的。一些最基本的东西需要遵循一定的规范,遵循规范的同时,其实可以带来一些编码的畅快感(少输入)。

基本语法:

* **typedef** - 类型重定向,也就是C里面的typedef。
* **struct** - 结构体,类似C的struct定义,唯一的差别就是支持默认值,直接在声明里写就可以了。
* **static** - 静态结构体定义,类struct,只是明确的表示,这是一个静态配置表。
* **enum** - 枚举,枚举其实是一组常量的集合,语法上和C有些不同。枚举必须大写,而且枚举内容必须是大写枚举加下划线开头,而且枚举是以分号分隔的,每一行定义必须显示的写好对应int值。枚举的值是可以在后面直接使用的。

// 英雄属性枚举 enum HEROATTR{ HEROATTR_VIT = 0; // 体力 - vitality HEROATTR_STA = 1; // 耐力 - stamina HEROATTR_STR = 2; // 力量 - strength HEROATTR_INT = 3; // 智力 - intelligence HEROATTR_DEX = 4; // 敏捷 - dexterity HEROATTR_CRIT = 5; // 暴击 - critical HEROATTR_PARR = 6; // 格挡 - parry HEROATTR_HIT = 7; // 命中 - hit HEROATTR_MISS = 8; // 闪避 - miss };

* **全局变量** - 任何定义在结构体和静态表之外的单独变量都是全局变量。全局变量必须由大写字母、下划线、数字组成,且必须以大写字母打头。
* **结构声明** - 可以在结构体定义以前,声明一个结构体,主要用于A依赖B,B依赖A的情况。

struct MAGInfo; // MAG磁力下载站

 > **注意:**结构声明的注释如果和后面定义的不一样,会拿定义的注释覆盖注释。

* **结构属性前缀** - 对于结构体来说,有一组简单的前缀,可以方便后续工作流的更好工作。
 * **primary** - 主键,也就是这个结构体的唯一标识,如果最后会选择sql数据库落地的话,这个也就是数据库的主键了。
 * **联合主键** - primary0、primary1,也就是多个主键一起构成联合主键。
 * **index** - 索引,逻辑上,可能会需要根据该属性做查找工作,在数据库里也就是要为他建索引。
 * **unique** - 唯一的索引,是一个全局唯一的索引。
 * **枚举展开** - 会有很多时候,我们需要一个特定意义的数组,每个单元是一个特殊含义(也就是有一个枚举做数组下标)。我们可以通过一个简单的前缀做到这点。

expand(HEROATTR) int heroattr; // 英雄属性数组,根据HEROATTR展开

 * **数组** - 类似protobuf的数组。

repeated HeroInfo lsthero; // 英雄列表

 * **单根树状数组** - 在游戏里,存在着大量的单根树状结构,譬如一个角色带着一堆宝宝,每个宝宝身上都还有一堆装备,每件装备上还有一堆宝石等,这种单根树状结构会自动被正确识别上层主键加载。
repeated PlayerGameInfo(pid) _gameinfo;	// 游戏信息
* **结构属性默认值** - 结构属性可以给一个默认值。
 * **基本默认值** - 基本类型都有一个基本的默认值,如果整数类的,默认值是0,字符串则是一个空字符串。
 * **常量默认值** - 就是一个常量,或者前面已经定义好的枚举值,如果是数值型的,可以有四则运算。
 * **默认没有值** - 主要也是数据库用的,默认该属性没有值,用 NULL 来表示。当前版本下,内存中是一定有值的,也就是基本默认值。
 * **自增长整数** - 数据库常用的,我们可以用 AUTOINC 来表示。这个只能用于主键。
primary PlayerID pid = AUTOINC;		// 角色唯一标识
 * **当前时间** - 也是数据库常用的,我们可以用 NOW 来表示。一般来说,数据库只允许一列用这个属性,而我们不存在这种要求。
time regtime = NOW;					// 注册时间
time lastlogintime = NOW;			// 最后一次登录时间
* **通信协议** - 通信协议是很重要的一部分内容,通信协议是message,成员变量的定义方式基本上和struct一样。
 * **通信协议的基本概念** - 一般来说,通信协议分为2部分,一个是通信协议标识,我们定义为MsgID,另外一个是通信协议结构定义,也就是我们的message定义。
 * **自动生成MsgID** - 只要按照我们的命名规范来定义协议,我们就可以自动生成MsgID,并保证全局唯一易于维护更新方便。
 * **协议的命名** - 客户端发出的协议必须以 **Req_** 开头,服务器发出的协议必须以 **Res_** 开头。这样命名以后,会自动生成2组MsgID。
 * **控制MsgID的区间** - 我们可以重载全局变量 **REQ_MSGID_BEGIN** 和 **RES_MSGID_BEGIN** 来重新定义服务器和客户端的消息起点,默认是10000和20000开始排列。
 * **重载MsgID类型** - 默认的**MsgID**是**int**的,我们也可以重载**MsgID**类型。

* **命名规范** - 命名有一定的要求。
 * **枚举类型命名** - 枚举名只能是大写字母和下划线,且不能以下划线开头。
 * **枚举值命名** - 枚举值一定是枚举类型名加下划线开头。
 * **类型命名** - 类型命名一定是大写字母开头。
 * **类型属性命名** - 类型属性一定是小写字母或者下划线开头。
 * **客户端不需要看到的属性** - 类型属性中,如果有部分属性是客户端不需要看到的,下划线开头。
 * **全局变量命名** - 全局变量一定由大写字母和下划线组成,且不能由下划线开头。
 * **下划线开头的名字** - 下划线开头的名字有特殊的意义,因此2个名字如果一个下划线开头,一个没有下划线开头的,算重名处理。

基本变量类型:

* **int** - 整数,32位整数,有符号
* **int64** - 64位整数,有符号
* **time** - 时间戳,32位,最大到2038年
* **float** - 浮点数,32位
* **string** - 字符串,一般来说,不建议超过256字符
* **info** - 长字符串
* **bytes** - 二进制数据流
* **bool** - bool类型,只有2个值,就是 **true** 和 **false**

例子:

int MAX_LEVEL = 80; // 最大等级

typedef int HeroExpType; // 英雄经验类型

// 英雄属性枚举,枚举必须大写,而且枚举内容必须是大写枚举加下划线开头 enum HEROATTR{ HEROATTR_VIT = 0; // 体力 - vitality HEROATTR_STA = 1; // 耐力 - stamina HEROATTR_STR = 2; // 力量 - strength HEROATTR_INT = 3; // 智力 - intelligence HEROATTR_DEX = 4; // 敏捷 - dexterity HEROATTR_CRIT = 5; // 暴击 - critical HEROATTR_PARR = 6; // 格挡 - parry HEROATTR_HIT = 7; // 命中 - hit HEROATTR_MISS = 8; // 闪避 - miss };

// 英雄技能枚举 enum HEROSKILL{ HEROSKILL_SKILLID1 = 0; // 普通技能1 HEROSKILL_SKILLID2 = 1; // 普通技能2 HEROSKILL_SKILLID3 = 2; // 普通技能3

HEROSKILL_BSKILLID1 = 3;	// 条件技能1
HEROSKILL_BSKILLID2 = 4;	// 条件技能2

HEROSKILL_MSKILLID1 = 5;	// 主动技能1

};

// 玩家经验表 static PlayerExp{ primary int playerlevel; // 玩家等级

index int totalplayerexp;	// 玩家当前等级需要的总经验(不算当前等级需要的经验)

int playerexp;				// 玩家当前等级升级需要的经验

};

// 英雄经验表 static HeroExp{ primary0 HeroExpType heroexptype; // 英雄经验类型 primary1 int herolevel; // 英雄等级

index int totalheroexp;				// 英雄当前等级需要的总经验(不算当前等级需要的经验)

int heroexp;						// 英雄当前等级升级需要的经验

};

// 英雄基本配置表 static HeroBase{ primary int heroid; // 英雄唯一标识

HeroExpType heroexptype;			// 英雄经验类型

expand(HEROATTR) int heroattr;		// 英雄属性数组,根据HEROATTR展开
expand(HEROSKILL) int heroskill;	// 英雄属性数组,根据HEROSKILL展开

};

// 英雄装备信息 struct HeroEquInfo{ primary int equid; // 装备唯一标识 };

// 英雄信息 struct HeroInfo{ primary int heroid; // 英雄唯一标识

int herolevel;						// 英雄等级
int heroexp;						// 英雄当前经验

expand(HEROATTR) int heroattr;		// 英雄属性数组,根据HEROATTR展开
expand(HEROSKILL) int heroskill;	// 英雄属性数组,根据HEROSKILL展开

repeated HeroEquInfo lstequ;		// 英雄列表

};

// 角色基本信息 struct PlayerInfo{ primary int pid; // 角色唯一标识

string name;						// 角色名
time regtime;						// 注册时间
time lastlogintime;					// 最后一次登录时间

int playerlevel;					// 角色等级
int playerexp;						// 角色经验

int gold;							// 金币
int gem;							// 钻石	

repeated HeroInfo lsthero;			// 英雄列表

};

数据类型
---
由于ds-lang支持**typedef**,所以可以根据类型建立起数据之间的联系。

为了更方便的自动建立各模块之间的联系,多用**typedef**吧。

typedef int PlayerID; // 玩家ID类型 typedef int RoomID; // 房间ID类型 typedef int Gem; // 钻石类型 typedef int Gold; // 金币类型 typedef int PlayerLevel; // 玩家等级类型 typedef int PlayerExp; // 玩家经验类型

自动生成代码
---
我们的自动代码生成是插件方式的,其实就是根据生成的json文件来做代码生成。这里我们提供了我们项目的几种方式,你也可以根据你自己项目需要来实现适合你们项目的代码生成插件。

* **HeyServ 框架** - 服务器是基于**PHP**的,**HTTP**短连接,客户端有一套**Cocos2d**的实现,包含**c++**和**lua**。
* **Crystal 框架** - 服务器基于**Nodejs**的分布式服务器框架,**WebSocket**长连接,客户端也有一套**Cocos2d**的实现,包含**c++**和**lua**。

数据存储级别
---
我们定义数据存储级别为```DB -> CACHE -> SERVICE -> CLIENT```4个级别,我们认为SERVICE的数据是最完整的。

下划线打头的结构成员是不同步到客户端的。

下划线打头的结构体是不生成独立的表或记录的。

自动的ID排布和版本管理
---
ds-lang为了简化编码,大量的使用了自动的ID排布,譬如message和MSGID的自动生成。这样自动的ID排布,最大的问题就在版本同步上了,那我们应该怎样做版本管理呢?

前面也说过ds-lang只是我们工作流的一部分,我们有一整套提交比对机制,保证版本同步,后续的工具也会逐步开放给大家的,譬如修改了数据建模,会自动生成差异化的sql脚本,供你同步sql数据库,也会主动修改excel文件内容,增减列。

> 注:理论上我们是不建议做删除数据的操作的,不能确定这一定是个合适的操作,所以后续版本会增加 DELETE 前缀。还会提供一个强制清理的命令,一次性清理所有不要的数据。

jison
---
grammar目录内的dsl.jison文件就是jison的语法文件,可以通过命令行命令生成出dsl.js文件。

jison dsl.jison

但是,为了能多次使用解释器,我们还加了一个初始化接口,最好在dsl.js文件里面显示的调用一下。

parse: function parse(input) { __onInit();

至于jison的安装,可以参考[jison](https://github.com/zaach/jison)相关文档。

如果对jison语法有兴趣,也可以看看我的另外一个项目[jison-demo](https://github.com/zhs007/jison-demo),这里面有一些基础的例子,基本上ds-lang就是基于这些例子完善起来的。

二次开发
---
ds-lang的二次开发有2种方式:

1. ds-lang是用jison生成的建模语言,相关的命令行工具和语法文件都是开源的,可以直接修改工具和语法文件获得新的支持。
2. ds-lang会生成一个json格式文件,这个文件其实是非常好读的,也可以基于这个输出文件做后续的二次开发。

如果你有更好的想法,也可以和我们联系。

使用到的第三方库
---

* 使用**[jison](https://github.com/zaach/jison)**做语法分析。
* 使用**[yargs](https://github.com/bcoe/yargs)**模块简化命令行工具的开发。
* 使用**[node-xlsx](https://github.com/mgcrea/node-xlsx)**模块生成xlsx文件。
* 使用**[handlebars](https://github.com/wycats/handlebars.js/)**模板做基本代码模板。
0.6.6

8 years ago

0.6.5

8 years ago

0.6.4

8 years ago

0.6.3

8 years ago

0.6.2

8 years ago

0.6.1

8 years ago

0.6.0

8 years ago

0.5.2

8 years ago

0.5.1

9 years ago

0.5.0

9 years ago

0.3.2

9 years ago

0.2.3

9 years ago

0.2.2

9 years ago

0.2.1

9 years ago