Skip to content

Commit

Permalink
impl(generator): handle all OpenAPI scalar types (#67)
Browse files Browse the repository at this point in the history
Or at least I believe these are all the scalar types that can be represented in
OpenAPI v3 specs.
  • Loading branch information
coryan authored Nov 1, 2024
1 parent 2920296 commit 97e05e6
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 124 deletions.
198 changes: 92 additions & 106 deletions generator/internal/genclient/translator/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,13 @@ func (t *Translator) makeField(messageName, name string, optional bool, field *b
}
switch field.Type[0] {
case "boolean":
return t.makeBooleanField(name, optional, field)
case "string":
return t.makeStringField(messageName, name, field.Format, optional, field)
return t.makeScalarField(messageName, name, field, optional, field)
case "integer":
return t.makeIntegerField(messageName, name, field.Format, optional, field)
return t.makeScalarField(messageName, name, field, optional, field)
case "number":
return t.makeScalarField(messageName, name, field, optional, field)
case "string":
return t.makeScalarField(messageName, name, field, optional, field)
case "object":
return t.makeObjectField(messageName, name, field)
case "array":
Expand All @@ -164,109 +166,20 @@ func (t *Translator) makeField(messageName, name string, optional bool, field *b
}
}

func (t *Translator) makeBooleanField(name string, optional bool, field *base.Schema) (*genclient.Field, error) {
func (t *Translator) makeScalarField(messageName, name string, schema *base.Schema, optional bool, field *base.Schema) (*genclient.Field, error) {
typez, typezID, err := scalarType(messageName, name, schema)
if err != nil {
return nil, err
}
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.BOOL_TYPE,
Optional: optional,
Typez: typez,
TypezID: typezID,
Optional: optional || (typez == genclient.MESSAGE_TYPE),
}, nil
}

func (t *Translator) makeStringField(messageName, name, format string, optional bool, field *base.Schema) (*genclient.Field, error) {
switch format {
case "":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.STRING_TYPE,
Optional: optional,
}, nil
case "int64":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.INT64_TYPE,
Optional: optional,
}, nil
case "uint64":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.UINT64_TYPE,
Optional: optional,
}, nil
case "byte":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.BYTES_TYPE,
Optional: optional,
}, nil
case "google-duration":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.MESSAGE_TYPE,
TypezID: ".google.protobuf.Duration",
Optional: true,
}, nil
case "date-time":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.MESSAGE_TYPE,
TypezID: ".google.protobuf.Timestamp",
Optional: true,
}, nil
case "google-fieldmask":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.MESSAGE_TYPE,
TypezID: ".google.protobuf.FieldMask",
Optional: true,
}, nil
default:
return nil, fmt.Errorf("unknown string format (%q) for field %s.%s", field.Format, messageName, name)
}
}

func (t *Translator) makeIntegerField(messageName, name, format string, optional bool, field *base.Schema) (*genclient.Field, error) {
switch format {
case "int32":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.INT32_TYPE,
Optional: optional,
}, nil
case "int64":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.INT64_TYPE,
Optional: optional,
}, nil
case "uint32":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.UINT32_TYPE,
Optional: optional,
}, nil
case "uint64":
return &genclient.Field{
Name: name,
Documentation: field.Description,
Typez: genclient.UINT64_TYPE,
Optional: optional,
}, nil
default:
return nil, fmt.Errorf("unknown integer format (%q) for field %s.%s", format, messageName, name)
}
}

