# Api语法描述 ## api示例 ``` golang /** * api语法示例及语法说明 */ // api语法版本 syntax = "v1" // import literal import "foo.api" // import group import ( "bar.api" "foo/bar.api" ) info( author: "songmeizi" date: "2020-01-08" desc: "api语法示例及语法说明" ) // type literal type Foo{ Foo int `json:"foo"` } // type group type( Bar{ Bar int `json:"bar"` } ) // service block @server( jwt: Auth group: foo ) service foo-api{ @doc "foo" @handler foo post /foo (Foo) returns (Bar) } ``` ## api语法结构 * syntax语法声明 * import语法块 * info语法块 * type语法块 * service语法块 * 隐藏通道 > ### 温馨提示️ > 在以上语法结构中,各个语法块从语法上来说,按照语法块为单位,可以在.api文件中任意位置声明, > 但是为了提高阅读效率,我们建议按照以上顺序进行声明,因为在将来可能会通过严格模式来控制语法块的顺序。 ### syntax语法声明 syntax是新加入的语法结构,该语法的引入可以解决: * 快速针对api版本定位存在问题的语法结构 * 针对版本做语法解析 * 防止api语法大版本升级导致前后不能向前兼容 > ### 警告 ⚠️ > 被import的api必须要和main api的syntax版本一致。 **语法定义** ``` antlrv4 'syntax'={checkVersion(p)}STRING ``` **语法说明** > syntax:固定token,标志一个syntax语法结构的开始 > > checkVersion:自定义go方法,检测`STRING`是否为一个合法的版本号,目前检测逻辑为,STRING必须是满足`(?m)"v[1-9][0-9]*"`正则。 > > STRING:一串英文双引号包裹的字符串,如"v1" > > 一个api语法文件只能有0或者1个syntax语法声明,如果没有syntax,则默认为v1版本 > **正确语法示例** ✅ eg1:不规范写法 ``` api syntax="v1" ``` eg2:规范写法(推荐) ``` api syntax = "v2" ``` **错误语法示例** ❌ eg1: ``` api syntax = "v0" ``` eg2: ``` api syntax = v1 ``` eg3: ``` api syntax = "V1" ``` ## import语法块 随着业务规模增大,api中定义的结构体和服务越来越多,所有的语法描述均为一个api文件,这是多么糟糕的一个问题, 其会大大增加了阅读难度和维护难度,import语法块可以帮助我们解决这个问题,通过拆分api文件, 不同的api文件按照一定规则声明,可以降低阅读难度和维护难度。 > ### 警告 ⚠️ > 这里import不像golang那样包含package声明,仅仅是一个文件路径的引入,最终解析后会把所有的声明都汇聚到一个spec.Spec中。 > 不能import多个相同路径,否则会解析错误。 **语法定义** ``` antlrv4 'import' {checkImportValue(p)}STRING |'import' '(' ({checkImportValue(p)}STRING)+ ')' ``` **语法说明** > import:固定token,标志一个import语法的开始 > > checkImportValue:自定义go方法,检测`STRING`是否为一个合法的文件路径,目前检测逻辑为,STRING必须是满足`(?m)"(/?[a-zA-Z0-9_#-])+\.api"`正则。 > > STRING:一串英文双引号包裹的字符串,如"foo.api" > **正确语法示例** ✅ eg: ``` api import "foo.api" import "foo/bar.api" import( "bar.api" "foo/bar/foo.api" ) ``` **错误语法示例** ❌ eg: ``` api import foo.api import "foo.txt" import ( bar.api bar.api ) ``` ## info语法块 info语法块是一个包含了多个键值对的语法体,其作用相当于一个api服务的描述,解析器会将其映射到spec.Spec中, 以备用于翻译成其他语言(golang、java等) 时需要携带的meta元素。如果仅仅是对当前api的一个说明,而不考虑其翻译 时传递到其他语言,则使用简单的多行注释或者java风格的文档注释即可,关于注释说明请参考下文的 **隐藏通道**。 > ### 警告 ⚠️ > 不能使用重复的key,每个api文件只能有0或者1个info语法块 **语法定义** ``` antlrv4 'info' '(' (ID {checkKeyValue(p)}VALUE)+ ')' ``` **语法说明** > info:固定token,标志一个info语法块的开始 > > checkKeyValue:自定义go方法,检测`VALUE`是否为一个合法值。 > > VALUE:key对应的值,可以为单行的除'\r','\n','/'后的任意字符,多行请以""包裹,不过强烈建议所有都以""包裹 > **正确语法示例** ✅ eg1:不规范写法 ``` api info( foo: foo value bar:"bar value" desc:"long long long long long long text" ) ``` eg2:规范写法(推荐) ``` api info( foo: "foo value" bar: "bar value" desc: "long long long long long long text" ) ``` **错误语法示例** ❌ eg1:没有key-value内容 ``` api info() ``` eg2:不包含冒号 ``` api info( foo value ) ``` eg3:key-value没有换行 ``` api info(foo:"value") ``` eg4:没有key ``` api info( : "value" ) ``` eg5:非法的key ``` api info( 12: "value" ) ``` eg6:移除旧版本多行语法 ``` api info( foo: > some text < ) ``` ## type语法块 在api服务中,我们需要用到一个结构体(类)来作为请求体,响应体的载体,因此我们需要声明一些结构体来完成这件事情, type语法块由golang的type演变而来,当然也保留着一些golang type的特性,沿用golang特性有: * 保留了golang内置数据类型`bool`,`int`,`int8`,`int16`,`int32`,`int64`,`uint`,`uint8`,`uint16`,`uint32`,`uint64`,`uintptr` ,`float32`,`float64`,`complex64`,`complex128`,`string`,`byte`,`rune`, * 兼容golang struct风格声明 * 保留golang关键字 > ### 警告 ⚠️ > * 不支持alias > * 不支持time.Time数据类型 > * 结构体名称、字段名称、不能为golang关键字 **语法定义** > 由于其和golang相似,因此不做详细说明,具体语法定义请在[ApiParser.g4](g4/ApiParser.g4)中查看typeSpec定义。 **语法说明** > 参考golang写法 **正确语法示例** ✅ eg1:不规范写法 ``` api type Foo struct{ Id int `path:"id"` // ① Foo int `json:"foo"` } type Bar struct{ // 非导出型字段 bar int `form:"bar"` } type( // 非导出型结构体 fooBar struct{ FooBar int } ) ``` eg2:规范写法(推荐) ``` api type Foo{ Id int `path:"id"` Foo int `json:"foo"` } type Bar{ Bar int `form:"bar"` } type( FooBar{ FooBar int } ) ``` **错误语法示例** ❌ eg ``` api type Gender int // 不支持 // 非struct token type Foo structure{ CreateTime time.Time // 不支持time.Time } // golang关键字 var type var{} type Foo{ // golang关键字 interface Foo interface } type Foo{ foo int // map key必须要golang内置数据类型 m map[Bar]string } ``` **① tag说明** > tag定义和golang中json tag语法一样,除了json tag外,go-zero还提供了另外一些tag来实现对字段的描述, > 详情见下表。 * tag表 |tag key |描述 |提供方 |有效范围 |示例 | |:--- |:--- |:--- |:--- |:--- | |json|json序列化tag|golang|request、response|`json:"fooo"`| |path|路由path,如`/foo/:id`|go-zero|request|`path:"id"`| |form|标志请求体是一个form(POST方法时)或者一个query(GET方法时`/search?name=keyword`)|go-zero|request|`form:"name"`| * tag修饰符 > 常见参数校验描述 |tag key |描述 |提供方 |有效范围 |示例 | |:--- |:--- |:--- |:--- |:--- | |optional|定义当前字段为可选参数|go-zero|request|`json:"name,optional"`| |options|定义当前字段的枚举值,多个以竖线②隔开|go-zero|request|`json:"gender,options=male"`| |default|定义当前字段默认值|go-zero|request|`json:"gender,default=male"`| |range|定义当前字段数值范围|go-zero|request|`json:"age,range=[0:120]"`| ② 竖线:| > ### 温馨提示 > tag修饰符需要在tag value后以引文逗号,隔开 ## service语法块 service语法块用于定义api服务,包含服务名称,服务metadata,中间件声明,路由,handler等。 > ### 警告 ⚠️ > * main api和被import的api服务名称必须一致,不能出现服务名称歧义。 > * handler名称不能重复 > * 路由(请求方法+请求path)名称不能重复 > * 请求体必须声明为普通(非指针)struct,响应体做了一些向前兼容处理,详请见下文说明 > **语法定义** ``` antlrv4 serviceSpec: atServer? serviceApi; atServer: '@server' lp='(' kvLit+ rp=')'; serviceApi: {match(p,"service")}serviceToken=ID serviceName lbrace='{' serviceRoute* rbrace='}'; serviceRoute: atDoc? (atServer|atHandler) route; atDoc: '@doc' lp='('? ((kvLit+)|STRING) rp=')'?; atHandler: '@handler' ID; route: {checkHttpMethod(p)}httpMethod=ID path request=body? returnToken=ID? response=replybody?; body: lp='(' (ID)? rp=')'; replybody: lp='(' dataType? rp=')'; // kv kvLit: key=ID {checkKeyValue(p)}value=LINE_VALUE; serviceName: (ID '-'?)+; path: (('/' (ID ('-' ID)*))|('/:' (ID ('-' ID)?)))+; ``` **语法说明** > serviceSpec:包含了一个可选语法块`atServer`和`serviceApi`语法块,其遵循序列模式(编写service必须要按照顺序,否则会解析出错) > > atServer: 可选语法块,定义key-value结构的server metadata,'@server'表示这一个server语法块的开始,其可以用于描述serviceApi或者route语法块,其用于描述不同语法块时有一些特殊关键key > 需要值得注意,见 **atServer关键key描述说明**。 > > serviceApi:包含了1到多个`serviceRoute`语法块 > > serviceRoute:按照序列模式包含了`atDoc`,handler和`route` > > atDoc:可选语法块,一个路由的key-value描述,其在解析后会传递到spec.Spec结构体,如果不关心传递到spec.Spec, > 推荐用单行注释替代。 > > handler:是对路由的handler层描述,可以通过atServer指定`handler` key来指定handler名称, > 也可以直接用atHandler语法块来定义handler名称 > > atHandler:'@handler' 固定token,后接一个遵循正则`[_a-zA-Z][a-zA-Z_-]*`)的值,用于声明一个handler名称 > > route:路由,有`httpMethod`、`path`、可选`request`、可选`response`组成,`httpMethod`是必须是小写。 > > body:api请求体语法定义,必须要由()包裹的可选的ID值 > > replyBody:api响应体语法定义,必须由()包裹的struct、~~array(向前兼容处理,后续可能会废弃,强烈推荐以struct包裹,不要直接用array作为响应体)~~ > > kvLit: 同info key-value > > serviceName: 可以有多个'-'join的ID值 > > path:api请求路径,必须以'/'或者'/:'开头,切不能以'/'结尾,中间可包含ID或者多个以'-'join的ID字符串 **atServer关键key描述说明** 修饰service时 |key|描述|示例| |:---|:---|:---| |jwt|声明当前service下所有路由需要jwt鉴权,且会自动生成包含jwt逻辑的代码|`jwt: Auth`| |group|声明当前service或者路由文件分组|`group: login`| |middleware|声明当前service需要开启中间件|`middleware: AuthMiddleware`| 修饰route时 |key|描述|示例| |:---|:---|:---| |handler|声明一个handler|-| **正确语法示例** ✅ eg1:不规范写法 ``` api @server( jwt: Auth group: foo middleware: AuthMiddleware ) service foo-api{ @doc( summary: foo ) @server( handler: foo ) // 非导出型body post /foo/:id (foo) returns (bar) @doc "bar" @handler bar post /bar returns ([]int)// 不推荐数组作为响应体 @handler fooBar post /foo/bar (Foo) returns // 可以省略'returns' } ``` eg2:规范写法(推荐) ``` api @server( jwt: Auth group: foo middleware: AuthMiddleware ) service foo-api{ @doc "foo" @handler: foo post /foo/:id (Foo) returns (Bar) } service foo-api{ @handler ping get /ping @doc "foo" @handler: bar post /bar/:id (Foo) } ``` **错误语法示例** ❌ ``` api // 不支持空的server语法块 @server( ) // 不支持空的service语法块 service foo-api{ } service foo-api{ @doc kkkk // 简版doc必须用英文双引号引起来 @handler foo post /foo @handler foo // 重复的handler post /bar @handler fooBar post /bar // 重复的路由 // @handler和@doc顺序错误 @handler someHandler @doc "some doc" post /some/path // handler缺失 post /some/path/:id @handler reqTest post /foo/req (*Foo) // 不支持除普通结构体外的其他数据类型作为请求体 @handler replyTest post /foo/reply returns (*Foo) // 不支持除普通结构体、数组(向前兼容,后续考虑废弃)外的其他数据类型作为响应体 } ``` ## 隐藏通道 隐藏通道目前主要为空白符号,换行符号以及注释,这里我们只说注释,因为空白符号和换行符号我们目前拿来也无用。 ### 单行注释 **语法定义** ``` antlrv4 '//' ~[\r\n]* ``` **语法说明** 由语法定义可知道,单行注释必须要以`//`开头,内容为不能包含换行符 **正确语法示例** ✅ ``` api // doc // comment ``` **错误语法示例** ❌ ``` api // break line comments ``` ### java风格文档注释 **语法定义** ``` antlrv4 '/*' .*? '*/' ``` **语法说明** 由语法定义可知道,单行注释必须要以`/*`开头,`*/`结尾的任意字符。 **正确语法示例** ✅ ``` api /** * java-style doc */ ``` **错误语法示例** ❌ ``` api /* * java-style doc */ */ ``` ## Doc&Comment 如果想获取某一个元素的doc或者comment开发人员需要怎么定义? **Doc** > 我们规定上一个语法块(非隐藏通道内容)的行数line+1到当前语法块第一个元素前的所有注释(当行,或者多行)均为doc, 且保留了`//`、`/*`、`*/`原始标记。 **Comment** > 我们规定当前语法块最后一个元素所在行开始的一个注释块(当行,或者多行)为comment 且保留了`//`、`/*`、`*/`原始标记。 语法块Doc和Comment的支持情况 |语法块|parent语法块|Doc|Comment| |:---|:---|:---|:---| |syntaxLit|api|✅|✅| |kvLit|infoSpec|✅|✅| |importLit|importSpec|✅|✅| |typeLit|api|✅|❌| |typeLit|typeBlock|✅|❌| |field|typeLit|✅|✅| |key-value|atServer|✅|✅| |atHandler|serviceRoute|✅|✅| |route|serviceRoute|✅|✅| 以下为对应语法块解析后细带doc和comment的写法 ``` api // syntaxLit doc syntax = "v1" // syntaxLit commnet info( // kvLit doc author: songmeizi // kvLit comment ) // typeLit doc type Foo {} type( // typeLit doc Bar{} FooBar{ // filed doc Name int // filed comment } ) @server( /** * kvLit doc * 开启jwt鉴权 */ jwt: Auth /**kvLit comment*/ ) service foo-api{ // atHandler doc @handler foo //atHandler comment /* * route doc * post请求 * path为 /foo * 请求体:Foo * 响应体:Foo */ post /foo (Foo) returns (Foo) // route comment } ```