Skip to content

Commit fd58d7b

Browse files
authored
Merge pull request #53 from link-duan/feature/openapi-security
feat: openapi security
2 parents 63f49ca + 61b3565 commit fd58d7b

File tree

20 files changed

+288
-72
lines changed

20 files changed

+288
-72
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,36 @@ func Create(c *gin.Context) {
373373
}
374374
```
375375

376+
### `@security`
377+
378+
用于设置接口鉴权 (Security Requirement) ,参考 https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-requirement-object
379+
380+
```go
381+
// @security oauth2 pets:write pets:read
382+
func XxxHandler() {
383+
// ...
384+
}
385+
```
386+
387+
对应的 securitySchemes 配置示例:
388+
```yaml
389+
openapi:
390+
info:
391+
title: This is an Example
392+
description: Example description for Example
393+
securitySchemes:
394+
oauth2:
395+
type: oauth2
396+
flows:
397+
implicit:
398+
authorizationUrl: "https://example.org/api/oauth/dialog"
399+
scopes:
400+
"pets:write": "modify pets in your account"
401+
"pets:read": "read your pets"
402+
```
403+
404+
通常需要配合 securitySchemes 使用,参考 https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-scheme-object
405+
376406
在上面示例中,`User.OldField` 字段会被标记为弃用,`Create` 函数对应的接口会被标记为弃用。
377407

378408
## 预览

analyzer.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func (a *Analyzer) processPkg(packagePath string) {
188188
}
189189

190190
func (a *Analyzer) processFile(ctx *Context, file *ast.File, pkg *packages.Package) {
191-
comment := ParseComment(file.Doc)
191+
comment := ctx.ParseComment(file.Doc)
192192
if comment.Ignore() {
193193
return
194194
}
@@ -210,7 +210,7 @@ func (a *Analyzer) processFile(ctx *Context, file *ast.File, pkg *packages.Packa
210210
}
211211

212212
func (a *Analyzer) funDecl(ctx *Context, node *ast.FuncDecl, file *ast.File, pkg *packages.Package) {
213-
comment := ParseComment(node.Doc)
213+
comment := ctx.ParseComment(node.Doc)
214214
if comment.Ignore() {
215215
return
216216
}
@@ -311,7 +311,7 @@ func (a *Analyzer) loadEnumDefinition(pkg *packages.Package, file *ast.File, nod
311311
}
312312

313313
func (a *Analyzer) blockStmt(ctx *Context, node *ast.BlockStmt, file *ast.File, pkg *packages.Package) {
314-
comment := ParseComment(a.context().WithPackage(pkg).WithFile(file).GetHeadingCommentOf(node.Lbrace))
314+
comment := ctx.ParseComment(a.context().WithPackage(pkg).WithFile(file).GetHeadingCommentOf(node.Lbrace))
315315
if comment.Ignore() {
316316
return
317317
}

annotation/annotation.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313
Summary
1414
ID
1515
Deprecated
16+
Security
1617
)
1718

1819
type Annotation interface {
@@ -87,3 +88,16 @@ type IdAnnotation struct {
8788
func (a *IdAnnotation) Type() Type {
8889
return ID
8990
}
91+
92+
type SecurityAnnotation struct {
93+
Name string
94+
Params []string
95+
}
96+
97+
func newSecurityAnnotation(name string, params []string) *SecurityAnnotation {
98+
return &SecurityAnnotation{Name: name, Params: params}
99+
}
100+
101+
func (a *SecurityAnnotation) Type() Type {
102+
return Security
103+
}

annotation/lexer.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ var patterns = []*pattern{
2525
newPattern(tokenIdentifier, "^[^\\s]+"),
2626
}
2727

28+
var tokenNameMap = map[TokenType]string{
29+
tokenTag: "tag",
30+
tokenString: "string",
31+
tokenNumber: "number",
32+
tokenBool: "bool",
33+
tokenWhiteSpace: "whitespace",
34+
tokenIdentifier: "identifier",
35+
}
36+
2837
type pattern struct {
2938
tokenType TokenType
3039
pattern *regexp.Regexp

annotation/parser.go

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,112 @@
11
package annotation
22

3-
import "strings"
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
type ParseError struct {
9+
Column int
10+
Message string
11+
}
12+
13+
func (e *ParseError) Error() string {
14+
return e.Message
15+
}
16+
17+
func NewParseError(column int, message string) *ParseError {
18+
return &ParseError{Column: column, Message: message}
19+
}
420

521
type Parser struct {
622
text string
723

824
tokens []*Token
925
position int
26+
column int
1027
}
1128

1229
func NewParser(text string) *Parser {
13-
text = strings.TrimPrefix(text, "//")
1430
return &Parser{text: text}
1531
}
1632

17-
func (p *Parser) Parse() Annotation {
18-
tokens, err := NewLexer(p.text).Lex()
33+
func (p *Parser) Parse() (Annotation, error) {
34+
var column = 0
35+
var text = p.text
36+
if strings.HasPrefix(text, "//") {
37+
column = 2
38+
text = strings.TrimPrefix(text, "//")
39+
}
40+
41+
tokens, err := NewLexer(text).Lex()
1942
if err != nil {
20-
return nil
43+
return nil, nil
2144
}
2245
if len(tokens) == 0 {
23-
return nil
46+
return nil, nil
2447
}
2548

2649
p.tokens = tokens
2750
p.position = 0
51+
p.column = column
2852

2953
return p.parse()
3054
}
3155

32-
func (p *Parser) parse() Annotation {
33-
tag := p.consume(tokenTag)
34-
if tag == nil {
35-
return nil
56+
func (p *Parser) parse() (Annotation, error) {
57+
tag, err := p.consume(tokenTag)
58+
if err != nil {
59+
return nil, nil
3660
}
3761

3862
switch strings.ToLower(tag.Image) {
3963
case "@required":
40-
return newSimpleAnnotation(Required)
64+
return newSimpleAnnotation(Required), nil
4165
case "@consume":
4266
return p.consumeAnnotation()
4367
case "@produce":
4468
return p.produceAnnotation()
4569
case "@ignore":
46-
return newSimpleAnnotation(Ignore)
70+
return newSimpleAnnotation(Ignore), nil
4771
case "@tag", "@tags":
48-
return p.tags()
72+
return p.tags(), nil
4973
case "@description":
50-
return p.description()
74+
return p.description(), nil
5175
case "@summary":
52-
return p.summary()
76+
return p.summary(), nil
5377
case "@id":
54-
return p.id()
78+
return p.id(), nil
5579
case "@deprecated":
56-
return newSimpleAnnotation(Deprecated)
80+
return newSimpleAnnotation(Deprecated), nil
81+
case "@security":
82+
return p.security()
5783
default: // unresolved plugin
58-
return p.unresolved(tag)
84+
return p.unresolved(tag), nil
5985
}
6086
}
6187

62-
func (p *Parser) consume(typ TokenType) *Token {
88+
func (p *Parser) consume(typ TokenType) (*Token, error) {
6389
for {
6490
t := p.lookahead()
6591
if t != nil && t.Type == tokenWhiteSpace {
6692
p.position += 1
93+
p.column += len(t.Image)
6794
} else {
6895
break
6996
}
7097
}
7198

7299
t := p.lookahead()
73-
if t == nil || t.Type != typ {
74-
return nil
100+
if t == nil {
101+
return nil, NewParseError(p.column, fmt.Sprintf("expect %s, but got EOF", tokenNameMap[typ]))
102+
}
103+
if t.Type != typ {
104+
return nil, NewParseError(p.column, fmt.Sprintf("expect %s, but got '%s'", tokenNameMap[typ], t.Image))
75105
}
76106

77107
p.position += 1
78-
return t
108+
p.column += len(t.Image)
109+
return t, nil
79110
}
80111

81112
func (p *Parser) consumeAny() *Token {
@@ -85,6 +116,7 @@ func (p *Parser) consumeAny() *Token {
85116
}
86117

87118
p.position += 1
119+
p.column += len(t.Image)
88120
return t
89121
}
90122

@@ -99,24 +131,27 @@ func (p *Parser) hasMore() bool {
99131
return len(p.tokens) > p.position
100132
}
101133

102-
func (p *Parser) consumeAnnotation() *ConsumeAnnotation {
103-
ident := p.consume(tokenIdentifier)
104-
if ident == nil {
105-
return nil
134+
func (p *Parser) consumeAnnotation() (*ConsumeAnnotation, error) {
135+
ident, err := p.consume(tokenIdentifier)
136+
if err != nil {
137+
return nil, err
106138
}
107139
return &ConsumeAnnotation{
108140
ContentType: ident.Image,
109-
}
141+
}, nil
110142
}
111143

112-
func (p *Parser) produceAnnotation() *ProduceAnnotation {
113-
ident := p.consume(tokenIdentifier)
144+
func (p *Parser) produceAnnotation() (*ProduceAnnotation, error) {
145+
ident, err := p.consume(tokenIdentifier)
146+
if err != nil {
147+
return nil, err
148+
}
114149
if ident == nil {
115-
return nil
150+
return nil, nil
116151
}
117152
return &ProduceAnnotation{
118153
ContentType: ident.Image,
119-
}
154+
}, nil
120155
}
121156

122157
func (p *Parser) unresolved(tag *Token) Annotation {
@@ -130,8 +165,10 @@ func (p *Parser) tags() Annotation {
130165
res := &TagAnnotation{}
131166
var tag []string
132167
for p.hasMore() {
133-
ident := p.consume(tokenIdentifier)
134-
tag = append(tag, ident.Image)
168+
ident, _ := p.consume(tokenIdentifier)
169+
if ident != nil {
170+
tag = append(tag, ident.Image)
171+
}
135172
}
136173
res.Tag = strings.Join(tag, " ")
137174
return res
@@ -163,3 +200,23 @@ func (p *Parser) id() Annotation {
163200
}
164201
return res
165202
}
203+
204+
// @security name scope1 [...]
205+
func (p *Parser) security() (*SecurityAnnotation, error) {
206+
name, err := p.consume(tokenIdentifier)
207+
if err != nil {
208+
return nil, NewParseError(p.column, "expect name after @security")
209+
}
210+
var security = SecurityAnnotation{
211+
Name: name.Image,
212+
Params: make([]string, 0),
213+
}
214+
for p.hasMore() {
215+
token := p.consumeAny()
216+
if token.Type == tokenIdentifier {
217+
security.Params = append(security.Params, token.Image)
218+
}
219+
}
220+
221+
return &security, nil
222+
}

annotation/parser_test.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,21 @@ import (
77

88
func TestParser_Parse(t *testing.T) {
99
tests := []struct {
10-
name string
11-
code string
12-
want Annotation
10+
name string
11+
code string
12+
want Annotation
13+
wantErr bool
1314
}{
15+
{
16+
name: "consume",
17+
code: "@consume application/json",
18+
want: &ConsumeAnnotation{ContentType: "application/json"},
19+
},
20+
{
21+
name: "produce",
22+
code: "@produce application/json",
23+
want: &ProduceAnnotation{ContentType: "application/json"},
24+
},
1425
{
1526
name: "required",
1627
code: " @required",
@@ -21,11 +32,34 @@ func TestParser_Parse(t *testing.T) {
2132
code: " @REQUIRED ",
2233
want: newSimpleAnnotation(Required),
2334
},
35+
{
36+
name: "security",
37+
code: " @security oauth2 pet:read pet:write",
38+
want: newSecurityAnnotation("oauth2", []string{"pet:read", "pet:write"}),
39+
},
40+
{
41+
name: "security error",
42+
code: "@security",
43+
wantErr: true,
44+
want: (*SecurityAnnotation)(nil),
45+
},
2446
}
2547
for _, tt := range tests {
2648
t.Run(tt.name, func(t *testing.T) {
2749
p := NewParser(tt.code)
28-
if got := p.Parse(); !reflect.DeepEqual(got, tt.want) {
50+
got, err := p.Parse()
51+
if err != nil {
52+
if !tt.wantErr {
53+
t.Errorf("unexpected error: %v", err)
54+
return
55+
} else {
56+
t.Logf("error: %v", err)
57+
}
58+
} else if tt.wantErr {
59+
t.Errorf("want error. but got nil")
60+
return
61+
}
62+
if !reflect.DeepEqual(got, tt.want) {
2963
t.Errorf("Parse() = %v, want %v", got, tt.want)
3064
}
3165
})

0 commit comments

Comments
 (0)