Skip to content

Commit

Permalink
consider arbitrary json for empty types and improve cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
hgiasac committed Jun 9, 2024
1 parent 47b83ad commit 3f179af
Show file tree
Hide file tree
Showing 14 changed files with 158 additions and 733 deletions.
7 changes: 7 additions & 0 deletions command/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type ConvertCommandArguments struct {
Output string `help:"The location where the ndc schema file will be generated. Print to stdout if not set" short:"o"`
Spec string `help:"The API specification of the file, is one of oas3 (openapi3), oas2 (openapi2)"`
Format string `help:"The output format, is one of json, yaml. If the output is set, automatically detect the format in the output file extension" default:"json"`
Strict bool `help:"Require strict validation" default:"false"`
Pure bool `help:"Return the pure NDC schema only" default:"false"`
TrimPrefix string `help:"Trim the prefix in URL, e.g. /v1"`
EnvPrefix string `help:"The environment variable prefix for security values, e.g. PET_STORE"`
Expand All @@ -42,6 +43,7 @@ func CommandConvertToNDCSchema(args *ConvertCommandArguments, logger *slog.Logge
slog.String("env_prefix", args.EnvPrefix),
slog.Any("patch_before", args.PatchBefore),
slog.Any("patch_after", args.PatchAfter),
slog.Bool("strict", args.Strict),
slog.Bool("pure", args.Pure),
)

Expand Down Expand Up @@ -127,6 +129,7 @@ type ConvertConfig struct {
TrimPrefix string `json:"trimPrefix" yaml:"trimPrefix"`
EnvPrefix string `json:"envPrefix" yaml:"envPrefix"`
Pure bool `json:"pure" yaml:"pure"`
Strict bool `json:"strict" yaml:"strict"`
PatchBefore []utils.PatchConfig `json:"patchBefore" yaml:"patchBefore"`
PatchAfter []utils.PatchConfig `json:"patchAfter" yaml:"patchAfter"`
Output string `json:"output" yaml:"output"`
Expand All @@ -151,6 +154,7 @@ func ConvertToNDCSchema(config *ConvertConfig, logger *slog.Logger) (*schema.NDC
MethodAlias: config.MethodAlias,
TrimPrefix: config.TrimPrefix,
EnvPrefix: config.EnvPrefix,
Strict: config.Strict,
Logger: logger,
}
switch config.Spec {
Expand Down Expand Up @@ -193,6 +197,9 @@ func ResolveConvertConfigArguments(config *ConvertConfig, configDir string, args
if args.Pure {
config.Pure = args.Pure
}
if args.Strict {
config.Strict = args.Strict
}
}
if config.Spec == "" {
config.Spec = schema.OAS3Spec
Expand Down
183 changes: 99 additions & 84 deletions openapi/internal/oas2.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ func (oc *OAS2Builder) getSchemaTypeFromProxy(schemaProxy *base.SchemaProxy, nul
if schemaProxy == nil {
return nil, nil, errParameterSchemaEmpty(fieldPaths)
}

innerSchema := schemaProxy.Schema()
if innerSchema == nil {
return nil, nil, fmt.Errorf("cannot get schema from proxy: %s", schemaProxy.GetReference())
Expand Down Expand Up @@ -249,28 +250,29 @@ func (oc *OAS2Builder) getSchemaTypeFromProxy(schemaProxy *base.SchemaProxy, nul
// get and convert an OpenAPI data type to a NDC type from parameter
func (oc *OAS2Builder) getSchemaTypeFromParameter(param *v2.Parameter, apiPath string, fieldPaths []string) (schema.TypeEncoder, error) {

if param.Type == "" {
return nil, errParameterSchemaEmpty(fieldPaths)
}

var result schema.TypeEncoder
if isPrimitiveScalar(param.Type) {
if param.Type == "" {
if oc.Strict {
return nil, errParameterSchemaEmpty(fieldPaths)
}
result = oc.buildScalarJSON()
} else if isPrimitiveScalar(param.Type) {
scalarName := getScalarFromType(oc.schema, []string{param.Type}, param.Format, param.Enum, oc.trimPathPrefix(apiPath), fieldPaths)
result = schema.NewNamedType(scalarName)
oc.typeUsageCounter.Increase(scalarName)
} else {
switch param.Type {
case "object":
return nil, errors.New("unsupported object parameter")
case "array":
if param.Items == nil && param.Items.Type == "" {
return nil, errors.New("array item is empty")
if oc.Strict {
return nil, errors.New("array item is empty")
}
result = schema.NewArrayType(oc.buildScalarJSON())
} else {
itemName := getScalarFromType(oc.schema, []string{param.Items.Type}, param.Format, param.Enum, oc.trimPathPrefix(apiPath), fieldPaths)
result = schema.NewArrayType(schema.NewNamedType(itemName))
}

itemName := getScalarFromType(oc.schema, []string{param.Items.Type}, param.Format, param.Enum, oc.trimPathPrefix(apiPath), fieldPaths)
result = schema.NewArrayType(schema.NewNamedType(itemName))
oc.typeUsageCounter.Increase(itemName)

default:
return nil, fmt.Errorf("unsupported schema type %s", param.Type)
}
Expand All @@ -295,94 +297,98 @@ func (oc *OAS2Builder) getSchemaType(typeSchema *base.Schema, apiPath string, fi
if _, ok := oc.schema.ScalarTypes[scalarName]; !ok {
oc.schema.ScalarTypes[scalarName] = *defaultScalarTypes[rest.ScalarJSON]
}
oc.typeUsageCounter.Increase(scalarName)
typeResult = createSchemaFromOpenAPISchema(typeSchema, scalarName)
return schema.NewNamedType(scalarName), typeResult, nil
}

if len(typeSchema.Type) == 0 {
return nil, nil, errParameterSchemaEmpty(fieldPaths)
}

var result schema.TypeEncoder
typeName := typeSchema.Type[0]
if isPrimitiveScalar(typeName) {
scalarName := getScalarFromType(oc.schema, typeSchema.Type, typeSchema.Format, typeSchema.Enum, oc.trimPathPrefix(apiPath), fieldPaths)
result = schema.NewNamedType(scalarName)
oc.typeUsageCounter.Increase(scalarName)
typeResult = createSchemaFromOpenAPISchema(typeSchema, scalarName)
if len(typeSchema.Type) == 0 {
if oc.Strict {
return nil, nil, errParameterSchemaEmpty(fieldPaths)
}
result = oc.buildScalarJSON()
typeResult = createSchemaFromOpenAPISchema(typeSchema, string(rest.ScalarJSON))
} else {

typeResult = createSchemaFromOpenAPISchema(typeSchema, "")
typeResult.Type = typeName
switch typeName {
case "object":
refName := utils.StringSliceToPascalCase(fieldPaths)

if typeSchema.Properties == nil || typeSchema.Properties.IsZero() {
// treat no-property objects as a JSON scalar
oc.schema.ScalarTypes[refName] = *defaultScalarTypes[rest.ScalarJSON]
} else {
object := schema.ObjectType{
Fields: make(schema.ObjectTypeFields),
}
if typeSchema.Description != "" {
object.Description = &typeSchema.Description
}

typeResult.Properties = make(map[string]rest.TypeSchema)
for prop := typeSchema.Properties.First(); prop != nil; prop = prop.Next() {
propName := prop.Key()
nullable := !slices.Contains(typeSchema.Required, propName)
propType, propApiSchema, err := oc.getSchemaTypeFromProxy(prop.Value(), nullable, apiPath, append(fieldPaths, propName))
if err != nil {
return nil, nil, err
typeName := typeSchema.Type[0]
if isPrimitiveScalar(typeName) {
scalarName := getScalarFromType(oc.schema, typeSchema.Type, typeSchema.Format, typeSchema.Enum, oc.trimPathPrefix(apiPath), fieldPaths)
result = schema.NewNamedType(scalarName)
typeResult = createSchemaFromOpenAPISchema(typeSchema, scalarName)
} else {

typeResult = createSchemaFromOpenAPISchema(typeSchema, "")
typeResult.Type = typeName
switch typeName {
case "object":
refName := utils.StringSliceToPascalCase(fieldPaths)

if typeSchema.Properties == nil || typeSchema.Properties.IsZero() {
// treat no-property objects as a JSON scalar
oc.schema.ScalarTypes[refName] = *defaultScalarTypes[rest.ScalarJSON]
} else {
object := schema.ObjectType{
Fields: make(schema.ObjectTypeFields),
}
objField := schema.ObjectField{
Type: propType.Encode(),
if typeSchema.Description != "" {
object.Description = &typeSchema.Description
}
if propApiSchema.Description != "" {
objField.Description = &propApiSchema.Description

typeResult.Properties = make(map[string]rest.TypeSchema)
for prop := typeSchema.Properties.First(); prop != nil; prop = prop.Next() {
propName := prop.Key()
nullable := !slices.Contains(typeSchema.Required, propName)
propType, propApiSchema, err := oc.getSchemaTypeFromProxy(prop.Value(), nullable, apiPath, append(fieldPaths, propName))
if err != nil {
return nil, nil, err
}
objField := schema.ObjectField{
Type: propType.Encode(),
}
if propApiSchema.Description != "" {
objField.Description = &propApiSchema.Description
}
propApiSchema.Nullable = nullable
typeResult.Properties[propName] = *propApiSchema
object.Fields[propName] = objField

oc.typeUsageCounter.Add(getNamedType(propType, true, ""), 1)
}
propApiSchema.Nullable = nullable
typeResult.Properties[propName] = *propApiSchema
object.Fields[propName] = objField

oc.typeUsageCounter.Increase(getNamedType(propType, true, ""))
oc.schema.ObjectTypes[refName] = object
}

oc.schema.ObjectTypes[refName] = object
}
result = schema.NewNamedType(refName)
case "array":
if typeSchema.Items == nil || typeSchema.Items.A == nil {
return nil, nil, errors.New("array item is empty")
}

itemName := getSchemaRefTypeNameV2(typeSchema.Items.A.GetReference())
if itemName != "" {
itemName := utils.ToPascalCase(itemName)
oc.typeUsageCounter.Increase(itemName)
result = schema.NewArrayType(schema.NewNamedType(itemName))
} else {
itemSchemaA := typeSchema.Items.A.Schema()
if itemSchemaA != nil {
itemSchema, propType, err := oc.getSchemaType(itemSchemaA, apiPath, fieldPaths)
if err != nil {
return nil, nil, err
result = schema.NewNamedType(refName)
case "array":
if typeSchema.Items == nil || typeSchema.Items.A == nil {
if oc.ConvertOptions.Strict {
return nil, nil, errors.New("array item is empty")
}
result = schema.NewArrayType(oc.buildScalarJSON())
} else {
itemName := getSchemaRefTypeNameV2(typeSchema.Items.A.GetReference())
if itemName != "" {
itemName := utils.ToPascalCase(itemName)
result = schema.NewArrayType(schema.NewNamedType(itemName))
} else {
itemSchemaA := typeSchema.Items.A.Schema()
if itemSchemaA != nil {
itemSchema, propType, err := oc.getSchemaType(itemSchemaA, apiPath, fieldPaths)
if err != nil {
return nil, nil, err
}

typeResult.Items = propType
result = schema.NewArrayType(itemSchema)
}
}

typeResult.Items = propType
result = schema.NewArrayType(itemSchema)
oc.typeUsageCounter.Increase(getNamedType(itemSchema, true, ""))
if result == nil {
return nil, nil, fmt.Errorf("cannot parse type reference name: %s", typeSchema.Items.A.GetReference())
}
}
}

if result == nil {
return nil, nil, fmt.Errorf("cannot parse type reference name: %s", typeSchema.Items.A.GetReference())
default:
return nil, nil, fmt.Errorf("unsupported schema type %s", typeName)
}
default:
return nil, nil, fmt.Errorf("unsupported schema type %s", typeName)
}
}

Expand Down Expand Up @@ -411,3 +417,12 @@ func (oc *OAS2Builder) trimPathPrefix(input string) string {
}
return strings.TrimPrefix(input, oc.ConvertOptions.TrimPrefix)
}

// build a named type for JSON scalar
func (oc *OAS2Builder) buildScalarJSON() *schema.NamedType {
scalarName := string(rest.ScalarJSON)
if _, ok := oc.schema.ScalarTypes[scalarName]; !ok {
oc.schema.ScalarTypes[scalarName] = *defaultScalarTypes[rest.ScalarJSON]
}
return schema.NewNamedType(scalarName)
}
8 changes: 4 additions & 4 deletions openapi/internal/oas2_operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ func (oc *oas2OperationBuilder) convertParameters(operation *v2.Operation, apiPa
return nil, err
}

oc.builder.typeUsageCounter.Increase(getNamedType(typeEncoder, true, ""))
oc.builder.typeUsageCounter.Add(getNamedType(typeEncoder, true, ""), 1)
schemaType := typeEncoder.Encode()
argument := schema.ArgumentInfo{
Type: schemaType,
Expand Down Expand Up @@ -240,7 +240,7 @@ func (oc *oas2OperationBuilder) convertParameters(operation *v2.Operation, apiPa
if len(formData.Properties) > 0 {
bodyName := fmt.Sprintf("%sBody", utils.StringSliceToPascalCase(fieldPaths))
oc.builder.schema.ObjectTypes[bodyName] = formDataObject
oc.builder.typeUsageCounter.Increase(bodyName)
oc.builder.typeUsageCounter.Add(bodyName, 1)

desc := fmt.Sprintf("Form data of %s", apiPath)
oc.Arguments["body"] = schema.ArgumentInfo{
Expand Down Expand Up @@ -279,14 +279,14 @@ func (oc *oas2OperationBuilder) convertResponse(responses *v2.Responses, apiPath
// return nullable boolean type if the response content is null
if resp == nil || resp.Schema == nil {
scalarName := string(rest.ScalarBoolean)
oc.builder.typeUsageCounter.Increase(scalarName)
oc.builder.typeUsageCounter.Add(scalarName, 1)
return schema.NewNullableNamedType(scalarName), nil
}

schemaType, _, err := oc.builder.getSchemaTypeFromProxy(resp.Schema, false, apiPath, fieldPaths)
if err != nil {
return nil, err
}
oc.builder.typeUsageCounter.Increase(getNamedType(schemaType, true, ""))
oc.builder.typeUsageCounter.Add(getNamedType(schemaType, true, ""), 1)
return schemaType, nil
}
9 changes: 4 additions & 5 deletions openapi/internal/oas3.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,6 @@ func (oc *OAS3Builder) buildScalarJSON() *schema.NamedType {
if _, ok := oc.schema.ScalarTypes[scalarName]; !ok {
oc.schema.ScalarTypes[scalarName] = *defaultScalarTypes[rest.ScalarJSON]
}
oc.typeUsageCounter.Increase(scalarName)
return schema.NewNamedType(scalarName)
}

Expand Down Expand Up @@ -319,8 +318,8 @@ func (oc *OAS3Builder) populateWriteSchemaType(schemaType schema.Type) (schema.T

writeName := formatWriteObjectName(ty.Name)
if _, ok := oc.schema.ObjectTypes[writeName]; ok {
oc.typeUsageCounter.Increase(writeName)
oc.typeUsageCounter.Decrease(ty.Name)
oc.typeUsageCounter.Add(writeName, 1)
oc.typeUsageCounter.Add(ty.Name, -1)
return schema.NewNamedType(writeName).Encode(), writeName, true
}
if evaluated {
Expand Down Expand Up @@ -349,8 +348,8 @@ func (oc *OAS3Builder) populateWriteSchemaType(schemaType schema.Type) (schema.T
}
}
if hasWriteField {
oc.typeUsageCounter.Increase(writeName)
oc.typeUsageCounter.Decrease(ty.Name)
oc.typeUsageCounter.Add(writeName, 1)
oc.typeUsageCounter.Add(ty.Name, -1)
oc.schema.ObjectTypes[writeName] = writeObject
return schema.NewNamedType(writeName).Encode(), writeName, true
}
Expand Down
8 changes: 4 additions & 4 deletions openapi/internal/oas3_operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func (oc *oas3OperationBuilder) convertRequestBody(reqBody *v3.RequestBody, apiP
return nil, nil, nil
}

oc.builder.typeUsageCounter.Increase(getNamedType(schemaType, true, ""))
oc.builder.typeUsageCounter.Add(getNamedType(schemaType, true, ""), 1)
bodyResult := &rest.RequestBody{
ContentType: contentType,
Schema: typeSchema,
Expand Down Expand Up @@ -301,7 +301,7 @@ func (oc *oas3OperationBuilder) convertRequestBody(reqBody *v3.RequestBody, apiP
EncodingObject: headerEncoding,
}

oc.builder.typeUsageCounter.Increase(getNamedType(ndcType, true, ""))
oc.builder.typeUsageCounter.Add(getNamedType(ndcType, true, ""), 1)
argument := schema.ArgumentInfo{
Type: ndcType.Encode(),
}
Expand Down Expand Up @@ -337,7 +337,7 @@ func (oc *oas3OperationBuilder) convertResponse(responses *v3.Responses, apiPath
// return nullable boolean type if the response content is null
if resp == nil || resp.Content == nil {
scalarName := string(rest.ScalarBoolean)
oc.builder.typeUsageCounter.Increase(scalarName)
oc.builder.typeUsageCounter.Add(scalarName, 1)
return schema.NewNullableNamedType(scalarName), nil
}
jsonContent, ok := resp.Content.Get("application/json")
Expand All @@ -350,6 +350,6 @@ func (oc *oas3OperationBuilder) convertResponse(responses *v3.Responses, apiPath
if err != nil {
return nil, err
}
oc.builder.typeUsageCounter.Increase(getNamedType(schemaType, true, ""))
oc.builder.typeUsageCounter.Add(getNamedType(schemaType, true, ""), 1)
return schemaType, nil
}
Loading

0 comments on commit 3f179af

Please sign in to comment.