Skip to content

Commit 9e3d217

Browse files
raphaelnitinmohan87NeenuAVargheseRaphael Simon
authored
Openapi v3 (goadesign#2607)
* OpenAPI v3 initial commit * Build Server for OpenAPI v3 * get host variable name and support retrieving multiple variables * Move OpenAPI V3 data structures to Goa * Use MatchString for matching string and we no longer return error for openapiv3 init * Started Components and body types work * Add security requirements and custom YAML marshaller/unmarshaller for Ref * Add paths and operations * Move URI scheme and parameter substitution to expressions * Remove regex to find URI scheme * Initial implementation of body types * Add back buildExternalDocs (whoops) * Finalize hashing algo * move v2 openapi builder to v2 folder * Update JSON Schema version * Rework a bit openapi package structure * Use openapi Schema in v3 * Continue OpenAPI v3 paths work * Update dependencies * clean up * More progress on OpenAPI v3 support * Add support for file servers * Move openapi v2 tests to its own package f * Start adding openapiv3 tests * more openapiv3 swagger gen tests * Save work * Add more tests and fix issues. * More OpenAPI v3 testing and fixes * Wrap up initial OpenAPI v3 support * Fix broken tests Co-authored-by: Nitin Mohan <[email protected]> Co-authored-by: NeenuAVarghese <[email protected]> Co-authored-by: Raphael Simon <[email protected]>
1 parent b4320c4 commit 9e3d217

File tree

114 files changed

+4225
-815
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

114 files changed

+4225
-815
lines changed

.golint_exclude

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
dsl[/\]http.go
22
expr[/\]http_response.go
33
codegen[/\]service[/\]testing[/\].*
4+
http[/\]codegen[/\]codegen[/\].openapi[/\]v3[/\]ref.go
45
http[/\]codegen[/\]testing[/\].*
56
http[/\]middleware[/\]trace.go
67
http[/\]middleware[/\]xray[/\]middleware.go
78
http[/\]middleware[/\]xray[/\]wrap_doer.go
89
http[/\]middleware[/\]xray[/\]wrap_doer_test.go
910
grpc[/\]pb[/\].*
11+

Makefile

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ DEPEND=\
2828
github.com/golang/protobuf/protoc-gen-go \
2929
github.com/golang/protobuf/proto \
3030
honnef.co/go/tools/cmd/staticcheck \
31-
github.com/hashicorp/go-getter/cmd/go-getter
31+
github.com/hashicorp/go-getter/cmd/go-getter \
32+
github.com/getkin/kin-openapi
3233

3334
all: lint test
3435

codegen/service/example_svc.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,8 @@ func basicEndpointSection(m *expr.MethodExpr, svcData *Data) *codegen.SectionTem
115115
ed.ResultIsStruct = expr.IsObject(m.Result.Type)
116116
if md.ViewedResult != nil {
117117
view := "default"
118-
if m.Result.Meta != nil {
119-
if v, ok := m.Result.Meta["view"]; ok {
120-
view = v[0]
121-
}
118+
if v, ok := m.Result.Meta["view"]; ok {
119+
view = v[0]
122120
}
123121
ed.ResultView = view
124122
}

dsl/error.go

+3-12
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,7 @@ func Temporary() {
7676
eval.IncompatibleDSL()
7777
return
7878
}
79-
if attr.Meta == nil {
80-
attr.Meta = make(expr.MetaExpr)
81-
}
82-
attr.Meta["goa:error:temporary"] = nil
79+
attr.AddMeta("goa:error:temporary")
8380
}
8481

8582
// Timeout qualifies an error type as describing errors due to timeouts.
@@ -101,10 +98,7 @@ func Timeout() {
10198
eval.IncompatibleDSL()
10299
return
103100
}
104-
if attr.Meta == nil {
105-
attr.Meta = make(expr.MetaExpr)
106-
}
107-
attr.Meta["goa:error:timeout"] = nil
101+
attr.AddMeta("goa:error:timeout")
108102
}
109103