func (t *Translator) makeObjectField(messageName, name string, field *base.Schema) (*genclient.Field, error) {
if len(field.AllOf) != 0 {
return t.makeObjectFieldAllOf(messageName, name, field)
Expand All @@ -286,16 +199,21 @@ func (t *Translator) makeArrayField(messageName, name string, field *base.Schema
}
schema, err := field.Items.A.BuildSchema()
if err != nil {
return nil, fmt.Errorf("cannot build schema for %s.%s error=%q", messageName, name, err)
return nil, fmt.Errorf("cannot build items schema for %s.%s error=%q", messageName, name, err)
}
if len(schema.Type) != 1 {
return nil, fmt.Errorf("the items for field %s.%s should have a single type", messageName, name)
}
var result *genclient.Field
switch schema.Type[0] {
case "boolean":
result, err = t.makeBooleanField(name, false, field)
case "string":
result, err = t.makeStringField(messageName, name, schema.Format, false, field)
result, err = t.makeScalarField(messageName, name, schema, false, field)
case "integer":
result, err = t.makeIntegerField(messageName, name, schema.Format, false, field)
result, err = t.makeScalarField(messageName, name, schema, false, field)
case "number":
result, err = t.makeScalarField(messageName, name, schema, false, field)
case "string":
result, err = t.makeScalarField(messageName, name, schema, false, field)
case "object":
result, err = t.makeObjectField(messageName, name, field)
default:
Expand All @@ -322,3 +240,71 @@ 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 scalarType(messageName, name string, schema *base.Schema) (genclient.Typez, string, error) {
for _, type_name := range schema.Type {
switch type_name {
case "boolean":
return genclient.BOOL_TYPE, "bool", nil
case "integer":
return scalarTypeForIntegerFormats(messageName, name, schema)
case "number":
return scalarTypeForNumberFormats(messageName, name, schema)
case "string":
return scalarTypeForStringFormats(messageName, name, schema)
}
}
return 0, "", fmt.Errorf("expected a scalar type for field %s.%s", messageName, name)
}

func scalarTypeForIntegerFormats(messageName, name string, schema *base.Schema) (genclient.Typez, string, error) {
switch schema.Format {
case "int32":
if schema.Minimum != nil && *schema.Minimum == 0 {
return genclient.UINT32_TYPE, "uint32", nil
}
return genclient.INT32_TYPE, "int32", nil
case "int64":
if schema.Minimum != nil && *schema.Minimum == 0 {
return genclient.UINT64_TYPE, "uint64", nil
}
return genclient.INT64_TYPE, "int64", nil
}
return 0, "", fmt.Errorf("unknown integer format (%s) for field %s.%s", schema.Format, messageName, name)
}

func scalarTypeForNumberFormats(messageName, name string, schema *base.Schema) (genclient.Typez, string, error) {
switch schema.Format {
case "float":
return genclient.FLOAT_TYPE, "float", nil
case "double":
return genclient.DOUBLE_TYPE, "double", nil
}
return 0, "", fmt.Errorf("unknown number format (%s) for field %s.%s", schema.Format, messageName, name)
}

func scalarTypeForStringFormats(messageName, name string, schema *base.Schema) (genclient.Typez, string, error) {
switch schema.Format {
case "":
return genclient.STRING_TYPE, "string", nil
case "byte":
return genclient.BYTES_TYPE, "bytes", nil
case "int32":
if schema.Minimum != nil && *schema.Minimum == 0 {
return genclient.UINT32_TYPE, "uint32", nil
}
return genclient.INT32_TYPE, "int32", nil
case "int64":
if schema.Minimum != nil && *schema.Minimum == 0 {
return genclient.UINT64_TYPE, "uint64", nil
}
return genclient.INT64_TYPE, "int64", nil
case "google-duration":
return genclient.MESSAGE_TYPE, ".google.protobuf.Duration", nil
case "date-time":
return genclient.MESSAGE_TYPE, ".google.protobuf.Timestamp", nil
case "google-fieldmask":
return genclient.MESSAGE_TYPE, ".google.protobuf.FieldMask", nil
}
return 0, "", fmt.Errorf("unknown string format (%s) for field %s.%s", schema.Format, messageName, name)
}
54 changes: 36 additions & 18 deletions generator/internal/genclient/translator/openapi/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,25 @@ func TestBasicTypes(t *testing.T) {
"fBool": { "type": "boolean" },
"fInt64": { "type": "integer", "format": "int64" },
"fInt32": { "type": "integer", "format": "int32" },
"fUInt32": { "type": "integer", "format": "int32", "minimum": 0 },
"fFloat": { "type": "number", "format": "float" },
"fDouble": { "type": "number", "format": "double" },
"fString": { "type": "string" },
"fOptional": { "type": "string" },
"fSInt64": { "type": "string", "format": "int64" },
"fSUInt64": { "type": "string", "format": "uint64" },
"fSUInt64": { "type": "string", "format": "int64", "minimum": 0 },
"fDuration": { "type": "string", "format": "google-duration" },
"fTimestamp": { "type": "string", "format": "date-time" },
"fFieldMask": { "type": "string", "format": "google-fieldmask" },
"fBytes": { "type": "string", "format": "byte" }
},
"required": [
"fBool", "fInt64", "fInt32", "fString", "fSInt64",
"fSUInt64", "fDuration", "fTimestamp", "fFieldMask", "fBytes" ]
"fBool", "fInt64", "fInt32", "fUInt32",
"fFloat", "fDouble",
"fString",
"fSInt64", "fSUInt64",
"fDuration", "fTimestamp", "fFieldMask", "fBytes"
]
},
`
contents := []byte(singleMessagePreamble + messageWithBasicTypes + singleMessageTrailer)
Expand All @@ -123,16 +131,20 @@ func TestBasicTypes(t *testing.T) {
Name: "Fake",
Documentation: "A test message.",
Fields: []*genclient.Field{
{Name: "fBool", Typez: genclient.BOOL_TYPE},
{Name: "fInt64", Typez: genclient.INT64_TYPE},
{Name: "fInt32", Typez: genclient.INT32_TYPE},
{Name: "fString", Typez: genclient.STRING_TYPE},
{Name: "fSInt64", Typez: genclient.INT64_TYPE},
{Name: "fSUInt64", Typez: genclient.UINT64_TYPE},
{Name: "fBool", Typez: genclient.BOOL_TYPE, TypezID: "bool"},
{Name: "fInt64", Typez: genclient.INT64_TYPE, TypezID: "int64"},
{Name: "fInt32", Typez: genclient.INT32_TYPE, TypezID: "int32"},
{Name: "fUInt32", Typez: genclient.UINT32_TYPE, TypezID: "uint32"},
{Name: "fFloat", Typez: genclient.FLOAT_TYPE, TypezID: "float"},
{Name: "fDouble", Typez: genclient.DOUBLE_TYPE, TypezID: "double"},
{Name: "fString", Typez: genclient.STRING_TYPE, TypezID: "string"},
{Name: "fOptional", Typez: genclient.STRING_TYPE, TypezID: "string", Optional: true},
{Name: "fSInt64", Typez: genclient.INT64_TYPE, TypezID: "int64"},
{Name: "fSUInt64", Typez: genclient.UINT64_TYPE, TypezID: "uint64"},
{Name: "fDuration", Typez: genclient.MESSAGE_TYPE, TypezID: ".google.protobuf.Duration", Optional: true},
{Name: "fTimestamp", Typez: genclient.MESSAGE_TYPE, TypezID: ".google.protobuf.Timestamp", Optional: true},
{Name: "fFieldMask", Typez: genclient.MESSAGE_TYPE, TypezID: ".google.protobuf.FieldMask", Optional: true},
{Name: "fBytes", Typez: genclient.BYTES_TYPE},
{Name: "fBytes", Typez: genclient.BYTES_TYPE, TypezID: "bytes"},
},
})
}
Expand All @@ -147,9 +159,10 @@ func TestArrayTypes(t *testing.T) {
"fBool": { "type": "array", "items": { "type": "boolean" }},
"fInt64": { "type": "array", "items": { "type": "integer", "format": "int64" }},
"fInt32": { "type": "array", "items": { "type": "integer", "format": "int32" }},
"fUInt32": { "type": "array", "items": { "type": "integer", "format": "int32", "minimum": 0 }},
"fString": { "type": "array", "items": { "type": "string" }},
"fSInt64": { "type": "array", "items": { "type": "string", "format": "int64" }},
"fSUInt64": { "type": "array", "items": { "type": "string", "format": "uint64" }},
"fSUInt64": { "type": "array", "items": { "type": "string", "format": "int64", "minimum": 0 }},
"fDuration": { "type": "array", "items": { "type": "string", "format": "google-duration" }},
"fTimestamp": { "type": "array", "items": { "type": "string", "format": "date-time" }},
"fFieldMask": { "type": "array", "items": { "type": "string", "format": "google-fieldmask" }},
Expand All @@ -176,16 +189,17 @@ func TestArrayTypes(t *testing.T) {
Name: "Fake",
Documentation: "A test message.",
Fields: []*genclient.Field{
{Repeated: true, Name: "fBool", Typez: genclient.BOOL_TYPE},
{Repeated: true, Name: "fInt64", Typez: genclient.INT64_TYPE},
{Repeated: true, Name: "fInt32", Typez: genclient.INT32_TYPE},
{Repeated: true, Name: "fString", Typez: genclient.STRING_TYPE},
{Repeated: true, Name: "fSInt64", Typez: genclient.INT64_TYPE},
{Repeated: true, Name: "fSUInt64", Typez: genclient.UINT64_TYPE},
{Repeated: true, Name: "fBool", Typez: genclient.BOOL_TYPE, TypezID: "bool"},
{Repeated: true, Name: "fInt64", Typez: genclient.INT64_TYPE, TypezID: "int64"},
{Repeated: true, Name: "fInt32", Typez: genclient.INT32_TYPE, TypezID: "int32"},
{Repeated: true, Name: "fUInt32", Typez: genclient.UINT32_TYPE, TypezID: "uint32"},
{Repeated: true, Name: "fString", Typez: genclient.STRING_TYPE, TypezID: "string"},
{Repeated: true, Name: "fSInt64", Typez: genclient.INT64_TYPE, TypezID: "int64"},
{Repeated: true, Name: "fSUInt64", Typez: genclient.UINT64_TYPE, TypezID: "uint64"},
{Repeated: true, Name: "fDuration", Typez: genclient.MESSAGE_TYPE, TypezID: ".google.protobuf.Duration"},
{Repeated: true, Name: "fTimestamp", Typez: genclient.MESSAGE_TYPE, TypezID: ".google.protobuf.Timestamp"},
{Repeated: true, Name: "fFieldMask", Typez: genclient.MESSAGE_TYPE, TypezID: ".google.protobuf.FieldMask"},
{Repeated: true, Name: "fBytes", Typez: genclient.BYTES_TYPE},
{Repeated: true, Name: "fBytes", Typez: genclient.BYTES_TYPE, TypezID: "bytes"},
},
})
}
Expand Down Expand Up @@ -214,18 +228,21 @@ func TestMakeAPI(t *testing.T) {
Name: "name",
Documentation: "Resource name for the location, which may vary between implementations.",
Typez: genclient.STRING_TYPE,
TypezID: "string",
Optional: true,
},
{
Name: "locationId",
Documentation: `The canonical id for this location.`,
Typez: genclient.STRING_TYPE,
TypezID: "string",
Optional: true,
},
{
Name: "displayName",
Documentation: `The friendly name for this location, typically a nearby city name.`,
Typez: genclient.STRING_TYPE,
TypezID: "string",
Optional: true,
},
{
Expand Down Expand Up @@ -257,6 +274,7 @@ func TestMakeAPI(t *testing.T) {
Name: "nextPageToken",
Documentation: "The standard List next-page token.",
Typez: genclient.STRING_TYPE,
TypezID: "string",
Optional: true,
},
},
Expand Down

0 comments on commit 97e05e6

Please sign in to comment.