diff --git a/generator/internal/genclient/translator/openapi/openapi.go b/generator/internal/genclient/translator/openapi/openapi.go index 2a61ccff3..fb16fb372 100644 --- a/generator/internal/genclient/translator/openapi/openapi.go +++ b/generator/internal/genclient/translator/openapi/openapi.go @@ -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, 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 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) @@ -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]) } @@ -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", 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 { diff --git a/generator/internal/genclient/translator/openapi/openapi_test.go b/generator/internal/genclient/translator/openapi/openapi_test.go index 742bf13d9..32d536199 100644 --- a/generator/internal/genclient/translator/openapi/openapi_test.go +++ b/generator/internal/genclient/translator/openapi/openapi_test.go @@ -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"}, + {Name: "fMapS32", Typez: genclient.MESSAGE_TYPE, TypezID: "$map"}, + {Name: "fMapS64", Typez: genclient.MESSAGE_TYPE, TypezID: "$map"}, + }, + }) +} + +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", Optional: false}, + {Name: "fMapI64", Typez: genclient.MESSAGE_TYPE, TypezID: "$map", Optional: false}, + }, + }) +} + func TestMakeAPI(t *testing.T) { contents := []byte(testDocument) translator, err := NewTranslator(contents, &Options{ @@ -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", + 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, }, }, @@ -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, }, {