XDef元模型定义语言

Nop平台中所有的DSL语言统一采用XML格式,而不是自定义的表观语法格式,这样可以简化DSL设计并提供统一的IDE开发工具。具体来说,所有的DSL采用统一的XDef元模型定义DSL的具体语法(XML的结构),
然后利用Nop平台内置的一系列机制自动生成代码,实现DSL的解析、验证等功能。

XDef元模型文件的作用类似于XSD(XML Schema Definition)文件,都是为XML格式增加语法约束。但是XDef相比于XSD更加简单易用,而且提供了更加强大的约束能力。

XDef语法示例

我们来看一个简单的工作流DSL定义:工作流包含多个步骤,每个步骤完成后指定下一个可执行的步骤。

<workflow name="Test" x:schema="/nop/schema/my-wf.xdef" xmlns:x="/nop/schema/xdsl.xdef">
<steps>
<step id="a" displayName="StepA" next="b">
<source>
<c:script>
import app.MyHelper;
MyHelper.doSomething();
</c:script>
</source>
</step>

<step id="b" displayName="StepB" joinType="and" />
</steps>
</workflow>

对应的元模型为


<workflow name="!string" x:schema="/nop/schema/xdef.xdef" xmlns:x="/nop/schema/xdsl.xdef">
<steps xdef:body-type="list" xdef:key-attr="id">
<step id="!string" displayName="string" internal="!boolean=false"
joinType="enum:io.nop.wf.core.model.WfJoinType" next="string">
<source xdef:value="xpl"/>
</step>
</steps>
</workflow>

首先我们看到,XDef元模型与它所描述的模型之间是一种同态关系,简单的说,将模型XML中的值替换成类型描述符就可以得到XDef元模型。

  • name="!string" 表示name属性为string类型,字符!表示属性值不能为空。
  • xdef:body-type="list" 表示节点解析后对应于列表类型,xdef:key-attr="id"表示列表中每个元素都必须具有一个id属性,通过id属性可以区分不同的元素。
  • internal="!boolean=false" 表示internal属性不为空,类型为boolean,缺省值为false
  • joinType="enum:io.nop.wf.core.model.WfJoinType" 表示joinType属性的值为WfJoinType类型,它是一个枚举值。
  • xdef:value="xpl" 表示source节点的内容(包含直接的文本内容以及所有的子节点)为Xpl模板语言的代码段,解析后可以直接得到一个IEvalAction对象(类似于JavaScript中的Function对象)。

