Skip to content

Commit a245541

Browse files
author
Josh Newman
committed
Add tests and patch empty schemas
1 parent 7ceb820 commit a245541

20 files changed

+624
-38
lines changed

.editorconfig

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,14 @@ root = true
55

66
[*]
77
indent_style = space
8-
indent_size = 4
98
end_of_line = lf
109
charset = utf-8
1110
trim_trailing_whitespace = true
1211
insert_final_newline = true
1312

14-
[*.go]
13+
[{*.go,Makefile}]
1514
indent_style = tab
15+
tab_width = 4
1616

1717
[*.{yaml,yml}]
18-
indent_size = 2
1918
quote_type = single
20-
21-
[*.proto]
22-
indent_size = 2
23-
24-
[*.md]
25-
indent_size = 2

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ go.work
2525
protoc-gen-openapi
2626

2727
.idea/
28+
29+
test_api/openapi.yaml

Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM golang:1.19-alpine3.16 as build
2+
3+
ENV CGO_ENABLED=0
4+
5+
RUN apk add gcc protoc protobuf-dev
6+
7+
WORKDIR /workspace
8+
9+
COPY api api
10+
COPY internal internal
11+
COPY Makefile go.* main.go ./
12+
RUN go mod download
13+
RUN go install .
14+
15+
COPY test test
16+
COPY main_test.go ./
17+
18+
CMD ["go", "test", "./..."]

Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.ONESHELL:
2+
.DEFAULT_GOAL := help
3+
4+
.PHONY: local-buf
5+
local-buf:
6+
docker build -f buf.Dockerfile -t local-buf:latest .
7+
8+
.PHONY: generate
9+
generate: local-buf ## generate protobuf outputs via Buf
10+
docker run --rm -v `pwd`:/workspace -w /workspace local-buf:latest generate
11+
12+
.PHONY: lint
13+
lint: local-buf ## lint proto and code
14+
docker run --rm -v `pwd`:/workspace -w /workspace local-buf:latest lint
15+
golangci-lint run ./...
16+
17+
.PHONY: test
18+
test:
19+
docker build -t technicallyjosh/protoc-gen-openapi .
20+
docker run --rm technicallyjosh/protoc-gen-openapi

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,21 @@ go install github.com/technicallyjosh/protoc-gen-openapi@latest
4343

4444
<sup>1</sup> _Can be overridden on a file, service, or method._
4545

46-
## Using Buf
46+
## Build Examples
4747

48-
Yup, I've only actually used this in `buf` so far. I'm sure it works with the
49-
standard protoc calls, but why would you do that to yourself 😂?
48+
Below are some basic examples on how to use this generator.
49+
50+
### Protoc
51+
52+
```bash
53+
protoc \
54+
--openapi_out=. \
55+
--openapi_opt=version=1.0.0 \
56+
--openapi_opt=title="My Awesome API" \
57+
api/some_service.proto
58+
```
59+
60+
### Buf
5061

5162
**buf.yaml**
5263

go.mod

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@ module github.com/technicallyjosh/protoc-gen-openapi
33
go 1.19
44

55
require (
6-
github.com/getkin/kin-openapi v0.111.0
6+
github.com/getkin/kin-openapi v0.114.0
7+
github.com/stretchr/objx v0.5.0
8+
github.com/stretchr/testify v1.8.1
79
google.golang.org/protobuf v1.28.1
810
gopkg.in/yaml.v3 v3.0.1
911
)
1012

1113
require (
14+
github.com/davecgh/go-spew v1.1.1 // indirect
1215
github.com/go-openapi/jsonpointer v0.19.5 // indirect
1316
github.com/go-openapi/swag v0.19.5 // indirect
1417
github.com/invopop/yaml v0.1.0 // indirect
15-
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect
18+
github.com/josharian/intern v1.0.0 // indirect
19+
github.com/mailru/easyjson v0.7.7 // indirect
1620
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
21+
github.com/perimeterx/marshmallow v1.1.4 // indirect
22+
github.com/pmezard/go-difflib v1.0.0 // indirect
1723
gopkg.in/yaml.v2 v2.4.0 // indirect
1824
)

go.sum

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,50 @@
11
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
33
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4-
github.com/getkin/kin-openapi v0.111.0 h1:zspOcFKBCQOY8d9Yockcbit8iVR2hco9qLaoQoj7kmw=
5-
github.com/getkin/kin-openapi v0.111.0/go.mod h1:QtwUNt0PAAgIIBEvFWYfB7dfngxtAaqCX1zYHMZDeK8=
4+
github.com/getkin/kin-openapi v0.114.0 h1:ar7QiJpDdlR+zSyPjrLf8mNnpoFP/lI90XcywMCFNe8=
5+
github.com/getkin/kin-openapi v0.114.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
66
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
77
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
88
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
99
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
10+
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
11+
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
1012
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
1113
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
1214
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
1315
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
1416
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
1517
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
18+
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
19+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
1620
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
1721
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
1822
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
1923
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
2024
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
2125
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
22-
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
2326
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
27+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
28+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
2429
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
2530
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
31+
github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw=
32+
github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
2633
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2734
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2835
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
2936
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
37+
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
3038
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
3139
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
3240
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
3341
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
3442
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
3543
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
44+
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
45+
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
46+
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
47+
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
3648
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
3749
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
3850
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=

