Skip to content

Commit

Permalink
impl(generator): handle OpenAPI map types
Browse files Browse the repository at this point in the history
OpenAPI does not really have `map<..>` types like Protobuf, but it has `object`
fields sometimes with restrictions on the value type. If one squints, `object`
is equivalent to `google.protobuf.Any`. And an JSON object where the values can
only be `int32` is the same as `map<string, int32>`.
  • Loading branch information
coryan committed Nov 3, 2024
1 parent bbddb19 commit 70d0a94
Show file tree
Hide file tree
Showing 2 changed files with 267 additions and 9 deletions.
101 changes: 93 additions & 8 deletions generator/internal/genclient/translator/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,19 +184,55 @@ func (t *Translator) makeObjectField(messageName, name string, field *base.Schem
if len(field.AllOf) != 0 {
return t.makeObjectFieldAllOf(messageName, name, field)
}
// TODO(#62) - this is an Any or a map<string, T>, needs a TypezID
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.MESSAGE_TYPE,
Optional: true,
}, nil
if field.AdditionalProperties != nil && field.AdditionalProperties.IsA() {
// This indicates we have a map<K, T> field. In OpenAPI, these are
// simply JSON objects, maybe with a restrictive value type.
schema, err := field.AdditionalProperties.A.BuildSchema()
if err != nil {
return nil, fmt.Errorf("cannot build schema for field %s.%s, error=%q", messageName, name, err)
}

if len(schema.Type) == 0 {
// Untyped message fields are .google.protobuf.Any
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.MESSAGE_TYPE,
TypezID: ".google.protobuf.Any",
Optional: true,
}, nil
}
message, err := t.makeMapMessage(messageName, name, schema)
if err != nil {
return nil, err
}
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.MESSAGE_TYPE,
TypezID: message.ID,
Optional: false,
}, nil
}
if field.Items != nil && field.Items.IsA() {
proxy := field.Items.A
typezID := strings.TrimPrefix(proxy.GetReference(), "#/components/schemas/")
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.MESSAGE_TYPE,
TypezID: typezID,
Optional: true,
}, nil
}
return nil, fmt.Errorf("unknown object field type for field %s.%s", messageName, name)
}

