Skip to content

Commit

Permalink
Add openapi:additionalProperties Meta (#3644)
Browse files Browse the repository at this point in the history
* Add openapi.AdditionalPropertiesFromExpr()

* Add openapi:additionalProperties Meta

---------

Co-authored-by: Raphael Simon <[email protected]>
  • Loading branch information
tchssk and raphael authored Feb 6, 2025
1 parent 526552d commit 25c8744
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 3 deletions.
19 changes: 19 additions & 0 deletions dsl/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,25 @@ const DefaultProtoc = expr.DefaultProtoc
// Meta("openapi:extension:x-api", `{"foo":"bar"}`)
// })
//
// - "openapi:additionalProperties" sets the OpenAPI additionalProperties field.
// The value can be true or false. Defaults to true. Applicable to types (including
// embedded Payload and Result definitions).
//
// var Foo = Type("Foo", func() {
// Attribute("name", String)
// Meta("openapi:additionalProperties", "false")
// })
//
// Payload(Bar, func() {
// Attribute("name", String)
// Meta("openapi:additionalProperties", "false")
// })
//
// Result(func() {
// Attribute("name", String)
// Meta("openapi:additionalProperties", "false")
// })
//
// - "openapi:typename" overrides the name of the type generated in the OpenAPI specification.
// Applicable to types (including embedded Payload and Result definitions).
//
Expand Down
16 changes: 13 additions & 3 deletions expr/http_body_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,16 @@ func httpRequestBody(a *HTTPEndpointExpr) *AttributeExpr {
}
appendSuffix(ut.Attribute().Type, suffix)

// Remember openapi typename for example to generate friendly OpenAPI specs.
if t, ok := payload.Type.(UserType); ok {
// Remember openapi typename for example to generate friendly OpenAPI specs.
if m, ok := t.Attribute().Meta["openapi:typename"]; ok {
ut.AttributeExpr.AddMeta("openapi:typename", m...)
}

// Remember additionalProperties.
if m, ok := t.Attribute().Meta["openapi:additionalProperties"]; ok {
ut.AttributeExpr.AddMeta("openapi:additionalProperties", m...)
}
}

return &AttributeExpr{
Expand Down Expand Up @@ -334,13 +339,18 @@ func buildHTTPResponseBody(name string, attr *AttributeExpr, resp *HTTPResponseE
UID: concat(svc.Name(), "#", name),
}

// Remember original type name and openapi typename for example
// to generate friendly OpenAPI specs.
if t, ok := attr.Type.(UserType); ok {
// Remember original type name and openapi typename for example
// to generate friendly OpenAPI specs.
userType.AttributeExpr.AddMeta("name:original", t.Name())
if m, ok := t.Attribute().Meta["openapi:typename"]; ok {
userType.AttributeExpr.AddMeta("openapi:typename", m...)
}

// Remember additionalProperties.
if m, ok := t.Attribute().Meta["openapi:additionalProperties"]; ok {
userType.AttributeExpr.AddMeta("openapi:additionalProperties", m...)
}
}

appendSuffix(userType.Attribute().Type, suffix)
Expand Down
12 changes: 12 additions & 0 deletions http/codegen/openapi/json_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,9 @@ func buildAttributeSchema(api *expr.APIExpr, s *Schema, at *expr.AttributeExpr)
s.Description = at.Description
s.Example = at.Example(api.ExampleGenerator)
s.Extensions = ExtensionsFromExpr(at.Meta)
if ap := AdditionalPropertiesFromExpr(at.Meta); ap != nil {
s.AdditionalProperties = ap
}
initAttributeValidation(s, at)

return s
Expand Down Expand Up @@ -593,3 +596,12 @@ func MustGenerate(meta expr.MetaExpr) bool {
}
return true
}

// AdditionalPropertiesFromExpr extracts the OpenAPI additionalProperties.
func AdditionalPropertiesFromExpr(meta expr.MetaExpr) any {
m, ok := meta.Last("openapi:additionalProperties")
if ok && m == "false" {
return false
}
return nil
}
3 changes: 3 additions & 0 deletions http/codegen/openapi/v2/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ func TestSections(t *testing.T) {
{"json-prefix", testdata.JSONPrefixDSL},
{"json-indent", testdata.JSONIndentDSL},
{"json-prefix-indent", testdata.JSONPrefixIndentDSL},
{"additional-properties-type", testdata.AdditionalPropertiesTypeDSL},
{"additional-properties-payload-result", testdata.AdditionalPropertiesPayloadResultDSL},
{"additional-properties-embedded-payload-result", testdata.AdditionalPropertiesPayloadResultDSL},
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"swagger":"2.0","info":{"title":"","version":"0.0.1"},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["testService"],"summary":"testEndpoint testService","operationId":"testService#testEndpoint","parameters":[{"name":"TestEndpointRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/Payload"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/Result"}}},"schemes":["https"]}}},"definitions":{"Payload":{"title":"Payload","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""},"additionalProperties":false},"Result":{"title":"Result","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""},"additionalProperties":false}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
swagger: "2.0"
info:
title: ""
version: 0.0.1
host: goa.design
consumes:
- application/json
- application/xml
- application/gob
produces:
- application/json
- application/xml
- application/gob
paths:
/:
get:
tags:
- testService
summary: testEndpoint testService
operationId: testService#testEndpoint
parameters:
- name: TestEndpointRequestBody
in: body
required: true
schema:
$ref: '#/definitions/Payload'
responses:
"200":
description: OK response.
schema:
$ref: '#/definitions/Result'
schemes:
- https
definitions:
Payload:
title: Payload
type: object
properties:
string:
type: string
example: ""
example:
string: ""
additionalProperties: false
Result:
title: Result
type: object
properties:
string:
type: string
example: ""
example:
string: ""
additionalProperties: false
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"swagger":"2.0","info":{"title":"","version":"0.0.1"},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["testService"],"summary":"testEndpoint testService","operationId":"testService#testEndpoint","parameters":[{"name":"TestEndpointRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/Payload"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/Result"}}},"schemes":["https"]}}},"definitions":{"Payload":{"title":"Payload","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""},"additionalProperties":false},"Result":{"title":"Result","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""},"additionalProperties":false}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
swagger: "2.0"
info:
title: ""
version: 0.0.1
host: goa.design
consumes:
- application/json
- application/xml
- application/gob
produces:
- application/json
- application/xml
- application/gob
paths:
/:
get:
tags:
- testService
summary: testEndpoint testService
operationId: testService#testEndpoint
parameters:
- name: TestEndpointRequestBody
in: body
required: true
schema:
$ref: '#/definitions/Payload'
responses:
"200":
description: OK response.
schema:
$ref: '#/definitions/Result'
schemes:
- https
definitions:
Payload:
title: Payload
type: object
properties:
string:
type: string
example: ""
example:
string: ""
additionalProperties: false
Result:
title: Result
type: object
properties:
string:
type: string
example: ""
example:
string: ""
additionalProperties: false
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"swagger":"2.0","info":{"title":"","version":"0.0.1"},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["testService"],"summary":"testEndpoint testService","operationId":"testService#testEndpoint","parameters":[{"name":"TestEndpointRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/Payload"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/Result"}}},"schemes":["https"]}}},"definitions":{"Payload":{"title":"Payload","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""},"additionalProperties":false},"Result":{"title":"Result","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""},"additionalProperties":false}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
swagger: "2.0"
info:
title: ""
version: 0.0.1
host: goa.design
consumes:
- application/json
- application/xml
- application/gob
produces:
- application/json
- application/xml
- application/gob
paths:
/:
get:
tags:
- testService
summary: testEndpoint testService
operationId: testService#testEndpoint
parameters:
- name: TestEndpointRequestBody
in: body
required: true
schema:
$ref: '#/definitions/Payload'
responses:
"200":
description: OK response.
schema:
$ref: '#/definitions/Result'
schemes:
- https
definitions:
Payload:
title: Payload
type: object
properties:
string:
type: string
example: ""
example:
string: ""
additionalProperties: false
Result:
title: Result
type: object
properties:
string:
type: string
example: ""
example:
string: ""
additionalProperties: false
3 changes: 3 additions & 0 deletions http/codegen/openapi/v3/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ func (sf *schemafier) schemafy(attr *expr.AttributeExpr, noref ...bool) *openapi
s.Extensions = openapi.ExtensionsFromExpr(attr.Meta)

// Validations
if ap := openapi.AdditionalPropertiesFromExpr(attr.Meta); ap != nil {
s.AdditionalProperties = ap
}
val := attr.Validation
if val == nil {
return s
Expand Down
93 changes: 93 additions & 0 deletions http/codegen/testdata/openapi_dsls.go
Original file line number Diff line number Diff line change
Expand Up @@ -945,3 +945,96 @@ var JSONPrefixIndentDSL = func() {
})
})
}

var AdditionalPropertiesTypeDSL = func() {
var PayloadT = Type("Payload", func() {
Attribute("string", String, func() {
Example("")
})
Meta("openapi:additionalProperties", "false")
})
var ResultT = Type("Result", func() {
Attribute("string", String, func() {
Example("")
})
Meta("openapi:additionalProperties", "false")
})
var _ = API("test", func() {
Server("test", func() {
Host("localhost", func() {
URI("https://goa.design")
})
})
})
Service("testService", func() {
Method("testEndpoint", func() {
Payload(PayloadT)
Result(ResultT)
HTTP(func() {
GET("/")
})
})
})
}

var AdditionalPropertiesPayloadResultDSL = func() {
var PayloadT = Type("Payload", func() {
Attribute("string", String, func() {
Example("")
})
})
var ResultT = Type("Result", func() {
Attribute("string", String, func() {
Example("")
})
})
var _ = API("test", func() {
Server("test", func() {
Host("localhost", func() {
URI("https://goa.design")
})
})
})
Service("testService", func() {
Method("testEndpoint", func() {
Payload(PayloadT, func() {
Meta("openapi:additionalProperties", "false")
})
Result(ResultT, func() {
Meta("openapi:additionalProperties", "false")
})
HTTP(func() {
GET("/")
})
})
})
}

var AdditionalPropertiesEmbeddedPayloadResultDSL = func() {
var _ = API("test", func() {
Server("test", func() {
Host("localhost", func() {
URI("https://goa.design")
})
})
})
Service("testService", func() {
Method("testEndpoint", func() {
Payload(func() {
Attribute("string", String, func() {
Example("")
})
Meta("openapi:additionalProperties", "false")
})
Result(func() {
Attribute("string", String, func() {
Example("")
})
Meta("openapi:additionalProperties", "false")
})
HTTP(func() {
GET("/")
})
})
})
}

0 comments on commit 25c8744

Please sign in to comment.