Skip to content

Commit 4f8ee09

Browse files
authored
Make read_only work with references using AllOf (#3082)
As described here: #1137 This partially reverts the recent commit: a0fe8d7 Which doesn't solve the problem for read_only being set on the field. This feature will require setting the "use_allof_for_refs" to true since this breaks existing compatibility with swagger-codegen generated files from the AllOf'd schema - type is now "object" and not $ref. Test added + ReDoc now works with read_only message/enum fields.
1 parent 4cceda0 commit 4f8ee09

File tree

6 files changed

+100
-0
lines changed

6 files changed

+100
-0
lines changed

internal/descriptor/registry.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ type Registry struct {
136136
// disableDefaultResponses disables the generation of default responses.
137137
// Useful if you have to support custom response codes that are not 200.
138138
disableDefaultResponses bool
139+
140+
// useAllOfForRefs, if set, will use allOf as container for $ref to preserve same-level
141+
// properties
142+
useAllOfForRefs bool
139143
}
140144

141145
type repeatedFieldSeparator struct {
@@ -772,3 +776,13 @@ func (r *Registry) SetDisableDefaultResponses(use bool) {
772776
func (r *Registry) GetDisableDefaultResponses() bool {
773777
return r.disableDefaultResponses
774778
}
779+
780+
// SetUseAllOfForRefs sets useAllOfForRefs
781+
func (r *Registry) SetUseAllOfForRefs(use bool) {
782+
r.useAllOfForRefs = use
783+
}
784+
785+
// GetUseAllOfForRefs returns useAllOfForRefs
786+
func (r *Registry) GetUseAllOfForRefs() bool {
787+
return r.useAllOfForRefs
788+
}

protoc-gen-openapiv2/defs.bzl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,12 @@ protoc_gen_openapiv2 = rule(
356356
" Repeat this option to supply multiple values. Elements without" +
357357
" visibility annotations are unaffected by this setting.",
358358
),
359+
"use_allof_for_refs": attr.bool(
360+
default = False,
361+
mandatory = False,
362+
doc = "if set, will use allOf as container for $ref to preserve" +
363+
" same-level properties.",
364+
),
359365
"_protoc": attr.label(
360366
default = "@com_google_protobuf//:protoc",
361367
executable = True,

protoc-gen-openapiv2/internal/genopenapi/template.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,24 @@ func renderMessageAsDefinition(msg *descriptor.Message, reg *descriptor.Registry
515515
fieldSchema.Required = nil
516516
}
517517

518+
if reg.GetUseAllOfForRefs() {
519+
if fieldSchema.Ref != "" {
520+
// Per the JSON Reference syntax: Any members other than "$ref" in a JSON Reference object SHALL be ignored.
521+
// https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03#section-3
522+
// However, use allOf to specify Title/Description/readOnly fields.
523+
if fieldSchema.Title != "" || fieldSchema.Description != "" || fieldSchema.ReadOnly {
524+
fieldSchema = openapiSchemaObject{
525+
Title: fieldSchema.Title,
526+
Description: fieldSchema.Description,
527+
ReadOnly: fieldSchema.ReadOnly,
528+
AllOf: []allOfEntry{{Ref: fieldSchema.Ref}},
529+
}
530+
} else {
531+
fieldSchema = openapiSchemaObject{schemaCore: schemaCore{Ref: fieldSchema.Ref}}
532+
}
533+
}
534+
}
535+
518536
kv := keyVal{Value: fieldSchema}
519537
kv.Key = reg.FieldName(f)
520538
if schema.Properties == nil {

protoc-gen-openapiv2/internal/genopenapi/template_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4690,6 +4690,7 @@ func TestRenderMessagesAsDefinition(t *testing.T) {
46904690
openAPIOptions *openapiconfig.OpenAPIOptions
46914691
pathParams []descriptor.Parameter
46924692
UseJSONNamesForFields bool
4693+
UseAllOfForRefs bool
46934694
}{
46944695
{
46954696
descr: "no OpenAPI options",
@@ -5292,6 +5293,55 @@ func TestRenderMessagesAsDefinition(t *testing.T) {
52925293
},
52935294
UseJSONNamesForFields: true,
52945295
},
5296+
{
5297+
descr: "JSONSchema with a read_only nested field",
5298+
msgDescs: []*descriptorpb.DescriptorProto{
5299+
{
5300+
Name: proto.String("Message"),
5301+
Field: []*descriptorpb.FieldDescriptorProto{
5302+
{
5303+
Name: proto.String("nested"),
5304+
Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(),
5305+
TypeName: proto.String(".example.Message.Nested"),
5306+
Number: proto.Int32(1),
5307+
Options: fieldBehaviorOutputOnlyOptions,
5308+
},
5309+
},
5310+
NestedType: []*descriptorpb.DescriptorProto{{
5311+
Name: proto.String("Nested"),
5312+
}},
5313+
},
5314+
},
5315+
UseAllOfForRefs: true,
5316+
schema: map[string]*openapi_options.Schema{
5317+
"Message": {
5318+
JsonSchema: &openapi_options.JSONSchema{
5319+
Title: "title",
5320+
Description: "desc",
5321+
Required: []string{},
5322+
},
5323+
},
5324+
},
5325+
defs: map[string]openapiSchemaObject{
5326+
"exampleMessage": {
5327+
schemaCore: schemaCore{
5328+
Type: "object",
5329+
},
5330+
Title: "title",
5331+
Description: "desc",
5332+
Required: nil,
5333+
Properties: &openapiSchemaObjectProperties{
5334+
{
5335+
Key: "nested",
5336+
Value: openapiSchemaObject{
5337+
AllOf: []allOfEntry{{Ref: "#/definitions/MessageNested"}},
5338+
ReadOnly: true,
5339+
},
5340+
},
5341+
},
5342+
},
5343+
},
5344+
},
52955345
}
52965346

52975347
for _, test := range tests {
@@ -5328,6 +5378,10 @@ func TestRenderMessagesAsDefinition(t *testing.T) {
53285378
reg.SetUseJSONNamesForFields(true)
53295379
}
53305380

5381+
if test.UseAllOfForRefs {
5382+
reg.SetUseAllOfForRefs(true)
5383+
}
5384+
53315385
if err != nil {
53325386
t.Fatalf("failed to load code generator request: %v", err)
53335387
}

protoc-gen-openapiv2/internal/genopenapi/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ type schemaCore struct {
183183
Default string `json:"default,omitempty" yaml:"default,omitempty"`
184184
}
185185

186+
type allOfEntry struct {
187+
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
188+
}
189+
186190
type RawExample json.RawMessage
187191

188192
func (m RawExample) MarshalJSON() ([]byte, error) {
@@ -332,6 +336,8 @@ type openapiSchemaObject struct {
332336
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
333337

334338
extensions []extension
339+
340+
AllOf []allOfEntry `json:"allOf,omitempty" yaml:"allOf,omitempty"`
335341
}
336342

337343
// http://swagger.io/specification/#definitionsObject

protoc-gen-openapiv2/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ var (
4242
visibilityRestrictionSelectors = utilities.StringArrayFlag(flag.CommandLine, "visibility_restriction_selectors", "list of `google.api.VisibilityRule` visibility labels to include in the generated output when a visibility annotation is defined. Repeat this option to supply multiple values. Elements without visibility annotations are unaffected by this setting.")
4343
disableServiceTags = flag.Bool("disable_service_tags", false, "if set, disables generation of service tags. This is useful if you do not want to expose the names of your backend grpc services.")
4444
disableDefaultResponses = flag.Bool("disable_default_responses", false, "if set, disables generation of default responses. Useful if you have to support custom response codes that are not 200.")
45+
useAllOfForRefs = flag.Bool("use_allof_for_refs", false, "if set, will use allOf as container for $ref to preserve same-level properties.")
4546
)
4647

4748
// Variables set by goreleaser at build time
@@ -127,6 +128,7 @@ func main() {
127128
reg.SetVisibilityRestrictionSelectors(*visibilityRestrictionSelectors)
128129
reg.SetDisableServiceTags(*disableServiceTags)
129130
reg.SetDisableDefaultResponses(*disableDefaultResponses)
131+
reg.SetUseAllOfForRefs(*useAllOfForRefs)
130132
if err := reg.SetRepeatedPathParamSeparator(*repeatedPathParamSeparator); err != nil {
131133
emitError(err)
132134
return

0 commit comments

Comments
 (0)