110104
// Fault qualifies an error type as describing errors due to a server-side
@@ -127,8 +121,5 @@ func Fault() {
127121
eval.IncompatibleDSL()
128122
return
129123
}
130-
if attr.Meta == nil {
131-
attr.Meta = make(expr.MetaExpr)
132-
}
133-
attr.Meta["goa:error:fault"] = nil
124+
attr.AddMeta("goa:error:fault")
134125
}

dsl/http.go

+3-13
Original file line numberDiff line numberDiff line change
@@ -934,11 +934,7 @@ func Body(args ...interface{}) {
934934
return
935935
}
936936
attr = expr.DupAtt(attr)
937-
if attr.Meta == nil {
938-
attr.Meta = expr.MetaExpr{"origin:attribute": []string{a}}
939-
} else {
940-
attr.Meta["origin:attribute"] = []string{a}
941-
}
937+
attr.AddMeta("origin:attribute", a)
942938
if rt, ok := attr.Type.(*expr.ResultTypeExpr); ok {
943939
// If the attribute type is a result type add the type to the
944940
// GeneratedTypes so that the type's DSLFunc is executed.
@@ -976,10 +972,7 @@ func Body(args ...interface{}) {
976972
if fn != nil {
977973
eval.Execute(fn, attr)
978974
}
979-
if attr.Meta == nil {
980-
attr.Meta = expr.MetaExpr{}
981-
}
982-
attr.Meta["http:body"] = []string{}
975+
attr.AddMeta("http:body")
983976
setter(attr)
984977
}
985978

@@ -1177,8 +1170,5 @@ func params(exp eval.Expression) *expr.MappedAttributeExpr {
11771170
// a HTTP cookie attribute for use by the HTTP code generator.
11781171
func cookieAttribute(name, value string) {
11791172
c := eval.Current().(*expr.HTTPResponseExpr).Cookies
1180-
if c.Meta == nil {
1181-
c.Meta = expr.MetaExpr{}
1182-
}
1183-
c.Meta["cookie:"+name] = []string{value}
1173+
c.AddMeta("cookie:"+name, value)
11841174
}

dsl/result_type.go

+3-14
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,7 @@ func TypeName(name string) {
104104
case expr.UserType:
105105
e.Rename(name)
106106
case *expr.AttributeExpr:
107-
if e.Meta == nil {
108-
e.Meta = make(expr.MetaExpr)
109-
}
110-
e.Meta["struct:type:name"] = []string{name}
107+
e.AddMeta("struct:type:name", name)
111108
default:
112109
eval.IncompatibleDSL()
113110
}
@@ -209,10 +206,7 @@ func View(name string, adsl ...func()) {
209206
}
210207

211208
case *expr.AttributeExpr:
212-
if e.Meta == nil {
213-
e.Meta = make(map[string][]string)
214-
}
215-
e.Meta["view"] = []string{name}
209+
e.AddMeta("view", name)
216210

217211
default:
218212
eval.IncompatibleDSL()
@@ -485,12 +479,7 @@ func buildView(name string, mt *expr.ResultTypeExpr, at *expr.AttributeExpr) (*e
485479
cat := nat.Attribute
486480
if existing := mt.Find(n); existing != nil {
487481
dup := expr.DupAtt(existing)
488-
if dup.Meta == nil {
489-
dup.Meta = make(map[string][]string)
490-
}
491-
if len(cat.Meta["view"]) > 0 {
492-
dup.Meta["view"] = cat.Meta["view"]
493-
}
482+
dup.AddMeta("view", cat.Meta["view"]...)
494483
o.Set(n, dup)
495484
} else if n != "links" {
496485
return nil, fmt.Errorf("unknown attribute %#v", n)

expr/attribute.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,6 @@ func TaggedAttribute(a *AttributeExpr, tag string) string {
171171
return ""
172172
}
173173

174-
// Prepare initializes the Meta expression.
175-
func (a *AttributeExpr) Prepare() {
176-
if a.Meta == nil {
177-
a.Meta = MetaExpr{}
178-
}
179-
}
180-
181174
// Validate tests whether the attribute required fields exist. Since attributes
182175
// are unaware of their context, additional context information can be provided
183176
// to be used in error messages. The parent definition context is automatically
@@ -504,6 +497,14 @@ func (a *AttributeExpr) Delete(name string) {
504497
}
505498
}
506499

500+
// AddMeta adds values to the meta field of the attribute.
501+
func (a *AttributeExpr) AddMeta(name string, vals ...string) {
502+
if a.Meta == nil {
503+
a.Meta = make(MetaExpr)
504+
}
505+
a.Meta[name] = append(a.Meta[name], vals...)
506+
}
507+
507508
// Debug dumps the attribute to STDOUT in a goa developer friendly way.
508509
func (a *AttributeExpr) Debug(prefix string) { a.debug(prefix, make(map[*AttributeExpr]int), 0) }
509510
func (a *AttributeExpr) debug(prefix string, seen map[*AttributeExpr]int, indent int) {

expr/http_body_types.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func httpRequestBody(a *HTTPEndpointExpr) *AttributeExpr {
9191
return &AttributeExpr{Type: Empty}
9292
}
9393

94-
// 2. Remove header and param attributes
94+
// 2. Remove header, param and cookies attributes
9595
body := NewMappedAttributeExpr(payload)
9696
removeAttributes(body, headers)
9797
removeAttributes(body, cookies)
@@ -225,6 +225,13 @@ func buildHTTPResponseBody(name string, attr *AttributeExpr, resp *HTTPResponseE
225225
TypeName: name,
226226
UID: concat(svc.Name(), "#", name),
227227
}
228+
229+
// Remember original type name for example to generate friendly OpenAPI
230+
// specs.
231+
if t, ok := attr.Type.(UserType); ok {
232+
userType.AttributeExpr.AddMeta("name:original", t.Name())
233+
}
234+
228235
appendSuffix(userType.Attribute().Type, suffix)
229236
rt, isrt := attr.Type.(*ResultTypeExpr)
230237
if !isrt {

expr/http_endpoint.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,10 @@ func (e *HTTPEndpointExpr) Prepare() {
258258
}
259259
}
260260

261-
// Make sure there's a default response if none define explicitly
261+
// Make sure there's a default success response if none define explicitly.
262262
if len(e.Responses) == 0 {
263263
status := StatusOK
264-
if e.MethodExpr.Payload.Type == Empty && !e.SkipResponseBodyEncodeDecode {
264+
if e.MethodExpr.Result.Type == Empty && !e.SkipResponseBodyEncodeDecode {
265265
status = StatusNoContent
266266
}
267267
e.Responses = []*HTTPResponseExpr{{StatusCode: status}}

expr/http_response.go

+4
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ func (r *HTTPResponseExpr) Finalize(a *HTTPEndpointExpr, svcAtt *AttributeExpr)
281281
r.Body.Validation.AddRequired(n)
282282
}
283283
}
284+
// Remember original name for example to generate friendlier OpenAPI specs.
285+
if t, ok := r.Body.Type.(UserType); ok {
286+
t.Attribute().AddMeta("name:original", t.Name())
287+
}
284288
// Wrap object with user type to simplify response rendering code.
285289
r.Body.Type = &UserTypeExpr{
286290
AttributeExpr: DupAtt(r.Body),

expr/result_type.go

+5
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ func (m *ResultTypeExpr) Finalize() {
202202
// The resulting result type defines a default view. The result type identifier is
203203
// computed by adding a parameter called "view" to the original identifier. The
204204
// value of the "view" parameter is the name of the view.
205+
//
206+
// Project returns an error if the view does not exist for the given result type
207+
// or any result type that makes up its attributes recursively. Note that
208+
// individual attributes may use a different view. In this case Project uses
209+
// that view and returns an error if it isn't defined on the attribute type.
205210
func Project(m *ResultTypeExpr, view string, seen ...map[string]*AttributeExpr) (*ResultTypeExpr, error) {
206211
_, params, _ := mime.ParseMediaType(m.Identifier)
207212
if params["view"] == view {

expr/server.go

+56-16
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ var validSchemes = map[string]struct{}{"http": {}, "https": {}, "grpc": {}, "grp
126126
func (h *HostExpr) Validate() error {
127127
verr := new(eval.ValidationErrors)
128128
if len(h.URIs) == 0 {
129-
verr.Add(h, "host must defined at least one URI")
129+
verr.Add(h, "host must define at least one URI")
130130
}
131131
for _, u := range h.URIs {
132132
vu := uriVariableRegex.ReplaceAllString(string(u), "/w")
@@ -184,21 +184,7 @@ func (h *HostExpr) Attribute() *AttributeExpr {
184184
func (h *HostExpr) Schemes() []string {
185185
schemes := make(map[string]struct{})
186186
for _, uri := range h.URIs {
187-
ustr := string(uri)
188-
// Did not use url package to find scheme because the url may
189-
// contain params (i.e. http://{version}.example.com) which needs
190-
// substition for url.Parse to succeed. Also URIs in host must have
191-
// a scheme otherwise validations would have failed.
192-
switch {
193-
case strings.HasPrefix(ustr, "https"):
194-
schemes["https"] = struct{}{}
195-
case strings.HasPrefix(ustr, "http"):
196-
schemes["http"] = struct{}{}
197-
case strings.HasPrefix(ustr, "grpcs"):
198-
schemes["grpcs"] = struct{}{}
199-
case strings.HasPrefix(ustr, "grpc"):
200-
schemes["grpc"] = struct{}{}
201-
}
187+
schemes[uri.Scheme()] = struct{}{}
202188
}
203189
ss := make([]string, len(schemes))
204190
i := 0
@@ -236,6 +222,38 @@ func (h *HostExpr) HasGRPCScheme() bool {
236222
return false
237223
}
238224

225+
// URIString returns a valid URI string by substituting the parameters with
226+
// their default value if present or the first item in their enum. It returns
227+
// an error if the given URI expression is not found in the host URIs.
228+
func (h *HostExpr) URIString(u URIExpr) (string, error) {
229+
found := false
230+
for _, ue := range h.URIs {
231+
if ue == u {
232+
found = true
233+
break
234+
}
235+
}
236+
if !found {
237+
return "", fmt.Errorf("uri %s not found in host", string(u))
238+
}
239+
uri := string(u)
240+
// Substitute URI parameters with the corresponding variables defined in
241+
// the host expression. Validations would have made sure that every
242+
// URI parameter have a corresponding variable.
243+
for _, p := range u.Params() {
244+
for _, v := range *AsObject(h.Variables.Type) {
245+
if p == v.Name {
246+
def := v.Attribute.DefaultValue
247+
if def == nil {
248+
def = v.Attribute.Validation.Values[0]
249+
}
250+
uri = strings.Replace(uri, fmt.Sprintf("{%s}", p), fmt.Sprintf("%v", def), -1)
251+
}
252+
}
253+
}
254+
return uri, nil
255+
}
256+
239257
// Params return the names of the parameters used in URI if any.
240258
func (u URIExpr) Params() []string {
241259
r := regexp.MustCompile(`\{([^\{\}]+)\}`)
@@ -249,3 +267,25 @@ func (u URIExpr) Params() []string {
249267
}
250268
return wcs
251269
}
270+
271+
// Scheme returns the URI scheme. Possible values are http, https, grpc, and
272+
// grpcs.
273+
func (u URIExpr) Scheme() string {
274+
ustr := string(u)
275+
// Did not use url package to find scheme because the url may
276+
// contain params (i.e. http://{version}.example.com) which needs
277+
// substition for url.Parse to succeed. Also URIs in host must have
278+
// a scheme otherwise validations would have failed.
279+
switch {
280+
case strings.HasPrefix(ustr, "https"):
281+
return "https"
282+
case strings.HasPrefix(ustr, "grpcs"):
283+
return "grpcs"
284+
case strings.HasPrefix(ustr, "grpc"):
285+
return "grpc"
286+
default:
287+
// No need to worry about other values because the URIExpr would have failed
288+
// validation.
289+
return "http"
290+
}
291+
}

expr/server_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestServerExprValidate(t *testing.T) {
3434
validURIs = []URIExpr{
3535
validURI,
3636
}
37-
errNoURI = fmt.Errorf("host must defined at least one URI")
37+
errNoURI = fmt.Errorf("host must define at least one URI")
3838
errServiceUndefined = fmt.Errorf("service %q undefined", bar)
3939
)
4040

@@ -173,7 +173,7 @@ func TestHostExprValidate(t *testing.T) {
173173
Type: d,
174174
}
175175
}
176-
errNoURI = fmt.Errorf("host must defined at least one URI")
176+
errNoURI = fmt.Errorf("host must define at least one URI")
177177
errMalformedURI = fmt.Errorf("malformed URI %q", malformedURI)
178178
errMissingSchemeURI = fmt.Errorf("missing scheme for URI %q, scheme must be one of 'http', 'https', 'grpc' or 'grpcs'", missingSchemeURI)
179179
errInvalidSchemeURI = fmt.Errorf("invalid scheme for URI %q, scheme must be one of 'http', 'https', 'grpc' or 'grpcs'", invalidSchemeURI)

expr/service.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,7 @@ func (e *ErrorExpr) Finalize() {
137137
// This type does not have an attribute with "struct:error:name" meta.
138138
// It means the type is used by at most one error (otherwise validations
139139
// would have failed).
140-
datt := dt.Attribute()
141-
if datt.Meta == nil {
142-
datt.Meta = MetaExpr{}
143-
}
144-
datt.Meta["struct:error:name"] = []string{e.Name}
140+
dt.Attribute().AddMeta("struct:error:name", e.Name)
145141
}
146142
default:
147143
ut := &UserTypeExpr{

expr/user_type.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ func (u *UserTypeExpr) Name() string {
4040
}
4141

4242
// Rename changes the type name to the given value.
43-
func (u *UserTypeExpr) Rename(n string) { u.TypeName = n }
43+
func (u *UserTypeExpr) Rename(n string) {
44+
// Remember original name for example to generate friendly docs.
45+
u.AttributeExpr.AddMeta("name:original", u.TypeName)
46+
u.TypeName = n
47+
}
4448

4549
// IsCompatible returns true if u describes the (Go) type of val.
4650
func (u *UserTypeExpr) IsCompatible(val interface{}) bool {

go.mod

+7-2
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,23 @@ go 1.14
55
require (
66
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
77
github.com/dimfeld/httptreemux/v5 v5.0.2
8+
github.com/getkin/kin-openapi v0.15.0
89
github.com/go-openapi/loads v0.19.5
910
github.com/golang/protobuf v1.4.2
1011
github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 // indirect
1112
github.com/gorilla/websocket v1.4.1
13+
github.com/hashicorp/go-getter v1.4.1 // indirect
1214
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
1315
github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b // indirect
1416
github.com/pkg/errors v0.9.1
1517
github.com/sergi/go-diff v1.1.0
1618
github.com/smartystreets/goconvey v1.6.4 // indirect
19+
github.com/stretchr/testify v1.5.1 // indirect
1720
github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
21+
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
1822
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 // indirect
19-
golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f
23+
golang.org/x/tools v0.0.0-20200619210111-0f592d2728bb
2024
google.golang.org/grpc v1.28.0
21-
gopkg.in/yaml.v2 v2.2.8
25+
gopkg.in/yaml.v2 v2.3.0
26+
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
2227
)

0 commit comments

Comments
 (0)