From ddf3d328677d3a5cc8998ac53d8a25beff858e81 Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Sat, 21 Dec 2024 07:45:24 +0700 Subject: [PATCH] cli: improve openapi union types conversion (#46) --- connector/internal/client.go | 8 +- connector/internal/request_builder.go | 9 +- connector/internal/request_raw.go | 8 +- connector/testdata/tls/config.yaml | 4 +- .../openapi/internal/oas2_operation.go | 17 +- .../openapi/internal/oas2_schema.go | 383 ++++++----- .../openapi/internal/oas3_operation.go | 20 +- .../openapi/internal/oas3_schema.go | 524 +++++++++------ ndc-http-schema/openapi/internal/types.go | 8 + ndc-http-schema/openapi/internal/utils.go | 129 +++- ndc-http-schema/openapi/oas2_test.go | 8 + ndc-http-schema/openapi/oas3_test.go | 9 + .../openapi/testdata/onesignal/expected.json | 70 +- .../openapi/testdata/onesignal/schema.json | 43 +- .../openapi/testdata/openai/expected.json | 600 +++++++++++++++++- .../openapi/testdata/openai/schema.json | 425 ++++++++++++- .../openapi/testdata/openai/source.json | 198 ++++++ .../openapi/testdata/petstore2/expected.json | 72 +-- .../openapi/testdata/petstore2/schema.json | 72 +-- .../openapi/testdata/petstore3/expected.json | 249 +++++++- .../openapi/testdata/petstore3/schema.json | 148 ++++- .../prefix3/expected_multi_words.json | 96 ++- .../prefix3/expected_multi_words.schema.json | 59 +- .../prefix3/expected_single_word.json | 96 ++- .../prefix3/expected_single_word.schema.json | 59 +- .../openapi/testdata/union2/expected.json | 298 +++++++++ .../openapi/testdata/union2/schema.json | 190 ++++++ .../openapi/testdata/union2/source.json | 115 ++++ .../openapi/testdata/union3/expected.json | 293 +++++++++ .../openapi/testdata/union3/schema.json | 190 ++++++ .../openapi/testdata/union3/source.json | 122 ++++ ndc-http-schema/utils/http.go | 32 + ndc-http-schema/utils/slice.go | 16 + 33 files changed, 3999 insertions(+), 571 deletions(-) create mode 100644 ndc-http-schema/openapi/testdata/union2/expected.json create mode 100644 ndc-http-schema/openapi/testdata/union2/schema.json create mode 100644 ndc-http-schema/openapi/testdata/union2/source.json create mode 100644 ndc-http-schema/openapi/testdata/union3/expected.json create mode 100644 ndc-http-schema/openapi/testdata/union3/schema.json create mode 100644 ndc-http-schema/openapi/testdata/union3/source.json create mode 100644 ndc-http-schema/utils/http.go diff --git a/connector/internal/client.go b/connector/internal/client.go index 366a05c..66b3a10 100644 --- a/connector/internal/client.go +++ b/connector/internal/client.go @@ -376,14 +376,14 @@ func (client *HTTPClient) evalHTTPResponse(ctx context.Context, span trace.Span, var result any switch { - case strings.HasPrefix(contentType, "text/") || strings.HasPrefix(contentType, "image/svg"): + case restUtils.IsContentTypeText(contentType): respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } result = string(respBody) - case contentType == rest.ContentTypeXML || strings.HasSuffix(contentType, "+xml"): + case restUtils.IsContentTypeXML(contentType): field, extractErr := client.extractResultType(resultType) if extractErr != nil { return nil, nil, extractErr @@ -394,7 +394,7 @@ func (client *HTTPClient) evalHTTPResponse(ctx context.Context, span trace.Span, if err != nil { return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } - case contentType == rest.ContentTypeJSON || strings.HasSuffix(contentType, "+json"): + case restUtils.IsContentTypeJSON(contentType): if len(resultType) > 0 { namedType, err := resultType.AsNamed() if err == nil && namedType.Name == string(rest.ScalarString) { @@ -445,7 +445,7 @@ func (client *HTTPClient) evalHTTPResponse(ctx context.Context, span trace.Span, } result = results - case strings.HasPrefix(contentType, "application/") || strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/"): + case restUtils.IsContentTypeBinary(contentType): rawBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) diff --git a/connector/internal/request_builder.go b/connector/internal/request_builder.go index df24acf..6215c49 100644 --- a/connector/internal/request_builder.go +++ b/connector/internal/request_builder.go @@ -12,6 +12,7 @@ import ( "github.com/hasura/ndc-http/connector/internal/contenttype" rest "github.com/hasura/ndc-http/ndc-http-schema/schema" + restUtils "github.com/hasura/ndc-http/ndc-http-schema/utils" "github.com/hasura/ndc-sdk-go/schema" "github.com/hasura/ndc-sdk-go/utils" ) @@ -111,13 +112,13 @@ func (c *RequestBuilder) buildRequestBody(request *RetryableRequest, rawRequest request.Body = r return nil - case strings.HasPrefix(contentType, "text/"): + case restUtils.IsContentTypeText(contentType): r := bytes.NewReader([]byte(fmt.Sprint(bodyData))) request.ContentLength = r.Size() request.Body = r return nil - case strings.HasPrefix(contentType, "multipart/"): + case restUtils.IsContentTypeMultipartForm(contentType): r, contentType, err := contenttype.NewMultipartFormEncoder(c.Schema, c.Operation, c.Arguments).Encode(bodyData) if err != nil { return err @@ -137,7 +138,7 @@ func (c *RequestBuilder) buildRequestBody(request *RetryableRequest, rawRequest request.ContentLength = size return nil - case contentType == rest.ContentTypeJSON || contentType == "" || strings.HasSuffix(contentType, "+json"): + case contentType == "" || restUtils.IsContentTypeJSON(contentType): var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) @@ -150,7 +151,7 @@ func (c *RequestBuilder) buildRequestBody(request *RetryableRequest, rawRequest request.Body = bytes.NewReader(buf.Bytes()) return nil - case contentType == rest.ContentTypeXML || strings.HasSuffix(contentType, "+xml"): + case restUtils.IsContentTypeXML(contentType): bodyBytes, err := contenttype.NewXMLEncoder(c.Schema).Encode(&bodyInfo, bodyData) if err != nil { return err diff --git a/connector/internal/request_raw.go b/connector/internal/request_raw.go index 2c38194..a947ab6 100644 --- a/connector/internal/request_raw.go +++ b/connector/internal/request_raw.go @@ -208,13 +208,13 @@ func (rqe *RawRequestBuilder) decodeArguments() (*RetryableRequest, error) { func (rqe *RawRequestBuilder) evalRequestBody(rawBody json.RawMessage, contentType string) (io.ReadSeeker, string, int64, error) { switch { - case contentType == rest.ContentTypeJSON || strings.HasSuffix(contentType, "+json"): + case restUtils.IsContentTypeJSON(contentType): if !json.Valid(rawBody) { return nil, "", 0, fmt.Errorf("invalid json body: %s", string(rawBody)) } return bytes.NewReader(rawBody), contentType, int64(len(rawBody)), nil - case contentType == rest.ContentTypeXML || strings.HasSuffix(contentType, "+xml"): + case restUtils.IsContentTypeXML(contentType): var bodyData any if err := json.Unmarshal(rawBody, &bodyData); err != nil { return nil, "", 0, fmt.Errorf("invalid body: %w", err) @@ -230,14 +230,14 @@ func (rqe *RawRequestBuilder) evalRequestBody(rawBody json.RawMessage, contentTy } return bytes.NewReader(bodyBytes), contentType, int64(len(bodyBytes)), nil - case strings.HasPrefix(contentType, "text/"): + case restUtils.IsContentTypeText(contentType): var bodyData string if err := json.Unmarshal(rawBody, &bodyData); err != nil { return nil, "", 0, fmt.Errorf("invalid body: %w", err) } return strings.NewReader(bodyData), contentType, int64(len(bodyData)), nil - case strings.HasPrefix(contentType, "multipart/"): + case restUtils.IsContentTypeMultipartForm(contentType): var bodyData any if err := json.Unmarshal(rawBody, &bodyData); err != nil { return nil, "", 0, fmt.Errorf("invalid body: %w", err) diff --git a/connector/testdata/tls/config.yaml b/connector/testdata/tls/config.yaml index 8ffee8b..14133d7 100644 --- a/connector/testdata/tls/config.yaml +++ b/connector/testdata/tls/config.yaml @@ -15,7 +15,7 @@ files: value: 10 retry: times: - value: 1 + value: 2 delay: - value: 500 + value: 1000 httpStatus: [429, 500, 501, 502] diff --git a/ndc-http-schema/openapi/internal/oas2_operation.go b/ndc-http-schema/openapi/internal/oas2_operation.go index e347cb8..1d1bede 100644 --- a/ndc-http-schema/openapi/internal/oas2_operation.go +++ b/ndc-http-schema/openapi/internal/oas2_operation.go @@ -3,6 +3,7 @@ package internal import ( "fmt" "log/slog" + "net/http" "slices" "strconv" "strings" @@ -325,13 +326,19 @@ func (oc *oas2OperationBuilder) convertResponse(operation *v2.Operation, fieldPa // return nullable boolean type if the response content is null if resp == nil || resp.Schema == nil { - scalarName := rest.ScalarJSON - if statusCode == 204 { - scalarName = rest.ScalarBoolean + if statusCode == http.StatusNoContent { + scalarName := rest.ScalarBoolean + oc.builder.schema.AddScalar(string(scalarName), *defaultScalarTypes[scalarName]) + + return schema.NewNullableNamedType(string(scalarName)), response, nil } - oc.builder.schema.AddScalar(string(scalarName), *defaultScalarTypes[scalarName]) - return schema.NewNullableNamedType(string(scalarName)), response, nil + if contentType != "" { + scalarName := guessScalarResultTypeFromContentType(contentType) + oc.builder.schema.AddScalar(string(scalarName), *defaultScalarTypes[scalarName]) + + return schema.NewNamedType(string(scalarName)), response, nil + } } if resp.Schema == nil { diff --git a/ndc-http-schema/openapi/internal/oas2_schema.go b/ndc-http-schema/openapi/internal/oas2_schema.go index 5f5d084..ea034e1 100644 --- a/ndc-http-schema/openapi/internal/oas2_schema.go +++ b/ndc-http-schema/openapi/internal/oas2_schema.go @@ -2,6 +2,7 @@ package internal import ( "fmt" + "log/slog" "slices" "strconv" "strings" @@ -71,150 +72,166 @@ func (oc *oas2SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ return nil, nil, errParameterSchemaEmpty(fieldPaths) } - description := utils.StripHTMLTags(typeSchema.Description) - nullable := typeSchema.Nullable != nil && *typeSchema.Nullable if len(typeSchema.AllOf) > 0 { - enc, ty, err := oc.buildAllOfAnyOfSchemaType(typeSchema.AllOf, nullable, fieldPaths) - if err != nil { - return nil, nil, err - } - if ty != nil && description != "" { - ty.Description = description - } - - return enc, ty, nil + return oc.buildUnionSchemaType(typeSchema, typeSchema.AllOf, oasAllOf, fieldPaths) } if len(typeSchema.AnyOf) > 0 { - enc, ty, err := oc.buildAllOfAnyOfSchemaType(typeSchema.AnyOf, true, fieldPaths) - if err != nil { - return nil, nil, err + return oc.buildUnionSchemaType(typeSchema, typeSchema.AnyOf, oasAnyOf, fieldPaths) + } + + if len(typeSchema.OneOf) > 0 { + return oc.buildUnionSchemaType(typeSchema, typeSchema.OneOf, oasOneOf, fieldPaths) + } + + var result schema.TypeEncoder + if len(typeSchema.Type) == 0 { + if oc.builder.Strict { + return nil, nil, errParameterSchemaEmpty(fieldPaths) } - if ty != nil && description != "" { - ty.Description = description + result = oc.builder.buildScalarJSON() + if typeSchema.Nullable != nil && *typeSchema.Nullable { + result = schema.NewNullableType(result) } - return enc, ty, nil + return result, createSchemaFromOpenAPISchema(typeSchema), nil } - oneOfLength := len(typeSchema.OneOf) - if oneOfLength == 1 { - enc, ty, err := oc.getSchemaTypeFromProxy(typeSchema.OneOf[0], nullable, fieldPaths) - if err != nil { - return nil, nil, err - } - if ty != nil && description != "" { - ty.Description = description + if len(typeSchema.Type) > 1 || isPrimitiveScalar(typeSchema.Type) { + scalarName, nullable := getScalarFromType(oc.builder.schema, typeSchema.Type, typeSchema.Format, typeSchema.Enum, oc.trimPathPrefix(oc.apiPath), fieldPaths) + result = schema.NewNamedType(scalarName) + if nullable || (typeSchema.Nullable != nil && *typeSchema.Nullable) { + result = schema.NewNullableType(result) } - return enc, ty, nil + return result, createSchemaFromOpenAPISchema(typeSchema), nil } - var typeResult *rest.TypeSchema - var result schema.TypeEncoder + typeName := typeSchema.Type[0] + switch typeName { + case "object": + return oc.evalObjectType(typeSchema, false, fieldPaths) + case "array": + typeResult := createSchemaFromOpenAPISchema(typeSchema) + nullable := (typeSchema.Nullable != nil && *typeSchema.Nullable) + if typeSchema.Items == nil || typeSchema.Items.A == nil { + if oc.builder.ConvertOptions.Strict { + return nil, nil, fmt.Errorf("%s: array item is empty", strings.Join(fieldPaths, ".")) + } - if len(typeSchema.Type) == 0 { - if oc.builder.Strict { - return nil, nil, errParameterSchemaEmpty(fieldPaths) + result = oc.builder.buildScalarJSON() + if nullable { + result = schema.NewNullableType(result) + } + + return result, typeResult, nil } - result = oc.builder.buildScalarJSON() - typeResult = createSchemaFromOpenAPISchema(typeSchema) - } else { - typeName := typeSchema.Type[0] - if isPrimitiveScalar(typeSchema.Type) { - scalarName, isNull := getScalarFromType(oc.builder.schema, typeSchema.Type, typeSchema.Format, typeSchema.Enum, oc.trimPathPrefix(oc.apiPath), fieldPaths) - result = schema.NewNamedType(scalarName) - typeResult = createSchemaFromOpenAPISchema(typeSchema) - nullable = nullable || isNull + + itemName := getSchemaRefTypeNameV2(typeSchema.Items.A.GetReference()) + if itemName != "" { + itemName := utils.ToPascalCase(itemName) + result = schema.NewArrayType(schema.NewNamedType(itemName)) } else { - typeResult = createSchemaFromOpenAPISchema(typeSchema) - switch typeName { - case "object": - refName := utils.StringSliceToPascalCase(fieldPaths) - - if typeSchema.Properties == nil || typeSchema.Properties.IsZero() { - // treat no-property objects as a JSON scalar - oc.builder.schema.ScalarTypes[refName] = *defaultScalarTypes[rest.ScalarJSON] - } else { - xmlSchema := typeResult.XML - if xmlSchema == nil { - xmlSchema = &rest.XMLSchema{} - } - - if xmlSchema.Name == "" { - xmlSchema.Name = fieldPaths[0] - } - object := rest.ObjectType{ - Fields: make(map[string]rest.ObjectField), - XML: xmlSchema, - } - if description != "" { - object.Description = &description - } - - 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, append(fieldPaths, propName)) - if err != nil { - return nil, nil, err - } - - objField := rest.ObjectField{ - ObjectField: schema.ObjectField{ - Type: propType.Encode(), - }, - HTTP: propApiSchema, - } - if propApiSchema.Description != "" { - objField.Description = &propApiSchema.Description - } - object.Fields[propName] = objField - } - - if isXMLLeafObject(object) { - object.Fields[xmlValueFieldName] = xmlValueField - } - oc.builder.schema.ObjectTypes[refName] = object - } - result = schema.NewNamedType(refName) - case "array": - if typeSchema.Items == nil || typeSchema.Items.A == nil { - if oc.builder.ConvertOptions.Strict { - return nil, nil, fmt.Errorf("%s: array item is empty", strings.Join(fieldPaths, ".")) - } - result = schema.NewArrayType(oc.builder.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, fieldPaths) - if err != nil { - return nil, nil, err - } - - typeResult.Items = propType - result = schema.NewArrayType(itemSchema) - } - } - - if result == nil { - return nil, nil, fmt.Errorf("cannot parse type reference name: %s", typeSchema.Items.A.GetReference()) - } + itemSchemaA := typeSchema.Items.A.Schema() + if itemSchemaA != nil { + itemSchema, propType, err := oc.getSchemaType(itemSchemaA, fieldPaths) + if err != nil { + return nil, nil, err } - default: - return nil, nil, fmt.Errorf("unsupported schema type %s", typeName) + typeResult.Items = propType + result = schema.NewArrayType(itemSchema) } } + + if result == nil { + return nil, nil, fmt.Errorf("cannot parse type reference name: %s", typeSchema.Items.A.GetReference()) + } + + if nullable { + return schema.NewNullableType(result), typeResult, nil + } + + return result, typeResult, nil + default: + return nil, nil, fmt.Errorf("unsupported schema type %s", typeName) } +} - if nullable { - return schema.NewNullableType(result), typeResult, nil +func (oc *oas2SchemaBuilder) evalObjectType(baseSchema *base.Schema, forcePropertiesNullable bool, fieldPaths []string) (schema.TypeEncoder, *rest.TypeSchema, error) { + typeResult := createSchemaFromOpenAPISchema(baseSchema) + refName := utils.StringSliceToPascalCase(fieldPaths) + + if baseSchema.Properties == nil || baseSchema.Properties.IsZero() { + // treat no-property objects as a JSON scalar + var scalarType schema.TypeEncoder = oc.builder.buildScalarJSON() + if baseSchema.Nullable != nil && *baseSchema.Nullable { + scalarType = schema.NewNullableType(scalarType) + } + + return scalarType, typeResult, nil + } + + xmlSchema := typeResult.XML + if xmlSchema == nil { + xmlSchema = &rest.XMLSchema{} + } + + if xmlSchema.Name == "" { + xmlSchema.Name = fieldPaths[0] + } + object := rest.ObjectType{ + Fields: make(map[string]rest.ObjectField), + XML: xmlSchema, + } + if typeResult.Description != "" { + object.Description = &typeResult.Description + } + + for prop := baseSchema.Properties.First(); prop != nil; prop = prop.Next() { + propName := prop.Key() + oc.builder.Logger.Debug( + "property", + slog.String("name", propName), + slog.Any("field", fieldPaths)) + + nullable := forcePropertiesNullable || !slices.Contains(baseSchema.Required, propName) + propType, propApiSchema, err := oc.getSchemaTypeFromProxy(prop.Value(), nullable, append(fieldPaths, propName)) + if err != nil { + return nil, nil, err + } + + if propType == nil { + continue + } + + objField := rest.ObjectField{ + ObjectField: schema.ObjectField{ + Type: propType.Encode(), + }, + HTTP: propApiSchema, + } + + if propApiSchema == nil { + propApiSchema = &rest.TypeSchema{ + Type: []string{}, + } + } + + if propApiSchema.Description != "" { + objField.Description = &propApiSchema.Description + } + + object.Fields[propName] = objField + } + + if isXMLLeafObject(object) { + object.Fields[xmlValueFieldName] = xmlValueField + } + oc.builder.schema.ObjectTypes[refName] = object + var result schema.TypeEncoder = schema.NewNamedType(refName) + if baseSchema.Nullable != nil && *baseSchema.Nullable { + result = schema.NewNullableType(result) } return result, typeResult, nil @@ -287,43 +304,79 @@ func (oc *oas2SchemaBuilder) getSchemaTypeFromProxy(schemaProxy *base.SchemaProx } // Support converting allOf and anyOf to object types with merge strategy -func (oc *oas2SchemaBuilder) buildAllOfAnyOfSchemaType(schemaProxies []*base.SchemaProxy, nullable bool, fieldPaths []string) (schema.TypeEncoder, *rest.TypeSchema, error) { +func (oc *oas2SchemaBuilder) buildUnionSchemaType(baseSchema *base.Schema, schemaProxies []*base.SchemaProxy, unionType oasUnionType, fieldPaths []string) (schema.TypeEncoder, *rest.TypeSchema, error) { proxies, mergedType, isNullable := evalSchemaProxiesSlice(schemaProxies, oc.location) - nullable = nullable || isNullable - + nullable := isNullable || (baseSchema.Nullable != nil && *baseSchema.Nullable) if mergedType != nil { - return oc.getSchemaType(mergedType, fieldPaths) - } + typeEncoder, typeSchema, err := oc.getSchemaType(mergedType, fieldPaths) + if err != nil { + return nil, nil, err + } + if typeSchema != nil && typeSchema.Description == "" && baseSchema.Description != "" { + typeSchema.Description = utils.StripHTMLTags(baseSchema.Description) + } - if len(proxies) == 1 { - return oc.getSchemaTypeFromProxy(proxies[0], nullable, fieldPaths) + return typeEncoder, typeSchema, nil } - readObject := rest.ObjectType{ - Fields: map[string]rest.ObjectField{}, - } - writeObject := rest.ObjectType{ - Fields: map[string]rest.ObjectField{}, + + switch len(proxies) { + case 0: + if len(baseSchema.Type) > 1 || isPrimitiveScalar(baseSchema.Type) { + scalarName, nullable := getScalarFromType(oc.builder.schema, baseSchema.Type, baseSchema.Format, baseSchema.Enum, oc.trimPathPrefix(oc.apiPath), fieldPaths) + var result schema.TypeEncoder = schema.NewNamedType(scalarName) + if nullable { + result = schema.NewNullableType(result) + } + + return result, createSchemaFromOpenAPISchema(baseSchema), nil + } + + if len(baseSchema.Type) == 1 && baseSchema.Type[0] == "object" { + return oc.evalObjectType(baseSchema, true, fieldPaths) + } + + return schema.NewNamedType(string(rest.ScalarJSON)), createSchemaFromOpenAPISchema(baseSchema), nil + case 1: + typeEncoder, typeSchema, err := oc.getSchemaTypeFromProxy(proxies[0], nullable, fieldPaths) + if err != nil { + return nil, nil, err + } + if typeSchema != nil && typeSchema.Description == "" && baseSchema.Description != "" { + typeSchema.Description = utils.StripHTMLTags(baseSchema.Description) + } + + return typeEncoder, typeSchema, nil } + typeSchema := &rest.TypeSchema{ Type: []string{"object"}, } + if baseSchema.Description != "" { + typeSchema.Description = utils.StripHTMLTags(baseSchema.Description) + } + + var readObjectItems []rest.ObjectType + var writeObjectItems []rest.ObjectType + for i, item := range proxies { - enc, ty, err := oc.getSchemaTypeFromProxy(item, nullable, append(fieldPaths, strconv.Itoa(i))) + enc, ty, err := newOAS2SchemaBuilder(oc.builder, oc.apiPath, oc.location). + getSchemaTypeFromProxy(item, nullable, append(fieldPaths, strconv.Itoa(i))) if err != nil { return nil, nil, err } - name := getNamedType(enc, true, "") - writeName := formatWriteObjectName(name) - isObject := !isPrimitiveScalar(ty.Type) && !slices.Contains(ty.Type, "array") + var readObj rest.ObjectType + name := getNamedType(enc, false, "") + isObject := name != "" || !isPrimitiveScalar(ty.Type) && !slices.Contains(ty.Type, "array") if isObject { - if _, ok := oc.builder.schema.ScalarTypes[name]; ok { - isObject = false + readObj, isObject = oc.builder.schema.ObjectTypes[name] + if isObject { + readObjectItems = append(readObjectItems, readObj) } } + if !isObject { - // TODO: should we keep the original anyOf or allOf type schema ty = &rest.TypeSchema{ Description: ty.Description, Type: []string{}, @@ -332,31 +385,33 @@ func (oc *oas2SchemaBuilder) buildAllOfAnyOfSchemaType(schemaProxies []*base.Sch return oc.builder.buildScalarJSON(), ty, nil } - readObj, ok := oc.builder.schema.ObjectTypes[name] - if ok { - if readObject.Description == nil && readObj.Description != nil { - readObject.Description = readObj.Description - if ty.Description == "" { - ty.Description = *readObj.Description - } - } - for k, v := range readObj.Fields { - if _, ok := readObject.Fields[k]; !ok { - readObject.Fields[k] = v - } - } - } + writeName := formatWriteObjectName(name) writeObj, ok := oc.builder.schema.ObjectTypes[writeName] - if ok { - if writeObject.Description == nil && writeObj.Description != nil { - writeObject.Description = writeObj.Description - } - for k, v := range writeObj.Fields { - if _, ok := writeObject.Fields[k]; !ok { - writeObject.Fields[k] = v - } - } + if !ok { + writeObj = readObj } + + writeObjectItems = append(writeObjectItems, writeObj) + } + + readObject := rest.ObjectType{ + Fields: map[string]rest.ObjectField{}, + } + writeObject := rest.ObjectType{ + Fields: map[string]rest.ObjectField{}, + } + + if baseSchema.Description != "" { + readObject.Description = &baseSchema.Description + writeObject.Description = &baseSchema.Description + } + + if err := mergeUnionObjects(oc.builder.schema, &readObject, readObjectItems, unionType, fieldPaths); err != nil { + return nil, nil, err + } + + if err := mergeUnionObjects(oc.builder.schema, &writeObject, writeObjectItems, unionType, fieldPaths); err != nil { + return nil, nil, err } refName := utils.ToPascalCase(strings.Join(fieldPaths, " ")) diff --git a/ndc-http-schema/openapi/internal/oas3_operation.go b/ndc-http-schema/openapi/internal/oas3_operation.go index d5bec34..ce8bdb2 100644 --- a/ndc-http-schema/openapi/internal/oas3_operation.go +++ b/ndc-http-schema/openapi/internal/oas3_operation.go @@ -3,6 +3,7 @@ package internal import ( "fmt" "log/slog" + "net/http" "slices" "strconv" "strings" @@ -387,7 +388,7 @@ func (oc *oas3OperationBuilder) convertResponse(responses *v3.Responses, apiPath // return nullable JSON type if the response content is null if resp == nil || resp.Content == nil { scalarName := rest.ScalarJSON - if statusCode == 204 { + if statusCode == http.StatusNoContent { scalarName = rest.ScalarBoolean } oc.builder.schema.AddScalar(string(scalarName), *defaultScalarTypes[scalarName]) @@ -399,15 +400,24 @@ func (oc *oas3OperationBuilder) convertResponse(responses *v3.Responses, apiPath contentType, bodyContent := oc.getContentType(resp.Content) if bodyContent == nil { - if statusCode == 204 { - scalarName := string(rest.ScalarBoolean) - oc.builder.schema.AddScalar(scalarName, *defaultScalarTypes[rest.ScalarBoolean]) + if statusCode == http.StatusNoContent { + scalarName := rest.ScalarBoolean + oc.builder.schema.AddScalar(string(scalarName), *defaultScalarTypes[scalarName]) - return schema.NewNullableNamedType(scalarName), &rest.Response{ + return schema.NewNullableNamedType(string(scalarName)), &rest.Response{ ContentType: rest.ContentTypeJSON, }, nil } + if contentType != "" { + scalarName := guessScalarResultTypeFromContentType(contentType) + oc.builder.schema.AddScalar(string(scalarName), *defaultScalarTypes[scalarName]) + + return schema.NewNamedType(string(scalarName)), &rest.Response{ + ContentType: contentType, + }, nil + } + return nil, nil, nil } diff --git a/ndc-http-schema/openapi/internal/oas3_schema.go b/ndc-http-schema/openapi/internal/oas3_schema.go index 321ac24..6545bb6 100644 --- a/ndc-http-schema/openapi/internal/oas3_schema.go +++ b/ndc-http-schema/openapi/internal/oas3_schema.go @@ -105,48 +105,20 @@ func (oc *oas3SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ return nil, nil, nil } - description := utils.StripHTMLTags(typeSchema.Description) - nullable := typeSchema.Nullable != nil && *typeSchema.Nullable if len(typeSchema.AllOf) > 0 { - enc, ty, err := oc.buildAllOfAnyOfSchemaType(typeSchema.AllOf, nullable, fieldPaths) - if err != nil { - return nil, nil, err - } - if ty != nil { - ty.Description = description - } - - return enc, ty, nil + return oc.buildUnionSchemaType(typeSchema, typeSchema.AllOf, oasAllOf, fieldPaths) } if len(typeSchema.AnyOf) > 0 { - enc, ty, err := oc.buildAllOfAnyOfSchemaType(typeSchema.AnyOf, true, fieldPaths) - if err != nil { - return nil, nil, err - } - if ty != nil { - ty.Description = description - } - - return enc, ty, nil + return oc.buildUnionSchemaType(typeSchema, typeSchema.AnyOf, oasAnyOf, fieldPaths) } - oneOfLength := len(typeSchema.OneOf) - if oneOfLength == 1 { - enc, ty, err := oc.getSchemaTypeFromProxy(typeSchema.OneOf[0], nullable, fieldPaths) - if err != nil { - return nil, nil, err - } - if ty != nil { - ty.Description = description - } - - return enc, ty, nil + if len(typeSchema.OneOf) > 0 { + return oc.buildUnionSchemaType(typeSchema, typeSchema.OneOf, oasOneOf, fieldPaths) } - typeResult := createSchemaFromOpenAPISchema(typeSchema) - if oneOfLength > 0 || (typeSchema.AdditionalProperties != nil && (typeSchema.AdditionalProperties.B || typeSchema.AdditionalProperties.A != nil)) { - return oc.builder.buildScalarJSON(), typeResult, nil + if typeSchema.AdditionalProperties != nil && (typeSchema.AdditionalProperties.B || typeSchema.AdditionalProperties.A != nil) { + return oc.builder.buildScalarJSON(), createSchemaFromOpenAPISchema(typeSchema), nil } var result schema.TypeEncoder @@ -154,132 +126,43 @@ func (oc *oas3SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ if oc.builder.Strict { return nil, nil, errParameterSchemaEmpty(fieldPaths) } + result = oc.builder.buildScalarJSON() + if typeSchema.Nullable != nil && *typeSchema.Nullable { + result = schema.NewNullableType(result) + } - return result, typeResult, nil + return result, createSchemaFromOpenAPISchema(typeSchema), nil } if len(typeSchema.Type) > 1 || isPrimitiveScalar(typeSchema.Type) { scalarName, nullable := getScalarFromType(oc.builder.schema, typeSchema.Type, typeSchema.Format, typeSchema.Enum, oc.builder.trimPathPrefix(oc.apiPath), fieldPaths) result = schema.NewNamedType(scalarName) - if nullable { + if nullable || (typeSchema.Nullable != nil && *typeSchema.Nullable) { result = schema.NewNullableType(result) } - return result, typeResult, nil + return result, createSchemaFromOpenAPISchema(typeSchema), nil } typeName := typeSchema.Type[0] switch typeName { case "object": - refName := utils.StringSliceToPascalCase(fieldPaths) - if typeSchema.Properties == nil || typeSchema.Properties.IsZero() { - if typeSchema.AdditionalProperties != nil && (typeSchema.AdditionalProperties.A == nil || !typeSchema.AdditionalProperties.B) { - return nil, nil, nil - } - // treat no-property objects as a JSON scalar - return oc.builder.buildScalarJSON(), typeResult, nil - } - - object := rest.ObjectType{ - Fields: make(map[string]rest.ObjectField), - XML: typeResult.XML, - } - readObject := rest.ObjectType{ - Fields: make(map[string]rest.ObjectField), - XML: typeResult.XML, - } - writeObject := rest.ObjectType{ - Fields: make(map[string]rest.ObjectField), - XML: typeResult.XML, - } - - if description != "" { - object.Description = &description - readObject.Description = &description - writeObject.Description = &description - } - - for prop := typeSchema.Properties.First(); prop != nil; prop = prop.Next() { - propName := prop.Key() - oc.builder.Logger.Debug( - "property", - slog.String("name", propName), - slog.Any("field", fieldPaths)) - nullable := !slices.Contains(typeSchema.Required, propName) - propType, propApiSchema, err := oc.getSchemaTypeFromProxy(prop.Value(), nullable, append(fieldPaths, propName)) - if err != nil { - return nil, nil, err - } - - if propType == nil { - continue - } - - objField := rest.ObjectField{ - ObjectField: schema.ObjectField{ - Type: propType.Encode(), - }, - HTTP: propApiSchema, - } - - if propApiSchema == nil { - propApiSchema = &rest.TypeSchema{ - Type: []string{}, - } - } - - if propApiSchema.Description != "" { - objField.Description = &propApiSchema.Description - } - - switch { - case !propApiSchema.ReadOnly && !propApiSchema.WriteOnly: - object.Fields[propName] = objField - case !oc.writeMode && propApiSchema.ReadOnly: - readObject.Fields[propName] = objField - default: - writeObject.Fields[propName] = objField - } - } - - if len(readObject.Fields) == 0 && len(writeObject.Fields) == 0 { - if len(object.Fields) > 0 && isXMLLeafObject(object) { - object.Fields[xmlValueFieldName] = xmlValueField - } - - oc.builder.schema.ObjectTypes[refName] = object - result = schema.NewNamedType(refName) - } else { - for key, field := range object.Fields { - readObject.Fields[key] = field - writeObject.Fields[key] = field - } - - if len(readObject.Fields) > 0 && isXMLLeafObject(readObject) { - readObject.Fields[xmlValueFieldName] = xmlValueField - } - - if len(writeObject.Fields) > 0 && isXMLLeafObject(writeObject) { - writeObject.Fields[xmlValueFieldName] = xmlValueField - } - - writeRefName := formatWriteObjectName(refName) - oc.builder.schema.ObjectTypes[refName] = readObject - oc.builder.schema.ObjectTypes[writeRefName] = writeObject - if oc.writeMode { - result = schema.NewNamedType(writeRefName) - } else { - result = schema.NewNamedType(refName) - } - } + return oc.evalObjectType(typeSchema, false, fieldPaths) case "array": + typeResult := createSchemaFromOpenAPISchema(typeSchema) + nullable := (typeSchema.Nullable != nil && *typeSchema.Nullable) if typeSchema.Items == nil || typeSchema.Items.A == nil { if oc.builder.Strict { return nil, nil, fmt.Errorf("%s: array item is empty", strings.Join(fieldPaths, ".")) } - return oc.builder.buildScalarJSON(), typeResult, nil + var result schema.TypeEncoder = oc.builder.buildScalarJSON() + if nullable { + result = schema.NewNullableType(result) + } + + return result, typeResult, nil } itemName := getSchemaRefTypeNameV3(typeSchema.Items.A.GetReference()) @@ -305,83 +188,243 @@ func (oc *oas3SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ if result == nil { return nil, nil, fmt.Errorf("cannot parse type reference name: %s", typeSchema.Items.A.GetReference()) } + + if nullable { + result = schema.NewNullableType(result) + } + + return result, typeResult, nil default: return nil, nil, fmt.Errorf("unsupported schema type %s", typeName) } - - return result, typeResult, nil } -// Support converting allOf and anyOf to object types with merge strategy -func (oc *oas3SchemaBuilder) buildAllOfAnyOfSchemaType(schemaProxies []*base.SchemaProxy, nullable bool, fieldPaths []string) (schema.TypeEncoder, *rest.TypeSchema, error) { - proxies, mergedType, isNullable := evalSchemaProxiesSlice(schemaProxies, oc.location) - nullable = nullable || isNullable +func (oc *oas3SchemaBuilder) evalObjectType(baseSchema *base.Schema, forcePropertiesNullable bool, fieldPaths []string) (schema.TypeEncoder, *rest.TypeSchema, error) { + typeResult := createSchemaFromOpenAPISchema(baseSchema) + refName := utils.StringSliceToPascalCase(fieldPaths) + if baseSchema.Properties == nil || baseSchema.Properties.IsZero() { + if baseSchema.AdditionalProperties != nil && (baseSchema.AdditionalProperties.A == nil || !baseSchema.AdditionalProperties.B) { + return nil, nil, nil + } + // treat no-property objects as a JSON scalar + var scalarType schema.TypeEncoder = oc.builder.buildScalarJSON() + if baseSchema.Nullable != nil && *baseSchema.Nullable { + scalarType = schema.NewNullableType(scalarType) + } - if mergedType != nil { - return oc.getSchemaType(mergedType, fieldPaths) + return scalarType, typeResult, nil } - if len(proxies) == 1 { - return oc.getSchemaTypeFromProxy(proxies[0], nullable, fieldPaths) + + var result schema.TypeEncoder + object := rest.ObjectType{ + Fields: make(map[string]rest.ObjectField), + XML: typeResult.XML, } readObject := rest.ObjectType{ - Fields: map[string]rest.ObjectField{}, + Fields: make(map[string]rest.ObjectField), + XML: typeResult.XML, } writeObject := rest.ObjectType{ - Fields: map[string]rest.ObjectField{}, + Fields: make(map[string]rest.ObjectField), + XML: typeResult.XML, + } + + if typeResult.Description != "" { + object.Description = &typeResult.Description + readObject.Description = &typeResult.Description + writeObject.Description = &typeResult.Description } + + for prop := baseSchema.Properties.First(); prop != nil; prop = prop.Next() { + propName := prop.Key() + oc.builder.Logger.Debug( + "property", + slog.String("name", propName), + slog.Any("field", fieldPaths)) + nullable := forcePropertiesNullable || !slices.Contains(baseSchema.Required, propName) + propType, propApiSchema, err := oc.getSchemaTypeFromProxy(prop.Value(), nullable, append(fieldPaths, propName)) + if err != nil { + return nil, nil, err + } + + if propType == nil { + continue + } + + objField := rest.ObjectField{ + ObjectField: schema.ObjectField{ + Type: propType.Encode(), + }, + HTTP: propApiSchema, + } + + if propApiSchema == nil { + propApiSchema = &rest.TypeSchema{ + Type: []string{}, + } + } + + if propApiSchema.Description != "" { + objField.Description = &propApiSchema.Description + } + + switch { + case !propApiSchema.ReadOnly && !propApiSchema.WriteOnly: + object.Fields[propName] = objField + case !oc.writeMode && propApiSchema.ReadOnly: + readObject.Fields[propName] = objField + default: + writeObject.Fields[propName] = objField + } + } + + writeRefName := formatWriteObjectName(refName) + if len(readObject.Fields) == 0 && len(writeObject.Fields) == 0 { + if len(object.Fields) > 0 && isXMLLeafObject(object) { + object.Fields[xmlValueFieldName] = xmlValueField + } + + oc.builder.schema.ObjectTypes[refName] = object + result = schema.NewNamedType(refName) + } else { + for key, field := range object.Fields { + readObject.Fields[key] = field + writeObject.Fields[key] = field + } + + if len(readObject.Fields) > 0 && isXMLLeafObject(readObject) { + readObject.Fields[xmlValueFieldName] = xmlValueField + } + + if len(writeObject.Fields) > 0 && isXMLLeafObject(writeObject) { + writeObject.Fields[xmlValueFieldName] = xmlValueField + } + + oc.builder.schema.ObjectTypes[refName] = readObject + oc.builder.schema.ObjectTypes[writeRefName] = writeObject + if oc.writeMode { + result = schema.NewNamedType(writeRefName) + } else { + result = schema.NewNamedType(refName) + } + } + + if baseSchema.Nullable != nil && *baseSchema.Nullable { + result = schema.NewNullableType(result) + } + + return result, typeResult, nil +} + +// Support converting oneOf, allOf or anyOf to object types with merge strategy +func (oc *oas3SchemaBuilder) buildUnionSchemaType(baseSchema *base.Schema, schemaProxies []*base.SchemaProxy, unionType oasUnionType, fieldPaths []string) (schema.TypeEncoder, *rest.TypeSchema, error) { + proxies, mergedType, isNullable := evalSchemaProxiesSlice(schemaProxies, oc.location) + nullable := isNullable || (baseSchema.Nullable != nil && *baseSchema.Nullable) + if mergedType != nil { + typeEncoder, typeSchema, err := oc.getSchemaType(mergedType, fieldPaths) + if err != nil { + return nil, nil, err + } + if typeSchema != nil && typeSchema.Description == "" && baseSchema.Description != "" { + typeSchema.Description = utils.StripHTMLTags(baseSchema.Description) + } + + return typeEncoder, typeSchema, nil + } + + switch len(proxies) { + case 0: + if len(baseSchema.Type) > 1 || isPrimitiveScalar(baseSchema.Type) { + scalarName, nullable := getScalarFromType(oc.builder.schema, baseSchema.Type, baseSchema.Format, baseSchema.Enum, oc.builder.trimPathPrefix(oc.apiPath), fieldPaths) + var result schema.TypeEncoder = schema.NewNamedType(scalarName) + if nullable { + result = schema.NewNullableType(result) + } + + return result, createSchemaFromOpenAPISchema(baseSchema), nil + } + + if len(baseSchema.Type) == 1 && baseSchema.Type[0] == "object" { + return oc.evalObjectType(baseSchema, true, fieldPaths) + } + + return schema.NewNamedType(string(rest.ScalarJSON)), createSchemaFromOpenAPISchema(baseSchema), nil + case 1: + typeEncoder, typeSchema, err := oc.getSchemaTypeFromProxy(proxies[0], nullable, fieldPaths) + if err != nil { + return nil, nil, err + } + if typeSchema != nil && typeSchema.Description == "" && baseSchema.Description != "" { + typeSchema.Description = utils.StripHTMLTags(baseSchema.Description) + } + + return typeEncoder, typeSchema, nil + } + typeSchema := &rest.TypeSchema{ Type: []string{"object"}, } + if baseSchema.Description != "" { + typeSchema.Description = utils.StripHTMLTags(baseSchema.Description) + } + + var readObjectItems []rest.ObjectType + var writeObjectItems []rest.ObjectType + for i, item := range proxies { - enc, ty, err := oc.getSchemaTypeFromProxy(item, nullable, append(fieldPaths, strconv.Itoa(i))) + enc, ty, err := newOAS3SchemaBuilder(oc.builder, oc.apiPath, oc.location, false). + getSchemaTypeFromProxy(item, nullable, append(fieldPaths, strconv.Itoa(i))) if err != nil { return nil, nil, err } - name := getNamedType(enc, true, "") - writeName := formatWriteObjectName(name) - isObject := !isPrimitiveScalar(ty.Type) && !slices.Contains(ty.Type, "array") + var readObj rest.ObjectType + name := getNamedType(enc, false, "") + isObject := name != "" && !isPrimitiveScalar(ty.Type) && !slices.Contains(ty.Type, "array") if isObject { - if _, ok := oc.builder.schema.ScalarTypes[name]; ok { - isObject = false + readObj, isObject = oc.builder.schema.ObjectTypes[name] + if isObject { + readObjectItems = append(readObjectItems, readObj) } } + if !isObject { - // TODO: should we keep the original anyOf or allOf type schema ty = &rest.TypeSchema{ - Description: ty.Description, + Description: typeSchema.Description, Type: []string{}, } return oc.builder.buildScalarJSON(), ty, nil } - readObj, ok := oc.builder.schema.ObjectTypes[name] - if ok { - if readObject.Description == nil && readObj.Description != nil { - readObject.Description = readObj.Description - if ty.Description == "" { - ty.Description = *readObj.Description - } - } - for k, v := range readObj.Fields { - if _, ok := readObject.Fields[k]; !ok { - readObject.Fields[k] = v - } - } - } + writeName := formatWriteObjectName(name) writeObj, ok := oc.builder.schema.ObjectTypes[writeName] - if ok { - if writeObject.Description == nil && writeObj.Description != nil { - writeObject.Description = writeObj.Description - } - for k, v := range writeObj.Fields { - if _, ok := writeObject.Fields[k]; !ok { - writeObject.Fields[k] = v - } - } + if !ok { + writeObj = readObj } + + writeObjectItems = append(writeObjectItems, writeObj) + } + + readObject := rest.ObjectType{ + Fields: map[string]rest.ObjectField{}, + } + writeObject := rest.ObjectType{ + Fields: map[string]rest.ObjectField{}, + } + + if baseSchema.Description != "" { + readObject.Description = &baseSchema.Description + writeObject.Description = &baseSchema.Description + } + + if err := mergeUnionObjects(oc.builder.schema, &readObject, readObjectItems, unionType, fieldPaths); err != nil { + return nil, nil, err + } + + if err := mergeUnionObjects(oc.builder.schema, &writeObject, writeObjectItems, unionType, fieldPaths); err != nil { + return nil, nil, err } refName := utils.ToPascalCase(strings.Join(fieldPaths, " ")) @@ -399,3 +442,120 @@ func (oc *oas3SchemaBuilder) buildAllOfAnyOfSchemaType(schemaProxies []*base.Sch return schema.NewNamedType(refName), typeSchema, nil } + +type unionSiblingField struct { + Type schema.TypeEncoder + EnumOneOf []string + Description *string + HTTP *rest.TypeSchema +} + +// Find common fields in all objects to merge the type. +// If they have the same type, we don't need to wrap it with the nullable type. +func mergeUnionObjects(httpSchema *rest.NDCHttpSchema, dest *rest.ObjectType, srcObjects []rest.ObjectType, unionType oasUnionType, fieldPaths []string) error { + objectItemLength := len(srcObjects) + siblingFields := make(map[string]unionSiblingField) + for i, object := range srcObjects { + if i >= objectItemLength-1 { + break + } + + for key, field := range object.Fields { + siblingField, siblingFieldExist := siblingFields[key] + nextField, ok := srcObjects[i+1].Fields[key] + + if !ok { + if siblingFieldExist && unionType != oasAllOf && !isNullableType(siblingField.Type) { + siblingField.Type = schema.NewNullableType(siblingField.Type) + siblingFields[key] = siblingField + } + + continue + } + + newField, ok := mergeUnionTypes(httpSchema, field.Type, nextField.Type, append(fieldPaths, key)) + switch { + case ok: + usField := unionSiblingField{ + Type: newField.Type, + EnumOneOf: append(siblingField.EnumOneOf, newField.EnumOneOf...), + } + + switch { + case siblingFieldExist && siblingField.Description != nil: + usField.Description = siblingField.Description + case field.Description != nil: + usField.Description = field.Description + case nextField.Description != nil: + usField.Description = nextField.Description + } + + switch { + case len(newField.EnumOneOf) > 0: + usField.HTTP = &rest.TypeSchema{ + Type: []string{"string"}, + } + case siblingFieldExist && siblingField.HTTP != nil: + usField.HTTP = siblingField.HTTP + case field.HTTP != nil: + usField.HTTP = field.HTTP + case nextField.HTTP != nil: + usField.HTTP = nextField.HTTP + } + + siblingFields[key] = usField + case siblingFieldExist: + newField, _ = mergeUnionTypes(httpSchema, siblingField.Type.Encode(), nextField.Type, append(fieldPaths, key)) + siblingFields[key] = unionSiblingField{ + Type: newField.Type, + } + default: + siblingFields[key] = *newField + } + } + } + + for key, field := range siblingFields { + fieldType := field.Type + if len(field.EnumOneOf) > 0 { + newScalar := schema.NewScalarType() + newScalar.Representation = schema.NewTypeRepresentationEnum(utils.SliceUnique(field.EnumOneOf)).Encode() + + newName := utils.StringSliceToPascalCase(append(fieldPaths, key, "Enum")) + httpSchema.ScalarTypes[newName] = *newScalar + + var err error + fieldType, err = replaceNamedType(field.Type.Encode(), newName) + if err != nil { + return fmt.Errorf("%s: failed to replace named type, %w", strings.Join(append(fieldPaths, key), "."), err) + } + } + + dest.Fields[key] = rest.ObjectField{ + ObjectField: schema.ObjectField{ + Description: field.Description, + Type: fieldType.Encode(), + }, + HTTP: field.HTTP, + } + } + + for _, objectItem := range srcObjects { + for key, field := range objectItem.Fields { + if _, ok := siblingFields[key]; ok { + continue + } + + // In anyOf and oneOf union objects, the API only requires one of union objects, other types are optional. + // Because the NDC spec hasn't supported union types yet we make all properties optional to enable autocompletion. + iType := field.Type.Interface() + if unionType != oasAllOf && !isNullableType(iType) { + field.ObjectField.Type = schema.NewNullableType(iType).Encode() + } + + dest.Fields[key] = field + } + } + + return nil +} diff --git a/ndc-http-schema/openapi/internal/types.go b/ndc-http-schema/openapi/internal/types.go index 6d1f96a..e647f1c 100644 --- a/ndc-http-schema/openapi/internal/types.go +++ b/ndc-http-schema/openapi/internal/types.go @@ -143,3 +143,11 @@ type ConvertOptions struct { NoDeprecation bool Logger *slog.Logger } + +type oasUnionType string + +const ( + oasOneOf oasUnionType = "oneOf" + oasAnyOf oasUnionType = "anyOf" + oasAllOf oasUnionType = "allOf" +) diff --git a/ndc-http-schema/openapi/internal/utils.go b/ndc-http-schema/openapi/internal/utils.go index 041c542..f5cb536 100644 --- a/ndc-http-schema/openapi/internal/utils.go +++ b/ndc-http-schema/openapi/internal/utils.go @@ -335,6 +335,98 @@ func isNullableType(input schema.TypeEncoder) bool { return ok } +func mergeUnionTypes(httpSchema *rest.NDCHttpSchema, a schema.Type, b schema.Type, fieldPaths []string) (*unionSiblingField, bool) { + switch at := a.Interface().(type) { + case *schema.NullableType: + bt, err := b.AsNullable() + buType := b + if err == nil { + buType = bt.UnderlyingType + } + + ut, ok := mergeUnionTypes(httpSchema, at.UnderlyingType, buType, fieldPaths) + if !ok { + return &unionSiblingField{ + Type: schema.NewNullableType(schema.NewNamedType(string(rest.ScalarJSON))), + }, false + } + + ut.Type = schema.NewNullableType(ut.Type) + + return ut, true + case *schema.ArrayType: + bt, err := b.AsArray() + if err != nil { + return &unionSiblingField{ + Type: schema.NewNullableType(schema.NewNamedType(string(rest.ScalarJSON))), + }, false + } + + ut, ok := mergeUnionTypes(httpSchema, at.ElementType, bt.ElementType, fieldPaths) + if !ok { + return &unionSiblingField{ + Type: schema.NewArrayType(schema.NewNamedType(string(rest.ScalarJSON))), + }, false + } + + ut.Type = schema.NewArrayType(ut.Type) + + return ut, true + case *schema.NamedType: + bt, err := b.AsNamed() + if err != nil { + return &unionSiblingField{ + Type: schema.NewNamedType(string(rest.ScalarJSON)), + }, false + } + + // if both types are enum scalars, a new enum scalar is created with the merged value set of both enums. + var enumA, enumB *schema.TypeRepresentationEnum + scalarA, ok := httpSchema.ScalarTypes[at.Name] + if ok { + enumA, _ = scalarA.Representation.AsEnum() + } + + scalarB, ok := httpSchema.ScalarTypes[bt.Name] + if ok { + enumB, _ = scalarB.Representation.AsEnum() + } + + if at.Name == bt.Name { + result := &unionSiblingField{ + Type: at, + } + if enumA != nil { + result.EnumOneOf = enumA.OneOf + } + + return result, true + } + + if enumA == nil || enumB == nil { + return &unionSiblingField{ + Type: schema.NewNamedType(string(rest.ScalarJSON)), + }, false + } + + enumValues := append(enumA.OneOf, enumB.OneOf...) + newScalar := schema.NewScalarType() + newScalar.Representation = schema.NewTypeRepresentationEnum(enumValues).Encode() + + newName := utils.StringSliceToPascalCase(append(fieldPaths, "Enum")) + "_" + strings.Join(enumValues, "_") + httpSchema.ScalarTypes[newName] = *newScalar + + return &unionSiblingField{ + Type: schema.NewNamedType(newName), + EnumOneOf: enumValues, + }, true + } + + return &unionSiblingField{ + Type: schema.NewNullableType(schema.NewNamedType(string(rest.ScalarJSON))), + }, false +} + // encodeHeaderArgumentName encodes header key to NDC schema field name func encodeHeaderArgumentName(name string) string { return "header" + utils.ToPascalCase(name) @@ -507,7 +599,7 @@ func evalOperationPath(httpSchema *rest.NDCHttpSchema, rawPath string, arguments } } - var newQuery url.Values + newQuery := url.Values{} q := pathURL.Query() for key, value := range q { if len(value) == 0 || value[0] == "" { @@ -560,3 +652,38 @@ func evalOperationPath(httpSchema *rest.NDCHttpSchema, rawPath string, arguments return pathURL.Path + queryString + fragment, arguments, nil } + +func guessScalarResultTypeFromContentType(contentType string) rest.ScalarName { + ct := strings.TrimSpace(strings.Split(contentType, ";")[0]) + switch { + case utils.IsContentTypeJSON(ct) || utils.IsContentTypeXML(ct) || ct == rest.ContentTypeNdJSON: + return rest.ScalarJSON + case utils.IsContentTypeText(ct): + return rest.ScalarString + default: + return rest.ScalarBinary + } +} + +func replaceNamedType(schemaType schema.Type, name string) (schema.TypeEncoder, error) { + switch t := schemaType.Interface().(type) { + case *schema.NullableType: + newType, err := replaceNamedType(t.UnderlyingType, name) + if err != nil { + return nil, err + } + + return schema.NewNullableType(newType), nil + case *schema.ArrayType: + newType, err := replaceNamedType(t.ElementType, name) + if err != nil { + return nil, err + } + + return schema.NewArrayType(newType), nil + case *schema.NamedType: + return schema.NewNamedType(name), nil + default: + return nil, fmt.Errorf("invalid type: %v", schemaType) + } +} diff --git a/ndc-http-schema/openapi/oas2_test.go b/ndc-http-schema/openapi/oas2_test.go index 13461b9..d116055 100644 --- a/ndc-http-schema/openapi/oas2_test.go +++ b/ndc-http-schema/openapi/oas2_test.go @@ -60,6 +60,14 @@ func TestOpenAPIv2ToRESTSchema(t *testing.T) { Prefix: "hasura_mock_json", }, }, + // go run ./ndc-http-schema convert -f ./ndc-http-schema/openapi/testdata/union2/source.json -o ./ndc-http-schema/openapi/testdata/union2/expected.json --spec oas2 + // go run ./ndc-http-schema convert -f ./ndc-http-schema/openapi/testdata/union2/source.json -o ./ndc-http-schema/openapi/testdata/union2/schema.json --pure --spec oas2 + { + Name: "union2", + Source: "testdata/union2/source.json", + Expected: "testdata/union2/expected.json", + Schema: "testdata/union2/schema.json", + }, } for _, tc := range testCases { diff --git a/ndc-http-schema/openapi/oas3_test.go b/ndc-http-schema/openapi/oas3_test.go index a06fde8..dca9829 100644 --- a/ndc-http-schema/openapi/oas3_test.go +++ b/ndc-http-schema/openapi/oas3_test.go @@ -72,6 +72,15 @@ func TestOpenAPIv3ToRESTSchema(t *testing.T) { Prefix: "hasura_one_signal", }, }, + // go run ./ndc-http-schema convert -f ./ndc-http-schema/openapi/testdata/union3/source.json -o ./ndc-http-schema/openapi/testdata/union3/expected.json --spec openapi3 + // go run ./ndc-http-schema convert -f ./ndc-http-schema/openapi/testdata/union3/source.json -o ./ndc-http-schema/openapi/testdata/union3/schema.json --pure --spec openapi3 + { + Name: "union", + Source: "testdata/union3/source.json", + Expected: "testdata/union3/expected.json", + Schema: "testdata/union3/schema.json", + Options: ConvertOptions{}, + }, } for _, tc := range testCases { diff --git a/ndc-http-schema/openapi/testdata/onesignal/expected.json b/ndc-http-schema/openapi/testdata/onesignal/expected.json index c9e313b..75345b3 100644 --- a/ndc-http-schema/openapi/testdata/onesignal/expected.json +++ b/ndc-http-schema/openapi/testdata/onesignal/expected.json @@ -206,7 +206,7 @@ } }, "http": { - "type": null + "type": [] } }, "external_id": { @@ -533,6 +533,28 @@ ] } }, + "excluded_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + }, "filters": { "type": { "type": "nullable", @@ -578,6 +600,28 @@ ] } }, + "included_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + }, "send_after": { "type": { "type": "nullable", @@ -606,6 +650,20 @@ "object" ] } + }, + "target_channel": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "PlayerNotificationTargetTargetChannel", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } } } }, @@ -705,7 +763,7 @@ } }, "converted": { - "description": "Number of messages that were clicked.", + "description": "Number of users who have clicked / tapped on your notification.", "type": { "type": "nullable", "underlying_type": { @@ -720,7 +778,7 @@ } }, "errored": { - "description": "Number of errors reported.", + "description": "Number of notifications that could not be delivered due to an error. You can find more information by viewing the notification in the dashboard.", "type": { "type": "nullable", "underlying_type": { @@ -757,7 +815,7 @@ } }, "failed": { - "description": "Number of messages sent to unsubscribed devices.", + "description": "Number of notifications that could not be delivered due to those devices being unsubscribed.", "type": { "type": "nullable", "underlying_type": { @@ -887,7 +945,7 @@ } }, "received": { - "description": "Number of devices that received the message.", + "description": "Confirmed Deliveries number of devices that received the push notification. Paid Feature Only. Free accounts will see 0.", "type": { "type": "nullable", "underlying_type": { @@ -947,7 +1005,7 @@ } }, "successful": { - "description": "Number of messages delivered to push servers, mobile carriers, or email service providers.", + "description": "Number of notifications that were successfully delivered.", "type": { "type": "nullable", "underlying_type": { diff --git a/ndc-http-schema/openapi/testdata/onesignal/schema.json b/ndc-http-schema/openapi/testdata/onesignal/schema.json index 0bbce12..aea9411 100644 --- a/ndc-http-schema/openapi/testdata/onesignal/schema.json +++ b/ndc-http-schema/openapi/testdata/onesignal/schema.json @@ -315,6 +315,18 @@ } } }, + "excluded_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + }, "filters": { "type": { "type": "nullable", @@ -345,6 +357,18 @@ } } }, + "included_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + }, "send_after": { "type": { "type": "nullable", @@ -362,6 +386,15 @@ "type": "named" } } + }, + "target_channel": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "PlayerNotificationTargetTargetChannel", + "type": "named" + } + } } } }, @@ -430,7 +463,7 @@ } }, "converted": { - "description": "Number of messages that were clicked.", + "description": "Number of users who have clicked / tapped on your notification.", "type": { "type": "nullable", "underlying_type": { @@ -440,7 +473,7 @@ } }, "errored": { - "description": "Number of errors reported.", + "description": "Number of notifications that could not be delivered due to an error. You can find more information by viewing the notification in the dashboard.", "type": { "type": "nullable", "underlying_type": { @@ -462,7 +495,7 @@ } }, "failed": { - "description": "Number of messages sent to unsubscribed devices.", + "description": "Number of notifications that could not be delivered due to those devices being unsubscribed.", "type": { "type": "nullable", "underlying_type": { @@ -546,7 +579,7 @@ } }, "received": { - "description": "Number of devices that received the message.", + "description": "Confirmed Deliveries number of devices that received the push notification. Paid Feature Only. Free accounts will see 0.", "type": { "type": "nullable", "underlying_type": { @@ -585,7 +618,7 @@ } }, "successful": { - "description": "Number of messages delivered to push servers, mobile carriers, or email service providers.", + "description": "Number of notifications that were successfully delivered.", "type": { "type": "nullable", "underlying_type": { diff --git a/ndc-http-schema/openapi/testdata/openai/expected.json b/ndc-http-schema/openapi/testdata/openai/expected.json index afe40d9..206a618 100644 --- a/ndc-http-schema/openapi/testdata/openai/expected.json +++ b/ndc-http-schema/openapi/testdata/openai/expected.json @@ -149,14 +149,134 @@ } } }, + "ChatCompletionRequestAssistantMessageFunctionCall": { + "description": "Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model.", + "fields": { + "arguments": { + "description": "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function.", + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + }, + "name": { + "description": "The name of the function to call.", + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + } + } + }, + "ChatCompletionRequestMessageInput": { + "fields": { + "content": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "function_call": { + "description": "Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "ChatCompletionRequestAssistantMessageFunctionCall", + "type": "named" + } + }, + "http": { + "type": [ + "object" + ] + } + }, + "name": { + "description": "An optional name for the participant. Provides the model information to differentiate between participants of the same role.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "role": { + "description": "The role of the messages author, in this case `system`.", + "type": { + "name": "ChatCompletionRequestMessageRoleEnum", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + }, + "tool_call_id": { + "description": "Tool call that this message is responding to.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "tool_calls": { + "description": "The tool calls generated by the model, such as function calls.", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "ChatCompletionMessageToolCall", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ] + } + } + } + }, "ChatCompletionResponseMessage": { "description": "A chat completion message generated by the model.", "fields": { "content": { "description": "The contents of the message.", "type": { - "name": "String", - "type": "named" + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } }, "http": { "type": [ @@ -265,11 +385,14 @@ "bytes": { "description": "A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token.", "type": { - "element_type": { - "name": "Int32", - "type": "named" - }, - "type": "array" + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "Int32", + "type": "named" + }, + "type": "array" + } }, "http": { "type": [ @@ -333,11 +456,14 @@ "bytes": { "description": "A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token.", "type": { - "element_type": { - "name": "Int32", - "type": "named" - }, - "type": "array" + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "Int32", + "type": "named" + }, + "type": "array" + } }, "http": { "type": [ @@ -403,7 +529,7 @@ } } }, - "CreateChatCompletionRequest": { + "CreateChatCompletionRequestInput": { "fields": { "frequency_penalty": { "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. [See more information about frequency and presence penalties.](/docs/guides/text-generation/parameter-details)", @@ -432,7 +558,7 @@ } }, "http": { - "type": null + "type": [] } }, "functions": { @@ -502,7 +628,7 @@ "description": "A list of messages comprising the conversation so far. [Example Python code](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models).", "type": { "element_type": { - "name": "ChatCompletionRequestMessage", + "name": "ChatCompletionRequestMessageInput", "type": "named" }, "type": "array" @@ -631,7 +757,7 @@ } }, "http": { - "type": null + "type": [] } }, "stream": { @@ -691,7 +817,7 @@ } }, "http": { - "type": null + "type": [] } }, "tools": { @@ -915,8 +1041,11 @@ "logprobs": { "description": "Log probability information for the choice.", "type": { - "name": "CreateChatCompletionResponseChoicesLogprobs", - "type": "named" + "type": "nullable", + "underlying_type": { + "name": "CreateChatCompletionResponseChoicesLogprobs", + "type": "named" + } }, "http": { "type": [ @@ -944,11 +1073,14 @@ "content": { "description": "A list of message content tokens with log probability information.", "type": { - "element_type": { - "name": "ChatCompletionTokenLogprob", - "type": "named" - }, - "type": "array" + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "ChatCompletionTokenLogprob", + "type": "named" + }, + "type": "array" + } }, "http": { "type": [ @@ -977,6 +1109,283 @@ } } }, + "CreateThreadAndRunRequestInput": { + "fields": { + "assistant_id": { + "description": "The ID of the [assistant](/docs/api-reference/assistants) to use to execute this run.", + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + }, + "thread": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestInput", + "type": "named" + } + }, + "http": { + "type": [ + "object" + ] + } + } + } + }, + "CreateThreadRequestInput": { + "fields": { + "tool_resources": { + "description": "A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestToolResourcesInput", + "type": "named" + } + }, + "http": { + "type": [ + "object" + ] + } + } + } + }, + "CreateThreadRequestToolResourcesCodeInterpreter": { + "fields": { + "file_ids": { + "description": "A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool.", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + } + } + }, + "CreateThreadRequestToolResourcesFileSearchInput": { + "fields": { + "vector_store_ids": { + "description": "The [vector store](/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread.", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + }, + "vector_stores": { + "description": "A helper to create a [vector store](/docs/api-reference/vector-stores/object) with file_ids and attach it to this thread. There can be a maximum of 1 vector store attached to the thread.", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "CreateThreadRequestToolResourcesFileSearchVectorStoresInput", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "object" + ] + } + } + } + } + }, + "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategy1Static": { + "fields": { + "chunk_overlap_tokens": { + "description": "The number of tokens that overlap between chunks. The default value is `400`. Note that the overlap must not exceed half of `max_chunk_size_tokens`.", + "type": { + "name": "Int32", + "type": "named" + }, + "http": { + "type": [ + "integer" + ] + } + }, + "max_chunk_size_tokens": { + "description": "The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`.", + "type": { + "name": "Int32", + "type": "named" + }, + "http": { + "type": [ + "integer" + ], + "maximum": 4096, + "minimum": 100 + } + } + } + }, + "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategyInput": { + "description": "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy.", + "fields": { + "static": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategy1Static", + "type": "named" + } + }, + "http": { + "type": [ + "object" + ] + } + }, + "type": { + "description": "Always `auto`.", + "type": { + "name": "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategyTypeEnum", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + } + } + }, + "CreateThreadRequestToolResourcesFileSearchVectorStoresInput": { + "fields": { + "chunking_strategy": { + "description": "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategyInput", + "type": "named" + } + }, + "http": { + "type": [ + "object" + ] + } + }, + "file_ids": { + "description": "A list of [file](/docs/api-reference/files) IDs to add to the vector store. There can be a maximum of 10000 files in a vector store.", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + }, + "metadata": { + "description": "Set of 16 key-value pairs that can be attached to a vector store. This can be useful for storing additional information about the vector store in a structured format. Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + }, + "http": { + "type": [ + "object" + ] + } + } + } + }, + "CreateThreadRequestToolResourcesInput": { + "description": "A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.", + "fields": { + "code_interpreter": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestToolResourcesCodeInterpreter", + "type": "named" + } + }, + "http": { + "type": [ + "object" + ] + } + }, + "file_search": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestToolResourcesFileSearchInput", + "type": "named" + } + }, + "http": { + "type": [ + "object" + ] + } + } + } + }, "FunctionObject": { "fields": { "description": { @@ -1086,6 +1495,86 @@ } } } + }, + "RunObject": { + "description": "Represents an execution run on a [thread](/docs/api-reference/threads).", + "fields": { + "assistant_id": { + "description": "The ID of the [assistant](/docs/api-reference/assistants) used for execution of this run.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "created_at": { + "description": "The Unix timestamp (in seconds) for when the run was created.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ] + } + }, + "id": { + "description": "The identifier, which can be referenced in API endpoints.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "object": { + "description": "The object type, which is always `thread.run`.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RunObjectObject", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "thread_id": { + "description": "The ID of the [thread](/docs/api-reference/threads) that was executed on as a part of this run.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + } + } } }, "procedures": { @@ -1136,7 +1625,7 @@ "body": { "description": "Request body of POST /chat/completions", "type": { - "name": "CreateChatCompletionRequest", + "name": "CreateChatCompletionRequestInput", "type": "named" }, "http": { @@ -1149,6 +1638,35 @@ "name": "CreateChatCompletionResponse", "type": "named" } + }, + "createThreadAndRun": { + "request": { + "url": "/threads/runs", + "method": "post", + "requestBody": { + "contentType": "application/json" + }, + "response": { + "contentType": "application/json" + } + }, + "arguments": { + "body": { + "description": "Request body of POST /threads/runs", + "type": { + "name": "CreateThreadAndRunRequestInput", + "type": "named" + }, + "http": { + "in": "body" + } + } + }, + "description": "Create a thread and run it in one request.", + "result_type": { + "name": "RunObject", + "type": "named" + } } }, "scalar_types": { @@ -1169,11 +1687,18 @@ "type": "enum" } }, - "ChatCompletionRequestMessage": { + "ChatCompletionRequestMessageRoleEnum": { "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "type": "json" + "one_of": [ + "assistant", + "function", + "system", + "tool", + "user" + ], + "type": "enum" } }, "ChatCompletionResponseMessageRole": { @@ -1260,6 +1785,17 @@ "type": "enum" } }, + "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategyTypeEnum": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "auto", + "static" + ], + "type": "enum" + } + }, "Float64": { "aggregate_functions": {}, "comparison_operators": {}, @@ -1288,6 +1824,16 @@ "type": "json" } }, + "RunObjectObject": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "thread.run" + ], + "type": "enum" + } + }, "String": { "aggregate_functions": {}, "comparison_operators": {}, diff --git a/ndc-http-schema/openapi/testdata/openai/schema.json b/ndc-http-schema/openapi/testdata/openai/schema.json index cf0d999..a2c8bdb 100644 --- a/ndc-http-schema/openapi/testdata/openai/schema.json +++ b/ndc-http-schema/openapi/testdata/openai/schema.json @@ -77,14 +77,99 @@ } } }, + "ChatCompletionRequestAssistantMessageFunctionCall": { + "description": "Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model.", + "fields": { + "arguments": { + "description": "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function.", + "type": { + "name": "String", + "type": "named" + } + }, + "name": { + "description": "The name of the function to call.", + "type": { + "name": "String", + "type": "named" + } + } + } + }, + "ChatCompletionRequestMessageInput": { + "fields": { + "content": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "function_call": { + "description": "Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "ChatCompletionRequestAssistantMessageFunctionCall", + "type": "named" + } + } + }, + "name": { + "description": "An optional name for the participant. Provides the model information to differentiate between participants of the same role.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "role": { + "description": "The role of the messages author, in this case `system`.", + "type": { + "name": "ChatCompletionRequestMessageRoleEnum", + "type": "named" + } + }, + "tool_call_id": { + "description": "Tool call that this message is responding to.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "tool_calls": { + "description": "The tool calls generated by the model, such as function calls.", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "ChatCompletionMessageToolCall", + "type": "named" + }, + "type": "array" + } + } + } + } + }, "ChatCompletionResponseMessage": { "description": "A chat completion message generated by the model.", "fields": { "content": { "description": "The contents of the message.", "type": { - "name": "String", - "type": "named" + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } } }, "function_call": { @@ -158,11 +243,14 @@ "bytes": { "description": "A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token.", "type": { - "element_type": { - "name": "Int32", - "type": "named" - }, - "type": "array" + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "Int32", + "type": "named" + }, + "type": "array" + } } }, "logprob": { @@ -196,11 +284,14 @@ "bytes": { "description": "A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token.", "type": { - "element_type": { - "name": "Int32", - "type": "named" - }, - "type": "array" + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "Int32", + "type": "named" + }, + "type": "array" + } } }, "logprob": { @@ -236,7 +327,7 @@ } } }, - "CreateChatCompletionRequest": { + "CreateChatCompletionRequestInput": { "fields": { "frequency_penalty": { "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. [See more information about frequency and presence penalties.](/docs/guides/text-generation/parameter-details)", @@ -305,7 +396,7 @@ "description": "A list of messages comprising the conversation so far. [Example Python code](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models).", "type": { "element_type": { - "name": "ChatCompletionRequestMessage", + "name": "ChatCompletionRequestMessageInput", "type": "named" }, "type": "array" @@ -570,8 +661,11 @@ "logprobs": { "description": "Log probability information for the choice.", "type": { - "name": "CreateChatCompletionResponseChoicesLogprobs", - "type": "named" + "type": "nullable", + "underlying_type": { + "name": "CreateChatCompletionResponseChoicesLogprobs", + "type": "named" + } } }, "message": { @@ -589,11 +683,14 @@ "content": { "description": "A list of message content tokens with log probability information.", "type": { - "element_type": { - "name": "ChatCompletionTokenLogprob", - "type": "named" - }, - "type": "array" + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "ChatCompletionTokenLogprob", + "type": "named" + }, + "type": "array" + } } } } @@ -612,6 +709,186 @@ } } }, + "CreateThreadAndRunRequestInput": { + "fields": { + "assistant_id": { + "description": "The ID of the [assistant](/docs/api-reference/assistants) to use to execute this run.", + "type": { + "name": "String", + "type": "named" + } + }, + "thread": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestInput", + "type": "named" + } + } + } + } + }, + "CreateThreadRequestInput": { + "fields": { + "tool_resources": { + "description": "A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestToolResourcesInput", + "type": "named" + } + } + } + } + }, + "CreateThreadRequestToolResourcesCodeInterpreter": { + "fields": { + "file_ids": { + "description": "A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool.", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + } + } + }, + "CreateThreadRequestToolResourcesFileSearchInput": { + "fields": { + "vector_store_ids": { + "description": "The [vector store](/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread.", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + }, + "vector_stores": { + "description": "A helper to create a [vector store](/docs/api-reference/vector-stores/object) with file_ids and attach it to this thread. There can be a maximum of 1 vector store attached to the thread.", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "CreateThreadRequestToolResourcesFileSearchVectorStoresInput", + "type": "named" + }, + "type": "array" + } + } + } + } + }, + "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategy1Static": { + "fields": { + "chunk_overlap_tokens": { + "description": "The number of tokens that overlap between chunks. The default value is `400`. Note that the overlap must not exceed half of `max_chunk_size_tokens`.", + "type": { + "name": "Int32", + "type": "named" + } + }, + "max_chunk_size_tokens": { + "description": "The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`.", + "type": { + "name": "Int32", + "type": "named" + } + } + } + }, + "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategyInput": { + "description": "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy.", + "fields": { + "static": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategy1Static", + "type": "named" + } + } + }, + "type": { + "description": "Always `auto`.", + "type": { + "name": "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategyTypeEnum", + "type": "named" + } + } + } + }, + "CreateThreadRequestToolResourcesFileSearchVectorStoresInput": { + "fields": { + "chunking_strategy": { + "description": "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategyInput", + "type": "named" + } + } + }, + "file_ids": { + "description": "A list of [file](/docs/api-reference/files) IDs to add to the vector store. There can be a maximum of 10000 files in a vector store.", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + }, + "metadata": { + "description": "Set of 16 key-value pairs that can be attached to a vector store. This can be useful for storing additional information about the vector store in a structured format. Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + } + } + }, + "CreateThreadRequestToolResourcesInput": { + "description": "A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.", + "fields": { + "code_interpreter": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestToolResourcesCodeInterpreter", + "type": "named" + } + } + }, + "file_search": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "CreateThreadRequestToolResourcesFileSearchInput", + "type": "named" + } + } + } + } + }, "FunctionObject": { "fields": { "description": { @@ -686,6 +963,61 @@ } } } + }, + "RunObject": { + "description": "Represents an execution run on a [thread](/docs/api-reference/threads).", + "fields": { + "assistant_id": { + "description": "The ID of the [assistant](/docs/api-reference/assistants) used for execution of this run.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "created_at": { + "description": "The Unix timestamp (in seconds) for when the run was created.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "id": { + "description": "The identifier, which can be referenced in API endpoints.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "object": { + "description": "The object type, which is always `thread.run`.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RunObjectObject", + "type": "named" + } + } + }, + "thread_id": { + "description": "The ID of the [thread](/docs/api-reference/threads) that was executed on as a part of this run.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } } }, "procedures": [ @@ -714,7 +1046,7 @@ "body": { "description": "Request body of POST /chat/completions", "type": { - "name": "CreateChatCompletionRequest", + "name": "CreateChatCompletionRequestInput", "type": "named" } } @@ -725,6 +1057,23 @@ "name": "CreateChatCompletionResponse", "type": "named" } + }, + { + "arguments": { + "body": { + "description": "Request body of POST /threads/runs", + "type": { + "name": "CreateThreadAndRunRequestInput", + "type": "named" + } + } + }, + "description": "Create a thread and run it in one request.", + "name": "createThreadAndRun", + "result_type": { + "name": "RunObject", + "type": "named" + } } ], "scalar_types": { @@ -745,11 +1094,18 @@ "type": "enum" } }, - "ChatCompletionRequestMessage": { + "ChatCompletionRequestMessageRoleEnum": { "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "type": "json" + "one_of": [ + "assistant", + "function", + "system", + "tool", + "user" + ], + "type": "enum" } }, "ChatCompletionResponseMessageRole": { @@ -836,6 +1192,17 @@ "type": "enum" } }, + "CreateThreadRequestToolResourcesFileSearchVectorStoresChunkingStrategyTypeEnum": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "auto", + "static" + ], + "type": "enum" + } + }, "Float64": { "aggregate_functions": {}, "comparison_operators": {}, @@ -864,6 +1231,16 @@ "type": "json" } }, + "RunObjectObject": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "thread.run" + ], + "type": "enum" + } + }, "String": { "aggregate_functions": {}, "comparison_operators": {}, diff --git a/ndc-http-schema/openapi/testdata/openai/source.json b/ndc-http-schema/openapi/testdata/openai/source.json index 20a1367..2104c21 100644 --- a/ndc-http-schema/openapi/testdata/openai/source.json +++ b/ndc-http-schema/openapi/testdata/openai/source.json @@ -138,6 +138,35 @@ ] } } + }, + "/threads/runs": { + "post": { + "operationId": "createThreadAndRun", + "tags": ["Assistants"], + "summary": "Create a thread and run it in one request.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateThreadAndRunRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunObject" + } + } + } + } + } + } } }, "components": { @@ -1061,6 +1090,175 @@ "description": "The completed size of the task" } } + }, + "CreateThreadRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "tool_resources": { + "type": "object", + "description": "A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.\n", + "properties": { + "code_interpreter": { + "type": "object", + "properties": { + "file_ids": { + "type": "array", + "description": "A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool.\n", + "default": [], + "maxItems": 20, + "items": { + "type": "string" + } + } + } + }, + "file_search": { + "type": "object", + "properties": { + "vector_store_ids": { + "type": "array", + "description": "The [vector store](/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread.\n", + "maxItems": 1, + "items": { + "type": "string" + } + }, + "vector_stores": { + "type": "array", + "description": "A helper to create a [vector store](/docs/api-reference/vector-stores/object) with file_ids and attach it to this thread. There can be a maximum of 1 vector store attached to the thread.\n", + "maxItems": 1, + "items": { + "type": "object", + "properties": { + "file_ids": { + "type": "array", + "description": "A list of [file](/docs/api-reference/files) IDs to add to the vector store. There can be a maximum of 10000 files in a vector store.\n", + "maxItems": 10000, + "items": { + "type": "string" + } + }, + "chunking_strategy": { + "type": "object", + "description": "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy.", + "oneOf": [ + { + "type": "object", + "title": "Auto Chunking Strategy", + "description": "The default strategy. This strategy currently uses a `max_chunk_size_tokens` of `800` and `chunk_overlap_tokens` of `400`.", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "Always `auto`.", + "enum": ["auto"] + } + }, + "required": ["type"] + }, + { + "type": "object", + "title": "Static Chunking Strategy", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "Always `static`.", + "enum": ["static"] + }, + "static": { + "type": "object", + "additionalProperties": false, + "properties": { + "max_chunk_size_tokens": { + "type": "integer", + "minimum": 100, + "maximum": 4096, + "description": "The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`." + }, + "chunk_overlap_tokens": { + "type": "integer", + "description": "The number of tokens that overlap between chunks. The default value is `400`.\n\nNote that the overlap must not exceed half of `max_chunk_size_tokens`.\n" + } + }, + "required": [ + "max_chunk_size_tokens", + "chunk_overlap_tokens" + ] + } + }, + "required": ["type", "static"] + } + ], + "x-oaiExpandable": true + }, + "metadata": { + "type": "object", + "description": "Set of 16 key-value pairs that can be attached to a vector store. This can be useful for storing additional information about the vector store in a structured format. Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long.\n", + "x-oaiTypeLabel": "map" + } + }, + "x-oaiExpandable": true + } + } + }, + "oneOf": [ + { + "required": ["vector_store_ids"] + }, + { + "required": ["vector_stores"] + } + ] + } + }, + "nullable": true + } + } + }, + "CreateThreadAndRunRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "assistant_id": { + "description": "The ID of the [assistant](/docs/api-reference/assistants) to use to execute this run.", + "type": "string" + }, + "thread": { + "$ref": "#/components/schemas/CreateThreadRequest", + "description": "If no thread is provided, an empty thread will be created." + } + }, + "required": ["assistant_id"] + }, + "RunObject": { + "type": "object", + "title": "A run on a thread", + "description": "Represents an execution run on a [thread](/docs/api-reference/threads).", + "properties": { + "id": { + "description": "The identifier, which can be referenced in API endpoints.", + "type": "string" + }, + "object": { + "description": "The object type, which is always `thread.run`.", + "type": "string", + "enum": ["thread.run"] + }, + "created_at": { + "description": "The Unix timestamp (in seconds) for when the run was created.", + "type": "integer" + }, + "thread_id": { + "description": "The ID of the [thread](/docs/api-reference/threads) that was executed on as a part of this run.", + "type": "string" + }, + "assistant_id": { + "description": "The ID of the [assistant](/docs/api-reference/assistants) used for execution of this run.", + "type": "string" + } + } } } }, diff --git a/ndc-http-schema/openapi/testdata/petstore2/expected.json b/ndc-http-schema/openapi/testdata/petstore2/expected.json index e97831e..5e082b0 100644 --- a/ndc-http-schema/openapi/testdata/petstore2/expected.json +++ b/ndc-http-schema/openapi/testdata/petstore2/expected.json @@ -442,7 +442,7 @@ "arguments": {}, "description": "Returns pet inventories by status", "result_type": { - "name": "GetInventoryResult", + "name": "JSON", "type": "named" } }, @@ -592,11 +592,8 @@ }, "description": "Explore details about a given subject node", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, "listOAuth2Clients": { @@ -2360,11 +2357,8 @@ }, "description": "Add a new pet to the store", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, "addSnake": { @@ -2411,11 +2405,8 @@ }, "description": "Delete purchase order by ID", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, "deletePet": { @@ -2472,11 +2463,8 @@ }, "description": "Deletes a pet", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, "deleteUser": { @@ -2507,11 +2495,8 @@ }, "description": "Delete user", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, "dynamicClientRegistrationCreateOAuth2Client": { @@ -2625,11 +2610,8 @@ }, "description": "PUT /pet/xml", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, "updatePet": { @@ -2673,11 +2655,8 @@ }, "description": "Update an existing pet", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, "updatePetWithForm": { @@ -2734,11 +2713,8 @@ }, "description": "Updates a pet in the store with form data", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, "updateUser": { @@ -2790,11 +2766,8 @@ }, "description": "Updated user", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, "uploadFile": { @@ -2902,13 +2875,6 @@ "type": "float64" } }, - "GetInventoryResult": { - "aggregate_functions": {}, - "comparison_operators": {}, - "representation": { - "type": "json" - } - }, "Int32": { "aggregate_functions": {}, "comparison_operators": {}, diff --git a/ndc-http-schema/openapi/testdata/petstore2/schema.json b/ndc-http-schema/openapi/testdata/petstore2/schema.json index f487b07..20f2a4f 100644 --- a/ndc-http-schema/openapi/testdata/petstore2/schema.json +++ b/ndc-http-schema/openapi/testdata/petstore2/schema.json @@ -196,7 +196,7 @@ "description": "Returns pet inventories by status", "name": "getInventory", "result_type": { - "name": "GetInventoryResult", + "name": "JSON", "type": "named" } }, @@ -273,11 +273,8 @@ "description": "Explore details about a given subject node", "name": "get_subject", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, { @@ -1351,11 +1348,8 @@ "description": "Add a new pet to the store", "name": "addPet", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, { @@ -1380,11 +1374,8 @@ "description": "Delete purchase order by ID", "name": "deleteOrder", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, { @@ -1409,11 +1400,8 @@ "description": "Deletes a pet", "name": "deletePet", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, { @@ -1429,11 +1417,8 @@ "description": "Delete user", "name": "deleteUser", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, { @@ -1482,11 +1467,8 @@ "description": "PUT /pet/xml", "name": "putPetXml", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, { @@ -1502,11 +1484,8 @@ "description": "Update an existing pet", "name": "updatePet", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, { @@ -1529,11 +1508,8 @@ "description": "Updates a pet in the store with form data", "name": "updatePetWithForm", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, { @@ -1556,11 +1532,8 @@ "description": "Updated user", "name": "updateUser", "result_type": { - "type": "nullable", - "underlying_type": { - "name": "JSON", - "type": "named" - } + "name": "JSON", + "type": "named" } }, { @@ -1634,13 +1607,6 @@ "type": "float64" } }, - "GetInventoryResult": { - "aggregate_functions": {}, - "comparison_operators": {}, - "representation": { - "type": "json" - } - }, "Int32": { "aggregate_functions": {}, "comparison_operators": {}, diff --git a/ndc-http-schema/openapi/testdata/petstore3/expected.json b/ndc-http-schema/openapi/testdata/petstore3/expected.json index 0d0c51b..d6742f5 100644 --- a/ndc-http-schema/openapi/testdata/petstore3/expected.json +++ b/ndc-http-schema/openapi/testdata/petstore3/expected.json @@ -652,7 +652,7 @@ }, "description": "Search in projects or in packages.", "result_type": { - "name": "JSON", + "name": "GetSearchXmlResult", "type": "named" } }, @@ -1331,6 +1331,253 @@ } } }, + "GetSearchXmlResult": { + "fields": { + "matches": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "attribute": true + } + } + }, + "package": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "GetSearchXmlResult1Package", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "object" + ] + } + } + }, + "project": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "GetSearchXmlResult0Project", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "object" + ] + } + } + } + } + }, + "GetSearchXmlResult0Project": { + "fields": { + "description": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "name": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "person": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "GetSearchXmlResult0ProjectPerson", + "type": "named" + } + }, + "http": { + "type": [ + "object" + ] + } + }, + "title": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + } + } + }, + "GetSearchXmlResult0ProjectPerson": { + "fields": { + "role": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "userid": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "xmlValue": { + "description": "Value of the xml field", + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ], + "xml": { + "text": true + } + } + } + } + }, + "GetSearchXmlResult1Package": { + "fields": { + "description": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "name": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "project": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "title": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + } + } + }, "Order": { "fields": { "complete": { diff --git a/ndc-http-schema/openapi/testdata/petstore3/schema.json b/ndc-http-schema/openapi/testdata/petstore3/schema.json index 12bc333..b28f3eb 100644 --- a/ndc-http-schema/openapi/testdata/petstore3/schema.json +++ b/ndc-http-schema/openapi/testdata/petstore3/schema.json @@ -288,7 +288,7 @@ "description": "Search in projects or in packages.", "name": "getSearchXml", "result_type": { - "name": "JSON", + "name": "GetSearchXmlResult", "type": "named" } }, @@ -683,6 +683,152 @@ } } }, + "GetSearchXmlResult": { + "fields": { + "matches": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "package": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "GetSearchXmlResult1Package", + "type": "named" + }, + "type": "array" + } + } + }, + "project": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "GetSearchXmlResult0Project", + "type": "named" + }, + "type": "array" + } + } + } + } + }, + "GetSearchXmlResult0Project": { + "fields": { + "description": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "name": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "person": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "GetSearchXmlResult0ProjectPerson", + "type": "named" + } + } + }, + "title": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, + "GetSearchXmlResult0ProjectPerson": { + "fields": { + "role": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "userid": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "xmlValue": { + "description": "Value of the xml field", + "type": { + "name": "String", + "type": "named" + } + } + } + }, + "GetSearchXmlResult1Package": { + "fields": { + "description": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "name": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "project": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "title": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, "Order": { "fields": { "complete": { diff --git a/ndc-http-schema/openapi/testdata/prefix3/expected_multi_words.json b/ndc-http-schema/openapi/testdata/prefix3/expected_multi_words.json index c0100f9..8617f8a 100644 --- a/ndc-http-schema/openapi/testdata/prefix3/expected_multi_words.json +++ b/ndc-http-schema/openapi/testdata/prefix3/expected_multi_words.json @@ -137,7 +137,7 @@ } }, "http": { - "type": null + "type": [] } }, "external_id": { @@ -366,6 +366,28 @@ ] } }, + "excluded_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + }, "filters": { "type": { "type": "nullable", @@ -411,6 +433,50 @@ ] } }, + "include_player_ids": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + }, + "included_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + }, "send_after": { "type": { "type": "nullable", @@ -439,6 +505,20 @@ "object" ] } + }, + "target_channel": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "HasuraOneSignalPlayerNotificationTargetTargetChannel", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } } } }, @@ -538,7 +618,7 @@ } }, "converted": { - "description": "Number of messages that were clicked.", + "description": "Number of users who have clicked / tapped on your notification.", "type": { "type": "nullable", "underlying_type": { @@ -553,7 +633,7 @@ } }, "errored": { - "description": "Number of errors reported.", + "description": "Number of notifications that could not be delivered due to an error. You can find more information by viewing the notification in the dashboard.", "type": { "type": "nullable", "underlying_type": { @@ -590,7 +670,7 @@ } }, "failed": { - "description": "Number of messages sent to unsubscribed devices.", + "description": "Number of notifications that could not be delivered due to those devices being unsubscribed.", "type": { "type": "nullable", "underlying_type": { @@ -742,7 +822,7 @@ } }, "received": { - "description": "Number of devices that received the message.", + "description": "Confirmed Deliveries number of devices that received the push notification. Paid Feature Only. Free accounts will see 0.", "type": { "type": "nullable", "underlying_type": { @@ -802,7 +882,7 @@ } }, "successful": { - "description": "Number of messages delivered to push servers, mobile carriers, or email service providers.", + "description": "Number of notifications that were successfully delivered.", "type": { "type": "nullable", "underlying_type": { @@ -1063,8 +1143,8 @@ "comparison_operators": {}, "representation": { "one_of": [ - "\u003e", - "\u003c", + ">", + "<", "=", "!=", "exists", diff --git a/ndc-http-schema/openapi/testdata/prefix3/expected_multi_words.schema.json b/ndc-http-schema/openapi/testdata/prefix3/expected_multi_words.schema.json index a34a6fd..065e0c8 100644 --- a/ndc-http-schema/openapi/testdata/prefix3/expected_multi_words.schema.json +++ b/ndc-http-schema/openapi/testdata/prefix3/expected_multi_words.schema.json @@ -212,6 +212,18 @@ } } }, + "excluded_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + }, "filters": { "type": { "type": "nullable", @@ -242,6 +254,30 @@ } } }, + "include_player_ids": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + }, + "included_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + }, "send_after": { "type": { "type": "nullable", @@ -259,6 +295,15 @@ "type": "named" } } + }, + "target_channel": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "HasuraOneSignalPlayerNotificationTargetTargetChannel", + "type": "named" + } + } } } }, @@ -327,7 +372,7 @@ } }, "converted": { - "description": "Number of messages that were clicked.", + "description": "Number of users who have clicked / tapped on your notification.", "type": { "type": "nullable", "underlying_type": { @@ -337,7 +382,7 @@ } }, "errored": { - "description": "Number of errors reported.", + "description": "Number of notifications that could not be delivered due to an error. You can find more information by viewing the notification in the dashboard.", "type": { "type": "nullable", "underlying_type": { @@ -359,7 +404,7 @@ } }, "failed": { - "description": "Number of messages sent to unsubscribed devices.", + "description": "Number of notifications that could not be delivered due to those devices being unsubscribed.", "type": { "type": "nullable", "underlying_type": { @@ -455,7 +500,7 @@ } }, "received": { - "description": "Number of devices that received the message.", + "description": "Confirmed Deliveries number of devices that received the push notification. Paid Feature Only. Free accounts will see 0.", "type": { "type": "nullable", "underlying_type": { @@ -494,7 +539,7 @@ } }, "successful": { - "description": "Number of messages delivered to push servers, mobile carriers, or email service providers.", + "description": "Number of notifications that were successfully delivered.", "type": { "type": "nullable", "underlying_type": { @@ -663,8 +708,8 @@ "comparison_operators": {}, "representation": { "one_of": [ - "\u003e", - "\u003c", + ">", + "<", "=", "!=", "exists", diff --git a/ndc-http-schema/openapi/testdata/prefix3/expected_single_word.json b/ndc-http-schema/openapi/testdata/prefix3/expected_single_word.json index 66b500d..0411be1 100644 --- a/ndc-http-schema/openapi/testdata/prefix3/expected_single_word.json +++ b/ndc-http-schema/openapi/testdata/prefix3/expected_single_word.json @@ -137,7 +137,7 @@ } }, "http": { - "type": null + "type": [] } }, "external_id": { @@ -366,6 +366,28 @@ ] } }, + "excluded_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + }, "filters": { "type": { "type": "nullable", @@ -411,6 +433,50 @@ ] } }, + "include_player_ids": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + }, + "included_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ] + } + } + }, "send_after": { "type": { "type": "nullable", @@ -439,6 +505,20 @@ "object" ] } + }, + "target_channel": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "HasuraPlayerNotificationTargetTargetChannel", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } } } }, @@ -538,7 +618,7 @@ } }, "converted": { - "description": "Number of messages that were clicked.", + "description": "Number of users who have clicked / tapped on your notification.", "type": { "type": "nullable", "underlying_type": { @@ -553,7 +633,7 @@ } }, "errored": { - "description": "Number of errors reported.", + "description": "Number of notifications that could not be delivered due to an error. You can find more information by viewing the notification in the dashboard.", "type": { "type": "nullable", "underlying_type": { @@ -590,7 +670,7 @@ } }, "failed": { - "description": "Number of messages sent to unsubscribed devices.", + "description": "Number of notifications that could not be delivered due to those devices being unsubscribed.", "type": { "type": "nullable", "underlying_type": { @@ -742,7 +822,7 @@ } }, "received": { - "description": "Number of devices that received the message.", + "description": "Confirmed Deliveries number of devices that received the push notification. Paid Feature Only. Free accounts will see 0.", "type": { "type": "nullable", "underlying_type": { @@ -802,7 +882,7 @@ } }, "successful": { - "description": "Number of messages delivered to push servers, mobile carriers, or email service providers.", + "description": "Number of notifications that were successfully delivered.", "type": { "type": "nullable", "underlying_type": { @@ -1063,8 +1143,8 @@ "comparison_operators": {}, "representation": { "one_of": [ - "\u003e", - "\u003c", + ">", + "<", "=", "!=", "exists", diff --git a/ndc-http-schema/openapi/testdata/prefix3/expected_single_word.schema.json b/ndc-http-schema/openapi/testdata/prefix3/expected_single_word.schema.json index 3fca3b8..0824d15 100644 --- a/ndc-http-schema/openapi/testdata/prefix3/expected_single_word.schema.json +++ b/ndc-http-schema/openapi/testdata/prefix3/expected_single_word.schema.json @@ -212,6 +212,18 @@ } } }, + "excluded_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + }, "filters": { "type": { "type": "nullable", @@ -242,6 +254,30 @@ } } }, + "include_player_ids": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + }, + "included_segments": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + } + } + }, "send_after": { "type": { "type": "nullable", @@ -259,6 +295,15 @@ "type": "named" } } + }, + "target_channel": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "HasuraPlayerNotificationTargetTargetChannel", + "type": "named" + } + } } } }, @@ -327,7 +372,7 @@ } }, "converted": { - "description": "Number of messages that were clicked.", + "description": "Number of users who have clicked / tapped on your notification.", "type": { "type": "nullable", "underlying_type": { @@ -337,7 +382,7 @@ } }, "errored": { - "description": "Number of errors reported.", + "description": "Number of notifications that could not be delivered due to an error. You can find more information by viewing the notification in the dashboard.", "type": { "type": "nullable", "underlying_type": { @@ -359,7 +404,7 @@ } }, "failed": { - "description": "Number of messages sent to unsubscribed devices.", + "description": "Number of notifications that could not be delivered due to those devices being unsubscribed.", "type": { "type": "nullable", "underlying_type": { @@ -455,7 +500,7 @@ } }, "received": { - "description": "Number of devices that received the message.", + "description": "Confirmed Deliveries number of devices that received the push notification. Paid Feature Only. Free accounts will see 0.", "type": { "type": "nullable", "underlying_type": { @@ -494,7 +539,7 @@ } }, "successful": { - "description": "Number of messages delivered to push servers, mobile carriers, or email service providers.", + "description": "Number of notifications that were successfully delivered.", "type": { "type": "nullable", "underlying_type": { @@ -663,8 +708,8 @@ "comparison_operators": {}, "representation": { "one_of": [ - "\u003e", - "\u003c", + ">", + "<", "=", "!=", "exists", diff --git a/ndc-http-schema/openapi/testdata/union2/expected.json b/ndc-http-schema/openapi/testdata/union2/expected.json new file mode 100644 index 0000000..52fac26 --- /dev/null +++ b/ndc-http-schema/openapi/testdata/union2/expected.json @@ -0,0 +1,298 @@ +{ + "$schema": "https://raw.githubusercontent.com/hasura/ndc-http/refs/heads/main/ndc-http-schema/jsonschema/ndc-http-schema.schema.json", + "settings": { + "servers": [ + { + "url": { + "value": "https://example.com/v1", + "env": "SERVER_URL" + } + } + ], + "securitySchemes": { + "api_key": { + "type": "apiKey", + "in": "header", + "name": "api_key", + "value": { + "env": "API_KEY" + } + } + } + }, + "functions": {}, + "object_types": { + "Pet": { + "fields": { + "age": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ] + } + }, + "icon": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "metadata": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "JSON", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [] + } + } + }, + "text": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "type": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "PetTypeEnum", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "value": { + "description": "The value of this recipient's custom field", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + } + } + }, + "PetBody": { + "fields": { + "age": { + "type": { + "name": "Int32", + "type": "named" + }, + "http": { + "type": [ + "integer" + ] + } + }, + "icon": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "id": { + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + }, + "metadata": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "JSON", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [] + } + } + }, + "text": { + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + }, + "type": { + "type": { + "name": "PetBodyTypeEnum", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + } + } + } + }, + "procedures": { + "addPet": { + "request": { + "url": "/pet", + "method": "post", + "requestBody": { + "contentType": "application/json" + }, + "response": { + "contentType": "application/json" + } + }, + "arguments": { + "body": { + "description": "Pet object that needs to be added to the store", + "type": { + "name": "PetBody", + "type": "named" + }, + "http": { + "in": "body", + "schema": { + "type": [ + "object" + ] + } + } + } + }, + "description": "Add a new pet to the store", + "result_type": { + "name": "Pet", + "type": "named" + } + } + }, + "scalar_types": { + "Int32": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "int32" + } + }, + "JSON": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "json" + } + }, + "PetBodyTypeEnum": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "cat", + "dog" + ], + "type": "enum" + } + }, + "PetTypeEnum": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "cat", + "dog" + ], + "type": "enum" + } + }, + "String": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "string" + } + } + } +} diff --git a/ndc-http-schema/openapi/testdata/union2/schema.json b/ndc-http-schema/openapi/testdata/union2/schema.json new file mode 100644 index 0000000..5c4a2e8 --- /dev/null +++ b/ndc-http-schema/openapi/testdata/union2/schema.json @@ -0,0 +1,190 @@ +{ + "collections": [], + "functions": [], + "object_types": { + "Pet": { + "fields": { + "age": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "icon": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "metadata": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "JSON", + "type": "named" + }, + "type": "array" + } + } + }, + "text": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "type": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "PetTypeEnum", + "type": "named" + } + } + }, + "value": { + "description": "The value of this recipient's custom field", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, + "PetBody": { + "fields": { + "age": { + "type": { + "name": "Int32", + "type": "named" + } + }, + "icon": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "id": { + "type": { + "name": "String", + "type": "named" + } + }, + "metadata": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "JSON", + "type": "named" + }, + "type": "array" + } + } + }, + "text": { + "type": { + "name": "String", + "type": "named" + } + }, + "type": { + "type": { + "name": "PetBodyTypeEnum", + "type": "named" + } + } + } + } + }, + "procedures": [ + { + "arguments": { + "body": { + "description": "Pet object that needs to be added to the store", + "type": { + "name": "PetBody", + "type": "named" + } + } + }, + "description": "Add a new pet to the store", + "name": "addPet", + "result_type": { + "name": "Pet", + "type": "named" + } + } + ], + "scalar_types": { + "Int32": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "int32" + } + }, + "JSON": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "json" + } + }, + "PetBodyTypeEnum": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "cat", + "dog" + ], + "type": "enum" + } + }, + "PetTypeEnum": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "cat", + "dog" + ], + "type": "enum" + } + }, + "String": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "string" + } + } + } +} diff --git a/ndc-http-schema/openapi/testdata/union2/source.json b/ndc-http-schema/openapi/testdata/union2/source.json new file mode 100644 index 0000000..0cb1a4e --- /dev/null +++ b/ndc-http-schema/openapi/testdata/union2/source.json @@ -0,0 +1,115 @@ +{ + "swagger": "2.0", + "host": "example.com", + "basePath": "/v1", + "schemes": ["https", "http"], + "paths": { + "/pet": { + "post": { + "tags": ["pet"], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": ["application/json", "application/xml"], + "produces": ["application/json", "application/xml"], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { "$ref": "#/definitions/PetBody" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "$ref": "#/definitions/Pet" } + } + } + } + } + }, + "securityDefinitions": { + "api_key": { "type": "apiKey", "name": "api_key", "in": "header" } + }, + "definitions": { + "PetBody": { + "allOf": [ + { + "$ref": "#/definitions/Dog" + }, + { + "$ref": "#/definitions/Cat" + } + ] + }, + "Pet": { + "oneOf": [ + { + "$ref": "#/definitions/Dog" + }, + { + "$ref": "#/definitions/Cat" + }, + { + "type": "object", + "properties": { + "value": { + "type": ["string", "null"], + "description": "The value of this recipient's custom field" + } + } + } + ] + }, + "Dog": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["dog"] + }, + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "icon": { + "type": "string" + } + }, + "required": ["id", "type", "text"] + }, + "Cat": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["cat"] + }, + "id": { + "type": "string" + }, + "age": { + "type": "integer" + }, + "metadata": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + } + } + }, + "required": ["id", "type", "age"] + } + } +} diff --git a/ndc-http-schema/openapi/testdata/union3/expected.json b/ndc-http-schema/openapi/testdata/union3/expected.json new file mode 100644 index 0000000..49f9afa --- /dev/null +++ b/ndc-http-schema/openapi/testdata/union3/expected.json @@ -0,0 +1,293 @@ +{ + "$schema": "https://raw.githubusercontent.com/hasura/ndc-http/refs/heads/main/ndc-http-schema/jsonschema/ndc-http-schema.schema.json", + "settings": { + "servers": [ + { + "url": { + "value": "", + "env": "SERVER_URL" + } + } + ], + "securitySchemes": { + "app_key": { + "type": "http", + "header": "Authorization", + "scheme": "bearer", + "value": { + "env": "APP_KEY_TOKEN" + } + } + } + }, + "functions": {}, + "object_types": { + "Pet": { + "fields": { + "age": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ] + } + }, + "icon": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "metadata": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "JSON", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [] + } + } + }, + "text": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "type": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "PetTypeEnum", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "value": { + "description": "The value of this recipient's custom field", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + } + } + }, + "PetBodyInput": { + "fields": { + "age": { + "type": { + "name": "Int32", + "type": "named" + }, + "http": { + "type": [ + "integer" + ] + } + }, + "icon": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "id": { + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + }, + "metadata": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "JSON", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [] + } + } + }, + "text": { + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + }, + "type": { + "type": { + "name": "PetBodyTypeEnum", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + } + } + } + }, + "procedures": { + "postPets": { + "request": { + "url": "/pets", + "method": "post", + "requestBody": { + "contentType": "application/json" + }, + "response": { + "contentType": "application/json" + } + }, + "arguments": { + "body": { + "description": "Request body of POST /pets", + "type": { + "name": "PetBodyInput", + "type": "named" + }, + "http": { + "in": "body" + } + } + }, + "description": "POST /pets", + "result_type": { + "name": "Pet", + "type": "named" + } + } + }, + "scalar_types": { + "Int32": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "int32" + } + }, + "JSON": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "json" + } + }, + "PetBodyTypeEnum": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "cat", + "dog" + ], + "type": "enum" + } + }, + "PetTypeEnum": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "cat", + "dog" + ], + "type": "enum" + } + }, + "String": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "string" + } + } + } +} diff --git a/ndc-http-schema/openapi/testdata/union3/schema.json b/ndc-http-schema/openapi/testdata/union3/schema.json new file mode 100644 index 0000000..b51da26 --- /dev/null +++ b/ndc-http-schema/openapi/testdata/union3/schema.json @@ -0,0 +1,190 @@ +{ + "collections": [], + "functions": [], + "object_types": { + "Pet": { + "fields": { + "age": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "icon": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "metadata": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "JSON", + "type": "named" + }, + "type": "array" + } + } + }, + "text": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "type": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "PetTypeEnum", + "type": "named" + } + } + }, + "value": { + "description": "The value of this recipient's custom field", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, + "PetBodyInput": { + "fields": { + "age": { + "type": { + "name": "Int32", + "type": "named" + } + }, + "icon": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "id": { + "type": { + "name": "String", + "type": "named" + } + }, + "metadata": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "JSON", + "type": "named" + }, + "type": "array" + } + } + }, + "text": { + "type": { + "name": "String", + "type": "named" + } + }, + "type": { + "type": { + "name": "PetBodyTypeEnum", + "type": "named" + } + } + } + } + }, + "procedures": [ + { + "arguments": { + "body": { + "description": "Request body of POST /pets", + "type": { + "name": "PetBodyInput", + "type": "named" + } + } + }, + "description": "POST /pets", + "name": "postPets", + "result_type": { + "name": "Pet", + "type": "named" + } + } + ], + "scalar_types": { + "Int32": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "int32" + } + }, + "JSON": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "json" + } + }, + "PetBodyTypeEnum": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "cat", + "dog" + ], + "type": "enum" + } + }, + "PetTypeEnum": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "cat", + "dog" + ], + "type": "enum" + } + }, + "String": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "string" + } + } + } +} diff --git a/ndc-http-schema/openapi/testdata/union3/source.json b/ndc-http-schema/openapi/testdata/union3/source.json new file mode 100644 index 0000000..8d1436b --- /dev/null +++ b/ndc-http-schema/openapi/testdata/union3/source.json @@ -0,0 +1,122 @@ +{ + "openapi": "3.0.0", + "servers": [ + { + "url": "/" + } + ], + "components": { + "securitySchemes": { + "app_key": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "PetBody": { + "allOf": [ + { + "$ref": "#/components/schemas/Dog" + }, + { + "$ref": "#/components/schemas/Cat" + } + ] + }, + "Pet": { + "oneOf": [ + { + "$ref": "#/components/schemas/Dog" + }, + { + "$ref": "#/components/schemas/Cat" + }, + { + "type": "object", + "properties": { + "value": { + "type": ["string", "null"], + "description": "The value of this recipient's custom field" + } + } + } + ] + }, + "Dog": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["dog"] + }, + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "icon": { + "type": "string" + } + }, + "required": ["id", "type", "text"] + }, + "Cat": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["cat"] + }, + "id": { + "type": "string" + }, + "age": { + "type": "integer" + }, + "metadata": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + } + } + }, + "required": ["id", "type", "age"] + } + } + }, + "paths": { + "/pets": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PetBody" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + } + } +} diff --git a/ndc-http-schema/utils/http.go b/ndc-http-schema/utils/http.go new file mode 100644 index 0000000..fef0b33 --- /dev/null +++ b/ndc-http-schema/utils/http.go @@ -0,0 +1,32 @@ +package utils + +import ( + "strings" + + "github.com/hasura/ndc-http/ndc-http-schema/schema" +) + +// IsContentTypeJSON checks if the content type is JSON +func IsContentTypeJSON(contentType string) bool { + return contentType == schema.ContentTypeJSON || strings.HasSuffix(contentType, "+json") +} + +// IsContentTypeXML checks if the content type is XML +func IsContentTypeXML(contentType string) bool { + return contentType == schema.ContentTypeXML || strings.HasSuffix(contentType, "+xml") +} + +// IsContentTypeText checks if the content type relates to text +func IsContentTypeText(contentType string) bool { + return strings.HasPrefix(contentType, "text/") || strings.HasPrefix(contentType, "image/svg") +} + +// IsContentTypeText checks if the content type relates to binary +func IsContentTypeBinary(contentType string) bool { + return strings.HasPrefix(contentType, "application/") || strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") +} + +// IsContentTypeMultipartForm checks the content type relates to multipart form. +func IsContentTypeMultipartForm(contentType string) bool { + return strings.HasPrefix(contentType, "multipart/") +} diff --git a/ndc-http-schema/utils/slice.go b/ndc-http-schema/utils/slice.go index 10ebd53..cfc2ad5 100644 --- a/ndc-http-schema/utils/slice.go +++ b/ndc-http-schema/utils/slice.go @@ -3,6 +3,8 @@ package utils import ( "cmp" "slices" + + "github.com/hasura/ndc-sdk-go/utils" ) // SliceUnorderedEqual compares if both slices are equal with unordered positions @@ -14,3 +16,17 @@ func SliceUnorderedEqual[T cmp.Ordered](a []T, b []T) bool { return slices.Equal(sortedA, sortedB) } + +// SliceUnique gets unique elements of the input slice. +func SliceUnique[T cmp.Ordered](input []T) []T { + if len(input) == 0 { + return []T{} + } + + valueMap := make(map[T]bool) + for _, elem := range input { + valueMap[elem] = true + } + + return utils.GetSortedKeys(valueMap) +}