func (t *Translator) makeArrayField(messageName, name string, field *base.Schema) (*genclient.Field, error) {
if !field.Items.IsA() {
return nil, fmt.Errorf("cannot handle arrays without an `Items` field for %s.%s", messageName, name)
}
reference := field.Items.A.GetReference()
schema, err := field.Items.A.BuildSchema()
if err != nil {
return nil, fmt.Errorf("cannot build items schema for %s.%s error=%q", messageName, name, err)
Expand All @@ -215,7 +251,18 @@ func (t *Translator) makeArrayField(messageName, name string, field *base.Schema
case "string":
result, err = t.makeScalarField(messageName, name, schema, false, field)
case "object":
result, err = t.makeObjectField(messageName, name, field)
typezID := strings.TrimPrefix(reference, "#/components/schemas/")
if len(typezID) > 0 {
new := &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.MESSAGE_TYPE,
TypezID: typezID,
}
result = new
} else {
result, err = t.makeObjectField(messageName, name, schema)
}
default:
return nil, fmt.Errorf("unknown array field type for %s.%s %q", messageName, name, schema.Type[0])
}
Expand All @@ -241,6 +288,44 @@ func (t *Translator) makeObjectFieldAllOf(messageName, name string, field *base.
return nil, fmt.Errorf("cannot build any AllOf schema for field %s.%s", messageName, name)
}

func (t *Translator) makeMapMessage(messageName, name string, schema *base.Schema) (*genclient.Message, error) {
value_typez, value_id, err := scalarType(messageName, name, schema)
if err != nil {
return nil, err
}
value := &genclient.Field{
Name: "$value",
ID: value_id,
Typez: value_typez,
TypezID: value_id,
}

id := fmt.Sprintf("$map<string, %s>", value.TypezID)
message := t.state.MessageByID[id]
if message == nil {
// The map was not found, insert the type.
key := &genclient.Field{
Name: "$key",
ID: id + "$key",
Typez: genclient.STRING_TYPE,
TypezID: "string",
}
new := &genclient.Message{
Name: id,
Documentation: id,
ID: id,
IsLocalToPackage: false,
IsMap: true,
Fields: []*genclient.Field{key, value},
Parent: nil,
Package: "$",
}
t.state.MessageByID[id] = new
message = new
}
return message, nil
}

func scalarType(messageName, name string, schema *base.Schema) (genclient.Typez, string, error) {
for _, type_name := range schema.Type {
switch type_name {
Expand Down
175 changes: 174 additions & 1 deletion generator/internal/genclient/translator/openapi/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,176 @@ func TestArrayTypes(t *testing.T) {
})
}

func TestSimpleObject(t *testing.T) {
const messageWithBasicTypes = `
"Fake": {
"description": "A test message.",
"type": "object",
"properties": {
"fObject" : { "type": "object", "description": "An object field.", "allOf": [{ "$ref": "#/components/schemas/Foo" }] },
"fObjectArray": { "type": "array", "description": "An object array field.", "items": [{ "$ref": "#/components/schemas/Bar" }] }
}
},
"Foo": {
"description": "Must have a Foo.",
"type": "object",
"properties": {}
},
"Bar": {
"description": "Must have a Bar.",
"type": "object",
"properties": {}
},
`
contents := []byte(singleMessagePreamble + messageWithBasicTypes + singleMessageTrailer)
translator, err := NewTranslator(contents, &Options{
Language: "not used",
OutDir: "not used",
TemplateDir: "not used",
})
if err != nil {
t.Errorf("Error in NewTranslator() %q", err)
}

api, err := translator.makeAPI()
if err != nil {
t.Errorf("Error in makeAPI() %q", err)
}

checkMessage(t, *api.Messages[0], genclient.Message{
Name: "Fake",
Documentation: "A test message.",
Fields: []*genclient.Field{
{
Name: "fObject",
Typez: genclient.MESSAGE_TYPE,
TypezID: "Foo",
Documentation: "An object field.",
Optional: true,
},
{
Name: "fObjectArray",
Typez: genclient.MESSAGE_TYPE,
TypezID: "Bar",
Documentation: "An object array field.",
Optional: false,
Repeated: true,
},
},
})
}

func TestAny(t *testing.T) {
// A message with basic types.
const messageWithBasicTypes = `
"Fake": {
"description": "A test message.",
"type": "object",
"properties": {
"fMap": { "type": "object", "additionalProperties": { "description": "Test Only." }}
}
},
`
contents := []byte(singleMessagePreamble + messageWithBasicTypes + singleMessageTrailer)
translator, err := NewTranslator(contents, &Options{
Language: "not used",
OutDir: "not used",
TemplateDir: "not used",
})
if err != nil {
t.Errorf("Error in NewTranslator() %q", err)
}

api, err := translator.makeAPI()
if err != nil {
t.Errorf("Error in makeAPI() %q", err)
}

checkMessage(t, *api.Messages[0], genclient.Message{
Name: "Fake",
Documentation: "A test message.",
Fields: []*genclient.Field{
{Name: "fMap", Typez: genclient.MESSAGE_TYPE, TypezID: ".google.protobuf.Any", Optional: true},
},
})
}

func TestMapString(t *testing.T) {
// A message with basic types.
const messageWithBasicTypes = `
"Fake": {
"description": "A test message.",
"type": "object",
"properties": {
"fMap": { "type": "object", "additionalProperties": { "type": "string" }},
"fMapS32": { "type": "object", "additionalProperties": { "type": "string", "format": "int32" }},
"fMapS64": { "type": "object", "additionalProperties": { "type": "string", "format": "int64" }}
}
},
`
contents := []byte(singleMessagePreamble + messageWithBasicTypes + singleMessageTrailer)
translator, err := NewTranslator(contents, &Options{
Language: "not used",
OutDir: "not used",
TemplateDir: "not used",
})
if err != nil {
t.Errorf("Error in NewTranslator() %q", err)
}

api, err := translator.makeAPI()
if err != nil {
t.Errorf("Error in makeAPI() %q", err)
}

checkMessage(t, *api.Messages[0], genclient.Message{
Name: "Fake",
Documentation: "A test message.",
Fields: []*genclient.Field{
{Name: "fMap", Typez: genclient.MESSAGE_TYPE, TypezID: "$map<string, string>"},
{Name: "fMapS32", Typez: genclient.MESSAGE_TYPE, TypezID: "$map<string, int32>"},
{Name: "fMapS64", Typez: genclient.MESSAGE_TYPE, TypezID: "$map<string, int64>"},
},
})
}

func TestMapInteger(t *testing.T) {
// A message with basic types.
const messageWithBasicTypes = `
"Fake": {
"description": "A test message.",
"type": "object",
"properties": {
"fMapI32": { "type": "object", "additionalProperties": { "type": "integer", "format": "int32" }},
"fMapI64": { "type": "object", "additionalProperties": { "type": "integer", "format": "int64" }}
}
},
`
contents := []byte(singleMessagePreamble + messageWithBasicTypes + singleMessageTrailer)
translator, err := NewTranslator(contents, &Options{
Language: "not used",
OutDir: "not used",
TemplateDir: "not used",
})
if err != nil {
t.Errorf("Error in NewTranslator() %q", err)
}

api, err := translator.makeAPI()
if err != nil {
t.Errorf("Error in makeAPI() %q", err)
}

checkMessage(t, *api.Messages[0], genclient.Message{
Name: "Fake",
Documentation: "A test message.",
Fields: []*genclient.Field{
{Name: "fMapI32", Typez: genclient.MESSAGE_TYPE, TypezID: "$map<string, int32>", Optional: false},
{Name: "fMapI64", Typez: genclient.MESSAGE_TYPE, TypezID: "$map<string, int64>", Optional: false},
},
})
}

func TestMakeAPI(t *testing.T) {
contents := []byte(testDocument)
translator, err := NewTranslator(contents, &Options{
Expand Down Expand Up @@ -249,12 +419,14 @@ func TestMakeAPI(t *testing.T) {
Name: "labels",
Documentation: "Cross-service attributes for the location.",
Typez: genclient.MESSAGE_TYPE,
Optional: true,
TypezID: "$map<string, string>",
Optional: false,
},
{
Name: "metadata",
Documentation: `Service-specific metadata. For example the available capacity at the given location.`,
Typez: genclient.MESSAGE_TYPE,
TypezID: ".google.protobuf.Any",
Optional: true,
},
},
Expand All @@ -268,6 +440,7 @@ func TestMakeAPI(t *testing.T) {
Name: "locations",
Documentation: "A list of locations that matches the specified filter in the request.",
Typez: genclient.MESSAGE_TYPE,
TypezID: "Location",
Repeated: true,
},
{
Expand Down

0 comments on commit 70d0a94

Please sign in to comment.