# 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 comment

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
}
```