internal/generator/generator.go

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ package generator
33
import (
44
"bytes"
55
"encoding/json"
6+
"fmt"
67
"strings"
78

89
"github.com/getkin/kin-openapi/openapi3"
10+
"github.com/stretchr/objx"
11+
oapiv1 "github.com/technicallyjosh/protoc-gen-openapi/api/oapi/v1"
912
"github.com/technicallyjosh/protoc-gen-openapi/internal/generator/util"
1013
"google.golang.org/protobuf/compiler/protogen"
14+
"google.golang.org/protobuf/proto"
1115
"gopkg.in/yaml.v3"
1216
)
1317

@@ -78,20 +82,88 @@ func (g *Generator) Run() error {
7882
}
7983
}
8084

85+
fileBytes := fileBuffer.Bytes()
86+
8187
outFile := g.plugin.NewGeneratedFile(filename, "")
82-
_, err = outFile.Write(fileBuffer.Bytes())
8388

89+
patchedBytes, err := g.patchEmptySchemas(fileBytes)
90+
if err != nil {
91+
return err
92+
}
93+
94+
_, err = outFile.Write(patchedBytes)
8495
return err
8596
}
8697

98+
// patchEmptySchemas finds any schemas that are empty and updates them to have
99+
// an empty `Properties` node.
100+
func (g *Generator) patchEmptySchemas(fileBytes []byte) ([]byte, error) {
101+
type M = map[string]any
102+
data := make(M)
103+
104+
useJSON := *g.config.JSONOutput
105+
106+
var err error
107+
if useJSON {
108+
err = json.Unmarshal(fileBytes, &data)
109+
} else {
110+
err = yaml.Unmarshal(fileBytes, &data)
111+
}
112+
if err != nil {
113+
return nil, err
114+
}
115+
116+
jsonBytes, err := json.Marshal(data)
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
m, err := objx.FromJSON(string(jsonBytes))
122+
if err != nil {
123+
return nil, err
124+
}
125+
126+
for pathKey := range m.Get("paths").ObjxMap() {
127+
pathPath := "paths." + pathKey
128+
129+
for methodKey := range m.Get(pathPath).ObjxMap() {
130+
methodPath := pathPath + "." + methodKey
131+
132+
schemaKey := fmt.Sprintf("%s.requestBody.content.application/json.schema", methodPath)
133+
schema := m.Get(schemaKey)
134+
135+
if schema.IsObjxMap() && len(schema.ObjxMap()) == 0 {
136+
schema.ObjxMap().Set("properties", M{})
137+
}
138+
139+
for resKey := range m.Get(methodPath + ".responses").ObjxMap() {
140+
schemaKey := fmt.Sprintf("%s.responses.%s.content.application/json.schema", methodPath, resKey)
141+
schema := m.Get(schemaKey)
142+
if schema.IsObjxMap() && len(schema.ObjxMap()) == 0 {
143+
schema.ObjxMap().Set("properties", M{})
144+
}
145+
}
146+
}
147+
}
148+
149+
if useJSON {
150+
return json.Marshal(m)
151+
}
152+
153+
buffer := bytes.Buffer{}
154+
encoder := yaml.NewEncoder(&buffer)
155+
encoder.SetIndent(2)
156+
157+
err = encoder.Encode(m)
158+
return buffer.Bytes(), err
159+
}
160+
87161
// buildDocument builds out the base of the OAPI document with some defaults.
88162
func (g *Generator) buildDocument() (*openapi3.T, error) {
89163
doc := &openapi3.T{
90-
ExtensionProps: openapi3.ExtensionProps{
91-
Extensions: make(map[string]any),
92-
},
93-
OpenAPI: "3.0.3",
94-
Components: openapi3.Components{
164+
Extensions: make(map[string]any),
165+
OpenAPI: "3.1.0",
166+
Components: &openapi3.Components{
95167
SecuritySchemes: make(openapi3.SecuritySchemes),
96168
Schemas: make(openapi3.Schemas),
97169
RequestBodies: make(openapi3.RequestBodies),
@@ -130,6 +202,12 @@ func (g *Generator) buildDocument() (*openapi3.T, error) {
130202
}
131203

132204
for _, file := range files {
205+
// Add servers even if there isn't a service. (File-based)
206+
err = addFileServersToDoc(doc, file)
207+
if err != nil {
208+
return nil, err
209+
}
210+
133211
err = g.addPathsToDoc(doc, file.Services)
134212
if err != nil {
135213
return nil, err
@@ -142,6 +220,23 @@ func (g *Generator) buildDocument() (*openapi3.T, error) {
142220
return doc, nil
143221
}
144222

223+
func addFileServersToDoc(doc *openapi3.T, file *protogen.File) error {
224+
extFile := proto.GetExtension(file.Desc.Options(), oapiv1.E_File)
225+
if extFile != nil && extFile != oapiv1.E_File.InterfaceOf(oapiv1.E_File.Zero()) {
226+
fileOptions := extFile.(*oapiv1.FileOptions)
227+
228+
if fileOptions.Host != "" {
229+
server, err := NewServer(fileOptions.Host)
230+
if err != nil {
231+
return err
232+
}
233+
doc.Servers = append(doc.Servers, server)
234+
}
235+
}
236+
237+
return nil
238+
}
239+
145240
func filterFiles(allFiles []*protogen.File, ignored []string) []*protogen.File {
146241
files := make([]*protogen.File, 0)
147242

0 commit comments

Comments
 (0)