Skip to content

Commit 711cfde

Browse files
OpenAPIv3: Generate different schemas for types with different validation rules (#3642)
Ensure that two user types with different validation rules produce different schemas in the generated OpenAPI specification. --------- Co-authored-by: Isaac Seymour <[email protected]>
1 parent dccae52 commit 711cfde

File tree

6 files changed

+288
-76
lines changed

6 files changed

+288
-76
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
99
github.com/getkin/kin-openapi v0.128.0
1010
github.com/go-chi/chi/v5 v5.2.0
11+
github.com/gohugoio/hashstructure v0.3.0
1112
github.com/google/uuid v1.6.0
1213
github.com/gorilla/websocket v1.5.3
1314
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
1616
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
1717
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
1818
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
19+
github.com/gohugoio/hashstructure v0.3.0 h1:orHavfqnBv0ffQmobOp41Y9HKEMcjrR/8EFAzpngmGs=
20+
github.com/gohugoio/hashstructure v0.3.0/go.mod h1:8ohPTAfQLTs2WdzB6k9etmQYclDUeNsIHGPAFejbsEA=
1921
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
2022
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
2123
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=

http/codegen/openapi/v3/files_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func TestFiles(t *testing.T) {
117117
left = buf.String()
118118
right = string(want)
119119
}
120-
assert.Equal(t, left, right)
120+
assert.Equal(t, right, left)
121121
}
122122
})
123123
}

http/codegen/openapi/v3/testdata/dsls/types.go

+43
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,49 @@ func StringBodyDSL(svcName, metName string) func() {
1515
}
1616
}
1717

18+
func StringEnumBodyDSL() func() {
19+
return func() {
20+
var T1 = Type("T1", func() {
21+
Attribute("my_attr", String, func() {
22+
Enum("a", "b", "c")
23+
})
24+
})
25+
26+
var T2 = Type("T2", func() {
27+
Attribute("my_attr", String, func() {
28+
Enum("d", "e")
29+
})
30+
})
31+
32+
var _ = Service("svc_enum_1", func() {
33+
Method("method_enum", func() {
34+
Payload(func() {
35+
Reference(T1)
36+
Attribute("my_attr")
37+
Required("my_attr")
38+
})
39+
HTTP(func() {
40+
POST("/")
41+
})
42+
})
43+
})
44+
45+
var _ = Service("svc_enum_2", func() {
46+
Method("method_enum", func() {
47+
Payload(func() {
48+
Reference(T2)
49+
Attribute("my_attr")
50+
Required("my_attr")
51+
})
52+
53+
HTTP(func() {
54+
POST("/other")
55+
})
56+
})
57+
})
58+
}
59+
}
60+
1861
func AliasStringBodyDSL(svcName, metName string) func() {
1962
return func() {
2063
var UUID = Type("UUID", String, func() {

http/codegen/openapi/v3/types.go

+35-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"strconv"
99
"strings"
1010

11+
"github.com/gohugoio/hashstructure"
12+
1113
"goa.design/goa/v3/codegen"
1214
"goa.design/goa/v3/expr"
1315
"goa.design/goa/v3/http/codegen/openapi"
@@ -376,7 +378,7 @@ func toString(val any) string {
376378
// identifiers. Structurally identical means same primitive types, arrays with
377379
// structurally equivalent element types, maps with structurally equivalent key
378380
// and value types or object with identical attribute names and structurally
379-
// equivalent types and identical set of required attributes.
381+
// equivalent types and identical set of validation rules.
380382
func (*schemafier) hashAttribute(att *expr.AttributeExpr, h hash.Hash64) uint64 {
381383
return *hashAttribute(att, h, make(map[string]*uint64))
382384
}
@@ -393,6 +395,7 @@ func hashAttribute(att *expr.AttributeExpr, h hash.Hash64, seen map[string]*uint
393395
}
394396
seen[t.Hash()] = res
395397

398+
hv := hashValidation(att.Validation, h)
396399
switch t.Kind() {
397400
case expr.ObjectKind:
398401
o := expr.AsObject(t)
@@ -404,25 +407,26 @@ func hashAttribute(att *expr.AttributeExpr, h hash.Hash64, seen map[string]*uint
404407
vh := hashAttribute(m.Attribute, h, seen)
405408
*res = *res ^ orderedHash(kh, *vh, h)
406409
}
407-
// Objects with a different set of required attributes should produce
408-
// different hashes.
409-
if att.Validation != nil {
410-
for _, req := range att.Validation.Required {
411-
rh := hashString(req, h)
412-
*res = *res ^ rh
413-
}
410+
if hv != 0 {
411+
*res = orderedHash(*res, hv, h)
414412
}
415413

416414
case expr.ArrayKind:
417415
kh := hashString("[]", h)
418416
vh := hashAttribute(expr.AsArray(t).ElemType, h, seen)
419417
*res = orderedHash(kh, *vh, h)
418+
if hv != 0 {
419+
*res = orderedHash(*res, hv, h)
420+
}
420421

421422
case expr.MapKind:
422423
m := expr.AsMap(t)
423424
kh := hashAttribute(m.KeyType, h, seen)
424425
vh := hashAttribute(m.ElemType, h, seen)
425426
*res = orderedHash(*kh, *vh, h)
427+
if hv != 0 {
428+
*res = orderedHash(*res, hv, h)
429+
}
426430

427431
case expr.UserTypeKind:
428432
*res = *hashAttribute(t.(expr.UserType).Attribute(), h, seen)
@@ -438,8 +442,31 @@ func hashAttribute(att *expr.AttributeExpr, h hash.Hash64, seen map[string]*uint
438442

439443
default: // Primitives or Any
440444
*res = hashString(t.Name(), h)
445+
if hv != 0 {
446+
*res = orderedHash(*res, hv, h)
447+
}
448+
}
449+
450+
return res
451+
}
452+
453+
func hashValidation(val *expr.ValidationExpr, h hash.Hash64) uint64 {
454+
// Note: we can't use hashstructure for attributes because it doesn't
455+
// handle recursive structures.
456+
if val == nil {
457+
return 0
441458
}
442459

460+
res, err := hashstructure.Hash(val, &hashstructure.HashOptions{
461+
Hasher: h,
462+
ZeroNil: false,
463+
IgnoreZeroValue: true,
464+
SlicesAsSets: true,
465+
})
466+
if err != nil {
467+
// should really never happen (OOM maybe)
468+
return 0
469+
}
443470
return res
444471
}
445472

0 commit comments

Comments
 (0)