diff --git a/.golangci.yml b/.golangci.yml index 604c8c5..56f01a7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -42,6 +42,10 @@ linters-settings: # Default: 5 min-complexity: 10 + gocritic: + disabled-checks: + - appendAssign + issues: exclude-files: - ".*_test\\.go$" diff --git a/README.md b/README.md index 7dbe61e..cdfe5b5 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,18 @@ The connector can automatically transform OpenAPI 2.0 and 3.0 definitions to NDC **Supported content types** -- `application/json` -- `application/x-www-form-urlencoded` -- `application/octet-stream` -- `multipart/form-data` -- `application/x-ndjson` -- `text/*` -- Upload file content types, e.g.`image/*` from `base64` arguments. +| Content Type | Supported | +| --------------------------------- | --------- | +| application/json | ✅ | +| application/xml | ✅ | +| application/x-www-form-urlencoded | ✅ | +| multipart/form-data | ✅ | +| application/octet-stream | ✅ (\*) | +| text/\* | ✅ | +| application/x-ndjson | ✅ | +| image/\* | ✅ (\*) | + +\*: Upload file content types are converted to `base64` encoding. ## Quick start @@ -83,9 +88,9 @@ HTTP connector supports both OpenAPI 2 and 3 specifications. - `oas3`: OpenAPI 3.0/3.1. - `oas2`: OpenAPI 2.0. -#### HTTP schema +#### HTTP Connector schema -Enum: `http` +Enum: `ndc` HTTP schema is the native configuration schema which other specs will be converted to behind the scene. The schema extends the NDC Specification with HTTP configuration and can be converted from other specs by the [NDC HTTP schema CLI](./ndc-http-schema). diff --git a/connector/connector.go b/connector/connector.go index 3f73b28..764e7bf 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -19,7 +19,7 @@ type HTTPConnector struct { capabilities *schema.RawCapabilitiesResponse rawSchema *schema.RawSchemaResponse schema *rest.NDCHttpSchema - client *internal.HTTPClient + client internal.Doer } // NewHTTPConnector creates a HTTP connector instance @@ -29,7 +29,7 @@ func NewHTTPConnector(opts ...Option) *HTTPConnector { } return &HTTPConnector{ - client: internal.NewHTTPClient(defaultOptions.client), + client: defaultOptions.client, } } @@ -93,8 +93,6 @@ func (c *HTTPConnector) ParseConfiguration(ctx context.Context, configurationDir // In addition, this function should register any // connector-specific metrics with the metrics registry. func (c *HTTPConnector) TryInitState(ctx context.Context, configuration *configuration.Configuration, metrics *connector.TelemetryState) (*State, error) { - c.client.SetTracer(metrics.Tracer) - return &State{ Tracer: metrics.Tracer, }, nil diff --git a/connector/connector_test.go b/connector/connector_test.go index 66fa22a..679de25 100644 --- a/connector/connector_test.go +++ b/connector/connector_test.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "log" + "log/slog" "net/http" "net/http/httptest" "os" @@ -99,7 +100,7 @@ func TestHTTPConnector_emptyServer(t *testing.T) { func TestHTTPConnector_authentication(t *testing.T) { apiKey := "random_api_key" bearerToken := "random_bearer_token" - // slog.SetLogLoggerLevel(slog.LevelDebug) + slog.SetLogLoggerLevel(slog.LevelDebug) server := createMockServer(t, apiKey, bearerToken) defer server.Close() @@ -166,6 +167,19 @@ func TestHTTPConnector_authentication(t *testing.T) { "body": { "name": "pet" } + }, + "fields": { + "type": "object", + "fields": { + "headers": { + "column": "headers", + "type": "column" + }, + "response": { + "column": "response", + "type": "column" + } + } } } ], @@ -287,19 +301,32 @@ func TestHTTPConnector_authentication(t *testing.T) { }, "fields": { "fields": { - "fields": { - "completed": { - "column": "completed", - "type": "column" - }, - "status": { - "column": "status", - "type": "column" - } + "headers": { + "column": "headers", + "type": "column" }, - "type": "object" + "response": { + "column": "response", + "type": "column", + "fields": { + "type": "array", + "fields": { + "fields": { + "completed": { + "column": "completed", + "type": "column" + }, + "status": { + "column": "status", + "type": "column" + } + }, + "type": "object" + } + } + } }, - "type": "array" + "type": "object" } } ], @@ -321,6 +348,137 @@ func TestHTTPConnector_authentication(t *testing.T) { }) }) + t.Run("encoding-xml", func(t *testing.T) { + reqBody := []byte(`{ + "operations": [ + { + "type": "procedure", + "name": "putPetXml", + "arguments": { + "body": { + "id": 10, + "name": "doggie", + "category": { + "id": 1, + "name": "Dogs" + }, + "photoUrls": ["string"], + "tags": [ + { + "id": 0, + "name": "string" + } + ], + "status": "available" + } + }, + "fields": { + "fields": { + "headers": { + "column": "headers", + "type": "column" + }, + "response": { + "column": "response", + "fields": { + "fields": { + "category": { + "column": "category", + "fields": { + "fields": { + "id": { + "column": "id", + "type": "column" + }, + "name": { + "column": "name", + "type": "column" + } + }, + "type": "object" + }, + "type": "column" + }, + "field": { + "column": "field", + "type": "column" + }, + "id": { + "column": "id", + "type": "column" + }, + "name": { + "column": "name", + "type": "column" + }, + "photoUrls": { + "column": "photoUrls", + "type": "column" + }, + "status": { + "column": "status", + "type": "column" + }, + "tags": { + "column": "tags", + "fields": { + "fields": { + "fields": { + "id": { + "column": "id", + "type": "column" + }, + "name": { + "column": "name", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + } + }, + "type": "object" + }, + "type": "column" + } + }, + "type": "object" + } + } + ], + "collection_relationships": {} + }`) + + res, err := http.Post(fmt.Sprintf("%s/mutation", testServer.URL), "application/json", bytes.NewBuffer(reqBody)) + assert.NilError(t, err) + assertHTTPResponse(t, res, http.StatusOK, schema.MutationResponse{ + OperationResults: []schema.MutationOperationResults{ + schema.NewProcedureResult(map[string]any{ + "headers": map[string]any{"Content-Type": string("application/xml")}, + "response": map[string]any{ + "id": float64(10), + "name": "doggie", + "category": map[string]any{ + "id": float64(1), + "name": "Dogs", + }, + "field": nil, + "photoUrls": []any{"string"}, + "tags": []any{ + map[string]any{ + "id": float64(0), + "name": "string", + }, + }, + "status": "available", + }, + }).Encode(), + }, + }) + }) } func TestHTTPConnector_distribution(t *testing.T) { @@ -670,6 +828,18 @@ func createMockServer(t *testing.T, apiKey string, bearerToken string) *httptest } }) + mux.HandleFunc("/pet/xml", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPut: + w.Header().Add("Content-Type", "application/xml") + w.WriteHeader(http.StatusOK) + + _, _ = w.Write([]byte("\n1Dogs10doggiestringavailable0string")) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + }) + return httptest.NewServer(mux) } diff --git a/connector/internal/client.go b/connector/internal/client.go index 6aca371..72f86d9 100644 --- a/connector/internal/client.go +++ b/connector/internal/client.go @@ -3,6 +3,7 @@ package internal import ( "bytes" "context" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -17,6 +18,7 @@ import ( "sync" "time" + "github.com/hasura/ndc-http/ndc-http-schema/configuration" rest "github.com/hasura/ndc-http/ndc-http-schema/schema" "github.com/hasura/ndc-sdk-go/connector" "github.com/hasura/ndc-sdk-go/schema" @@ -36,16 +38,21 @@ type Doer interface { // HTTPClient represents a http client wrapper with advanced methods type HTTPClient struct { - client Doer - tracer *connector.Tracer - propagator propagation.TextMapPropagator + client Doer + schema *rest.NDCHttpSchema + forwardHeaders configuration.ForwardHeadersSettings + tracer *connector.Tracer + propagator propagation.TextMapPropagator } // NewHTTPClient creates a http client wrapper -func NewHTTPClient(client Doer) *HTTPClient { +func NewHTTPClient(client Doer, httpSchema *rest.NDCHttpSchema, forwardHeaders configuration.ForwardHeadersSettings, tracer *connector.Tracer) *HTTPClient { return &HTTPClient{ - client: client, - propagator: otel.GetTextMapPropagator(), + client: client, + tracer: tracer, + schema: httpSchema, + forwardHeaders: forwardHeaders, + propagator: otel.GetTextMapPropagator(), } } @@ -224,9 +231,21 @@ func (client *HTTPClient) sendSingle(ctx context.Context, request *RetryableRequ contentType := parseContentType(resp.Header.Get(contentTypeHeader)) if resp.StatusCode >= 400 { details := make(map[string]any) - if contentType == rest.ContentTypeJSON && json.Valid(errorBytes) { - details["error"] = json.RawMessage(errorBytes) - } else { + switch contentType { + case rest.ContentTypeJSON: + if json.Valid(errorBytes) { + details["error"] = json.RawMessage(errorBytes) + } else { + details["error"] = string(errorBytes) + } + case rest.ContentTypeXML: + errData, err := decodeArbitraryXML(bytes.NewReader(errorBytes)) + if err != nil { + details["error"] = string(errorBytes) + } else { + details["error"] = errData + } + default: details["error"] = string(errorBytes) } @@ -235,7 +254,7 @@ func (client *HTTPClient) sendSingle(ctx context.Context, request *RetryableRequ return nil, nil, schema.NewConnectorError(resp.StatusCode, resp.Status, details) } - result, headers, evalErr := evalHTTPResponse(ctx, span, resp, contentType, selection, resultType, logger) + result, headers, evalErr := client.evalHTTPResponse(ctx, span, resp, contentType, selection, resultType, logger) if evalErr != nil { span.SetStatus(codes.Error, "failed to decode the http response") span.RecordError(evalErr) @@ -317,7 +336,7 @@ func (client *HTTPClient) doRequest(ctx context.Context, request *RetryableReque return resp, body, cancel, nil } -func evalHTTPResponse(ctx context.Context, span trace.Span, resp *http.Response, contentType string, selection schema.NestedField, resultType schema.Type, logger *slog.Logger) (any, http.Header, *schema.ConnectorError) { +func (client *HTTPClient) evalHTTPResponse(ctx context.Context, span trace.Span, resp *http.Response, contentType string, selection schema.NestedField, resultType schema.Type, logger *slog.Logger) (any, http.Header, *schema.ConnectorError) { if logger.Enabled(ctx, slog.LevelDebug) { logAttrs := []any{ slog.Int("http_status", resp.StatusCode), @@ -356,25 +375,26 @@ func evalHTTPResponse(ctx context.Context, span trace.Span, resp *http.Response, return nil, resp.Header, nil } - switch contentType { - case "": + var result any + switch { + case strings.HasPrefix(contentType, "text/"): respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } - if len(respBody) == 0 { - return nil, resp.Header, nil + + result = string(respBody) + case contentType == rest.ContentTypeXML: + field, err := client.extractResultType(resultType) + if err != nil { + return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, "failed to extract forwarded headers response: "+err.Error(), nil) } - return string(respBody), resp.Header, nil - case "text/plain", "text/html": - respBody, err := io.ReadAll(resp.Body) + result, err = NewXMLDecoder(client.schema).Decode(resp.Body, field) if err != nil { return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } - - return string(respBody), resp.Header, nil - case rest.ContentTypeJSON: + case contentType == rest.ContentTypeJSON: if len(resultType) > 0 { namedType, err := resultType.AsNamed() if err == nil && namedType.Name == string(rest.ScalarString) { @@ -391,26 +411,17 @@ func evalHTTPResponse(ctx context.Context, span trace.Span, resp *http.Response, return string(respBytes), resp.Header, nil } - return strResult, resp.Header, nil + result = strResult + + break } } - var result any err := json.NewDecoder(resp.Body).Decode(&result) if err != nil { return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } - if selection == nil || selection.IsNil() { - return result, resp.Header, nil - } - - result, err = utils.EvalNestedColumnFields(selection, result) - if err != nil { - return nil, nil, schema.InternalServerError(err.Error(), nil) - } - - return result, resp.Header, nil - case rest.ContentTypeNdJSON: + case contentType == rest.ContentTypeNdJSON: var results []any decoder := json.NewDecoder(resp.Body) for decoder.More() { @@ -421,21 +432,90 @@ func evalHTTPResponse(ctx context.Context, span trace.Span, resp *http.Response, } results = append(results, r) } - if selection == nil || selection.IsNil() { - return results, resp.Header, nil - } - result, err := utils.EvalNestedColumnFields(selection, any(results)) + result = results + case strings.HasPrefix(contentType, "application/") || strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/"): + rawBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, schema.InternalServerError(err.Error(), nil) + return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } - - return result, resp.Header, nil + result = base64.StdEncoding.EncodeToString(rawBytes) default: return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, "failed to evaluate response", map[string]any{ "cause": "unsupported content type " + contentType, }) } + + result = client.createHeaderForwardingResponse(result, resp.Header) + if len(selection) == 0 { + return result, resp.Header, nil + } + + result, err := utils.EvalNestedColumnFields(selection, result) + if err != nil { + return nil, nil, schema.InternalServerError(err.Error(), nil) + } + + return result, resp.Header, nil +} + +func (client *HTTPClient) extractResultType(resultType schema.Type) (schema.Type, error) { + if !client.forwardHeaders.Enabled || client.forwardHeaders.ResponseHeaders == nil || client.forwardHeaders.ResponseHeaders.ResultField == "" { + return resultType, nil + } + + return client.extractForwardedHeadersResultType(resultType) +} + +func (client *HTTPClient) extractForwardedHeadersResultType(resultType schema.Type) (schema.Type, error) { + rawType, err := resultType.InterfaceT() + switch t := rawType.(type) { + case *schema.NullableType: + return client.extractForwardedHeadersResultType(t.UnderlyingType) + case *schema.ArrayType: + return nil, errors.New("expected object type, got array") + case *schema.NamedType: + objectType, ok := client.schema.ObjectTypes[t.Name] + if !ok { + return nil, fmt.Errorf("%s: expected object type", t.Name) + } + + if len(objectType.Fields) == 0 { + return nil, fmt.Errorf("%s: empty object field", t.Name) + } + + resultField, ok := objectType.Fields[client.forwardHeaders.ResponseHeaders.ResultField] + if !ok { + return nil, fmt.Errorf("%s: result field %s does not exist", t.Name, client.forwardHeaders.ResponseHeaders.ResultField) + } + + return resultField.Type, nil + case *schema.PredicateType: + return nil, errors.New("expected object type, got predicate type") + default: + return nil, err + } +} + +func (client *HTTPClient) createHeaderForwardingResponse(result any, rawHeaders http.Header) any { + if !client.forwardHeaders.Enabled || client.forwardHeaders.ResponseHeaders == nil { + return result + } + + headers := make(map[string]string) + for key, values := range rawHeaders { + if len(client.forwardHeaders.ResponseHeaders.ForwardHeaders) > 0 && !slices.Contains(client.forwardHeaders.ResponseHeaders.ForwardHeaders, key) { + continue + } + if len(values) > 0 && values[0] != "" { + headers[key] = values[0] + } + } + + return map[string]any{ + client.forwardHeaders.ResponseHeaders.HeadersField: headers, + client.forwardHeaders.ResponseHeaders.ResultField: result, + } } func parseContentType(input string) string { diff --git a/connector/internal/multipart.go b/connector/internal/multipart.go index 72144f0..a224d5f 100644 --- a/connector/internal/multipart.go +++ b/connector/internal/multipart.go @@ -49,7 +49,7 @@ func (w *MultipartWriter) WriteDataURI(name string, value any, headers http.Head escapeQuotes(name), escapeQuotes(name))) if dataURI.MediaType == "" { - h.Set("Content-Type", "application/octet-stream") + h.Set("Content-Type", schema.ContentTypeOctetStream) } else { h.Set("Content-Type", dataURI.MediaType) } diff --git a/connector/internal/request_builder.go b/connector/internal/request_builder.go index 808bc9c..4cfa6cd 100644 --- a/connector/internal/request_builder.go +++ b/connector/internal/request_builder.go @@ -84,8 +84,8 @@ func (c *RequestBuilder) buildRequestBody(request *RetryableRequest, rawRequest return nil } - contentType := rawRequest.RequestBody.ContentType - request.ContentType = contentType + contentType := parseContentType(rawRequest.RequestBody.ContentType) + request.ContentType = rawRequest.RequestBody.ContentType bodyInfo, infoOk := c.Operation.Arguments[rest.BodyKey] bodyData, ok := c.Arguments[rest.BodyKey] @@ -142,6 +142,16 @@ func (c *RequestBuilder) buildRequestBody(request *RetryableRequest, rawRequest request.ContentLength = int64(len(bodyBytes)) request.Body = bytes.NewReader(bodyBytes) + return nil + case contentType == rest.ContentTypeXML: + bodyBytes, err := NewXMLEncoder(c.Schema).Encode(&bodyInfo, bodyData) + if err != nil { + return err + } + + request.ContentLength = int64(len(bodyBytes)) + request.Body = bytes.NewReader(bodyBytes) + return nil default: return fmt.Errorf("unsupported content type %s", contentType) @@ -434,7 +444,7 @@ func (c *RequestBuilder) getRequestUploadBody(rawRequest *rest.Request, bodyInfo if rawRequest.RequestBody == nil || bodyInfo == nil { return nil } - if rawRequest.RequestBody.ContentType == "application/octet-stream" { + if rawRequest.RequestBody.ContentType == rest.ContentTypeOctetStream { return rawRequest.RequestBody } diff --git a/connector/internal/request_parameter.go b/connector/internal/request_parameter.go index b4df690..4c566b0 100644 --- a/connector/internal/request_parameter.go +++ b/connector/internal/request_parameter.go @@ -287,25 +287,13 @@ func encodeParameterReflectionValues(reflectValue reflect.Value, fieldPaths []st } kind := reflectValue.Kind() - switch kind { - case reflect.Bool: - return []ParameterItem{ - NewParameterItem([]Key{}, []string{strconv.FormatBool(reflectValue.Bool())}), - }, nil - case reflect.String: - return []ParameterItem{NewParameterItem([]Key{}, []string{reflectValue.String()})}, nil - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return []ParameterItem{ - NewParameterItem([]Key{}, []string{strconv.FormatInt(reflectValue.Int(), 10)}), - }, nil - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if result, err := stringifySimpleScalar(reflectValue, kind); err == nil { return []ParameterItem{ - NewParameterItem([]Key{}, []string{strconv.FormatUint(reflectValue.Uint(), 10)}), - }, nil - case reflect.Float32, reflect.Float64: - return []ParameterItem{ - NewParameterItem([]Key{}, []string{fmt.Sprintf("%f", reflectValue.Float())}), + NewParameterItem([]Key{}, []string{result}), }, nil + } + + switch kind { case reflect.Slice, reflect.Array: return encodeParameterReflectionSlice(reflectValue, fieldPaths) case reflect.Map, reflect.Interface: diff --git a/connector/internal/utils.go b/connector/internal/utils.go index ca6039d..67428dd 100644 --- a/connector/internal/utils.go +++ b/connector/internal/utils.go @@ -4,8 +4,11 @@ import ( "fmt" "net/http" "net/url" + "reflect" + "strconv" "strings" + rest "github.com/hasura/ndc-http/ndc-http-schema/schema" "github.com/hasura/ndc-sdk-go/schema" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -81,3 +84,80 @@ func cloneURL(input *url.URL) *url.URL { RawFragment: input.RawFragment, } } + +func stringifySimpleScalar(val reflect.Value, kind reflect.Kind) (string, error) { + switch kind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(val.Int(), 10), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(val.Uint(), 10), nil + case reflect.Float32, reflect.Float64: + return strconv.FormatFloat(val.Float(), 'g', -1, val.Type().Bits()), nil + case reflect.String: + return val.String(), nil + case reflect.Bool: + return strconv.FormatBool(val.Bool()), nil + case reflect.Interface: + return fmt.Sprint(val.Interface()), nil + default: + return "", fmt.Errorf("invalid value: %v", val.Interface()) + } +} + +func findXMLLeafObjectField(objectType rest.ObjectType) (*rest.ObjectField, string, bool) { + var f *rest.ObjectField + var fieldName string + for key, field := range objectType.Fields { + if field.HTTP == nil || field.HTTP.XML == nil { + return nil, "", false + } + if field.HTTP.XML.Text { + f = &field + fieldName = key + } else if !field.HTTP.XML.Attribute { + return nil, "", false + } + } + + return f, fieldName, true +} + +func getTypeSchemaXMLName(typeSchema *rest.TypeSchema, defaultName string) string { + if typeSchema != nil { + return getXMLName(typeSchema.XML, defaultName) + } + + return defaultName +} + +func getXMLName(xmlSchema *rest.XMLSchema, defaultName string) string { + if xmlSchema != nil { + if xmlSchema.Name != "" { + return xmlSchema.GetFullName() + } + + if xmlSchema.Prefix != "" { + return xmlSchema.Prefix + ":" + defaultName + } + } + + return defaultName +} + +func getArrayOrNamedType(schemaType schema.Type) (*schema.ArrayType, *schema.NamedType, error) { + rawType, err := schemaType.InterfaceT() + if err != nil { + return nil, nil, err + } + + switch t := rawType.(type) { + case *schema.NullableType: + return getArrayOrNamedType(t.UnderlyingType) + case *schema.ArrayType: + return t, nil, nil + case *schema.NamedType: + return nil, t, nil + default: + return nil, nil, nil + } +} diff --git a/connector/internal/xml_decode.go b/connector/internal/xml_decode.go new file mode 100644 index 0000000..5389930 --- /dev/null +++ b/connector/internal/xml_decode.go @@ -0,0 +1,462 @@ +package internal + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "io" + "strconv" + "strings" + + rest "github.com/hasura/ndc-http/ndc-http-schema/schema" + "github.com/hasura/ndc-sdk-go/schema" +) + +// XMLDecoder implements a dynamic XML decoder from the HTTP schema. +type XMLDecoder struct { + schema *rest.NDCHttpSchema + decoder *xml.Decoder +} + +// NewXMLDecoder creates a new XML encoder. +func NewXMLDecoder(httpSchema *rest.NDCHttpSchema) *XMLDecoder { + return &XMLDecoder{ + schema: httpSchema, + } +} + +// Decode unmarshals xml bytes to a dynamic type. +func (c *XMLDecoder) Decode(r io.Reader, resultType schema.Type) (any, error) { + c.decoder = xml.NewDecoder(r) + + for { + token, err := c.decoder.Token() + if err != nil { + return nil, err + } + if token == nil { + break + } + + if se, ok := token.(xml.StartElement); ok { + xmlTree := createXMLBlock(se) + if err := evalXMLTree(c.decoder, xmlTree); err != nil { + return nil, fmt.Errorf("failed to decode the xml result: %w", err) + } + + result, err := c.evalXMLField(xmlTree, "", rest.ObjectField{ + ObjectField: schema.ObjectField{ + Type: resultType, + }, + HTTP: &rest.TypeSchema{}, + }, []string{}) + if err != nil { + return nil, fmt.Errorf("failed to decode the xml result: %w", err) + } + + return result, nil + } + } + + return nil, nil +} + +func (c *XMLDecoder) evalXMLField(block *xmlBlock, fieldName string, field rest.ObjectField, fieldPaths []string) (any, error) { + rawType, err := field.Type.InterfaceT() + if err != nil { + return nil, err + } + + switch t := rawType.(type) { + case *schema.NullableType: + return c.evalXMLField(block, fieldName, rest.ObjectField{ + ObjectField: schema.ObjectField{ + Type: t.UnderlyingType, + }, + HTTP: field.HTTP, + }, fieldPaths) + case *schema.ArrayType: + return c.evalArrayField(block, fieldName, field, t, fieldPaths) + case *schema.NamedType: + return c.evalNamedField(block, t, fieldPaths) + default: + return nil, err + } +} + +func (c *XMLDecoder) getArrayItemObjectField(field rest.ObjectField, t *schema.ArrayType) rest.ObjectField { + fieldItem := rest.ObjectField{ + ObjectField: schema.ObjectField{ + Type: t.ElementType, + }, + } + + if field.HTTP != nil && field.HTTP.Items != nil { + fieldItem.HTTP = field.HTTP.Items + } + + return fieldItem +} +func (c *XMLDecoder) evalArrayField(block *xmlBlock, fieldName string, field rest.ObjectField, t *schema.ArrayType, fieldPaths []string) (any, error) { + if block.Fields == nil { + return nil, nil + } + if len(block.Fields) == 0 { + return []any{}, nil + } + + var elements []xmlBlock + itemTokenName := fieldName + wrapped := len(fieldPaths) == 0 + fieldItem := c.getArrayItemObjectField(field, t) + + if field.HTTP != nil { + wrapped = wrapped || (field.HTTP.XML != nil && field.HTTP.XML.Wrapped) + if field.HTTP.Items != nil && field.HTTP.Items.XML != nil && field.HTTP.Items.XML.Name != "" { + itemTokenName = field.HTTP.Items.XML.Name + } + } + + if wrapped { + for _, elems := range block.Fields { + if len(elems) > 0 { + elements = elems + + break + } + } + } else if elems, ok := block.Fields[itemTokenName]; ok { + elements = elems + } + + return c.evalArrayElements(elements, itemTokenName, fieldItem, fieldPaths) +} + +func (c *XMLDecoder) evalArrayElements(elements []xmlBlock, itemTokenName string, fieldItem rest.ObjectField, fieldPaths []string) ([]any, error) { + if len(elements) == 0 { + return []any{}, nil + } + + results := make([]any, len(elements)) + for i, elem := range elements { + result, err := c.evalXMLField(&elem, itemTokenName, fieldItem, append(fieldPaths, strconv.Itoa(i))) + if err != nil { + return nil, err + } + results[i] = result + } + + return results, nil +} + +func (c *XMLDecoder) evalNamedField(block *xmlBlock, t *schema.NamedType, fieldPaths []string) (any, error) { + if scalarType, ok := c.schema.ScalarTypes[t.Name]; ok { + return c.decodeSimpleScalarValue(block, scalarType, fieldPaths) + } + + objectType, ok := c.schema.ObjectTypes[t.Name] + if !ok { + return nil, fmt.Errorf("%s: invalid response type", strings.Join(fieldPaths, ".")) + } + + result := map[string]any{} + + for _, attr := range block.Start.Attr { + for key, objectField := range objectType.Fields { + if objectField.HTTP == nil || objectField.HTTP.XML == nil || !objectField.HTTP.XML.Attribute { + continue + } + + xmlKey := key + if objectField.HTTP.XML.Name != "" { + xmlKey = objectField.HTTP.XML.Name + } + if attr.Name.Local != xmlKey { + continue + } + + attrValue, err := c.evalAttribute(objectField.Type, attr, append(fieldPaths, key)) + if err != nil { + return nil, err + } + + result[key] = attrValue + + break + } + } + + _, textFieldName, isLeaf := findXMLLeafObjectField(objectType) + if isLeaf { + textValue, err := c.decodeSimpleScalarValue(block, c.schema.ScalarTypes[string(rest.ScalarString)], fieldPaths) + if err != nil { + return nil, err + } + + result[textFieldName] = textValue + + return result, nil + } + + for key, objectField := range objectType.Fields { + if objectField.HTTP == nil { + continue + } + xmlKey := key + if objectField.HTTP.XML != nil { + if objectField.HTTP.XML.Attribute { + continue + } + + xmlKey = getXMLName(objectField.HTTP.XML, key) + } + + fieldElems, ok := block.Fields[xmlKey] + if !ok || fieldElems == nil { + continue + } + + switch len(fieldElems) { + case 0: + result[key] = []any{} + case 1: + propPaths := append(fieldPaths, key) + if objectField.HTTP.XML != nil && objectField.HTTP.XML.Wrapped { + // this can be a wrapped array + fieldResult, err := c.evalXMLField(&fieldElems[0], xmlKey, objectField, propPaths) + if err != nil { + return nil, err + } + + result[key] = fieldResult + + continue + } + + at, nt, err := getArrayOrNamedType(objectField.Type) + if err != nil { + return nil, fmt.Errorf("%s: %w", strings.Join(propPaths, "."), err) + } + + if at != nil { + fieldItem := c.getArrayItemObjectField(objectField, at) + fieldResult, err := c.evalArrayElements(fieldElems, xmlKey, fieldItem, propPaths) + if err != nil { + return nil, err + } + + result[key] = fieldResult + } else if nt != nil { + fieldResult, err := c.evalNamedField(&fieldElems[0], nt, propPaths) + if err != nil { + return nil, err + } + + result[key] = fieldResult + } + default: + fieldResult, err := c.evalXMLField(&xmlBlock{ + Start: fieldElems[0].Start, + Fields: map[string][]xmlBlock{ + xmlKey: fieldElems, + }, + }, xmlKey, objectField, append(fieldPaths, key)) + if err != nil { + return nil, err + } + + result[key] = fieldResult + } + } + + return result, nil +} + +func (c *XMLDecoder) evalAttribute(schemaType schema.Type, attr xml.Attr, fieldPaths []string) (any, error) { + rawType, err := schemaType.InterfaceT() + if err != nil { + return nil, fmt.Errorf("%s: %w", strings.Join(fieldPaths, "."), err) + } + + switch t := rawType.(type) { + case *schema.NullableType: + return c.evalAttribute(t.UnderlyingType, attr, fieldPaths) + case *schema.ArrayType: + var result any + if err := json.Unmarshal([]byte(attr.Value), &result); err != nil { + return nil, fmt.Errorf("%s: failed to decode xml attribute, %w", strings.Join(fieldPaths, ","), err) + } + + return result, nil + case *schema.NamedType: + if scalarType, ok := c.schema.ScalarTypes[t.Name]; ok { + return c.decodeSimpleScalarValue(&xmlBlock{ + Data: attr.Value, + }, scalarType, fieldPaths) + } + + var result any + if err := json.Unmarshal([]byte(attr.Value), &result); err != nil { + return nil, fmt.Errorf("%s: failed to decode xml attribute, %w", strings.Join(fieldPaths, ","), err) + } + + return result, nil + default: + return nil, err + } +} + +func (c *XMLDecoder) decodeSimpleScalarValue(block *xmlBlock, scalarType schema.ScalarType, fieldPaths []string) (any, error) { + respType, err := scalarType.Representation.InterfaceT() + + var result any = nil + switch respType.(type) { + case *schema.TypeRepresentationString: + result = block.Data + case *schema.TypeRepresentationDate, *schema.TypeRepresentationTimestamp, *schema.TypeRepresentationTimestampTZ, *schema.TypeRepresentationUUID, *schema.TypeRepresentationEnum: + if len(block.Data) > 0 { + result = block.Data + } + case *schema.TypeRepresentationBytes: + result = block.Data + case *schema.TypeRepresentationBoolean: + if len(block.Data) == 0 { + break + } + + result, err = strconv.ParseBool(block.Data) + case *schema.TypeRepresentationBigDecimal, *schema.TypeRepresentationBigInteger: + if len(block.Data) == 0 { + break + } + + result = block.Data + case *schema.TypeRepresentationInteger, *schema.TypeRepresentationInt8, *schema.TypeRepresentationInt16, *schema.TypeRepresentationInt32, *schema.TypeRepresentationInt64: //nolint:all + if len(block.Data) == 0 { + break + } + + result, err = strconv.ParseInt(block.Data, 10, 64) + case *schema.TypeRepresentationNumber, *schema.TypeRepresentationFloat32, *schema.TypeRepresentationFloat64: //nolint:all + if len(block.Data) == 0 { + break + } + + result, err = strconv.ParseFloat(block.Data, 64) + case *schema.TypeRepresentationGeography, *schema.TypeRepresentationGeometry, *schema.TypeRepresentationJSON: + result = decodeArbitraryXMLBlock(block) + } + + if err != nil { + return nil, fmt.Errorf("%s: %w", strings.Join(fieldPaths, "."), err) + } + + return result, nil +} + +func decodeArbitraryXMLBlock(block *xmlBlock) any { + if len(block.Start.Attr) == 0 && len(block.Fields) == 0 { + return block.Data + } + + result := make(map[string]any) + if len(block.Start.Attr) > 0 { + attributes := make(map[string]string) + for _, attr := range block.Start.Attr { + attributes[attr.Name.Local] = attr.Value + } + result["attributes"] = attributes + } + + if len(block.Fields) == 0 { + result["content"] = block.Data + + return result + } + + for key, field := range block.Fields { + switch len(field) { + case 0: + case 1: + // limitation: we can't know if the array is wrapped + result[key] = decodeArbitraryXMLBlock(&field[0]) + default: + items := make([]any, len(field)) + for i, f := range field { + items[i] = decodeArbitraryXMLBlock(&f) + } + result[key] = items + } + } + + return result +} + +type xmlBlock struct { + Start xml.StartElement + Data string + Fields map[string][]xmlBlock +} + +func createXMLBlock(start xml.StartElement) *xmlBlock { + return &xmlBlock{ + Start: start, + Fields: map[string][]xmlBlock{}, + } +} + +func evalXMLTree(decoder *xml.Decoder, block *xmlBlock) error { +L: + for { + nextToken, err := decoder.Token() + if err != nil { + return err + } + + if nextToken == nil { + return nil + } + + switch tok := nextToken.(type) { + case xml.StartElement: + childBlock := createXMLBlock(tok) + if err := evalXMLTree(decoder, childBlock); err != nil { + return err + } + block.Fields[tok.Name.Local] = append(block.Fields[tok.Name.Local], *childBlock) + case xml.CharData: + block.Data = string(tok) + case xml.EndElement: + break L + } + } + + return nil +} + +func decodeArbitraryXML(r io.Reader) (any, error) { + decoder := xml.NewDecoder(r) + + for { + token, err := decoder.Token() + if err != nil { + return nil, err + } + if token == nil { + break + } + + if se, ok := token.(xml.StartElement); ok { + xmlTree := createXMLBlock(se) + if err := evalXMLTree(decoder, xmlTree); err != nil { + return nil, fmt.Errorf("failed to decode the xml result: %w", err) + } + + result := decodeArbitraryXMLBlock(xmlTree) + + return result, nil + } + } + + return nil, nil +} diff --git a/connector/internal/xml_decode_test.go b/connector/internal/xml_decode_test.go new file mode 100644 index 0000000..a2a21fd --- /dev/null +++ b/connector/internal/xml_decode_test.go @@ -0,0 +1,70 @@ +package internal + +import ( + "strings" + "testing" + + "github.com/hasura/ndc-sdk-go/schema" + "gotest.tools/v3/assert" +) + +func TestDecodeXML(t *testing.T) { + testCases := []struct { + Name string + Body string + Type schema.Type + Expected map[string]any + }{ + { + Name: "getSearchXml", + Type: schema.NewNamedType("JSON").Encode(), + Body: `x86_64x86_64Standard OBS instance at build.opensuse.orgThis instance delivers the default build targets for OBS.https://api.opensuse.org/public`, + Expected: map[string]any{ + "project": []any{ + map[string]any{ + "attributes": map[string]string{"name": "home:Admin"}, + "description": string(""), + "person": map[string]any{ + "attributes": map[string]string{"role": "maintainer", "userid": "Admin"}, + "content": string(""), + }, + "repository": []any{ + map[string]any{ + "arch": string("x86_64"), + "attributes": map[string]string{"name": "openSUSE_Tumbleweed"}, + "path": map[string]any{ + "attributes": map[string]string{"project": "openSUSE.org:openSUSE:Factory", "repository": "snapshot"}, + "content": string(""), + }, + }, + map[string]any{ + "arch": string("x86_64"), + "attributes": map[string]string{"name": "15.3"}, + "path": map[string]any{ + "attributes": map[string]string{"project": "openSUSE.org:openSUSE:Leap:15.3", "repository": "standard"}, + "content": string(""), + }, + }, + }, + "title": string(""), + }, + map[string]any{ + "attributes": map[string]string{"name": "openSUSE.org"}, + "description": string("This instance delivers the default build targets for OBS."), + "remoteurl": string("https://api.opensuse.org/public"), + "title": string("Standard OBS instance at build.opensuse.org"), + }, + }, + }, + }, + } + + ndcSchema := createMockSchema(t) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + result, err := NewXMLDecoder(ndcSchema).Decode(strings.NewReader(tc.Body), tc.Type) + assert.NilError(t, err) + assert.DeepEqual(t, tc.Expected, result) + }) + } +} diff --git a/connector/internal/xml_encode.go b/connector/internal/xml_encode.go new file mode 100644 index 0000000..14dc64e --- /dev/null +++ b/connector/internal/xml_encode.go @@ -0,0 +1,325 @@ +package internal + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "fmt" + "reflect" + "strconv" + "strings" + + rest "github.com/hasura/ndc-http/ndc-http-schema/schema" + "github.com/hasura/ndc-sdk-go/schema" + "github.com/hasura/ndc-sdk-go/utils" +) + +// XMLEncoder implements a dynamic XML encoder from the HTTP schema. +type XMLEncoder struct { + schema *rest.NDCHttpSchema +} + +// NewXMLEncoder creates a new XML encoder. +func NewXMLEncoder(httpSchema *rest.NDCHttpSchema) *XMLEncoder { + return &XMLEncoder{ + schema: httpSchema, + } +} + +// Encode marshals the body to xml bytes. +func (c *XMLEncoder) Encode(bodyInfo *rest.ArgumentInfo, bodyData any) ([]byte, error) { + var buf bytes.Buffer + enc := xml.NewEncoder(&buf) + + err := c.evalXMLField(enc, "", rest.ObjectField{ + ObjectField: schema.ObjectField{ + Type: bodyInfo.Type, + }, + HTTP: bodyInfo.HTTP.Schema, + }, bodyData, []string{}) + + if err != nil { + return nil, err + } + + if err := enc.Flush(); err != nil { + return nil, err + } + + return append([]byte(xml.Header), buf.Bytes()...), nil +} + +func (c *XMLEncoder) evalXMLField(enc *xml.Encoder, name string, field rest.ObjectField, value any, fieldPaths []string) error { + rawType, err := field.Type.InterfaceT() + var innerValue reflect.Value + var notNull bool + + if value != nil { + innerValue, notNull = utils.UnwrapPointerFromAnyToReflectValue(value) + } + + switch t := rawType.(type) { + case *schema.NullableType: + if !notNull { + return nil + } + + return c.evalXMLField(enc, name, rest.ObjectField{ + ObjectField: schema.ObjectField{ + Type: t.UnderlyingType, + }, + HTTP: field.HTTP, + }, innerValue.Interface(), fieldPaths) + case *schema.ArrayType: + if !notNull { + return fmt.Errorf("%s: expect an array, got null", strings.Join(fieldPaths, ".")) + } + + vi := innerValue.Interface() + values, ok := vi.([]any) + if !ok { + return fmt.Errorf("%s: expect an array, got %v", strings.Join(fieldPaths, "."), vi) + } + + var wrapped bool + xmlName := name + if field.HTTP.XML != nil { + wrapped = field.HTTP.XML.Wrapped + if field.HTTP.XML.Name != "" { + xmlName = field.HTTP.XML.Name + } + } + + if wrapped { + err := enc.EncodeToken(xml.StartElement{ + Name: xml.Name{Space: "", Local: xmlName}, + }) + if err != nil { + return fmt.Errorf("%s: %w", strings.Join(fieldPaths, "."), err) + } + } + + for i, v := range values { + err := c.evalXMLField(enc, name, rest.ObjectField{ + ObjectField: schema.ObjectField{ + Type: t.ElementType, + }, + HTTP: field.HTTP.Items, + }, v, append(fieldPaths, strconv.FormatInt(int64(i), 10))) + + if err != nil { + return err + } + } + + if wrapped { + err := enc.EncodeToken(xml.EndElement{ + Name: xml.Name{Space: "", Local: xmlName}, + }) + if err != nil { + return fmt.Errorf("%s: %w", strings.Join(fieldPaths, "."), err) + } + } + + return nil + case *schema.NamedType: + if !notNull { + return fmt.Errorf("%s: expect a non-null type, got null", strings.Join(fieldPaths, ".")) + } + + xmlName := getTypeSchemaXMLName(field.HTTP, name) + var attributes []xml.Attr + if field.HTTP != nil && field.HTTP.XML != nil && field.HTTP.XML.Namespace != "" { + attributes = append(attributes, field.HTTP.XML.GetNamespaceAttribute()) + } + + if _, ok := c.schema.ScalarTypes[t.Name]; ok { + if err := c.encodeSimpleScalar(enc, xmlName, reflect.ValueOf(value), attributes); err != nil { + return fmt.Errorf("%s: %w", strings.Join(fieldPaths, "."), err) + } + + return nil + } + + objectType, ok := c.schema.ObjectTypes[t.Name] + if !ok { + return fmt.Errorf("%s: invalid type %s", strings.Join(fieldPaths, "."), t.Name) + } + + iv := innerValue.Interface() + values, ok := iv.(map[string]any) + if !ok { + return fmt.Errorf("%s: expected a map, got %s", strings.Join(fieldPaths, "."), innerValue.Kind()) + } + + attributes, fieldKeys, err := c.evalAttributes(objectType, utils.GetSortedKeys(objectType.Fields), values, fieldPaths) + if err != nil { + return err + } + + xmlName = getXMLName(objectType.XML, name) + if objectType.XML != nil && objectType.XML.Namespace != "" { + attributes = append(attributes, objectType.XML.GetNamespaceAttribute()) + } + + err = enc.EncodeToken(xml.StartElement{ + Name: xml.Name{Space: "", Local: xmlName}, + Attr: attributes, + }) + if err != nil { + return fmt.Errorf("%s: %w", strings.Join(fieldPaths, "."), err) + } + + if len(fieldKeys) == 1 && objectType.Fields[fieldKeys[0]].HTTP != nil && objectType.Fields[fieldKeys[0]].HTTP.XML != nil && objectType.Fields[fieldKeys[0]].HTTP.XML.Text { + objectField := objectType.Fields[fieldKeys[0]] + fieldValue, ok := values[fieldKeys[0]] + if ok && fieldValue != nil { + textValue, err := c.encodeXMLText(objectField.Type, reflect.ValueOf(fieldValue), fieldPaths) + if err != nil { + return err + } + + if textValue != nil { + err = enc.EncodeToken(xml.CharData(*textValue)) + if err != nil { + return fmt.Errorf("%s: %w", strings.Join(fieldPaths, "."), err) + } + } + } + } else { + for _, key := range fieldKeys { + objectField := objectType.Fields[key] + fieldValue := values[key] + if err := c.evalXMLField(enc, key, objectField, fieldValue, append(fieldPaths, key)); err != nil { + return err + } + } + } + + err = enc.EncodeToken(xml.EndElement{ + Name: xml.Name{Space: "", Local: xmlName}, + }) + if err != nil { + return fmt.Errorf("%s: %w", strings.Join(fieldPaths, "."), err) + } + + return nil + default: + return fmt.Errorf("%s: %w", strings.Join(fieldPaths, "."), err) + } +} + +func (c *XMLEncoder) evalAttributes(objectType rest.ObjectType, keys []string, values map[string]any, fieldPaths []string) ([]xml.Attr, []string, error) { + var attrs []xml.Attr + remainKeys := make([]string, 0) + for _, key := range keys { + objectField := objectType.Fields[key] + isNullXML := objectField.HTTP == nil || objectField.HTTP.XML == nil + if isNullXML || !objectField.HTTP.XML.Attribute { + remainKeys = append(remainKeys, key) + + continue + } + + value, ok := values[key] + if !ok || value == nil { + continue + } + + // the attribute value is usually a primitive scalar, + // otherwise just encode the value as json string + str, err := c.encodeXMLText(objectField.Type, reflect.ValueOf(value), append(fieldPaths, key)) + if err != nil { + return nil, nil, err + } + + if str == nil { + continue + } + + attrs = append(attrs, xml.Attr{ + Name: xml.Name{Local: getTypeSchemaXMLName(objectField.HTTP, key)}, + Value: *str, + }) + } + + return attrs, remainKeys, nil +} + +func (c *XMLEncoder) encodeXMLText(schemaType schema.Type, value reflect.Value, fieldPaths []string) (*string, error) { + rawType, err := schemaType.InterfaceT() + if err != nil { + return nil, fmt.Errorf("%s: %w", strings.Join(fieldPaths, "."), err) + } + + innerValue, notNull := utils.UnwrapPointerFromReflectValue(value) + + switch t := rawType.(type) { + case *schema.NullableType: + if !notNull { + return nil, nil + } + + return c.encodeXMLText(t.UnderlyingType, value, fieldPaths) + case *schema.ArrayType: + if !notNull { + return nil, fmt.Errorf("%s: field it required", strings.Join(fieldPaths, ".")) + } + + resultBytes, err := json.Marshal(innerValue.Interface()) + if err != nil { + return nil, fmt.Errorf("%s: failed to encode xml attribute, %w", strings.Join(fieldPaths, "."), err) + } + + result := string(resultBytes) + + return &result, nil + case *schema.NamedType: + if !notNull { + return nil, fmt.Errorf("%s, field it required", strings.Join(fieldPaths, ".")) + } + + if _, ok := c.schema.ScalarTypes[t.Name]; ok { + str, err := stringifySimpleScalar(value, value.Kind()) + if err != nil { + return nil, err + } + + return &str, nil + } + + resultBytes, err := json.Marshal(innerValue.Interface()) + if err != nil { + return nil, fmt.Errorf("%s: failed to encode xml attribute, %w", strings.Join(fieldPaths, "."), err) + } + + result := string(resultBytes) + + return &result, nil + default: + return nil, fmt.Errorf("%s: failed to encode xml attribute, unsupported schema type", strings.Join(fieldPaths, ".")) + } +} + +func (c *XMLEncoder) encodeSimpleScalar(enc *xml.Encoder, name string, value reflect.Value, attributes []xml.Attr) error { + str, err := stringifySimpleScalar(value, value.Kind()) + if err != nil { + return err + } + + err = enc.EncodeToken(xml.StartElement{ + Name: xml.Name{Space: "", Local: name}, + Attr: attributes, + }) + if err != nil { + return err + } + + if err := enc.EncodeToken(xml.CharData(str)); err != nil { + return err + } + + return enc.EncodeToken(xml.EndElement{ + Name: xml.Name{Space: "", Local: name}, + }) +} diff --git a/connector/internal/xml_encode_test.go b/connector/internal/xml_encode_test.go new file mode 100644 index 0000000..783c38d --- /dev/null +++ b/connector/internal/xml_encode_test.go @@ -0,0 +1,123 @@ +package internal + +import ( + "bytes" + "testing" + + rest "github.com/hasura/ndc-http/ndc-http-schema/schema" + "gotest.tools/v3/assert" +) + +func TestCreateXMLForm(t *testing.T) { + testCases := []struct { + Name string + Body map[string]any + + Expected string + }{ + { + Name: "putPetXml", + Body: map[string]any{ + "id": int64(10), + "name": "doggie", + "category": map[string]any{ + "id": int64(1), + "name": "Dogs", + }, + "photoUrls": []any{"string"}, + "tags": []any{ + map[string]any{ + "id": int64(0), + "name": "string", + }, + }, + "status": "available", + }, + Expected: "\n1Dogs10doggiestringavailable0string", + }, + { + Name: "putCommentXml", + Body: map[string]any{ + "user": "Iggy", + "comment_count": int64(6), + "comment": []any{ + map[string]any{ + "who": "Iggy", + "when": "2021-10-15 13:28:22 UTC", + "id": int64(1), + "bsrequest": int64(115), + "xmlValue": "This is a pretty cool request!", + }, + map[string]any{ + "who": "Iggy", + "when": "2021-10-15 13:49:39 UTC", + "id": int64(2), + "project": "home:Admin", + "xmlValue": "This is a pretty cool project!", + }, + map[string]any{ + "who": "Iggy", + "when": "2021-10-15 13:54:38 UTC", + "id": int64(3), + "project": "home:Admin", + "package": "0ad", + "xmlValue": "This is a pretty cool package!", + }, + }, + }, + Expected: ` +This is a pretty cool request!This is a pretty cool project!This is a pretty cool package!`, + }, + { + Name: "putBookXml", + Body: map[string]any{ + "id": int64(0), + "title": "string", + "author": "Author", + "attr": "foo", + }, + Expected: ` +Author0string`, + }, + { + Name: "putCommentXml", + Body: map[string]any{ + "project": "home:Admin", + "package": "0ad", + "comment": []any{ + map[string]any{ + "who": "Iggy", + "when": "2021-10-15 13:28:22 UTC", + "id": int64(1), + "xmlValue": "This is a pretty cool comment!", + }, + }, + }, + Expected: "\nThis is a pretty cool comment!", + }, + } + + ndcSchema := createMockSchema(t) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + var info *rest.OperationInfo + for key, f := range ndcSchema.Procedures { + if key == tc.Name { + info = &f + break + } + } + assert.Assert(t, info != nil) + argumentInfo := info.Arguments["body"] + result, err := NewXMLEncoder(ndcSchema).Encode(&argumentInfo, tc.Body) + assert.NilError(t, err) + assert.Equal(t, tc.Expected, string(result)) + + dec := NewXMLDecoder(ndcSchema) + parsedResult, err := dec.Decode(bytes.NewBuffer([]byte(tc.Expected)), info.ResultType) + assert.NilError(t, err) + + assert.DeepEqual(t, tc.Body, parsedResult) + }) + } +} diff --git a/connector/mutation.go b/connector/mutation.go index 0fe8f94..65e8034 100644 --- a/connector/mutation.go +++ b/connector/mutation.go @@ -32,7 +32,7 @@ func (c *HTTPConnector) MutationExplain(ctx context.Context, configuration *conf operation := request.Operations[0] switch operation.Type { case schema.MutationOperationProcedure: - httpRequest, _, httpOptions, err := c.explainProcedure(&operation) + httpRequest, _, _, httpOptions, err := c.explainProcedure(&operation) if err != nil { return nil, err } @@ -43,16 +43,16 @@ func (c *HTTPConnector) MutationExplain(ctx context.Context, configuration *conf } } -func (c *HTTPConnector) explainProcedure(operation *schema.MutationOperation) (*internal.RetryableRequest, *rest.OperationInfo, *internal.HTTPOptions, error) { +func (c *HTTPConnector) explainProcedure(operation *schema.MutationOperation) (*internal.RetryableRequest, *rest.OperationInfo, *configuration.NDCHttpRuntimeSchema, *internal.HTTPOptions, error) { procedure, metadata, err := c.metadata.GetProcedure(operation.Name) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } // 1. resolve arguments, evaluate URL and query parameters var rawArgs map[string]any if err := json.Unmarshal(operation.Arguments, &rawArgs); err != nil { - return nil, nil, nil, schema.BadRequestError("failed to decode arguments", map[string]any{ + return nil, nil, nil, nil, schema.BadRequestError("failed to decode arguments", map[string]any{ "cause": err.Error(), }) } @@ -61,25 +61,25 @@ func (c *HTTPConnector) explainProcedure(operation *schema.MutationOperation) (* builder := internal.NewRequestBuilder(c.schema, procedure, rawArgs, metadata.Runtime) httpRequest, err := builder.Build() if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } if err := c.evalForwardedHeaders(httpRequest, rawArgs); err != nil { - return nil, nil, nil, schema.UnprocessableContentError("invalid forwarded headers", map[string]any{ + return nil, nil, nil, nil, schema.UnprocessableContentError("invalid forwarded headers", map[string]any{ "cause": err.Error(), }) } httpOptions, err := c.parseHTTPOptionsFromArguments(procedure.Arguments, rawArgs) if err != nil { - return nil, nil, nil, schema.UnprocessableContentError("invalid http options", map[string]any{ + return nil, nil, nil, nil, schema.UnprocessableContentError("invalid http options", map[string]any{ "cause": err.Error(), }) } httpOptions.Settings = metadata.Settings - return httpRequest, procedure, httpOptions, nil + return httpRequest, procedure, &metadata, httpOptions, nil } func (c *HTTPConnector) execMutationSync(ctx context.Context, state *State, request *schema.MutationRequest) (*schema.MutationResponse, error) { @@ -130,7 +130,7 @@ func (c *HTTPConnector) execMutationOperation(parentCtx context.Context, state * ctx, span := state.Tracer.Start(parentCtx, fmt.Sprintf("Execute Operation %d", index)) defer span.End() - httpRequest, procedure, httpOptions, err := c.explainProcedure(&operation) + httpRequest, procedure, metadata, httpOptions, err := c.explainProcedure(&operation) if err != nil { span.SetStatus(codes.Error, "failed to explain mutation") span.RecordError(err) @@ -139,7 +139,8 @@ func (c *HTTPConnector) execMutationOperation(parentCtx context.Context, state * } httpOptions.Concurrency = c.config.Concurrency.HTTP - result, headers, err := c.client.Send(ctx, httpRequest, operation.Fields, procedure.ResultType, httpOptions) + client := internal.NewHTTPClient(c.client, metadata.NDCHttpSchema, c.config.ForwardHeaders, state.Tracer) + result, _, err := client.Send(ctx, httpRequest, operation.Fields, procedure.ResultType, httpOptions) if err != nil { span.SetStatus(codes.Error, "failed to execute mutation") span.RecordError(err) @@ -147,5 +148,5 @@ func (c *HTTPConnector) execMutationOperation(parentCtx context.Context, state * return nil, err } - return schema.NewProcedureResult(c.createHeaderForwardingResponse(result, headers)).Encode(), nil + return schema.NewProcedureResult(result).Encode(), nil } diff --git a/connector/query.go b/connector/query.go index b0347dc..1c6bbe1 100644 --- a/connector/query.go +++ b/connector/query.go @@ -40,7 +40,7 @@ func (c *HTTPConnector) QueryExplain(ctx context.Context, configuration *configu requestVars = []schema.QueryRequestVariablesElem{make(schema.QueryRequestVariablesElem)} } - httpRequest, _, httpOptions, err := c.explainQuery(request, requestVars[0]) + httpRequest, _, _, httpOptions, err := c.explainQuery(request, requestVars[0]) if err != nil { return nil, err } @@ -48,16 +48,16 @@ func (c *HTTPConnector) QueryExplain(ctx context.Context, configuration *configu return serializeExplainResponse(httpRequest, httpOptions) } -func (c *HTTPConnector) explainQuery(request *schema.QueryRequest, variables map[string]any) (*internal.RetryableRequest, *rest.OperationInfo, *internal.HTTPOptions, error) { +func (c *HTTPConnector) explainQuery(request *schema.QueryRequest, variables map[string]any) (*internal.RetryableRequest, *rest.OperationInfo, *configuration.NDCHttpRuntimeSchema, *internal.HTTPOptions, error) { function, metadata, err := c.metadata.GetFunction(request.Collection) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } // 1. resolve arguments, evaluate URL and query parameters rawArgs, err := utils.ResolveArgumentVariables(request.Arguments, variables) if err != nil { - return nil, nil, nil, schema.UnprocessableContentError("failed to resolve argument variables", map[string]any{ + return nil, nil, nil, nil, schema.UnprocessableContentError("failed to resolve argument variables", map[string]any{ "cause": err.Error(), }) } @@ -65,25 +65,25 @@ func (c *HTTPConnector) explainQuery(request *schema.QueryRequest, variables map // 2. build the request req, err := internal.NewRequestBuilder(c.schema, function, rawArgs, metadata.Runtime).Build() if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } if err := c.evalForwardedHeaders(req, rawArgs); err != nil { - return nil, nil, nil, schema.UnprocessableContentError("invalid forwarded headers", map[string]any{ + return nil, nil, nil, nil, schema.UnprocessableContentError("invalid forwarded headers", map[string]any{ "cause": err.Error(), }) } httpOptions, err := c.parseHTTPOptionsFromArguments(function.Arguments, rawArgs) if err != nil { - return nil, nil, nil, schema.UnprocessableContentError("invalid http options", map[string]any{ + return nil, nil, nil, nil, schema.UnprocessableContentError("invalid http options", map[string]any{ "cause": err.Error(), }) } httpOptions.Settings = metadata.Settings - return req, function, httpOptions, err + return req, function, &metadata, httpOptions, err } func (c *HTTPConnector) execQuerySync(ctx context.Context, state *State, request *schema.QueryRequest, valueField schema.NestedField, requestVars []schema.QueryRequestVariablesElem) ([]schema.RowSet, error) { @@ -145,7 +145,7 @@ func (c *HTTPConnector) execQuery(ctx context.Context, state *State, request *sc ctx, span := state.Tracer.Start(ctx, fmt.Sprintf("Execute Query %d", index)) defer span.End() - httpRequest, function, httpOptions, err := c.explainQuery(request, variables) + httpRequest, function, metadata, httpOptions, err := c.explainQuery(request, variables) if err != nil { span.SetStatus(codes.Error, "failed to explain query") span.RecordError(err) @@ -154,7 +154,8 @@ func (c *HTTPConnector) execQuery(ctx context.Context, state *State, request *sc } httpOptions.Concurrency = c.config.Concurrency.HTTP - result, headers, err := c.client.Send(ctx, httpRequest, queryFields, function.ResultType, httpOptions) + client := internal.NewHTTPClient(c.client, metadata.NDCHttpSchema, c.config.ForwardHeaders, state.Tracer) + result, _, err := client.Send(ctx, httpRequest, queryFields, function.ResultType, httpOptions) if err != nil { span.SetStatus(codes.Error, "failed to execute the http request") span.RecordError(err) @@ -162,7 +163,7 @@ func (c *HTTPConnector) execQuery(ctx context.Context, state *State, request *sc return nil, err } - return c.createHeaderForwardingResponse(result, headers), nil + return result, nil } func serializeExplainResponse(httpRequest *internal.RetryableRequest, httpOptions *internal.HTTPOptions) (*schema.ExplainResponse, error) { diff --git a/connector/schema.go b/connector/schema.go index 30c6100..3a5553e 100644 --- a/connector/schema.go +++ b/connector/schema.go @@ -5,8 +5,6 @@ import ( "encoding/json" "fmt" "log/slog" - "net/http" - "slices" "github.com/go-viper/mapstructure/v2" "github.com/hasura/ndc-http/connector/internal" @@ -90,24 +88,3 @@ func (c *HTTPConnector) evalForwardedHeaders(req *internal.RetryableRequest, raw return nil } - -func (c *HTTPConnector) createHeaderForwardingResponse(result any, rawHeaders http.Header) any { - if !c.config.ForwardHeaders.Enabled || c.config.ForwardHeaders.ResponseHeaders == nil { - return result - } - - headers := make(map[string]string) - for key, values := range rawHeaders { - if len(c.config.ForwardHeaders.ResponseHeaders.ForwardHeaders) > 0 && !slices.Contains(c.config.ForwardHeaders.ResponseHeaders.ForwardHeaders, key) { - continue - } - if len(values) > 0 && values[0] != "" { - headers[key] = values[0] - } - } - - return map[string]any{ - c.config.ForwardHeaders.ResponseHeaders.HeadersField: headers, - c.config.ForwardHeaders.ResponseHeaders.ResultField: result, - } -} diff --git a/connector/testdata/auth/schema.yaml b/connector/testdata/auth/schema.yaml index 099301a..9e68d9d 100644 --- a/connector/testdata/auth/schema.yaml +++ b/connector/testdata/auth/schema.yaml @@ -139,6 +139,27 @@ procedures: name: ProgressResponse type: named type: array + putPetXml: + request: + url: "/pet/xml" + method: put + security: [] + requestBody: + contentType: application/xml + response: + contentType: application/xml + arguments: + body: + description: Request body of PUT /pet/xml + type: + name: PetXml + type: named + http: + in: body + description: Update an existing pet + result_type: + name: PetXml + type: named object_types: Pet: fields: @@ -152,6 +173,136 @@ object_types: type: name: String type: named + PetXml: + fields: + category: + type: + type: nullable + underlying_type: + name: Category + type: named + http: + type: + - object + xml: + name: category + field: + description: + This empty field is returned instead of the list of scopes if the + user making the call doesn't have the authorization required. + type: + type: nullable + underlying_type: + name: JSON + type: named + http: + type: + id: + type: + type: nullable + underlying_type: + name: Int64 + type: named + http: + type: + - integer + format: int64 + name: + type: + name: String + type: named + http: + type: + - string + photoUrls: + type: + element_type: + name: String + type: named + type: array + http: + type: + - array + items: + type: + - string + xml: + name: photoUrl + xml: + name: "" + wrapped: true + status: + description: pet status in the store + type: + type: nullable + underlying_type: + name: String + type: named + http: + type: + - string + tags: + type: + type: nullable + underlying_type: + element_type: + name: Tag + type: named + type: array + http: + type: + - array + xml: + name: "" + wrapped: true + xml: + name: pet + Tag: + fields: + id: + type: + type: nullable + underlying_type: + name: Int64 + type: named + http: + type: + - integer + format: int64 + name: + type: + type: nullable + underlying_type: + name: String + type: named + http: + type: + - string + xml: + name: tag + Category: + fields: + id: + type: + type: nullable + underlying_type: + name: Int64 + type: named + http: + type: + - integer + format: int64 + name: + type: + type: nullable + underlying_type: + name: String + type: named + http: + type: + - string + xml: + name: category CreateModelRequest: fields: model: @@ -181,12 +332,30 @@ scalar_types: Boolean: aggregate_functions: {} comparison_operators: {} + representation: + type: boolean Int: aggregate_functions: {} comparison_operators: {} + representation: + type: int32 + Int32: + aggregate_functions: {} + comparison_operators: {} + representation: + type: int32 + Int64: + aggregate_functions: {} + comparison_operators: {} + representation: + type: int64 JSON: aggregate_functions: {} comparison_operators: {} + representation: + type: json String: aggregate_functions: {} comparison_operators: {} + representation: + type: string diff --git a/connector/types.go b/connector/types.go index 2e07c46..fc50f61 100644 --- a/connector/types.go +++ b/connector/types.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/hasura/ndc-http/connector/internal" - "go.opentelemetry.io/otel/trace" + "github.com/hasura/ndc-sdk-go/connector" ) var ( @@ -15,7 +15,7 @@ var ( // State is the global state which is shared for every connector request. type State struct { - Tracer trace.Tracer + Tracer *connector.Tracer } type options struct { diff --git a/ndc-http-schema/command/convert.go b/ndc-http-schema/command/convert.go index a2126d8..5193b51 100644 --- a/ndc-http-schema/command/convert.go +++ b/ndc-http-schema/command/convert.go @@ -17,23 +17,6 @@ import ( // ConvertToNDCSchema converts to NDC HTTP schema from file func CommandConvertToNDCSchema(args *configuration.ConvertCommandArguments, logger *slog.Logger) error { start := time.Now() - logger.Debug( - "converting the document to NDC HTTP schema", - slog.String("file", args.File), - slog.String("config", args.Config), - slog.String("output", args.Output), - slog.String("spec", args.Spec), - slog.String("format", args.Format), - slog.String("prefix", args.Prefix), - slog.String("trim_prefix", args.TrimPrefix), - slog.String("env_prefix", args.EnvPrefix), - slog.Any("patch_before", args.PatchBefore), - slog.Any("patch_after", args.PatchAfter), - slog.Any("allowed_content_types", args.AllowedContentTypes), - slog.Bool("strict", args.Strict), - slog.Bool("pure", args.Pure), - ) - if args.File == "" && args.Config == "" { err := errors.New("--config or --file argument is required") logger.Error(err.Error()) @@ -65,6 +48,24 @@ func CommandConvertToNDCSchema(args *configuration.ConvertCommandArguments, logg } configuration.ResolveConvertConfigArguments(&config, configDir, args) + logger.Debug( + "converting the document to NDC HTTP schema", + slog.String("file", config.File), + slog.String("config", config.File), + slog.String("output", config.Output), + slog.String("spec", string(config.Spec)), + slog.String("format", args.Format), + slog.String("prefix", config.Prefix), + slog.String("trim_prefix", config.TrimPrefix), + slog.String("env_prefix", config.EnvPrefix), + slog.Any("patch_before", config.PatchBefore), + slog.Any("patch_after", config.PatchAfter), + slog.Any("allowed_content_types", config.AllowedContentTypes), + slog.Bool("strict", config.Strict), + slog.Bool("pure", config.Pure), + slog.Bool("no_deprecation", config.NoDeprecation), + ) + result, err := configuration.ConvertToNDCSchema(&config, logger) if err != nil { diff --git a/ndc-http-schema/command/testdata/auth/expected.json b/ndc-http-schema/command/testdata/auth/expected.json index a1753a0..c33923b 100644 --- a/ndc-http-schema/command/testdata/auth/expected.json +++ b/ndc-http-schema/command/testdata/auth/expected.json @@ -10,19 +10,29 @@ "securitySchemes": { "api_key": { "type": "apiKey", + "in": "header", + "name": "api_key", "value": { "env": "PET_STORE_API_KEY" + } + }, + "basic": { + "type": "basic", + "header": "Authorization", + "username": { + "value": "user" }, - "in": "header", - "name": "api_key" + "password": { + "value": "password" + } }, "bearer": { "type": "http", + "header": "", + "scheme": "bearer", "value": { "env": "PET_STORE_BEARER_TOKEN" - }, - "header": "", - "scheme": "bearer" + } }, "petstore_auth": { "type": "oauth2", @@ -47,19 +57,29 @@ "securitySchemes": { "api_key": { "type": "apiKey", + "in": "header", + "name": "api_key", "value": { "env": "PET_STORE_API_KEY" + } + }, + "basic": { + "type": "basic", + "header": "Authorization", + "username": { + "value": "user" }, - "in": "header", - "name": "api_key" + "password": { + "value": "password" + } }, "bearer": { "type": "http", + "header": "", + "scheme": "bearer", "value": { "env": "PET_STORE_BEARER_TOKEN" - }, - "header": "", - "scheme": "bearer" + } }, "petstore_auth": { "type": "oauth2", @@ -86,7 +106,6 @@ "request": { "url": "/pet", "method": "get", - "type": "http", "response": { "contentType": "" } @@ -105,7 +124,6 @@ "request": { "url": "/pet/findByStatus", "method": "get", - "type": "http", "security": [ { "bearer": [] @@ -146,7 +164,6 @@ "request": { "url": "/pet/retry", "method": "get", - "type": "http", "response": { "contentType": "" } @@ -162,6 +179,38 @@ } }, "object_types": { + "Category": { + "fields": { + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int64", + "type": "named" + } + }, + "http": { + "type": ["integer"], + "format": "int64" + } + }, + "name": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": ["string"] + } + } + }, + "xml": { + "name": "category" + } + }, "CreateModelRequest": { "fields": { "model": { @@ -195,6 +244,115 @@ } } }, + "PetXml": { + "fields": { + "category": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Category", + "type": "named" + } + }, + "http": { + "type": ["object"], + "xml": { + "name": "category" + } + } + }, + "field": { + "description": "This empty field is returned instead of the list of scopes if the user making the call doesn't have the authorization required.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + }, + "http": { + "type": null + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int64", + "type": "named" + } + }, + "http": { + "type": ["integer"], + "format": "int64" + } + }, + "name": { + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": ["string"] + } + }, + "photoUrls": { + "type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + }, + "http": { + "type": ["array"], + "items": { + "type": ["string"], + "xml": { + "name": "photoUrl" + } + }, + "xml": { + "wrapped": true + } + } + }, + "status": { + "description": "pet status in the store", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": ["string"] + } + }, + "tags": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "Tag", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": ["array"], + "xml": { + "wrapped": true + } + } + } + }, + "xml": { + "name": "pet" + } + }, "ProgressResponse": { "fields": { "completed": { @@ -218,6 +376,38 @@ } } } + }, + "Tag": { + "fields": { + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int64", + "type": "named" + } + }, + "http": { + "type": ["integer"], + "format": "int64" + } + }, + "name": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": ["string"] + } + } + }, + "xml": { + "name": "tag" + } } }, "procedures": { @@ -225,7 +415,6 @@ "request": { "url": "/pet", "method": "post", - "type": "http", "headers": { "Content-Type": { "value": "application/json" @@ -265,7 +454,11 @@ "request": { "url": "/model", "method": "post", - "type": "http", + "security": [ + { + "basic": [] + } + ], "requestBody": { "contentType": "application/json" }, @@ -289,6 +482,35 @@ }, "type": "array" } + }, + "putPetXml": { + "request": { + "url": "/pet/xml", + "method": "put", + "requestBody": { + "contentType": "application/xml" + }, + "response": { + "contentType": "application/xml" + } + }, + "arguments": { + "body": { + "description": "Request body of PUT /pet/xml", + "type": { + "name": "PetXml", + "type": "named" + }, + "http": { + "in": "body" + } + } + }, + "description": "Update an existing pet", + "result_type": { + "name": "PetXml", + "type": "named" + } } }, "scalar_types": { @@ -306,6 +528,27 @@ "type": "int32" } }, + "Int32": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "int32" + } + }, + "Int64": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "int64" + } + }, + "JSON": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "json" + } + }, "String": { "aggregate_functions": {}, "comparison_operators": {}, diff --git a/ndc-http-schema/command/testdata/auth/schema.yaml b/ndc-http-schema/command/testdata/auth/schema.yaml index ed5faa1..9e68d9d 100644 --- a/ndc-http-schema/command/testdata/auth/schema.yaml +++ b/ndc-http-schema/command/testdata/auth/schema.yaml @@ -15,6 +15,14 @@ settings: value: env: PET_STORE_BEARER_TOKEN scheme: bearer + basic: + type: basic + header: Authorization + username: + value: user + password: + value: password + scheme: bearer petstore_auth: type: oauth2 flows: @@ -113,6 +121,8 @@ procedures: request: url: /model method: post + security: + - basic: [] requestBody: contentType: application/json response: @@ -129,6 +139,27 @@ procedures: name: ProgressResponse type: named type: array + putPetXml: + request: + url: "/pet/xml" + method: put + security: [] + requestBody: + contentType: application/xml + response: + contentType: application/xml + arguments: + body: + description: Request body of PUT /pet/xml + type: + name: PetXml + type: named + http: + in: body + description: Update an existing pet + result_type: + name: PetXml + type: named object_types: Pet: fields: @@ -142,6 +173,136 @@ object_types: type: name: String type: named + PetXml: + fields: + category: + type: + type: nullable + underlying_type: + name: Category + type: named + http: + type: + - object + xml: + name: category + field: + description: + This empty field is returned instead of the list of scopes if the + user making the call doesn't have the authorization required. + type: + type: nullable + underlying_type: + name: JSON + type: named + http: + type: + id: + type: + type: nullable + underlying_type: + name: Int64 + type: named + http: + type: + - integer + format: int64 + name: + type: + name: String + type: named + http: + type: + - string + photoUrls: + type: + element_type: + name: String + type: named + type: array + http: + type: + - array + items: + type: + - string + xml: + name: photoUrl + xml: + name: "" + wrapped: true + status: + description: pet status in the store + type: + type: nullable + underlying_type: + name: String + type: named + http: + type: + - string + tags: + type: + type: nullable + underlying_type: + element_type: + name: Tag + type: named + type: array + http: + type: + - array + xml: + name: "" + wrapped: true + xml: + name: pet + Tag: + fields: + id: + type: + type: nullable + underlying_type: + name: Int64 + type: named + http: + type: + - integer + format: int64 + name: + type: + type: nullable + underlying_type: + name: String + type: named + http: + type: + - string + xml: + name: tag + Category: + fields: + id: + type: + type: nullable + underlying_type: + name: Int64 + type: named + http: + type: + - integer + format: int64 + name: + type: + type: nullable + underlying_type: + name: String + type: named + http: + type: + - string + xml: + name: category CreateModelRequest: fields: model: @@ -178,6 +339,21 @@ scalar_types: comparison_operators: {} representation: type: int32 + Int32: + aggregate_functions: {} + comparison_operators: {} + representation: + type: int32 + Int64: + aggregate_functions: {} + comparison_operators: {} + representation: + type: int64 + JSON: + aggregate_functions: {} + comparison_operators: {} + representation: + type: json String: aggregate_functions: {} comparison_operators: {} diff --git a/ndc-http-schema/command/testdata/patch/expected.json b/ndc-http-schema/command/testdata/patch/expected.json index 4eb3fe8..becd002 100644 --- a/ndc-http-schema/command/testdata/patch/expected.json +++ b/ndc-http-schema/command/testdata/patch/expected.json @@ -11,19 +11,29 @@ "securitySchemes": { "api_key": { "type": "apiKey", + "in": "header", + "name": "api_key", "value": { "value": "dog-secret" + } + }, + "basic": { + "type": "basic", + "header": "Authorization", + "username": { + "value": "user" }, - "in": "header", - "name": "api_key" + "password": { + "value": "password" + } }, "bearer": { "type": "http", + "header": "", + "scheme": "bearer", "value": { "env": "PET_STORE_BEARER_TOKEN" - }, - "header": "", - "scheme": "bearer" + } } }, "security": [ @@ -40,19 +50,29 @@ "securitySchemes": { "api_key": { "type": "apiKey", + "in": "header", + "name": "api_key", "value": { "value": "cat-secret" + } + }, + "basic": { + "type": "basic", + "header": "Authorization", + "username": { + "value": "user" }, - "in": "header", - "name": "api_key" + "password": { + "value": "password" + } }, "bearer": { "type": "http", + "header": "", + "scheme": "bearer", "value": { "env": "PET_STORE_BEARER_TOKEN" - }, - "header": "", - "scheme": "bearer" + } } }, "security": [ @@ -65,19 +85,29 @@ "securitySchemes": { "api_key": { "type": "apiKey", + "in": "header", + "name": "api_key", "value": { "env": "PET_STORE_API_KEY" + } + }, + "basic": { + "type": "basic", + "header": "Authorization", + "username": { + "value": "user" }, - "in": "header", - "name": "api_key" + "password": { + "value": "password" + } }, "bearer": { "type": "http", + "header": "", + "scheme": "bearer", "value": { "env": "PET_STORE_BEARER_TOKEN" - }, - "header": "", - "scheme": "bearer" + } } }, "security": [ @@ -92,7 +122,6 @@ "request": { "url": "/pet", "method": "get", - "type": "http", "response": { "contentType": "" } @@ -129,7 +158,6 @@ "request": { "url": "/pet/findByStatus", "method": "get", - "type": "http", "security": [ { "bearer": [] @@ -187,7 +215,6 @@ "request": { "url": "/pet/findByStatus", "method": "get", - "type": "http", "security": [ { "bearer": [] @@ -245,7 +272,6 @@ "request": { "url": "/pet", "method": "get", - "type": "http", "response": { "contentType": "" } @@ -282,7 +308,6 @@ "request": { "url": "/pet/retry", "method": "get", - "type": "http", "response": { "contentType": "" } @@ -318,7 +343,6 @@ "request": { "url": "/pet/retry", "method": "get", - "type": "http", "response": { "contentType": "" } @@ -434,6 +458,38 @@ } } }, + "Category": { + "fields": { + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int64", + "type": "named" + } + }, + "http": { + "type": ["integer"], + "format": "int64" + } + }, + "name": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": ["string"] + } + } + }, + "xml": { + "name": "category" + } + }, "CreateModelDistributedHeadersResponse": { "fields": { "headers": { @@ -744,6 +800,52 @@ } } }, + "HttpDistributedOptions": { + "description": "Distributed execution options for HTTP requests to multiple servers", + "fields": { + "parallel": { + "description": "Execute requests to remote servers in parallel", + "type": { + "type": "nullable", + "underlying_type": { + "name": "Boolean", + "type": "named" + } + } + }, + "servers": { + "description": "Specify remote servers to receive the request", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "HttpServerId", + "type": "named" + }, + "type": "array" + } + } + } + } + }, + "HttpSingleOptions": { + "description": "Execution options for HTTP requests to a single server", + "fields": { + "servers": { + "description": "Specify remote servers to receive the request. If there are many server IDs the server is selected randomly", + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "HttpServerId", + "type": "named" + }, + "type": "array" + } + } + } + } + }, "Pet": { "fields": { "id": { @@ -851,6 +953,115 @@ } } }, + "PetXml": { + "fields": { + "category": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Category", + "type": "named" + } + }, + "http": { + "type": ["object"], + "xml": { + "name": "category" + } + } + }, + "field": { + "description": "This empty field is returned instead of the list of scopes if the user making the call doesn't have the authorization required.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + }, + "http": { + "type": null + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int64", + "type": "named" + } + }, + "http": { + "type": ["integer"], + "format": "int64" + } + }, + "name": { + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": ["string"] + } + }, + "photoUrls": { + "type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + }, + "http": { + "type": ["array"], + "items": { + "type": ["string"], + "xml": { + "name": "photoUrl" + } + }, + "xml": { + "wrapped": true + } + } + }, + "status": { + "description": "pet status in the store", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": ["string"] + } + }, + "tags": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "Tag", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": ["array"], + "xml": { + "wrapped": true + } + } + } + }, + "xml": { + "name": "pet" + } + }, "ProgressResponse": { "fields": { "completed": { @@ -875,50 +1086,118 @@ } } }, - "HttpDistributedOptions": { - "description": "Distributed execution options for HTTP requests to multiple servers", + "PutPetXmlDistributedHeadersResponse": { "fields": { - "parallel": { - "description": "Execute requests to remote servers in parallel", + "headers": { "type": { "type": "nullable", "underlying_type": { - "name": "Boolean", + "name": "JSON", "type": "named" } } }, - "servers": { - "description": "Specify remote servers to receive the request", + "response": { + "type": { + "name": "PutPetXmlDistributedResult", + "type": "named" + } + } + } + }, + "PutPetXmlDistributedResult": { + "description": "Distributed responses of putPetXmlDistributed", + "fields": { + "errors": { + "description": "Error responses of putPetXmlDistributed", + "type": { + "element_type": { + "name": "DistributedError", + "type": "named" + }, + "type": "array" + } + }, + "results": { + "description": "Results of putPetXmlDistributed", + "type": { + "element_type": { + "name": "PutPetXmlDistributedResultData", + "type": "named" + }, + "type": "array" + } + } + } + }, + "PutPetXmlDistributedResultData": { + "description": "Distributed response data of putPetXmlDistributed", + "fields": { + "data": { + "description": "A result of putPetXmlDistributed", + "type": { + "name": "PetXml", + "type": "named" + } + }, + "server": { + "description": "Identity of the remote server", + "type": { + "name": "HttpServerId", + "type": "named" + } + } + } + }, + "PutPetXmlHeadersResponse": { + "fields": { + "headers": { "type": { "type": "nullable", "underlying_type": { - "element_type": { - "name": "HttpServerId", - "type": "named" - }, - "type": "array" + "name": "JSON", + "type": "named" } } + }, + "response": { + "type": { + "name": "PetXml", + "type": "named" + } } } }, - "HttpSingleOptions": { - "description": "Execution options for HTTP requests to a single server", + "Tag": { "fields": { - "servers": { - "description": "Specify remote servers to receive the request. If there are many server IDs the server is selected randomly", + "id": { "type": { "type": "nullable", "underlying_type": { - "element_type": { - "name": "HttpServerId", - "type": "named" - }, - "type": "array" + "name": "Int64", + "type": "named" + } + }, + "http": { + "type": ["integer"], + "format": "int64" + } + }, + "name": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" } + }, + "http": { + "type": ["string"] } } + }, + "xml": { + "name": "tag" } } }, @@ -927,7 +1206,6 @@ "request": { "url": "/pet", "method": "post", - "type": "http", "headers": { "Content-Type": { "value": "application/json" @@ -987,7 +1265,6 @@ "request": { "url": "/pet", "method": "post", - "type": "http", "headers": { "Content-Type": { "value": "application/json" @@ -1047,7 +1324,11 @@ "request": { "url": "/model", "method": "post", - "type": "http", + "security": [ + { + "basic": [] + } + ], "requestBody": { "contentType": "application/json" }, @@ -1093,7 +1374,11 @@ "request": { "url": "/model", "method": "post", - "type": "http", + "security": [ + { + "basic": [] + } + ], "requestBody": { "contentType": "application/json" }, @@ -1134,6 +1419,104 @@ "name": "CreateModelDistributedHeadersResponse", "type": "named" } + }, + "putPetXml": { + "request": { + "url": "/pet/xml", + "method": "put", + "requestBody": { + "contentType": "application/xml" + }, + "response": { + "contentType": "application/xml" + } + }, + "arguments": { + "body": { + "description": "Request body of PUT /pet/xml", + "type": { + "name": "PetXml", + "type": "named" + }, + "http": { + "in": "body" + } + }, + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "httpOptions": { + "description": "Execution options for HTTP requests to a single server", + "type": { + "type": "nullable", + "underlying_type": { + "name": "HttpSingleOptions", + "type": "named" + } + } + } + }, + "description": "Update an existing pet", + "result_type": { + "name": "PutPetXmlHeadersResponse", + "type": "named" + } + }, + "putPetXmlDistributed": { + "request": { + "url": "/pet/xml", + "method": "put", + "requestBody": { + "contentType": "application/xml" + }, + "response": { + "contentType": "application/xml" + } + }, + "arguments": { + "body": { + "description": "Request body of PUT /pet/xml", + "type": { + "name": "PetXml", + "type": "named" + }, + "http": { + "in": "body" + } + }, + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "httpOptions": { + "description": "Distributed execution options for HTTP requests to multiple servers", + "type": { + "type": "nullable", + "underlying_type": { + "name": "HttpDistributedOptions", + "type": "named" + } + } + } + }, + "description": "Update an existing pet", + "result_type": { + "name": "PutPetXmlDistributedHeadersResponse", + "type": "named" + } } }, "scalar_types": { @@ -1144,6 +1527,14 @@ "type": "boolean" } }, + "HttpServerId": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": ["dog", "cat"], + "type": "enum" + } + }, "Int": { "aggregate_functions": {}, "comparison_operators": {}, @@ -1151,19 +1542,25 @@ "type": "int32" } }, - "JSON": { + "Int32": { "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "type": "json" + "type": "int32" } }, - "HttpServerId": { + "Int64": { "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["dog", "cat"], - "type": "enum" + "type": "int64" + } + }, + "JSON": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "json" } }, "String": { diff --git a/ndc-http-schema/configuration/convert.go b/ndc-http-schema/configuration/convert.go index afc7804..df619d0 100644 --- a/ndc-http-schema/configuration/convert.go +++ b/ndc-http-schema/configuration/convert.go @@ -32,6 +32,7 @@ func ConvertToNDCSchema(config *ConvertConfig, logger *slog.Logger) (*schema.NDC EnvPrefix: config.EnvPrefix, AllowedContentTypes: config.AllowedContentTypes, Strict: config.Strict, + NoDeprecation: config.NoDeprecation, Logger: logger, } rawContent = utils.RemoveYAMLSpecialCharacters(rawContent) @@ -82,6 +83,9 @@ func ResolveConvertConfigArguments(config *ConvertConfig, configDir string, args if args.Strict { config.Strict = args.Strict } + if args.NoDeprecation { + config.NoDeprecation = args.NoDeprecation + } if len(args.AllowedContentTypes) > 0 { config.AllowedContentTypes = args.AllowedContentTypes } diff --git a/ndc-http-schema/configuration/types.go b/ndc-http-schema/configuration/types.go index 6c6f36c..80bbe59 100644 --- a/ndc-http-schema/configuration/types.go +++ b/ndc-http-schema/configuration/types.go @@ -214,6 +214,8 @@ type ConvertConfig struct { Pure bool `json:"pure,omitempty" yaml:"pure"` // Require strict validation Strict bool `json:"strict,omitempty" yaml:"strict"` + // Ignore deprecated fields. + NoDeprecation bool `json:"noDeprecation,omitempty" yaml:"noDeprecation"` // Patch files to be applied into the input file before converting PatchBefore []restUtils.PatchConfig `json:"patchBefore,omitempty" yaml:"patchBefore"` // Patch files to be applied into the input file after converting @@ -239,6 +241,7 @@ type ConvertCommandArguments struct { Spec string `help:"The API specification of the file, is one of oas3 (openapi3), oas2 (openapi2)"` Format string `default:"json" help:"The output format, is one of json, yaml. If the output is set, automatically detect the format in the output file extension"` Strict bool `default:"false" help:"Require strict validation"` + NoDeprecation bool `default:"false" help:"Ignore deprecated fields"` Pure bool `default:"false" help:"Return the pure NDC schema only"` Prefix string `help:"Add a prefix to the function and procedure names"` TrimPrefix string `help:"Trim the prefix in URL, e.g. /v1"` diff --git a/ndc-http-schema/openapi/internal/ndc.go b/ndc-http-schema/openapi/internal/ndc.go index df1e221..8def99a 100644 --- a/ndc-http-schema/openapi/internal/ndc.go +++ b/ndc-http-schema/openapi/internal/ndc.go @@ -148,6 +148,7 @@ func (nsc *NDCBuilder) validateType(schemaType schema.Type) (schema.TypeEncoder, newObjectType := rest.ObjectType{ Description: objectType.Description, + XML: objectType.XML, Fields: make(map[string]rest.ObjectField), } diff --git a/ndc-http-schema/openapi/internal/oas2.go b/ndc-http-schema/openapi/internal/oas2.go index 805a4b7..287f6b6 100644 --- a/ndc-http-schema/openapi/internal/oas2.go +++ b/ndc-http-schema/openapi/internal/oas2.go @@ -59,12 +59,6 @@ func (oc *OAS2Builder) BuildDocumentModel(docModel *libopenapi.DocumentModel[v2. }) } - for iterPath := docModel.Model.Paths.PathItems.First(); iterPath != nil; iterPath = iterPath.Next() { - if err := oc.pathToNDCOperations(iterPath); err != nil { - return nil, err - } - } - if docModel.Model.Definitions != nil { for cSchema := docModel.Model.Definitions.Definitions.First(); cSchema != nil; cSchema = cSchema.Next() { if err := oc.convertComponentSchemas(cSchema); err != nil { @@ -73,6 +67,12 @@ func (oc *OAS2Builder) BuildDocumentModel(docModel *libopenapi.DocumentModel[v2. } } + for iterPath := docModel.Model.Paths.PathItems.First(); iterPath != nil; iterPath = iterPath.Next() { + if err := oc.pathToNDCOperations(iterPath); err != nil { + return nil, err + } + } + if docModel.Model.SecurityDefinitions != nil && docModel.Model.SecurityDefinitions.Definitions != nil { oc.schema.Settings.SecuritySchemes = make(map[string]rest.SecurityScheme) for scheme := docModel.Model.SecurityDefinitions.Definitions.First(); scheme != nil; scheme = scheme.Next() { @@ -149,7 +149,7 @@ func (oc *OAS2Builder) pathToNDCOperations(pathItem orderedmap.Pair[string, *v2. pathKey := pathItem.Key() pathValue := pathItem.Value() - funcGet, funcName, err := newOAS2OperationBuilder(oc).BuildFunction(pathKey, pathValue.Get, pathValue.Parameters) + funcGet, funcName, err := newOAS2OperationBuilder(oc, pathKey, "get").BuildFunction(pathValue.Get, pathValue.Parameters) if err != nil { return err } @@ -157,7 +157,7 @@ func (oc *OAS2Builder) pathToNDCOperations(pathItem orderedmap.Pair[string, *v2. oc.schema.Functions[funcName] = *funcGet } - procPost, procPostName, err := newOAS2OperationBuilder(oc).BuildProcedure(pathKey, "post", pathValue.Post, pathValue.Parameters) + procPost, procPostName, err := newOAS2OperationBuilder(oc, pathKey, "post").BuildProcedure(pathValue.Post, pathValue.Parameters) if err != nil { return err } @@ -165,7 +165,7 @@ func (oc *OAS2Builder) pathToNDCOperations(pathItem orderedmap.Pair[string, *v2. oc.schema.Procedures[procPostName] = *procPost } - procPut, procPutName, err := newOAS2OperationBuilder(oc).BuildProcedure(pathKey, "put", pathValue.Put, pathValue.Parameters) + procPut, procPutName, err := newOAS2OperationBuilder(oc, pathKey, "put").BuildProcedure(pathValue.Put, pathValue.Parameters) if err != nil { return err } @@ -173,7 +173,7 @@ func (oc *OAS2Builder) pathToNDCOperations(pathItem orderedmap.Pair[string, *v2. oc.schema.Procedures[procPutName] = *procPut } - procPatch, procPatchName, err := newOAS2OperationBuilder(oc).BuildProcedure(pathKey, "patch", pathValue.Patch, pathValue.Parameters) + procPatch, procPatchName, err := newOAS2OperationBuilder(oc, pathKey, "patch").BuildProcedure(pathValue.Patch, pathValue.Parameters) if err != nil { return err } @@ -181,7 +181,7 @@ func (oc *OAS2Builder) pathToNDCOperations(pathItem orderedmap.Pair[string, *v2. oc.schema.Procedures[procPatchName] = *procPatch } - procDelete, procDeleteName, err := newOAS2OperationBuilder(oc).BuildProcedure(pathKey, "delete", pathValue.Delete, pathValue.Parameters) + procDelete, procDeleteName, err := newOAS2OperationBuilder(oc, pathKey, "delete").BuildProcedure(pathValue.Delete, pathValue.Parameters) if err != nil { return err } @@ -207,6 +207,7 @@ func (oc *OAS2Builder) convertComponentSchemas(schemaItem orderedmap.Pair[string if typeEncoder != nil { typeName = getNamedType(typeEncoder, true, "") } + cacheKey := "#/definitions/" + typeKey // treat no-property objects as a Arbitrary JSON scalar if typeEncoder == nil || typeName == string(rest.ScalarJSON) { diff --git a/ndc-http-schema/openapi/internal/oas2_operation.go b/ndc-http-schema/openapi/internal/oas2_operation.go index 9492226..ff2446c 100644 --- a/ndc-http-schema/openapi/internal/oas2_operation.go +++ b/ndc-http-schema/openapi/internal/oas2_operation.go @@ -15,65 +15,52 @@ import ( type oas2OperationBuilder struct { builder *OAS2Builder + pathKey string + method string Arguments map[string]rest.ArgumentInfo } -func newOAS2OperationBuilder(builder *OAS2Builder) *oas2OperationBuilder { +func newOAS2OperationBuilder(builder *OAS2Builder, pathKey string, method string) *oas2OperationBuilder { return &oas2OperationBuilder{ builder: builder, + pathKey: pathKey, + method: method, Arguments: make(map[string]rest.ArgumentInfo), } } // BuildFunction build a HTTP NDC function information from OpenAPI v2 operation -func (oc *oas2OperationBuilder) BuildFunction(pathKey string, operation *v2.Operation, commonParams []*v2.Parameter) (*rest.OperationInfo, string, error) { +func (oc *oas2OperationBuilder) BuildFunction(operation *v2.Operation, commonParams []*v2.Parameter) (*rest.OperationInfo, string, error) { if operation == nil { return nil, "", nil } - funcName := formatOperationName(operation.OperationId) - if funcName == "" { - funcName = buildPathMethodName(pathKey, "get", oc.builder.ConvertOptions) - } + + funcName := buildUniqueOperationName(oc.builder.schema, operation.OperationId, oc.pathKey, oc.method, oc.builder.ConvertOptions) oc.builder.Logger.Info("function", slog.String("name", funcName), - slog.String("path", pathKey), + slog.String("path", oc.pathKey), ) - responseContentType := oc.getResponseContentTypeV2(operation.Produces) - if responseContentType == "" { - oc.builder.Logger.Info("supported response content type", - slog.String("name", funcName), - slog.String("path", pathKey), - slog.String("method", "get"), - slog.Any("produces", operation.Produces), - slog.Any("consumes", operation.Consumes), - ) - - return nil, "", nil - } - - resultType, err := oc.convertResponse(operation.Responses, pathKey, []string{funcName, "Result"}) + resultType, response, err := oc.convertResponse(operation, []string{funcName, "Result"}) if err != nil { - return nil, "", fmt.Errorf("%s: %w", pathKey, err) + return nil, "", fmt.Errorf("%s: %w", oc.pathKey, err) } if resultType == nil { return nil, "", nil } - reqBody, err := oc.convertParameters(operation, pathKey, commonParams, []string{funcName}) + reqBody, err := oc.convertParameters(operation, commonParams, []string{funcName}) if err != nil { return nil, "", fmt.Errorf("%s: %w", funcName, err) } - description := oc.getOperationDescription(pathKey, "get", operation) + description := oc.getOperationDescription(operation) function := rest.OperationInfo{ Request: &rest.Request{ - URL: pathKey, + URL: oc.pathKey, Method: "get", RequestBody: reqBody, - Response: rest.Response{ - ContentType: responseContentType, - }, - Security: convertSecurities(operation.Security), + Response: *response, + Security: convertSecurities(operation.Security), }, Description: &description, Arguments: oc.Arguments, @@ -84,59 +71,41 @@ func (oc *oas2OperationBuilder) BuildFunction(pathKey string, operation *v2.Oper } // BuildProcedure build a HTTP NDC function information from OpenAPI v2 operation -func (oc *oas2OperationBuilder) BuildProcedure(pathKey string, method string, operation *v2.Operation, commonParams []*v2.Parameter) (*rest.OperationInfo, string, error) { +func (oc *oas2OperationBuilder) BuildProcedure(operation *v2.Operation, commonParams []*v2.Parameter) (*rest.OperationInfo, string, error) { if operation == nil { return nil, "", nil } - procName := formatOperationName(operation.OperationId) - if procName == "" { - procName = buildPathMethodName(pathKey, method, oc.builder.ConvertOptions) - } + procName := buildUniqueOperationName(oc.builder.schema, operation.OperationId, oc.pathKey, oc.method, oc.builder.ConvertOptions) oc.builder.Logger.Info("procedure", slog.String("name", procName), - slog.String("path", pathKey), - slog.String("method", method), + slog.String("path", oc.pathKey), + slog.String("method", oc.method), ) - responseContentType := oc.getResponseContentTypeV2(operation.Produces) - if responseContentType == "" { - oc.builder.Logger.Info("supported response content type", - slog.String("name", procName), - slog.String("path", pathKey), - slog.String("method", method), - slog.Any("produces", operation.Produces), - slog.Any("consumes", operation.Consumes), - ) - - return nil, "", nil - } - - resultType, err := oc.convertResponse(operation.Responses, pathKey, []string{procName, "Result"}) + resultType, response, err := oc.convertResponse(operation, []string{procName, "Result"}) if err != nil { - return nil, "", fmt.Errorf("%s: %w", pathKey, err) + return nil, "", fmt.Errorf("%s: %w", oc.pathKey, err) } if resultType == nil { return nil, "", nil } - reqBody, err := oc.convertParameters(operation, pathKey, commonParams, []string{procName}) + reqBody, err := oc.convertParameters(operation, commonParams, []string{procName}) if err != nil { - return nil, "", fmt.Errorf("%s: %w", pathKey, err) + return nil, "", fmt.Errorf("%s: %w", oc.pathKey, err) } - description := oc.getOperationDescription(pathKey, method, operation) + description := oc.getOperationDescription(operation) procedure := rest.OperationInfo{ Request: &rest.Request{ - URL: pathKey, - Method: method, + URL: oc.pathKey, + Method: oc.method, RequestBody: reqBody, Security: convertSecurities(operation.Security), - Response: rest.Response{ - ContentType: responseContentType, - }, + Response: *response, }, Description: &description, Arguments: oc.Arguments, @@ -146,14 +115,14 @@ func (oc *oas2OperationBuilder) BuildProcedure(pathKey string, method string, op return &procedure, procName, nil } -func (oc *oas2OperationBuilder) convertParameters(operation *v2.Operation, apiPath string, commonParams []*v2.Parameter, fieldPaths []string) (*rest.RequestBody, error) { +func (oc *oas2OperationBuilder) convertParameters(operation *v2.Operation, commonParams []*v2.Parameter, fieldPaths []string) (*rest.RequestBody, error) { if operation == nil || (len(operation.Parameters) == 0 && len(commonParams) == 0) { return nil, nil } - contentType := rest.ContentTypeJSON - if len(operation.Consumes) > 0 && !slices.Contains(operation.Consumes, rest.ContentTypeJSON) { - contentType = operation.Consumes[0] + contentType := oc.getContentTypeV2(operation.Consumes) + if contentType == "" { + contentType = rest.ContentTypeJSON } var requestBody *rest.RequestBody @@ -183,7 +152,7 @@ func (oc *oas2OperationBuilder) convertParameters(operation *v2.Operation, apiPa switch { case param.Type != "": - typeEncoder, err = newOAS2SchemaBuilder(oc.builder, apiPath, rest.ParameterLocation(param.In)).getSchemaTypeFromParameter(param, fieldPaths) + typeEncoder, err = newOAS2SchemaBuilder(oc.builder, oc.pathKey, rest.ParameterLocation(param.In)).getSchemaTypeFromParameter(param, fieldPaths) if err != nil { return nil, err } @@ -208,7 +177,7 @@ func (oc *oas2OperationBuilder) convertParameters(operation *v2.Operation, apiPa typeSchema.MinLength = &minLength } case param.Schema != nil: - typeEncoder, typeSchema, err = newOAS2SchemaBuilder(oc.builder, apiPath, rest.ParameterLocation(param.In)). + typeEncoder, typeSchema, err = newOAS2SchemaBuilder(oc.builder, oc.pathKey, rest.ParameterLocation(param.In)). getSchemaTypeFromProxy(param.Schema, !paramRequired, fieldPaths) if err != nil { return nil, err @@ -279,7 +248,7 @@ func (oc *oas2OperationBuilder) convertParameters(operation *v2.Operation, apiPa bodyName := utils.StringSliceToPascalCase(fieldPaths) + "Body" oc.builder.schema.ObjectTypes[bodyName] = formDataObject - desc := "Form data of " + apiPath + desc := "Form data of " + oc.pathKey oc.Arguments["body"] = rest.ArgumentInfo{ ArgumentInfo: schema.ArgumentInfo{ Type: schema.NewNamedType(bodyName).Encode(), @@ -298,59 +267,84 @@ func (oc *oas2OperationBuilder) convertParameters(operation *v2.Operation, apiPa return requestBody, nil } -func (oc *oas2OperationBuilder) convertResponse(responses *v2.Responses, apiPath string, fieldPaths []string) (schema.TypeEncoder, error) { - if responses == nil || responses.Codes == nil || responses.Codes.IsZero() { - return nil, nil +func (oc *oas2OperationBuilder) convertResponse(operation *v2.Operation, fieldPaths []string) (schema.TypeEncoder, *rest.Response, error) { + if operation.Responses == nil || operation.Responses.Codes == nil || operation.Responses.Codes.IsZero() { + return nil, nil, nil + } + + contentType := oc.getContentTypeV2(operation.Produces) + if contentType == "" { + oc.builder.Logger.Info("empty content type in response", + slog.String("path", oc.pathKey), + slog.String("method", oc.method), + slog.Any("produces", operation.Produces), + slog.Any("consumes", operation.Consumes), + ) + + return nil, nil, nil } var resp *v2.Response - if responses.Codes == nil || responses.Codes.IsZero() { + var statusCode int64 + if operation.Responses.Codes == nil || operation.Responses.Codes.IsZero() { // the response is always successful - resp = responses.Default + resp = operation.Responses.Default } else { - for r := responses.Codes.First(); r != nil; r = r.Next() { + for r := operation.Responses.Codes.First(); r != nil; r = r.Next() { if r.Key() == "" { continue } + code, err := strconv.ParseInt(r.Key(), 10, 32) if err != nil { continue } if isUnsupportedResponseCodes(code) { - return nil, nil + return nil, nil, nil } else if code >= 200 && code < 300 { resp = r.Value() + statusCode = code break } } } + response := &rest.Response{ + ContentType: contentType, + } + // return nullable boolean type if the response content is null - if resp == nil || resp.Schema == nil { + if resp == nil || (resp.Schema == nil && statusCode == 204) { scalarName := string(rest.ScalarBoolean) if _, ok := oc.builder.schema.ScalarTypes[scalarName]; !ok { oc.builder.schema.ScalarTypes[scalarName] = *defaultScalarTypes[rest.ScalarBoolean] } - return schema.NewNullableNamedType(scalarName), nil + return schema.NewNullableNamedType(scalarName), response, nil } - schemaType, _, err := newOAS2SchemaBuilder(oc.builder, apiPath, rest.InBody). + if resp.Schema == nil { + return getResultTypeFromContentType(oc.builder.schema, contentType), response, nil + } + + schemaType, _, err := newOAS2SchemaBuilder(oc.builder, oc.pathKey, rest.InBody). getSchemaTypeFromProxy(resp.Schema, false, fieldPaths) if err != nil { - return nil, err + return nil, nil, err } - return schemaType, nil + return schemaType, response, nil } -func (oc *oas2OperationBuilder) getResponseContentTypeV2(contentTypes []string) string { - contentType := rest.ContentTypeJSON - if len(contentTypes) == 0 || slices.Contains(contentTypes, contentType) { - return contentType +func (oc *oas2OperationBuilder) getContentTypeV2(contentTypes []string) string { + for _, contentType := range preferredContentTypes { + if len(contentTypes) == 0 || slices.Contains(contentTypes, contentType) { + return contentType + } } + if len(oc.builder.ConvertOptions.AllowedContentTypes) == 0 { return contentTypes[0] } @@ -364,7 +358,7 @@ func (oc *oas2OperationBuilder) getResponseContentTypeV2(contentTypes []string) return "" } -func (oc *oas2OperationBuilder) getOperationDescription(pathKey string, method string, operation *v2.Operation) string { +func (oc *oas2OperationBuilder) getOperationDescription(operation *v2.Operation) string { if operation.Summary != "" { return utils.StripHTMLTags(operation.Summary) } @@ -372,5 +366,5 @@ func (oc *oas2OperationBuilder) getOperationDescription(pathKey string, method s return utils.StripHTMLTags(operation.Description) } - return strings.ToUpper(method) + " " + pathKey + return strings.ToUpper(oc.method) + " " + oc.pathKey } diff --git a/ndc-http-schema/openapi/internal/oas2_schema.go b/ndc-http-schema/openapi/internal/oas2_schema.go index 5bb57a0..5f5d084 100644 --- a/ndc-http-schema/openapi/internal/oas2_schema.go +++ b/ndc-http-schema/openapi/internal/oas2_schema.go @@ -1,7 +1,6 @@ package internal import ( - "errors" "fmt" "slices" "strconv" @@ -36,12 +35,13 @@ func (oc *oas2SchemaBuilder) getSchemaTypeFromParameter(param *v2.Parameter, fie switch param.Type { case "object": - return nil, errors.New("unsupported object parameter") + return nil, fmt.Errorf("%s: unsupported object parameter", strings.Join(fieldPaths, ".")) case "array": if param.Items == nil || param.Items.Type == "" { if oc.builder.Strict { - return nil, errors.New("array item is empty") + return nil, fmt.Errorf("%s: array item is empty", strings.Join(fieldPaths, ".")) } + typeEncoder = schema.NewArrayType(oc.builder.buildScalarJSON()) } else { itemName, isNull := getScalarFromType(oc.builder.schema, []string{param.Items.Type}, param.Format, param.Enum, oc.trimPathPrefix(oc.apiPath), fieldPaths) @@ -50,7 +50,7 @@ func (oc *oas2SchemaBuilder) getSchemaTypeFromParameter(param *v2.Parameter, fie } default: if !isPrimitiveScalar([]string{param.Type}) { - return nil, fmt.Errorf("unsupported schema type %s", param.Type) + return nil, fmt.Errorf("%s: unsupported schema type %s", strings.Join(fieldPaths, "."), param.Type) } scalarName, isNull := getScalarFromType(oc.builder.schema, []string{param.Type}, param.Format, param.Enum, oc.trimPathPrefix(oc.apiPath), fieldPaths) @@ -136,8 +136,17 @@ func (oc *oas2SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ // 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 @@ -163,13 +172,16 @@ func (oc *oas2SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ 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, errors.New("array item is empty") + return nil, nil, fmt.Errorf("%s: array item is empty", strings.Join(fieldPaths, ".")) } result = schema.NewArrayType(oc.builder.buildScalarJSON()) } else { diff --git a/ndc-http-schema/openapi/internal/oas3.go b/ndc-http-schema/openapi/internal/oas3.go index 429b531..7050793 100644 --- a/ndc-http-schema/openapi/internal/oas3.go +++ b/ndc-http-schema/openapi/internal/oas3.go @@ -262,6 +262,14 @@ func (oc *OAS3Builder) convertComponentSchemas(schemaItem orderedmap.Pair[string if typeEncoder != nil { typeName = getNamedType(typeEncoder, true, "") } + + if schemaResult.XML == nil { + schemaResult.XML = &rest.XMLSchema{} + } + if schemaResult.XML.Name == "" { + schemaResult.XML.Name = typeKey + } + cacheKey := "#/components/schemas/" + typeKey // treat no-property objects as a Arbitrary JSON scalar if typeEncoder == nil || typeName == string(rest.ScalarJSON) { @@ -296,9 +304,7 @@ func (oc *OAS3Builder) trimPathPrefix(input string) string { // build a named type for JSON scalar func (oc *OAS3Builder) buildScalarJSON() *schema.NamedType { scalarName := string(rest.ScalarJSON) - if _, ok := oc.schema.ScalarTypes[scalarName]; !ok { - oc.schema.ScalarTypes[scalarName] = *defaultScalarTypes[rest.ScalarJSON] - } + oc.schema.AddScalar(scalarName, *defaultScalarTypes[rest.ScalarJSON]) return schema.NewNamedType(scalarName) } @@ -361,6 +367,7 @@ func (oc *OAS3Builder) populateWriteSchemaType(schemaType schema.Type) (schema.T } writeObject := rest.ObjectType{ Description: objectType.Description, + XML: objectType.XML, Fields: make(map[string]rest.ObjectField), } var hasWriteField bool diff --git a/ndc-http-schema/openapi/internal/oas3_operation.go b/ndc-http-schema/openapi/internal/oas3_operation.go index 360c391..c33ce04 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" + "slices" "strconv" "strings" "time" @@ -11,6 +12,7 @@ import ( "github.com/hasura/ndc-http/ndc-http-schema/utils" "github.com/hasura/ndc-sdk-go/schema" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi/orderedmap" ) type oas3OperationBuilder struct { @@ -34,12 +36,13 @@ func newOAS3OperationBuilder(builder *OAS3Builder, pathKey string, method string // BuildFunction build a HTTP NDC function information from OpenAPI v3 operation func (oc *oas3OperationBuilder) BuildFunction(itemGet *v3.Operation) (*rest.OperationInfo, string, error) { - start := time.Now() - funcName := formatOperationName(itemGet.OperationId) - if funcName == "" { - funcName = buildPathMethodName(oc.pathKey, "get", oc.builder.ConvertOptions) + if oc.builder.ConvertOptions.NoDeprecation && itemGet.Deprecated != nil && *itemGet.Deprecated { + return nil, "", nil } + start := time.Now() + funcName := buildUniqueOperationName(oc.builder.schema, itemGet.OperationId, oc.pathKey, oc.method, oc.builder.ConvertOptions) + defer func() { oc.builder.Logger.Info("function", slog.String("name", funcName), @@ -53,6 +56,7 @@ func (oc *oas3OperationBuilder) BuildFunction(itemGet *v3.Operation) (*rest.Oper if err != nil { return nil, "", fmt.Errorf("%s: %w", oc.pathKey, err) } + if resultType == nil { return nil, "", nil } @@ -80,14 +84,12 @@ func (oc *oas3OperationBuilder) BuildFunction(itemGet *v3.Operation) (*rest.Oper } func (oc *oas3OperationBuilder) BuildProcedure(operation *v3.Operation) (*rest.OperationInfo, string, error) { - if operation == nil { + if operation == nil || (oc.builder.ConvertOptions.NoDeprecation && operation.Deprecated != nil && *operation.Deprecated) { return nil, "", nil } + start := time.Now() - procName := formatOperationName(operation.OperationId) - if procName == "" { - procName = buildPathMethodName(oc.pathKey, oc.method, oc.builder.ConvertOptions) - } + procName := buildUniqueOperationName(oc.builder.schema, operation.OperationId, oc.pathKey, oc.method, oc.builder.ConvertOptions) defer func() { oc.builder.Logger.Info("procedure", @@ -158,7 +160,7 @@ func (oc *oas3OperationBuilder) convertParameters(params []*v3.Parameter, apiPat } for _, param := range append(params, oc.commonParams...) { - if param == nil { + if param == nil || (param.Deprecated && oc.builder.ConvertOptions.NoDeprecation) { continue } paramName := param.Name @@ -214,18 +216,33 @@ func (oc *oas3OperationBuilder) convertParameters(params []*v3.Parameter, apiPat return nil } +func (oc *oas3OperationBuilder) getContentType(contents *orderedmap.Map[string, *v3.MediaType]) (string, *v3.MediaType) { + var contentType string + var media *v3.MediaType + for _, ct := range preferredContentTypes { + for iter := contents.First(); iter != nil; iter = iter.Next() { + key := iter.Key() + value := iter.Value() + if strings.HasPrefix(key, ct) && value != nil { + return key, value + } + + if media == nil && value != nil && (len(oc.builder.AllowedContentTypes) == 0 || slices.Contains(oc.builder.AllowedContentTypes, key)) { + contentType = key + media = value + } + } + } + + return contentType, media +} + func (oc *oas3OperationBuilder) convertRequestBody(reqBody *v3.RequestBody, apiPath string, fieldPaths []string) (*rest.RequestBody, schema.TypeEncoder, error) { if reqBody == nil || reqBody.Content == nil { return nil, nil, nil } - contentType := rest.ContentTypeJSON - content, ok := reqBody.Content.Get(contentType) - if !ok { - contentPair := reqBody.Content.First() - contentType = contentPair.Key() - content = contentPair.Value() - } + contentType, content := oc.getContentType(reqBody.Content) bodyRequired := false if reqBody.Required != nil && *reqBody.Required { @@ -335,6 +352,7 @@ func (oc *oas3OperationBuilder) convertResponse(responses *v3.Responses, apiPath } var resp *v3.Response + var statusCode int64 if responses.Codes != nil && !responses.Codes.IsZero() { for r := responses.Codes.First(); r != nil; r = r.Next() { if r.Key() == "" { @@ -349,6 +367,7 @@ func (oc *oas3OperationBuilder) convertResponse(responses *v3.Responses, apiPath return nil, nil, nil } else if code >= 200 && code < 300 { resp = r.Value() + statusCode = code break } @@ -358,48 +377,40 @@ func (oc *oas3OperationBuilder) convertResponse(responses *v3.Responses, apiPath // return nullable boolean type if the response content is null if resp == nil || resp.Content == nil { scalarName := string(rest.ScalarBoolean) - if _, ok := oc.builder.schema.ScalarTypes[scalarName]; !ok { - oc.builder.schema.ScalarTypes[scalarName] = *defaultScalarTypes[rest.ScalarBoolean] - } + oc.builder.schema.AddScalar(scalarName, *defaultScalarTypes[rest.ScalarBoolean]) return schema.NewNullableNamedType(scalarName), &rest.Response{ ContentType: rest.ContentTypeJSON, }, nil } - contentType := rest.ContentTypeJSON - bodyContent, present := resp.Content.Get(contentType) - if !present { - if len(oc.builder.AllowedContentTypes) == 0 { - firstContent := resp.Content.First() - bodyContent = firstContent.Value() - contentType = firstContent.Key() - present = true - } else { - for _, ct := range oc.builder.AllowedContentTypes { - bodyContent, present = resp.Content.Get(ct) - if present { - contentType = ct - - break - } - } + contentType, bodyContent := oc.getContentType(resp.Content) + if bodyContent == nil { + if statusCode == 204 { + scalarName := string(rest.ScalarBoolean) + oc.builder.schema.AddScalar(scalarName, *defaultScalarTypes[rest.ScalarBoolean]) + + return schema.NewNullableNamedType(scalarName), &rest.Response{ + ContentType: rest.ContentTypeJSON, + }, nil } - } - if !present { return nil, nil, nil } + schemaResponse := &rest.Response{ + ContentType: contentType, + } + if bodyContent.Schema == nil { + return getResultTypeFromContentType(oc.builder.schema, contentType), schemaResponse, nil + } + schemaType, _, err := newOAS3SchemaBuilder(oc.builder, apiPath, rest.InBody, false). getSchemaTypeFromProxy(bodyContent.Schema, false, fieldPaths) if err != nil { return nil, nil, err } - schemaResponse := &rest.Response{ - ContentType: contentType, - } switch contentType { case rest.ContentTypeNdJSON: // Newline Delimited JSON (ndjson) format represents a stream of structured objects diff --git a/ndc-http-schema/openapi/internal/oas3_schema.go b/ndc-http-schema/openapi/internal/oas3_schema.go index 562dcb4..321ac24 100644 --- a/ndc-http-schema/openapi/internal/oas3_schema.go +++ b/ndc-http-schema/openapi/internal/oas3_schema.go @@ -1,7 +1,6 @@ package internal import ( - "errors" "fmt" "log/slog" "slices" @@ -35,6 +34,7 @@ func (oc *oas3SchemaBuilder) getSchemaTypeFromProxy(schemaProxy *base.SchemaProx if schemaProxy == nil { return nil, nil, errParameterSchemaEmpty(fieldPaths) } + innerSchema := schemaProxy.Schema() if innerSchema == nil { return nil, nil, fmt.Errorf("cannot get schema of $.%s from proxy: %s", strings.Join(fieldPaths, "."), schemaProxy.GetReference()) @@ -101,6 +101,10 @@ func (oc *oas3SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ return nil, nil, errParameterSchemaEmpty(fieldPaths) } + if oc.builder.ConvertOptions.NoDeprecation && typeSchema.Deprecated != nil && *typeSchema.Deprecated { + return nil, nil, nil + } + description := utils.StripHTMLTags(typeSchema.Description) nullable := typeSchema.Nullable != nil && *typeSchema.Nullable if len(typeSchema.AllOf) > 0 { @@ -179,12 +183,15 @@ func (oc *oas3SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ 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 != "" { @@ -237,6 +244,10 @@ func (oc *oas3SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ } 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 { @@ -244,6 +255,15 @@ func (oc *oas3SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ 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 @@ -255,7 +275,11 @@ func (oc *oas3SchemaBuilder) getSchemaType(typeSchema *base.Schema, fieldPaths [ } case "array": if typeSchema.Items == nil || typeSchema.Items.A == nil { - return nil, nil, errors.New("array item is empty") + if oc.builder.Strict { + return nil, nil, fmt.Errorf("%s: array item is empty", strings.Join(fieldPaths, ".")) + } + + return oc.builder.buildScalarJSON(), typeResult, nil } itemName := getSchemaRefTypeNameV3(typeSchema.Items.A.GetReference()) diff --git a/ndc-http-schema/openapi/internal/types.go b/ndc-http-schema/openapi/internal/types.go index f1eea24..dac31fa 100644 --- a/ndc-http-schema/openapi/internal/types.go +++ b/ndc-http-schema/openapi/internal/types.go @@ -7,6 +7,7 @@ import ( rest "github.com/hasura/ndc-http/ndc-http-schema/schema" "github.com/hasura/ndc-sdk-go/schema" + "github.com/hasura/ndc-sdk-go/utils" ) var ( @@ -19,6 +20,8 @@ var ( errParameterNameRequired = errors.New("parameter name is empty") ) +var preferredContentTypes = []string{rest.ContentTypeJSON, rest.ContentTypeXML} + var defaultScalarTypes = map[rest.ScalarName]*schema.ScalarType{ rest.ScalarBoolean: { AggregateFunctions: schema.ScalarTypeAggregateFunctions{}, @@ -113,6 +116,21 @@ var defaultScalarTypes = map[rest.ScalarName]*schema.ScalarType{ }, } +const xmlValueFieldName string = "xmlValue" + +var xmlValueField = rest.ObjectField{ + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("Value of the xml field"), + Type: schema.NewNamedType(string(rest.ScalarString)).Encode(), + }, + HTTP: &rest.TypeSchema{ + Type: []string{"string"}, + XML: &rest.XMLSchema{ + Text: true, + }, + }, +} + // ConvertOptions represent the common convert options for both OpenAPI v2 and v3 type ConvertOptions struct { MethodAlias map[string]string @@ -121,5 +139,6 @@ type ConvertOptions struct { TrimPrefix string EnvPrefix string Strict bool + NoDeprecation bool Logger *slog.Logger } diff --git a/ndc-http-schema/openapi/internal/utils.go b/ndc-http-schema/openapi/internal/utils.go index bd49861..3e0a69d 100644 --- a/ndc-http-schema/openapi/internal/utils.go +++ b/ndc-http-schema/openapi/internal/utils.go @@ -239,6 +239,16 @@ func createSchemaFromOpenAPISchema(input *base.Schema) *rest.TypeSchema { ps.ReadOnly = input.ReadOnly != nil && *input.ReadOnly ps.WriteOnly = input.WriteOnly != nil && *input.WriteOnly + if input.XML != nil { + ps.XML = &rest.XMLSchema{ + Name: input.XML.Name, + Prefix: input.XML.Prefix, + Namespace: input.XML.Namespace, + Wrapped: input.XML.Wrapped, + Attribute: input.XML.Attribute, + } + } + return ps } @@ -407,3 +417,48 @@ func formatOperationName(input string) string { return sb.String() } + +func buildUniqueOperationName(httpSchema *rest.NDCHttpSchema, operationId, pathKey, method string, options *ConvertOptions) string { + opName := formatOperationName(operationId) + exists := opName == "" + if !exists { + _, exists = httpSchema.Functions[opName] + if !exists { + _, exists = httpSchema.Procedures[opName] + } + } + + if exists { + opName = buildPathMethodName(pathKey, method, options) + } + + return opName +} + +// guess the result type from content type +func getResultTypeFromContentType(httpSchema *rest.NDCHttpSchema, contentType string) schema.TypeEncoder { + var scalarName rest.ScalarName + switch { + case strings.HasPrefix(contentType, "text/"): + scalarName = rest.ScalarString + case contentType == rest.ContentTypeOctetStream || strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/"): + scalarName = rest.ScalarBinary + default: + scalarName = rest.ScalarJSON + } + + httpSchema.AddScalar(string(scalarName), *defaultScalarTypes[scalarName]) + + return schema.NewNamedType(string(scalarName)) +} + +// check if the XML object doesn't have any child element. +func isXMLLeafObject(objectType rest.ObjectType) bool { + for _, field := range objectType.Fields { + if field.HTTP == nil || field.HTTP.XML == nil || !field.HTTP.XML.Attribute { + return false + } + } + + return true +} diff --git a/ndc-http-schema/openapi/oas3_test.go b/ndc-http-schema/openapi/oas3_test.go index 35bc6c4..43a0e30 100644 --- a/ndc-http-schema/openapi/oas3_test.go +++ b/ndc-http-schema/openapi/oas3_test.go @@ -33,14 +33,16 @@ func TestOpenAPIv3ToRESTSchema(t *testing.T) { EnvPrefix: "PET_STORE", }, }, - // go run ./ndc-http-schema convert -f ./ndc-http-schema/openapi/testdata/onesignal/source.json -o ./ndc-http-schema/openapi/testdata/onesignal/expected.json --spec openapi3 - // go run ./ndc-http-schema convert -f ./ndc-http-schema/openapi/testdata/onesignal/source.json -o ./ndc-http-schema/openapi/testdata/onesignal/schema.json --pure --spec openapi3 + // go run ./ndc-http-schema convert -f ./ndc-http-schema/openapi/testdata/onesignal/source.json -o ./ndc-http-schema/openapi/testdata/onesignal/expected.json --spec openapi3 --no-deprecation + // go run ./ndc-http-schema convert -f ./ndc-http-schema/openapi/testdata/onesignal/source.json -o ./ndc-http-schema/openapi/testdata/onesignal/schema.json --pure --spec openapi3 --no-deprecation { Name: "onesignal", Source: "testdata/onesignal/source.json", Expected: "testdata/onesignal/expected.json", Schema: "testdata/onesignal/schema.json", - Options: ConvertOptions{}, + Options: ConvertOptions{ + NoDeprecation: true, + }, }, // go run ./ndc-http-schema convert -f ./ndc-http-schema/openapi/testdata/openai/source.json -o ./ndc-http-schema/openapi/testdata/openai/expected.json --spec openapi3 // go run ./ndc-http-schema convert -f ./ndc-http-schema/openapi/testdata/openai/source.json -o ./ndc-http-schema/openapi/testdata/openai/schema.json --pure --spec openapi3 diff --git a/ndc-http-schema/openapi/testdata/jsonplaceholder/expected.json b/ndc-http-schema/openapi/testdata/jsonplaceholder/expected.json index 3e6c6f9..af06af1 100644 --- a/ndc-http-schema/openapi/testdata/jsonplaceholder/expected.json +++ b/ndc-http-schema/openapi/testdata/jsonplaceholder/expected.json @@ -679,6 +679,9 @@ "format": "int64" } } + }, + "xml": { + "name": "Album" } }, "Comment": { @@ -756,6 +759,9 @@ "format": "int64" } } + }, + "xml": { + "name": "Comment" } }, "Photo": { @@ -834,6 +840,9 @@ "format": "uri" } } + }, + "xml": { + "name": "Photo" } }, "Post": { @@ -896,6 +905,9 @@ "format": "int64" } } + }, + "xml": { + "name": "Post" } }, "Todo": { @@ -958,6 +970,9 @@ "format": "int64" } } + }, + "xml": { + "name": "Todo" } }, "User": { @@ -1076,6 +1091,9 @@ ] } } + }, + "xml": { + "name": "User" } }, "UserAddress": { @@ -1150,6 +1168,9 @@ ] } } + }, + "xml": { + "name": "User" } }, "UserAddressGeo": { @@ -1182,6 +1203,9 @@ ] } } + }, + "xml": { + "name": "User" } }, "UserCompany": { @@ -1228,6 +1252,9 @@ ] } } + }, + "xml": { + "name": "User" } } }, diff --git a/ndc-http-schema/openapi/testdata/onesignal/expected.json b/ndc-http-schema/openapi/testdata/onesignal/expected.json index be9e4a4..998cece 100644 --- a/ndc-http-schema/openapi/testdata/onesignal/expected.json +++ b/ndc-http-schema/openapi/testdata/onesignal/expected.json @@ -798,28 +798,6 @@ ] } }, - "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", diff --git a/ndc-http-schema/openapi/testdata/onesignal/schema.json b/ndc-http-schema/openapi/testdata/onesignal/schema.json index 7e98fc7..2909135 100644 --- a/ndc-http-schema/openapi/testdata/onesignal/schema.json +++ b/ndc-http-schema/openapi/testdata/onesignal/schema.json @@ -488,18 +488,6 @@ } } }, - "include_player_ids": { - "type": { - "type": "nullable", - "underlying_type": { - "element_type": { - "name": "String", - "type": "named" - }, - "type": "array" - } - } - }, "included_segments": { "type": { "type": "nullable", diff --git a/ndc-http-schema/openapi/testdata/petstore2/expected.json b/ndc-http-schema/openapi/testdata/petstore2/expected.json index cfb8e31..0884050 100644 --- a/ndc-http-schema/openapi/testdata/petstore2/expected.json +++ b/ndc-http-schema/openapi/testdata/petstore2/expected.json @@ -71,7 +71,9 @@ "name": "aggregated_by", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -88,7 +90,9 @@ "name": "browsers", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -105,7 +109,9 @@ "name": "end_date", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -122,7 +128,9 @@ "name": "limit", "in": "query", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } }, @@ -139,7 +147,9 @@ "name": "offset", "in": "query", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } }, @@ -155,7 +165,9 @@ "name": "on-behalf-of", "in": "header", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -169,7 +181,9 @@ "name": "start_date", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -207,7 +221,9 @@ "name": "list_id", "in": "path", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } }, @@ -223,7 +239,9 @@ "name": "on-behalf-of", "in": "header", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -260,7 +278,9 @@ "name": "on-behalf-of", "in": "header", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -298,7 +318,9 @@ "name": "username", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -315,7 +337,10 @@ "method": "get", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "response": { @@ -336,7 +361,9 @@ "name": "status", "in": "query", "schema": { - "type": ["array"] + "type": [ + "array" + ] } } } @@ -356,7 +383,10 @@ "method": "get", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "response": { @@ -377,7 +407,9 @@ "name": "tags", "in": "query", "schema": { - "type": ["array"] + "type": [ + "array" + ] } } } @@ -430,7 +462,9 @@ "name": "orderId", "in": "path", "schema": { - "type": ["integer"], + "type": [ + "integer" + ], "maximum": 10, "minimum": 1 } @@ -467,7 +501,9 @@ "name": "petId", "in": "path", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -512,7 +548,9 @@ "name": "username", "in": "path", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -545,7 +583,9 @@ "name": "client_name", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -562,7 +602,9 @@ "name": "limit", "in": "query", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } }, @@ -579,7 +621,9 @@ "name": "offset", "in": "query", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } }, @@ -596,7 +640,9 @@ "name": "owner", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -646,7 +692,9 @@ "name": "password", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -660,7 +708,9 @@ "name": "username", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -686,7 +736,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "unique_clicks": { @@ -699,9 +751,14 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } } + }, + "xml": { + "name": "advanced_stats_clicks" } }, "AdvancedStatsClicksOpens": { @@ -717,7 +774,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "opens": { @@ -730,7 +789,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "unique_clicks": { @@ -743,7 +804,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "unique_opens": { @@ -756,7 +819,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -772,7 +837,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int32" } }, @@ -785,7 +852,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -797,9 +866,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "ApiResponse" } }, "Category": { @@ -813,7 +887,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -826,9 +902,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "Category" } }, "ContactdbCustomFieldWithIdValue": { @@ -843,7 +924,9 @@ } }, "http": { - "type": ["number"] + "type": [ + "number" + ] } }, "name": { @@ -856,7 +939,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -869,7 +954,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "value": { @@ -882,7 +969,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -901,12 +990,19 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["object"] + "type": [ + "object" + ] } } } + }, + "xml": { + "name": "contactdb_recipient" } }, "ContactdbRecipientRecipients": { @@ -924,7 +1020,9 @@ } }, "http": { - "type": ["array"] + "type": [ + "array" + ] } }, "first_name": { @@ -937,9 +1035,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "contactdb_recipient" } }, "DomainAuthenticationDomainSpf": { @@ -951,7 +1054,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "custom_spf": { @@ -961,7 +1066,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "default": { @@ -971,7 +1078,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "dns": { @@ -981,7 +1090,9 @@ "type": "named" }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "domain": { @@ -991,7 +1102,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "id": { @@ -1001,7 +1114,9 @@ "type": "named" }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "ips": { @@ -1014,7 +1129,9 @@ "type": "array" }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { "type": null } @@ -1027,7 +1144,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "subdomain": { @@ -1040,7 +1159,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "user_id": { @@ -1050,7 +1171,9 @@ "type": "named" }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "username": { @@ -1060,7 +1183,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "valid": { @@ -1070,9 +1195,14 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } } + }, + "xml": { + "name": "domain_authentication:domain_spf" } }, "DomainAuthenticationDomainSpfDns": { @@ -1085,7 +1215,9 @@ "type": "named" }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "domain_spf": { @@ -1095,7 +1227,9 @@ "type": "named" }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "mail_server": { @@ -1105,7 +1239,9 @@ "type": "named" }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "subdomain_spf": { @@ -1115,9 +1251,14 @@ "type": "named" }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } + }, + "xml": { + "name": "domain_authentication:domain_spf" } }, "DomainAuthenticationDomainSpfDnsDkim": { @@ -1130,7 +1271,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "host": { @@ -1140,7 +1283,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -1150,7 +1295,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "valid": { @@ -1160,9 +1307,14 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } } + }, + "xml": { + "name": "domain_authentication:domain_spf" } }, "DomainAuthenticationDomainSpfDnsDomainSpf": { @@ -1175,7 +1327,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "host": { @@ -1185,7 +1339,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -1195,7 +1351,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "valid": { @@ -1205,9 +1363,14 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } } + }, + "xml": { + "name": "domain_authentication:domain_spf" } }, "DomainAuthenticationDomainSpfDnsMailServer": { @@ -1220,7 +1383,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "host": { @@ -1230,7 +1395,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -1240,7 +1407,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "valid": { @@ -1250,9 +1419,14 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } } + }, + "xml": { + "name": "domain_authentication:domain_spf" } }, "DomainAuthenticationDomainSpfDnsSubdomainSpf": { @@ -1265,7 +1439,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "host": { @@ -1275,7 +1451,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -1285,7 +1463,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "valid": { @@ -1295,9 +1475,14 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } } + }, + "xml": { + "name": "domain_authentication:domain_spf" } }, "GETBrowsersStatsResult": { @@ -1312,7 +1497,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "stats": { @@ -1328,12 +1515,19 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["object"] + "type": [ + "object" + ] } } } + }, + "xml": { + "name": "GET_browsers_stats" } }, "GETBrowsersStatsResultStats": { @@ -1348,7 +1542,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "name": { @@ -1361,7 +1557,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -1374,9 +1572,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "GET_browsers_stats" } }, "GETContactdbListsListIdRecipientsResult": { @@ -1393,9 +1596,14 @@ } }, "http": { - "type": ["array"] + "type": [ + "array" + ] } } + }, + "xml": { + "name": "GET_contactdb_lists_list_id_recipients" } }, "GETGeoStatsResult": { @@ -1410,7 +1618,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "stats": { @@ -1426,12 +1636,19 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["object"] + "type": [ + "object" + ] } } } + }, + "xml": { + "name": "GET_geo_stats" } }, "GETGeoStatsResultStats": { @@ -1446,7 +1663,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "name": { @@ -1459,7 +1678,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -1472,9 +1693,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "GET_geo_stats" } }, "OAuth2Client": { @@ -1489,7 +1715,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "client_name": { @@ -1502,7 +1730,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "client_secret": { @@ -1515,7 +1745,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "client_secret_expires_at": { @@ -1528,7 +1760,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -1542,9 +1776,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "oAuth2Client" } }, "Order": { @@ -1558,7 +1797,9 @@ } }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "id": { @@ -1570,7 +1811,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -1583,7 +1826,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -1596,7 +1841,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int32" } }, @@ -1609,7 +1856,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "format": "date-time" } }, @@ -1623,9 +1872,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "Order" } }, "Pet": { @@ -1639,7 +1893,12 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ], + "xml": { + "name": "Category" + } } }, "field": { @@ -1664,7 +1923,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -1674,7 +1935,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "photoUrls": { @@ -1686,9 +1949,20 @@ "type": "array" }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"] + "type": [ + "string" + ], + "xml": { + "name": "photoUrl" + } + }, + "xml": { + "name": "", + "wrapped": true } } }, @@ -1702,7 +1976,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "tags": { @@ -1717,9 +1993,18 @@ } }, "http": { - "type": ["array"] + "type": [ + "array" + ], + "xml": { + "name": "", + "wrapped": true + } } } + }, + "xml": { + "name": "Pet" } }, "SnakeObject": { @@ -1745,7 +2030,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -1758,9 +2045,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "User" } }, "Tag": { @@ -1774,7 +2066,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -1787,9 +2081,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "Tag" } }, "UpdatePetWithFormBody": { @@ -1804,7 +2103,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "status": { @@ -1817,7 +2118,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -1834,7 +2137,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "file": { @@ -1847,7 +2152,9 @@ } }, "http": { - "type": ["file"] + "type": [ + "file" + ] } } } @@ -1863,7 +2170,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "firstName": { @@ -1875,7 +2184,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "id": { @@ -1887,7 +2198,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -1900,7 +2213,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "password": { @@ -1912,7 +2227,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "phone": { @@ -1924,7 +2241,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "userStatus": { @@ -1937,7 +2256,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int32" } }, @@ -1950,9 +2271,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "User" } } }, @@ -1963,7 +2289,10 @@ "method": "post", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "requestBody": { @@ -1983,7 +2312,12 @@ "http": { "in": "body", "schema": { - "type": ["object"] + "type": [ + "object" + ], + "xml": { + "name": "Pet" + } } } } @@ -2031,7 +2365,9 @@ "name": "orderId", "in": "path", "schema": { - "type": ["integer"], + "type": [ + "integer" + ], "minimum": 1 } } @@ -2052,7 +2388,10 @@ "method": "delete", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "response": { @@ -2072,7 +2411,9 @@ "name": "api_key", "in": "header", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -2086,7 +2427,9 @@ "name": "petId", "in": "path", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -2119,7 +2462,9 @@ "name": "username", "in": "path", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -2153,7 +2498,9 @@ "http": { "in": "body", "schema": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -2185,7 +2532,12 @@ "http": { "in": "body", "schema": { - "type": ["object"] + "type": [ + "object" + ], + "xml": { + "name": "Order" + } } } } @@ -2196,13 +2548,64 @@ "type": "named" } }, + "putPetXml": { + "request": { + "url": "/pet/xml", + "method": "put", + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "requestBody": { + "contentType": "application/xml" + }, + "response": { + "contentType": "application/xml" + } + }, + "arguments": { + "body": { + "description": "Pet object that needs to be added to the store", + "type": { + "name": "Pet", + "type": "named" + }, + "http": { + "in": "body", + "schema": { + "type": [ + "object" + ], + "xml": { + "name": "Pet" + } + } + } + } + }, + "description": "PUT /pet/xml", + "result_type": { + "type": "nullable", + "underlying_type": { + "name": "Boolean", + "type": "named" + } + } + }, "updatePet": { "request": { "url": "/pet", "method": "put", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "requestBody": { @@ -2222,7 +2625,12 @@ "http": { "in": "body", "schema": { - "type": ["object"] + "type": [ + "object" + ], + "xml": { + "name": "Pet" + } } } } @@ -2242,7 +2650,10 @@ "method": "post", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "requestBody": { @@ -2262,7 +2673,9 @@ "http": { "in": "formData", "schema": { - "type": ["object"] + "type": [ + "object" + ] } } }, @@ -2276,7 +2689,9 @@ "name": "petId", "in": "path", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -2311,7 +2726,12 @@ "http": { "in": "body", "schema": { - "type": ["object"] + "type": [ + "object" + ], + "xml": { + "name": "User" + } } } }, @@ -2325,7 +2745,9 @@ "name": "username", "in": "path", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -2345,7 +2767,10 @@ "method": "post", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "requestBody": { @@ -2365,7 +2790,9 @@ "http": { "in": "formData", "schema": { - "type": ["object"] + "type": [ + "object" + ] } } }, @@ -2379,7 +2806,9 @@ "name": "petId", "in": "path", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -2410,7 +2839,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["day", "week", "month"], + "one_of": [ + "day", + "week", + "month" + ], "type": "enum" } }, @@ -2418,7 +2851,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["date", "text", "number"], + "one_of": [ + "date", + "text", + "number" + ], "type": "enum" } }, @@ -2461,7 +2898,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["placed", "approved", "delivered"], + "one_of": [ + "placed", + "approved", + "delivered" + ], "type": "enum" } }, @@ -2469,7 +2910,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["available", "pending", "sold"], + "one_of": [ + "available", + "pending", + "sold" + ], "type": "enum" } }, diff --git a/ndc-http-schema/openapi/testdata/petstore2/schema.json b/ndc-http-schema/openapi/testdata/petstore2/schema.json index 88ff895..a660857 100644 --- a/ndc-http-schema/openapi/testdata/petstore2/schema.json +++ b/ndc-http-schema/openapi/testdata/petstore2/schema.json @@ -1449,6 +1449,26 @@ "type": "named" } }, + { + "arguments": { + "body": { + "description": "Pet object that needs to be added to the store", + "type": { + "name": "Pet", + "type": "named" + } + } + }, + "description": "PUT /pet/xml", + "name": "putPetXml", + "result_type": { + "type": "nullable", + "underlying_type": { + "name": "Boolean", + "type": "named" + } + } + }, { "arguments": { "body": { diff --git a/ndc-http-schema/openapi/testdata/petstore2/swagger.json b/ndc-http-schema/openapi/testdata/petstore2/swagger.json index 0e0e09d..4ae38ad 100644 --- a/ndc-http-schema/openapi/testdata/petstore2/swagger.json +++ b/ndc-http-schema/openapi/testdata/petstore2/swagger.json @@ -119,6 +119,29 @@ "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] } }, + + "/pet/xml": { + "put": { + "operationId": "updatePet", + "consumes": ["application/xml"], + "produces": ["application/xml"], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { "$ref": "#/definitions/Pet" } + } + ], + "responses": { + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Pet not found" }, + "405": { "description": "Validation exception" } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, "/pet/findByStatus": { "get": { "tags": ["pet"], diff --git a/ndc-http-schema/openapi/testdata/petstore3/expected.json b/ndc-http-schema/openapi/testdata/petstore3/expected.json index 4ae617e..2e69209 100644 --- a/ndc-http-schema/openapi/testdata/petstore3/expected.json +++ b/ndc-http-schema/openapi/testdata/petstore3/expected.json @@ -50,7 +50,10 @@ "security": [ {}, { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "version": "1.0.19" @@ -79,7 +82,9 @@ "name": "collection_method", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -116,7 +121,9 @@ "name": "customer", "in": "query", "schema": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -153,7 +160,9 @@ "name": "ending_before", "in": "query", "schema": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -176,9 +185,13 @@ "name": "expand", "in": "query", "schema": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -198,7 +211,9 @@ "name": "limit", "in": "query", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } }, @@ -216,7 +231,9 @@ "name": "starting_after", "in": "query", "schema": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -235,7 +252,9 @@ "name": "status", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -253,7 +272,9 @@ "name": "subscription", "in": "query", "schema": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -271,7 +292,10 @@ "method": "get", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "response": { @@ -292,7 +316,9 @@ "name": "start_date", "in": "query", "schema": { - "type": ["number"] + "type": [ + "number" + ] } } }, @@ -310,7 +336,9 @@ "name": "status", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -330,7 +358,10 @@ "method": "get", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "response": { @@ -355,9 +386,13 @@ "name": "tags", "in": "query", "schema": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -372,6 +407,69 @@ "type": "array" } }, + "getArchitectures": { + "request": { + "url": "/architectures", + "method": "get", + "response": { + "contentType": "application/xml; charset=utf-8" + } + }, + "arguments": {}, + "description": "List all known architectures.", + "result_type": { + "name": "GetArchitecturesResult", + "type": "named" + } + }, + "getBuildProjectNameRepositoryNameBuildconfig": { + "request": { + "url": "/build/{project_name}/{repository_name}/_buildconfig", + "method": "get", + "response": { + "contentType": "text/plain" + } + }, + "arguments": { + "project_name": { + "description": "Project name", + "type": { + "name": "String", + "type": "named" + }, + "http": { + "name": "project_name", + "in": "path", + "schema": { + "type": [ + "string" + ] + } + } + }, + "repository_name": { + "description": "Repository name", + "type": { + "name": "String", + "type": "named" + }, + "http": { + "name": "repository_name", + "in": "path", + "schema": { + "type": [ + "string" + ] + } + } + } + }, + "description": "Show the build configuration for the specified repository.", + "result_type": { + "name": "String", + "type": "named" + } + }, "getInventory": { "request": { "url": "/store/inventory", @@ -411,7 +509,9 @@ "name": "orderId", "in": "path", "schema": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } } @@ -432,7 +532,10 @@ "api_key": [] }, { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "response": { @@ -450,7 +553,9 @@ "name": "petId", "in": "path", "schema": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } } @@ -462,6 +567,92 @@ "type": "named" } }, + "getSearchXml": { + "request": { + "url": "/search/xml", + "method": "get", + "response": { + "contentType": "application/xml; charset=utf-8" + } + }, + "arguments": { + "in": { + "description": "Where to search and apply a XPath expression: either the list of projects or the list of packages. Example of a result when `projects` is selected: ``` x86_64 x86_64 Standard OBS instance at build.opensuse.org This instance delivers the default build targets for OBS. https://api.opensuse.org/public ``` Example of a result when `packages` is selected: ``` ```", + "type": { + "name": "SearchIn", + "type": "named" + }, + "http": { + "name": "in", + "in": "query", + "schema": { + "type": [ + "string" + ] + } + } + }, + "match": { + "description": "XPath expression used to filter the results.", + "type": { + "name": "String", + "type": "named" + }, + "http": { + "name": "match", + "in": "query", + "schema": { + "type": [ + "string" + ] + } + } + }, + "return": { + "description": "XPath expression that defines which values will be returned. Instead of returning a collection of projects or packages, the result will be a collection of: - contents of elements, when that XPath expression match elements, or, - values of atttributes, when that XPath expression match atttributes. Example of result, for a query string like `?in=projects\u0026match=repository/arch='x86_64'\u0026return=repository/name`: ``` openSUSE_Tumbleweed 15.3 ```", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "name": "return", + "in": "query", + "schema": { + "type": [ + "string" + ] + } + } + }, + "values": { + "description": "When set to `1`, return a list of values, instead of returning a list of projects or packages. The result will be a collection of: - contents of elements, when the expression defined in the `match` query parameter match elements. For example: `match=repository/arch`. - values of atttributes, when the expression defined in the `match` query parameter match atttributes. For example: `match=repository/name`. Example of result, for a query string like `?in=projects\u0026match=repository/path/project\u0026values=1`: ``` openSUSE.org:openSUSE:Factory openSUSE.org:openSUSE:Leap:15.3 ```", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "name": "values", + "in": "query", + "schema": { + "type": [ + "string" + ] + } + } + } + }, + "description": "Search in projects or in packages.", + "result_type": { + "name": "JSON", + "type": "named" + } + }, "getSnake": { "request": { "url": "/snake", @@ -496,7 +687,9 @@ "name": "username", "in": "path", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -529,7 +722,9 @@ "name": "password", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -546,7 +741,9 @@ "name": "username", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -570,7 +767,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "state": { @@ -582,7 +781,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "street": { @@ -594,7 +795,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "zip": { @@ -606,9 +809,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "address" } }, "ApiResponse": { @@ -622,7 +830,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int32" } }, @@ -635,7 +845,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -647,27 +859,37 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "##default" } }, - "Category": { + "Book": { "fields": { - "id": { + "attr": { "type": { "type": "nullable", "underlying_type": { - "name": "Int64", + "name": "String", "type": "named" } }, "http": { - "type": ["integer"], - "format": "int64" + "type": [ + "string" + ], + "xml": { + "prefix": "smp", + "attribute": true + } } }, - "name": { + "author": { "type": { "type": "nullable", "underlying_type": { @@ -676,14 +898,26 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } - } - } - }, - "CreateFineTuningJobRequest": { - "fields": { - "model": { + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ] + } + }, + "title": { "type": { "type": "nullable", "underlying_type": { @@ -692,220 +926,532 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "book", + "prefix": "smp", + "namespace": "http://example.com/schema" } }, - "GetInvoicesResult": { + "Category": { "fields": { - "has_more": { - "description": "True if this list has another page of items after this one that can be fetched.", + "id": { "type": { - "name": "Boolean", - "type": "named" + "type": "nullable", + "underlying_type": { + "name": "Int64", + "type": "named" + } }, "http": { - "type": ["boolean"] + "type": [ + "integer" + ], + "format": "int64" } }, - "object": { - "description": "String representing the object's type. Objects of the same type share the same value. Always has the value `list`.", + "name": { "type": { - "name": "InvoicesObject", - "type": "named" + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } - }, - "url": { - "description": "The URL where this list can be accessed.", + } + }, + "xml": { + "name": "category" + } + }, + "CreateFineTuningJobRequest": { + "fields": { + "model": { "type": { - "name": "String", - "type": "named" + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } }, "http": { - "type": ["string"], - "pattern": "^/v1/invoices", - "maxLength": 5000 + "type": [ + "string" + ] } } } }, - "Order": { + "GetArchitecturesResult": { "fields": { - "complete": { + "count": { "type": { "type": "nullable", "underlying_type": { - "name": "Boolean", + "name": "Int32", "type": "named" } }, "http": { - "type": ["boolean"] + "type": [ + "integer" + ], + "xml": { + "attribute": true + } } }, - "id": { + "entry": { "type": { "type": "nullable", "underlying_type": { - "name": "Int64", - "type": "named" + "element_type": { + "name": "GetArchitecturesResultEntry", + "type": "named" + }, + "type": "array" } }, "http": { - "type": ["integer"], - "format": "int64" + "type": [ + "array" + ], + "items": { + "type": [ + "object" + ] + } } }, - "petId": { + "name": { "type": { "type": "nullable", "underlying_type": { - "name": "Int64", + "name": "String", "type": "named" } }, "http": { - "type": ["integer"], - "format": "int64" + "type": [ + "string" + ], + "xml": { + "attribute": true + } } }, - "quantity": { + "rev": { "type": { "type": "nullable", "underlying_type": { - "name": "Int32", + "name": "String", "type": "named" } }, "http": { - "type": ["integer"], - "format": "int32" + "type": [ + "string" + ], + "xml": { + "attribute": true + } } }, - "shipDate": { + "srcmd5": { "type": { "type": "nullable", "underlying_type": { - "name": "TimestampTZ", + "name": "String", "type": "named" } }, "http": { - "type": ["string"], - "format": "date-time" + "type": [ + "string" + ], + "xml": { + "attribute": true + } } }, - "status": { - "description": "Order Status", + "vrev": { "type": { "type": "nullable", "underlying_type": { - "name": "OrderStatus", + "name": "String", "type": "named" } }, "http": { - "type": ["string"] + "type": [ + "string" + ], + "xml": { + "attribute": true + } } } + }, + "xml": { + "name": "directory" } }, - "Pet": { + "GetArchitecturesResultEntry": { "fields": { - "category": { + "md5": { "type": { "type": "nullable", "underlying_type": { - "name": "Category", + "name": "String", "type": "named" } }, "http": { - "type": ["object"] + "type": [ + "string" + ], + "xml": { + "attribute": true + } } }, - "field": { - "description": "This empty field is returned instead of the list of scopes if the user making the call doesn't have the authorization required.", + "mtime": { "type": { "type": "nullable", "underlying_type": { - "name": "JSON", + "name": "String", "type": "named" } }, "http": { - "type": null + "type": [ + "string" + ], + "xml": { + "attribute": true + } } }, - "id": { + "name": { "type": { "type": "nullable", "underlying_type": { - "name": "Int64", + "name": "String", "type": "named" } }, "http": { - "type": ["integer"], - "format": "int64" - } - }, - "name": { - "type": { - "name": "String", - "type": "named" - }, - "http": { - "type": ["string"] + "type": [ + "string" + ], + "xml": { + "attribute": true + } } }, - "photoUrls": { + "size": { "type": { - "element_type": { + "type": "nullable", + "underlying_type": { "name": "String", "type": "named" - }, - "type": "array" + } }, "http": { - "type": ["array"], - "items": { - "type": ["string"] + "type": [ + "string" + ], + "xml": { + "attribute": true } } }, - "status": { - "description": "pet status in the store", + "xmlValue": { + "description": "Value of the xml field", "type": { - "type": "nullable", - "underlying_type": { - "name": "PetStatus", - "type": "named" - } + "name": "String", + "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ], + "xml": { + "text": true + } } - }, - "tags": { - "type": { - "type": "nullable", - "underlying_type": { - "element_type": { - "name": "Tag", + } + } + }, + "GetInvoicesResult": { + "fields": { + "has_more": { + "description": "True if this list has another page of items after this one that can be fetched.", + "type": { + "name": "Boolean", + "type": "named" + }, + "http": { + "type": [ + "boolean" + ] + } + }, + "object": { + "description": "String representing the object's type. Objects of the same type share the same value. Always has the value `list`.", + "type": { + "name": "InvoicesObject", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + }, + "url": { + "description": "The URL where this list can be accessed.", + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ], + "pattern": "^/v1/invoices", + "maxLength": 5000 + } + } + } + }, + "Order": { + "fields": { + "complete": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Boolean", + "type": "named" + } + }, + "http": { + "type": [ + "boolean" + ] + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int64", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "format": "int64" + } + }, + "petId": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int64", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "format": "int64" + } + }, + "quantity": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "format": "int32" + } + }, + "shipDate": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "TimestampTZ", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "format": "date-time" + } + }, + "status": { + "description": "Order Status", + "type": { + "type": "nullable", + "underlying_type": { + "name": "OrderStatus", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + } + }, + "xml": { + "name": "order" + } + }, + "Pet": { + "fields": { + "category": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Category", + "type": "named" + } + }, + "http": { + "type": [ + "object" + ], + "xml": { + "name": "category" + } + } + }, + "field": { + "description": "This empty field is returned instead of the list of scopes if the user making the call doesn't have the authorization required.", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + }, + "http": { + "type": null + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int64", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "format": "int64" + } + }, + "name": { + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ] + } + }, + "photoUrls": { + "type": { + "element_type": { + "name": "String", + "type": "named" + }, + "type": "array" + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "string" + ], + "xml": { + "name": "photoUrl" + } + }, + "xml": { + "wrapped": true + } + } + }, + "status": { + "description": "pet status in the store", + "type": { + "type": "nullable", + "underlying_type": { + "name": "PetStatus", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + }, + "tags": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "Tag", "type": "named" }, "type": "array" } }, "http": { - "type": ["array"] + "type": [ + "array" + ], + "xml": { + "wrapped": true + } } } + }, + "xml": { + "name": "pet" } }, "PostCheckoutSessionsBody": { @@ -920,7 +1466,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "allow_promotion_codes": { @@ -933,7 +1481,9 @@ } }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "automatic_tax": { @@ -946,7 +1496,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "billing_address_collection": { @@ -959,7 +1511,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "cancel_url": { @@ -972,7 +1526,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -986,7 +1542,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 200 } }, @@ -1000,7 +1558,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "currency": { @@ -1013,7 +1573,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "custom_fields": { @@ -1029,9 +1591,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["object"] + "type": [ + "object" + ] } } }, @@ -1045,7 +1611,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "customer": { @@ -1058,7 +1626,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -1072,7 +1642,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "customer_email": { @@ -1085,7 +1657,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "customer_update": { @@ -1098,7 +1672,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "discounts": { @@ -1114,9 +1690,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["object"] + "type": [ + "object" + ] } } }, @@ -1133,9 +1713,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -1150,7 +1734,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "unix-time" } }, @@ -1164,7 +1750,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "line_items": { @@ -1180,9 +1768,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["object"] + "type": [ + "object" + ] } } }, @@ -1196,7 +1788,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "metadata": { @@ -1209,7 +1803,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "mode": { @@ -1222,7 +1818,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "payment_intent_data": { @@ -1235,7 +1833,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "payment_method_collection": { @@ -1248,7 +1848,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "payment_method_configuration": { @@ -1261,7 +1863,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 100 } }, @@ -1275,7 +1879,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "payment_method_types": { @@ -1291,9 +1897,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -1307,7 +1917,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "redirect_on_completion": { @@ -1320,7 +1932,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "return_url": { @@ -1333,7 +1947,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -1347,7 +1963,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "shipping_address_collection": { @@ -1360,7 +1978,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "shipping_options": { @@ -1376,9 +1996,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["object"] + "type": [ + "object" + ] } } }, @@ -1392,7 +2016,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "subscription_data": { @@ -1405,7 +2031,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "success_url": { @@ -1418,7 +2046,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -1432,7 +2062,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "ui_mode": { @@ -1445,7 +2077,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -1462,7 +2096,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -1478,7 +2114,9 @@ } }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "enabled": { @@ -1487,7 +2125,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } } } @@ -1501,7 +2141,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "liability": { @@ -1513,7 +2155,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -1529,7 +2173,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -1538,7 +2184,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -1555,7 +2203,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "promotions": { @@ -1567,7 +2217,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "terms_of_service": { @@ -1579,7 +2231,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -1592,7 +2246,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -1608,7 +2264,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "key": { @@ -1617,7 +2275,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 200 } }, @@ -1627,7 +2287,9 @@ "type": "named" }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "numeric": { @@ -1639,7 +2301,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "optional": { @@ -1651,7 +2315,9 @@ } }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "text": { @@ -1663,7 +2329,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "type": { @@ -1672,7 +2340,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -1688,9 +2358,13 @@ "type": "array" }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -1704,7 +2378,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 100 } }, @@ -1714,7 +2390,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 100 } } @@ -1728,7 +2406,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 50 } }, @@ -1738,7 +2418,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -1754,7 +2436,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "minimum_length": { @@ -1766,7 +2450,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -1782,7 +2468,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "minimum_length": { @@ -1794,7 +2482,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -1811,7 +2501,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "shipping_address": { @@ -1823,7 +2515,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "submit": { @@ -1835,7 +2529,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "terms_of_service_acceptance": { @@ -1847,7 +2543,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -1860,7 +2558,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 1200 } } @@ -1874,7 +2574,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 1200 } } @@ -1888,7 +2590,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 1200 } } @@ -1902,7 +2606,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 1200 } } @@ -1920,7 +2626,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "name": { @@ -1932,7 +2640,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "shipping": { @@ -1944,7 +2654,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -1960,7 +2672,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -1973,7 +2687,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -1988,7 +2704,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "invoice_data": { @@ -2000,7 +2718,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -2019,9 +2739,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -2038,9 +2762,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["object"] + "type": [ + "object" + ] } } }, @@ -2053,7 +2781,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 1500 } }, @@ -2066,7 +2796,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2079,7 +2811,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "metadata": { @@ -2091,7 +2825,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "rendering_options": { @@ -2103,7 +2839,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -2116,7 +2854,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 40 } }, @@ -2126,7 +2866,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 140 } } @@ -2143,7 +2885,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -2152,7 +2896,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -2168,7 +2914,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -2184,7 +2932,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "dynamic_tax_rates": { @@ -2199,9 +2949,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -2215,7 +2969,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2228,7 +2984,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "quantity": { @@ -2240,7 +2998,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "tax_rates": { @@ -2255,9 +3015,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -2272,7 +3036,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "maximum": { @@ -2284,7 +3050,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "minimum": { @@ -2296,7 +3064,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -2309,7 +3079,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "product": { @@ -2321,7 +3093,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2334,7 +3108,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "recurring": { @@ -2346,7 +3122,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "tax_behavior": { @@ -2358,7 +3136,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "unit_amount": { @@ -2370,7 +3150,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "unit_amount_decimal": { @@ -2382,7 +3164,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "format": "decimal" } } @@ -2399,7 +3183,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 40000 } }, @@ -2415,9 +3201,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -2430,7 +3220,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "name": { @@ -2439,7 +3231,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2452,7 +3246,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -2466,7 +3262,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "interval_count": { @@ -2478,7 +3276,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -2495,7 +3295,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "capture_method": { @@ -2507,7 +3309,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "description": { @@ -2519,7 +3323,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 1000 } }, @@ -2532,7 +3338,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "on_behalf_of": { @@ -2544,7 +3352,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "receipt_email": { @@ -2556,7 +3366,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "setup_future_usage": { @@ -2568,7 +3380,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "shipping": { @@ -2580,7 +3394,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "statement_descriptor": { @@ -2592,7 +3408,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 22 } }, @@ -2605,7 +3423,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 22 } }, @@ -2618,7 +3438,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "transfer_group": { @@ -2630,7 +3452,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -2643,7 +3467,9 @@ "type": "named" }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "carrier": { @@ -2655,7 +3481,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2665,7 +3493,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2678,7 +3508,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2691,7 +3523,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -2708,7 +3542,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2721,7 +3557,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2731,7 +3569,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2744,7 +3584,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2757,7 +3599,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -2770,7 +3614,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -2787,7 +3633,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "destination": { @@ -2796,7 +3644,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -2813,7 +3663,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "affirm": { @@ -2825,7 +3677,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "afterpay_clearpay": { @@ -2837,7 +3691,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "alipay": { @@ -2849,7 +3705,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "au_becs_debit": { @@ -2861,7 +3719,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "bacs_debit": { @@ -2873,7 +3733,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "bancontact": { @@ -2885,7 +3747,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "boleto": { @@ -2897,7 +3761,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "card": { @@ -2909,7 +3775,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "cashapp": { @@ -2921,7 +3789,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "customer_balance": { @@ -2933,7 +3803,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "eps": { @@ -2945,7 +3817,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "fpx": { @@ -2957,7 +3831,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "giropay": { @@ -2969,7 +3845,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "grabpay": { @@ -2981,7 +3859,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "ideal": { @@ -2993,7 +3873,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "klarna": { @@ -3005,7 +3887,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "konbini": { @@ -3017,7 +3901,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "link": { @@ -3029,7 +3915,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "oxxo": { @@ -3041,7 +3929,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "p24": { @@ -3053,7 +3943,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "paynow": { @@ -3065,7 +3957,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "paypal": { @@ -3077,7 +3971,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "pix": { @@ -3089,7 +3985,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "revolut_pay": { @@ -3101,7 +3999,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "sepa_debit": { @@ -3113,7 +4013,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "sofort": { @@ -3125,7 +4027,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "swish": { @@ -3137,7 +4041,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "us_bank_account": { @@ -3149,7 +4055,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "wechat_pay": { @@ -3161,7 +4069,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -3177,7 +4087,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "currency": { @@ -3189,7 +4101,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "mandate_options": { @@ -3201,7 +4115,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "setup_future_usage": { @@ -3213,7 +4129,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "verification_method": { @@ -3225,7 +4143,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3241,7 +4161,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "default_for": { @@ -3256,9 +4178,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -3271,7 +4197,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 500 } }, @@ -3284,7 +4212,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "transaction_type": { @@ -3296,7 +4226,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3312,7 +4244,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3328,7 +4262,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3344,7 +4280,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3360,7 +4298,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3376,7 +4316,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3392,7 +4334,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3408,7 +4352,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "setup_future_usage": { @@ -3420,7 +4366,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3436,7 +4384,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "request_three_d_secure": { @@ -3448,7 +4398,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "setup_future_usage": { @@ -3460,7 +4412,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "statement_descriptor_suffix_kana": { @@ -3472,7 +4426,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 22 } }, @@ -3485,7 +4441,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 17 } } @@ -3502,7 +4460,9 @@ } }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } } } @@ -3518,7 +4478,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3534,7 +4496,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "funding_type": { @@ -3546,7 +4510,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "setup_future_usage": { @@ -3558,7 +4524,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3574,7 +4542,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "requested_address_types": { @@ -3589,9 +4559,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -3601,7 +4575,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3614,7 +4590,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -3631,7 +4609,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3647,7 +4627,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3663,7 +4645,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3679,7 +4663,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3695,7 +4681,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3711,7 +4699,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3727,7 +4717,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "setup_future_usage": { @@ -3739,7 +4731,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3755,7 +4749,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3771,7 +4767,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "setup_future_usage": { @@ -3783,7 +4781,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3799,7 +4799,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "tos_shown_and_accepted": { @@ -3811,7 +4813,9 @@ } }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } } } @@ -3827,7 +4831,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3843,7 +4849,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "preferred_locale": { @@ -3855,7 +4863,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "reference": { @@ -3867,7 +4877,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 127 } }, @@ -3880,7 +4892,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 32 } }, @@ -3893,7 +4907,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3909,7 +4925,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -3925,7 +4943,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3941,7 +4961,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3957,7 +4979,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -3973,7 +4997,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -3990,7 +5016,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "setup_future_usage": { @@ -4002,7 +5030,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "verification_method": { @@ -4014,7 +5044,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -4033,9 +5065,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -4052,9 +5088,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -4071,7 +5111,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -4081,7 +5123,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "setup_future_usage": { @@ -4093,7 +5137,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -4107,7 +5153,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } } } @@ -4124,7 +5172,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 1000 } }, @@ -4137,7 +5187,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "on_behalf_of": { @@ -4149,7 +5201,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -4166,9 +5220,13 @@ "type": "array" }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -4185,7 +5243,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -4198,7 +5258,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -4214,7 +5276,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "display_name": { @@ -4223,7 +5287,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 100 } }, @@ -4236,7 +5302,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "metadata": { @@ -4248,7 +5316,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "tax_behavior": { @@ -4260,7 +5330,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "tax_code": { @@ -4272,7 +5344,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -4284,7 +5358,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -4300,7 +5376,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "minimum": { @@ -4312,7 +5390,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -4325,7 +5405,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "value": { @@ -4334,7 +5416,9 @@ "type": "named" }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -4347,7 +5431,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "value": { @@ -4356,7 +5442,9 @@ "type": "named" }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -4369,7 +5457,9 @@ "type": "named" }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "currency": { @@ -4378,7 +5468,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "currency_options": { @@ -4390,7 +5482,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -4407,7 +5501,9 @@ } }, "http": { - "type": ["number"] + "type": [ + "number" + ] } }, "billing_cycle_anchor": { @@ -4419,7 +5515,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "unix-time" } }, @@ -4435,9 +5533,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -4451,7 +5553,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 500 } }, @@ -4464,7 +5568,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "metadata": { @@ -4476,7 +5582,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "on_behalf_of": { @@ -4488,7 +5596,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "proration_behavior": { @@ -4500,7 +5610,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "transfer_data": { @@ -4512,7 +5624,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "trial_end": { @@ -4524,7 +5638,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "unix-time" } }, @@ -4537,7 +5653,9 @@ } }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "trial_settings": { @@ -4549,7 +5667,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -4565,7 +5685,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -4581,7 +5703,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "type": { @@ -4590,7 +5714,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -4606,7 +5732,9 @@ } }, "http": { - "type": ["number"] + "type": [ + "number" + ] } }, "destination": { @@ -4615,7 +5743,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -4628,7 +5758,9 @@ "type": "named" }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -4641,7 +5773,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -4655,7 +5789,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } } } @@ -4675,9 +5811,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -4694,9 +5834,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -4708,7 +5852,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "format": "binary" } }, @@ -4722,7 +5868,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "purpose": { @@ -4732,7 +5880,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -4746,7 +5896,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "expires_at": { @@ -4758,7 +5910,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "unix-time" } }, @@ -4791,9 +5945,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -4808,7 +5966,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } } } @@ -4820,12 +5980,520 @@ "type": { "type": "nullable", "underlying_type": { - "name": "TestHelpersCode", + "name": "TestHelpersCode", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ] + } + } + } + }, + "PutCommentXmlBody": { + "fields": { + "comment": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "PutCommentXmlBodyComment", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "object" + ] + } + } + }, + "comment_count": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "name": "comment", + "attribute": true + } + } + }, + "package": { + "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 + } + } + }, + "request": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "attribute": true + } + } + }, + "user": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + } + }, + "xml": { + "name": "comments" + } + }, + "PutCommentXmlBodyComment": { + "fields": { + "bsrequest": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "attribute": true + } + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "attribute": true + } + } + }, + "package": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "parent": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "attribute": true + } + } + }, + "project": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "when": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "who": { + "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 + } + } + } + } + }, + "PutCommentXmlResult": { + "fields": { + "comment": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "PutCommentXmlResultComment", + "type": "named" + }, + "type": "array" + } + }, + "http": { + "type": [ + "array" + ], + "items": { + "type": [ + "object" + ] + } + } + }, + "comment_count": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "name": "comment", + "attribute": true + } + } + }, + "package": { + "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 + } + } + }, + "request": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "attribute": true + } + } + }, + "user": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + } + }, + "xml": { + "name": "comments" + } + }, + "PutCommentXmlResultComment": { + "fields": { + "bsrequest": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "attribute": true + } + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "attribute": true + } + } + }, + "package": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "parent": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + }, + "http": { + "type": [ + "integer" + ], + "xml": { + "attribute": true + } + } + }, + "project": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "when": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "http": { + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "who": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", "type": "named" } }, "http": { - "type": ["string"] + "type": [ + "string" + ], + "xml": { + "attribute": true + } + } + }, + "xmlValue": { + "description": "Value of the xml field", + "type": { + "name": "String", + "type": "named" + }, + "http": { + "type": [ + "string" + ], + "xml": { + "text": true + } } } } @@ -4841,7 +6509,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "id": { @@ -4853,7 +6523,12 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ], + "xml": { + "name": "order" + } } } } @@ -4869,7 +6544,9 @@ } }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "id": { @@ -4881,7 +6558,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -4894,7 +6573,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -4907,7 +6588,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int32" } }, @@ -4920,7 +6603,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "format": "date-time" } }, @@ -4934,9 +6619,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "order" } }, "Tag": { @@ -4950,7 +6640,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -4963,9 +6655,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "tag" } }, "TreasuryInboundTransfer": { @@ -4978,7 +6675,9 @@ "type": "named" }, "http": { - "type": ["integer"] + "type": [ + "integer" + ] } }, "cancelable": { @@ -4988,7 +6687,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "context": { @@ -5010,7 +6711,9 @@ "type": "named" }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "unix-time" } }, @@ -5021,7 +6724,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "description": { @@ -5034,7 +6739,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -5048,7 +6755,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "financial_account": { @@ -5058,7 +6767,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -5072,7 +6783,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -5083,7 +6796,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -5094,7 +6809,9 @@ "type": "named" }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "metadata": { @@ -5104,7 +6821,9 @@ "type": "named" }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "object": { @@ -5114,7 +6833,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "origin_payment_method": { @@ -5124,7 +6845,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -5138,7 +6861,9 @@ } }, "http": { - "type": ["boolean"] + "type": [ + "boolean" + ] } }, "statement_descriptor": { @@ -5148,7 +6873,9 @@ "type": "named" }, "http": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } }, @@ -5159,7 +6886,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "status_transitions": { @@ -5168,7 +6897,9 @@ "type": "named" }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "transaction": { @@ -5195,7 +6926,9 @@ "type": "named" }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -5212,7 +6945,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "unix-time" } }, @@ -5226,7 +6961,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "unix-time" } }, @@ -5240,7 +6977,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "unix-time" } } @@ -5257,7 +6996,9 @@ } }, "http": { - "type": ["object"] + "type": [ + "object" + ] } }, "addresses": { @@ -5272,7 +7013,9 @@ } }, "http": { - "type": ["array"] + "type": [ + "array" + ] } }, "children": { @@ -5287,9 +7030,13 @@ } }, "http": { - "type": ["array"], + "type": [ + "array" + ], "items": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -5302,7 +7049,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "format": "uuid" } }, @@ -5315,7 +7064,9 @@ } }, "http": { - "type": ["string"], + "type": [ + "string" + ], "format": "binary" } } @@ -5332,7 +7083,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "firstName": { @@ -5344,7 +7097,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "id": { @@ -5356,7 +7111,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } }, @@ -5369,7 +7126,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "password": { @@ -5381,7 +7140,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "phone": { @@ -5393,7 +7154,9 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } }, "userStatus": { @@ -5406,7 +7169,9 @@ } }, "http": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int32" } }, @@ -5419,9 +7184,14 @@ } }, "http": { - "type": ["string"] + "type": [ + "string" + ] } } + }, + "xml": { + "name": "user" } } }, @@ -5445,7 +7215,9 @@ "name": "account", "in": "path", "schema": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -5591,7 +7363,9 @@ "explode": true }, "expand_json": { - "contentType": ["application/json"] + "contentType": [ + "application/json" + ] }, "file": { "headers": { @@ -5599,7 +7373,9 @@ "explode": false, "argumentName": "headerXRateLimitLimit", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -5634,7 +7410,9 @@ "explode": false, "argumentName": "headerXRateLimitLimit", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -5690,7 +7468,9 @@ "name": "id", "in": "path", "schema": { - "type": ["string"], + "type": [ + "string" + ], "maxLength": 5000 } } @@ -5708,7 +7488,10 @@ "method": "post", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "requestBody": { @@ -5834,7 +7617,9 @@ "name": "orderId", "in": "path", "schema": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } } @@ -5855,7 +7640,10 @@ "method": "delete", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "response": { @@ -5875,7 +7663,9 @@ "name": "api_key", "in": "header", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -5889,7 +7679,9 @@ "name": "petId", "in": "path", "schema": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } } @@ -5923,7 +7715,9 @@ "name": "username", "in": "path", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -5969,13 +7763,117 @@ "type": "named" } }, + "putBookXml": { + "request": { + "url": "/book/xml", + "method": "put", + "requestBody": { + "contentType": "application/xml; charset=utf-8" + }, + "response": { + "contentType": "application/xml; charset=utf-8" + } + }, + "arguments": { + "body": { + "description": "Request body of PUT /book/xml", + "type": { + "type": "nullable", + "underlying_type": { + "name": "Book", + "type": "named" + } + }, + "http": { + "in": "body" + } + } + }, + "description": "PUT /book/xml", + "result_type": { + "name": "Book", + "type": "named" + } + }, + "putCommentXml": { + "request": { + "url": "/comment/xml", + "method": "put", + "requestBody": { + "contentType": "application/xml; charset=utf-8" + }, + "response": { + "contentType": "application/xml; charset=utf-8" + } + }, + "arguments": { + "body": { + "description": "Request body of PUT /comment/xml", + "type": { + "type": "nullable", + "underlying_type": { + "name": "PutCommentXmlBody", + "type": "named" + } + }, + "http": { + "in": "body" + } + } + }, + "description": "PUT /comment/xml", + "result_type": { + "name": "PutCommentXmlResult", + "type": "named" + } + }, + "putPetXml": { + "request": { + "url": "/pet/xml", + "method": "put", + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "requestBody": { + "contentType": "application/xml" + }, + "response": { + "contentType": "application/xml" + } + }, + "arguments": { + "body": { + "description": "Request body of PUT /pet/xml", + "type": { + "name": "Pet", + "type": "named" + }, + "http": { + "in": "body" + } + } + }, + "description": "Update an existing pet", + "result_type": { + "name": "Pet", + "type": "named" + } + }, "updatePet": { "request": { "url": "/pet", "method": "put", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "requestBody": { @@ -6009,7 +7907,10 @@ "method": "post", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "response": { @@ -6030,7 +7931,9 @@ "name": "name", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -6044,7 +7947,9 @@ "name": "petId", "in": "path", "schema": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } } @@ -6062,7 +7967,9 @@ "name": "status", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } } @@ -6082,7 +7989,10 @@ "method": "post", "security": [ { - "petstore_auth": ["write:pets", "read:pets"] + "petstore_auth": [ + "write:pets", + "read:pets" + ] } ], "requestBody": { @@ -6106,7 +8016,9 @@ "name": "additionalMetadata", "in": "query", "schema": { - "type": ["string"] + "type": [ + "string" + ] } } }, @@ -6133,7 +8045,9 @@ "name": "petId", "in": "path", "schema": { - "type": ["integer"], + "type": [ + "integer" + ], "format": "int64" } } @@ -6153,13 +8067,18 @@ "contentType": "multipart/form-data", "encoding": { "profileImage": { - "contentType": ["image/png", "image/jpeg"], + "contentType": [ + "image/png", + "image/jpeg" + ], "headers": { "X-Rate-Limit-Limit": { "explode": false, "argumentName": "headerXRateLimitLimit", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -6194,7 +8113,9 @@ "explode": false, "argumentName": "headerXRateLimitLimit", "schema": { - "type": ["integer"] + "type": [ + "integer" + ] } } } @@ -6225,7 +8146,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["auto", "never"], + "one_of": [ + "auto", + "never" + ], "type": "enum" } }, @@ -6233,7 +8157,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["AC", "AD"], + "one_of": [ + "AC", + "AD" + ], "type": "enum" } }, @@ -6241,7 +8168,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["", "exclude_tax", "include_inclusive_tax"], + "one_of": [ + "", + "exclude_tax", + "include_inclusive_tax" + ], "type": "enum" } }, @@ -6249,7 +8180,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["auto", "required"], + "one_of": [ + "auto", + "required" + ], "type": "enum" } }, @@ -6257,7 +8191,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["automatic", "automatic_async", "manual"], + "one_of": [ + "automatic", + "automatic_async", + "manual" + ], "type": "enum" } }, @@ -6265,7 +8203,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["android", "ios", "web"], + "one_of": [ + "android", + "ios", + "web" + ], "type": "enum" } }, @@ -6273,7 +8215,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["cad", "usd"], + "one_of": [ + "cad", + "usd" + ], "type": "enum" } }, @@ -6281,7 +8226,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["always", "if_required"], + "one_of": [ + "always", + "if_required" + ], "type": "enum" } }, @@ -6289,7 +8237,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["invoice", "subscription"], + "one_of": [ + "invoice", + "subscription" + ], "type": "enum" } }, @@ -6297,7 +8248,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["bank_transfer"], + "one_of": [ + "bank_transfer" + ], "type": "enum" } }, @@ -6305,7 +8258,12 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["day", "month", "week", "year"], + "one_of": [ + "day", + "month", + "week", + "year" + ], "type": "enum" } }, @@ -6313,7 +8271,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["auto", "bg", "cs"], + "one_of": [ + "auto", + "bg", + "cs" + ], "type": "enum" } }, @@ -6321,7 +8283,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["cancel", "create_invoice", "pause"], + "one_of": [ + "cancel", + "create_invoice", + "pause" + ], "type": "enum" } }, @@ -6329,7 +8295,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["payment", "setup", "subscription"], + "one_of": [ + "payment", + "setup", + "subscription" + ], "type": "enum" } }, @@ -6337,7 +8307,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["auto", "never"], + "one_of": [ + "auto", + "never" + ], "type": "enum" } }, @@ -6345,7 +8318,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["always", "if_required"], + "one_of": [ + "always", + "if_required" + ], "type": "enum" } }, @@ -6353,7 +8329,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["acss_debit", "affirm"], + "one_of": [ + "acss_debit", + "affirm" + ], "type": "enum" } }, @@ -6361,7 +8340,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["combined", "interval", "sporadic"], + "one_of": [ + "combined", + "interval", + "sporadic" + ], "type": "enum" } }, @@ -6369,7 +8352,12 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["balances", "ownership", "payment_method", "transactions"], + "one_of": [ + "balances", + "ownership", + "payment_method", + "transactions" + ], "type": "enum" } }, @@ -6377,7 +8365,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["auto", "hidden"], + "one_of": [ + "auto", + "hidden" + ], "type": "enum" } }, @@ -6385,7 +8376,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["cs-CZ", "da-DK"], + "one_of": [ + "cs-CZ", + "da-DK" + ], "type": "enum" } }, @@ -6393,7 +8387,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["balances", "transactions"], + "one_of": [ + "balances", + "transactions" + ], "type": "enum" } }, @@ -6401,7 +8398,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["auto", "none"], + "one_of": [ + "auto", + "none" + ], "type": "enum" } }, @@ -6409,7 +8409,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["create_prorations", "none"], + "one_of": [ + "create_prorations", + "none" + ], "type": "enum" } }, @@ -6417,7 +8420,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["always", "if_required", "never"], + "one_of": [ + "always", + "if_required", + "never" + ], "type": "enum" } }, @@ -6425,7 +8432,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["any", "automatic", "challenge"], + "one_of": [ + "any", + "automatic", + "challenge" + ], "type": "enum" } }, @@ -6433,7 +8444,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["aba", "iban"], + "one_of": [ + "aba", + "iban" + ], "type": "enum" } }, @@ -6441,7 +8455,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["off_session", "on_session"], + "one_of": [ + "off_session", + "on_session" + ], "type": "enum" } }, @@ -6449,7 +8466,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["auto", "never"], + "one_of": [ + "auto", + "never" + ], "type": "enum" } }, @@ -6457,7 +8477,12 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["auto", "book", "donate", "pay"], + "one_of": [ + "auto", + "book", + "donate", + "pay" + ], "type": "enum" } }, @@ -6465,7 +8490,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["exclusive", "inclusive", "unspecified"], + "one_of": [ + "exclusive", + "inclusive", + "unspecified" + ], "type": "enum" } }, @@ -6473,7 +8502,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none", "required"], + "one_of": [ + "none", + "required" + ], "type": "enum" } }, @@ -6481,7 +8513,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["business", "personal"], + "one_of": [ + "business", + "personal" + ], "type": "enum" } }, @@ -6489,7 +8524,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["account", "self"], + "one_of": [ + "account", + "self" + ], "type": "enum" } }, @@ -6497,7 +8535,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["embedded", "hosted"], + "one_of": [ + "embedded", + "hosted" + ], "type": "enum" } }, @@ -6505,7 +8546,13 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["business_day", "day", "hour", "month", "week"], + "one_of": [ + "business_day", + "day", + "hour", + "month", + "week" + ], "type": "enum" } }, @@ -6513,7 +8560,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["automatic", "instant", "microdeposits"], + "one_of": [ + "automatic", + "instant", + "microdeposits" + ], "type": "enum" } }, @@ -6562,7 +8613,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["charge_automatically", "send_invoice"], + "one_of": [ + "charge_automatically", + "send_invoice" + ], "type": "enum" } }, @@ -6570,7 +8624,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["list"], + "one_of": [ + "list" + ], "type": "enum" } }, @@ -6578,7 +8634,13 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["draft", "open", "paid", "uncollectible", "void"], + "one_of": [ + "draft", + "open", + "paid", + "uncollectible", + "void" + ], "type": "enum" } }, @@ -6593,7 +8655,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["placed", "approved", "delivered"], + "one_of": [ + "placed", + "approved", + "delivered" + ], "type": "enum" } }, @@ -6608,7 +8674,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["available", "pending", "sold"], + "one_of": [ + "available", + "pending", + "sold" + ], "type": "enum" } }, @@ -6616,7 +8686,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["custom"], + "one_of": [ + "custom" + ], "type": "enum" } }, @@ -6624,7 +8696,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["dropdown", "numeric", "text"], + "one_of": [ + "dropdown", + "numeric", + "text" + ], "type": "enum" } }, @@ -6632,7 +8708,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none", "off_session", "on_session"], + "one_of": [ + "none", + "off_session", + "on_session" + ], "type": "enum" } }, @@ -6640,7 +8720,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6648,7 +8730,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6656,7 +8740,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6664,7 +8750,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6672,7 +8760,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none", "off_session", "on_session"], + "one_of": [ + "none", + "off_session", + "on_session" + ], "type": "enum" } }, @@ -6680,7 +8772,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6688,7 +8782,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none", "off_session", "on_session"], + "one_of": [ + "none", + "off_session", + "on_session" + ], "type": "enum" } }, @@ -6696,7 +8794,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none", "off_session", "on_session"], + "one_of": [ + "none", + "off_session", + "on_session" + ], "type": "enum" } }, @@ -6704,7 +8806,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["eu_bank_transfer", "gb_bank_transfer"], + "one_of": [ + "eu_bank_transfer", + "gb_bank_transfer" + ], "type": "enum" } }, @@ -6712,7 +8817,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6720,7 +8827,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6728,7 +8837,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6736,7 +8847,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6744,7 +8857,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6752,7 +8867,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6760,7 +8877,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6768,7 +8887,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6776,7 +8897,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none", "off_session"], + "one_of": [ + "none", + "off_session" + ], "type": "enum" } }, @@ -6784,7 +8908,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6792,7 +8918,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6800,7 +8928,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6808,7 +8938,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["", "manual"], + "one_of": [ + "", + "manual" + ], "type": "enum" } }, @@ -6816,7 +8949,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["", "none", "off_session"], + "one_of": [ + "", + "none", + "off_session" + ], "type": "enum" } }, @@ -6824,7 +8961,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none", "off_session"], + "one_of": [ + "none", + "off_session" + ], "type": "enum" } }, @@ -6832,7 +8972,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none", "off_session", "on_session"], + "one_of": [ + "none", + "off_session", + "on_session" + ], "type": "enum" } }, @@ -6840,7 +8984,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6848,7 +8994,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none", "off_session", "on_session"], + "one_of": [ + "none", + "off_session", + "on_session" + ], "type": "enum" } }, @@ -6856,7 +9006,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["automatic", "instant"], + "one_of": [ + "automatic", + "instant" + ], "type": "enum" } }, @@ -6864,7 +9017,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["none"], + "one_of": [ + "none" + ], "type": "enum" } }, @@ -6872,7 +9027,20 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["fixed_amount"], + "one_of": [ + "fixed_amount" + ], + "type": "enum" + } + }, + "SearchIn": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "projects", + "packages" + ], "type": "enum" } }, @@ -6880,7 +9048,11 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["placed", "approved", "delivered"], + "one_of": [ + "placed", + "approved", + "delivered" + ], "type": "enum" } }, @@ -6924,7 +9096,9 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["treasury.inbound_transfer"], + "one_of": [ + "treasury.inbound_transfer" + ], "type": "enum" } }, @@ -6932,7 +9106,12 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["canceled", "failed", "processing", "succeeded"], + "one_of": [ + "canceled", + "failed", + "processing", + "succeeded" + ], "type": "enum" } }, diff --git a/ndc-http-schema/openapi/testdata/petstore3/schema.json b/ndc-http-schema/openapi/testdata/petstore3/schema.json index ec0d6f6..b0ab1c1 100644 --- a/ndc-http-schema/openapi/testdata/petstore3/schema.json +++ b/ndc-http-schema/openapi/testdata/petstore3/schema.json @@ -172,6 +172,39 @@ "type": "array" } }, + { + "arguments": {}, + "description": "List all known architectures.", + "name": "getArchitectures", + "result_type": { + "name": "GetArchitecturesResult", + "type": "named" + } + }, + { + "arguments": { + "project_name": { + "description": "Project name", + "type": { + "name": "String", + "type": "named" + } + }, + "repository_name": { + "description": "Repository name", + "type": { + "name": "String", + "type": "named" + } + } + }, + "description": "Show the build configuration for the specified repository.", + "name": "getBuildProjectNameRepositoryNameBuildconfig", + "result_type": { + "name": "String", + "type": "named" + } + }, { "arguments": {}, "description": "Returns pet inventories by status", @@ -215,6 +248,50 @@ "type": "named" } }, + { + "arguments": { + "in": { + "description": "Where to search and apply a XPath expression: either the list of projects or the list of packages. Example of a result when `projects` is selected: ``` x86_64 x86_64 Standard OBS instance at build.opensuse.org This instance delivers the default build targets for OBS. https://api.opensuse.org/public ``` Example of a result when `packages` is selected: ``` ```", + "type": { + "name": "SearchIn", + "type": "named" + } + }, + "match": { + "description": "XPath expression used to filter the results.", + "type": { + "name": "String", + "type": "named" + } + }, + "return": { + "description": "XPath expression that defines which values will be returned. Instead of returning a collection of projects or packages, the result will be a collection of: - contents of elements, when that XPath expression match elements, or, - values of atttributes, when that XPath expression match atttributes. Example of result, for a query string like `?in=projects\u0026match=repository/arch='x86_64'\u0026return=repository/name`: ``` openSUSE_Tumbleweed 15.3 ```", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "values": { + "description": "When set to `1`, return a list of values, instead of returning a list of projects or packages. The result will be a collection of: - contents of elements, when the expression defined in the `match` query parameter match elements. For example: `match=repository/arch`. - values of atttributes, when the expression defined in the `match` query parameter match atttributes. For example: `match=repository/name`. Example of result, for a query string like `?in=projects\u0026match=repository/path/project\u0026values=1`: ``` openSUSE.org:openSUSE:Factory openSUSE.org:openSUSE:Leap:15.3 ```", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + }, + "description": "Search in projects or in packages.", + "name": "getSearchXml", + "result_type": { + "name": "JSON", + "type": "named" + } + }, { "arguments": {}, "description": "Get snake object", @@ -344,6 +421,46 @@ } } }, + "Book": { + "fields": { + "attr": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "author": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "title": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, "Category": { "fields": { "id": { @@ -379,6 +496,114 @@ } } }, + "GetArchitecturesResult": { + "fields": { + "count": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "entry": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "GetArchitecturesResultEntry", + "type": "named" + }, + "type": "array" + } + } + }, + "name": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "rev": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "srcmd5": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "vrev": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, + "GetArchitecturesResultEntry": { + "fields": { + "md5": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "mtime": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "name": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "size": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "xmlValue": { + "description": "Value of the xml field", + "type": { + "name": "String", + "type": "named" + } + } + } + }, "GetInvoicesResult": { "fields": { "has_more": { @@ -3462,6 +3687,276 @@ } } }, + "PutCommentXmlBody": { + "fields": { + "comment": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "PutCommentXmlBodyComment", + "type": "named" + }, + "type": "array" + } + } + }, + "comment_count": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "package": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "project": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "request": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "user": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, + "PutCommentXmlBodyComment": { + "fields": { + "bsrequest": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "package": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "parent": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "project": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "when": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "who": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "xmlValue": { + "description": "Value of the xml field", + "type": { + "name": "String", + "type": "named" + } + } + } + }, + "PutCommentXmlResult": { + "fields": { + "comment": { + "type": { + "type": "nullable", + "underlying_type": { + "element_type": { + "name": "PutCommentXmlResultComment", + "type": "named" + }, + "type": "array" + } + } + }, + "comment_count": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "package": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "project": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "request": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "user": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, + "PutCommentXmlResultComment": { + "fields": { + "bsrequest": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "package": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "parent": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "project": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "when": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "who": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "xmlValue": { + "description": "Value of the xml field", + "type": { + "name": "String", + "type": "named" + } + } + } + }, "SnakeObject": { "fields": { "features": { @@ -4138,6 +4633,63 @@ "type": "named" } }, + { + "arguments": { + "body": { + "description": "Request body of PUT /book/xml", + "type": { + "type": "nullable", + "underlying_type": { + "name": "Book", + "type": "named" + } + } + } + }, + "description": "PUT /book/xml", + "name": "putBookXml", + "result_type": { + "name": "Book", + "type": "named" + } + }, + { + "arguments": { + "body": { + "description": "Request body of PUT /comment/xml", + "type": { + "type": "nullable", + "underlying_type": { + "name": "PutCommentXmlBody", + "type": "named" + } + } + } + }, + "description": "PUT /comment/xml", + "name": "putCommentXml", + "result_type": { + "name": "PutCommentXmlResult", + "type": "named" + } + }, + { + "arguments": { + "body": { + "description": "Request body of PUT /pet/xml", + "type": { + "name": "Pet", + "type": "named" + } + } + }, + "description": "Update an existing pet", + "name": "putPetXml", + "result_type": { + "name": "Pet", + "type": "named" + } + }, { "arguments": { "body": { @@ -5166,6 +5718,17 @@ "type": "enum" } }, + "SearchIn": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": [ + "projects", + "packages" + ], + "type": "enum" + } + }, "SnakeObjectIdStatus": { "aggregate_functions": {}, "comparison_operators": {}, diff --git a/ndc-http-schema/openapi/testdata/petstore3/source.json b/ndc-http-schema/openapi/testdata/petstore3/source.json index a0998a5..4bad2dd 100644 --- a/ndc-http-schema/openapi/testdata/petstore3/source.json +++ b/ndc-http-schema/openapi/testdata/petstore3/source.json @@ -160,6 +160,51 @@ ] } }, + "/pet/xml": { + "put": { + "tags": ["pet"], + "summary": "Update an existing pet", + "description": "Update an existing pet by Id", + "operationId": "updatePet", + "requestBody": { + "description": "Update an existent pet in the store", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + } + }, "/pet/findByStatus": { "get": { "tags": ["pet"], @@ -3135,6 +3180,503 @@ "summary": "Initialize Browser-Based Logout User Flow", "tags": ["public"] } + }, + "/build/{project_name}/{repository_name}/_buildconfig": { + "get": { + "summary": "Show the build configuration for the specified repository.", + "description": "Show the build configuration for the specified repository. Includes all base package\nrequirements, mappings and macros.\n", + "security": [], + "parameters": [ + { + "in": "path", + "name": "project_name", + "schema": { + "type": "string" + }, + "required": true, + "description": "Project name", + "example": "home:Admin" + }, + { + "in": "path", + "name": "repository_name", + "schema": { + "type": "string" + }, + "required": true, + "description": "Repository name", + "example": "openSUSE_Tumbleweed" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": {} + } + } + } + } + }, + "/architectures": { + "get": { + "summary": "List all known architectures.", + "description": "Get a list of all known architectures known to OBS in general.\nThis is not the list of architectures provided by this instance. Check the\nschedulers element from the `/configuration` route for this.\n", + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "content": { + "application/xml; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "name": { + "type": "string", + "xml": { + "attribute": true + } + }, + "rev": { + "type": "string", + "xml": { + "attribute": true + } + }, + "vrev": { + "type": "string", + "xml": { + "attribute": true + } + }, + "srcmd5": { + "type": "string", + "xml": { + "attribute": true + } + }, + "entry": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "xml": { + "attribute": true + } + }, + "md5": { + "type": "string", + "xml": { + "attribute": true + } + }, + "size": { + "type": "string", + "xml": { + "attribute": true + } + }, + "mtime": { + "type": "string", + "xml": { + "attribute": true + } + } + } + } + } + }, + "xml": { + "name": "directory" + } + } + } + } + } + } + } + }, + "/comment/xml": { + "put": { + "security": [], + "parameters": [], + "requestBody": { + "content": { + "application/xml; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "comment_count": { + "type": "integer", + "xml": { + "attribute": true, + "name": "comment" + } + }, + "comment": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "who": { + "type": "string", + "xml": { + "attribute": true + } + }, + "when": { + "type": "string", + "xml": { + "attribute": true + } + }, + "parent": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "bsrequest": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "project": { + "type": "string", + "xml": { + "attribute": true + } + }, + "package": { + "type": "string", + "xml": { + "attribute": true + } + } + } + } + }, + "request": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "package": { + "type": "string", + "xml": { + "attribute": true + } + }, + "project": { + "type": "string", + "xml": { + "attribute": true + } + }, + "user": { + "type": "string", + "xml": { + "attribute": true + } + } + }, + "xml": { + "name": "comments" + } + } + } + } + }, + "responses": { + "200": { + "content": { + "application/xml; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "comment_count": { + "type": "integer", + "xml": { + "attribute": true, + "name": "comment" + } + }, + "comment": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "who": { + "type": "string", + "xml": { + "attribute": true + } + }, + "when": { + "type": "string", + "xml": { + "attribute": true + } + }, + "parent": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "bsrequest": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "project": { + "type": "string", + "xml": { + "attribute": true + } + }, + "package": { + "type": "string", + "xml": { + "attribute": true + } + } + } + } + }, + "request": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "package": { + "type": "string", + "xml": { + "attribute": true + } + }, + "project": { + "type": "string", + "xml": { + "attribute": true + } + }, + "user": { + "type": "string", + "xml": { + "attribute": true + } + } + }, + "xml": { + "name": "comments" + } + } + } + } + } + } + } + }, + "/book/xml": { + "put": { + "security": [], + "parameters": [], + "requestBody": { + "content": { + "application/xml; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/book" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/xml; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/book" + } + } + } + } + } + } + }, + "/search/xml": { + "get": { + "summary": "Search in projects or in packages.", + "description": "Get a list of projects or a list of packages that match a XPath expression, passed in the query parameter `match`.", + "security": [], + "parameters": [ + { + "in": "query", + "name": "in", + "required": true, + "schema": { + "type": "string", + "enum": ["projects", "packages"], + "example": "projects" + }, + "description": "Where to search and apply a XPath expression: either the list of projects or the list of packages.\n\nExample of a result when `projects` is selected:\n```\n\n \n \n \n \n \n \n x86_64\n \n \n \n x86_64\n \n \n \n Standard OBS instance at build.opensuse.org\n This instance delivers the default build targets for OBS.\n https://api.opensuse.org/public\n \n\n```\n\nExample of a result when `packages` is selected:\n```\n\n \n \n \n \n\n```\n" + }, + { + "in": "query", + "name": "match", + "required": true, + "schema": { + "type": "string" + }, + "description": "XPath expression used to filter the results." + }, + { + "in": "query", + "name": "return", + "schema": { + "type": "string", + "example": "repository/name" + }, + "description": "XPath expression that defines which values will be returned.\n\nInstead of returning a collection of projects or packages, the result will be a collection of:\n - contents of elements, when that XPath expression match elements, or,\n - values of atttributes, when that XPath expression match atttributes.\n\nExample of result, for a query string like `?in=projects&match=repository/arch='x86_64'&return=repository/name`:\n```\n\n openSUSE_Tumbleweed\n 15.3\n\n```\n" + }, + { + "in": "query", + "name": "values", + "schema": { + "type": "string", + "example": 1 + }, + "description": "When set to `1`, return a list of values, instead of returning a list of projects or packages.\n\nThe result will be a collection of:\n - contents of elements, when the expression defined in the `match` query parameter match elements.\n For example: `match=repository/arch`.\n - values of atttributes, when the expression defined in the `match` query parameter match atttributes.\n For example: `match=repository/name`.\n\nExample of result, for a query string like `?in=projects&match=repository/path/project&values=1`:\n```\n\n openSUSE.org:openSUSE:Factory\n openSUSE.org:openSUSE:Leap:15.3\n\n```\n" + } + ], + "responses": { + "200": { + "description": "OK. The request has succeeded.", + "content": { + "application/xml; charset=utf-8": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "matches": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "project": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "xml": { + "attribute": true + } + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "person": { + "type": "object", + "properties": { + "userid": { + "type": "string", + "xml": { + "attribute": true + } + }, + "role": { + "type": "string", + "xml": { + "attribute": true + } + } + } + } + } + } + } + }, + "xml": { + "name": "collection" + } + }, + { + "type": "object", + "properties": { + "matches": { + "type": "integer", + "xml": { + "attribute": true + } + }, + "package": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "xml": { + "attribute": true + } + }, + "project": { + "type": "string", + "xml": { + "attribute": true + } + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + }, + "xml": { + "name": "collection" + } + } + ] + } + } + } + } + } + } } }, "components": { @@ -3631,6 +4173,31 @@ ] } } + }, + "book": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "author": { + "type": "string" + }, + "attr": { + "type": "string", + "xml": { + "prefix": "smp", + "attribute": true + } + } + }, + "xml": { + "prefix": "smp", + "namespace": "http://example.com/schema" + } } }, "requestBodies": { diff --git a/ndc-http-schema/openapi/testdata/prefix2/expected_multi_words.json b/ndc-http-schema/openapi/testdata/prefix2/expected_multi_words.json index fcc8888..2b88659 100644 --- a/ndc-http-schema/openapi/testdata/prefix2/expected_multi_words.json +++ b/ndc-http-schema/openapi/testdata/prefix2/expected_multi_words.json @@ -131,6 +131,9 @@ "format": "int64" } } + }, + "xml": { + "name": "Post" } } }, diff --git a/ndc-http-schema/openapi/testdata/prefix2/expected_single_word.json b/ndc-http-schema/openapi/testdata/prefix2/expected_single_word.json index a0c0a45..ce17ec9 100644 --- a/ndc-http-schema/openapi/testdata/prefix2/expected_single_word.json +++ b/ndc-http-schema/openapi/testdata/prefix2/expected_single_word.json @@ -131,6 +131,9 @@ "format": "int64" } } + }, + "xml": { + "name": "Post" } } }, diff --git a/ndc-http-schema/schema/enum.go b/ndc-http-schema/schema/enum.go index fbb1ced..6828be4 100644 --- a/ndc-http-schema/schema/enum.go +++ b/ndc-http-schema/schema/enum.go @@ -224,6 +224,7 @@ const ( ContentTypeMultipartFormData = "multipart/form-data" ContentTypeTextPlain = "text/plain" ContentTypeTextHTML = "text/html" + ContentTypeOctetStream = "application/octet-stream" ) // ParameterEncodingStyle represents the encoding style of the parameter. diff --git a/ndc-http-schema/schema/schema.go b/ndc-http-schema/schema/schema.go index ea437be..e3f096e 100644 --- a/ndc-http-schema/schema/schema.go +++ b/ndc-http-schema/schema/schema.go @@ -2,6 +2,7 @@ package schema import ( "encoding/json" + "encoding/xml" "errors" "fmt" @@ -91,6 +92,14 @@ func (rm NDCHttpSchema) GetProcedure(name string) *OperationInfo { return &fn } +// AddScalar adds a new scalar if not exist. +func (rm *NDCHttpSchema) AddScalar(name string, scalar schema.ScalarType) { + _, ok := rm.ScalarTypes[name] + if !ok { + rm.ScalarTypes[name] = scalar + } +} + type Response struct { ContentType string `json:"contentType" mapstructure:"contentType" yaml:"contentType"` } @@ -148,6 +157,7 @@ type TypeSchema struct { MaxLength *int64 `json:"maxLength,omitempty" mapstructure:"maxLength" yaml:"maxLength,omitempty"` MinLength *int64 `json:"minLength,omitempty" mapstructure:"minLength" yaml:"minLength,omitempty"` Items *TypeSchema `json:"items,omitempty" mapstructure:"items" yaml:"items,omitempty"` + XML *XMLSchema `json:"xml,omitempty" mapstructure:"xml" yaml:"xml,omitempty"` Description string `json:"-" yaml:"-"` ReadOnly bool `json:"-" yaml:"-"` WriteOnly bool `json:"-" yaml:"-"` @@ -309,6 +319,8 @@ type ObjectType struct { Description *string `json:"description,omitempty" mapstructure:"description,omitempty" yaml:"description,omitempty"` // Fields defined on this object type Fields map[string]ObjectField `json:"fields" mapstructure:"fields" yaml:"fields"` + // XML schema + XML *XMLSchema `json:"xml,omitempty" mapstructure:"xml" yaml:"xml,omitempty"` } // Schema returns schema the object field @@ -421,6 +433,48 @@ func (j *ArgumentInfo) UnmarshalJSON(b []byte) error { return nil } +// XMLSchema represents a XML schema that adds additional metadata to describe the XML representation of this property. +type XMLSchema struct { + // Replaces the name of the element/attribute used for the described schema property. + // When defined within items, it will affect the name of the individual XML elements within the list. + // When defined alongside type being array (outside the items), it will affect the wrapping element and only if wrapped is true. + // If wrapped is false, it will be ignored. + Name string `json:"name,omitempty" mapstructure:"name" yaml:"name,omitempty"` + // The prefix to be used for the name. + Prefix string `json:"prefix,omitempty" mapstructure:"prefix" yaml:"prefix,omitempty"` + // The URI of the namespace definition. This MUST be in the form of an absolute URI. + Namespace string `json:"namespace,omitempty" mapstructure:"namespace" yaml:"namespace,omitempty"` + // Used only for an array definition. Signifies whether the array is wrapped (for example, ) or unwrapped (). + Wrapped bool `json:"wrapped,omitempty" mapstructure:"wrapped" yaml:"wrapped,omitempty"` + // Declares whether the property definition translates to an attribute instead of an element. + Attribute bool `json:"attribute,omitempty" mapstructure:"attribute" yaml:"attribute,omitempty"` + // Represents a text value of the xml element. + Text bool `json:"text,omitempty" mapstructure:"text" yaml:"text,omitempty"` +} + +// GetFullName gets the full name with prefix. +func (xs XMLSchema) GetFullName() string { + if xs.Prefix == "" { + return xs.Name + } + + return xs.Prefix + ":" + xs.Name +} + +// GetNamespaceAttribute gets the namespace attribute +func (xs XMLSchema) GetNamespaceAttribute() xml.Attr { + // xmlns:smp="http://example.com/schema" + name := "xmlns" + if xs.Prefix != "" { + name += ":" + xs.Prefix + } + + return xml.Attr{ + Name: xml.Name{Local: name}, + Value: xs.Namespace, + } +} + func toAnySlice[T any](values []T) []any { results := make([]any, len(values)) for i, v := range values { diff --git a/ndc-http-schema/utils/file.go b/ndc-http-schema/utils/file.go index aa8db40..82b50a3 100644 --- a/ndc-http-schema/utils/file.go +++ b/ndc-http-schema/utils/file.go @@ -55,6 +55,13 @@ func WriteSchemaFile(outputPath string, content any) error { return err } + basePath := filepath.Dir(outputPath) + if basePath != "." { + if err := os.MkdirAll(basePath, 0664); err != nil { + return err + } + } + return os.WriteFile(outputPath, rawBytes, 0664) }