xdef文件中的所有属性(除去xdef名字空间以及x名字空间中的内置属性)的值类型都是def-type类型,它的格式为 (!~#)?{stdDomain}:{options}={defaultValue}

  • !表示必填属性,~表示内部属性,#表示可以使用编译期表达式
  • stdDomain是比数据类型更严格的格式限制,例如stdDomain=email等,具体值参见字典定义core/std-domain
  • 某些def-type定义需要options参数,例如enum:xxx.yyy,通过options来设置具体的字典名称
  • 可以为属性指定缺省值

XDSL公共语法

在XML的根节点上必须通过x:schema属性引入元模型定义。例如x:schema="/nop/schema/my-wf.xdef"表示模型由my-wf.xdef元模型来约束。

Nop平台中所有的DSL语言都具有一些公共的属性和子节点,相当于是为所有DSL引入一些公共的语法,x:schema属性就是这个公共语法的一部分。这些公共语法在xdsl.xdef
元模型中定义,所以我们要在根节点上通过属性xmlns:x="/nop/schema/xdsl.xdef"表示x名字空间对应于DSL公共语法空间。具体介绍参见
xdsl.xdef
XDSL:通用的领域特定语言设计

XDef元模型定义语言的能力足够强大,它可以被用于描述XDef元模型自身,具体参见xdef.xdef

xdef.xdef这个元元模型定义文件中,xdef名字空间必须被看作是普通属性空间,不能被解释为XDef元属性,所以在根节点上我们增加了属性定义xmlns:meta="/nop/schema/xdef.xdef",使用meta名字空间来表达元属性。

<workflow xmlns:meta="/nop/schema/xdef.xdef">
<steps meta:body-type="list" meta:key-attr="id">
...
</steps>
</workflow>

等价于

<workflow xmlns:xdef="/nop/schema/xdef.xdef">
<steps xdef:body-type="list" xdef:key-attr="id">
...
</steps>
</workflow>

复用节点定义

在xdef文件中可以通过xdef:ref来引用已有的元模型定义。

  1. 引入外部xdef文件
<form id="!string" xdef:ref="form.xdef" />
  1. 引用内部节点
    在任意节点上可以增加xdef:name属性,将它标记为命名节点。然后就可以通过xdef:ref来引用。
<steps>
<step id="!string" xdef:name="WorkflowStepModel">
...
</step>

<join id="!string" xdef:ref="WorkflowStepModel" xdef:name="WfJoinStepModel">
</join>
</steps>

注:目前因为实现上的原因,id等作为集合元素唯一区分的属性需要被重复,而其他属性则可直接引用自其他节点,无需重复定义。

在代码生成的时候,xdef:name会被看作是节点对应的Java类名,xdef:ref会被看作是当前节点类的基类。

xdef:ref="WorkflowStepModel" xdef:name="WfJoinStepModel"对应于代码生成 class WfJoinStepModel extends WorkflowStepModel

为了简化节点复用,XDef语言还规定了一种特殊的、仅用于复用的特殊节点xdef:define,例如

<workflow>
<xdef:define xdef:name="WorkflowStepModel" id="!string">
....
</xdef:define>

<steps xdef:body-type="list" xdef:key-attr="id">
<step xdef:ref="WorkflowStepModel" id="!string"/>
</steps>
</workflow>

xdef:define是定义一个可重用的部分,相当于定义基类,然后在节点上可以通过xdef:ref来继承这个基类。xdef:name相当于是基类的类名。

集合节点定义

除了上面介绍的xdef:body-type="list"来表示集合节点之外,xdef语言还提供了一种简化的集合节点定义方式: 使用xdef:unique-attr表示集合元素的唯一表示属性。

<arg name="!string" xdef:unique-attr="name" value="any" />

具有xdef:unique-attr属性的节点会被解析为集合属性,属性名一般为 节点名驼峰变换+'s',比如<task-step xdef:unique-attr="id">对应于taskSteps
我们也可以通过xdef:bean-prop属性来指定对应的属性名,例如可以指定xdef:bean-prop="taskStepList"

<!--
以下 DSL 定义等价于代码:
bp.taskSteps.add({id: 'a', displayName: 'A'})
bp.taskSteps.add({id: 'b', displayName: 'B'})
-->
<task-step id="a" displayName="A"></task-step>
<task-step id="b" displayName="B"></task-step>

使用xdef:body-type="list"方式来定义集合属性的好处在于,它允许集合中包含不同类型的子节点,例如


<steps xdef:body-type="list" xdef:key-attr="name" xdef:bean-sub-type-prop="type" xdef:bean-child-name="step"
xdef:bean-body-type="List&lt;io.nop.wf.core.model.WfStepModel>">
<step name="!string" xdef:bean-tag-prop="type" />
<join name="!string" xdef:bean-tag-prop="type" />
</steps>
  • xdef:bean-body-type用于指定生成的集合属性类型名
  • xdef:bean-child-name="step"表示自动为模型对象增加getStep(String name)方法,用于按照唯一标识属性来获取子节点
  • xdef:bean-tag-prop="type"表示节点的标签名称(stepjoin)在json序列化时将被解析为type属性的值
  • xdef:bean-sub-type-prop="type"表示json反序列化的时候,根据type属性来确定子节点类型
<!--
以下 DSL 定义转换为 JSON 后的结构为:
{
"steps": [{
"type": "step",
"name": "a"
}, {
"type": "join",
"name": "b"
}]
}
-->
<steps>
<step name="a"/>
<join name="b"/>
</steps>

常见问题解答

  1. 使用<parent> <item xdef:value="string"> </parent>定义 和<parent item="string"> 定义有什么区别?
    生成到java中没有区别,这种只是在xml形式层面有区别

  2. 如果指定xdef:body-type="list", 那么它的子节点必须是对象类型吗?如果是int或者string之类的基本类型应该怎么写?
    我没有处理过这种情况,如果一定要处理。目前的做法大概是 <list xdef:body-type="list"> <_ xdef:value="int" /> 类似这种。
    另外对于常用的逗号分隔的列表值,可以直接写 <list xdef:value="csv-list" />

  3. 如果一个节点已经设置了xdef:body-type="list", 但是还有别的属性 例如
    <row name="string" xdef:body-type="list" height="string">, 这会生成什么结构?
    缺省情况下body内容会对应于body属性。这是和AMIS的约定保持一致,tagName对应于type, body内容对应于body。