Skip to content

Commit

Permalink
Merge pull request #53 from link-duan/feature/openapi-security
Browse files Browse the repository at this point in the history
feat: openapi security
  • Loading branch information
link-duan authored Feb 3, 2023
2 parents 63f49ca + 61b3565 commit fd58d7b
Show file tree
Hide file tree
Showing 20 changed files with 288 additions and 72 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,36 @@ func Create(c *gin.Context) {
}
```

### `@security`

用于设置接口鉴权 (Security Requirement) ,参考 https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-requirement-object

```go
// @security oauth2 pets:write pets:read
func XxxHandler() {
// ...
}
```

对应的 securitySchemes 配置示例:
```yaml
openapi:
info:
title: This is an Example
description: Example description for Example
securitySchemes:
oauth2:
type: oauth2
flows:
implicit:
authorizationUrl: "https://example.org/api/oauth/dialog"
scopes:
"pets:write": "modify pets in your account"
"pets:read": "read your pets"
```
通常需要配合 securitySchemes 使用,参考 https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-scheme-object
在上面示例中,`User.OldField` 字段会被标记为弃用,`Create` 函数对应的接口会被标记为弃用。

## 预览
Expand Down
6 changes: 3 additions & 3 deletions analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func (a *Analyzer) processPkg(packagePath string) {
}

func (a *Analyzer) processFile(ctx *Context, file *ast.File, pkg *packages.Package) {
comment := ParseComment(file.Doc)
comment := ctx.ParseComment(file.Doc)
if comment.Ignore() {
return
}
Expand All @@ -210,7 +210,7 @@ func (a *Analyzer) processFile(ctx *Context, file *ast.File, pkg *packages.Packa
}

func (a *Analyzer) funDecl(ctx *Context, node *ast.FuncDecl, file *ast.File, pkg *packages.Package) {
comment := ParseComment(node.Doc)
comment := ctx.ParseComment(node.Doc)
if comment.Ignore() {
return
}
Expand Down Expand Up @@ -311,7 +311,7 @@ func (a *Analyzer) loadEnumDefinition(pkg *packages.Package, file *ast.File, nod
}

func (a *Analyzer) blockStmt(ctx *Context, node *ast.BlockStmt, file *ast.File, pkg *packages.Package) {
comment := ParseComment(a.context().WithPackage(pkg).WithFile(file).GetHeadingCommentOf(node.Lbrace))
comment := ctx.ParseComment(a.context().WithPackage(pkg).WithFile(file).GetHeadingCommentOf(node.Lbrace))
if comment.Ignore() {
return
}
Expand Down
14 changes: 14 additions & 0 deletions annotation/annotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const (
Summary
ID
Deprecated
Security
)

type Annotation interface {
Expand Down Expand Up @@ -87,3 +88,16 @@ type IdAnnotation struct {
func (a *IdAnnotation) Type() Type {
return ID
}

type SecurityAnnotation struct {
Name string
Params []string
}

func newSecurityAnnotation(name string, params []string) *SecurityAnnotation {
return &SecurityAnnotation{Name: name, Params: params}
}

func (a *SecurityAnnotation) Type() Type {
return Security
}
9 changes: 9 additions & 0 deletions annotation/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ var patterns = []*pattern{
newPattern(tokenIdentifier, "^[^\\s]+"),
}

var tokenNameMap = map[TokenType]string{
tokenTag: "tag",
tokenString: "string",
tokenNumber: "number",
tokenBool: "bool",
tokenWhiteSpace: "whitespace",
tokenIdentifier: "identifier",
}

type pattern struct {
tokenType TokenType
pattern *regexp.Regexp
Expand Down
123 changes: 90 additions & 33 deletions annotation/parser.go
Original file line number Diff line number Diff line change
@@ -1,81 +1,112 @@
package annotation

import "strings"
import (
"fmt"
"strings"
)

type ParseError struct {
Column int
Message string
}

func (e *ParseError) Error() string {
return e.Message
}

func NewParseError(column int, message string) *ParseError {
return &ParseError{Column: column, Message: message}
}

type Parser struct {
text string

tokens []*Token
position int
column int
}

func NewParser(text string) *Parser {
text = strings.TrimPrefix(text, "//")
return &Parser{text: text}
}

func (p *Parser) Parse() Annotation {
tokens, err := NewLexer(p.text).Lex()
func (p *Parser) Parse() (Annotation, error) {
var column = 0
var text = p.text
if strings.HasPrefix(text, "//") {
column = 2
text = strings.TrimPrefix(text, "//")
}

tokens, err := NewLexer(text).Lex()
if err != nil {
return nil
return nil, nil
}
if len(tokens) == 0 {
return nil
return nil, nil
}

p.tokens = tokens
p.position = 0
p.column = column

return p.parse()
}

func (p *Parser) parse() Annotation {
tag := p.consume(tokenTag)
if tag == nil {
return nil
func (p *Parser) parse() (Annotation, error) {
tag, err := p.consume(tokenTag)
if err != nil {
return nil, nil
}

switch strings.ToLower(tag.Image) {
case "@required":
return newSimpleAnnotation(Required)
return newSimpleAnnotation(Required), nil
case "@consume":
return p.consumeAnnotation()
case "@produce":
return p.produceAnnotation()
case "@ignore":
return newSimpleAnnotation(Ignore)
return newSimpleAnnotation(Ignore), nil
case "@tag", "@tags":
return p.tags()
return p.tags(), nil
case "@description":
return p.description()
return p.description(), nil
case "@summary":
return p.summary()
return p.summary(), nil
case "@id":
return p.id()
return p.id(), nil
case "@deprecated":
return newSimpleAnnotation(Deprecated)
return newSimpleAnnotation(Deprecated), nil
case "@security":
return p.security()
default: // unresolved plugin
return p.unresolved(tag)
return p.unresolved(tag), nil
}
}

func (p *Parser) consume(typ TokenType) *Token {
func (p *Parser) consume(typ TokenType) (*Token, error) {
for {
t := p.lookahead()
if t != nil && t.Type == tokenWhiteSpace {
p.position += 1
p.column += len(t.Image)
} else {
break
}
}

t := p.lookahead()
if t == nil || t.Type != typ {
return nil
if t == nil {
return nil, NewParseError(p.column, fmt.Sprintf("expect %s, but got EOF", tokenNameMap[typ]))
}
if t.Type != typ {
return nil, NewParseError(p.column, fmt.Sprintf("expect %s, but got '%s'", tokenNameMap[typ], t.Image))
}

p.position += 1
return t
p.column += len(t.Image)
return t, nil
}

func (p *Parser) consumeAny() *Token {
Expand All @@ -85,6 +116,7 @@ func (p *Parser) consumeAny() *Token {
}

p.position += 1
p.column += len(t.Image)
return t
}

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

func (p *Parser) consumeAnnotation() *ConsumeAnnotation {
ident := p.consume(tokenIdentifier)
if ident == nil {
return nil
func (p *Parser) consumeAnnotation() (*ConsumeAnnotation, error) {
ident, err := p.consume(tokenIdentifier)
if err != nil {
return nil, err
}
return &ConsumeAnnotation{
ContentType: ident.Image,
}
}, nil
}

func (p *Parser) produceAnnotation() *ProduceAnnotation {
ident := p.consume(tokenIdentifier)
func (p *Parser) produceAnnotation() (*ProduceAnnotation, error) {
ident, err := p.consume(tokenIdentifier)
if err != nil {
return nil, err
}
if ident == nil {
return nil
return nil, nil
}
return &ProduceAnnotation{
ContentType: ident.Image,
}
}, nil
}

func (p *Parser) unresolved(tag *Token) Annotation {
Expand All @@ -130,8 +165,10 @@ func (p *Parser) tags() Annotation {
res := &TagAnnotation{}
var tag []string
for p.hasMore() {
ident := p.consume(tokenIdentifier)
tag = append(tag, ident.Image)
ident, _ := p.consume(tokenIdentifier)
if ident != nil {
tag = append(tag, ident.Image)
}
}
res.Tag = strings.Join(tag, " ")
return res
Expand Down Expand Up @@ -163,3 +200,23 @@ func (p *Parser) id() Annotation {
}
return res
}

// @security name scope1 [...]
func (p *Parser) security() (*SecurityAnnotation, error) {
name, err := p.consume(tokenIdentifier)
if err != nil {
return nil, NewParseError(p.column, "expect name after @security")
}
var security = SecurityAnnotation{
Name: name.Image,
Params: make([]string, 0),
}
for p.hasMore() {
token := p.consumeAny()
if token.Type == tokenIdentifier {
security.Params = append(security.Params, token.Image)
}
}

return &security, nil
}
42 changes: 38 additions & 4 deletions annotation/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,21 @@ import (

func TestParser_Parse(t *testing.T) {
tests := []struct {
name string
code string
want Annotation
name string
code string
want Annotation
wantErr bool
}{
{
name: "consume",
code: "@consume application/json",
want: &ConsumeAnnotation{ContentType: "application/json"},
},
{
name: "produce",
code: "@produce application/json",
want: &ProduceAnnotation{ContentType: "application/json"},
},
{
name: "required",
code: " @required",
Expand All @@ -21,11 +32,34 @@ func TestParser_Parse(t *testing.T) {
code: " @REQUIRED ",
want: newSimpleAnnotation(Required),
},
{
name: "security",
code: " @security oauth2 pet:read pet:write",
want: newSecurityAnnotation("oauth2", []string{"pet:read", "pet:write"}),
},
{
name: "security error",
code: "@security",
wantErr: true,
want: (*SecurityAnnotation)(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewParser(tt.code)
if got := p.Parse(); !reflect.DeepEqual(got, tt.want) {
got, err := p.Parse()
if err != nil {
if !tt.wantErr {
t.Errorf("unexpected error: %v", err)
return
} else {
t.Logf("error: %v", err)
}
} else if tt.wantErr {
t.Errorf("want error. but got nil")
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Parse() = %v, want %v", got, tt.want)
}
})
Expand Down
Loading

0 comments on commit fd58d7b

Please sign in to comment.