diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9410426..0e4c0ab 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,7 +2,7 @@ name: Release container definition on: push: tags: - - "*" + - "v*" env: DOCKER_REGISTRY: ghcr.io diff --git a/.gitignore b/.gitignore index 515701d..91b0982 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ go.work.sum # Release directory release/ -_output/ \ No newline at end of file +_output/ +schema.output.json \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..38cd3d0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "yaml.schemas": { + "file:///home/hgiasac/.vscode/extensions/docsmsft.docs-yaml-1.0.4/dist/toc.schema.json": "/toc\\.yml/i", + "file:///home/hgiasac/projects/hasura/src/ndc-rest/ndc-rest-schema/jsonschema/configuration.schema.json": "file:///home/hgiasac/projects/hasura/src/ndc-rest/ndc-rest-schema/command/testdata/auth/config.yaml" + } +} \ No newline at end of file diff --git a/Makefile b/Makefile index ba0d6dc..c193715 100644 --- a/Makefile +++ b/Makefile @@ -45,4 +45,24 @@ ci-build-cli: clean go run github.com/mitchellh/gox -ldflags '-X github.com/hasura/ndc-rest/ndc-rest-schema/version.BuildVersion=$(VERSION) -s -w -extldflags "-static"' \ -osarch="linux/amd64 darwin/amd64 windows/amd64 darwin/arm64 linux/arm64" \ -output="../$(OUTPUT_DIR)/ndc-rest-schema-{{.OS}}-{{.Arch}}" \ - . \ No newline at end of file + . + +.PHONY: generate-test-config +generate-test-config: + go run ./ndc-rest-schema update -d ./tests/configuration + +.PHONY: start-ddn +start-ddn: + HASURA_DDN_PAT=$$(ddn auth print-pat) docker compose --env-file tests/engine/.env up --build -d + +.PHONY: stop-ddn +stop-ddn: + docker compose down --remove-orphans + +.PHONY: build-supergraph-test +build-supergraph-test: + docker compose up -d --build ndc-rest + cd tests/engine && \ + ddn connector-link update myapi --add-all-resources --subgraph ./app/subgraph.yaml && \ + ddn supergraph build local + make start-ddn \ No newline at end of file diff --git a/README.md b/README.md index 646481b..a8959d0 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ files: The config of each element follows the [config schema](https://github.com/hasura/ndc-rest/ndc-rest-schema/blob/main/config.example.yaml) of `ndc-rest-schema`. +You can add many OpenAPI files into the same connector. + > [!IMPORTANT] > Conflicted object and scalar types will be ignored. Only the type of the first file is kept in the schema. @@ -64,29 +66,67 @@ ndc-rest-schema convert -f ./rest/testdata/jsonplaceholder/swagger.json -o ./res - `text/*` - Upload file content types, e.g.`image/*` from `base64` arguments. -### Environment variable template +### Authentication -The connector can replaces `{{xxx}}` templates with environment variables. The converter automatically renders variables for API keys and tokens when converting OpenAPI documents. However, you can add your custom variables as well. +The current version supports API key and Auth token authentication schemes. The configuration is inspired from `securitySchemes` [with env variables](https://github.com/hasura/ndc-rest/ndc-rest-schema#authentication). The connector supports the following authentication strategies: -### Timeout and retry +- API Key +- Bearer Auth +- Cookies +- OAuth 2.0 + +The configuration automatically generates environment variables for the api key and Bearer token. + +For Cookie authentication and OAuth 2.0, you need to enable headers forwarding from the Hasura engine to the connector. -The global timeout and retry strategy can be configured in the `settings` object. +### Header Forwarding + +Enable `forwardHeaders` in the configuration file. ```yaml -settings: - timeout: 30 - retry: - times: 2 - # delay between each retry in milliseconds - delay: 1000 - httpStatus: [429, 500, 502, 503] +# ... +forwardHeaders: + enabled: true + argumentField: headers ``` -### Authentication +And configure in the connector link metadata. + +```yaml +kind: DataConnectorLink +version: v1 +definition: + name: my_api + # ... + argumentPresets: + - argument: headers + value: + httpHeaders: + forward: + - Cookie + additional: {} +``` + +See the configuration example in [Hasura docs](https://hasura.io/docs/3.0/recipes/business-logic/http-header-forwarding/#step-2-update-the-metadata-1). + +### Timeout and retry -The current version supports API key and Auth token authentication schemes. The configuration is inspired from `securitySchemes` [with env variables](https://github.com/hasura/ndc-rest/ndc-rest-schema#authentication) +The global timeout and retry strategy can be configured in each file: -See [this example](rest/testdata/auth/schema.yaml) for more context. +```yaml +files: + - file: swagger.json + spec: oas2 + timeout: + value: 30 + retry: + times: + value: 1 + delay: + # delay between each retry in milliseconds + value: 500 + httpStatus: [429, 500, 502, 503] +``` ## Distributed execution diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..6096e70 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,15 @@ +include: + - tests/engine/compose.yaml +services: + ndc-rest: + build: + context: . + ports: + - 8080:8080 + volumes: + - ./tests/configuration:/etc/connector:ro + extra_hosts: + - local.hasura.dev=host-gateway + environment: + OTEL_EXPORTER_OTLP_ENDPOINT: http://local.hasura.dev:4317 + HASURA_LOG_LEVEL: debug diff --git a/connector-definition/config.yaml b/connector-definition/config.yaml index c3c168e..98d7c0b 100644 --- a/connector-definition/config.yaml +++ b/connector-definition/config.yaml @@ -1,3 +1,21 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/hasura/ndc-rest-schema/main/jsonschema/configuration.schema.json +output: schema.output.json +strict: true +forwardHeaders: + enabled: false + argumentField: headers + responseHeaders: null +concurrency: + query: 1 + mutation: 1 + rest: 5 files: - file: https://raw.githubusercontent.com/hasura/ndc-rest/main/connector/testdata/jsonplaceholder/swagger.json - spec: openapi2 + spec: oas2 + timeout: + value: 30 + retry: + times: + value: 1 + delay: + value: 500 diff --git a/connector/cli.go b/connector/cli.go index 4248c40..1ff9d3d 100644 --- a/connector/cli.go +++ b/connector/cli.go @@ -1,44 +1,10 @@ package rest import ( - "context" - "log/slog" - "os" - "strings" - - "github.com/hasura/ndc-rest/ndc-rest-schema/command" "github.com/hasura/ndc-sdk-go/connector" - "github.com/lmittmann/tint" ) -// CLI extends the NDC SDK with custom commands -type CLI struct { - connector.ServeCLI - Convert command.ConvertCommandArguments `cmd:"" help:"Convert API spec to NDC schema. For example:\n ndc-rest-schema convert -f petstore.yaml -o petstore.json"` -} - -// Execute executes custom commands -func (c CLI) Execute(ctx context.Context, cmd string) error { - switch cmd { - case "convert": - var logLevel slog.Level - err := logLevel.UnmarshalText([]byte(strings.ToUpper(c.LogLevel))) - if err != nil { - return err - } - logger := slog.New(tint.NewHandler(os.Stderr, &tint.Options{ - Level: logLevel, - TimeFormat: "15:04", - })) - - return command.CommandConvertToNDCSchema(&c.Convert, logger) - default: - return c.ServeCLI.Execute(ctx, cmd) - } -} - -// Start wrap the connector.Start function with custom CLI +// Start and serve the connector API server func Start[Configuration, State any](restConnector connector.Connector[Configuration, State], options ...connector.ServeOption) error { - var cli CLI - return connector.StartCustom[Configuration, State](&cli, restConnector, options...) + return connector.Start(restConnector, options...) } diff --git a/connector/connector.go b/connector/connector.go index e6ee043..d19c46c 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -4,18 +4,17 @@ import ( "context" "encoding/json" "fmt" - "os" "github.com/hasura/ndc-rest/connector/internal" "github.com/hasura/ndc-rest/ndc-rest-schema/configuration" rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" "github.com/hasura/ndc-sdk-go/connector" "github.com/hasura/ndc-sdk-go/schema" - "gopkg.in/yaml.v3" ) // RESTConnector implements the SDK interface of NDC specification type RESTConnector struct { + config *configuration.Configuration metadata internal.MetadataCollection capabilities *schema.RawCapabilitiesResponse rawSchema *schema.RawSchemaResponse @@ -43,8 +42,11 @@ func (c *RESTConnector) ParseConfiguration(ctx context.Context, configurationDir Query: schema.QueryCapabilities{ Variables: schema.LeafCapability{}, NestedFields: schema.NestedFieldCapabilities{}, + Explain: schema.LeafCapability{}, + }, + Mutation: schema.MutationCapabilities{ + Explain: schema.LeafCapability{}, }, - Mutation: schema.MutationCapabilities{}, }, } rawCapabilities, err := json.Marshal(restCapabilities) @@ -53,23 +55,32 @@ func (c *RESTConnector) ParseConfiguration(ctx context.Context, configurationDir } c.capabilities = schema.NewRawCapabilitiesResponseUnsafe(rawCapabilities) - config, err := parseConfiguration(configurationDir) + config, err := configuration.ReadConfigurationFile(configurationDir) if err != nil { return nil, err } logger := connector.GetLogger(ctx) - schemas, errs := BuildSchemaFiles(configurationDir, config.Files, logger) - if len(errs) > 0 { - printSchemaValidationError(logger, errs) - return nil, errBuildSchemaFailed + schemas, err := configuration.ReadSchemaOutputFile(configurationDir, config.Output, logger) + if err != nil { + return nil, err } - if errs := c.ApplyNDCRestSchemas(schemas); len(errs) > 0 { - printSchemaValidationError(logger, errs) + var errs map[string][]string + if schemas == nil { + schemas, errs = configuration.BuildSchemaFromConfig(config, configurationDir, logger) + if len(errs) > 0 { + printSchemaValidationError(logger, errs) + return nil, errBuildSchemaFailed + } + } + + if err := c.ApplyNDCRestSchemas(config, schemas, logger); err != nil { return nil, errInvalidSchema } + c.config = config + return config, nil } @@ -82,7 +93,10 @@ func (c *RESTConnector) ParseConfiguration(ctx context.Context, configurationDir // connector-specific metrics with the metrics registry. func (c *RESTConnector) TryInitState(ctx context.Context, configuration *configuration.Configuration, metrics *connector.TelemetryState) (*State, error) { c.client.SetTracer(metrics.Tracer) - return &State{}, nil + + return &State{ + Tracer: metrics.Tracer, + }, nil } // HealthCheck checks the health of the connector. @@ -99,41 +113,3 @@ func (c *RESTConnector) HealthCheck(ctx context.Context, configuration *configur func (c *RESTConnector) GetCapabilities(configuration *configuration.Configuration) schema.CapabilitiesResponseMarshaler { return c.capabilities } - -func parseConfiguration(configurationDir string) (*configuration.Configuration, error) { - var config configuration.Configuration - jsonBytes, err := os.ReadFile(configurationDir + "/config.json") - if err == nil { - if err = json.Unmarshal(jsonBytes, &config); err != nil { - return nil, err - } - return &config, nil - } - - if !os.IsNotExist(err) { - return nil, err - } - - // try to read and parse yaml file - yamlBytes, err := os.ReadFile(configurationDir + "/config.yaml") - if err != nil { - if !os.IsNotExist(err) { - return nil, err - } - yamlBytes, err = os.ReadFile(configurationDir + "/config.yml") - } - - if err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("the config.{json,yaml,yml} file does not exist at %s", configurationDir) - } else { - return nil, err - } - } - - if err = yaml.Unmarshal(yamlBytes, &config); err != nil { - return nil, err - } - - return &config, nil -} diff --git a/connector/connector_test.go b/connector/connector_test.go index 9c6b6e5..2f6f82a 100644 --- a/connector/connector_test.go +++ b/connector/connector_test.go @@ -144,7 +144,14 @@ func TestRESTConnector_authentication(t *testing.T) { assertHTTPResponse(t, res, http.StatusOK, schema.QueryResponse{ { Rows: []map[string]any{ - {"__value": map[string]any{}}, + { + "__value": map[string]any{ + "headers": map[string]any{ + "Content-Type": string("application/json"), + }, + "response": map[string]any{}, + }, + }, }, }, }) @@ -182,7 +189,12 @@ func TestRESTConnector_authentication(t *testing.T) { assert.NilError(t, err) assertHTTPResponse(t, res, http.StatusOK, schema.MutationResponse{ OperationResults: []schema.MutationOperationResults{ - schema.NewProcedureResult(map[string]any{}).Encode(), + schema.NewProcedureResult(map[string]any{ + "headers": map[string]any{ + "Content-Type": string("application/json"), + }, + "response": map[string]any{}, + }).Encode(), }, }) }) @@ -198,6 +210,12 @@ func TestRESTConnector_authentication(t *testing.T) { } }, "arguments": { + "headers": { + "type": "literal", + "value": { + "X-Custom-Header": "This is a test" + } + }, "status": { "type": "literal", "value": "available" @@ -212,7 +230,7 @@ func TestRESTConnector_authentication(t *testing.T) { assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{ Details: schema.ExplainResponseDetails{ "url": server.URL + "/pet/findByStatus?status=available", - "headers": `{"Authorization":["Bearer ran*******(19)"]}`, + "headers": `{"Authorization":["Bearer ran*******(19)"],"X-Custom-Header":["This is a test"]}`, }, }) }) @@ -224,7 +242,12 @@ func TestRESTConnector_authentication(t *testing.T) { assertHTTPResponse(t, res, http.StatusOK, schema.QueryResponse{ { Rows: []map[string]any{ - {"__value": map[string]any{}}, + { + "__value": map[string]any{ + "headers": map[string]any{"Content-Type": string("application/json")}, + "response": map[string]any{}, + }, + }, }, }, }) @@ -287,9 +310,12 @@ func TestRESTConnector_authentication(t *testing.T) { assert.NilError(t, err) assertHTTPResponse(t, res, http.StatusOK, schema.MutationResponse{ OperationResults: []schema.MutationOperationResults{ - schema.NewProcedureResult([]any{ - map[string]any{"completed": float64(1), "status": string("OK")}, - map[string]any{"completed": float64(0), "status": string("FAILED")}, + schema.NewProcedureResult(map[string]any{ + "headers": map[string]any{"Content-Type": string("application/x-ndjson")}, + "response": []any{ + map[string]any{"completed": float64(1), "status": string("OK")}, + map[string]any{"completed": float64(0), "status": string("FAILED")}, + }, }).Encode(), }, }) @@ -303,46 +329,6 @@ func TestRESTConnector_distribution(t *testing.T) { t.Setenv("PET_STORE_API_KEY", apiKey) t.Setenv("PET_STORE_BEARER_TOKEN", bearerToken) - rc := NewRESTConnector() - connServer, err := connector.NewServer(rc, &connector.ServerOptions{ - Configuration: "testdata/patch", - }, connector.WithoutRecovery()) - assert.NilError(t, err) - - timeout, err := rc.metadata[0].Settings.Servers[0].Timeout.Value() - assert.NilError(t, err) - assert.Equal(t, int64(30), *timeout) - - retryTimes, err := rc.metadata[0].Settings.Servers[0].Retry.Times.Value() - assert.NilError(t, err) - assert.Equal(t, int64(2), *retryTimes) - - retryDelay, err := rc.metadata[0].Settings.Servers[0].Retry.Delay.Value() - assert.NilError(t, err) - assert.Equal(t, int64(1000), *retryDelay) - - retryStatus, err := rc.metadata[0].Settings.Servers[0].Retry.HTTPStatus.Value() - assert.NilError(t, err) - assert.DeepEqual(t, []int64{429, 500}, retryStatus) - - timeout1, err := rc.metadata[0].Settings.Servers[1].Timeout.Value() - assert.NilError(t, err) - assert.Equal(t, int64(10), *timeout1) - - retryTimes1, err := rc.metadata[0].Settings.Servers[1].Retry.Times.Value() - assert.NilError(t, err) - assert.Equal(t, int64(1), *retryTimes1) - - retryDelay1, err := rc.metadata[0].Settings.Servers[1].Retry.Delay.Value() - assert.NilError(t, err) - assert.Equal(t, int64(500), *retryDelay1) - - retryStatus1, err := rc.metadata[0].Settings.Servers[1].Retry.HTTPStatus.Value() - assert.NilError(t, err) - assert.DeepEqual(t, []int64{429, 500, 501, 502}, retryStatus1) - - testServer := connServer.BuildTestServer() - defer testServer.Close() t.Run("distributed_sequence", func(t *testing.T) { mock := mockDistributedServer{} @@ -352,6 +338,21 @@ func TestRESTConnector_distribution(t *testing.T) { t.Setenv("PET_STORE_DOG_URL", fmt.Sprintf("%s/dog", server.URL)) t.Setenv("PET_STORE_CAT_URL", fmt.Sprintf("%s/cat", server.URL)) + rc := NewRESTConnector() + connServer, err := connector.NewServer(rc, &connector.ServerOptions{ + Configuration: "testdata/patch", + }, connector.WithoutRecovery()) + assert.NilError(t, err) + + testServer := connServer.BuildTestServer() + defer testServer.Close() + + assert.Equal(t, uint(30), rc.metadata[0].Runtime.Timeout) + assert.Equal(t, uint(2), rc.metadata[0].Runtime.Retry.Times) + assert.Equal(t, uint(1000), rc.metadata[0].Runtime.Retry.Delay) + assert.Equal(t, uint(1000), rc.metadata[0].Runtime.Retry.Delay) + assert.DeepEqual(t, []int{429, 500}, rc.metadata[0].Runtime.Retry.HTTPStatus) + reqBody := []byte(`{ "collection": "findPetsDistributed", "query": { @@ -403,6 +404,14 @@ func TestRESTConnector_distribution(t *testing.T) { t.Setenv("PET_STORE_DOG_URL", fmt.Sprintf("%s/dog", server.URL)) t.Setenv("PET_STORE_CAT_URL", fmt.Sprintf("%s/cat", server.URL)) + rc := NewRESTConnector() + connServer, err := connector.NewServer(rc, &connector.ServerOptions{ + Configuration: "testdata/patch", + }, connector.WithoutRecovery()) + assert.NilError(t, err) + + testServer := connServer.BuildTestServer() + defer testServer.Close() reqBody := []byte(`{ "operations": [ @@ -457,6 +466,15 @@ func TestRESTConnector_distribution(t *testing.T) { t.Setenv("PET_STORE_DOG_URL", fmt.Sprintf("%s/dog", server.URL)) t.Setenv("PET_STORE_CAT_URL", fmt.Sprintf("%s/cat", server.URL)) + rc := NewRESTConnector() + connServer, err := connector.NewServer(rc, &connector.ServerOptions{ + Configuration: "testdata/patch", + }, connector.WithoutRecovery()) + assert.NilError(t, err) + + testServer := connServer.BuildTestServer() + defer testServer.Close() + reqBody := []byte(`{ "collection": "findPetsDistributed", "query": { @@ -504,13 +522,6 @@ func TestRESTConnector_distribution(t *testing.T) { } func TestRESTConnector_multiSchemas(t *testing.T) { - connServer, err := connector.NewServer(NewRESTConnector(), &connector.ServerOptions{ - Configuration: "testdata/multi-schemas", - }, connector.WithoutRecovery()) - assert.NilError(t, err) - testServer := connServer.BuildTestServer() - defer testServer.Close() - mock := mockMultiSchemaServer{} server := mock.createServer() defer server.Close() @@ -518,6 +529,13 @@ func TestRESTConnector_multiSchemas(t *testing.T) { t.Setenv("CAT_STORE_URL", fmt.Sprintf("%s/cat", server.URL)) t.Setenv("DOG_STORE_URL", fmt.Sprintf("%s/dog", server.URL)) + connServer, err := connector.NewServer(NewRESTConnector(), &connector.ServerOptions{ + Configuration: "testdata/multi-schemas", + }, connector.WithoutRecovery()) + assert.NilError(t, err) + testServer := connServer.BuildTestServer() + defer testServer.Close() + reqBody := []byte(`{ "collection": "findCats", "query": { @@ -608,6 +626,11 @@ func createMockServer(t *testing.T, apiKey string, bearerToken string) *httptest t.Fatalf("invalid bearer token, expected %s, got %s", bearerToken, r.Header.Get("Authorization")) return } + if r.Header.Get("X-Custom-Header") != "This is a test" { + t.Fatalf("invalid X-Custom-Header, expected `This is a test`, got %s", r.Header.Get("X-Custom-Header")) + return + } + if r.URL.Query().Encode() != "status=available" { t.Fatalf("expected query param: status=available, got: %s", r.URL.Query().Encode()) return diff --git a/connector/internal/client.go b/connector/internal/client.go index 4149675..90bf865 100644 --- a/connector/internal/client.go +++ b/connector/internal/client.go @@ -4,10 +4,14 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "log/slog" + "math" "net/http" + "slices" + "strconv" "strings" "sync" "time" @@ -21,6 +25,7 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" + "golang.org/x/sync/errgroup" ) // Doer abstracts a HTTP client with Do method @@ -49,32 +54,39 @@ func (client *HTTPClient) SetTracer(tracer *connector.Tracer) { } // Send creates and executes the request and evaluate response selection -func (client *HTTPClient) Send(ctx context.Context, request *RetryableRequest, selection schema.NestedField, resultType schema.Type, restOptions *RESTOptions) (any, error) { +func (client *HTTPClient) Send(ctx context.Context, request *RetryableRequest, selection schema.NestedField, resultType schema.Type, restOptions *RESTOptions) (any, http.Header, error) { requests, err := BuildDistributedRequestsWithOptions(request, restOptions) if err != nil { - return nil, err + return nil, nil, err } + if !restOptions.Distributed { - result, err := client.sendSingle(ctx, &requests[0], selection, resultType) + result, headers, err := client.sendSingle(ctx, &requests[0], selection, resultType, requests[0].ServerID, "single") if err != nil { - return nil, err + return nil, nil, err } - return result, nil + + return result, headers, nil } - if !restOptions.Parallel { - results := client.sendSequence(ctx, requests, selection, resultType) - return results, nil + + if !restOptions.Parallel || restOptions.Concurrency <= 1 { + results, headers := client.sendSequence(ctx, requests, selection, resultType) + + return results, headers, nil } - results := client.sendParallel(ctx, requests, selection, resultType) - return results, nil + results, headers := client.sendParallel(ctx, requests, selection, resultType, restOptions) + + return results, headers, nil } // execute a request to a list of remote servers in sequence -func (client *HTTPClient) sendSequence(ctx context.Context, requests []RetryableRequest, selection schema.NestedField, resultType schema.Type) *DistributedResponse[any] { +func (client *HTTPClient) sendSequence(ctx context.Context, requests []RetryableRequest, selection schema.NestedField, resultType schema.Type) (*DistributedResponse[any], http.Header) { results := NewDistributedResponse[any]() + var firstHeaders http.Header + for _, req := range requests { - result, err := client.sendSingle(ctx, &req, selection, resultType) + result, headers, err := client.sendSingle(ctx, &req, selection, resultType, req.ServerID, "sequence") if err != nil { results.Errors = append(results.Errors, DistributedError{ Server: req.ServerID, @@ -85,63 +97,87 @@ func (client *HTTPClient) sendSequence(ctx context.Context, requests []Retryable Server: req.ServerID, Data: result, }) + + if firstHeaders == nil { + firstHeaders = headers + } } } - return results + return results, firstHeaders } // execute a request to a list of remote servers in parallel -func (client *HTTPClient) sendParallel(ctx context.Context, requests []RetryableRequest, selection schema.NestedField, resultType schema.Type) *DistributedResponse[any] { +func (client *HTTPClient) sendParallel(ctx context.Context, requests []RetryableRequest, selection schema.NestedField, resultType schema.Type, restOptions *RESTOptions) (*DistributedResponse[any], http.Header) { results := NewDistributedResponse[any]() - var wg sync.WaitGroup - wg.Add(len(requests)) + var firstHeaders http.Header var lock sync.Mutex + eg, ctx := errgroup.WithContext(ctx) + if restOptions.Concurrency > 0 { + eg.SetLimit(int(restOptions.Concurrency)) + } + sendFunc := func(req RetryableRequest) { - defer wg.Done() - result, err := client.sendSingle(ctx, &req, selection, resultType) - lock.Lock() - defer lock.Unlock() - if err != nil { - results.Errors = append(results.Errors, DistributedError{ - Server: req.ServerID, - ConnectorError: *err, - }) - } else { - results.Results = append(results.Results, DistributedResult[any]{ - Server: req.ServerID, - Data: result, - }) - } + eg.Go(func() error { + result, headers, err := client.sendSingle(ctx, &req, selection, resultType, req.ServerID, "parallel") + lock.Lock() + defer lock.Unlock() + if err != nil { + results.Errors = append(results.Errors, DistributedError{ + Server: req.ServerID, + ConnectorError: *err, + }) + } else { + results.Results = append(results.Results, DistributedResult[any]{ + Server: req.ServerID, + Data: result, + }) + if firstHeaders == nil { + firstHeaders = headers + } + } + + return nil + }) } for _, req := range requests { - go sendFunc(req) + sendFunc(req) } - wg.Wait() - return results + + _ = eg.Wait() + + return results, firstHeaders } // execute a request to the remote server with retries -func (client *HTTPClient) sendSingle(ctx context.Context, request *RetryableRequest, selection schema.NestedField, resultType schema.Type) (any, *schema.ConnectorError) { - ctx, span := client.tracer.Start(ctx, "request_remote_server") +func (client *HTTPClient) sendSingle(ctx context.Context, request *RetryableRequest, selection schema.NestedField, resultType schema.Type, serverID string, mode string) (any, http.Header, *schema.ConnectorError) { + ctx, span := client.tracer.Start(ctx, "Send Request to Server "+serverID) defer span.End() - span.SetAttributes( - attribute.String("request_url", request.URL), - attribute.String("method", request.RawRequest.Method), - ) + + span.SetAttributes(attribute.String("execution.mode", mode)) + + requestURL := request.URL.String() + rawPort := request.URL.Port() + port := 80 + if rawPort != "" { + if p, err := strconv.ParseInt(rawPort, 10, 32); err == nil { + port = int(p) + } + } else if strings.HasPrefix(request.URL.Scheme, "https") { + port = 443 + } + client.propagator.Inject(ctx, propagation.HeaderCarrier(request.Headers)) - var resp *http.Response - var err error logger := connector.GetLogger(ctx) - if logger.Enabled(ctx, slog.LevelDebug) { logAttrs := []any{ - slog.String("request_url", request.URL), + slog.String("request_url", requestURL), slog.String("request_method", request.RawRequest.Method), slog.Any("request_headers", request.Headers), } + if request.Body != nil { bs, _ := io.ReadAll(request.Body) logAttrs = append(logAttrs, slog.String("request_body", string(bs))) @@ -149,96 +185,129 @@ func (client *HTTPClient) sendSingle(ctx context.Context, request *RetryableRequ logger.Debug("sending request to remote server...", logAttrs...) } - times := int(request.Retry.Times) - for i := 0; i <= times; i++ { - req, cancel, reqError := request.CreateRequest(ctx) - if reqError != nil { - cancel() - span.SetStatus(codes.Error, "error happened when creating request") + var resp *http.Response + var errorBytes []byte + var err error + var cancel context.CancelFunc + + times := int(math.Max(float64(request.Runtime.Retry.Times), 1)) + delayMs := int(math.Max(float64(request.Runtime.Retry.Delay), 100)) + for i := range times { + resp, errorBytes, cancel, err = client.doRequest(ctx, request, port, i) //nolint:all + if err != nil { + span.SetStatus(codes.Error, "failed to execute the request") span.RecordError(err) - return nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) + + return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } - resp, err = client.client.Do(req) - if (err == nil && resp.StatusCode >= 200 && resp.StatusCode < 299) || i >= times { + + if (resp.StatusCode >= 200 && resp.StatusCode < 299) || + !slices.Contains(request.Runtime.Retry.HTTPStatus, resp.StatusCode) || i >= times { break } - logAttrs := []any{} - if err != nil { - span.AddEvent(fmt.Sprintf("request error, retry %d of %d", i+1, times), trace.WithAttributes(attribute.String("error", err.Error()))) - logAttrs = append(logAttrs, slog.Any("error", err.Error())) - } else { - var respBody []byte - if resp.Body != nil { - respBody, _ = io.ReadAll(resp.Body) - _ = resp.Body.Close() - } - - logAttrs = append(logAttrs, + if logger.Enabled(ctx, slog.LevelDebug) { + logger.Debug( + fmt.Sprintf("received error from remote server, retry %d of %d...", i+1, times), slog.Int("http_status", resp.StatusCode), slog.Any("response_headers", resp.Header), - slog.String("response_body", string(respBody)), - ) - span.AddEvent( - fmt.Sprintf("received error from remote server, retry %d of %d", i+1, times), - trace.WithAttributes( - attribute.Int("http_status", resp.StatusCode), - attribute.String("response_body", string(respBody)), - ), + slog.String("response_body", string(errorBytes)), ) } - if logger.Enabled(ctx, slog.LevelDebug) { - logger.Debug( - fmt.Sprintf("received error from remote server, retry %d of %d...", i+1, times), - logAttrs..., - ) + time.Sleep(time.Duration(delayMs) * time.Millisecond) + } + + defer cancel() + + 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 { + details["error"] = string(errorBytes) } - time.Sleep(time.Duration(request.Retry.Delay) * time.Millisecond) + span.SetStatus(codes.Error, "received error from remote server") + + return nil, nil, schema.NewConnectorError(resp.StatusCode, resp.Status, details) + } + + result, headers, evalErr := evalHTTPResponse(ctx, span, resp, contentType, selection, resultType, logger) + if evalErr != nil { + span.SetStatus(codes.Error, "failed to decode the http response") + span.RecordError(evalErr) + + return nil, nil, evalErr + } + + return result, headers, nil +} + +func (client *HTTPClient) doRequest(ctx context.Context, request *RetryableRequest, port int, retryCount int) (*http.Response, []byte, context.CancelFunc, error) { + method := strings.ToUpper(request.RawRequest.Method) + ctx, span := client.tracer.Start(ctx, fmt.Sprintf("%s %s", method, request.RawRequest.URL), trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + requestURL := request.URL.String() + + span.SetAttributes( + attribute.String("db.system", "rest"), + attribute.String("http.request.method", method), + attribute.String("url.full", requestURL), + attribute.String("server.address", request.URL.Hostname()), + attribute.Int("server.port", port), + attribute.String("network.protocol.name", "http"), + ) + + if request.ContentLength > 0 { + span.SetAttributes(attribute.Int64("http.request.body.size", request.ContentLength)) } + if retryCount > 0 { + span.SetAttributes(attribute.Int("http.request.resend_count", retryCount)) + } + setHeaderAttributes(span, "http.request.header.", request.Headers) + client.propagator.Inject(ctx, propagation.HeaderCarrier(request.Headers)) + + req, cancel, err := request.CreateRequest(ctx) if err != nil { span.SetStatus(codes.Error, "error happened when creating request") span.RecordError(err) - return nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) + + return nil, nil, nil, err } - span.SetAttributes(attribute.Int("http_status", resp.StatusCode)) + resp, err := client.client.Do(req) + if err != nil { + span.SetStatus(codes.Error, "error happened when executing the request") + span.RecordError(err) + cancel() - return evalHTTPResponse(ctx, span, resp, selection, resultType) -} + return nil, nil, nil, err + } -func evalHTTPResponse(ctx context.Context, span trace.Span, resp *http.Response, selection schema.NestedField, resultType schema.Type) (any, *schema.ConnectorError) { - logger := connector.GetLogger(ctx) - contentType := parseContentType(resp.Header.Get(contentTypeHeader)) - if resp.StatusCode >= 400 { - var respBody []byte - if resp.Body != nil { - var err error - respBody, err = io.ReadAll(resp.Body) - _ = resp.Body.Close() + span.SetAttributes(attribute.Int("http.response.status_code", resp.StatusCode)) + setHeaderAttributes(span, "http.response.header.", resp.Header) - if err != nil { - span.SetStatus(codes.Error, "error happened when reading response body") - span.RecordError(err) - return nil, schema.NewConnectorError(http.StatusInternalServerError, resp.Status, map[string]any{ - "error": err, - }) - } - } - details := make(map[string]any) - if contentType == rest.ContentTypeJSON && json.Valid(respBody) { - details["error"] = json.RawMessage(respBody) - } else { - details["error"] = string(respBody) - } + if resp.StatusCode < 300 { + return resp, nil, cancel, nil + } - span.SetAttributes(attribute.String("response_error", string(respBody))) - span.SetStatus(codes.Error, "received error from remote server") - return nil, schema.NewConnectorError(resp.StatusCode, resp.Status, details) + defer resp.Body.Close() + span.SetStatus(codes.Error, "Non-2xx status") + body, err := io.ReadAll(resp.Body) + if err != nil { + span.RecordError(err) + } else { + span.RecordError(errors.New(string(body))) } + 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) { if logger.Enabled(ctx, slog.LevelDebug) { logAttrs := []any{ slog.Int("http_status", resp.StatusCode), @@ -247,55 +316,59 @@ func evalHTTPResponse(ctx context.Context, span trace.Span, resp *http.Response, if resp.Body != nil && resp.StatusCode != http.StatusNoContent { respBody, readErr := io.ReadAll(resp.Body) _ = resp.Body.Close() + if readErr != nil { span.SetStatus(codes.Error, "error happened when reading response body") span.RecordError(readErr) - return nil, schema.NewConnectorError(http.StatusInternalServerError, "error happened when reading response body", map[string]any{ + + return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, "error happened when reading response body", map[string]any{ "error": readErr.Error(), }) } resp.Body = io.NopCloser(bytes.NewBuffer(respBody)) logAttrs = append(logAttrs, slog.String("response_body", string(respBody))) } + logger.Debug("received response from remote server", logAttrs...) } + defer func() { + if resp.Body != nil { + _ = resp.Body.Close() + } + }() + if resp.StatusCode == http.StatusNoContent { - return true, nil + return true, resp.Header, nil } if resp.Body == nil { - return nil, nil + return nil, resp.Header, nil } - defer func() { - _ = resp.Body.Close() - }() - switch contentType { case "": respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) + return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } if len(respBody) == 0 { - return nil, nil + return nil, resp.Header, nil } - return string(respBody), nil + return string(respBody), resp.Header, nil case "text/plain", "text/html": respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) + return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } - return string(respBody), nil + return string(respBody), resp.Header, nil case rest.ContentTypeJSON: if len(resultType) > 0 { namedType, err := resultType.AsNamed() if err == nil && namedType.Name == string(rest.ScalarString) { respBytes, err := io.ReadAll(resp.Body) - _ = resp.Body.Close() if err != nil { - return nil, schema.NewConnectorError(http.StatusInternalServerError, "failed to read response", map[string]any{ + return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, "failed to read response", map[string]any{ "reason": err.Error(), }) } @@ -303,27 +376,26 @@ func evalHTTPResponse(ctx context.Context, span trace.Span, resp *http.Response, var strResult string if err := json.Unmarshal(respBytes, &strResult); err != nil { // fallback to raw string response if the result type is String - return string(respBytes), nil + return string(respBytes), resp.Header, nil } - return strResult, nil + return strResult, resp.Header, nil } } var result any err := json.NewDecoder(resp.Body).Decode(&result) - _ = resp.Body.Close() if err != nil { - return nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) + return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } if selection == nil || selection.IsNil() { - return result, nil + return result, resp.Header, nil } result, err = utils.EvalNestedColumnFields(selection, result) if err != nil { - return nil, schema.InternalServerError(err.Error(), nil) + return nil, nil, schema.InternalServerError(err.Error(), nil) } - return result, nil + return result, resp.Header, nil case rest.ContentTypeNdJSON: var results []any decoder := json.NewDecoder(resp.Body) @@ -331,21 +403,21 @@ func evalHTTPResponse(ctx context.Context, span trace.Span, resp *http.Response, var r any err := decoder.Decode(&r) if err != nil { - return nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) + return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, err.Error(), nil) } results = append(results, r) } if selection == nil || selection.IsNil() { - return results, nil + return results, resp.Header, nil } result, err := utils.EvalNestedColumnFields(selection, any(results)) if err != nil { - return nil, schema.InternalServerError(err.Error(), nil) + return nil, nil, schema.InternalServerError(err.Error(), nil) } - return result, nil + return result, resp.Header, nil default: - return nil, schema.NewConnectorError(http.StatusInternalServerError, "failed to evaluate response", map[string]any{ + return nil, nil, schema.NewConnectorError(http.StatusInternalServerError, "failed to evaluate response", map[string]any{ "cause": "unsupported content type " + contentType, }) } diff --git a/connector/internal/metadata.go b/connector/internal/metadata.go index 49e9426..73d33eb 100644 --- a/connector/internal/metadata.go +++ b/connector/internal/metadata.go @@ -1,31 +1,32 @@ package internal import ( + "github.com/hasura/ndc-rest/ndc-rest-schema/configuration" rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" "github.com/hasura/ndc-sdk-go/schema" ) // MetadataCollection stores list of REST metadata with helper methods -type MetadataCollection []rest.NDCRestSchema +type MetadataCollection []configuration.NDCRestRuntimeSchema // GetFunction gets the NDC function by name -func (rms MetadataCollection) GetFunction(name string) (*rest.OperationInfo, *rest.NDCRestSettings, error) { +func (rms MetadataCollection) GetFunction(name string) (*rest.OperationInfo, configuration.NDCRestRuntimeSchema, error) { for _, rm := range rms { fn := rm.GetFunction(name) if fn != nil { - return fn, rm.Settings, nil + return fn, rm, nil } } - return nil, nil, schema.UnprocessableContentError("unsupported query: "+name, nil) + return nil, configuration.NDCRestRuntimeSchema{}, schema.UnprocessableContentError("unsupported query: "+name, nil) } // GetProcedure gets the NDC procedure by name -func (rms MetadataCollection) GetProcedure(name string) (*rest.OperationInfo, *rest.NDCRestSettings, error) { +func (rms MetadataCollection) GetProcedure(name string) (*rest.OperationInfo, configuration.NDCRestRuntimeSchema, error) { for _, rm := range rms { fn := rm.GetProcedure(name) if fn != nil { - return fn, rm.Settings, nil + return fn, rm, nil } } - return nil, nil, schema.UnprocessableContentError("unsupported query: "+name, nil) + return nil, configuration.NDCRestRuntimeSchema{}, schema.UnprocessableContentError("unsupported mutation: "+name, nil) } diff --git a/connector/internal/request.go b/connector/internal/request.go index a53f9d0..23f9381 100644 --- a/connector/internal/request.go +++ b/connector/internal/request.go @@ -18,14 +18,14 @@ import ( // RetryableRequest wraps the raw request with retryable type RetryableRequest struct { - RawRequest *rest.Request - URL string - ServerID string - ContentType string - Headers http.Header - Body io.ReadSeeker - Timeout uint - Retry *rest.RetryPolicy + RawRequest *rest.Request + URL url.URL + ServerID string + ContentType string + ContentLength int64 + Headers http.Header + Body io.ReadSeeker + Runtime rest.RuntimeSettings } // CreateRequest creates an HTTP request with body copied @@ -37,14 +37,16 @@ func (r *RetryableRequest) CreateRequest(ctx context.Context) (*http.Request, co } } - timeout := r.Timeout + timeout := r.Runtime.Timeout if timeout == 0 { timeout = defaultTimeoutSeconds } + ctxR, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) - request, err := http.NewRequestWithContext(ctxR, strings.ToUpper(r.RawRequest.Method), r.URL, r.Body) + request, err := http.NewRequestWithContext(ctxR, strings.ToUpper(r.RawRequest.Method), r.URL.String(), r.Body) if err != nil { cancel() + return nil, nil, err } for key, header := range r.Headers { @@ -55,40 +57,44 @@ func (r *RetryableRequest) CreateRequest(ctx context.Context) (*http.Request, co return request, cancel, nil } -func getHostFromServers(servers []rest.ServerConfig, serverIDs []string) (string, string) { - var results []string +func getBaseURLFromServers(servers []rest.ServerConfig, serverIDs []string) (*url.URL, string) { + var results []url.URL var selectedServerIDs []string for _, server := range servers { if len(serverIDs) > 0 && !slices.Contains(serverIDs, server.ID) { continue } - hostPtr := server.URL.Value() - if hostPtr != nil && *hostPtr != "" { - results = append(results, *hostPtr) + hostPtr, err := server.GetURL() + if err == nil { + results = append(results, hostPtr) selectedServerIDs = append(selectedServerIDs, server.ID) } } switch len(results) { case 0: - return "", "" + return nil, "" case 1: - return results[0], selectedServerIDs[0] + result := results[0] + return &result, selectedServerIDs[0] default: index := rand.IntN(len(results) - 1) - return results[index], selectedServerIDs[index] + host := results[index] + return &host, selectedServerIDs[index] } } // BuildDistributedRequestsWithOptions builds distributed requests with options func BuildDistributedRequestsWithOptions(request *RetryableRequest, restOptions *RESTOptions) ([]RetryableRequest, error) { - if strings.HasPrefix(request.URL, "http") { + if strings.HasPrefix(request.URL.Scheme, "http") { return []RetryableRequest{*request}, nil } if !restOptions.Distributed || len(restOptions.Settings.Servers) == 1 { - host, serverID := getHostFromServers(restOptions.Settings.Servers, restOptions.Servers) - request.URL = host + request.URL + baseURL, serverID := getBaseURLFromServers(restOptions.Settings.Servers, restOptions.Servers) + request.URL.Scheme = baseURL.Scheme + request.URL.Host = baseURL.Host + request.URL.Path = baseURL.Path + request.URL.Path request.ServerID = serverID if err := request.applySettings(restOptions.Settings, restOptions.Explain); err != nil { return nil, err @@ -113,13 +119,15 @@ func BuildDistributedRequestsWithOptions(request *RetryableRequest, restOptions } } for _, serverID := range serverIDs { - host, serverID := getHostFromServers(restOptions.Settings.Servers, []string{serverID}) - if host == "" { + baseURL, serverID := getBaseURLFromServers(restOptions.Settings.Servers, []string{serverID}) + if baseURL == nil { continue } - + baseURL.Path += request.URL.Path + baseURL.RawQuery = request.URL.RawQuery + baseURL.Fragment = request.URL.Fragment req := RetryableRequest{ - URL: fmt.Sprintf("%s%s", host, request.URL), + URL: *baseURL, ServerID: serverID, RawRequest: request.RawRequest, ContentType: request.ContentType, @@ -177,7 +185,7 @@ func (req *RetryableRequest) applySecurity(serverConfig *rest.ServerConfig, isEx securityScheme = &sc if slices.Contains([]rest.SecuritySchemeType{rest.HTTPAuthScheme, rest.APIKeyScheme}, sc.Type) && - sc.Value != nil && sc.Value.Value() != nil && *sc.Value.Value() != "" { + sc.Value != nil && sc.GetValue() != "" { break } } @@ -200,45 +208,36 @@ func (req *RetryableRequest) applySecurity(serverConfig *rest.ServerConfig, isEx if scheme == "bearer" || scheme == "basic" { scheme = utils.ToPascalCase(securityScheme.Scheme) } - if securityScheme.Value != nil { - v := securityScheme.Value.Value() - if v != nil { - req.Headers.Set(headerName, fmt.Sprintf("%s %s", scheme, eitherMaskSecret(*v, isExplain))) - } + v := securityScheme.GetValue() + if v != "" { + req.Headers.Set(headerName, fmt.Sprintf("%s %s", scheme, eitherMaskSecret(v, isExplain))) } case rest.APIKeyScheme: switch securityScheme.In { case rest.APIKeyInHeader: if securityScheme.Value != nil { - value := securityScheme.Value.Value() - if value != nil { - req.Headers.Set(securityScheme.Name, eitherMaskSecret(*value, isExplain)) + value := securityScheme.GetValue() + if value != "" { + req.Headers.Set(securityScheme.Name, eitherMaskSecret(value, isExplain)) } } case rest.APIKeyInQuery: - value := securityScheme.Value.Value() - if value != nil { - endpoint, err := url.Parse(req.URL) - if err != nil { - return err - } - + value := securityScheme.GetValue() + if value != "" { + endpoint := req.URL q := endpoint.Query() - q.Add(securityScheme.Name, eitherMaskSecret(*value, isExplain)) + q.Add(securityScheme.Name, eitherMaskSecret(value, isExplain)) endpoint.RawQuery = q.Encode() - req.URL = endpoint.String() + req.URL = endpoint } case rest.APIKeyInCookie: - if securityScheme.Value != nil { - v := securityScheme.Value.Value() - if v != nil { - req.Headers.Set("Cookie", fmt.Sprintf("%s=%s", securityScheme.Name, eitherMaskSecret(*v, isExplain))) - } - } + // Cookie header should be forwarded from Hasura engine default: return fmt.Errorf("unsupported location for apiKey scheme: %s", securityScheme.In) } // TODO: support OAuth and OIDC + // Authentication headers can be forwarded from Hasura engine + case rest.OAuth2Scheme, rest.OpenIDConnectScheme: default: return fmt.Errorf("unsupported security scheme: %s", securityScheme.Type) } @@ -246,9 +245,6 @@ func (req *RetryableRequest) applySecurity(serverConfig *rest.ServerConfig, isEx } func (req *RetryableRequest) applySettings(settings *rest.NDCRestSettings, isExplain bool) error { - if req.Retry == nil { - req.Retry = &rest.RetryPolicy{} - } if settings == nil { return nil } @@ -260,59 +256,19 @@ func (req *RetryableRequest) applySettings(settings *rest.NDCRestSettings, isExp return err } - if req.Timeout <= 0 && serverConfig.Timeout != nil { - timeout, err := serverConfig.Timeout.Value() - if err != nil { - return err - } - if timeout != nil && *timeout > 0 { - req.Timeout = uint(*timeout) - } - } - if req.Timeout == 0 { - req.Timeout = defaultTimeoutSeconds - } - - if serverConfig.Retry != nil { - if req.Retry.Times <= 0 { - times, err := serverConfig.Retry.Times.Value() - if err == nil && times != nil && *times > 0 { - req.Retry.Times = uint(*times) - } - } - if req.Retry.Delay <= 0 { - delay, err := serverConfig.Retry.Delay.Value() - if err == nil && delay != nil && *delay > 0 { - req.Retry.Delay = uint(*delay) - } else { - req.Retry.Delay = defaultRetryDelays - } - } - if len(req.Retry.HTTPStatus) == 0 { - status, err := serverConfig.Retry.HTTPStatus.Value() - if err != nil || len(status) == 0 { - status = defaultRetryHTTPStatus - } - for _, st := range status { - req.Retry.HTTPStatus = append(req.Retry.HTTPStatus, int(st)) - } - } - } - - req.applyDefaultHeaders(serverConfig.Headers) - req.applyDefaultHeaders(settings.Headers) + req.applyDefaultHeaders(serverConfig.GetHeaders()) + req.applyDefaultHeaders(settings.GetHeaders()) return nil } -func (req *RetryableRequest) applyDefaultHeaders(defaultHeaders map[string]rest.EnvString) { +func (req *RetryableRequest) applyDefaultHeaders(defaultHeaders map[string]string) { for k, envValue := range defaultHeaders { if req.Headers.Get(k) != "" { continue } - value := envValue.Value() - if value != nil && *value != "" { - req.Headers.Set(k, *value) + if envValue != "" { + req.Headers.Set(k, envValue) } } } diff --git a/connector/internal/request_builder.go b/connector/internal/request_builder.go index 55e00ae..5b7648e 100644 --- a/connector/internal/request_builder.go +++ b/connector/internal/request_builder.go @@ -21,14 +21,16 @@ type RequestBuilder struct { Schema *rest.NDCRestSchema Operation *rest.OperationInfo Arguments map[string]any + Runtime rest.RuntimeSettings } // NewRequestBuilder creates a new RequestBuilder instance -func NewRequestBuilder(restSchema *rest.NDCRestSchema, operation *rest.OperationInfo, arguments map[string]any) *RequestBuilder { +func NewRequestBuilder(restSchema *rest.NDCRestSchema, operation *rest.OperationInfo, arguments map[string]any, runtime rest.RuntimeSettings) *RequestBuilder { return &RequestBuilder{ Schema: restSchema, Operation: operation, Arguments: arguments, + Runtime: runtime, } } @@ -41,11 +43,16 @@ func (c *RequestBuilder) Build() (*RetryableRequest, error) { }) } - var buffer io.ReadSeeker - rawRequest := c.Operation.Request contentType := rest.ContentTypeJSON + request := &RetryableRequest{ + URL: *endpoint, + RawRequest: rawRequest, + Headers: headers, + Runtime: c.Runtime, + } + if rawRequest.RequestBody != nil { contentType = rawRequest.RequestBody.ContentType bodyInfo, infoOk := c.Operation.Arguments[rest.BodyKey] @@ -62,28 +69,37 @@ func (c *RequestBuilder) Build() (*RetryableRequest, error) { if err != nil { return nil, err } - buffer = bytes.NewReader([]byte(dataURI.Data)) + r := bytes.NewReader([]byte(dataURI.Data)) + request.ContentLength = r.Size() + request.Body = r } else if strings.HasPrefix(contentType, "text/") { - buffer = bytes.NewReader([]byte(fmt.Sprint(bodyData))) + r := bytes.NewReader([]byte(fmt.Sprint(bodyData))) + request.ContentLength = r.Size() + request.Body = r } else if strings.HasPrefix(contentType, "multipart/") { - buffer, contentType, err = c.createMultipartForm(bodyData) + var r *bytes.Reader + r, contentType, err = c.createMultipartForm(bodyData) if err != nil { return nil, err } + request.ContentLength = r.Size() + request.Body = r } else { switch contentType { case rest.ContentTypeFormURLEncoded: - buffer, err = c.createFormURLEncoded(&bodyInfo, bodyData) + r, err := c.createFormURLEncoded(&bodyInfo, bodyData) if err != nil { return nil, err } + request.Body = r case rest.ContentTypeJSON, "": bodyBytes, err := json.Marshal(bodyData) if err != nil { return nil, err } - buffer = bytes.NewReader(bodyBytes) + request.ContentLength = int64(len(bodyBytes)) + request.Body = bytes.NewReader(bodyBytes) default: return nil, fmt.Errorf("unsupported content type %s", contentType) } @@ -99,14 +115,24 @@ func (c *RequestBuilder) Build() (*RetryableRequest, error) { } } - request := &RetryableRequest{ - URL: endpoint, - RawRequest: rawRequest, - ContentType: contentType, - Headers: headers, - Body: buffer, - Timeout: rawRequest.Timeout, - Retry: rawRequest.Retry, + request.ContentType = contentType + + if rawRequest.RuntimeSettings != nil { + if rawRequest.RuntimeSettings.Timeout > 0 { + request.Runtime.Timeout = rawRequest.RuntimeSettings.Timeout + } + if rawRequest.RuntimeSettings.Retry.Times > 0 { + request.Runtime.Retry.Times = rawRequest.RuntimeSettings.Retry.Times + } + if rawRequest.RuntimeSettings.Retry.Delay > 0 { + request.Runtime.Retry.Delay = rawRequest.RuntimeSettings.Retry.Delay + } + if rawRequest.RuntimeSettings.Retry.HTTPStatus != nil { + request.Runtime.Retry.HTTPStatus = rawRequest.RuntimeSettings.Retry.HTTPStatus + } + } + if request.Runtime.Retry.HTTPStatus == nil { + request.Runtime.Retry.HTTPStatus = defaultRetryHTTPStatus } return request, nil @@ -136,7 +162,7 @@ func (c *RequestBuilder) createFormURLEncoded(bodyInfo *rest.ArgumentInfo, bodyD return bytes.NewReader([]byte(rawQuery)), nil } -func (c *RequestBuilder) createMultipartForm(bodyData any) (io.ReadSeeker, string, error) { +func (c *RequestBuilder) createMultipartForm(bodyData any) (*bytes.Reader, string, error) { bodyInfo, ok := c.Operation.Arguments[rest.BodyKey] if !ok { return nil, "", errRequestBodyTypeRequired diff --git a/connector/internal/request_parameter.go b/connector/internal/request_parameter.go index 3b35c7e..34fd25f 100644 --- a/connector/internal/request_parameter.go +++ b/connector/internal/request_parameter.go @@ -21,16 +21,19 @@ import ( var urlAndHeaderLocations = []rest.ParameterLocation{rest.InPath, rest.InQuery, rest.InHeader} // evaluate URL and header parameters -func (c *RequestBuilder) evalURLAndHeaderParameters() (string, http.Header, error) { +func (c *RequestBuilder) evalURLAndHeaderParameters() (*url.URL, http.Header, error) { endpoint, err := url.Parse(c.Operation.Request.URL) if err != nil { - return "", nil, err + return nil, nil, err } headers := http.Header{} for k, h := range c.Operation.Request.Headers { - v := h.Value() - if v != nil && *v != "" { - headers.Add(k, *v) + v, err := h.Get() + if err != nil { + return nil, nil, fmt.Errorf("invalid header value, key: %s, %w", k, err) + } + if v != "" { + headers.Add(k, v) } } @@ -39,10 +42,10 @@ func (c *RequestBuilder) evalURLAndHeaderParameters() (string, http.Header, erro continue } if err := c.evalURLAndHeaderParameterBySchema(endpoint, &headers, argumentKey, &argumentInfo, c.Arguments[argumentKey]); err != nil { - return "", nil, fmt.Errorf("%s: %w", argumentKey, err) + return nil, nil, fmt.Errorf("%s: %w", argumentKey, err) } } - return endpoint.String(), headers, nil + return endpoint, headers, nil } // the query parameters serialization follows [OAS 3.1 spec] @@ -233,7 +236,7 @@ func encodeScalarParameterReflectionValues(reflectValue reflect.Value, scalar *s } return []ParameterItem{NewParameterItem([]Key{}, []string{value})}, nil case *schema.TypeRepresentationDate: - value, err := utils.DecodeDateReflection(reflectValue) + value, err := utils.DecodeDateTimeReflection(reflectValue) if err != nil { return nil, fmt.Errorf("%s: %w", strings.Join(fieldPaths, ""), err) } diff --git a/connector/internal/request_parameter_test.go b/connector/internal/request_parameter_test.go index 0e5c070..23dba43 100644 --- a/connector/internal/request_parameter_test.go +++ b/connector/internal/request_parameter_test.go @@ -301,7 +301,7 @@ func TestEvalURLAndHeaderParameters(t *testing.T) { assert.ErrorContains(t, err, tc.errorMsg) } else { assert.NilError(t, err) - decodedValue, err := url.QueryUnescape(result) + decodedValue, err := url.QueryUnescape(result.String()) assert.NilError(t, err) assert.Equal(t, tc.expectedURL, decodedValue) for k, v := range tc.headers { diff --git a/connector/internal/types.go b/connector/internal/types.go index 3a98789..0c1a7a5 100644 --- a/connector/internal/types.go +++ b/connector/internal/types.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "regexp" rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" "github.com/hasura/ndc-sdk-go/schema" @@ -22,55 +23,17 @@ var ( errRequestBodyTypeRequired = errors.New("failed to decode request body, empty body type") ) -var defaultRetryHTTPStatus = []int64{429, 500, 502, 503} - -const ( - RESTOptionsArgumentName string = "restOptions" - RESTSingleOptionsObjectName string = "RestSingleOptions" - RESTDistributedOptionsObjectName string = "RestDistributedOptions" - RESTServerIDScalarName string = "RestServerId" - DistributedErrorObjectName string = "DistributedError" -) - -// SingleObjectType represents the object type of REST execution options for single server -var SingleObjectType rest.ObjectType = rest.ObjectType{ - Description: utils.ToPtr("Execution options for REST requests to a single server"), - Fields: map[string]rest.ObjectField{ - "servers": { - ObjectField: schema.ObjectField{ - Description: utils.ToPtr("Specify remote servers to receive the request. If there are many server IDs the server is selected randomly"), - Type: schema.NewNullableType(schema.NewArrayType(schema.NewNamedType(RESTServerIDScalarName))).Encode(), - }, - }, - }, -} - -// DistributedObjectType represents the object type of REST execution options for distributed servers -var DistributedObjectType rest.ObjectType = rest.ObjectType{ - Description: utils.ToPtr("Distributed execution options for REST requests to multiple servers"), - Fields: map[string]rest.ObjectField{ - "servers": { - ObjectField: schema.ObjectField{ - Description: utils.ToPtr("Specify remote servers to receive the request"), - Type: schema.NewNullableType(schema.NewArrayType(schema.NewNamedType(RESTServerIDScalarName))).Encode(), - }, - }, - "parallel": { - ObjectField: schema.ObjectField{ - Description: utils.ToPtr("Execute requests to remote servers in parallel"), - Type: schema.NewNullableNamedType(string(rest.ScalarBoolean)).Encode(), - }, - }, - }, -} +var defaultRetryHTTPStatus = []int{429, 500, 502, 503} +var sensitiveHeaderRegex = regexp.MustCompile(`auth|key|secret|token`) // RESTOptions represent execution options for REST requests type RESTOptions struct { - Servers []string `json:"servers" yaml:"serverIds"` + Servers []string `json:"serverIds" yaml:"serverIds"` Parallel bool `json:"parallel" yaml:"parallel"` Explain bool `json:"-" yaml:"-"` Distributed bool `json:"-" yaml:"-"` + Concurrency uint `json:"-" yaml:"-"` Settings *rest.NDCRestSettings `json:"-" yaml:"-"` } diff --git a/connector/internal/utils.go b/connector/internal/utils.go index 93858d1..2a33f47 100644 --- a/connector/internal/utils.go +++ b/connector/internal/utils.go @@ -2,9 +2,12 @@ package internal import ( "fmt" + "net/http" "strings" "github.com/hasura/ndc-sdk-go/schema" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) // UnwrapNullableType unwraps the underlying type of the nullable type @@ -43,3 +46,19 @@ func MaskString(input string) string { return input[0:3] + strings.Repeat("*", 7) + fmt.Sprintf("(%d)", inputLength) } } + +func setHeaderAttributes(span trace.Span, prefix string, httpHeaders http.Header) { + for key, headers := range httpHeaders { + if len(headers) == 0 { + continue + } + values := headers + if sensitiveHeaderRegex.MatchString(strings.ToLower(key)) { + values = make([]string, len(headers)) + for i, header := range headers { + values[i] = MaskString(header) + } + } + span.SetAttributes(attribute.StringSlice(prefix+strings.ToLower(key), values)) + } +} diff --git a/connector/mutation.go b/connector/mutation.go index c72dff1..0a24990 100644 --- a/connector/mutation.go +++ b/connector/mutation.go @@ -9,28 +9,17 @@ import ( "github.com/hasura/ndc-rest/ndc-rest-schema/configuration" rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" "github.com/hasura/ndc-sdk-go/schema" + "go.opentelemetry.io/otel/codes" + "golang.org/x/sync/errgroup" ) // Mutation executes a mutation. func (c *RESTConnector) Mutation(ctx context.Context, configuration *configuration.Configuration, state *State, request *schema.MutationRequest) (*schema.MutationResponse, error) { - operationResults := make([]schema.MutationOperationResults, len(request.Operations)) - - for i, operation := range request.Operations { - switch operation.Type { - case schema.MutationOperationProcedure: - result, err := c.execProcedure(ctx, &operation) - if err != nil { - return nil, err - } - operationResults[i] = result - default: - return nil, schema.BadRequestError(fmt.Sprintf("invalid operation type: %s", operation.Type), nil) - } + if len(request.Operations) == 1 || c.config.Concurrency.Mutation <= 1 { + return c.execMutationSync(ctx, state, request) } - return &schema.MutationResponse{ - OperationResults: operationResults, - }, nil + return c.execMutationAsync(ctx, state, request) } // MutationExplain explains a mutation by creating an execution plan. @@ -55,7 +44,7 @@ func (c *RESTConnector) MutationExplain(ctx context.Context, configuration *conf } func (c *RESTConnector) explainProcedure(operation *schema.MutationOperation) (*internal.RetryableRequest, *rest.OperationInfo, *internal.RESTOptions, error) { - procedure, settings, err := c.metadata.GetProcedure(operation.Name) + procedure, metadata, err := c.metadata.GetProcedure(operation.Name) if err != nil { return nil, nil, nil, err } @@ -69,32 +58,93 @@ func (c *RESTConnector) explainProcedure(operation *schema.MutationOperation) (* } // 2. build the request - builder := internal.NewRequestBuilder(c.schema, procedure, rawArgs) + builder := internal.NewRequestBuilder(c.schema, procedure, rawArgs, metadata.Runtime) httpRequest, err := builder.Build() if err != nil { return nil, nil, nil, err } - restOptions, err := parseRESTOptionsFromArguments(procedure.Arguments, rawArgs[internal.RESTOptionsArgumentName]) + if err := c.evalForwardedHeaders(httpRequest, rawArgs); err != nil { + return nil, nil, nil, schema.UnprocessableContentError("invalid forwarded headers", map[string]any{ + "cause": err.Error(), + }) + } + + restOptions, err := c.parseRESTOptionsFromArguments(procedure.Arguments, rawArgs) if err != nil { return nil, nil, nil, schema.UnprocessableContentError("invalid rest options", map[string]any{ "cause": err.Error(), }) } - restOptions.Settings = settings + restOptions.Settings = metadata.Settings return httpRequest, procedure, restOptions, nil } -func (c *RESTConnector) execProcedure(ctx context.Context, operation *schema.MutationOperation) (schema.MutationOperationResults, error) { - httpRequest, procedure, restOptions, err := c.explainProcedure(operation) +func (c *RESTConnector) execMutationSync(ctx context.Context, state *State, request *schema.MutationRequest) (*schema.MutationResponse, error) { + operationResults := make([]schema.MutationOperationResults, len(request.Operations)) + for i, operation := range request.Operations { + result, err := c.execMutationOperation(ctx, state, operation, i) + if err != nil { + return nil, err + } + operationResults[i] = result + } + + return &schema.MutationResponse{ + OperationResults: operationResults, + }, nil +} + +func (c *RESTConnector) execMutationAsync(ctx context.Context, state *State, request *schema.MutationRequest) (*schema.MutationResponse, error) { + operationResults := make([]schema.MutationOperationResults, len(request.Operations)) + + eg, ctx := errgroup.WithContext(ctx) + eg.SetLimit(int(c.config.Concurrency.Mutation)) + + for i, operation := range request.Operations { + func(index int, op schema.MutationOperation) { + eg.Go(func() error { + result, err := c.execMutationOperation(ctx, state, op, index) + if err != nil { + return err + } + operationResults[index] = result + + return nil + }) + }(i, operation) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + + return &schema.MutationResponse{ + OperationResults: operationResults, + }, nil +} + +func (c *RESTConnector) execMutationOperation(parentCtx context.Context, state *State, operation schema.MutationOperation, index int) (schema.MutationOperationResults, error) { + ctx, span := state.Tracer.Start(parentCtx, fmt.Sprintf("Execute Operation %d", index)) + defer span.End() + + httpRequest, procedure, restOptions, err := c.explainProcedure(&operation) if err != nil { + span.SetStatus(codes.Error, "failed to explain mutation") + span.RecordError(err) + return nil, err } - result, err := c.client.Send(ctx, httpRequest, operation.Fields, procedure.ResultType, restOptions) + restOptions.Concurrency = c.config.Concurrency.REST + result, headers, err := c.client.Send(ctx, httpRequest, operation.Fields, procedure.ResultType, restOptions) if err != nil { + span.SetStatus(codes.Error, "failed to execute mutation") + span.RecordError(err) + return nil, err } - return schema.NewProcedureResult(result).Encode(), nil + + return schema.NewProcedureResult(c.createHeaderForwardingResponse(result, headers)).Encode(), nil } diff --git a/connector/query.go b/connector/query.go index 43214a2..384e9a1 100644 --- a/connector/query.go +++ b/connector/query.go @@ -3,6 +3,7 @@ package rest import ( "context" "encoding/json" + "fmt" "io" "github.com/hasura/ndc-rest/connector/internal" @@ -10,6 +11,8 @@ import ( rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" "github.com/hasura/ndc-sdk-go/schema" "github.com/hasura/ndc-sdk-go/utils" + "go.opentelemetry.io/otel/codes" + "golang.org/x/sync/errgroup" ) // Query executes a query. @@ -23,23 +26,11 @@ func (c *RESTConnector) Query(ctx context.Context, configuration *configuration. requestVars = []schema.QueryRequestVariablesElem{make(schema.QueryRequestVariablesElem)} } - rowSets := make([]schema.RowSet, len(requestVars)) - for i, requestVar := range requestVars { - result, err := c.execQuery(ctx, request, valueField, requestVar) - if err != nil { - return nil, err - } - rowSets[i] = schema.RowSet{ - Aggregates: schema.RowSetAggregates{}, - Rows: []map[string]any{ - { - "__value": result, - }, - }, - } + if len(requestVars) == 1 || c.config.Concurrency.Query <= 1 { + return c.execQuerySync(ctx, state, request, valueField, requestVars) } - return rowSets, nil + return c.execQueryAsync(ctx, state, request, valueField, requestVars) } // QueryExplain explains a query by creating an execution plan. @@ -58,7 +49,7 @@ func (c *RESTConnector) QueryExplain(ctx context.Context, configuration *configu } func (c *RESTConnector) explainQuery(request *schema.QueryRequest, variables map[string]any) (*internal.RetryableRequest, *rest.OperationInfo, *internal.RESTOptions, error) { - function, settings, err := c.metadata.GetFunction(request.Collection) + function, metadata, err := c.metadata.GetFunction(request.Collection) if err != nil { return nil, nil, nil, err } @@ -72,30 +63,106 @@ func (c *RESTConnector) explainQuery(request *schema.QueryRequest, variables map } // 2. build the request - req, err := internal.NewRequestBuilder(c.schema, function, rawArgs).Build() + req, err := internal.NewRequestBuilder(c.schema, function, rawArgs, metadata.Runtime).Build() if err != nil { return nil, nil, nil, err } - restOptions, err := parseRESTOptionsFromArguments(function.Arguments, rawArgs[internal.RESTOptionsArgumentName]) + if err := c.evalForwardedHeaders(req, rawArgs); err != nil { + return nil, nil, nil, schema.UnprocessableContentError("invalid forwarded headers", map[string]any{ + "cause": err.Error(), + }) + } + + restOptions, err := c.parseRESTOptionsFromArguments(function.Arguments, rawArgs) if err != nil { return nil, nil, nil, schema.UnprocessableContentError("invalid rest options", map[string]any{ "cause": err.Error(), }) } - restOptions.Settings = settings + restOptions.Settings = metadata.Settings return req, function, restOptions, err } -func (c *RESTConnector) execQuery(ctx context.Context, request *schema.QueryRequest, queryFields schema.NestedField, variables map[string]any) (any, error) { +func (c *RESTConnector) execQuerySync(ctx context.Context, state *State, request *schema.QueryRequest, valueField schema.NestedField, requestVars []schema.QueryRequestVariablesElem) ([]schema.RowSet, error) { + rowSets := make([]schema.RowSet, len(requestVars)) + + for i, requestVar := range requestVars { + result, err := c.execQuery(ctx, state, request, valueField, requestVar, i) + if err != nil { + return nil, err + } + rowSets[i] = schema.RowSet{ + Aggregates: schema.RowSetAggregates{}, + Rows: []map[string]any{ + { + "__value": result, + }, + }, + } + } + + return rowSets, nil +} + +func (c *RESTConnector) execQueryAsync(ctx context.Context, state *State, request *schema.QueryRequest, valueField schema.NestedField, requestVars []schema.QueryRequestVariablesElem) ([]schema.RowSet, error) { + rowSets := make([]schema.RowSet, len(requestVars)) + + eg, ctx := errgroup.WithContext(ctx) + eg.SetLimit(int(c.config.Concurrency.Query)) + + for i, requestVar := range requestVars { + func(index int, vars schema.QueryRequestVariablesElem) { + eg.Go(func() error { + result, err := c.execQuery(ctx, state, request, valueField, requestVar, i) + if err != nil { + return err + } + rowSets[index] = schema.RowSet{ + Aggregates: schema.RowSetAggregates{}, + Rows: []map[string]any{ + { + "__value": result, + }, + }, + } + + return nil + }) + }(i, requestVar) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + + return rowSets, nil +} + +func (c *RESTConnector) execQuery(ctx context.Context, state *State, request *schema.QueryRequest, queryFields schema.NestedField, variables map[string]any, index int) (any, error) { + ctx, span := state.Tracer.Start(ctx, fmt.Sprintf("Execute Query %d", index)) + defer span.End() + httpRequest, function, restOptions, err := c.explainQuery(request, variables) if err != nil { + span.SetStatus(codes.Error, "failed to explain query") + span.RecordError(err) + + return nil, err + } + + restOptions.Concurrency = c.config.Concurrency.REST + result, headers, err := c.client.Send(ctx, httpRequest, queryFields, function.ResultType, restOptions) + if err != nil { + span.SetStatus(codes.Error, "failed to execute the http request") + span.RecordError(err) + return nil, err } - return c.client.Send(ctx, httpRequest, queryFields, function.ResultType, restOptions) + return c.createHeaderForwardingResponse(result, headers), nil } func serializeExplainResponse(httpRequest *internal.RetryableRequest, restOptions *internal.RESTOptions) (*schema.ExplainResponse, error) { @@ -119,7 +186,7 @@ func serializeExplainResponse(httpRequest *internal.RetryableRequest, restOption if err != nil { return nil, err } - explainResp.Details["url"] = requests[0].URL + explainResp.Details["url"] = requests[0].URL.String() if httpRequest.Body != nil { bodyBytes, err := io.ReadAll(httpRequest.Body) diff --git a/connector/schema.go b/connector/schema.go index dbd733b..57afef1 100644 --- a/connector/schema.go +++ b/connector/schema.go @@ -5,16 +5,14 @@ import ( "encoding/json" "fmt" "log/slog" - "reflect" - "strconv" + "net/http" + "slices" + "github.com/go-viper/mapstructure/v2" "github.com/hasura/ndc-rest/connector/internal" - "github.com/hasura/ndc-rest/ndc-rest-schema/command" "github.com/hasura/ndc-rest/ndc-rest-schema/configuration" rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" - restUtils "github.com/hasura/ndc-rest/ndc-rest-schema/utils" "github.com/hasura/ndc-sdk-go/schema" - "github.com/hasura/ndc-sdk-go/utils" ) // GetSchema gets the connector's schema. @@ -22,407 +20,94 @@ func (c *RESTConnector) GetSchema(ctx context.Context, configuration *configurat return c.rawSchema, nil } -// BuildSchemaFiles build NDC REST schema from file list -func BuildSchemaFiles(configDir string, files []configuration.ConfigItem, logger *slog.Logger) ([]NDCRestSchemaWithName, map[string][]string) { - schemas := make([]NDCRestSchemaWithName, len(files)) - errors := make(map[string][]string) - for i, file := range files { - var errs []string - schemaOutput, err := buildSchemaFile(configDir, &file, logger) - if err != nil { - errs = append(errs, err.Error()) - } - if schemaOutput != nil { - schemas[i] = NDCRestSchemaWithName{ - name: file.File, - schema: schemaOutput, - } - } - if len(errs) > 0 { - errors[file.File] = errs - } - } - - return schemas, errors -} - -func buildSchemaFile(configDir string, conf *configuration.ConfigItem, logger *slog.Logger) (*rest.NDCRestSchema, error) { - if conf.ConvertConfig.File == "" { - return nil, errFilePathRequired - } - command.ResolveConvertConfigArguments(&conf.ConvertConfig, configDir, nil) - ndcSchema, err := command.ConvertToNDCSchema(&conf.ConvertConfig, logger) - if err != nil { - return nil, err - } - - if ndcSchema.Settings == nil || len(ndcSchema.Settings.Servers) == 0 { - return nil, fmt.Errorf("the servers setting of schema %s is empty", conf.ConvertConfig.File) - } - - buildRESTArguments(ndcSchema, conf) - - return ndcSchema, nil -} - // ApplyNDCRestSchemas applies slice of raw NDC REST schemas to the connector -func (c *RESTConnector) ApplyNDCRestSchemas(schemas []NDCRestSchemaWithName) map[string][]string { - ndcSchema := &rest.NDCRestSchema{ - ScalarTypes: make(schema.SchemaResponseScalarTypes), - ObjectTypes: make(map[string]rest.ObjectType), - Functions: make(map[string]rest.OperationInfo), - Procedures: make(map[string]rest.OperationInfo), - } - errors := make(map[string][]string) - - for _, item := range schemas { - settings := item.schema.Settings - if settings == nil { - settings = &rest.NDCRestSettings{} - } else { - for i, server := range settings.Servers { - if server.Retry == nil { - if settings.Retry != nil { - server.Retry = settings.Retry - } - } else if settings.Retry != nil { - delay, err := server.Retry.Delay.Value() - if err != nil { - errors[fmt.Sprintf("settings.servers[%d].retry.delay", i)] = []string{err.Error()} - return errors - } - if delay == nil || *delay <= 0 { - server.Retry.Delay = settings.Retry.Delay - } - - times, err := server.Retry.Times.Value() - if err != nil { - errors[fmt.Sprintf("settings.servers[%d].retry.times", i)] = []string{err.Error()} - return errors - } - if times == nil || *times <= 0 { - server.Retry.Times = settings.Retry.Times - } - - status, err := server.Retry.HTTPStatus.Value() - if err != nil { - errors[fmt.Sprintf("settings.servers[%d].retry.httpStatus", i)] = []string{err.Error()} - return errors - } - if len(status) == 0 { - server.Retry.HTTPStatus = settings.Retry.HTTPStatus - } - } - - if server.Timeout == nil { - server.Timeout = settings.Timeout - } else { - timeout, err := server.Timeout.Value() - if err != nil { - errors[fmt.Sprintf("settings.servers[%d].timeout", i)] = []string{err.Error()} - return errors - } - if timeout == nil || *timeout <= 0 { - settings.Timeout = rest.NewEnvIntValue(*timeout) - } - } - - if server.Security.IsEmpty() { - server.Security = settings.Security - } - if server.SecuritySchemes == nil { - server.SecuritySchemes = make(map[string]rest.SecurityScheme) - } - for key, scheme := range settings.SecuritySchemes { - _, ok := server.SecuritySchemes[key] - if !ok { - server.SecuritySchemes[key] = scheme - } - } - - if server.Headers == nil { - server.Headers = make(map[string]rest.EnvString) - } - for key, value := range settings.Headers { - _, ok := server.Headers[key] - if !ok { - server.Headers[key] = value - } - } - settings.Servers[i] = server - } - } - meta := rest.NDCRestSchema{ - Settings: settings, - Functions: map[string]rest.OperationInfo{}, - Procedures: map[string]rest.OperationInfo{}, - } - var errs []string - - for name, scalar := range item.schema.ScalarTypes { - if originScalar, ok := ndcSchema.ScalarTypes[name]; !ok { - ndcSchema.ScalarTypes[name] = scalar - } else if !rest.IsDefaultScalar(name) && !reflect.DeepEqual(originScalar, scalar) { - slog.Warn(fmt.Sprintf("Scalar type %s is conflicted", name)) - } - } - for name, object := range item.schema.ObjectTypes { - if _, ok := ndcSchema.ObjectTypes[name]; !ok { - ndcSchema.ObjectTypes[name] = object - } else { - slog.Warn(fmt.Sprintf("Object type %s is conflicted", name)) - } - } - - for fnName, fnItem := range item.schema.Functions { - if fnItem.Request == nil || fnItem.Request.URL == "" { - continue - } - req, err := validateRequestSchema(fnItem.Request, "get") - if err != nil { - errs = append(errs, fmt.Sprintf("function %s: %s", fnName, err)) - continue - } - fn := rest.OperationInfo{ - Request: req, - Arguments: fnItem.Arguments, - Description: fnItem.Description, - ResultType: fnItem.ResultType, - } - meta.Functions[fnName] = fn - ndcSchema.Functions[fnName] = fn - } - - for procName, procItem := range item.schema.Procedures { - if procItem.Request == nil || procItem.Request.URL == "" { - continue - } - req, err := validateRequestSchema(procItem.Request, "") - if err != nil { - errs = append(errs, fmt.Sprintf("procedure %s: %s", procName, err)) - continue - } - - proc := rest.OperationInfo{ - Request: req, - Arguments: procItem.Arguments, - Description: procItem.Description, - ResultType: procItem.ResultType, - } - meta.Procedures[procName] = proc - ndcSchema.Procedures[procName] = proc - } - - if len(errs) > 0 { - errors[item.name] = errs - continue +func (c *RESTConnector) ApplyNDCRestSchemas(config *configuration.Configuration, schemas []configuration.NDCRestRuntimeSchema, logger *slog.Logger) error { + ndcSchema, metadata, errs := configuration.MergeNDCRestSchemas(config, schemas) + if len(errs) > 0 { + printSchemaValidationError(logger, errs) + if ndcSchema == nil || config.Strict { + return errBuildSchemaFailed } - c.metadata = append(c.metadata, meta) } schemaBytes, err := json.Marshal(ndcSchema.ToSchemaResponse()) if err != nil { - errors["schema"] = []string{err.Error()} - } - - if len(errors) > 0 { - return errors + return err } c.schema = &rest.NDCRestSchema{ ScalarTypes: ndcSchema.ScalarTypes, ObjectTypes: ndcSchema.ObjectTypes, } + c.metadata = metadata c.rawSchema = schema.NewRawSchemaResponseUnsafe(schemaBytes) + return nil } -func validateRequestSchema(req *rest.Request, defaultMethod string) (*rest.Request, error) { - if req.Method == "" { - if defaultMethod == "" { - return nil, errHTTPMethodRequired - } - req.Method = defaultMethod - } - - if req.Type == "" { - req.Type = rest.RequestTypeREST - } - - return req, nil +func printSchemaValidationError(logger *slog.Logger, errors map[string][]string) { + logger.Error("errors happen when validating NDC REST schemas", slog.Any("errors", errors)) } -func buildRESTArguments(restSchema *rest.NDCRestSchema, conf *configuration.ConfigItem) { - if restSchema.Settings == nil || len(restSchema.Settings.Servers) < 2 { - return +func (c *RESTConnector) parseRESTOptionsFromArguments(argumentsInfo map[string]rest.ArgumentInfo, rawArgs map[string]any) (*internal.RESTOptions, error) { + var result internal.RESTOptions + argInfo, ok := argumentsInfo[rest.RESTOptionsArgumentName] + if !ok { + return &result, nil } - - var serverIDs []string - for i, server := range restSchema.Settings.Servers { - if server.ID != "" { - serverIDs = append(serverIDs, server.ID) - } else { - server.ID = strconv.Itoa(i) - restSchema.Settings.Servers[i] = server - serverIDs = append(serverIDs, server.ID) + rawRestOptions, ok := rawArgs[rest.RESTOptionsArgumentName] + if ok { + if err := result.FromValue(rawRestOptions); err != nil { + return nil, err } } + restOptionsNamedType := schema.GetUnderlyingNamedType(argInfo.Type) + result.Distributed = restOptionsNamedType != nil && restOptionsNamedType.Name == rest.RESTDistributedOptionsObjectName - serverScalar := schema.NewScalarType() - serverScalar.Representation = schema.NewTypeRepresentationEnum(serverIDs).Encode() - - restSchema.ScalarTypes[internal.RESTServerIDScalarName] = *serverScalar - restSchema.ObjectTypes[internal.RESTSingleOptionsObjectName] = internal.SingleObjectType - - restSingleOptionsArgument := rest.ArgumentInfo{ - ArgumentInfo: schema.ArgumentInfo{ - Description: internal.SingleObjectType.Description, - Type: schema.NewNullableNamedType(internal.RESTSingleOptionsObjectName).Encode(), - }, - } - - for _, fn := range restSchema.Functions { - fn.Arguments[internal.RESTOptionsArgumentName] = restSingleOptionsArgument - } - - for _, proc := range restSchema.Procedures { - proc.Arguments[internal.RESTOptionsArgumentName] = restSingleOptionsArgument - } + return &result, nil +} - if !conf.Distributed { - return +func (c *RESTConnector) evalForwardedHeaders(req *internal.RetryableRequest, rawArgs map[string]any) error { + if !c.config.ForwardHeaders.Enabled || c.config.ForwardHeaders.ArgumentField == nil { + return nil } - - restSchema.ObjectTypes[internal.RESTDistributedOptionsObjectName] = internal.DistributedObjectType - restSchema.ObjectTypes[internal.DistributedErrorObjectName] = rest.ObjectType{ - Description: utils.ToPtr("The error response of the remote request"), - Fields: map[string]rest.ObjectField{ - "server": { - ObjectField: schema.ObjectField{ - Description: utils.ToPtr("Identity of the remote server"), - Type: schema.NewNamedType(internal.RESTServerIDScalarName).Encode(), - }, - }, - "message": { - ObjectField: schema.ObjectField{ - Description: utils.ToPtr("An optional human-readable summary of the error"), - Type: schema.NewNullableType(schema.NewNamedType(string(rest.ScalarString))).Encode(), - }, - }, - "details": { - ObjectField: schema.ObjectField{ - Description: utils.ToPtr("Any additional structured information about the error"), - Type: schema.NewNullableType(schema.NewNamedType(string(rest.ScalarJSON))).Encode(), - }, - }, - }, + rawHeaders, ok := rawArgs[*c.config.ForwardHeaders.ArgumentField] + if !ok { + return nil } - functionKeys := utils.GetKeys(restSchema.Functions) - for _, key := range functionKeys { - fn := restSchema.Functions[key] - funcName := buildDistributedName(key) - distributedFn := rest.OperationInfo{ - Request: fn.Request, - Arguments: cloneDistributedArguments(fn.Arguments), - Description: fn.Description, - ResultType: schema.NewNamedType(buildDistributedResultObjectType(restSchema, funcName, fn.ResultType)).Encode(), - } - restSchema.Functions[funcName] = distributedFn + var headers map[string]string + if err := mapstructure.Decode(rawHeaders, &headers); err != nil { + return fmt.Errorf("arguments.%s: %w", *c.config.ForwardHeaders.ArgumentField, err) } - procedureKeys := utils.GetKeys(restSchema.Procedures) - for _, key := range procedureKeys { - proc := restSchema.Procedures[key] - procName := buildDistributedName(key) - - distributedProc := rest.OperationInfo{ - Request: proc.Request, - Arguments: cloneDistributedArguments(proc.Arguments), - Description: proc.Description, - ResultType: schema.NewNamedType(buildDistributedResultObjectType(restSchema, procName, proc.ResultType)).Encode(), + for key, value := range headers { + if req.Headers.Get(key) != "" { + continue } - restSchema.Procedures[procName] = distributedProc + req.Headers.Set(key, value) } -} -func cloneDistributedArguments(arguments map[string]rest.ArgumentInfo) map[string]rest.ArgumentInfo { - result := map[string]rest.ArgumentInfo{} - for k, v := range arguments { - if k != internal.RESTOptionsArgumentName { - result[k] = v - } - } - result[internal.RESTOptionsArgumentName] = rest.ArgumentInfo{ - ArgumentInfo: schema.ArgumentInfo{ - Description: internal.DistributedObjectType.Description, - Type: schema.NewNullableNamedType(internal.RESTDistributedOptionsObjectName).Encode(), - }, - } - return result + return nil } -func buildDistributedResultObjectType(restSchema *rest.NDCRestSchema, operationName string, underlyingType schema.Type) string { - distResultType := restUtils.StringSliceToPascalCase([]string{operationName, "Result"}) - distResultDataType := distResultType + "Data" - - restSchema.ObjectTypes[distResultDataType] = rest.ObjectType{ - Description: utils.ToPtr("Distributed response data of " + operationName), - Fields: map[string]rest.ObjectField{ - "server": { - ObjectField: schema.ObjectField{ - Description: utils.ToPtr("Identity of the remote server"), - Type: schema.NewNamedType(internal.RESTServerIDScalarName).Encode(), - }, - }, - "data": { - ObjectField: schema.ObjectField{ - Description: utils.ToPtr("A result of " + operationName), - Type: underlyingType, - }, - }, - }, +func (c *RESTConnector) createHeaderForwardingResponse(result any, rawHeaders http.Header) any { + if !c.config.ForwardHeaders.Enabled || c.config.ForwardHeaders.ResponseHeaders == nil { + return result } - restSchema.ObjectTypes[distResultType] = rest.ObjectType{ - Description: utils.ToPtr("Distributed responses of " + operationName), - Fields: map[string]rest.ObjectField{ - "results": { - ObjectField: schema.ObjectField{ - Description: utils.ToPtr("Results of " + operationName), - Type: schema.NewArrayType(schema.NewNamedType(distResultDataType)).Encode(), - }, - }, - "errors": { - ObjectField: schema.ObjectField{ - Description: utils.ToPtr("Error responses of " + operationName), - Type: schema.NewArrayType(schema.NewNamedType(internal.DistributedErrorObjectName)).Encode(), - }, - }, - }, + 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 distResultType -} - -func buildDistributedName(name string) string { - return name + "Distributed" -} - -func printSchemaValidationError(logger *slog.Logger, errors map[string][]string) { - logger.Error("errors happen when validating NDC REST schemas", slog.Any("errors", errors)) -} - -func parseRESTOptionsFromArguments(arguments map[string]rest.ArgumentInfo, rawRestOptions any) (*internal.RESTOptions, error) { - var result internal.RESTOptions - if err := result.FromValue(rawRestOptions); err != nil { - return nil, err + return map[string]any{ + c.config.ForwardHeaders.ResponseHeaders.HeadersField: headers, + c.config.ForwardHeaders.ResponseHeaders.ResultField: result, } - argInfo, ok := arguments[internal.RESTOptionsArgumentName] - if !ok { - return &result, nil - } - restOptionsNamedType := schema.GetUnderlyingNamedType(argInfo.Type) - result.Distributed = restOptionsNamedType != nil && restOptionsNamedType.Name == internal.RESTDistributedOptionsObjectName - return &result, nil } diff --git a/connector/testdata/auth/config.yaml b/connector/testdata/auth/config.yaml index e880933..3dcdc5f 100644 --- a/connector/testdata/auth/config.yaml +++ b/connector/testdata/auth/config.yaml @@ -1,3 +1,26 @@ +# yaml-language-server: $schema=../../../ndc-rest-schema/jsonschema/configuration.schema.json +strict: true +forwardHeaders: + enabled: true + argumentField: headers + responseHeaders: + headersField: "headers" + resultField: "response" + forwardHeaders: + - Content-Type + - X-Custom-Header +concurrency: + query: 1 + mutation: 1 + rest: 0 files: - file: schema.yaml spec: ndc + timeout: + value: 10 + retry: + times: + value: 1 + delay: + value: 500 + httpStatus: [429, 500, 501, 502] diff --git a/connector/testdata/auth/schema.yaml b/connector/testdata/auth/schema.yaml index cba438d..f8c6ba5 100644 --- a/connector/testdata/auth/schema.yaml +++ b/connector/testdata/auth/schema.yaml @@ -1,16 +1,19 @@ --- settings: servers: - - url: "{{PET_STORE_URL}}" + - url: + env: PET_STORE_URL securitySchemes: api_key: type: apiKey - value: "{{PET_STORE_API_KEY}}" + value: + env: PET_STORE_API_KEY in: header name: api_key bearer: type: http - value: "{{PET_STORE_BEARER_TOKEN}}" + value: + env: PET_STORE_BEARER_TOKEN scheme: bearer petstore_auth: type: oauth2 @@ -20,11 +23,6 @@ settings: scopes: read:pets: read your pets write:pets: modify pets in your account - timeout: 10 - retry: - times: 1 - delay: 500 - httpStatus: [429, 500, 501, 502] security: - api_key: [] version: 1.0.18 @@ -92,19 +90,20 @@ procedures: url: "/pet" method: post headers: - Content-Type: application/json + Content-Type: + value: application/json security: - api_key: [] requestBody: contentType: application/json - schema: - type": Pet arguments: body: description: Request body of /pet type: name: Pet type: named + rest: + in: body description: Add a new pet to the store name: addPet result_type: @@ -116,8 +115,6 @@ procedures: method: post requestBody: contentType: application/json - schema: - type: CreateModelRequest response: contentType: application/x-ndjson arguments: diff --git a/connector/testdata/multi-schemas/cat.yaml b/connector/testdata/multi-schemas/cat.yaml index 138b51b..79403fb 100644 --- a/connector/testdata/multi-schemas/cat.yaml +++ b/connector/testdata/multi-schemas/cat.yaml @@ -1,8 +1,10 @@ settings: servers: - - url: "{{CAT_STORE_URL}}" + - url: + env: CAT_STORE_URL headers: - pet: cat + pet: + value: cat collections: [] functions: findCats: diff --git a/connector/testdata/multi-schemas/dog.yaml b/connector/testdata/multi-schemas/dog.yaml index ca28b14..69f9cb7 100644 --- a/connector/testdata/multi-schemas/dog.yaml +++ b/connector/testdata/multi-schemas/dog.yaml @@ -1,8 +1,10 @@ settings: servers: - - url: "{{DOG_STORE_URL}}" + - url: + env: DOG_STORE_URL headers: - pet: dog + pet: + value: dog collections: [] functions: findDogs: diff --git a/connector/testdata/patch/config.yaml b/connector/testdata/patch/config.yaml index d92e0b8..4508d56 100644 --- a/connector/testdata/patch/config.yaml +++ b/connector/testdata/patch/config.yaml @@ -2,6 +2,14 @@ files: - file: ../auth/schema.yaml spec: ndc distributed: true + timeout: + value: 30 + retry: + times: + value: 2 + delay: + value: 1000 + httpStatus: [429, 500] patchBefore: - path: patch-before.yaml strategy: merge diff --git a/connector/testdata/patch/patch-before.yaml b/connector/testdata/patch/patch-before.yaml index c3cc94b..4b049bf 100644 --- a/connector/testdata/patch/patch-before.yaml +++ b/connector/testdata/patch/patch-before.yaml @@ -1,23 +1,22 @@ settings: servers: - id: dog - url: "{{PET_STORE_DOG_URL}}" - timeout: 30 - retry: - times: 2 - delay: 1000 - httpStatus: [429, 500] + url: + env: PET_STORE_DOG_URL securitySchemes: api_key: type: apiKey - value: "dog-secret" + value: + value: dog-secret in: header name: api_key - id: cat - url: "{{PET_STORE_CAT_URL}}" + url: + env: PET_STORE_CAT_URL securitySchemes: api_key: type: apiKey - value: "cat-secret" + value: + value: cat-secret in: header name: api_key diff --git a/connector/testdata/server-empty/config.yaml b/connector/testdata/server-empty/config.yaml index e880933..10c46f1 100644 --- a/connector/testdata/server-empty/config.yaml +++ b/connector/testdata/server-empty/config.yaml @@ -1,3 +1,11 @@ files: - file: schema.yaml spec: ndc + timeout: + value: 10 + retry: + times: + value: 1 + delay: + value: 500 + httpStatus: [429, 500, 501, 502] diff --git a/connector/testdata/server-empty/schema.yaml b/connector/testdata/server-empty/schema.yaml index 3cba6be..76a559d 100644 --- a/connector/testdata/server-empty/schema.yaml +++ b/connector/testdata/server-empty/schema.yaml @@ -1,10 +1,5 @@ --- -settings: - timeout: 10 - retry: - times: 1 - delay: 500 - httpStatus: [429, 500, 501, 502] +settings: {} collections: [] functions: findPets: diff --git a/connector/types.go b/connector/types.go index 1b2523f..b0c0e1b 100644 --- a/connector/types.go +++ b/connector/types.go @@ -5,19 +5,17 @@ import ( "net/http" "github.com/hasura/ndc-rest/connector/internal" - rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" + "go.opentelemetry.io/otel/trace" ) var ( - errInvalidSchema = errors.New("failed to validate NDC REST schema") - errBuildSchemaFailed = errors.New("failed to build NDC REST schema") - errHTTPMethodRequired = errors.New("the HTTP method is required") - errFilePathRequired = errors.New("file path is empty") + errInvalidSchema = errors.New("failed to validate NDC REST schema") + errBuildSchemaFailed = errors.New("failed to build NDC REST schema") ) // State is the global state which is shared for every connector request. type State struct { - Schema *rest.NDCRestSchema + Tracer trace.Tracer } type options struct { @@ -39,9 +37,3 @@ func WithClient(client internal.Doer) Option { opts.client = client } } - -// NDCRestSchemaWithName wraps NDCRestSchema with identity name -type NDCRestSchemaWithName struct { - name string - schema *rest.NDCRestSchema -} diff --git a/go.mod b/go.mod index babbb64..31314b2 100644 --- a/go.mod +++ b/go.mod @@ -5,18 +5,18 @@ go 1.23.0 toolchain go1.23.1 require ( + github.com/go-viper/mapstructure/v2 v2.2.1 github.com/google/uuid v1.6.0 - github.com/hasura/ndc-rest/ndc-rest-schema v0.2.5 - github.com/hasura/ndc-sdk-go v1.5.2-0.20241020093415-b752942bd505 - github.com/lmittmann/tint v1.0.5 - go.opentelemetry.io/otel v1.31.0 - go.opentelemetry.io/otel/trace v1.31.0 - gopkg.in/yaml.v3 v3.0.1 + github.com/hasura/ndc-rest/ndc-rest-schema v0.0.0-00010101000000-000000000000 + github.com/hasura/ndc-sdk-go v1.6.2-0.20241109102535-399b739f7af5 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/trace v1.32.0 + golang.org/x/sync v0.9.0 gotest.tools/v3 v3.5.1 ) require ( - github.com/alecthomas/kong v1.2.1 // indirect + github.com/alecthomas/kong v1.4.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect @@ -26,39 +26,39 @@ require ( github.com/evanphx/json-patch v0.5.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect github.com/invopop/jsonschema v0.12.0 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pb33f/libopenapi v0.18.3 // indirect + github.com/pb33f/libopenapi v0.18.6 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.60.0 // indirect + github.com/prometheus/common v0.60.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.53.0 // indirect - go.opentelemetry.io/otel/metric v1.31.0 // indirect - go.opentelemetry.io/otel/sdk v1.31.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.54.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/sdk v1.32.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect - google.golang.org/grpc v1.67.1 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/grpc v1.68.0 // indirect google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/hasura/ndc-rest/ndc-rest-schema => ./ndc-rest-schema diff --git a/go.sum b/go.sum index 20081ac..2122b92 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= -github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= -github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.4.0 h1:UL7tzGMnnY0YRMMvJyITIRX1EpO6RbBRZDNcCevy3HA= +github.com/alecthomas/kong v1.4.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -45,6 +45,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -54,10 +56,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= -github.com/hasura/ndc-sdk-go v1.5.2-0.20241020093415-b752942bd505 h1:6ATW3s+x5aJ+hGzdlNr1f5nu9IV8SY/lakVuOCMvBjM= -github.com/hasura/ndc-sdk-go v1.5.2-0.20241020093415-b752942bd505/go.mod h1:oik0JrwuN5iZwZjZJzIRMw9uO2xDJbCXwhS1GgaRejk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= +github.com/hasura/ndc-sdk-go v1.6.2-0.20241109102535-399b739f7af5 h1:YA3ix2/SMZ+vR/96YXuSPNYHsocsWnY8xCmhJeT3RYs= +github.com/hasura/ndc-sdk-go v1.6.2-0.20241109102535-399b739f7af5/go.mod h1:H7iN3SFXSou2rjBKv9fLumbvDXMDGP0Eg+cXWHpkA3k= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -77,8 +79,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw= -github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -98,8 +98,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/pb33f/libopenapi v0.18.3 h1:j4lm8xMM/GYSj2M8S7qNwZ//rOtEK5ACEiuNC7mTJzE= -github.com/pb33f/libopenapi v0.18.3/go.mod h1:9ap4lXBHgxGyFwxtOfa+B1C3IQ0rvnqteqjJvJ11oiQ= +github.com/pb33f/libopenapi v0.18.6 h1:adxzZUnOBOAuKxFAIrtb1Qt8GA4XnDWUAxEnqiSoTh0= +github.com/pb33f/libopenapi v0.18.6/go.mod h1:qZRs2IHIcs9SjHPmQfSUCyeD3OY9JkLJQOuFxd0bYCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -108,8 +108,8 @@ github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+ github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= -github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= +github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -127,30 +127,30 @@ github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd h1:dLuIF2kX9c+KknGJUdJi1Il1SDiTSK158/BB9kdgAew= github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/contrib/propagators/b3 v1.31.0 h1:PQPXYscmwbCp76QDvO4hMngF2j8Bx/OTV86laEl8uqo= -go.opentelemetry.io/contrib/propagators/b3 v1.31.0/go.mod h1:jbqfV8wDdqSDrAYxVpXQnpM0XFMq2FtDesblJ7blOwQ= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= -go.opentelemetry.io/otel/exporters/prometheus v0.53.0 h1:QXobPHrwiGLM4ufrY3EOmDPJpo2P90UuFau4CDPJA/I= -go.opentelemetry.io/otel/exporters/prometheus v0.53.0/go.mod h1:WOAXGr3D00CfzmFxtTV1eR0GpoHuPEu+HJT8UWW2SIU= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/contrib/propagators/b3 v1.32.0 h1:MazJBz2Zf6HTN/nK/s3Ru1qme+VhWU5hm83QxEP+dvw= +go.opentelemetry.io/contrib/propagators/b3 v1.32.0/go.mod h1:B0s70QHYPrJwPOwD1o3V/R8vETNOG9N3qZf4LDYvA30= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -166,11 +166,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -185,16 +187,16 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -202,12 +204,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= -google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/ndc-rest-schema/README.md b/ndc-rest-schema/README.md index 0f41f3e..55ec110 100644 --- a/ndc-rest-schema/README.md +++ b/ndc-rest-schema/README.md @@ -92,7 +92,8 @@ The URL can be a relative path or an absolute URL. If the URL is relative, there ```yaml settings: servers: - - url: http://petstore.swagger.io/v1 + - url: + value: http://petstore.swagger.io/v1 ``` `parameters` include the list of URL and query parameters so the connector can replace values from request arguments. @@ -121,14 +122,17 @@ Environment variable template which is in `{{CONSTANT_CASE}}` or `{{CONSTANT_CAS ```yaml settings: servers: - - url: "{{PET_STORE_SERVER_URL:-https://petstore3.swagger.io/api/v3}}" + - url: + env: PET_STORE_SERVER_URL + value: https://petstore3.swagger.io/api/v3 timeout: 30 headers: foo: bar securitySchemes: api_key: type: apiKey - value: "{{PET_STORE_API_KEY}}" + value: + env: PET_STORE_API_KEY in: header name: api_key petstore_auth: @@ -257,7 +261,9 @@ There is an extra `value` field with the environment variable template to be rep "securitySchemes": { "api_key": { "type": "apiKey", - "value": "{{API_KEY}}", // the constant case of api_key + "value": { + "env": "API_KEY" // the constant case of api_key + }, "in": "header", "name": "api_key" } @@ -277,7 +283,9 @@ This is the general authentication for [Basic](https://swagger.io/docs/specifica "bearer_auth": { "type": "http", "scheme": "bearer", - "value": "{{BEARER_AUTH_TOKEN}}", // the constant case of bearer_auth + _TOKEN suffix + "value": { + "env": "BEARER_AUTH_TOKEN" // the constant case of bearer_auth + _TOKEN suffix + }, "header": "Authentication" } } diff --git a/ndc-rest-schema/command/convert.go b/ndc-rest-schema/command/convert.go index 846d1ea..a6e73f2 100644 --- a/ndc-rest-schema/command/convert.go +++ b/ndc-rest-schema/command/convert.go @@ -1,7 +1,6 @@ package command import ( - "encoding/json" "errors" "fmt" "log/slog" @@ -10,32 +9,13 @@ import ( "time" "github.com/hasura/ndc-rest/ndc-rest-schema/configuration" - "github.com/hasura/ndc-rest/ndc-rest-schema/openapi" "github.com/hasura/ndc-rest/ndc-rest-schema/schema" "github.com/hasura/ndc-rest/ndc-rest-schema/utils" "gopkg.in/yaml.v3" ) -// ConvertCommandArguments represent available command arguments for the convert command -type ConvertCommandArguments struct { - File string `help:"File path needs to be converted." short:"f"` - Config string `help:"Path of the config file." short:"c"` - Output string `help:"The location where the ndc schema file will be generated. Print to stdout if not set" short:"o"` - Spec string `help:"The API specification of the file, is one of oas3 (openapi3), oas2 (openapi2)"` - Format string `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"` - 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"` - EnvPrefix string `help:"The environment variable prefix for security values, e.g. PET_STORE"` - MethodAlias map[string]string `help:"Alias names for HTTP method. Used for prefix renaming, e.g. getUsers, postUser"` - AllowedContentTypes []string `help:"Allowed content types. All content types are allowed by default"` - PatchBefore []string `help:"Patch files to be applied into the input file before converting"` - PatchAfter []string `help:"Patch files to be applied into the input file after converting"` -} - // ConvertToNDCSchema converts to NDC REST schema from file -func CommandConvertToNDCSchema(args *ConvertCommandArguments, logger *slog.Logger) error { +func CommandConvertToNDCSchema(args *configuration.ConvertCommandArguments, logger *slog.Logger) error { start := time.Now() logger.Debug( "converting the document to NDC REST schema", @@ -80,8 +60,8 @@ func CommandConvertToNDCSchema(args *ConvertCommandArguments, logger *slog.Logge configDir = filepath.Dir(args.Config) } - ResolveConvertConfigArguments(&config, configDir, args) - result, err := ConvertToNDCSchema(&config, logger) + configuration.ResolveConvertConfigArguments(&config, configDir, args) + result, err := configuration.ConvertToNDCSchema(&config, logger) if err != nil { logger.Error(err.Error()) @@ -127,121 +107,3 @@ func CommandConvertToNDCSchema(args *ConvertCommandArguments, logger *slog.Logge fmt.Print(string(resultBytes)) return nil } - -// ConvertToNDCSchema converts to NDC REST schema from config -func ConvertToNDCSchema(config *configuration.ConvertConfig, logger *slog.Logger) (*schema.NDCRestSchema, error) { - rawContent, err := utils.ReadFileFromPath(config.File) - if err != nil { - return nil, err - } - - rawContent, err = utils.ApplyPatch(rawContent, config.PatchBefore) - if err != nil { - return nil, err - } - - var result *schema.NDCRestSchema - var errs []error - options := openapi.ConvertOptions{ - MethodAlias: config.MethodAlias, - Prefix: config.Prefix, - TrimPrefix: config.TrimPrefix, - EnvPrefix: config.EnvPrefix, - AllowedContentTypes: config.AllowedContentTypes, - Strict: config.Strict, - Logger: logger, - } - switch config.Spec { - case schema.OpenAPIv3Spec, schema.OAS3Spec: - result, errs = openapi.OpenAPIv3ToNDCSchema(rawContent, options) - case schema.OpenAPIv2Spec, (schema.OAS2Spec): - result, errs = openapi.OpenAPIv2ToNDCSchema(rawContent, options) - case schema.NDCSpec: - if err := json.Unmarshal(rawContent, &result); err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("invalid spec %s, expected %+v", config.Spec, []schema.SchemaSpecType{schema.OpenAPIv3Spec, schema.OpenAPIv2Spec}) - } - - if result == nil { - return nil, errors.Join(errs...) - } else if len(errs) > 0 { - logger.Error(errors.Join(errs...).Error()) - } - - return utils.ApplyPatchToRestSchema(result, config.PatchAfter) -} - -// ResolveConvertConfigArguments resolves convert config arguments -func ResolveConvertConfigArguments(config *configuration.ConvertConfig, configDir string, args *ConvertCommandArguments) { - if args != nil { - if args.Spec != "" { - config.Spec = schema.SchemaSpecType(args.Spec) - } - if len(args.MethodAlias) > 0 { - config.MethodAlias = args.MethodAlias - } - if args.Prefix != "" { - config.Prefix = args.Prefix - } - if args.TrimPrefix != "" { - config.TrimPrefix = args.TrimPrefix - } - if args.EnvPrefix != "" { - config.EnvPrefix = args.EnvPrefix - } - if args.Pure { - config.Pure = args.Pure - } - if args.Strict { - config.Strict = args.Strict - } - if len(args.AllowedContentTypes) > 0 { - config.AllowedContentTypes = args.AllowedContentTypes - } - } - if config.Spec == "" { - config.Spec = schema.OAS3Spec - } - - if args != nil && args.File != "" { - config.File = args.File - } else if config.File != "" { - config.File = utils.ResolveFilePath(configDir, config.File) - } - - if args != nil && args.Output != "" { - config.Output = args.Output - } else if config.Output != "" { - config.Output = utils.ResolveFilePath(configDir, config.Output) - } - - if args != nil && len(args.PatchBefore) > 0 { - config.PatchBefore = make([]utils.PatchConfig, len(args.PatchBefore)) - for i, p := range args.PatchBefore { - config.PatchBefore[i] = utils.PatchConfig{ - Path: p, - } - } - } else { - for i, p := range config.PatchBefore { - p.Path = utils.ResolveFilePath(configDir, p.Path) - config.PatchBefore[i] = p - } - } - - if args != nil && len(args.PatchAfter) > 0 { - config.PatchAfter = make([]utils.PatchConfig, len(args.PatchAfter)) - for i, p := range args.PatchAfter { - config.PatchAfter[i] = utils.PatchConfig{ - Path: p, - } - } - } else { - for i, p := range config.PatchAfter { - p.Path = utils.ResolveFilePath(configDir, p.Path) - config.PatchAfter[i] = p - } - } -} diff --git a/ndc-rest-schema/command/convert_test.go b/ndc-rest-schema/command/convert_test.go index 7677602..2ce66a2 100644 --- a/ndc-rest-schema/command/convert_test.go +++ b/ndc-rest-schema/command/convert_test.go @@ -8,6 +8,7 @@ import ( "os" "testing" + "github.com/hasura/ndc-rest/ndc-rest-schema/configuration" "github.com/hasura/ndc-rest/ndc-rest-schema/schema" "gotest.tools/v3/assert" ) @@ -92,7 +93,7 @@ func TestConvertToNDCSchema(t *testing.T) { tempDir := t.TempDir() outputFilePath = fmt.Sprintf("%s/output.json", tempDir) } - args := &ConvertCommandArguments{ + args := &configuration.ConvertCommandArguments{ File: tc.filePath, Output: outputFilePath, Pure: tc.pure, @@ -103,7 +104,7 @@ func TestConvertToNDCSchema(t *testing.T) { AllowedContentTypes: tc.allowedContentTypes, } if tc.config != "" { - args = &ConvertCommandArguments{ + args = &configuration.ConvertCommandArguments{ Config: tc.config, Output: outputFilePath, EnvPrefix: "", @@ -127,12 +128,12 @@ func TestConvertToNDCSchema(t *testing.T) { } outputBytes, err := os.ReadFile(outputFilePath) if err != nil { - t.Errorf("cannot read the output file at %s", outputFilePath) + t.Errorf("cannot read the output file at %s, %s", outputFilePath, err) t.FailNow() } var output schema.NDCRestSchema if err := json.Unmarshal(outputBytes, &output); err != nil { - t.Errorf("cannot decode the output file json at %s", outputFilePath) + t.Errorf("cannot decode the output file json at %s: %s", outputFilePath, err) t.FailNow() } if tc.expected == "" { @@ -141,12 +142,12 @@ func TestConvertToNDCSchema(t *testing.T) { expectedBytes, err := os.ReadFile(tc.expected) if err != nil { - t.Errorf("cannot read the expected file at %s", outputFilePath) + t.Errorf("cannot read the expected file at %s: %s", outputFilePath, err) t.FailNow() } var expectedSchema schema.NDCRestSchema if err := json.Unmarshal(expectedBytes, &expectedSchema); err != nil { - t.Errorf("cannot decode the output file json at %s", tc.expected) + t.Errorf("cannot decode the output file json at %s: %s", tc.expected, err) t.FailNow() } assert.DeepEqual(t, expectedSchema.Settings, output.Settings) diff --git a/ndc-rest-schema/command/testdata/auth/config.yaml b/ndc-rest-schema/command/testdata/auth/config.yaml new file mode 100644 index 0000000..1f2eabc --- /dev/null +++ b/ndc-rest-schema/command/testdata/auth/config.yaml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=../../../jsonschema/configuration.schema.json +output: schema.output.json +strict: true +forwardHeaders: + enabled: false + argumentField: headers + responseHeaders: null +concurrency: + query: 1 + mutation: 1 + rest: 10 +files: + - file: schema.yaml + spec: ndc + timeout: + value: 10 + retry: + times: + value: 1 + delay: + value: 500 + httpStatus: [429, 500, 501, 502] diff --git a/ndc-rest-schema/command/testdata/auth/expected.json b/ndc-rest-schema/command/testdata/auth/expected.json new file mode 100644 index 0000000..d08dd27 --- /dev/null +++ b/ndc-rest-schema/command/testdata/auth/expected.json @@ -0,0 +1,318 @@ +[ + { + "name": "testdata/auth/schema.yaml", + "settings": { + "servers": [ + { + "url": { + "env": "PET_STORE_URL" + }, + "securitySchemes": { + "api_key": { + "type": "apiKey", + "value": { + "env": "PET_STORE_API_KEY" + }, + "in": "header", + "name": "api_key" + }, + "bearer": { + "type": "http", + "value": { + "env": "PET_STORE_BEARER_TOKEN" + }, + "header": "", + "scheme": "bearer" + }, + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", + "scopes": { + "read:pets": "read your pets", + "write:pets": "modify pets in your account" + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + ], + "securitySchemes": { + "api_key": { + "type": "apiKey", + "value": { + "env": "PET_STORE_API_KEY" + }, + "in": "header", + "name": "api_key" + }, + "bearer": { + "type": "http", + "value": { + "env": "PET_STORE_BEARER_TOKEN" + }, + "header": "", + "scheme": "bearer" + }, + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", + "scopes": { + "read:pets": "read your pets", + "write:pets": "modify pets in your account" + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ], + "version": "1.0.18" + }, + "functions": { + "findPets": { + "request": { + "url": "/pet", + "method": "get", + "type": "rest", + "response": { + "contentType": "" + } + }, + "arguments": {}, + "description": "Finds Pets", + "result_type": { + "element_type": { + "name": "Pet", + "type": "named" + }, + "type": "array" + } + }, + "findPetsByStatus": { + "request": { + "url": "/pet/findByStatus", + "method": "get", + "type": "rest", + "security": [ + { + "bearer": [] + } + ], + "response": { + "contentType": "" + } + }, + "arguments": { + "status": { + "description": "Status values that need to be considered for filter", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "rest": { + "in": "query", + "schema": { + "type": ["string"] + } + } + } + }, + "description": "Finds Pets by status", + "result_type": { + "element_type": { + "name": "Pet", + "type": "named" + }, + "type": "array" + } + }, + "petRetry": { + "request": { + "url": "/pet/retry", + "method": "get", + "type": "rest", + "response": { + "contentType": "" + } + }, + "arguments": {}, + "result_type": { + "element_type": { + "name": "Pet", + "type": "named" + }, + "type": "array" + } + } + }, + "object_types": { + "CreateModelRequest": { + "fields": { + "model": { + "description": "The name of the model to create", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, + "Pet": { + "fields": { + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int", + "type": "named" + } + } + }, + "name": { + "type": { + "name": "String", + "type": "named" + } + } + } + }, + "ProgressResponse": { + "fields": { + "completed": { + "description": "The completed size of the task", + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "status": { + "description": "The status of the request", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + } + }, + "procedures": { + "addPet": { + "request": { + "url": "/pet", + "method": "post", + "type": "rest", + "headers": { + "Content-Type": { + "value": "application/json" + } + }, + "security": [ + { + "api_key": [] + } + ], + "requestBody": { + "contentType": "application/json" + }, + "response": { + "contentType": "" + } + }, + "arguments": { + "body": { + "description": "Request body of /pet", + "type": { + "name": "Pet", + "type": "named" + }, + "rest": { + "in": "body" + } + } + }, + "description": "Add a new pet to the store", + "result_type": { + "name": "Pet", + "type": "named" + } + }, + "createModel": { + "request": { + "url": "/model", + "method": "post", + "type": "rest", + "requestBody": { + "contentType": "application/json" + }, + "response": { + "contentType": "application/x-ndjson" + } + }, + "arguments": { + "body": { + "description": "Request body of POST /api/create", + "type": { + "name": "CreateModelRequest", + "type": "named" + } + } + }, + "result_type": { + "element_type": { + "name": "ProgressResponse", + "type": "named" + }, + "type": "array" + } + } + }, + "scalar_types": { + "Boolean": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "boolean" + } + }, + "Int": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "int32" + } + }, + "String": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "string" + } + } + } + } +] diff --git a/ndc-rest-schema/command/testdata/auth/schema.yaml b/ndc-rest-schema/command/testdata/auth/schema.yaml new file mode 100644 index 0000000..d9fe12c --- /dev/null +++ b/ndc-rest-schema/command/testdata/auth/schema.yaml @@ -0,0 +1,185 @@ +--- +settings: + servers: + - url: + env: PET_STORE_URL + securitySchemes: + api_key: + type: apiKey + value: + env: PET_STORE_API_KEY + in: header + name: api_key + bearer: + type: http + value: + env: PET_STORE_BEARER_TOKEN + scheme: bearer + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + read:pets: read your pets + write:pets: modify pets in your account + security: + - api_key: [] + version: 1.0.18 +collections: [] +functions: + findPets: + request: + url: "/pet" + method: get + parameters: [] + security: [] + arguments: {} + description: Finds Pets + name: findPets + result_type: + element_type: + name: Pet + type: named + type: array + findPetsByStatus: + request: + url: "/pet/findByStatus" + method: get + security: + - bearer: [] + arguments: + status: + description: Status values that need to be considered for filter + type: + type: nullable + underlying_type: + name: String + type: named + rest: + in: query + schema: + type: [string] + enum: + - available + - pending + - sold + description: Finds Pets by status + name: findPetsByStatus + result_type: + element_type: + name: Pet + type: named + type: array + petRetry: + request: + url: "/pet/retry" + method: get + parameters: [] + security: [] + arguments: {} + name: petRetry + result_type: + element_type: + name: Pet + type: named + type: array +procedures: + addPet: + request: + url: "/pet" + method: post + headers: + Content-Type: + value: application/json + security: + - api_key: [] + requestBody: + contentType: application/json + arguments: + body: + description: Request body of /pet + type: + name: Pet + type: named + rest: + in: body + description: Add a new pet to the store + name: addPet + result_type: + name: Pet + type: named + createModel: + request: + url: /model + method: post + requestBody: + contentType: application/json + response: + contentType: application/x-ndjson + arguments: + body: + description: Request body of POST /api/create + type: + name: CreateModelRequest + type: named + name: createModel + result_type: + element_type: + name: ProgressResponse + type: named + type: array +object_types: + Pet: + fields: + id: + type: + type: nullable + underlying_type: + name: Int + type: named + name: + type: + name: String + type: named + CreateModelRequest: + fields: + model: + description: The name of the model to create + type: + type: nullable + underlying_type: + name: String + type: named + ProgressResponse: + fields: + completed: + description: The completed size of the task + type: + type: nullable + underlying_type: + name: Int32 + type: named + status: + description: The status of the request + type: + type: nullable + underlying_type: + name: String + type: named +scalar_types: + Boolean: + aggregate_functions: {} + comparison_operators: {} + representation: + type: boolean + Int: + aggregate_functions: {} + comparison_operators: {} + representation: + type: int32 + String: + aggregate_functions: {} + comparison_operators: {} + representation: + type: string diff --git a/ndc-rest-schema/command/testdata/patch/config.yaml b/ndc-rest-schema/command/testdata/patch/config.yaml new file mode 100644 index 0000000..e050e22 --- /dev/null +++ b/ndc-rest-schema/command/testdata/patch/config.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=../../../jsonschema/configuration.schema.json +output: schema.output.json +strict: true +forwardHeaders: + enabled: true + argumentField: headers + responseHeaders: + headersField: headers + resultField: response + forwardHeaders: [] +concurrency: + query: 1 + mutation: 1 + rest: 0 +files: + - file: ../auth/schema.yaml + spec: ndc + distributed: true + timeout: + value: 30 + retry: + times: + value: 2 + delay: + value: 1000 + httpStatus: [429, 500] + patchBefore: + - path: patch-before.yaml + strategy: merge + patchAfter: + - path: patch-after.yaml + strategy: json6902 diff --git a/ndc-rest-schema/command/testdata/patch/expected.json b/ndc-rest-schema/command/testdata/patch/expected.json new file mode 100644 index 0000000..cdfd62a --- /dev/null +++ b/ndc-rest-schema/command/testdata/patch/expected.json @@ -0,0 +1,1178 @@ +[ + { + "name": "testdata/auth/schema.yaml", + "settings": { + "servers": [ + { + "url": { + "env": "PET_STORE_DOG_URL" + }, + "id": "dog", + "securitySchemes": { + "api_key": { + "type": "apiKey", + "value": { + "value": "dog-secret" + }, + "in": "header", + "name": "api_key" + }, + "bearer": { + "type": "http", + "value": { + "env": "PET_STORE_BEARER_TOKEN" + }, + "header": "", + "scheme": "bearer" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + { + "url": { + "env": "PET_STORE_CAT_URL" + }, + "id": "cat", + "securitySchemes": { + "api_key": { + "type": "apiKey", + "value": { + "value": "cat-secret" + }, + "in": "header", + "name": "api_key" + }, + "bearer": { + "type": "http", + "value": { + "env": "PET_STORE_BEARER_TOKEN" + }, + "header": "", + "scheme": "bearer" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + ], + "securitySchemes": { + "api_key": { + "type": "apiKey", + "value": { + "env": "PET_STORE_API_KEY" + }, + "in": "header", + "name": "api_key" + }, + "bearer": { + "type": "http", + "value": { + "env": "PET_STORE_BEARER_TOKEN" + }, + "header": "", + "scheme": "bearer" + } + }, + "security": [ + { + "api_key": [] + } + ], + "version": "1.0.18" + }, + "functions": { + "findPets": { + "request": { + "url": "/pet", + "method": "get", + "type": "rest", + "response": { + "contentType": "" + } + }, + "arguments": { + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "restOptions": { + "description": "Execution options for REST requests to a single server", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RestSingleOptions", + "type": "named" + } + } + } + }, + "description": "Finds Pets", + "result_type": { + "name": "FindPetsHeadersResponse", + "type": "named" + } + }, + "findPetsByStatus": { + "request": { + "url": "/pet/findByStatus", + "method": "get", + "type": "rest", + "security": [ + { + "bearer": [] + } + ], + "response": { + "contentType": "" + } + }, + "arguments": { + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "restOptions": { + "description": "Execution options for REST requests to a single server", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RestSingleOptions", + "type": "named" + } + } + }, + "status": { + "description": "Status values that need to be considered for filter", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "rest": { + "in": "query", + "schema": { + "type": ["string"] + } + } + } + }, + "description": "Finds Pets by status", + "result_type": { + "name": "FindPetsByStatusHeadersResponse", + "type": "named" + } + }, + "findPetsByStatusDistributed": { + "request": { + "url": "/pet/findByStatus", + "method": "get", + "type": "rest", + "security": [ + { + "bearer": [] + } + ], + "response": { + "contentType": "" + } + }, + "arguments": { + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "restOptions": { + "description": "Distributed execution options for REST requests to multiple servers", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RestDistributedOptions", + "type": "named" + } + } + }, + "status": { + "description": "Status values that need to be considered for filter", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + }, + "rest": { + "in": "query", + "schema": { + "type": ["string"] + } + } + } + }, + "description": "Finds Pets by status", + "result_type": { + "name": "FindPetsByStatusDistributedHeadersResponse", + "type": "named" + } + }, + "findPetsDistributed": { + "request": { + "url": "/pet", + "method": "get", + "type": "rest", + "response": { + "contentType": "" + } + }, + "arguments": { + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "restOptions": { + "description": "Distributed execution options for REST requests to multiple servers", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RestDistributedOptions", + "type": "named" + } + } + } + }, + "description": "Finds Pets", + "result_type": { + "name": "FindPetsDistributedHeadersResponse", + "type": "named" + } + }, + "petRetry": { + "request": { + "url": "/pet/retry", + "method": "get", + "type": "rest", + "response": { + "contentType": "" + } + }, + "arguments": { + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "restOptions": { + "description": "Execution options for REST requests to a single server", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RestSingleOptions", + "type": "named" + } + } + } + }, + "result_type": { + "name": "PetRetryHeadersResponse", + "type": "named" + } + }, + "petRetryDistributed": { + "request": { + "url": "/pet/retry", + "method": "get", + "type": "rest", + "response": { + "contentType": "" + } + }, + "arguments": { + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "restOptions": { + "description": "Distributed execution options for REST requests to multiple servers", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RestDistributedOptions", + "type": "named" + } + } + } + }, + "result_type": { + "name": "PetRetryDistributedHeadersResponse", + "type": "named" + } + } + }, + "object_types": { + "AddPetDistributedHeadersResponse": { + "fields": { + "headers": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "response": { + "type": { + "name": "AddPetDistributedResult", + "type": "named" + } + } + } + }, + "AddPetDistributedResult": { + "description": "Distributed responses of addPetDistributed", + "fields": { + "errors": { + "description": "Error responses of addPetDistributed", + "type": { + "element_type": { + "name": "DistributedError", + "type": "named" + }, + "type": "array" + } + }, + "results": { + "description": "Results of addPetDistributed", + "type": { + "element_type": { + "name": "AddPetDistributedResultData", + "type": "named" + }, + "type": "array" + } + } + } + }, + "AddPetDistributedResultData": { + "description": "Distributed response data of addPetDistributed", + "fields": { + "data": { + "description": "A result of addPetDistributed", + "type": { + "name": "Pet", + "type": "named" + } + }, + "server": { + "description": "Identity of the remote server", + "type": { + "name": "RestServerId", + "type": "named" + } + } + } + }, + "AddPetHeadersResponse": { + "fields": { + "headers": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "response": { + "type": { + "name": "Pet", + "type": "named" + } + } + } + }, + "CreateModelDistributedHeadersResponse": { + "fields": { + "headers": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "response": { + "type": { + "name": "CreateModelDistributedResult", + "type": "named" + } + } + } + }, + "CreateModelDistributedResult": { + "description": "Distributed responses of createModelDistributed", + "fields": { + "errors": { + "description": "Error responses of createModelDistributed", + "type": { + "element_type": { + "name": "DistributedError", + "type": "named" + }, + "type": "array" + } + }, + "results": { + "description": "Results of createModelDistributed", + "type": { + "element_type": { + "name": "CreateModelDistributedResultData", + "type": "named" + }, + "type": "array" + } + } + } + }, + "CreateModelDistributedResultData": { + "description": "Distributed response data of createModelDistributed", + "fields": { + "data": { + "description": "A result of createModelDistributed", + "type": { + "element_type": { + "name": "ProgressResponse", + "type": "named" + }, + "type": "array" + } + }, + "server": { + "description": "Identity of the remote server", + "type": { + "name": "RestServerId", + "type": "named" + } + } + } + }, + "CreateModelHeadersResponse": { + "fields": { + "headers": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "response": { + "type": { + "element_type": { + "name": "ProgressResponse", + "type": "named" + }, + "type": "array" + } + } + } + }, + "CreateModelRequest": { + "fields": { + "model": { + "description": "The name of the model to create", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, + "DistributedError": { + "description": "The error response of the remote request", + "fields": { + "details": { + "description": "Any additional structured information about the error", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "message": { + "description": "An optional human-readable summary of the error", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + }, + "server": { + "description": "Identity of the remote server", + "type": { + "name": "RestServerId", + "type": "named" + } + } + } + }, + "FindPetsByStatusDistributedHeadersResponse": { + "fields": { + "headers": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "response": { + "type": { + "name": "FindPetsByStatusDistributedResult", + "type": "named" + } + } + } + }, + "FindPetsByStatusDistributedResult": { + "description": "Distributed responses of findPetsByStatusDistributed", + "fields": { + "errors": { + "description": "Error responses of findPetsByStatusDistributed", + "type": { + "element_type": { + "name": "DistributedError", + "type": "named" + }, + "type": "array" + } + }, + "results": { + "description": "Results of findPetsByStatusDistributed", + "type": { + "element_type": { + "name": "FindPetsByStatusDistributedResultData", + "type": "named" + }, + "type": "array" + } + } + } + }, + "FindPetsByStatusDistributedResultData": { + "description": "Distributed response data of findPetsByStatusDistributed", + "fields": { + "data": { + "description": "A result of findPetsByStatusDistributed", + "type": { + "element_type": { + "name": "Pet", + "type": "named" + }, + "type": "array" + } + }, + "server": { + "description": "Identity of the remote server", + "type": { + "name": "RestServerId", + "type": "named" + } + } + } + }, + "FindPetsByStatusHeadersResponse": { + "fields": { + "headers": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "response": { + "type": { + "element_type": { + "name": "Pet", + "type": "named" + }, + "type": "array" + } + } + } + }, + "FindPetsDistributedHeadersResponse": { + "fields": { + "headers": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "response": { + "type": { + "name": "FindPetsDistributedResult", + "type": "named" + } + } + } + }, + "FindPetsDistributedResult": { + "description": "Distributed responses of findPetsDistributed", + "fields": { + "errors": { + "description": "Error responses of findPetsDistributed", + "type": { + "element_type": { + "name": "DistributedError", + "type": "named" + }, + "type": "array" + } + }, + "results": { + "description": "Results of findPetsDistributed", + "type": { + "element_type": { + "name": "FindPetsDistributedResultData", + "type": "named" + }, + "type": "array" + } + } + } + }, + "FindPetsDistributedResultData": { + "description": "Distributed response data of findPetsDistributed", + "fields": { + "data": { + "description": "A result of findPetsDistributed", + "type": { + "element_type": { + "name": "Pet", + "type": "named" + }, + "type": "array" + } + }, + "server": { + "description": "Identity of the remote server", + "type": { + "name": "RestServerId", + "type": "named" + } + } + } + }, + "FindPetsHeadersResponse": { + "fields": { + "headers": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "response": { + "type": { + "element_type": { + "name": "Pet", + "type": "named" + }, + "type": "array" + } + } + } + }, + "Pet": { + "fields": { + "id": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int", + "type": "named" + } + } + }, + "name": { + "type": { + "name": "String", + "type": "named" + } + } + } + }, + "PetRetryDistributedHeadersResponse": { + "fields": { + "headers": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "response": { + "type": { + "name": "PetRetryDistributedResult", + "type": "named" + } + } + } + }, + "PetRetryDistributedResult": { + "description": "Distributed responses of petRetryDistributed", + "fields": { + "errors": { + "description": "Error responses of petRetryDistributed", + "type": { + "element_type": { + "name": "DistributedError", + "type": "named" + }, + "type": "array" + } + }, + "results": { + "description": "Results of petRetryDistributed", + "type": { + "element_type": { + "name": "PetRetryDistributedResultData", + "type": "named" + }, + "type": "array" + } + } + } + }, + "PetRetryDistributedResultData": { + "description": "Distributed response data of petRetryDistributed", + "fields": { + "data": { + "description": "A result of petRetryDistributed", + "type": { + "element_type": { + "name": "Pet", + "type": "named" + }, + "type": "array" + } + }, + "server": { + "description": "Identity of the remote server", + "type": { + "name": "RestServerId", + "type": "named" + } + } + } + }, + "PetRetryHeadersResponse": { + "fields": { + "headers": { + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "response": { + "type": { + "element_type": { + "name": "Pet", + "type": "named" + }, + "type": "array" + } + } + } + }, + "ProgressResponse": { + "fields": { + "completed": { + "description": "The completed size of the task", + "type": { + "type": "nullable", + "underlying_type": { + "name": "Int32", + "type": "named" + } + } + }, + "status": { + "description": "The status of the request", + "type": { + "type": "nullable", + "underlying_type": { + "name": "String", + "type": "named" + } + } + } + } + }, + "RestDistributedOptions": { + "description": "Distributed execution options for REST 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": "RestServerId", + "type": "named" + }, + "type": "array" + } + } + } + } + }, + "RestSingleOptions": { + "description": "Execution options for REST 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": "RestServerId", + "type": "named" + }, + "type": "array" + } + } + } + } + } + }, + "procedures": { + "addPet": { + "request": { + "url": "/pet", + "method": "post", + "type": "rest", + "headers": { + "Content-Type": { + "value": "application/json" + } + }, + "security": [ + { + "api_key": [] + } + ], + "requestBody": { + "contentType": "application/json" + }, + "response": { + "contentType": "" + } + }, + "arguments": { + "body": { + "description": "Request body of /pet", + "type": { + "name": "Pet", + "type": "named" + }, + "rest": { + "in": "body" + } + }, + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "restOptions": { + "description": "Execution options for REST requests to a single server", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RestSingleOptions", + "type": "named" + } + } + } + }, + "description": "Add a new pet to the store", + "result_type": { + "name": "AddPetHeadersResponse", + "type": "named" + } + }, + "addPetDistributed": { + "request": { + "url": "/pet", + "method": "post", + "type": "rest", + "headers": { + "Content-Type": { + "value": "application/json" + } + }, + "security": [ + { + "api_key": [] + } + ], + "requestBody": { + "contentType": "application/json" + }, + "response": { + "contentType": "" + } + }, + "arguments": { + "body": { + "description": "Request body of /pet", + "type": { + "name": "Pet", + "type": "named" + }, + "rest": { + "in": "body" + } + }, + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "restOptions": { + "description": "Distributed execution options for REST requests to multiple servers", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RestDistributedOptions", + "type": "named" + } + } + } + }, + "description": "Add a new pet to the store", + "result_type": { + "name": "AddPetDistributedHeadersResponse", + "type": "named" + } + }, + "createModel": { + "request": { + "url": "/model", + "method": "post", + "type": "rest", + "requestBody": { + "contentType": "application/json" + }, + "response": { + "contentType": "application/x-ndjson" + } + }, + "arguments": { + "body": { + "description": "Request body of POST /api/create", + "type": { + "name": "CreateModelRequest", + "type": "named" + } + }, + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "restOptions": { + "description": "Execution options for REST requests to a single server", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RestSingleOptions", + "type": "named" + } + } + } + }, + "result_type": { + "name": "CreateModelHeadersResponse", + "type": "named" + } + }, + "createModelDistributed": { + "request": { + "url": "/model", + "method": "post", + "type": "rest", + "requestBody": { + "contentType": "application/json" + }, + "response": { + "contentType": "application/x-ndjson" + } + }, + "arguments": { + "body": { + "description": "Request body of POST /api/create", + "type": { + "name": "CreateModelRequest", + "type": "named" + } + }, + "headers": { + "description": "Headers forwarded from the Hasura engine", + "type": { + "type": "nullable", + "underlying_type": { + "name": "JSON", + "type": "named" + } + } + }, + "restOptions": { + "description": "Distributed execution options for REST requests to multiple servers", + "type": { + "type": "nullable", + "underlying_type": { + "name": "RestDistributedOptions", + "type": "named" + } + } + } + }, + "result_type": { + "name": "CreateModelDistributedHeadersResponse", + "type": "named" + } + } + }, + "scalar_types": { + "Boolean": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "boolean" + } + }, + "Int": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "int32" + } + }, + "JSON": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "json" + } + }, + "RestServerId": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "one_of": ["dog", "cat"], + "type": "enum" + } + }, + "String": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "string" + } + } + } + } +] diff --git a/ndc-rest-schema/command/testdata/patch/patch-after.yaml b/ndc-rest-schema/command/testdata/patch/patch-after.yaml new file mode 100644 index 0000000..fcdabba --- /dev/null +++ b/ndc-rest-schema/command/testdata/patch/patch-after.yaml @@ -0,0 +1,2 @@ +- op: remove + path: /settings/securitySchemes/petstore_auth diff --git a/ndc-rest-schema/command/testdata/patch/patch-before.yaml b/ndc-rest-schema/command/testdata/patch/patch-before.yaml new file mode 100644 index 0000000..4b049bf --- /dev/null +++ b/ndc-rest-schema/command/testdata/patch/patch-before.yaml @@ -0,0 +1,22 @@ +settings: + servers: + - id: dog + url: + env: PET_STORE_DOG_URL + securitySchemes: + api_key: + type: apiKey + value: + value: dog-secret + in: header + name: api_key + - id: cat + url: + env: PET_STORE_CAT_URL + securitySchemes: + api_key: + type: apiKey + value: + value: cat-secret + in: header + name: api_key diff --git a/ndc-rest-schema/command/update.go b/ndc-rest-schema/command/update.go new file mode 100644 index 0000000..a079df2 --- /dev/null +++ b/ndc-rest-schema/command/update.go @@ -0,0 +1,25 @@ +package command + +import ( + "log/slog" + "time" + + "github.com/hasura/ndc-rest/ndc-rest-schema/configuration" +) + +// UpdateCommandArguments represent input arguments of the `update` command +type UpdateCommandArguments struct { + Dir string `help:"The directory where the config.yaml file is present" short:"d" env:"HASURA_PLUGIN_CONNECTOR_CONTEXT_PATH" default:"."` +} + +// UpdateConfiguration updates the configuration for the REST connector +func UpdateConfiguration(args *UpdateCommandArguments, logger *slog.Logger) error { + start := time.Now() + if err := configuration.UpdateRESTConfiguration(args.Dir, logger); err != nil { + return err + } + + logger.Info("updated successfully", slog.Duration("exec_time", time.Since(start))) + + return nil +} diff --git a/ndc-rest-schema/command/update_test.go b/ndc-rest-schema/command/update_test.go new file mode 100644 index 0000000..66141d4 --- /dev/null +++ b/ndc-rest-schema/command/update_test.go @@ -0,0 +1,96 @@ +package command + +import ( + "encoding/json" + "log/slog" + "os" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hasura/ndc-rest/ndc-rest-schema/configuration" + "github.com/hasura/ndc-rest/ndc-rest-schema/schema" + "gotest.tools/v3/assert" +) + +func TestUpdateCommand(t *testing.T) { + testCases := []struct { + Argument UpdateCommandArguments + Expected string + }{ + // go run ./ndc-rest-schema update -d ./ndc-rest-schema/command/testdata/patch + { + Argument: UpdateCommandArguments{ + Dir: "testdata/patch", + }, + Expected: "testdata/patch/expected.json", + }, + // go run ./ndc-rest-schema update -d ./ndc-rest-schema/command/testdata/auth + { + Argument: UpdateCommandArguments{ + Dir: "testdata/auth", + }, + Expected: "testdata/auth/expected.json", + }, + } + + for _, tc := range testCases { + t.Run(tc.Argument.Dir, func(t *testing.T) { + assert.NilError(t, UpdateConfiguration(&tc.Argument, slog.Default())) + + output := readRuntimeSchemaFile(t, tc.Argument.Dir+"/schema.output.json") + expected := readRuntimeSchemaFile(t, tc.Argument.Dir+"/expected.json") + assertSchemaEqual(t, expected, output) + }) + } +} + +func readRuntimeSchemaFile(t *testing.T, filePath string) []configuration.NDCRestRuntimeSchema { + t.Helper() + rawBytes, err := os.ReadFile(filePath) + assert.NilError(t, err) + + var result []configuration.NDCRestRuntimeSchema + assert.NilError(t, json.Unmarshal(rawBytes, &result)) + + return result +} + +func assertSchemaEqual(t *testing.T, expectedSchemas []configuration.NDCRestRuntimeSchema, outputSchemas []configuration.NDCRestRuntimeSchema) { + t.Helper() + + assert.Equal(t, len(expectedSchemas), len(outputSchemas)) + for i, expected := range expectedSchemas { + output := outputSchemas[i] + assetDeepEqual(t, expected.Settings.Headers, output.Settings.Headers) + assetDeepEqual(t, expected.Settings.Security, output.Settings.Security) + assetDeepEqual(t, expected.Settings.SecuritySchemes, output.Settings.SecuritySchemes) + assetDeepEqual(t, expected.Settings.Version, output.Settings.Version) + for i, server := range expected.Settings.Servers { + sv := output.Settings.Servers[i] + assetDeepEqual(t, server.Headers, sv.Headers) + assetDeepEqual(t, server.ID, sv.ID) + assetDeepEqual(t, server.Security, sv.Security) + assetDeepEqual(t, server.SecuritySchemes, sv.SecuritySchemes) + assetDeepEqual(t, server.URL, sv.URL) + assetDeepEqual(t, server.TLS, sv.TLS) + } + assetDeepEqual(t, expected.ScalarTypes, output.ScalarTypes) + objectBs, _ := json.Marshal(output.ObjectTypes) + var objectTypes map[string]schema.ObjectType + assert.NilError(t, json.Unmarshal(objectBs, &objectTypes)) + assetDeepEqual(t, expected.ObjectTypes, objectTypes) + assetDeepEqual(t, expected.Procedures, output.Procedures) + assetDeepEqual(t, expected.Functions, output.Functions) + } +} + +func assetDeepEqual(t *testing.T, expected any, reality any) { + t.Helper() + assert.DeepEqual(t, + expected, reality, + cmpopts.IgnoreUnexported(schema.ServerConfig{}, schema.NDCRestSettings{}), + cmp.Exporter(func(t reflect.Type) bool { return true }), + ) +} diff --git a/ndc-rest-schema/configuration/convert.go b/ndc-rest-schema/configuration/convert.go new file mode 100644 index 0000000..2cef373 --- /dev/null +++ b/ndc-rest-schema/configuration/convert.go @@ -0,0 +1,130 @@ +package configuration + +import ( + "encoding/json" + "errors" + "fmt" + "log/slog" + + "github.com/hasura/ndc-rest/ndc-rest-schema/openapi" + "github.com/hasura/ndc-rest/ndc-rest-schema/schema" + "github.com/hasura/ndc-rest/ndc-rest-schema/utils" +) + +// ConvertToNDCSchema converts to NDC REST schema from config +func ConvertToNDCSchema(config *ConvertConfig, logger *slog.Logger) (*schema.NDCRestSchema, error) { + rawContent, err := utils.ReadFileFromPath(config.File) + if err != nil { + return nil, err + } + + rawContent, err = utils.ApplyPatch(rawContent, config.PatchBefore) + if err != nil { + return nil, err + } + + var result *schema.NDCRestSchema + var errs []error + options := openapi.ConvertOptions{ + MethodAlias: config.MethodAlias, + Prefix: config.Prefix, + TrimPrefix: config.TrimPrefix, + EnvPrefix: config.EnvPrefix, + AllowedContentTypes: config.AllowedContentTypes, + Strict: config.Strict, + Logger: logger, + } + switch config.Spec { + case schema.OpenAPIv3Spec, schema.OAS3Spec: + result, errs = openapi.OpenAPIv3ToNDCSchema(rawContent, options) + case schema.OpenAPIv2Spec, (schema.OAS2Spec): + result, errs = openapi.OpenAPIv2ToNDCSchema(rawContent, options) + case schema.NDCSpec: + if err := json.Unmarshal(rawContent, &result); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("invalid spec %s, expected %+v", config.Spec, []schema.SchemaSpecType{schema.OpenAPIv3Spec, schema.OpenAPIv2Spec}) + } + + if result == nil { + return nil, errors.Join(errs...) + } else if len(errs) > 0 { + logger.Error(errors.Join(errs...).Error()) + } + + return utils.ApplyPatchToRestSchema(result, config.PatchAfter) +} + +// ResolveConvertConfigArguments resolves convert config arguments +func ResolveConvertConfigArguments(config *ConvertConfig, configDir string, args *ConvertCommandArguments) { + if args != nil { + if args.Spec != "" { + config.Spec = schema.SchemaSpecType(args.Spec) + } + if len(args.MethodAlias) > 0 { + config.MethodAlias = args.MethodAlias + } + if args.Prefix != "" { + config.Prefix = args.Prefix + } + if args.TrimPrefix != "" { + config.TrimPrefix = args.TrimPrefix + } + if args.EnvPrefix != "" { + config.EnvPrefix = args.EnvPrefix + } + if args.Pure { + config.Pure = args.Pure + } + if args.Strict { + config.Strict = args.Strict + } + if len(args.AllowedContentTypes) > 0 { + config.AllowedContentTypes = args.AllowedContentTypes + } + } + if config.Spec == "" { + config.Spec = schema.OAS3Spec + } + + if args != nil && args.File != "" { + config.File = args.File + } else if config.File != "" { + config.File = utils.ResolveFilePath(configDir, config.File) + } + + if args != nil && args.Output != "" { + config.Output = args.Output + } else if config.Output != "" { + config.Output = utils.ResolveFilePath(configDir, config.Output) + } + + if args != nil && len(args.PatchBefore) > 0 { + config.PatchBefore = make([]utils.PatchConfig, len(args.PatchBefore)) + for i, p := range args.PatchBefore { + config.PatchBefore[i] = utils.PatchConfig{ + Path: p, + } + } + } else { + for i, p := range config.PatchBefore { + p.Path = utils.ResolveFilePath(configDir, p.Path) + config.PatchBefore[i] = p + } + } + + if args != nil && len(args.PatchAfter) > 0 { + config.PatchAfter = make([]utils.PatchConfig, len(args.PatchAfter)) + for i, p := range args.PatchAfter { + config.PatchAfter[i] = utils.PatchConfig{ + Path: p, + } + } + } else { + for i, p := range config.PatchAfter { + p.Path = utils.ResolveFilePath(configDir, p.Path) + config.PatchAfter[i] = p + } + } +} diff --git a/ndc-rest-schema/configuration/schema.go b/ndc-rest-schema/configuration/schema.go new file mode 100644 index 0000000..22df4c6 --- /dev/null +++ b/ndc-rest-schema/configuration/schema.go @@ -0,0 +1,439 @@ +package configuration + +import ( + "encoding/json" + "fmt" + "log/slog" + "os" + "path/filepath" + "reflect" + "strconv" + + rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" + restUtils "github.com/hasura/ndc-rest/ndc-rest-schema/utils" + "github.com/hasura/ndc-sdk-go/schema" + "github.com/hasura/ndc-sdk-go/utils" +) + +// BuildSchemaFromConfig build NDC REST schema from the configuration +func BuildSchemaFromConfig(config *Configuration, configDir string, logger *slog.Logger) ([]NDCRestRuntimeSchema, map[string][]string) { + schemas := make([]NDCRestRuntimeSchema, len(config.Files)) + errors := make(map[string][]string) + for i, file := range config.Files { + schemaOutput, err := buildSchemaFile(config, configDir, &file, logger) + if err != nil { + errors[file.File] = []string{err.Error()} + } + + if schemaOutput == nil { + continue + } + ndcSchema := NDCRestRuntimeSchema{ + Name: file.File, + NDCRestSchema: schemaOutput, + } + + runtime, err := file.GetRuntimeSettings() + if err != nil { + errors[file.File] = []string{err.Error()} + } else { + ndcSchema.Runtime = *runtime + } + + schemas[i] = ndcSchema + } + + return schemas, errors +} + +// ReadSchemaOutputFile reads the schema output file in disk +func ReadSchemaOutputFile(configDir string, filePath string, logger *slog.Logger) ([]NDCRestRuntimeSchema, error) { + if filePath == "" { + return nil, nil + } + + outputFilePath := filepath.Join(configDir, filePath) + rawBytes, err := os.ReadFile(outputFilePath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + + return nil, fmt.Errorf("failed to read the file at %s: %w", outputFilePath, err) + } + + var result []NDCRestRuntimeSchema + if err := json.Unmarshal(rawBytes, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal the schema file at %s: %w", outputFilePath, err) + } + + return result, nil +} + +// MergeNDCRestSchemas merge REST schemas into a single schema object +func MergeNDCRestSchemas(config *Configuration, schemas []NDCRestRuntimeSchema) (*rest.NDCRestSchema, []NDCRestRuntimeSchema, map[string][]string) { + ndcSchema := &rest.NDCRestSchema{ + ScalarTypes: make(schema.SchemaResponseScalarTypes), + ObjectTypes: make(map[string]rest.ObjectType), + Functions: make(map[string]rest.OperationInfo), + Procedures: make(map[string]rest.OperationInfo), + } + + appliedSchemas := make([]NDCRestRuntimeSchema, len(schemas)) + errors := make(map[string][]string) + + for i, item := range schemas { + if item.NDCRestSchema == nil { + errors[item.Name] = []string{fmt.Sprintf("schema of the item %d (%s) is empty", i, item.Name)} + return nil, nil, errors + } + settings := item.Settings + if settings == nil { + settings = &rest.NDCRestSettings{} + } else { + for i, server := range settings.Servers { + if server.Security.IsEmpty() { + server.Security = settings.Security + } + if server.SecuritySchemes == nil { + server.SecuritySchemes = make(map[string]rest.SecurityScheme) + } + for key, scheme := range settings.SecuritySchemes { + _, ok := server.SecuritySchemes[key] + if !ok { + server.SecuritySchemes[key] = scheme + } + } + + if server.Headers == nil { + server.Headers = make(map[string]utils.EnvString) + } + for key, value := range settings.Headers { + _, ok := server.Headers[key] + if !ok { + server.Headers[key] = value + } + } + settings.Servers[i] = server + } + } + + meta := NDCRestRuntimeSchema{ + Name: item.Name, + Runtime: item.Runtime, + NDCRestSchema: &rest.NDCRestSchema{ + Settings: settings, + Functions: map[string]rest.OperationInfo{}, + Procedures: map[string]rest.OperationInfo{}, + ObjectTypes: item.ObjectTypes, + ScalarTypes: item.ScalarTypes, + }, + } + var errs []string + + for name, scalar := range item.ScalarTypes { + if originScalar, ok := ndcSchema.ScalarTypes[name]; !ok { + ndcSchema.ScalarTypes[name] = scalar + } else if !rest.IsDefaultScalar(name) && !reflect.DeepEqual(originScalar, scalar) { + slog.Warn(fmt.Sprintf("Scalar type %s is conflicted", name)) + } + } + for name, object := range item.ObjectTypes { + if _, ok := ndcSchema.ObjectTypes[name]; !ok { + ndcSchema.ObjectTypes[name] = object + } else { + slog.Warn(fmt.Sprintf("Object type %s is conflicted", name)) + } + } + + for fnName, fnItem := range item.Functions { + if fnItem.Request == nil || fnItem.Request.URL == "" { + continue + } + req, err := validateRequestSchema(fnItem.Request, "get") + if err != nil { + errs = append(errs, fmt.Sprintf("function %s: %s", fnName, err)) + continue + } + + fn := rest.OperationInfo{ + Request: req, + Arguments: fnItem.Arguments, + Description: fnItem.Description, + ResultType: fnItem.ResultType, + } + meta.Functions[fnName] = fn + ndcSchema.Functions[fnName] = fn + } + + for procName, procItem := range item.Procedures { + if procItem.Request == nil || procItem.Request.URL == "" { + continue + } + req, err := validateRequestSchema(procItem.Request, "") + if err != nil { + errs = append(errs, fmt.Sprintf("procedure %s: %s", procName, err)) + continue + } + + proc := rest.OperationInfo{ + Request: req, + Arguments: procItem.Arguments, + Description: procItem.Description, + ResultType: procItem.ResultType, + } + meta.Procedures[procName] = proc + ndcSchema.Procedures[procName] = proc + } + + if len(errs) > 0 { + errors[item.Name] = errs + continue + } + appliedSchemas[i] = meta + } + + return ndcSchema, appliedSchemas, errors +} + +func buildSchemaFile(config *Configuration, configDir string, configItem *ConfigItem, logger *slog.Logger) (*rest.NDCRestSchema, error) { + if configItem.ConvertConfig.File == "" { + return nil, errFilePathRequired + } + ResolveConvertConfigArguments(&configItem.ConvertConfig, configDir, nil) + ndcSchema, err := ConvertToNDCSchema(&configItem.ConvertConfig, logger) + if err != nil { + return nil, err + } + + if ndcSchema.Settings == nil || len(ndcSchema.Settings.Servers) == 0 { + return nil, fmt.Errorf("the servers setting of schema %s is empty", configItem.ConvertConfig.File) + } + + buildRESTArguments(config, ndcSchema, configItem) + buildHeadersForwardingResponse(config, ndcSchema) + + return ndcSchema, nil +} + +func buildRESTArguments(config *Configuration, restSchema *rest.NDCRestSchema, conf *ConfigItem) { + if restSchema.Settings == nil || len(restSchema.Settings.Servers) < 2 { + return + } + + var serverIDs []string + for i, server := range restSchema.Settings.Servers { + if server.ID != "" { + serverIDs = append(serverIDs, server.ID) + } else { + server.ID = strconv.Itoa(i) + restSchema.Settings.Servers[i] = server + serverIDs = append(serverIDs, server.ID) + } + } + + serverScalar := schema.NewScalarType() + serverScalar.Representation = schema.NewTypeRepresentationEnum(serverIDs).Encode() + + restSchema.ScalarTypes[rest.RESTServerIDScalarName] = *serverScalar + restSchema.ObjectTypes[rest.RESTSingleOptionsObjectName] = singleObjectType + + for _, fn := range restSchema.Functions { + applyOperationInfo(config, &fn) + } + + for _, proc := range restSchema.Procedures { + applyOperationInfo(config, &proc) + } + + if !conf.IsDistributed() { + return + } + + restSchema.ObjectTypes[rest.RESTDistributedOptionsObjectName] = distributedObjectType + restSchema.ObjectTypes[rest.DistributedErrorObjectName] = rest.ObjectType{ + Description: utils.ToPtr("The error response of the remote request"), + Fields: map[string]rest.ObjectField{ + "server": { + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("Identity of the remote server"), + Type: schema.NewNamedType(rest.RESTServerIDScalarName).Encode(), + }, + }, + "message": { + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("An optional human-readable summary of the error"), + Type: schema.NewNullableType(schema.NewNamedType(string(rest.ScalarString))).Encode(), + }, + }, + "details": { + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("Any additional structured information about the error"), + Type: schema.NewNullableType(schema.NewNamedType(string(rest.ScalarJSON))).Encode(), + }, + }, + }, + } + + functionKeys := utils.GetKeys(restSchema.Functions) + for _, key := range functionKeys { + fn := restSchema.Functions[key] + funcName := buildDistributedName(key) + distributedFn := rest.OperationInfo{ + Request: fn.Request, + Arguments: cloneDistributedArguments(fn.Arguments), + Description: fn.Description, + ResultType: schema.NewNamedType(buildDistributedResultObjectType(restSchema, funcName, fn.ResultType)).Encode(), + } + restSchema.Functions[funcName] = distributedFn + } + + procedureKeys := utils.GetKeys(restSchema.Procedures) + for _, key := range procedureKeys { + proc := restSchema.Procedures[key] + procName := buildDistributedName(key) + + distributedProc := rest.OperationInfo{ + Request: proc.Request, + Arguments: cloneDistributedArguments(proc.Arguments), + Description: proc.Description, + ResultType: schema.NewNamedType(buildDistributedResultObjectType(restSchema, procName, proc.ResultType)).Encode(), + } + restSchema.Procedures[procName] = distributedProc + } +} + +func buildHeadersForwardingResponse(config *Configuration, restSchema *rest.NDCRestSchema) { + if !config.ForwardHeaders.Enabled { + return + } + + if _, ok := restSchema.ScalarTypes[string(rest.ScalarJSON)]; !ok { + restSchema.ScalarTypes[string(rest.ScalarJSON)] = schema.ScalarType{ + AggregateFunctions: schema.ScalarTypeAggregateFunctions{}, + ComparisonOperators: map[string]schema.ComparisonOperatorDefinition{}, + Representation: schema.NewTypeRepresentationJSON().Encode(), + } + } + + if config.ForwardHeaders.ResponseHeaders == nil { + return + } + + for name, op := range restSchema.Functions { + op.ResultType = createHeaderForwardingResponseTypes(restSchema, name, op.ResultType, config.ForwardHeaders.ResponseHeaders) + restSchema.Functions[name] = op + } + for name, op := range restSchema.Procedures { + op.ResultType = createHeaderForwardingResponseTypes(restSchema, name, op.ResultType, config.ForwardHeaders.ResponseHeaders) + restSchema.Procedures[name] = op + } +} + +func applyOperationInfo(config *Configuration, info *rest.OperationInfo) { + info.Arguments[rest.RESTOptionsArgumentName] = restSingleOptionsArgument + if config.ForwardHeaders.Enabled && config.ForwardHeaders.ArgumentField != nil { + info.Arguments[*config.ForwardHeaders.ArgumentField] = headersArguments + } +} + +func cloneDistributedArguments(arguments map[string]rest.ArgumentInfo) map[string]rest.ArgumentInfo { + result := map[string]rest.ArgumentInfo{} + for k, v := range arguments { + if k != rest.RESTOptionsArgumentName { + result[k] = v + } + } + result[rest.RESTOptionsArgumentName] = rest.ArgumentInfo{ + ArgumentInfo: schema.ArgumentInfo{ + Description: distributedObjectType.Description, + Type: schema.NewNullableNamedType(rest.RESTDistributedOptionsObjectName).Encode(), + }, + } + + return result +} + +func buildDistributedResultObjectType(restSchema *rest.NDCRestSchema, operationName string, underlyingType schema.Type) string { + distResultType := restUtils.StringSliceToPascalCase([]string{operationName, "Result"}) + distResultDataType := distResultType + "Data" + + restSchema.ObjectTypes[distResultDataType] = rest.ObjectType{ + Description: utils.ToPtr("Distributed response data of " + operationName), + Fields: map[string]rest.ObjectField{ + "server": { + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("Identity of the remote server"), + Type: schema.NewNamedType(rest.RESTServerIDScalarName).Encode(), + }, + }, + "data": { + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("A result of " + operationName), + Type: underlyingType, + }, + }, + }, + } + + restSchema.ObjectTypes[distResultType] = rest.ObjectType{ + Description: utils.ToPtr("Distributed responses of " + operationName), + Fields: map[string]rest.ObjectField{ + "results": { + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("Results of " + operationName), + Type: schema.NewArrayType(schema.NewNamedType(distResultDataType)).Encode(), + }, + }, + "errors": { + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("Error responses of " + operationName), + Type: schema.NewArrayType(schema.NewNamedType(rest.DistributedErrorObjectName)).Encode(), + }, + }, + }, + } + + return distResultType +} + +func buildDistributedName(name string) string { + return name + "Distributed" +} + +func validateRequestSchema(req *rest.Request, defaultMethod string) (*rest.Request, error) { + if req.Method == "" { + if defaultMethod == "" { + return nil, errHTTPMethodRequired + } + req.Method = defaultMethod + } + + if req.Type == "" { + req.Type = rest.RequestTypeREST + } + + return req, nil +} + +func createHeaderForwardingResponseTypes(restSchema *rest.NDCRestSchema, operationName string, resultType schema.Type, settings *ForwardResponseHeadersSettings) schema.Type { + objectName := restUtils.ToPascalCase(operationName) + "HeadersResponse" + objectType := rest.ObjectType{ + Fields: map[string]rest.ObjectField{ + settings.HeadersField: { + ObjectField: schema.ObjectField{ + Type: schema.NewNullableNamedType(string(rest.ScalarJSON)).Encode(), + }, + }, + settings.ResultField: { + ObjectField: schema.ObjectField{ + Type: resultType, + }, + }, + }, + } + + restSchema.ObjectTypes[objectName] = objectType + + return schema.NewNamedType(objectName).Encode() +} diff --git a/ndc-rest-schema/configuration/types.go b/ndc-rest-schema/configuration/types.go index 5d2d26e..72b18d4 100644 --- a/ndc-rest-schema/configuration/types.go +++ b/ndc-rest-schema/configuration/types.go @@ -1,22 +1,195 @@ package configuration import ( - "github.com/hasura/ndc-rest/ndc-rest-schema/schema" - "github.com/hasura/ndc-rest/ndc-rest-schema/utils" + "encoding/json" + "errors" + "fmt" + "regexp" + + rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" + restUtils "github.com/hasura/ndc-rest/ndc-rest-schema/utils" + "github.com/hasura/ndc-sdk-go/schema" + "github.com/hasura/ndc-sdk-go/utils" +) + +var ( + errFilePathRequired = errors.New("file path is empty") + errHTTPMethodRequired = errors.New("the HTTP method is required") ) +var fieldNameRegex = regexp.MustCompile(`^[a-zA-Z_]\w+$`) + +// Configuration contains required settings for the connector. +type Configuration struct { + Output string `json:"output,omitempty" yaml:"output,omitempty"` + // Require strict validation + Strict bool `json:"strict" yaml:"strict"` + ForwardHeaders ForwardHeadersSettings `json:"forwardHeaders" yaml:"forwardHeaders"` + Concurrency ConcurrencySettings `json:"concurrency" yaml:"concurrency"` + Files []ConfigItem `json:"files" yaml:"files"` +} + +// ConcurrencySettings represent settings for concurrent webhook executions to remote servers. +type ConcurrencySettings struct { + // Maximum number of concurrent executions if there are many query variables. + Query uint `json:"query" yaml:"query"` + // Maximum number of concurrent executions if there are many mutation operations. + Mutation uint `json:"mutation" yaml:"mutation"` + // Maximum number of concurrent requests to remote servers (distribution mode). + REST uint `json:"rest" yaml:"rest"` +} + +// ForwardHeadersSettings hold settings of header forwarding from and to Hasura engine +type ForwardHeadersSettings struct { + // Enable headers forwarding. + Enabled bool `json:"enabled" yaml:"enabled"` + // The argument field name to be added for headers forwarding. + ArgumentField *string `json:"argumentField" yaml:"argumentField" jsonschema:"oneof_type=string;null,pattern=^[a-zA-Z_]\\w+$"` + // HTTP response headers to be forwarded from a data connector to the client. + ResponseHeaders *ForwardResponseHeadersSettings `json:"responseHeaders" yaml:"responseHeaders" jsonschema:"nullable"` +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *ForwardHeadersSettings) UnmarshalJSON(b []byte) error { + type Plain ForwardHeadersSettings + var rawResult Plain + if err := json.Unmarshal(b, &rawResult); err != nil { + return err + } + + if !rawResult.Enabled { + *j = ForwardHeadersSettings(rawResult) + + return nil + } + + if rawResult.ArgumentField != nil && !fieldNameRegex.MatchString(*rawResult.ArgumentField) { + return fmt.Errorf("invalid forwardHeaders.argumentField name format: %s", *rawResult.ArgumentField) + } + + if rawResult.ResponseHeaders != nil { + if err := rawResult.ResponseHeaders.Validate(); err != nil { + return fmt.Errorf("responseHeaders: %w", err) + } + } + + *j = ForwardHeadersSettings(rawResult) + return nil +} + +// ForwardHeadersSettings hold settings of header forwarding from http response to Hasura engine. +type ForwardResponseHeadersSettings struct { + // Name of the field in the NDC function/procedure's result which contains the response headers. + HeadersField string `json:"headersField" yaml:"headersField" jsonschema:"pattern=^[a-zA-Z_]\\w+$"` + // Name of the field in the NDC function/procedure's result which contains the result. + ResultField string `json:"resultField" yaml:"resultField" jsonschema:"pattern=^[a-zA-Z_]\\w+$"` + // List of actual HTTP response headers from the data connector to be set as response headers. Returns all headers if empty. + ForwardHeaders []string `json:"forwardHeaders" yaml:"forwardHeaders"` +} + +// Validate checks if the setting is valid. +func (j ForwardResponseHeadersSettings) Validate() error { + if !fieldNameRegex.MatchString(j.HeadersField) { + return fmt.Errorf("invalid format in headersField: %s", j.HeadersField) + } + + if !fieldNameRegex.MatchString(j.ResultField) { + return fmt.Errorf("invalid format in resultField: %s", j.ResultField) + } + + return nil +} + +// RetryPolicySetting represents retry policy settings +type RetryPolicySetting struct { + // Number of retry times + Times utils.EnvInt `json:"times,omitempty" mapstructure:"times" yaml:"times,omitempty"` + // Delay retry delay in milliseconds + Delay utils.EnvInt `json:"delay,omitempty" mapstructure:"delay" yaml:"delay,omitempty"` + // HTTPStatus retries if the remote service returns one of these http status + HTTPStatus []int `json:"httpStatus,omitempty" mapstructure:"httpStatus" yaml:"httpStatus,omitempty"` +} + +// Validate if the current instance is valid +func (rs RetryPolicySetting) Validate() (*rest.RetryPolicy, error) { + var errs []error + times, err := rs.Times.Get() + if err != nil { + errs = append(errs, err) + } else if times < 0 { + errs = append(errs, errors.New("retry policy times must be positive")) + } + + delay, err := rs.Delay.Get() + if err != nil { + errs = append(errs, err) + } else if delay < 0 { + errs = append(errs, errors.New("retry delay must be larger than 0")) + } + + for _, status := range rs.HTTPStatus { + if status < 400 || status >= 600 { + errs = append(errs, errors.New("retry http status must be in between 400 and 599")) + break + } + } + + result := &rest.RetryPolicy{ + Times: uint(times), + Delay: uint(delay), + HTTPStatus: rs.HTTPStatus, + } + + if len(errs) > 0 { + return result, errors.Join(errs...) + } + return result, nil +} + // ConfigItem extends the ConvertConfig with advanced options type ConfigItem struct { ConvertConfig `yaml:",inline"` // Distributed enables distributed schema - Distributed bool `json:"distributed" yaml:"distributed"` + Distributed *bool `json:"distributed,omitempty" yaml:"distributed,omitempty"` + // configure the request timeout in seconds. + Timeout *utils.EnvInt `json:"timeout,omitempty" mapstructure:"timeout" yaml:"timeout,omitempty"` + Retry *RetryPolicySetting `json:"retry,omitempty" mapstructure:"retry" yaml:"retry,omitempty"` } -// Configuration contains required settings for the connector. -type Configuration struct { - Output string `json:"output,omitempty" yaml:"output,omitempty"` - Files []ConfigItem `json:"files" yaml:"files"` +// IsDistributed checks if the distributed option is enabled +func (ci ConfigItem) IsDistributed() bool { + return ci.Distributed != nil && *ci.Distributed +} + +// GetRuntimeSettings validate and get runtime settings +func (ci ConfigItem) GetRuntimeSettings() (*rest.RuntimeSettings, error) { + result := &rest.RuntimeSettings{} + var errs []error + if ci.Timeout != nil { + timeout, err := ci.Timeout.Get() + if err != nil { + errs = append(errs, fmt.Errorf("timeout: %w", err)) + } else if timeout < 0 { + errs = append(errs, fmt.Errorf("timeout must be positive, got: %d", timeout)) + } else { + result.Timeout = uint(timeout) + } + } + + if ci.Retry != nil { + retryPolicy, err := ci.Retry.Validate() + if err != nil { + errs = append(errs, fmt.Errorf("ConfigItem.retry: %w", err)) + } + result.Retry = *retryPolicy + } + + if len(errs) > 0 { + return result, errors.Join(errs...) + } + + return result, nil } // ConvertConfig represents the content of convert config file @@ -24,7 +197,7 @@ type ConvertConfig struct { // File path needs to be converted File string `json:"file" jsonschema:"required" yaml:"file"` // The API specification of the file, is one of oas3 (openapi3), oas2 (openapi2) - Spec schema.SchemaSpecType `json:"spec,omitempty" jsonschema:"default=oas3" yaml:"spec"` + Spec rest.SchemaSpecType `json:"spec,omitempty" jsonschema:"default=oas3" yaml:"spec"` // Alias names for HTTP method. Used for prefix renaming, e.g. getUsers, postUser MethodAlias map[string]string `json:"methodAlias,omitempty" yaml:"methodAlias"` // Add a prefix to the function and procedure names @@ -38,11 +211,82 @@ type ConvertConfig struct { // Require strict validation Strict bool `json:"strict,omitempty" yaml:"strict"` // Patch files to be applied into the input file before converting - PatchBefore []utils.PatchConfig `json:"patchBefore,omitempty" yaml:"patchBefore"` + PatchBefore []restUtils.PatchConfig `json:"patchBefore,omitempty" yaml:"patchBefore"` // Patch files to be applied into the input file after converting - PatchAfter []utils.PatchConfig `json:"patchAfter,omitempty" yaml:"patchAfter"` + PatchAfter []restUtils.PatchConfig `json:"patchAfter,omitempty" yaml:"patchAfter"` // Allowed content types. All content types are allowed by default AllowedContentTypes []string `json:"allowedContentTypes,omitempty" yaml:"allowedContentTypes"` // The location where the ndc schema file will be generated. Print to stdout if not set - Output string `json:"output,omitempty" yaml:"output"` + Output string `json:"output,omitempty" yaml:"output,omitempty"` +} + +// NDCRestRuntimeSchema wraps NDCRestSchema with runtime settings +type NDCRestRuntimeSchema struct { + Name string `json:"name" yaml:"name"` + Runtime rest.RuntimeSettings `json:"-" yaml:"-"` + *rest.NDCRestSchema +} + +// ConvertCommandArguments represent available command arguments for the convert command +type ConvertCommandArguments struct { + File string `help:"File path needs to be converted." short:"f"` + Config string `help:"Path of the config file." short:"c"` + Output string `help:"The location where the ndc schema file will be generated. Print to stdout if not set" short:"o"` + Spec string `help:"The API specification of the file, is one of oas3 (openapi3), oas2 (openapi2)"` + Format string `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"` + 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"` + EnvPrefix string `help:"The environment variable prefix for security values, e.g. PET_STORE"` + MethodAlias map[string]string `help:"Alias names for HTTP method. Used for prefix renaming, e.g. getUsers, postUser"` + AllowedContentTypes []string `help:"Allowed content types. All content types are allowed by default"` + PatchBefore []string `help:"Patch files to be applied into the input file before converting"` + PatchAfter []string `help:"Patch files to be applied into the input file after converting"` +} + +// the object type of REST execution options for single server +var singleObjectType = rest.ObjectType{ + Description: utils.ToPtr("Execution options for REST requests to a single server"), + Fields: map[string]rest.ObjectField{ + "servers": { + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("Specify remote servers to receive the request. If there are many server IDs the server is selected randomly"), + Type: schema.NewNullableType(schema.NewArrayType(schema.NewNamedType(rest.RESTServerIDScalarName))).Encode(), + }, + }, + }, +} + +// the object type of REST execution options for distributed servers +var distributedObjectType rest.ObjectType = rest.ObjectType{ + Description: utils.ToPtr("Distributed execution options for REST requests to multiple servers"), + Fields: map[string]rest.ObjectField{ + "servers": { + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("Specify remote servers to receive the request"), + Type: schema.NewNullableType(schema.NewArrayType(schema.NewNamedType(rest.RESTServerIDScalarName))).Encode(), + }, + }, + "parallel": { + ObjectField: schema.ObjectField{ + Description: utils.ToPtr("Execute requests to remote servers in parallel"), + Type: schema.NewNullableNamedType(string(rest.ScalarBoolean)).Encode(), + }, + }, + }, +} + +var headersArguments = rest.ArgumentInfo{ + ArgumentInfo: schema.ArgumentInfo{ + Description: utils.ToPtr("Headers forwarded from the Hasura engine"), + Type: schema.NewNullableNamedType(string(rest.ScalarJSON)).Encode(), + }, +} + +var restSingleOptionsArgument = rest.ArgumentInfo{ + ArgumentInfo: schema.ArgumentInfo{ + Description: singleObjectType.Description, + Type: schema.NewNullableNamedType(rest.RESTSingleOptionsObjectName).Encode(), + }, } diff --git a/ndc-rest-schema/configuration/update.go b/ndc-rest-schema/configuration/update.go new file mode 100644 index 0000000..d6d0e8f --- /dev/null +++ b/ndc-rest-schema/configuration/update.go @@ -0,0 +1,87 @@ +package configuration + +import ( + "encoding/json" + "errors" + "fmt" + "log/slog" + "os" + "path/filepath" + + "github.com/hasura/ndc-rest/ndc-rest-schema/utils" + "gopkg.in/yaml.v3" +) + +// UpdateRESTConfiguration validates and updates the REST configuration +func UpdateRESTConfiguration(configurationDir string, logger *slog.Logger) error { + config, err := ReadConfigurationFile(configurationDir) + if err != nil { + return err + } + + schemas, errs := BuildSchemaFromConfig(config, configurationDir, logger) + if len(errs) > 0 { + printSchemaValidationError(logger, errs) + if config.Strict { + return errors.New("failed to build schema files") + } + } + + _, validatedSchemas, errs := MergeNDCRestSchemas(config, schemas) + if len(errs) > 0 { + printSchemaValidationError(logger, errs) + if validatedSchemas == nil || config.Strict { + return errors.New("invalid rest schema") + } + } + + // cache the output file to disk + if config.Output == "" { + return nil + } + + return utils.WriteSchemaFile(filepath.Join(configurationDir, config.Output), schemas) +} + +func printSchemaValidationError(logger *slog.Logger, errors map[string][]string) { + logger.Error("errors happen when validating NDC REST schemas", slog.Any("errors", errors)) +} + +// ReadConfigurationFile reads and decodes the configuration file from the configuration directory +func ReadConfigurationFile(configurationDir string) (*Configuration, error) { + var config Configuration + jsonBytes, err := os.ReadFile(configurationDir + "/config.json") + if err == nil { + if err = json.Unmarshal(jsonBytes, &config); err != nil { + return nil, err + } + return &config, nil + } + + if !os.IsNotExist(err) { + return nil, err + } + + // try to read and parse yaml file + yamlBytes, err := os.ReadFile(configurationDir + "/config.yaml") + if err != nil { + if !os.IsNotExist(err) { + return nil, err + } + yamlBytes, err = os.ReadFile(configurationDir + "/config.yml") + } + + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("the config.{json,yaml,yml} file does not exist at %s", configurationDir) + } else { + return nil, err + } + } + + if err = yaml.Unmarshal(yamlBytes, &config); err != nil { + return nil, err + } + + return &config, nil +} diff --git a/ndc-rest-schema/go.mod b/ndc-rest-schema/go.mod index bd9833f..16b4bf1 100644 --- a/ndc-rest-schema/go.mod +++ b/ndc-rest-schema/go.mod @@ -5,12 +5,13 @@ go 1.23.0 toolchain go1.23.1 require ( - github.com/alecthomas/kong v1.2.1 + github.com/alecthomas/kong v1.4.0 github.com/evanphx/json-patch v0.5.2 - github.com/hasura/ndc-sdk-go v1.5.2-0.20241020093415-b752942bd505 + github.com/google/go-cmp v0.6.0 + github.com/hasura/ndc-sdk-go v1.6.2-0.20241109102535-399b739f7af5 github.com/invopop/jsonschema v0.12.0 github.com/lmittmann/tint v1.0.5 - github.com/pb33f/libopenapi v0.18.3 + github.com/pb33f/libopenapi v0.18.6 github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 @@ -21,12 +22,15 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/dprotaso/go-yit v0.0.0-20240618133044-5a0af90af097 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.60.1 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect ) diff --git a/ndc-rest-schema/go.sum b/ndc-rest-schema/go.sum index 79ed1c2..51ca3fd 100644 --- a/ndc-rest-schema/go.sum +++ b/ndc-rest-schema/go.sum @@ -1,7 +1,7 @@ -github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= -github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= -github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.4.0 h1:UL7tzGMnnY0YRMMvJyITIRX1EpO6RbBRZDNcCevy3HA= +github.com/alecthomas/kong v1.4.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -43,8 +43,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hasura/ndc-sdk-go v1.5.2-0.20241020093415-b752942bd505 h1:6ATW3s+x5aJ+hGzdlNr1f5nu9IV8SY/lakVuOCMvBjM= -github.com/hasura/ndc-sdk-go v1.5.2-0.20241020093415-b752942bd505/go.mod h1:oik0JrwuN5iZwZjZJzIRMw9uO2xDJbCXwhS1GgaRejk= +github.com/hasura/ndc-sdk-go v1.6.2-0.20241109102535-399b739f7af5 h1:YA3ix2/SMZ+vR/96YXuSPNYHsocsWnY8xCmhJeT3RYs= +github.com/hasura/ndc-sdk-go v1.6.2-0.20241109102535-399b739f7af5/go.mod h1:H7iN3SFXSou2rjBKv9fLumbvDXMDGP0Eg+cXWHpkA3k= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -77,12 +77,16 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/pb33f/libopenapi v0.18.3 h1:j4lm8xMM/GYSj2M8S7qNwZ//rOtEK5ACEiuNC7mTJzE= -github.com/pb33f/libopenapi v0.18.3/go.mod h1:9ap4lXBHgxGyFwxtOfa+B1C3IQ0rvnqteqjJvJ11oiQ= +github.com/pb33f/libopenapi v0.18.6 h1:adxzZUnOBOAuKxFAIrtb1Qt8GA4XnDWUAxEnqiSoTh0= +github.com/pb33f/libopenapi v0.18.6/go.mod h1:qZRs2IHIcs9SjHPmQfSUCyeD3OY9JkLJQOuFxd0bYCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= +github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= @@ -96,8 +100,8 @@ github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd h1:dLuIF2kX9c+KknGJUdJi1Il1SDiTSK158/BB9kdgAew= github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -153,9 +157,12 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/ndc-rest-schema/jsonschema/configuration.schema.json b/ndc-rest-schema/jsonschema/configuration.schema.json index 7686885..408922c 100644 --- a/ndc-rest-schema/jsonschema/configuration.schema.json +++ b/ndc-rest-schema/jsonschema/configuration.schema.json @@ -3,6 +3,30 @@ "$id": "https://github.com/hasura/ndc-rest/ndc-rest-schema/configuration/configuration", "$ref": "#/$defs/Configuration", "$defs": { + "ConcurrencySettings": { + "properties": { + "query": { + "type": "integer", + "description": "Maximum number of concurrent executions if there are many query variables. Zero means unlimited." + }, + "mutation": { + "type": "integer", + "description": "Maximum number of concurrent executions if there are many mutation operations. Zero means unlimited." + }, + "rest": { + "type": "integer", + "description": "Maximum number of concurrent requests to remote servers (distribution mode). Zero means unlimited." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "query", + "mutation", + "rest" + ], + "description": "ConcurrencySettings represent settings for concurrent webhook executions to remote servers." + }, "ConfigItem": { "properties": { "file": { @@ -68,13 +92,19 @@ "distributed": { "type": "boolean", "description": "Distributed enables distributed schema" + }, + "timeout": { + "$ref": "#/$defs/EnvInt", + "description": "configure the request timeout in seconds." + }, + "retry": { + "$ref": "#/$defs/RetryPolicySetting" } }, "additionalProperties": false, "type": "object", "required": [ - "file", - "distributed" + "file" ], "description": "ConfigItem extends the ConvertConfig with advanced options" }, @@ -83,6 +113,16 @@ "output": { "type": "string" }, + "strict": { + "type": "boolean", + "description": "Require strict validation" + }, + "forwardHeaders": { + "$ref": "#/$defs/ForwardHeadersSettings" + }, + "concurrency": { + "$ref": "#/$defs/ConcurrencySettings" + }, "files": { "items": { "$ref": "#/$defs/ConfigItem" @@ -93,10 +133,106 @@ "additionalProperties": false, "type": "object", "required": [ + "strict", + "forwardHeaders", + "concurrency", "files" ], "description": "Configuration contains required settings for the connector." }, + "EnvInt": { + "anyOf": [ + { + "required": [ + "value" + ], + "title": "value" + }, + { + "required": [ + "env" + ], + "title": "env" + } + ], + "properties": { + "value": { + "type": "integer" + }, + "env": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "ForwardHeadersSettings": { + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable headers forwarding." + }, + "argumentField": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The argument field name to be added for headers forwarding." + }, + "responseHeaders": { + "oneOf": [ + { + "$ref": "#/$defs/ForwardResponseHeadersSettings", + "description": "HTTP response headers to be forwarded from a data connector to the client." + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "enabled", + "argumentField", + "responseHeaders" + ], + "description": "ForwardHeadersSettings hold settings of header forwarding from and to Hasura engine" + }, + "ForwardResponseHeadersSettings": { + "properties": { + "headersField": { + "type": "string", + "pattern": "^[a-zA-Z_]\\w+$", + "description": "Name of the field in the NDC function/procedure's result which contains the response headers." + }, + "resultField": { + "type": "string", + "pattern": "^[a-zA-Z_]\\w+$", + "description": "Name of the field in the NDC function/procedure's result which contains the result." + }, + "forwardHeaders": { + "items": { + "type": "string" + }, + "type": "array", + "description": "List of actual HTTP response headers from the data connector to be set as response headers. Returns all headers if empty." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "headersField", + "resultField", + "forwardHeaders" + ], + "description": "ForwardHeadersSettings hold settings of header forwarding from http response to Hasura engine." + }, "PatchConfig": { "properties": { "path": { @@ -117,6 +253,28 @@ "strategy" ] }, + "RetryPolicySetting": { + "properties": { + "times": { + "$ref": "#/$defs/EnvInt", + "description": "Number of retry times" + }, + "delay": { + "$ref": "#/$defs/EnvInt", + "description": "Delay retry delay in milliseconds" + }, + "httpStatus": { + "items": { + "type": "integer" + }, + "type": "array", + "description": "HTTPStatus retries if the remote service returns one of these http status" + } + }, + "additionalProperties": false, + "type": "object", + "description": "RetryPolicySetting represents retry policy settings" + }, "SchemaSpecType": { "type": "string", "enum": [ diff --git a/ndc-rest-schema/jsonschema/ndc-rest-schema.schema.json b/ndc-rest-schema/jsonschema/ndc-rest-schema.schema.json index cbfa0e5..68fc49b 100644 --- a/ndc-rest-schema/jsonschema/ndc-rest-schema.schema.json +++ b/ndc-rest-schema/jsonschema/ndc-rest-schema.schema.json @@ -88,54 +88,83 @@ "type": "object", "description": "EncodingObject represents the Encoding Object that contains serialization strategy for application/x-www-form-urlencoded\n\n[Encoding Object]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#encoding-object" }, - "EnvBoolean": { - "oneOf": [ + "EnvBool": { + "anyOf": [ { - "type": "boolean" + "required": [ + "value" + ], + "title": "value" }, { + "required": [ + "env" + ], + "title": "env" + } + ], + "properties": { + "value": { + "type": "boolean" + }, + "env": { "type": "string" } - ] + }, + "additionalProperties": false, + "type": "object" }, "EnvInt": { - "oneOf": [ + "anyOf": [ { - "type": "integer" + "required": [ + "value" + ], + "title": "value" }, { + "required": [ + "env" + ], + "title": "env" + } + ], + "properties": { + "value": { + "type": "integer" + }, + "env": { "type": "string" } - ] + }, + "additionalProperties": false, + "type": "object" }, - "EnvInts": { - "oneOf": [ + "EnvString": { + "anyOf": [ { - "type": "string" + "required": [ + "value" + ], + "title": "value" }, { - "items": { - "type": "integer" - }, - "type": "array" + "required": [ + "env" + ], + "title": "env" } - ] - }, - "EnvString": { - "type": "string" - }, - "EnvStrings": { - "oneOf": [ - { + ], + "properties": { + "value": { "type": "string" }, - { - "items": { - "type": "string" - }, - "type": "array" + "env": { + "type": "string" } - ] + }, + "additionalProperties": false, + "type": "object" }, "NDCRestSchema": { "properties": { @@ -195,13 +224,6 @@ }, "type": "object" }, - "timeout": { - "$ref": "#/$defs/EnvInt", - "description": "configure the request timeout in seconds, default 30s" - }, - "retry": { - "$ref": "#/$defs/RetryPolicySetting" - }, "securitySchemes": { "additionalProperties": { "$ref": "#/$defs/SecurityScheme" @@ -288,10 +310,6 @@ "type": "string", "description": "Column description" }, - "name": { - "type": "string", - "description": "The name of the procedure" - }, "result_type": { "$ref": "#/$defs/Type", "description": "The name of the result type" @@ -302,7 +320,6 @@ "required": [ "request", "arguments", - "name", "result_type" ], "description": "OperationInfo extends connector command operation with OpenAPI REST information" @@ -357,10 +374,6 @@ "security": { "$ref": "#/$defs/AuthSecurities" }, - "timeout": { - "type": "integer", - "description": "configure the request timeout in seconds, default 30s" - }, "servers": { "items": { "$ref": "#/$defs/ServerConfig" @@ -373,6 +386,9 @@ "response": { "$ref": "#/$defs/Response" }, + "timeout": { + "type": "integer" + }, "retry": { "$ref": "#/$defs/RetryPolicy" } @@ -479,25 +495,6 @@ "type": "object", "description": "RetryPolicy represents the retry policy of request" }, - "RetryPolicySetting": { - "properties": { - "times": { - "$ref": "#/$defs/EnvInt", - "description": "Number of retry times" - }, - "delay": { - "$ref": "#/$defs/EnvInt", - "description": "Delay retry delay in milliseconds" - }, - "httpStatus": { - "$ref": "#/$defs/EnvInts", - "description": "HTTPStatus retries if the remote service returns one of these http status" - } - }, - "additionalProperties": false, - "type": "object", - "description": "RetryPolicySetting represents retry policy settings" - }, "ScalarType": { "properties": { "aggregate_functions": { @@ -644,13 +641,6 @@ }, "type": "object" }, - "timeout": { - "$ref": "#/$defs/EnvInt", - "description": "configure the request timeout in seconds, default 30s" - }, - "retry": { - "$ref": "#/$defs/RetryPolicySetting" - }, "securitySchemes": { "additionalProperties": { "$ref": "#/$defs/SecurityScheme" @@ -698,11 +688,11 @@ "description": "Alternative to ca_file. Provide the CA cert contents as a string instead of a filepath." }, "insecureSkipVerify": { - "$ref": "#/$defs/EnvBoolean", + "$ref": "#/$defs/EnvBool", "description": "Additionally you can configure TLS to be enabled but skip verifying the server's certificate chain." }, "includeSystemCACertsPool": { - "$ref": "#/$defs/EnvBoolean", + "$ref": "#/$defs/EnvBool", "description": "Whether to load the system certificate authorities pool alongside the certificate authority." }, "minVersion": { @@ -714,7 +704,10 @@ "description": "Maximum acceptable TLS version." }, "cipherSuites": { - "$ref": "#/$defs/EnvStrings", + "items": { + "type": "string" + }, + "type": "array", "description": "Explicit cipher suites can be set. If left blank, a safe default list is used.\nSee https://go.dev/src/crypto/tls/cipher_suites.go for a list of supported cipher suites." }, "reloadInterval": { diff --git a/ndc-rest-schema/main.go b/ndc-rest-schema/main.go index 018cbe6..b66928a 100644 --- a/ndc-rest-schema/main.go +++ b/ndc-rest-schema/main.go @@ -8,15 +8,17 @@ import ( "github.com/alecthomas/kong" "github.com/hasura/ndc-rest/ndc-rest-schema/command" + "github.com/hasura/ndc-rest/ndc-rest-schema/configuration" "github.com/hasura/ndc-rest/ndc-rest-schema/version" "github.com/lmittmann/tint" ) var cli struct { - LogLevel string `default:"info" enum:"debug,info,warn,error" help:"Log level."` - Convert command.ConvertCommandArguments `cmd:"" help:"Convert API spec to NDC schema. For example:\n ndc-rest-schema convert -f petstore.yaml -o petstore.json"` - Json2Yaml command.Json2YamlCommandArguments `cmd:"" help:"Convert JSON file to YAML. For example:\n ndc-rest-schema json2yaml -f petstore.json -o petstore.yaml" name:"json2yaml"` - Version struct{} `cmd:"" help:"Print the CLI version."` + LogLevel string `default:"info" enum:"debug,info,warn,error" help:"Log level."` + Update command.UpdateCommandArguments `cmd:"" help:"Update REST connector configuration"` + Convert configuration.ConvertCommandArguments `cmd:"" help:"Convert API spec to NDC schema. For example:\n ndc-rest-schema convert -f petstore.yaml -o petstore.json"` + Json2Yaml command.Json2YamlCommandArguments `cmd:"" help:"Convert JSON file to YAML. For example:\n ndc-rest-schema json2yaml -f petstore.json -o petstore.yaml" name:"json2yaml"` + Version struct{} `cmd:"" help:"Print the CLI version."` } func main() { @@ -29,6 +31,8 @@ func main() { } switch cmd.Command() { + case "update": + err = command.UpdateConfiguration(&cli.Update, logger) case "convert": err = command.CommandConvertToNDCSchema(&cli.Convert, logger) case "json2yaml": diff --git a/ndc-rest-schema/openapi/internal/ndc.go b/ndc-rest-schema/openapi/internal/ndc.go new file mode 100644 index 0000000..5bf0569 --- /dev/null +++ b/ndc-rest-schema/openapi/internal/ndc.go @@ -0,0 +1 @@ +package internal diff --git a/ndc-rest-schema/openapi/internal/oas2.go b/ndc-rest-schema/openapi/internal/oas2.go index 7875769..187cc1e 100644 --- a/ndc-rest-schema/openapi/internal/oas2.go +++ b/ndc-rest-schema/openapi/internal/oas2.go @@ -10,6 +10,7 @@ import ( rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" "github.com/hasura/ndc-rest/ndc-rest-schema/utils" "github.com/hasura/ndc-sdk-go/schema" + sdkUtils "github.com/hasura/ndc-sdk-go/utils" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" v2 "github.com/pb33f/libopenapi/datamodel/high/v2" @@ -32,7 +33,6 @@ func NewOAS2Builder(schema *rest.NDCRestSchema, options ConvertOptions) *OAS2Bui ConvertOptions: applyConvertOptions(options), } - setDefaultSettings(builder.schema.Settings, builder.ConvertOptions) return builder } @@ -57,7 +57,7 @@ func (oc *OAS2Builder) BuildDocumentModel(docModel *libopenapi.DocumentModel[v2. envName := utils.StringSliceToConstantCase([]string{oc.EnvPrefix, "SERVER_URL"}) serverURL := fmt.Sprintf("%s://%s%s", scheme, docModel.Model.Host, docModel.Model.BasePath) oc.schema.Settings.Servers = append(oc.schema.Settings.Servers, rest.ServerConfig{ - URL: *rest.NewEnvStringTemplate(rest.NewEnvTemplateWithDefault(envName, serverURL)), + URL: sdkUtils.NewEnvString(envName, serverURL), }) } @@ -109,9 +109,8 @@ func (oc *OAS2Builder) convertSecuritySchemes(scheme orderedmap.Pair[string, *v2 In: inLocation, Name: security.Name, } - result.Value = rest.NewEnvStringTemplate(rest.EnvTemplate{ - Name: utils.StringSliceToConstantCase([]string{oc.EnvPrefix, key}), - }) + valueEnv := sdkUtils.NewEnvStringVariable(utils.StringSliceToConstantCase([]string{oc.EnvPrefix, key})) + result.Value = &valueEnv result.APIKeyAuthConfig = &apiConfig case "basic": result.Type = rest.HTTPAuthScheme @@ -119,9 +118,8 @@ func (oc *OAS2Builder) convertSecuritySchemes(scheme orderedmap.Pair[string, *v2 Scheme: "Basic", Header: "Authorization", } - result.Value = rest.NewEnvStringTemplate(rest.EnvTemplate{ - Name: utils.StringSliceToConstantCase([]string{oc.EnvPrefix, key, "TOKEN"}), - }) + valueEnv := sdkUtils.NewEnvStringVariable(utils.StringSliceToConstantCase([]string{oc.EnvPrefix, key, "TOKEN"})) + result.Value = &valueEnv result.HTTPAuthConfig = &httpConfig case "oauth2": var flowType rest.OAuthFlowType diff --git a/ndc-rest-schema/openapi/internal/oas3.go b/ndc-rest-schema/openapi/internal/oas3.go index 5ee044e..533ff31 100644 --- a/ndc-rest-schema/openapi/internal/oas3.go +++ b/ndc-rest-schema/openapi/internal/oas3.go @@ -8,6 +8,7 @@ import ( rest "github.com/hasura/ndc-rest/ndc-rest-schema/schema" "github.com/hasura/ndc-rest/ndc-rest-schema/utils" "github.com/hasura/ndc-sdk-go/schema" + sdkUtils "github.com/hasura/ndc-sdk-go/utils" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" @@ -43,7 +44,6 @@ func NewOAS3Builder(schema *rest.NDCRestSchema, options ConvertOptions) *OAS3Bui ConvertOptions: applyConvertOptions(options), } - setDefaultSettings(builder.schema.Settings, builder.ConvertOptions) return builder } @@ -122,7 +122,7 @@ func (oc *OAS3Builder) convertServers(servers []*v3.Server) []rest.ServerConfig conf := rest.ServerConfig{ ID: serverID, - URL: *rest.NewEnvStringTemplate(rest.NewEnvTemplateWithDefault(envName, serverURL)), + URL: sdkUtils.NewEnvString(envName, serverURL), } results = append(results, conf) } @@ -154,14 +154,17 @@ func (oc *OAS3Builder) convertSecuritySchemes(scheme orderedmap.Pair[string, *v3 In: inLocation, Name: security.Name, } - result.Value = rest.NewEnvStringTemplate(rest.NewEnvTemplate(utils.StringSliceToConstantCase([]string{oc.EnvPrefix, key}))) + valueEnv := sdkUtils.NewEnvStringVariable(utils.StringSliceToConstantCase([]string{oc.EnvPrefix, key})) + result.Value = &valueEnv result.APIKeyAuthConfig = &apiConfig case rest.HTTPAuthScheme: httpConfig := rest.HTTPAuthConfig{ Scheme: security.Scheme, Header: "Authorization", } - result.Value = rest.NewEnvStringTemplate(rest.NewEnvTemplate(utils.StringSliceToConstantCase([]string{oc.EnvPrefix, key, "TOKEN"}))) + + valueEnv := sdkUtils.NewEnvStringVariable(utils.StringSliceToConstantCase([]string{oc.EnvPrefix, key, "TOKEN"})) + result.Value = &valueEnv result.HTTPAuthConfig = &httpConfig case rest.OAuth2Scheme: if security.Flows == nil { diff --git a/ndc-rest-schema/openapi/internal/utils.go b/ndc-rest-schema/openapi/internal/utils.go index f3fd0cd..e34b7c6 100644 --- a/ndc-rest-schema/openapi/internal/utils.go +++ b/ndc-rest-schema/openapi/internal/utils.go @@ -287,23 +287,6 @@ func encodeHeaderArgumentName(name string) string { return "header" + utils.ToPascalCase(name) } -func setDefaultSettings(settings *rest.NDCRestSettings, opts *ConvertOptions) { - settings.Timeout = rest.NewEnvIntTemplate(rest.EnvTemplate{ - Name: utils.StringSliceToConstantCase([]string{opts.EnvPrefix, "TIMEOUT"}), - }) - settings.Retry = &rest.RetryPolicySetting{ - Times: *rest.NewEnvIntTemplate(rest.EnvTemplate{ - Name: utils.StringSliceToConstantCase([]string{opts.EnvPrefix, "RETRY_TIMES"}), - }), - Delay: *rest.NewEnvIntTemplate(rest.EnvTemplate{ - Name: utils.StringSliceToConstantCase([]string{opts.EnvPrefix, "RETRY_DELAY"}), - }), - HTTPStatus: *rest.NewEnvIntsTemplate(rest.EnvTemplate{ - Name: utils.StringSliceToConstantCase([]string{opts.EnvPrefix, "RETRY_HTTP_STATUS"}), - }), - } -} - // evaluate and filter invalid types in allOf, anyOf or oneOf schemas func evalSchemaProxiesSlice(schemaProxies []*base.SchemaProxy, location rest.ParameterLocation) ([]*base.SchemaProxy, *base.Schema, bool) { var results []*base.SchemaProxy diff --git a/ndc-rest-schema/openapi/oas2_test.go b/ndc-rest-schema/openapi/oas2_test.go index 8e19331..d079308 100644 --- a/ndc-rest-schema/openapi/oas2_test.go +++ b/ndc-rest-schema/openapi/oas2_test.go @@ -12,7 +12,6 @@ import ( ) func TestOpenAPIv2ToRESTSchema(t *testing.T) { - testCases := []struct { Name string Source string @@ -100,5 +99,5 @@ func assertConnectorSchema(t *testing.T, schemaPath string, output *rest.NDCRest var expectedSchema schema.SchemaResponse assert.NilError(t, json.Unmarshal(schemaBytes, &expectedSchema)) outputSchema := output.ToSchemaResponse() - assert.DeepEqual(t, expectedSchema, *outputSchema) + assetDeepEqual(t, expectedSchema, *outputSchema) } diff --git a/ndc-rest-schema/openapi/oas3_test.go b/ndc-rest-schema/openapi/oas3_test.go index 2037b19..c8b4c9a 100644 --- a/ndc-rest-schema/openapi/oas3_test.go +++ b/ndc-rest-schema/openapi/oas3_test.go @@ -4,8 +4,11 @@ import ( "encoding/json" "errors" "os" + "reflect" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hasura/ndc-rest/ndc-rest-schema/schema" "gotest.tools/v3/assert" ) @@ -101,12 +104,33 @@ func TestOpenAPIv3ToRESTSchema(t *testing.T) { func assertRESTSchemaEqual(t *testing.T, expected *schema.NDCRestSchema, output *schema.NDCRestSchema) { t.Helper() - assert.DeepEqual(t, expected.Settings, output.Settings) - assert.DeepEqual(t, expected.ScalarTypes, output.ScalarTypes) + assetDeepEqual(t, expected.Settings.Headers, output.Settings.Headers) + assetDeepEqual(t, expected.Settings.Security, output.Settings.Security) + assetDeepEqual(t, expected.Settings.SecuritySchemes, output.Settings.SecuritySchemes) + assetDeepEqual(t, expected.Settings.Version, output.Settings.Version) + for i, server := range expected.Settings.Servers { + sv := output.Settings.Servers[i] + assetDeepEqual(t, server.Headers, sv.Headers) + assetDeepEqual(t, server.ID, sv.ID) + assetDeepEqual(t, server.Security, sv.Security) + assetDeepEqual(t, server.SecuritySchemes, sv.SecuritySchemes) + assetDeepEqual(t, server.URL, sv.URL) + assetDeepEqual(t, server.TLS, sv.TLS) + } + assetDeepEqual(t, expected.ScalarTypes, output.ScalarTypes) objectBs, _ := json.Marshal(output.ObjectTypes) var objectTypes map[string]schema.ObjectType assert.NilError(t, json.Unmarshal(objectBs, &objectTypes)) - assert.DeepEqual(t, expected.ObjectTypes, objectTypes) - assert.DeepEqual(t, expected.Procedures, output.Procedures) - assert.DeepEqual(t, expected.Functions, output.Functions) + assetDeepEqual(t, expected.ObjectTypes, objectTypes) + assetDeepEqual(t, expected.Procedures, output.Procedures) + assetDeepEqual(t, expected.Functions, output.Functions) +} + +func assetDeepEqual(t *testing.T, expected any, reality any) { + t.Helper() + assert.DeepEqual(t, + expected, reality, + cmpopts.IgnoreUnexported(schema.ServerConfig{}, schema.NDCRestSettings{}), + cmp.Exporter(func(t reflect.Type) bool { return true }), + ) } diff --git a/ndc-rest-schema/openapi/testdata/jsonplaceholder/expected.json b/ndc-rest-schema/openapi/testdata/jsonplaceholder/expected.json index dd616a5..8bdcfed 100644 --- a/ndc-rest-schema/openapi/testdata/jsonplaceholder/expected.json +++ b/ndc-rest-schema/openapi/testdata/jsonplaceholder/expected.json @@ -3,15 +3,12 @@ "settings": { "servers": [ { - "url": "{{SERVER_URL:-https://jsonplaceholder.typicode.com}}" + "url": { + "value": "https://jsonplaceholder.typicode.com", + "env": "SERVER_URL" + } } ], - "timeout": "{{TIMEOUT}}", - "retry": { - "times": "{{RETRY_TIMES}}", - "delay": "{{RETRY_DELAY}}", - "httpStatus": "{{RETRY_HTTP_STATUS}}" - }, "version": "1.0.0" }, "functions": { @@ -64,7 +61,6 @@ } }, "description": "Get all available albums", - "name": "getAlbums", "result_type": { "element_type": { "name": "Album", @@ -100,7 +96,6 @@ } }, "description": "Get specific album", - "name": "getAlbumsId", "result_type": { "name": "Album", "type": "named" @@ -133,7 +128,6 @@ } }, "description": "Get photos for a specific album", - "name": "getAlbumsIdPhotos", "result_type": { "element_type": { "name": "Photo", @@ -169,7 +163,6 @@ } }, "description": "Get specific comment", - "name": "getComment", "result_type": { "name": "Comment", "type": "named" @@ -224,7 +217,6 @@ } }, "description": "Get all available comments", - "name": "getComments", "result_type": { "element_type": { "name": "Comment", @@ -260,7 +252,6 @@ } }, "description": "Get specific photo", - "name": "getPhoto", "result_type": { "name": "Photo", "type": "named" @@ -315,7 +306,6 @@ } }, "description": "Get all available photos", - "name": "getPhotos", "result_type": { "element_type": { "name": "Photo", @@ -351,7 +341,6 @@ } }, "description": "Get specific post", - "name": "getPostById", "result_type": { "name": "Post", "type": "named" @@ -406,7 +395,6 @@ } }, "description": "Get all available posts", - "name": "getPosts", "result_type": { "element_type": { "name": "Post", @@ -442,7 +430,6 @@ } }, "description": "Get comments for a specific post", - "name": "getPostsIdComments", "result_type": { "element_type": { "name": "Comment", @@ -461,7 +448,6 @@ }, "arguments": {}, "description": "Get test", - "name": "getTest", "result_type": { "name": "User", "type": "named" @@ -494,7 +480,6 @@ } }, "description": "Get specific todo", - "name": "getTodo", "result_type": { "name": "Todo", "type": "named" @@ -549,7 +534,6 @@ } }, "description": "Get all available todos", - "name": "getTodos", "result_type": { "element_type": { "name": "Todo", @@ -585,7 +569,6 @@ } }, "description": "Get specific user", - "name": "getUser", "result_type": { "name": "User", "type": "named" @@ -640,7 +623,6 @@ } }, "description": "Get all available users", - "name": "getUsers", "result_type": { "element_type": { "name": "User", @@ -1279,7 +1261,6 @@ } }, "description": "Create a post", - "name": "createPost", "result_type": { "name": "Post", "type": "named" @@ -1312,7 +1293,6 @@ } }, "description": "Delete specific post", - "name": "deletePostById", "result_type": { "type": "nullable", "underlying_type": { @@ -1366,7 +1346,6 @@ } }, "description": "patch specific post", - "name": "patchPostById", "result_type": { "name": "Post", "type": "named" @@ -1417,7 +1396,6 @@ } }, "description": "Update specific post", - "name": "updatePostById", "result_type": { "name": "Post", "type": "named" diff --git a/ndc-rest-schema/openapi/testdata/onesignal/expected-patch.json b/ndc-rest-schema/openapi/testdata/onesignal/expected-patch.json index 73bd94a..24c68e8 100644 --- a/ndc-rest-schema/openapi/testdata/onesignal/expected-patch.json +++ b/ndc-rest-schema/openapi/testdata/onesignal/expected-patch.json @@ -4,25 +4,26 @@ "servers": [ { "id": "foo", - "url": "{{FOO_SERVER_URL:-https://onesignal.com/api/v1}}" + "url": { + "env": "FOO_SERVER_URL", + "value": "https://onesignal.com/api/v1" + } } ], - "timeout": "{{TIMEOUT}}", - "retry": { - "times": "{{RETRY_TIMES}}", - "delay": "{{RETRY_DELAY}}", - "httpStatus": "{{RETRY_HTTP_STATUS}}" - }, "securitySchemes": { "app_key": { "type": "http", - "value": "{{APP_KEY_TOKEN}}", + "value": { + "env": "APP_KEY_TOKEN" + }, "header": "Authorization", "scheme": "bearer" }, "user_key": { "type": "http", - "value": "{{USER_KEY_TOKEN}}", + "value": { + "env": "USER_KEY_TOKEN" + }, "header": "Authorization", "scheme": "bearer" } @@ -30,8 +31,8 @@ "version": "1.2.2" }, "collections": [], - "procedures": [ - { + "procedures": { + "create_notification": { "request": { "url": "/notifications", "method": "post", @@ -56,6 +57,9 @@ "type": { "name": "NotificationInput", "type": "named" + }, + "rest": { + "in": "body" } } }, @@ -66,7 +70,7 @@ "type": "named" } }, - { + "cancel_notification": { "request": { "url": "/notifications/{notification_id}", "method": "delete", @@ -100,12 +104,26 @@ "type": { "name": "String", "type": "named" + }, + "rest": { + "name": "app_id", + "in": "query", + "schema": { + "type": ["string"] + } } }, "notification_id": { "type": { "name": "String", "type": "named" + }, + "rest": { + "name": "notification_id", + "in": "path", + "schema": { + "type": ["string"] + } } } }, @@ -116,7 +134,7 @@ "type": "named" } } - ], + }, "scalar_types": { "Boolean": { "aggregate_functions": {}, diff --git a/ndc-rest-schema/openapi/testdata/onesignal/expected.json b/ndc-rest-schema/openapi/testdata/onesignal/expected.json index ee2476d..bf5ff41 100644 --- a/ndc-rest-schema/openapi/testdata/onesignal/expected.json +++ b/ndc-rest-schema/openapi/testdata/onesignal/expected.json @@ -3,25 +3,26 @@ "settings": { "servers": [ { - "url": "{{SERVER_URL:-https://onesignal.com/api/v1}}" + "url": { + "env": "SERVER_URL", + "value": "https://onesignal.com/api/v1" + } } ], - "timeout": "{{TIMEOUT}}", - "retry": { - "times": "{{RETRY_TIMES}}", - "delay": "{{RETRY_DELAY}}", - "httpStatus": "{{RETRY_HTTP_STATUS}}" - }, "securitySchemes": { "app_key": { "type": "http", - "value": "{{APP_KEY_TOKEN}}", + "value": { + "env": "APP_KEY_TOKEN" + }, "header": "Authorization", "scheme": "bearer" }, "user_key": { "type": "http", - "value": "{{USER_KEY_TOKEN}}", + "value": { + "env": "USER_KEY_TOKEN" + }, "header": "Authorization", "scheme": "bearer" } @@ -52,9 +53,7 @@ "name": "app_id", "in": "query", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -67,9 +66,7 @@ "name": "notification_id", "in": "path", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -105,9 +102,7 @@ "name": "app_id", "in": "query", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -124,9 +119,7 @@ "name": "kind", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -143,9 +136,7 @@ "name": "limit", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -162,9 +153,7 @@ "name": "offset", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -189,9 +178,7 @@ } }, "rest": { - "type": [ - "boolean" - ] + "type": ["boolean"] } } } @@ -219,9 +206,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "id": { @@ -233,9 +218,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "recipients": { @@ -247,9 +230,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -266,9 +247,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "errored": { @@ -281,9 +260,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "failed": { @@ -296,9 +273,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "received": { @@ -311,9 +286,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "successful": { @@ -326,9 +299,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -342,9 +313,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "key": { @@ -357,9 +326,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "relation": { @@ -369,9 +336,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "value": { @@ -384,9 +349,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -402,9 +365,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "email": { @@ -417,9 +378,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "events": { @@ -432,9 +391,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -450,9 +407,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "success": { @@ -464,9 +419,7 @@ } }, "rest": { - "type": [ - "boolean" - ] + "type": ["boolean"] } } } @@ -482,9 +435,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "custom_data": { @@ -496,9 +447,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "data": { @@ -511,9 +460,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "filters": { @@ -528,9 +475,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "headings": { @@ -542,9 +487,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "id": { @@ -556,9 +499,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "send_after": { @@ -570,9 +511,7 @@ } }, "rest": { - "type": [ - "string" - ], + "type": ["string"], "format": "date-time" } }, @@ -585,9 +524,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -603,9 +540,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "notifications": { @@ -620,9 +555,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "offset": { @@ -634,9 +567,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "total_count": { @@ -648,9 +579,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -667,9 +596,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -682,9 +609,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "converted": { @@ -697,9 +622,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "errored": { @@ -712,9 +635,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "excluded_segments": { @@ -729,13 +650,9 @@ } }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -749,9 +666,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "filters": { @@ -766,9 +681,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "headings": { @@ -780,9 +693,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "id": { @@ -794,9 +705,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "include_player_ids": { @@ -811,13 +720,9 @@ } }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -833,13 +738,9 @@ } }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -855,9 +756,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "platform_delivery_stats": { @@ -870,9 +769,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "queued_at": { @@ -885,9 +782,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -901,9 +796,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "remaining": { @@ -916,9 +809,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "send_after": { @@ -931,9 +822,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -946,9 +835,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "successful": { @@ -961,9 +848,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "target_channel": { @@ -975,9 +860,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "throttle_rate_per_minute": { @@ -990,9 +873,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -1005,9 +886,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "id": { @@ -1016,9 +895,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "value": { @@ -1027,9 +904,7 @@ "type": "named" }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -1046,9 +921,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "chrome_web_push": { @@ -1060,9 +933,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "edge_web_push": { @@ -1074,9 +945,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "email": { @@ -1088,9 +957,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "firefox_web_push": { @@ -1102,9 +969,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "ios": { @@ -1116,9 +981,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "safari_web_push": { @@ -1130,9 +993,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "sms": { @@ -1144,9 +1005,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -1163,9 +1022,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -1195,9 +1052,7 @@ "name": "app_id", "in": "query", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -1210,9 +1065,7 @@ "name": "notification_id", "in": "path", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -1296,9 +1149,7 @@ "name": "notification_id", "in": "path", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -1368,10 +1219,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "sent", - "clicked" - ], + "one_of": ["sent", "clicked"], "type": "enum" } }, @@ -1379,10 +1227,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "sum", - "count" - ], + "one_of": ["sum", "count"], "type": "enum" } }, @@ -1390,11 +1235,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "push", - "email", - "sms" - ], + "one_of": ["push", "email", "sms"], "type": "enum" } }, diff --git a/ndc-rest-schema/openapi/testdata/openai/expected.json b/ndc-rest-schema/openapi/testdata/openai/expected.json index 1e751f7..8c418a6 100644 --- a/ndc-rest-schema/openapi/testdata/openai/expected.json +++ b/ndc-rest-schema/openapi/testdata/openai/expected.json @@ -3,22 +3,24 @@ "settings": { "servers": [ { - "url": "{{SERVER_URL:-https://api.openai.com/v1}}" + "url": { + "env": "SERVER_URL", + "value": "https://api.openai.com/v1" + } }, { - "url": "{{SERVER_URL_2:-http://127.0.0.1:11434}}" + "url": { + "env": "SERVER_URL_2", + "value": "http://127.0.0.1:11434" + } } ], - "timeout": "{{TIMEOUT}}", - "retry": { - "times": "{{RETRY_TIMES}}", - "delay": "{{RETRY_DELAY}}", - "httpStatus": "{{RETRY_HTTP_STATUS}}" - }, "securitySchemes": { "ApiKeyAuth": { "type": "http", - "value": "{{API_KEY_AUTH_TOKEN}}", + "value": { + "env": "API_KEY_AUTH_TOKEN" + }, "header": "Authorization", "scheme": "bearer" } @@ -44,9 +46,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "name": { @@ -56,9 +56,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "parameters": { @@ -71,9 +69,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -87,9 +83,7 @@ "type": "named" }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "id": { @@ -99,9 +93,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "type": { @@ -111,9 +103,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -128,9 +118,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "name": { @@ -140,9 +128,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -157,9 +143,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "function_call": { @@ -172,9 +156,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "role": { @@ -184,9 +166,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "tool_calls": { @@ -202,9 +182,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } } } @@ -219,9 +197,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "name": { @@ -231,9 +207,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -251,9 +225,7 @@ } }, "rest": { - "type": [ - "boolean" - ] + "type": ["boolean"] } } } @@ -270,13 +242,9 @@ "type": "array" }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -287,9 +255,7 @@ "type": "named" }, "rest": { - "type": [ - "number" - ] + "type": ["number"] } }, "token": { @@ -299,9 +265,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "top_logprobs": { @@ -314,13 +278,9 @@ "type": "array" }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -338,13 +298,9 @@ "type": "array" }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -355,9 +311,7 @@ "type": "named" }, "rest": { - "type": [ - "number" - ] + "type": ["number"] } }, "token": { @@ -367,9 +321,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -382,9 +334,7 @@ "type": "named" }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "type": { @@ -394,9 +344,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -413,9 +361,7 @@ } }, "rest": { - "type": [ - "number" - ], + "type": ["number"], "maximum": 2, "minimum": -2 } @@ -446,9 +392,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "logit_bias": { @@ -461,9 +405,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "logprobs": { @@ -476,9 +418,7 @@ } }, "rest": { - "type": [ - "boolean" - ] + "type": ["boolean"] } }, "max_tokens": { @@ -491,9 +431,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "messages": { @@ -506,9 +444,7 @@ "type": "array" }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "model": { @@ -518,9 +454,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "n": { @@ -533,9 +467,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "maximum": 128, "minimum": 1 } @@ -550,9 +482,7 @@ } }, "rest": { - "type": [ - "boolean" - ] + "type": ["boolean"] } }, "presence_penalty": { @@ -565,9 +495,7 @@ } }, "rest": { - "type": [ - "number" - ], + "type": ["number"], "maximum": 2, "minimum": -2 } @@ -582,9 +510,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "seed": { @@ -597,9 +523,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "maximum": 9223372036854776000, "minimum": -9223372036854776000 } @@ -614,9 +538,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "stop": { @@ -642,9 +564,7 @@ } }, "rest": { - "type": [ - "boolean" - ] + "type": ["boolean"] } }, "stream_options": { @@ -657,9 +577,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "temperature": { @@ -672,9 +590,7 @@ } }, "rest": { - "type": [ - "number" - ], + "type": ["number"], "maximum": 2, "minimum": 0 } @@ -705,9 +621,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "top_logprobs": { @@ -720,9 +634,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "maximum": 20, "minimum": 0 } @@ -737,9 +649,7 @@ } }, "rest": { - "type": [ - "number" - ], + "type": ["number"], "maximum": 1, "minimum": 0 } @@ -754,9 +664,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -774,9 +682,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -794,13 +700,9 @@ "type": "array" }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "object" - ] + "type": ["object"] } } }, @@ -811,9 +713,7 @@ "type": "named" }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "id": { @@ -823,9 +723,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "model": { @@ -835,9 +733,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "object": { @@ -847,9 +743,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "service_tier": { @@ -862,9 +756,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "system_fingerprint": { @@ -877,9 +769,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -893,9 +783,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "index": { @@ -905,9 +793,7 @@ "type": "named" }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "logprobs": { @@ -917,9 +803,7 @@ "type": "named" }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "message": { @@ -929,9 +813,7 @@ "type": "named" }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -949,9 +831,7 @@ "type": "array" }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } } } @@ -968,9 +848,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -987,9 +865,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "name": { @@ -999,9 +875,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "parameters": { @@ -1014,9 +888,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -1033,9 +905,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "digest": { @@ -1048,9 +918,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "status": { @@ -1063,9 +931,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "total": { @@ -1078,9 +944,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -1163,9 +1027,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "function" - ], + "one_of": ["function"], "type": "enum" } }, @@ -1180,9 +1042,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "assistant" - ], + "one_of": ["assistant"], "type": "enum" } }, @@ -1197,9 +1057,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "function" - ], + "one_of": ["function"], "type": "enum" } }, @@ -1207,10 +1065,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "text", - "json_object" - ], + "one_of": ["text", "json_object"], "type": "enum" } }, @@ -1218,10 +1073,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "auto", - "default" - ], + "one_of": ["auto", "default"], "type": "enum" } }, @@ -1243,9 +1095,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "chat.completion" - ], + "one_of": ["chat.completion"], "type": "enum" } }, @@ -1253,10 +1103,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "scale", - "default" - ], + "one_of": ["scale", "default"], "type": "enum" } }, diff --git a/ndc-rest-schema/openapi/testdata/petstore2/expected.json b/ndc-rest-schema/openapi/testdata/petstore2/expected.json index 2d57c01..6ddd20f 100644 --- a/ndc-rest-schema/openapi/testdata/petstore2/expected.json +++ b/ndc-rest-schema/openapi/testdata/petstore2/expected.json @@ -3,25 +3,26 @@ "settings": { "servers": [ { - "url": "{{SERVER_URL:-https://petstore.swagger.io/v2}}" + "url": { + "env": "SERVER_URL", + "value": "https://petstore.swagger.io/v2" + } } ], - "timeout": "{{TIMEOUT}}", - "retry": { - "times": "{{RETRY_TIMES}}", - "delay": "{{RETRY_DELAY}}", - "httpStatus": "{{RETRY_HTTP_STATUS}}" - }, "securitySchemes": { "api_key": { "type": "apiKey", - "value": "{{API_KEY}}", + "value": { + "env": "API_KEY" + }, "in": "header", "name": "api_key" }, "basic": { "type": "http", - "value": "{{BASIC_TOKEN}}", + "value": { + "env": "BASIC_TOKEN" + }, "header": "Authorization", "scheme": "Basic" }, @@ -47,10 +48,7 @@ "method": "get", "security": [ { - "petstore_auth": [ - "write:pets", - "read:pets" - ] + "petstore_auth": ["write:pets", "read:pets"] } ], "response": { @@ -71,9 +69,7 @@ "name": "status", "in": "query", "schema": { - "type": [ - "array" - ] + "type": ["array"] } } } @@ -94,10 +90,7 @@ "method": "get", "security": [ { - "petstore_auth": [ - "write:pets", - "read:pets" - ] + "petstore_auth": ["write:pets", "read:pets"] } ], "response": { @@ -118,9 +111,7 @@ "name": "tags", "in": "query", "schema": { - "type": [ - "array" - ] + "type": ["array"] } } } @@ -175,9 +166,7 @@ "name": "orderId", "in": "path", "schema": { - "type": [ - "integer" - ], + "type": ["integer"], "maximum": 10, "minimum": 1 } @@ -215,9 +204,7 @@ "name": "petId", "in": "path", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -264,9 +251,7 @@ "name": "username", "in": "path", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -300,9 +285,7 @@ "name": "client_name", "in": "query", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -319,9 +302,7 @@ "name": "limit", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -338,9 +319,7 @@ "name": "offset", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -357,9 +336,7 @@ "name": "owner", "in": "query", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -393,9 +370,7 @@ "name": "password", "in": "query", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -409,9 +384,7 @@ "name": "username", "in": "query", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -436,9 +409,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int32" } }, @@ -451,9 +422,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "type": { @@ -465,9 +434,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -483,9 +450,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -498,9 +463,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -517,9 +480,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "client_name": { @@ -532,9 +493,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "client_secret": { @@ -547,9 +506,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "client_secret_expires_at": { @@ -562,9 +519,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -578,9 +533,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -596,9 +549,7 @@ } }, "rest": { - "type": [ - "boolean" - ] + "type": ["boolean"] } }, "id": { @@ -610,9 +561,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -625,9 +574,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -640,9 +587,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int32" } }, @@ -655,9 +600,7 @@ } }, "rest": { - "type": [ - "string" - ], + "type": ["string"], "format": "date-time" } }, @@ -671,9 +614,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -689,9 +630,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "id": { @@ -703,9 +642,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -715,9 +652,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "photoUrls": { @@ -729,13 +664,9 @@ "type": "array" }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -749,9 +680,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "tags": { @@ -766,9 +695,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } } } @@ -796,9 +723,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -811,9 +736,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -829,9 +752,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -844,9 +765,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -863,9 +782,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "status": { @@ -878,9 +795,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -897,9 +812,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "file": { @@ -912,9 +825,7 @@ } }, "rest": { - "type": [ - "file" - ] + "type": ["file"] } } } @@ -930,9 +841,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "firstName": { @@ -944,9 +853,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "id": { @@ -958,9 +865,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -973,9 +878,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "password": { @@ -987,9 +890,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "phone": { @@ -1001,9 +902,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "userStatus": { @@ -1016,9 +915,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int32" } }, @@ -1031,9 +928,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -1046,10 +941,7 @@ "method": "post", "security": [ { - "petstore_auth": [ - "write:pets", - "read:pets" - ] + "petstore_auth": ["write:pets", "read:pets"] } ], "requestBody": { @@ -1069,9 +961,7 @@ "rest": { "in": "body", "schema": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -1121,9 +1011,7 @@ "name": "orderId", "in": "path", "schema": { - "type": [ - "integer" - ], + "type": ["integer"], "minimum": 1 } } @@ -1145,10 +1033,7 @@ "method": "delete", "security": [ { - "petstore_auth": [ - "write:pets", - "read:pets" - ] + "petstore_auth": ["write:pets", "read:pets"] } ], "response": { @@ -1168,9 +1053,7 @@ "name": "api_key", "in": "header", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -1184,9 +1067,7 @@ "name": "petId", "in": "path", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -1220,9 +1101,7 @@ "name": "username", "in": "path", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -1257,9 +1136,7 @@ "rest": { "in": "body", "schema": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -1292,9 +1169,7 @@ "rest": { "in": "body", "schema": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -1312,10 +1187,7 @@ "method": "put", "security": [ { - "petstore_auth": [ - "write:pets", - "read:pets" - ] + "petstore_auth": ["write:pets", "read:pets"] } ], "requestBody": { @@ -1335,9 +1207,7 @@ "rest": { "in": "body", "schema": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -1358,10 +1228,7 @@ "method": "post", "security": [ { - "petstore_auth": [ - "write:pets", - "read:pets" - ] + "petstore_auth": ["write:pets", "read:pets"] } ], "requestBody": { @@ -1381,9 +1248,7 @@ "rest": { "in": "formData", "schema": { - "type": [ - "object" - ] + "type": ["object"] } } }, @@ -1397,9 +1262,7 @@ "name": "petId", "in": "path", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -1435,9 +1298,7 @@ "rest": { "in": "body", "schema": { - "type": [ - "object" - ] + "type": ["object"] } } }, @@ -1451,9 +1312,7 @@ "name": "username", "in": "path", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -1474,10 +1333,7 @@ "method": "post", "security": [ { - "petstore_auth": [ - "write:pets", - "read:pets" - ] + "petstore_auth": ["write:pets", "read:pets"] } ], "requestBody": { @@ -1497,9 +1353,7 @@ "rest": { "in": "formData", "schema": { - "type": [ - "object" - ] + "type": ["object"] } } }, @@ -1513,9 +1367,7 @@ "name": "petId", "in": "path", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -1568,11 +1420,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "placed", - "approved", - "delivered" - ], + "one_of": ["placed", "approved", "delivered"], "type": "enum" } }, @@ -1580,11 +1428,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "available", - "pending", - "sold" - ], + "one_of": ["available", "pending", "sold"], "type": "enum" } }, diff --git a/ndc-rest-schema/openapi/testdata/petstore3/expected.json b/ndc-rest-schema/openapi/testdata/petstore3/expected.json index 12801ec..db064b3 100644 --- a/ndc-rest-schema/openapi/testdata/petstore3/expected.json +++ b/ndc-rest-schema/openapi/testdata/petstore3/expected.json @@ -3,28 +3,32 @@ "settings": { "servers": [ { - "url": "{{PET_STORE_SERVER_URL:-https://petstore3.swagger.io/api/v3}}" + "url": { + "value": "https://petstore3.swagger.io/api/v3", + "env": "PET_STORE_SERVER_URL" + } }, { - "url": "{{PET_STORE_SERVER_URL_2:-https://petstore3.swagger.io/api/v3.1}}" + "url": { + "value": "https://petstore3.swagger.io/api/v3.1", + "env": "PET_STORE_SERVER_URL_2" + } } ], - "timeout": "{{PET_STORE_TIMEOUT}}", - "retry": { - "times": "{{PET_STORE_RETRY_TIMES}}", - "delay": "{{PET_STORE_RETRY_DELAY}}", - "httpStatus": "{{PET_STORE_RETRY_HTTP_STATUS}}" - }, "securitySchemes": { "api_key": { "type": "apiKey", - "value": "{{PET_STORE_API_KEY}}", + "value": { + "env": "PET_STORE_API_KEY" + }, "in": "header", "name": "api_key" }, "basic": { "type": "http", - "value": "{{PET_STORE_BASIC_TOKEN}}", + "value": { + "env": "PET_STORE_BASIC_TOKEN" + }, "header": "Authorization", "scheme": "basic" }, @@ -275,7 +279,6 @@ } }, "description": "You can list all invoices, or list the invoices for a specific customer. The invoices are returned sorted by creation date, with the most recently created invoices appearing first.", - "name": "GetInvoices", "result_type": { "name": "GetInvoicesResult", "type": "named" @@ -320,7 +323,6 @@ } }, "description": "Finds Pets by status", - "name": "findPetsByStatus", "result_type": { "element_type": { "name": "Pet", @@ -376,7 +378,6 @@ } }, "description": "Finds Pets by tags", - "name": "findPetsByTags", "result_type": { "element_type": { "name": "Pet", @@ -400,7 +401,6 @@ }, "arguments": {}, "description": "Returns pet inventories by status", - "name": "getInventory", "result_type": { "name": "JSON", "type": "named" @@ -434,7 +434,6 @@ } }, "description": "Find purchase order by ID", - "name": "getOrderById", "result_type": { "name": "Order", "type": "named" @@ -479,7 +478,6 @@ } }, "description": "Find pet by ID", - "name": "getPetById", "result_type": { "name": "Pet", "type": "named" @@ -495,7 +493,6 @@ }, "arguments": {}, "description": "Get snake object", - "name": "getSnake", "result_type": { "name": "SnakeObject", "type": "named" @@ -528,7 +525,6 @@ } }, "description": "Get user by user name", - "name": "getUserByName", "result_type": { "name": "User", "type": "named" @@ -583,7 +579,6 @@ } }, "description": "Logs user into the system", - "name": "loginUser", "result_type": { "name": "String", "type": "named" @@ -6231,7 +6226,6 @@ } }, "description": "DELETE /v1/accounts/{account}", - "name": "DeleteAccountsAccount", "result_type": { "name": "TreasuryInboundTransfer", "type": "named" @@ -6346,7 +6340,6 @@ } }, "description": "Creates a Session object.", - "name": "PostCheckoutSessions", "result_type": { "name": "Order", "type": "named" @@ -6358,7 +6351,10 @@ "method": "post", "servers": [ { - "url": "{{PET_STORE_SERVER_URL:-https://files.stripe.com/}}" + "url": { + "value": "https://files.stripe.com/", + "env": "PET_STORE_SERVER_URL" + } } ], "requestBody": { @@ -6424,7 +6420,6 @@ } }, "description": "POST /v1/files", - "name": "PostFiles", "result_type": { "name": "ApiResponse", "type": "named" @@ -6484,7 +6479,6 @@ } }, "description": "Transitions a test mode created InboundTransfer to the failed status. The InboundTransfer must already be in the processing state.", - "name": "PostTestHelpersTreasuryInboundTransfersIdFail", "result_type": { "name": "TreasuryInboundTransfer", "type": "named" @@ -6522,7 +6516,6 @@ } }, "description": "Add a new pet to the store", - "name": "addPet", "result_type": { "name": "Pet", "type": "named" @@ -6538,7 +6531,6 @@ }, "arguments": {}, "description": "Add snake object", - "name": "addSnake", "result_type": { "name": "SnakeObject", "type": "named" @@ -6568,7 +6560,6 @@ } }, "description": "POST /fine_tuning/jobs", - "name": "createFineTuningJob", "result_type": { "name": "CreateFineTuningJobRequest", "type": "named" @@ -6604,7 +6595,6 @@ } }, "description": "Creates list of users with given input array", - "name": "createUsersWithListInput", "result_type": { "name": "User", "type": "named" @@ -6638,7 +6628,6 @@ } }, "description": "Delete purchase order by ID", - "name": "deleteOrder", "result_type": { "type": "nullable", "underlying_type": { @@ -6701,7 +6690,6 @@ } }, "description": "Deletes a pet", - "name": "deletePet", "result_type": { "type": "nullable", "underlying_type": { @@ -6737,7 +6725,6 @@ } }, "description": "Delete user", - "name": "deleteUser", "result_type": { "type": "nullable", "underlying_type": { @@ -6773,7 +6760,6 @@ } }, "description": "Place an order for a pet", - "name": "placeOrder", "result_type": { "name": "Order", "type": "named" @@ -6811,7 +6797,6 @@ } }, "description": "Update an existing pet", - "name": "updatePet", "result_type": { "name": "Pet", "type": "named" @@ -6891,7 +6876,6 @@ } }, "description": "Updates a pet in the store with form data", - "name": "updatePetWithForm", "result_type": { "type": "nullable", "underlying_type": { @@ -6971,7 +6955,6 @@ } }, "description": "uploads an image", - "name": "uploadFile", "result_type": { "name": "ApiResponse", "type": "named" @@ -7039,7 +7022,6 @@ } }, "description": "POST /pet/multipart", - "name": "uploadPetMultipart", "result_type": { "name": "ApiResponse", "type": "named" diff --git a/ndc-rest-schema/openapi/testdata/prefix2/expected_multi_words.json b/ndc-rest-schema/openapi/testdata/prefix2/expected_multi_words.json index 69c1b92..b5af35c 100644 --- a/ndc-rest-schema/openapi/testdata/prefix2/expected_multi_words.json +++ b/ndc-rest-schema/openapi/testdata/prefix2/expected_multi_words.json @@ -3,15 +3,12 @@ "settings": { "servers": [ { - "url": "{{SERVER_URL:-https://jsonplaceholder.typicode.com}}" + "url": { + "env": "SERVER_URL", + "value": "https://jsonplaceholder.typicode.com" + } } ], - "timeout": "{{TIMEOUT}}", - "retry": { - "times": "{{RETRY_TIMES}}", - "delay": "{{RETRY_DELAY}}", - "httpStatus": "{{RETRY_HTTP_STATUS}}" - }, "version": "1.0.0" }, "functions": { @@ -37,9 +34,7 @@ "name": "id", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -56,9 +51,7 @@ "name": "userId", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -86,9 +79,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "id": { @@ -100,9 +91,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -115,9 +104,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "userId": { @@ -129,9 +116,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } } @@ -160,9 +145,7 @@ "rest": { "in": "body", "schema": { - "type": [ - "object" - ] + "type": ["object"] } } } diff --git a/ndc-rest-schema/openapi/testdata/prefix2/expected_single_word.json b/ndc-rest-schema/openapi/testdata/prefix2/expected_single_word.json index 576d18f..0247167 100644 --- a/ndc-rest-schema/openapi/testdata/prefix2/expected_single_word.json +++ b/ndc-rest-schema/openapi/testdata/prefix2/expected_single_word.json @@ -3,15 +3,12 @@ "settings": { "servers": [ { - "url": "{{SERVER_URL:-https://jsonplaceholder.typicode.com}}" + "url": { + "env": "SERVER_URL", + "value": "https://jsonplaceholder.typicode.com" + } } ], - "timeout": "{{TIMEOUT}}", - "retry": { - "times": "{{RETRY_TIMES}}", - "delay": "{{RETRY_DELAY}}", - "httpStatus": "{{RETRY_HTTP_STATUS}}" - }, "version": "1.0.0" }, "functions": { @@ -37,9 +34,7 @@ "name": "id", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -56,9 +51,7 @@ "name": "userId", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -86,9 +79,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "id": { @@ -100,9 +91,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -115,9 +104,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "userId": { @@ -129,9 +116,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } } @@ -160,9 +145,7 @@ "rest": { "in": "body", "schema": { - "type": [ - "object" - ] + "type": ["object"] } } } diff --git a/ndc-rest-schema/openapi/testdata/prefix3/expected_multi_words.json b/ndc-rest-schema/openapi/testdata/prefix3/expected_multi_words.json index ac0162c..e3476be 100644 --- a/ndc-rest-schema/openapi/testdata/prefix3/expected_multi_words.json +++ b/ndc-rest-schema/openapi/testdata/prefix3/expected_multi_words.json @@ -3,25 +3,26 @@ "settings": { "servers": [ { - "url": "{{SERVER_URL:-https://onesignal.com/api/v1}}" + "url": { + "env": "SERVER_URL", + "value": "https://onesignal.com/api/v1" + } } ], - "timeout": "{{TIMEOUT}}", - "retry": { - "times": "{{RETRY_TIMES}}", - "delay": "{{RETRY_DELAY}}", - "httpStatus": "{{RETRY_HTTP_STATUS}}" - }, "securitySchemes": { "app_key": { "type": "http", - "value": "{{APP_KEY_TOKEN}}", + "value": { + "env": "APP_KEY_TOKEN" + }, "header": "Authorization", "scheme": "bearer" }, "user_key": { "type": "http", - "value": "{{USER_KEY_TOKEN}}", + "value": { + "env": "USER_KEY_TOKEN" + }, "header": "Authorization", "scheme": "bearer" } @@ -53,9 +54,7 @@ "name": "app_id", "in": "query", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -72,9 +71,7 @@ "name": "kind", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -91,9 +88,7 @@ "name": "limit", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -110,9 +105,7 @@ "name": "offset", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -149,9 +142,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "id": { @@ -163,9 +154,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "recipients": { @@ -177,9 +166,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -196,9 +183,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "errored": { @@ -211,9 +196,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "failed": { @@ -226,9 +209,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "received": { @@ -241,9 +222,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "successful": { @@ -256,9 +235,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -272,9 +249,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "key": { @@ -287,9 +262,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "relation": { @@ -299,9 +272,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "value": { @@ -314,9 +285,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -332,9 +301,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "custom_data": { @@ -346,9 +313,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "data": { @@ -361,9 +326,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "filters": { @@ -378,9 +341,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "headings": { @@ -392,9 +353,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "id": { @@ -406,9 +365,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "send_after": { @@ -420,9 +377,7 @@ } }, "rest": { - "type": [ - "string" - ], + "type": ["string"], "format": "date-time" } }, @@ -435,9 +390,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -453,9 +406,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "notifications": { @@ -470,9 +421,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "offset": { @@ -484,9 +433,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "total_count": { @@ -498,9 +445,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -517,9 +462,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -532,9 +475,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "converted": { @@ -547,9 +488,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "errored": { @@ -562,9 +501,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "excluded_segments": { @@ -579,13 +516,9 @@ } }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -599,9 +532,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "filters": { @@ -616,9 +547,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "headings": { @@ -630,9 +559,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "id": { @@ -644,9 +571,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "include_player_ids": { @@ -661,13 +586,9 @@ } }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -683,13 +604,9 @@ } }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -705,9 +622,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "platform_delivery_stats": { @@ -720,9 +635,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "queued_at": { @@ -735,9 +648,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -751,9 +662,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "remaining": { @@ -766,9 +675,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "send_after": { @@ -781,9 +688,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -796,9 +701,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "successful": { @@ -811,9 +714,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "target_channel": { @@ -825,9 +726,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "throttle_rate_per_minute": { @@ -840,9 +739,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -855,9 +752,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "id": { @@ -866,9 +761,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "value": { @@ -877,9 +770,7 @@ "type": "named" }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -896,9 +787,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "chrome_web_push": { @@ -910,9 +799,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "edge_web_push": { @@ -924,9 +811,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "email": { @@ -938,9 +823,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "firefox_web_push": { @@ -952,9 +835,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "ios": { @@ -966,9 +847,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "safari_web_push": { @@ -980,9 +859,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "sms": { @@ -994,9 +871,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -1013,9 +888,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -1108,10 +981,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "sum", - "count" - ], + "one_of": ["sum", "count"], "type": "enum" } }, @@ -1119,11 +989,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "push", - "email", - "sms" - ], + "one_of": ["push", "email", "sms"], "type": "enum" } }, diff --git a/ndc-rest-schema/openapi/testdata/prefix3/expected_single_word.json b/ndc-rest-schema/openapi/testdata/prefix3/expected_single_word.json index e5965a4..c79deb3 100644 --- a/ndc-rest-schema/openapi/testdata/prefix3/expected_single_word.json +++ b/ndc-rest-schema/openapi/testdata/prefix3/expected_single_word.json @@ -3,25 +3,26 @@ "settings": { "servers": [ { - "url": "{{SERVER_URL:-https://onesignal.com/api/v1}}" + "url": { + "env": "SERVER_URL", + "value": "https://onesignal.com/api/v1" + } } ], - "timeout": "{{TIMEOUT}}", - "retry": { - "times": "{{RETRY_TIMES}}", - "delay": "{{RETRY_DELAY}}", - "httpStatus": "{{RETRY_HTTP_STATUS}}" - }, "securitySchemes": { "app_key": { "type": "http", - "value": "{{APP_KEY_TOKEN}}", + "value": { + "env": "APP_KEY_TOKEN" + }, "header": "Authorization", "scheme": "bearer" }, "user_key": { "type": "http", - "value": "{{USER_KEY_TOKEN}}", + "value": { + "env": "USER_KEY_TOKEN" + }, "header": "Authorization", "scheme": "bearer" } @@ -53,9 +54,7 @@ "name": "app_id", "in": "query", "schema": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -72,9 +71,7 @@ "name": "kind", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -91,9 +88,7 @@ "name": "limit", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } }, @@ -110,9 +105,7 @@ "name": "offset", "in": "query", "schema": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -149,9 +142,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "id": { @@ -163,9 +154,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "recipients": { @@ -177,9 +166,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -196,9 +183,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "errored": { @@ -211,9 +196,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "failed": { @@ -226,9 +209,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "received": { @@ -241,9 +222,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "successful": { @@ -256,9 +235,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -272,9 +249,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "key": { @@ -287,9 +262,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "relation": { @@ -299,9 +272,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "value": { @@ -314,9 +285,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -332,9 +301,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "custom_data": { @@ -346,9 +313,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "data": { @@ -361,9 +326,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "filters": { @@ -378,9 +341,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "headings": { @@ -392,9 +353,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "id": { @@ -406,9 +365,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "send_after": { @@ -420,9 +377,7 @@ } }, "rest": { - "type": [ - "string" - ], + "type": ["string"], "format": "date-time" } }, @@ -435,9 +390,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -453,9 +406,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "notifications": { @@ -470,9 +421,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "offset": { @@ -484,9 +433,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "total_count": { @@ -498,9 +445,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -517,9 +462,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -532,9 +475,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "converted": { @@ -547,9 +488,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "errored": { @@ -562,9 +501,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "excluded_segments": { @@ -579,13 +516,9 @@ } }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -599,9 +532,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "filters": { @@ -616,9 +547,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "headings": { @@ -630,9 +559,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "id": { @@ -644,9 +571,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "include_player_ids": { @@ -661,13 +586,9 @@ } }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -683,13 +604,9 @@ } }, "rest": { - "type": [ - "array" - ], + "type": ["array"], "items": { - "type": [ - "string" - ] + "type": ["string"] } } }, @@ -705,9 +622,7 @@ } }, "rest": { - "type": [ - "array" - ] + "type": ["array"] } }, "platform_delivery_stats": { @@ -720,9 +635,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "queued_at": { @@ -735,9 +648,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -751,9 +662,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "remaining": { @@ -766,9 +675,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "send_after": { @@ -781,9 +688,7 @@ } }, "rest": { - "type": [ - "integer" - ], + "type": ["integer"], "format": "int64" } }, @@ -796,9 +701,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "successful": { @@ -811,9 +714,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } }, "target_channel": { @@ -825,9 +726,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "throttle_rate_per_minute": { @@ -840,9 +739,7 @@ } }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -855,9 +752,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "id": { @@ -866,9 +761,7 @@ "type": "named" }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } }, "value": { @@ -877,9 +770,7 @@ "type": "named" }, "rest": { - "type": [ - "integer" - ] + "type": ["integer"] } } } @@ -896,9 +787,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "chrome_web_push": { @@ -910,9 +799,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "edge_web_push": { @@ -924,9 +811,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "email": { @@ -938,9 +823,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "firefox_web_push": { @@ -952,9 +835,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "ios": { @@ -966,9 +847,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "safari_web_push": { @@ -980,9 +859,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } }, "sms": { @@ -994,9 +871,7 @@ } }, "rest": { - "type": [ - "object" - ] + "type": ["object"] } } } @@ -1013,9 +888,7 @@ } }, "rest": { - "type": [ - "string" - ] + "type": ["string"] } } } @@ -1108,10 +981,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "sum", - "count" - ], + "one_of": ["sum", "count"], "type": "enum" } }, @@ -1119,11 +989,7 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": [ - "push", - "email", - "sms" - ], + "one_of": ["push", "email", "sms"], "type": "enum" } }, diff --git a/ndc-rest-schema/schema/auth.go b/ndc-rest-schema/schema/auth.go index 7f59c04..cc288ef 100644 --- a/ndc-rest-schema/schema/auth.go +++ b/ndc-rest-schema/schema/auth.go @@ -6,6 +6,7 @@ import ( "fmt" "slices" + "github.com/hasura/ndc-sdk-go/utils" "github.com/invopop/jsonschema" orderedmap "github.com/wk8/go-ordered-map/v2" ) @@ -112,11 +113,13 @@ func ParseAPIKeyLocation(value string) (APIKeyLocation, error) { // [OpenAPI 3]: https://swagger.io/docs/specification/authentication type SecurityScheme struct { Type SecuritySchemeType `json:"type" mapstructure:"type" yaml:"type"` - Value *EnvString `json:"value,omitempty" mapstructure:"value" yaml:"value,omitempty"` + Value *utils.EnvString `json:"value,omitempty" mapstructure:"value" yaml:"value,omitempty"` *APIKeyAuthConfig `yaml:",inline"` *HTTPAuthConfig `yaml:",inline"` *OAuth2Config `yaml:",inline"` *OpenIDConfig `yaml:",inline"` + + value *string } // JSONSchema is used to generate a custom jsonschema @@ -213,7 +216,7 @@ func (j *SecurityScheme) UnmarshalJSON(b []byte) error { } // Validate if the current instance is valid -func (ss SecurityScheme) Validate() error { +func (ss *SecurityScheme) Validate() error { if _, err := ParseSecuritySchemeType(string(ss.Type)); err != nil { return err } @@ -239,9 +242,34 @@ func (ss SecurityScheme) Validate() error { } return ss.OpenIDConfig.Validate() } + + if ss.Value != nil { + value, err := ss.Value.Get() + if err != nil { + return fmt.Errorf("SecurityScheme.Value: %w", err) + } + if value != "" { + ss.value = &value + } + } + return nil } +// GetValue get the authentication credential value +func (ss SecurityScheme) GetValue() string { + if ss.value != nil { + return *ss.value + } + + if ss.Value != nil { + value, _ := ss.Value.Get() + return value + } + + return "" +} + // APIKeyAuthConfig contains configurations for [apiKey authentication] // // [apiKey authentication]: https://swagger.io/docs/specification/authentication/api-keys/ diff --git a/ndc-rest-schema/schema/constant.go b/ndc-rest-schema/schema/constant.go new file mode 100644 index 0000000..edf128d --- /dev/null +++ b/ndc-rest-schema/schema/constant.go @@ -0,0 +1,9 @@ +package schema + +const ( + RESTOptionsArgumentName string = "restOptions" + RESTSingleOptionsObjectName string = "RestSingleOptions" + RESTDistributedOptionsObjectName string = "RestDistributedOptions" + RESTServerIDScalarName string = "RestServerId" + DistributedErrorObjectName string = "DistributedError" +) diff --git a/ndc-rest-schema/schema/env.go b/ndc-rest-schema/schema/env.go deleted file mode 100644 index 6b8480f..0000000 --- a/ndc-rest-schema/schema/env.go +++ /dev/null @@ -1,882 +0,0 @@ -package schema - -import ( - "encoding/json" - "fmt" - "os" - "regexp" - "slices" - "strconv" - "strings" - - "github.com/invopop/jsonschema" - "gopkg.in/yaml.v3" -) - -var envVariableRegex = regexp.MustCompile(`{{([A-Z0-9_]+)(:-([^}]*))?}}`) - -// EnvTemplate represents an environment variable template -type EnvTemplate struct { - Name string - DefaultValue *string -} - -// NewEnvTemplate creates an EnvTemplate without default value -func NewEnvTemplate(name string) EnvTemplate { - return EnvTemplate{ - Name: name, - } -} - -// NewEnvTemplateWithDefault creates an EnvTemplate with a default value -func NewEnvTemplateWithDefault(name string, defaultValue string) EnvTemplate { - return EnvTemplate{ - Name: name, - DefaultValue: &defaultValue, - } -} - -// IsEmpty checks if env template is empty -func (et EnvTemplate) IsEmpty() bool { - return et.Name == "" -} - -// Value returns the value which is retrieved from system or the default value if exist -func (et EnvTemplate) Value() (string, bool) { - value, ok := os.LookupEnv(et.Name) - if !ok && et.DefaultValue != nil { - return *et.DefaultValue, true - } - return value, ok -} - -// String implements the Stringer interface -func (et EnvTemplate) String() string { - if et.IsEmpty() { - return "" - } - if et.DefaultValue == nil { - return fmt.Sprintf("{{%s}}", et.Name) - } - return fmt.Sprintf("{{%s:-%s}}", et.Name, *et.DefaultValue) -} - -// MarshalJSON implements json.Marshaler. -func (j EnvTemplate) MarshalJSON() ([]byte, error) { - if j.IsEmpty() { - return json.Marshal(nil) - } - return json.Marshal(j.String()) -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *EnvTemplate) UnmarshalJSON(b []byte) error { - var rawValue string - if err := json.Unmarshal(b, &rawValue); err != nil { - return err - } - - value := FindEnvTemplate(rawValue) - if value != nil { - *j = *value - } - return nil -} - -// MarshalYAML implements yaml.Marshaler interface -func (j EnvTemplate) MarshalYAML() (any, error) { - if j.IsEmpty() { - return yaml.Marshal(nil) - } - return j.String(), nil -} - -// UnmarshalYAML implements yaml.Unmarshaler interface -func (j *EnvTemplate) UnmarshalYAML(node *yaml.Node) error { - if node.Value == "" { - return nil - } - value := FindEnvTemplate(node.Value) - if value != nil { - *j = *value - } - return nil -} - -// FindEnvTemplate finds one environment template from string -func FindEnvTemplate(input string) *EnvTemplate { - matches := envVariableRegex.FindStringSubmatch(input) - return parseEnvTemplateFromMatches(matches) -} - -// FindAllEnvTemplates finds all unique environment templates from string -func FindAllEnvTemplates(input string) []EnvTemplate { - matches := envVariableRegex.FindAllStringSubmatch(input, -1) - var results []EnvTemplate - for _, item := range matches { - env := parseEnvTemplateFromMatches(item) - if env == nil { - continue - } - doesExist := false - for _, result := range results { - if env.String() == result.String() { - doesExist = true - break - } - } - if !doesExist { - results = append(results, *env) - } - } - return results -} - -func parseEnvTemplateFromMatches(matches []string) *EnvTemplate { - if len(matches) != 4 { - return nil - } - result := &EnvTemplate{ - Name: matches[1], - } - - if matches[2] != "" { - result.DefaultValue = &matches[3] - } - return result -} - -// ReplaceEnvTemplates replaces env templates in the input string with values -func ReplaceEnvTemplates(input string, envTemplates []EnvTemplate) string { - for _, env := range envTemplates { - value, _ := env.Value() - input = strings.ReplaceAll(input, env.String(), value) - } - return input -} - -// EnvString implements the environment encoding and decoding value -type EnvString struct { - value *string - EnvTemplate -} - -// JSONSchema is used to generate a custom jsonschema -func (j EnvString) JSONSchema() *jsonschema.Schema { - return &jsonschema.Schema{ - Type: "string", - } -} - -// WithValue returns a new EnvString instance with new value -func (j EnvString) WithValue(value string) *EnvString { - j.value = &value - return &j -} - -// Value returns the value which is retrieved from system or the default value if exist -func (et *EnvString) Value() *string { - if et.value != nil { - v := *et.value - return &v - } - - strValue, ok := et.EnvTemplate.Value() - if !ok && strValue == "" { - return nil - } - - if ok { - et.value = &strValue - } - copyVal := strValue - return ©Val -} - -// Equal checks if the current value equals the target -func (et EnvString) Equal(target EnvString) bool { - srcValue := et.Value() - targetValue := target.Value() - - return (srcValue == nil && targetValue == nil) || - (srcValue != nil && targetValue != nil && *srcValue == *targetValue) -} - -// String implements the Stringer interface -func (et EnvString) String() string { - if et.IsEmpty() { - if et.value == nil { - return "" - } - return *et.value - } - return et.EnvTemplate.String() -} - -// MarshalJSON implements json.Marshaler. -func (j EnvString) MarshalJSON() ([]byte, error) { - if j.EnvTemplate.IsEmpty() { - return json.Marshal(j.value) - } - return j.EnvTemplate.MarshalJSON() -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *EnvString) UnmarshalJSON(b []byte) error { - var rawValue string - if err := json.Unmarshal(b, &rawValue); err != nil { - return err - } - - return j.unmarshalText(rawValue) -} - -// MarshalYAML implements yaml.Marshaler interface -func (j EnvString) MarshalYAML() (any, error) { - if j.EnvTemplate.IsEmpty() { - return j.value, nil - } - return j.EnvTemplate.MarshalYAML() -} - -// UnmarshalYAML implements yaml.Unmarshaler. -func (j *EnvString) UnmarshalYAML(node *yaml.Node) error { - if node.Value == "" { - return nil - } - return j.unmarshalText(node.Value) -} - -// UnmarshalText decodes the integer slice from string -func (j *EnvString) UnmarshalText(text []byte) error { - return j.unmarshalText(string(text)) -} - -func (j *EnvString) unmarshalText(rawValue string) error { - value := FindEnvTemplate(rawValue) - if value != nil { - j.EnvTemplate = *value - j.Value() - } else { - j.value = &rawValue - } - return nil -} - -// NewEnvStringValue creates an EnvString from value -func NewEnvStringValue(value string) *EnvString { - return &EnvString{ - value: &value, - } -} - -// NewEnvStringTemplate creates an EnvString from template -func NewEnvStringTemplate(template EnvTemplate) *EnvString { - return &EnvString{ - EnvTemplate: template, - } -} - -// EnvInt implements the integer environment encoder and decoder -type EnvInt struct { - value *int64 - EnvTemplate -} - -// NewEnvIntValue creates an EnvInt from value -func NewEnvIntValue(value int64) *EnvInt { - return &EnvInt{ - value: &value, - } -} - -// NewEnvIntTemplate creates an EnvInt from template -func NewEnvIntTemplate(template EnvTemplate) *EnvInt { - return &EnvInt{ - EnvTemplate: template, - } -} - -// JSONSchema is used to generate a custom jsonschema -func (j EnvInt) JSONSchema() *jsonschema.Schema { - return &jsonschema.Schema{ - OneOf: []*jsonschema.Schema{ - {Type: "integer"}, - {Type: "string"}, - }, - } -} - -// WithValue returns a new EnvInt instance with new value -func (j EnvInt) WithValue(value int64) *EnvInt { - j.value = &value - return &j -} - -// Equal checks if the current value equals the target -func (et EnvInt) Equal(target EnvInt) bool { - srcValue, err := et.Value() - if err != nil { - return false - } - targetValue, err := target.Value() - if err != nil { - return false - } - - return (srcValue == nil && targetValue == nil) || - (srcValue != nil && targetValue != nil && *srcValue == *targetValue) -} - -// String implements the Stringer interface -func (et EnvInt) String() string { - if et.IsEmpty() { - if et.value == nil { - return "" - } - return strconv.FormatInt(*et.value, 10) - } - return et.EnvTemplate.String() -} - -// MarshalJSON implements json.Marshaler. -func (j EnvInt) MarshalJSON() ([]byte, error) { - if j.EnvTemplate.IsEmpty() { - return json.Marshal(j.value) - } - return j.EnvTemplate.MarshalJSON() -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *EnvInt) UnmarshalJSON(b []byte) error { - var v int64 - if err := json.Unmarshal(b, &v); err == nil { - j.value = &v - return nil - } - - var rawValue string - if err := json.Unmarshal(b, &rawValue); err != nil { - return err - } - - return j.unmarshalText(rawValue) -} - -// MarshalYAML implements yaml.Marshaler interface -func (j EnvInt) MarshalYAML() (any, error) { - if j.EnvTemplate.IsEmpty() { - return j.value, nil - } - return j.EnvTemplate.MarshalYAML() -} - -// UnmarshalYAML implements yaml.Unmarshaler. -func (j *EnvInt) UnmarshalYAML(node *yaml.Node) error { - if node.Value == "" { - return nil - } - return j.unmarshalText(node.Value) -} - -// UnmarshalText decodes the integer slice from string -func (j *EnvInt) UnmarshalText(text []byte) error { - return j.unmarshalText(string(text)) -} - -func (j *EnvInt) unmarshalText(rawValue string) error { - value := FindEnvTemplate(rawValue) - if value != nil { - j.EnvTemplate = *value - _, err := j.Value() - return err - } - if rawValue != "" { - intValue, err := strconv.ParseInt(rawValue, 10, 64) - if err != nil { - return err - } - - j.value = &intValue - } - - return nil -} - -// Value returns the value which is retrieved from system or the default value if exist -func (et *EnvInt) Value() (*int64, error) { - if et.value != nil { - v := *et.value - return &v, nil - } - - strValue, ok := et.EnvTemplate.Value() - if !ok && strValue == "" { - return nil, nil - } - - intValue, err := strconv.ParseInt(strValue, 10, 64) - if err != nil { - return nil, err - } - - if ok { - et.value = &intValue - } - - copyVal := intValue - return ©Val, nil -} - -// EnvInts implements the integer environment encoder and decoder -type EnvInts struct { - value []int64 - EnvTemplate -} - -// NewEnvIntsValue creates EnvInts from value -func NewEnvIntsValue(value []int64) *EnvInts { - return &EnvInts{ - value: value, - } -} - -// NewEnvIntsTemplate creates EnvInts from template -func NewEnvIntsTemplate(template EnvTemplate) *EnvInts { - return &EnvInts{ - EnvTemplate: template, - } -} - -// JSONSchema is used to generate a custom jsonschema -func (j EnvInts) JSONSchema() *jsonschema.Schema { - return &jsonschema.Schema{ - OneOf: []*jsonschema.Schema{ - {Type: "string"}, - {Type: "array", Items: &jsonschema.Schema{Type: "integer"}}, - }, - } -} - -// WithValue returns a new EnvInts instance with new value -func (j EnvInts) WithValue(value []int64) *EnvInts { - j.value = value - return &j -} - -// Equal checks if the current value equals the target -func (et EnvInts) Equal(target EnvInts) bool { - srcValue, err := et.Value() - if err != nil { - return false - } - targetValue, err := target.Value() - if err != nil { - return false - } - - return slices.Equal(srcValue, targetValue) -} - -// String implements the Stringer interface -func (et EnvInts) String() string { - if et.IsEmpty() { - return fmt.Sprintf("%v", et.value) - } - return et.EnvTemplate.String() -} - -// MarshalJSON implements json.Marshaler. -func (j EnvInts) MarshalJSON() ([]byte, error) { - if j.EnvTemplate.IsEmpty() { - return json.Marshal(j.value) - } - return j.EnvTemplate.MarshalJSON() -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *EnvInts) UnmarshalJSON(b []byte) error { - var v []int64 - if err := json.Unmarshal(b, &v); err == nil { - j.value = v - return nil - } - - var rawValue string - if err := json.Unmarshal(b, &rawValue); err != nil { - return err - } - - return j.unmarshalText(rawValue) -} - -// MarshalYAML implements yaml.Marshaler. -func (j EnvInts) MarshalYAML() (any, error) { - if j.EnvTemplate.IsEmpty() { - return j.value, nil - } - return j.EnvTemplate.MarshalYAML() -} - -// UnmarshalYAML implements yaml.Unmarshaler. -func (j *EnvInts) UnmarshalYAML(node *yaml.Node) error { - if node.Value == "" { - return nil - } - - return j.unmarshalText(node.Value) -} - -// UnmarshalText decodes the integer slice from string -func (j *EnvInts) UnmarshalText(text []byte) error { - return j.unmarshalText(string(text)) -} - -func (j *EnvInts) unmarshalText(rawValue string) error { - value := FindEnvTemplate(rawValue) - if value != nil { - j.EnvTemplate = *value - _, err := j.Value() - return err - } - - if rawValue != "" { - intValues, err := parseIntsFromString(rawValue) - if err != nil { - return err - } - - j.value = intValues - } - - return nil -} - -// Value returns the value which is retrieved from system or the default value if exist -func (et *EnvInts) Value() ([]int64, error) { - if et.value != nil { - return et.value, nil - } - - strValue, ok := et.EnvTemplate.Value() - if !ok && strValue == "" { - return nil, nil - } - - intValues, err := parseIntsFromString(strValue) - if err != nil { - return nil, err - } - if ok { - et.value = intValues - } - - return intValues, nil -} - -func parseIntsFromString(input string) ([]int64, error) { - var intValues []int64 - for _, str := range strings.Split(input, ",") { - intValue, err := strconv.ParseInt(strings.TrimSpace(str), 10, 64) - if err != nil { - return nil, err - } - intValues = append(intValues, intValue) - } - - return intValues, nil -} - -// EnvBoolean implements the boolean environment encoder and decoder -type EnvBoolean struct { - value *bool - EnvTemplate -} - -// NewEnvBooleanValue creates an EnvBoolean from value -func NewEnvBooleanValue(value bool) *EnvBoolean { - return &EnvBoolean{ - value: &value, - } -} - -// NewEnvBooleanTemplate creates an EnvBoolean from template -func NewEnvBooleanTemplate(template EnvTemplate) *EnvBoolean { - return &EnvBoolean{ - EnvTemplate: template, - } -} - -// JSONSchema is used to generate a custom jsonschema -func (j EnvBoolean) JSONSchema() *jsonschema.Schema { - return &jsonschema.Schema{ - OneOf: []*jsonschema.Schema{ - {Type: "boolean"}, - {Type: "string"}, - }, - } -} - -// WithValue returns a new EnvBoolean instance with new value -func (j EnvBoolean) WithValue(value bool) *EnvBoolean { - j.value = &value - return &j -} - -// Equal checks if the current value equals the target -func (et EnvBoolean) Equal(target EnvBoolean) bool { - srcValue, err := et.Value() - if err != nil { - return false - } - targetValue, err := target.Value() - if err != nil { - return false - } - - return (srcValue == nil && targetValue == nil) || - (srcValue != nil && targetValue != nil && *srcValue == *targetValue) -} - -// String implements the Stringer interface -func (et EnvBoolean) String() string { - if et.IsEmpty() { - if et.value == nil { - return "" - } - return strconv.FormatBool(*et.value) - } - return et.EnvTemplate.String() -} - -// MarshalJSON implements json.Marshaler. -func (j EnvBoolean) MarshalJSON() ([]byte, error) { - if j.EnvTemplate.IsEmpty() { - return json.Marshal(j.value) - } - return j.EnvTemplate.MarshalJSON() -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *EnvBoolean) UnmarshalJSON(b []byte) error { - var v bool - if err := json.Unmarshal(b, &v); err == nil { - j.value = &v - return nil - } - - var rawValue string - if err := json.Unmarshal(b, &rawValue); err != nil { - return err - } - - return j.unmarshalText(rawValue) -} - -// UnmarshalText decodes boolean from string -func (j *EnvBoolean) UnmarshalText(text []byte) error { - return j.unmarshalText(string(text)) -} - -func (j *EnvBoolean) unmarshalText(rawValue string) error { - value := FindEnvTemplate(rawValue) - if value != nil { - j.EnvTemplate = *value - _, err := j.Value() - return err - } - if rawValue != "" { - boolValue, err := strconv.ParseBool(rawValue) - if err != nil { - return err - } - - j.value = &boolValue - } - - return nil -} - -// MarshalYAML implements yaml.Marshaler interface -func (j EnvBoolean) MarshalYAML() (any, error) { - if j.EnvTemplate.IsEmpty() { - return j.value, nil - } - return j.EnvTemplate.MarshalYAML() -} - -// UnmarshalYAML implements yaml.Unmarshaler. -func (j *EnvBoolean) UnmarshalYAML(node *yaml.Node) error { - if node.Value == "" { - return nil - } - return j.unmarshalText(node.Value) -} - -// Value returns the value which is retrieved from system or the default value if exist -func (et *EnvBoolean) Value() (*bool, error) { - if et.value != nil { - v := *et.value - return &v, nil - } - - strValue, ok := et.EnvTemplate.Value() - if !ok && strValue == "" { - return nil, nil - } - - boolValue, err := strconv.ParseBool(strValue) - if err != nil { - return nil, err - } - - if ok { - et.value = &boolValue - } - - copyVal := boolValue - return ©Val, nil -} - -// EnvStrings implements the string slice environment encoder and decoder -type EnvStrings struct { - value []string - EnvTemplate -} - -// NewEnvStringsValue creates EnvStrings from value -func NewEnvStringsValue(value []string) *EnvStrings { - return &EnvStrings{ - value: value, - } -} - -// NewEnvStringsTemplate creates EnvStrings from template -func NewEnvStringsTemplate(template EnvTemplate) *EnvStrings { - return &EnvStrings{ - EnvTemplate: template, - } -} - -// JSONSchema is used to generate a custom jsonschema -func (j EnvStrings) JSONSchema() *jsonschema.Schema { - return &jsonschema.Schema{ - OneOf: []*jsonschema.Schema{ - {Type: "string"}, - {Type: "array", Items: &jsonschema.Schema{Type: "string"}}, - }, - } -} - -// WithValue returns a new EnvStrings instance with new value -func (j EnvStrings) WithValue(value []string) *EnvStrings { - j.value = value - return &j -} - -// Equal checks if the current value equals the target -func (et EnvStrings) Equal(target EnvStrings) bool { - srcValue, err := et.Value() - if err != nil { - return false - } - targetValue, err := target.Value() - if err != nil { - return false - } - - return slices.Equal(srcValue, targetValue) -} - -// String implements the Stringer interface -func (et EnvStrings) String() string { - if et.IsEmpty() { - return fmt.Sprintf("%v", et.value) - } - return et.EnvTemplate.String() -} - -// MarshalJSON implements json.Marshaler. -func (j EnvStrings) MarshalJSON() ([]byte, error) { - if j.EnvTemplate.IsEmpty() { - return json.Marshal(j.value) - } - return j.EnvTemplate.MarshalJSON() -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *EnvStrings) UnmarshalJSON(b []byte) error { - var v []string - if err := json.Unmarshal(b, &v); err == nil { - j.value = v - return nil - } - - var rawValue string - if err := json.Unmarshal(b, &rawValue); err != nil { - return err - } - - return j.unmarshalText(rawValue) -} - -// MarshalYAML implements yaml.Marshaler. -func (j EnvStrings) MarshalYAML() (any, error) { - if j.EnvTemplate.IsEmpty() { - return j.value, nil - } - return j.EnvTemplate.MarshalYAML() -} - -// UnmarshalYAML implements yaml.Unmarshaler. -func (j *EnvStrings) UnmarshalYAML(node *yaml.Node) error { - if node.Value == "" { - return nil - } - - return j.unmarshalText(node.Value) -} - -// UnmarshalText decodes the integer slice from string -func (j *EnvStrings) UnmarshalText(text []byte) error { - return j.unmarshalText(string(text)) -} - -func (j *EnvStrings) unmarshalText(rawValue string) error { - value := FindEnvTemplate(rawValue) - if value != nil { - j.EnvTemplate = *value - _, err := j.Value() - return err - } - - if rawValue != "" { - values := strings.Split(rawValue, ",") - j.value = make([]string, len(values)) - for i, v := range values { - j.value[i] = strings.TrimSpace(v) - } - } else { - j.value = []string{} - } - - return nil -} - -// Value returns the value which is retrieved from system or the default value if exist -func (et *EnvStrings) Value() ([]string, error) { - if et.value != nil { - return et.value, nil - } - - strValue, ok := et.EnvTemplate.Value() - if !ok && strValue == "" { - return nil, nil - } - - err := et.unmarshalText(strValue) - if err != nil { - return nil, err - } - return et.value, nil -} diff --git a/ndc-rest-schema/schema/env_test.go b/ndc-rest-schema/schema/env_test.go deleted file mode 100644 index 8873d0a..0000000 --- a/ndc-rest-schema/schema/env_test.go +++ /dev/null @@ -1,402 +0,0 @@ -package schema - -import ( - "encoding/json" - "fmt" - "strings" - "testing" - - "github.com/hasura/ndc-sdk-go/utils" - "gopkg.in/yaml.v3" - "gotest.tools/v3/assert" -) - -func TestEnvTemplate(t *testing.T) { - testCases := []struct { - input string - expected string - templateStr string - templates []EnvTemplate - }{ - {}, - { - input: "http://localhost:8080", - expected: "http://localhost:8080", - }, - { - input: "{{SERVER_URL}}", - templates: []EnvTemplate{ - NewEnvTemplate("SERVER_URL"), - }, - templateStr: "{{SERVER_URL}}", - expected: "", - }, - { - input: "{{SERVER_URL:-http://localhost:8080}}", - templates: []EnvTemplate{ - NewEnvTemplateWithDefault("SERVER_URL", "http://localhost:8080"), - }, - templateStr: "{{SERVER_URL:-http://localhost:8080}}", - expected: "http://localhost:8080", - }, - { - input: "{{SERVER_URL:-}}", - templates: []EnvTemplate{ - { - Name: "SERVER_URL", - DefaultValue: utils.ToPtr(""), - }, - }, - templateStr: "{{SERVER_URL:-}}", - expected: "", - }, - { - input: "{{SERVER_URL:-http://localhost:8080}},{{SERVER_URL:-http://localhost:8080}},{{SERVER_URL}}", - templates: []EnvTemplate{ - { - Name: "SERVER_URL", - DefaultValue: utils.ToPtr("http://localhost:8080"), - }, - { - Name: "SERVER_URL", - }, - }, - templateStr: "{{SERVER_URL:-http://localhost:8080}},{{SERVER_URL}}", - expected: "http://localhost:8080,http://localhost:8080,", - }, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - tmpl := FindEnvTemplate(tc.input) - if len(tc.templates) == 0 { - if tmpl != nil { - t.Errorf("failed to find env template, expected nil, got %s", tmpl) - } - } else { - assert.DeepEqual(t, tc.templates[0].String(), tmpl.String()) - - var jTemplate EnvTemplate - if err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, tc.input)), &jTemplate); err != nil { - t.Errorf("failed to unmarshal template from json: %s", err) - t.FailNow() - } - assert.DeepEqual(t, jTemplate, *tmpl) - bs, err := json.Marshal(jTemplate) - if err != nil { - t.Errorf("failed to marshal template from json: %s", err) - t.FailNow() - } - assert.DeepEqual(t, tmpl.String(), strings.Trim(string(bs), `"`)) - - if err := yaml.Unmarshal([]byte(fmt.Sprintf(`"%s"`, tc.input)), &jTemplate); err != nil { - t.Errorf("failed to unmarshal template from yaml: %s", err) - t.FailNow() - } - assert.DeepEqual(t, jTemplate, *tmpl) - bs, err = yaml.Marshal(jTemplate) - if err != nil { - t.Errorf("failed to marshal template from yaml: %s", err) - t.FailNow() - } - assert.DeepEqual(t, tmpl.String(), strings.TrimSpace(strings.ReplaceAll(string(bs), "'", ""))) - } - - templates := FindAllEnvTemplates(tc.input) - assert.DeepEqual(t, tc.templates, templates) - templateStrings := []string{} - for i, item := range templates { - assert.DeepEqual(t, tc.templates[i].String(), item.String()) - templateStrings = append(templateStrings, item.String()) - } - assert.DeepEqual(t, tc.expected, ReplaceEnvTemplates(tc.input, templates)) - if len(templateStrings) > 0 { - assert.DeepEqual(t, tc.templateStr, strings.Join(templateStrings, ",")) - } - }) - } -} - -func TestEnvString(t *testing.T) { - testCases := []struct { - input string - expected EnvString - }{ - { - input: `"{{FOO:-bar}}"`, - expected: *NewEnvStringTemplate(EnvTemplate{ - Name: "FOO", - DefaultValue: utils.ToPtr("bar"), - }), - }, - { - input: `"baz"`, - expected: *NewEnvStringValue("baz"), - }, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - var result EnvString - if err := yaml.Unmarshal([]byte(tc.input), &result); err != nil { - t.Error(t, err) - t.FailNow() - } - assert.DeepEqual(t, tc.expected.EnvTemplate, result.EnvTemplate) - assert.DeepEqual(t, strings.Trim(tc.input, "\""), tc.expected.String()) - bs, err := yaml.Marshal(result) - if err != nil { - t.Fatal(t, err) - } - assert.DeepEqual(t, strings.Trim(tc.input, `"`), strings.TrimSpace(strings.ReplaceAll(string(bs), "'", ""))) - result.JSONSchema() - }) - } -} - -func TestEnvInt(t *testing.T) { - testCases := []struct { - input string - expected EnvInt - }{ - { - input: `400`, - expected: EnvInt{value: utils.ToPtr[int64](400)}, - }, - { - input: `"400"`, - expected: *EnvInt{}.WithValue(400), - }, - { - input: `"{{FOO:-401}}"`, - expected: EnvInt{ - value: utils.ToPtr(int64(401)), - EnvTemplate: EnvTemplate{ - Name: "FOO", - DefaultValue: utils.ToPtr("401"), - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - var result EnvInt - if err := json.Unmarshal([]byte(tc.input), &result); err != nil { - t.Error(t, err) - t.FailNow() - } - assert.DeepEqual(t, tc.expected.EnvTemplate, result.EnvTemplate) - assert.DeepEqual(t, tc.expected.value, result.value) - - if err := yaml.Unmarshal([]byte(tc.input), &result); err != nil { - t.Error(t, err) - t.FailNow() - } - assert.DeepEqual(t, tc.expected.EnvTemplate, result.EnvTemplate) - assert.DeepEqual(t, tc.expected.value, result.value) - assert.DeepEqual(t, strings.Trim(tc.input, "\""), tc.expected.String()) - bs, err := yaml.Marshal(result) - if err != nil { - t.Fatal(t, err) - } - assert.DeepEqual(t, strings.Trim(tc.input, `"`), strings.TrimSpace(strings.ReplaceAll(string(bs), "'", ""))) - result.JSONSchema() - }) - } -} - -func TestEnvInts(t *testing.T) { - testCases := []struct { - input string - expected EnvInts - expectedYaml string - }{ - { - input: `[400, 401, 403]`, - expected: EnvInts{value: []int64{400, 401, 403}}, - expectedYaml: `- 400 -- 401 -- 403`, - }, - { - input: `"400, 401, 403"`, - expected: *NewEnvIntsValue(nil).WithValue([]int64{400, 401, 403}), - expectedYaml: `- 400 -- 401 -- 403`, - }, - { - input: `"{{FOO:-400, 401, 403}}"`, - expected: EnvInts{ - value: []int64{400, 401, 403}, - EnvTemplate: EnvTemplate{ - Name: "FOO", - DefaultValue: utils.ToPtr("400, 401, 403"), - }, - }, - expectedYaml: `{{FOO:-400, 401, 403}}`, - }, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - var result EnvInts - if err := json.Unmarshal([]byte(tc.input), &result); err != nil { - t.Error(t, err) - t.FailNow() - } - assert.DeepEqual(t, tc.expected.EnvTemplate, result.EnvTemplate) - assert.DeepEqual(t, tc.expected.value, result.value) - - if err := yaml.Unmarshal([]byte(tc.input), &result); err != nil { - t.Error(t, err) - t.FailNow() - } - assert.DeepEqual(t, tc.expected.String(), result.String()) - assert.DeepEqual(t, tc.expected.value, result.value) - bs, err := yaml.Marshal(result) - if err != nil { - t.Fatal(t, err) - } - assert.DeepEqual(t, tc.expectedYaml, strings.TrimSpace(strings.ReplaceAll(string(bs), "'", ""))) - result.JSONSchema() - }) - } -} - -func TestEnvBoolean(t *testing.T) { - t.Setenv("TEST_BOOL", "false") - testCases := []struct { - input string - expected EnvBoolean - }{ - { - input: `false`, - expected: *NewEnvBooleanValue(false), - }, - { - input: `"true"`, - expected: *NewEnvBooleanValue(true), - }, - { - input: `"{{FOO:-true}}"`, - expected: EnvBoolean{ - value: utils.ToPtr(true), - EnvTemplate: EnvTemplate{ - Name: "FOO", - DefaultValue: utils.ToPtr("true"), - }, - }, - }, - { - input: fmt.Sprintf(`"%s"`, NewEnvBooleanTemplate(EnvTemplate{ - Name: "TEST_BOOL", - }).String()), - expected: EnvBoolean{ - value: utils.ToPtr(false), - EnvTemplate: EnvTemplate{ - Name: "TEST_BOOL", - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - var result EnvBoolean - if err := json.Unmarshal([]byte(tc.input), &result); err != nil { - t.Error(t, err) - t.FailNow() - } - assert.DeepEqual(t, tc.expected.EnvTemplate, result.EnvTemplate) - assert.DeepEqual(t, tc.expected.value, result.value) - - if err := yaml.Unmarshal([]byte(tc.input), &result); err != nil { - t.Error(t, err) - t.FailNow() - } - assert.DeepEqual(t, tc.expected.EnvTemplate, result.EnvTemplate) - assert.DeepEqual(t, tc.expected.value, result.value) - assert.DeepEqual(t, strings.Trim(tc.input, "\""), tc.expected.String()) - bs, err := yaml.Marshal(result) - if err != nil { - t.Fatal(t, err) - } - assert.DeepEqual(t, strings.Trim(tc.input, `"`), strings.TrimSpace(strings.ReplaceAll(string(bs), "'", ""))) - result.JSONSchema() - if err = (&EnvBoolean{}).UnmarshalText([]byte(strings.Trim(tc.input, `"`))); err != nil { - t.Error(t, err) - t.FailNow() - } - }) - } -} - -func TestEnvStrings(t *testing.T) { - t.Setenv("TEST_STRINGS", "a,b,c") - testCases := []struct { - input string - expected EnvStrings - expectedYaml string - }{ - { - input: `["foo", "bar"]`, - expected: *NewEnvStringsValue([]string{"foo", "bar"}), - expectedYaml: `- foo -- bar`, - }, - { - input: `"foo, baz"`, - expected: *NewEnvStringsValue(nil).WithValue([]string{"foo", "baz"}), - expectedYaml: `- foo -- baz`, - }, - { - input: fmt.Sprintf(`"%s"`, NewEnvStringsTemplate(NewEnvTemplate("TEST_STRINGS")).String()), - expected: EnvStrings{ - value: []string{"a", "b", "c"}, - EnvTemplate: EnvTemplate{ - Name: "TEST_STRINGS", - }, - }, - expectedYaml: `{{TEST_STRINGS}}`, - }, - { - input: `"{{FOO:-foo, bar}}"`, - expected: EnvStrings{ - value: []string{"foo", "bar"}, - EnvTemplate: EnvTemplate{ - Name: "FOO", - DefaultValue: utils.ToPtr("foo, bar"), - }, - }, - expectedYaml: `{{FOO:-foo, bar}}`, - }, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - var result EnvStrings - if err := json.Unmarshal([]byte(tc.input), &result); err != nil { - t.Error(t, err) - t.FailNow() - } - assert.DeepEqual(t, tc.expected.EnvTemplate, result.EnvTemplate) - assert.DeepEqual(t, tc.expected.value, result.value) - - if err := yaml.Unmarshal([]byte(tc.input), &result); err != nil { - t.Error(t, err) - t.FailNow() - } - assert.DeepEqual(t, tc.expected.String(), result.String()) - assert.DeepEqual(t, tc.expected.value, result.value) - bs, err := yaml.Marshal(result) - if err != nil { - t.Fatal(t, err) - } - assert.DeepEqual(t, tc.expectedYaml, strings.TrimSpace(strings.ReplaceAll(string(bs), "'", ""))) - result.JSONSchema() - }) - } -} diff --git a/ndc-rest-schema/schema/schema.go b/ndc-rest-schema/schema/schema.go index 801509a..89fc2ac 100644 --- a/ndc-rest-schema/schema/schema.go +++ b/ndc-rest-schema/schema/schema.go @@ -94,34 +94,38 @@ type Response struct { ContentType string `json:"contentType" mapstructure:"contentType" yaml:"contentType"` } +// RuntimeSettings contain runtime settings for a server +type RuntimeSettings struct { // configure the request timeout in seconds, default 30s + Timeout uint `json:"timeout,omitempty" mapstructure:"timeout" yaml:"timeout,omitempty"` + Retry RetryPolicy `json:"retry,omitempty" mapstructure:"retry" yaml:"retry,omitempty"` +} + // Request represents the HTTP request information of the webhook type Request struct { - URL string `json:"url,omitempty" mapstructure:"url" yaml:"url,omitempty"` - Method string `json:"method,omitempty" jsonschema:"enum=get,enum=post,enum=put,enum=patch,enum=delete" mapstructure:"method" yaml:"method,omitempty"` - Type RequestType `json:"type,omitempty" mapstructure:"type" yaml:"type,omitempty"` - Headers map[string]EnvString `json:"headers,omitempty" mapstructure:"headers" yaml:"headers,omitempty"` - Security AuthSecurities `json:"security,omitempty" mapstructure:"security" yaml:"security,omitempty"` - // configure the request timeout in seconds, default 30s - Timeout uint `json:"timeout,omitempty" mapstructure:"timeout" yaml:"timeout,omitempty"` - Servers []ServerConfig `json:"servers,omitempty" mapstructure:"servers" yaml:"servers,omitempty"` - RequestBody *RequestBody `json:"requestBody,omitempty" mapstructure:"requestBody" yaml:"requestBody,omitempty"` - Response Response `json:"response" mapstructure:"response" yaml:"response"` - Retry *RetryPolicy `json:"retry,omitempty" mapstructure:"retry" yaml:"retry,omitempty"` + URL string `json:"url,omitempty" mapstructure:"url" yaml:"url,omitempty"` + Method string `json:"method,omitempty" jsonschema:"enum=get,enum=post,enum=put,enum=patch,enum=delete" mapstructure:"method" yaml:"method,omitempty"` + Type RequestType `json:"type,omitempty" mapstructure:"type" yaml:"type,omitempty"` + Headers map[string]utils.EnvString `json:"headers,omitempty" mapstructure:"headers" yaml:"headers,omitempty"` + Security AuthSecurities `json:"security,omitempty" mapstructure:"security" yaml:"security,omitempty"` + Servers []ServerConfig `json:"servers,omitempty" mapstructure:"servers" yaml:"servers,omitempty"` + RequestBody *RequestBody `json:"requestBody,omitempty" mapstructure:"requestBody" yaml:"requestBody,omitempty"` + Response Response `json:"response" mapstructure:"response" yaml:"response"` + + *RuntimeSettings `yaml:",inline"` } // Clone copies this instance to a new one func (r Request) Clone() *Request { return &Request{ - URL: r.URL, - Method: r.Method, - Type: r.Type, - Headers: r.Headers, - Timeout: r.Timeout, - Retry: r.Retry, - Security: r.Security, - Servers: r.Servers, - RequestBody: r.RequestBody, - Response: r.Response, + URL: r.URL, + Method: r.Method, + Type: r.Type, + Headers: r.Headers, + Security: r.Security, + Servers: r.Servers, + RequestBody: r.RequestBody, + Response: r.Response, + RuntimeSettings: r.RuntimeSettings, } } diff --git a/ndc-rest-schema/schema/setting.go b/ndc-rest-schema/schema/setting.go index c84bb6a..7dda824 100644 --- a/ndc-rest-schema/schema/setting.go +++ b/ndc-rest-schema/schema/setting.go @@ -6,18 +6,19 @@ import ( "fmt" "net/url" "strings" + + "github.com/hasura/ndc-sdk-go/utils" ) // NDCRestSettings represent global settings of the REST API, including base URL, headers, etc... type NDCRestSettings struct { - Servers []ServerConfig `json:"servers" mapstructure:"servers" yaml:"servers"` - Headers map[string]EnvString `json:"headers,omitempty" mapstructure:"headers" yaml:"headers,omitempty"` - // configure the request timeout in seconds, default 30s - Timeout *EnvInt `json:"timeout,omitempty" mapstructure:"timeout" yaml:"timeout,omitempty"` - Retry *RetryPolicySetting `json:"retry,omitempty" mapstructure:"retry" yaml:"retry,omitempty"` - SecuritySchemes map[string]SecurityScheme `json:"securitySchemes,omitempty" mapstructure:"securitySchemes" yaml:"securitySchemes,omitempty"` - Security AuthSecurities `json:"security,omitempty" mapstructure:"security" yaml:"security,omitempty"` - Version string `json:"version,omitempty" mapstructure:"version" yaml:"version,omitempty"` + Servers []ServerConfig `json:"servers" mapstructure:"servers" yaml:"servers"` + Headers map[string]utils.EnvString `json:"headers,omitempty" mapstructure:"headers" yaml:"headers,omitempty"` + SecuritySchemes map[string]SecurityScheme `json:"securitySchemes,omitempty" mapstructure:"securitySchemes" yaml:"securitySchemes,omitempty"` + Security AuthSecurities `json:"security,omitempty" mapstructure:"security" yaml:"security,omitempty"` + Version string `json:"version,omitempty" mapstructure:"version" yaml:"version,omitempty"` + + headers map[string]string } // UnmarshalJSON implements json.Unmarshaler. @@ -30,16 +31,14 @@ func (j *NDCRestSettings) UnmarshalJSON(b []byte) error { } result := NDCRestSettings(raw) + _ = result.Validate() - if err := result.Validate(); err != nil { - return err - } *j = result return nil } // Validate if the current instance is valid -func (rs NDCRestSettings) Validate() error { +func (rs *NDCRestSettings) Validate() error { for _, server := range rs.Servers { if err := server.Validate(); err != nil { return err @@ -52,82 +51,107 @@ func (rs NDCRestSettings) Validate() error { } } - if rs.Retry != nil { - if err := rs.Retry.Validate(); err != nil { - return fmt.Errorf("retry: %w", err) - } + headers, err := getHeadersFromEnv(rs.Headers) + if err != nil { + return err } + rs.headers = headers + return nil } -// RetryPolicySetting represents retry policy settings -type RetryPolicySetting struct { - // Number of retry times - Times EnvInt `json:"times,omitempty" mapstructure:"times" yaml:"times,omitempty"` - // Delay retry delay in milliseconds - Delay EnvInt `json:"delay,omitempty" mapstructure:"delay" yaml:"delay,omitempty"` - // HTTPStatus retries if the remote service returns one of these http status - HTTPStatus EnvInts `json:"httpStatus,omitempty" mapstructure:"httpStatus" yaml:"httpStatus,omitempty"` +// Validate if the current instance is valid +func (rs NDCRestSettings) GetHeaders() map[string]string { + if rs.headers != nil { + return rs.headers + } + + return getHeadersFromEnvUnsafe(rs.Headers) +} + +// ServerConfig contains server configurations +type ServerConfig struct { + URL utils.EnvString `json:"url" mapstructure:"url" yaml:"url"` + ID string `json:"id,omitempty" mapstructure:"id" yaml:"id,omitempty"` + Headers map[string]utils.EnvString `json:"headers,omitempty" mapstructure:"headers" yaml:"headers,omitempty"` + SecuritySchemes map[string]SecurityScheme `json:"securitySchemes,omitempty" mapstructure:"securitySchemes" yaml:"securitySchemes,omitempty"` + Security AuthSecurities `json:"security,omitempty" mapstructure:"security" yaml:"security,omitempty"` + TLS *TLSConfig `json:"tls,omitempty" mapstructure:"tls" yaml:"tls,omitempty"` + + // cached values that are loaded from environment variables + url *url.URL + headers map[string]string +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *ServerConfig) UnmarshalJSON(b []byte) error { + type Plain ServerConfig + + var raw Plain + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + + result := ServerConfig(raw) + _ = result.Validate() + + *j = result + + return nil } // Validate if the current instance is valid -func (rs RetryPolicySetting) Validate() error { - times, err := rs.Times.Value() +func (ss *ServerConfig) Validate() error { + rawURL, err := ss.URL.Get() if err != nil { - return err + return fmt.Errorf("server url: %w", err) } - if times != nil && *times < 0 { - return errors.New("retry policy times must be positive") + + if rawURL == "" { + return errors.New("url is required for server") } - delay, err := rs.Times.Value() + urlValue, err := parseHttpURL(rawURL) if err != nil { - return err - } - if delay != nil && *delay < 0 { - return errors.New("retry delay must be larger than 0") + return fmt.Errorf("server url: %w", err) } - httpStatus, err := rs.HTTPStatus.Value() + ss.url = urlValue + + headers, err := getHeadersFromEnv(ss.Headers) if err != nil { return err } - for _, status := range httpStatus { - if status < 400 || status >= 600 { - return errors.New("retry http status must be in between 400 and 599") - } - } + ss.headers = headers return nil } -// ServerConfig contains server configurations -type ServerConfig struct { - URL EnvString `json:"url" mapstructure:"url" yaml:"url"` - ID string `json:"id,omitempty" mapstructure:"id" yaml:"id,omitempty"` - Headers map[string]EnvString `json:"headers,omitempty" mapstructure:"headers" yaml:"headers,omitempty"` - // configure the request timeout in seconds, default 30s - Timeout *EnvInt `json:"timeout,omitempty" mapstructure:"timeout" yaml:"timeout,omitempty"` - Retry *RetryPolicySetting `json:"retry,omitempty" mapstructure:"retry" yaml:"retry,omitempty"` - SecuritySchemes map[string]SecurityScheme `json:"securitySchemes,omitempty" mapstructure:"securitySchemes" yaml:"securitySchemes,omitempty"` - Security AuthSecurities `json:"security,omitempty" mapstructure:"security" yaml:"security,omitempty"` - TLS *TLSConfig `json:"tls,omitempty" mapstructure:"tls" yaml:"tls,omitempty"` +// Validate if the current instance is valid +func (ss ServerConfig) GetURL() (url.URL, error) { + if ss.url != nil { + return *ss.url, nil + } + + rawURL, err := ss.URL.Get() + if err != nil { + return url.URL{}, err + } + urlValue, err := parseHttpURL(rawURL) + if err != nil { + return url.URL{}, fmt.Errorf("server url: %w", err) + } + + return *urlValue, nil } // Validate if the current instance is valid -func (ss ServerConfig) Validate() error { - urlValue := ss.URL.Value() - if urlValue == nil || *urlValue == "" { - if ss.URL.EnvTemplate.IsEmpty() { - return errors.New("url is required for server") - } - return nil +func (ss ServerConfig) GetHeaders() map[string]string { + if ss.headers != nil { + return ss.headers } - if _, err := parseHttpURL(*urlValue); err != nil { - return fmt.Errorf("server url: %w", err) - } - return nil + return getHeadersFromEnvUnsafe(ss.Headers) } // parseHttpURL parses and validate if the URL has HTTP scheme @@ -149,35 +173,62 @@ func parseRelativeOrHttpURL(input string) (*url.URL, error) { // TLSConfig represents the transport layer security (LTS) configuration for the mutualTLS authentication type TLSConfig struct { // Path to the TLS cert to use for TLS required connections. - CertFile *EnvString `json:"certFile,omitempty" mapstructure:"certFile" yaml:"certFile,omitempty"` + CertFile *utils.EnvString `json:"certFile,omitempty" mapstructure:"certFile" yaml:"certFile,omitempty"` // Alternative to cert_file. Provide the certificate contents as a string instead of a filepath. - CertPem *EnvString `json:"certPem,omitempty" mapstructure:"certPem" yaml:"certPem,omitempty"` + CertPem *utils.EnvString `json:"certPem,omitempty" mapstructure:"certPem" yaml:"certPem,omitempty"` // Path to the TLS key to use for TLS required connections. - KeyFile *EnvString `json:"keyFile,omitempty" mapstructure:"keyFile" yaml:"keyFile,omitempty"` + KeyFile *utils.EnvString `json:"keyFile,omitempty" mapstructure:"keyFile" yaml:"keyFile,omitempty"` // Alternative to key_file. Provide the key contents as a string instead of a filepath. - KeyPem *EnvString `json:"keyPem,omitempty" mapstructure:"keyPem" yaml:"keyPem,omitempty"` + KeyPem *utils.EnvString `json:"keyPem,omitempty" mapstructure:"keyPem" yaml:"keyPem,omitempty"` // Path to the CA cert. For a client this verifies the server certificate. For a server this verifies client certificates. // If empty uses system root CA. - CAFile *EnvString `json:"caFile,omitempty" mapstructure:"caFile" yaml:"caFile,omitempty"` + CAFile *utils.EnvString `json:"caFile,omitempty" mapstructure:"caFile" yaml:"caFile,omitempty"` // Alternative to ca_file. Provide the CA cert contents as a string instead of a filepath. - CAPem *EnvString `json:"caPem,omitempty" mapstructure:"caPem" yaml:"caPem,omitempty"` + CAPem *utils.EnvString `json:"caPem,omitempty" mapstructure:"caPem" yaml:"caPem,omitempty"` // Additionally you can configure TLS to be enabled but skip verifying the server's certificate chain. - InsecureSkipVerify *EnvBoolean `json:"insecureSkipVerify,omitempty" mapstructure:"insecureSkipVerify" yaml:"insecureSkipVerify,omitempty"` + InsecureSkipVerify *utils.EnvBool `json:"insecureSkipVerify,omitempty" mapstructure:"insecureSkipVerify" yaml:"insecureSkipVerify,omitempty"` // Whether to load the system certificate authorities pool alongside the certificate authority. - IncludeSystemCACertsPool *EnvBoolean `json:"includeSystemCACertsPool,omitempty" mapstructure:"includeSystemCACertsPool" yaml:"includeSystemCACertsPool,omitempty"` + IncludeSystemCACertsPool *utils.EnvBool `json:"includeSystemCACertsPool,omitempty" mapstructure:"includeSystemCACertsPool" yaml:"includeSystemCACertsPool,omitempty"` // Minimum acceptable TLS version. - MinVersion *EnvString `json:"minVersion,omitempty" mapstructure:"minVersion" yaml:"minVersion,omitempty"` + MinVersion *utils.EnvString `json:"minVersion,omitempty" mapstructure:"minVersion" yaml:"minVersion,omitempty"` // Maximum acceptable TLS version. - MaxVersion *EnvString `json:"maxVersion,omitempty" mapstructure:"maxVersion" yaml:"maxVersion,omitempty"` + MaxVersion *utils.EnvString `json:"maxVersion,omitempty" mapstructure:"maxVersion" yaml:"maxVersion,omitempty"` // Explicit cipher suites can be set. If left blank, a safe default list is used. // See https://go.dev/src/crypto/tls/cipher_suites.go for a list of supported cipher suites. - CipherSuites *EnvStrings `json:"cipherSuites,omitempty" mapstructure:"cipherSuites" yaml:"cipherSuites,omitempty"` + CipherSuites []string `json:"cipherSuites,omitempty" mapstructure:"cipherSuites" yaml:"cipherSuites,omitempty"` // Specifies the duration after which the certificate will be reloaded. If not set, it will never be reloaded. // The interval unit is minute - ReloadInterval *EnvInt `json:"reloadInterval,omitempty" mapstructure:"reloadInterval" yaml:"reloadInterval,omitempty"` + ReloadInterval *utils.EnvInt `json:"reloadInterval,omitempty" mapstructure:"reloadInterval" yaml:"reloadInterval,omitempty"` } // Validate if the current instance is valid func (ss TLSConfig) Validate() error { return nil } + +func getHeadersFromEnv(headers map[string]utils.EnvString) (map[string]string, error) { + results := make(map[string]string) + for key, header := range headers { + value, err := header.Get() + if err != nil { + return nil, fmt.Errorf("headers[%s]: %w", key, err) + } + if value != "" { + results[key] = value + } + } + + return results, nil +} + +func getHeadersFromEnvUnsafe(headers map[string]utils.EnvString) map[string]string { + results := make(map[string]string) + for key, header := range headers { + value, _ := header.Get() + if value != "" { + results[key] = value + } + } + + return results +} diff --git a/ndc-rest-schema/schema/setting_test.go b/ndc-rest-schema/schema/setting_test.go index 545a903..4cc1306 100644 --- a/ndc-rest-schema/schema/setting_test.go +++ b/ndc-rest-schema/schema/setting_test.go @@ -2,8 +2,11 @@ package schema import ( "encoding/json" + "reflect" "testing" + "github.com/google/go-cmp/cmp" + "github.com/hasura/ndc-sdk-go/utils" "gotest.tools/v3/assert" ) @@ -18,16 +21,23 @@ func TestNDCRestSettings(t *testing.T) { input: `{ "servers": [ { - "url": "{{PET_STORE_SERVER_URL:-https://petstore3.swagger.io/api/v3}}" + "url": { + "env": "PET_STORE_SERVER_URL", + "value": "https://petstore3.swagger.io/api/v3" + } }, { - "url": "https://petstore3.swagger.io/api/v3.1" + "url": { + "value": "https://petstore3.swagger.io/api/v3.1" + } } ], "securitySchemes": { "api_key": { "type": "apiKey", - "value": "{{PET_STORE_API_KEY}}", + "value": { + "env": "PET_STORE_API_KEY" + }, "in": "header", "name": "api_key" }, @@ -44,12 +54,6 @@ func TestNDCRestSettings(t *testing.T) { } } }, - "timeout": "{{PET_STORE_TIMEOUT}}", - "retry": { - "times": "{{PET_STORE_RETRY_TIMES}}", - "delay": 1000, - "httpStatus": "{{PET_STORE_RETRY_HTTP_STATUS}}" - }, "security": [ {}, { @@ -61,16 +65,16 @@ func TestNDCRestSettings(t *testing.T) { expected: NDCRestSettings{ Servers: []ServerConfig{ { - URL: *NewEnvStringTemplate(NewEnvTemplateWithDefault("PET_STORE_SERVER_URL", "https://petstore3.swagger.io/api/v3")), + URL: utils.NewEnvString("PET_STORE_SERVER_URL", "https://petstore3.swagger.io/api/v3"), }, { - URL: *EnvString{}.WithValue("https://petstore3.swagger.io/api/v3.1"), + URL: utils.NewEnvStringValue("https://petstore3.swagger.io/api/v3.1"), }, }, SecuritySchemes: map[string]SecurityScheme{ "api_key": { Type: APIKeyScheme, - Value: NewEnvStringTemplate(NewEnvTemplate("PET_STORE_API_KEY")), + Value: utils.ToPtr(utils.NewEnvStringVariable("PET_STORE_API_KEY")), APIKeyAuthConfig: &APIKeyAuthConfig{ In: APIKeyInHeader, Name: "api_key", @@ -91,12 +95,6 @@ func TestNDCRestSettings(t *testing.T) { }, }, }, - Timeout: NewEnvIntTemplate(NewEnvTemplate("PET_STORE_TIMEOUT")), - Retry: &RetryPolicySetting{ - Times: *NewEnvIntTemplate(NewEnvTemplate("PET_STORE_RETRY_TIMES")), - Delay: *NewEnvIntValue(1000), - HTTPStatus: *NewEnvIntsTemplate(NewEnvTemplate("PET_STORE_RETRY_HTTP_STATUS")), - }, Security: AuthSecurities{ AuthSecurity{}, NewAuthSecurity("petstore_auth", []string{"write:pets", "read:pets"}), @@ -114,15 +112,12 @@ func TestNDCRestSettings(t *testing.T) { t.FailNow() } for i, s := range tc.expected.Servers { - assert.DeepEqual(t, s.URL.String(), result.Servers[i].URL.String()) + assert.DeepEqual(t, s.URL.Variable, result.Servers[i].URL.Variable) + assert.DeepEqual(t, s.URL.Value, result.Servers[i].URL.Value) } assert.DeepEqual(t, tc.expected.Headers, result.Headers) - assert.DeepEqual(t, tc.expected.Retry.Delay.String(), result.Retry.Delay.String()) - assert.DeepEqual(t, tc.expected.Retry.Times.String(), result.Retry.Times.String()) - assert.DeepEqual(t, tc.expected.Retry.HTTPStatus.String(), result.Retry.HTTPStatus.String()) assert.DeepEqual(t, tc.expected.Security, result.Security) - assert.DeepEqual(t, tc.expected.SecuritySchemes, result.SecuritySchemes) - assert.DeepEqual(t, tc.expected.Timeout, result.Timeout) + assert.DeepEqual(t, tc.expected.SecuritySchemes, result.SecuritySchemes, cmp.Exporter(func(t reflect.Type) bool { return true })) assert.DeepEqual(t, tc.expected.Version, result.Version) _, err := json.Marshal(tc.expected) diff --git a/tests/configuration/config.yaml b/tests/configuration/config.yaml new file mode 100644 index 0000000..22b1f10 --- /dev/null +++ b/tests/configuration/config.yaml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=../../ndc-rest-schema/jsonschema/configuration.schema.json +output: schema.output.json +strict: true +forwardHeaders: + enabled: false + argumentField: headers + responseHeaders: + headersField: "headers" + resultField: "response" + forwardHeaders: + - Content-Type + - X-Custom-Header +concurrency: + query: 1 + mutation: 1 + rest: 0 +files: + - file: https://raw.githubusercontent.com/hasura/ndc-rest/refs/heads/main/connector/testdata/jsonplaceholder/swagger.json + spec: oas2 diff --git a/tests/engine/.env b/tests/engine/.env new file mode 100644 index 0000000..829e46d --- /dev/null +++ b/tests/engine/.env @@ -0,0 +1,2 @@ +APP_MYAPI_READ_URL=http://local.hasura.dev:8080 +APP_MYAPI_WRITE_URL=http://local.hasura.dev:8080 \ No newline at end of file diff --git a/tests/engine/.gitattributes b/tests/engine/.gitattributes new file mode 100644 index 0000000..8ddc99f --- /dev/null +++ b/tests/engine/.gitattributes @@ -0,0 +1 @@ +*.hml linguist-language=yaml \ No newline at end of file diff --git a/tests/engine/.gitignore b/tests/engine/.gitignore new file mode 100644 index 0000000..d168928 --- /dev/null +++ b/tests/engine/.gitignore @@ -0,0 +1,2 @@ +engine/build +/.env.* diff --git a/tests/engine/.hasura/context.yaml b/tests/engine/.hasura/context.yaml new file mode 100644 index 0000000..3822ed0 --- /dev/null +++ b/tests/engine/.hasura/context.yaml @@ -0,0 +1,14 @@ +kind: Context +version: v3 +definition: + current: default + contexts: + default: + supergraph: ../supergraph.yaml + subgraph: ../app/subgraph.yaml + localEnvFile: ../.env + scripts: + docker-start: + bash: HASURA_DDN_PAT=$(ddn auth print-pat) PROMPTQL_SECRET_KEY=$(ddn auth print-promptql-secret-key) docker compose -f compose.yaml --env-file .env up --build --pull always + powershell: $Env:HASURA_DDN_PAT = ddn auth print-pat; $Env:PROMPTQL_SECRET_KEY = ddn auth print-promptql-secret-key; docker compose -f compose.yaml --env-file .env up --build --pull always + promptQL: false diff --git a/tests/engine/app/metadata/createPost.hml b/tests/engine/app/metadata/createPost.hml new file mode 100644 index 0000000..d140526 --- /dev/null +++ b/tests/engine/app/metadata/createPost.hml @@ -0,0 +1,28 @@ +--- +kind: Command +version: v1 +definition: + name: createPost + outputType: Post! + arguments: + - name: body + type: Post! + description: Post object that needs to be added + source: + dataConnectorName: myapi + dataConnectorCommand: + procedure: createPost + graphql: + rootFieldName: createPost + rootFieldKind: Mutation + description: Create a post + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: createPost + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/deletePostById.hml b/tests/engine/app/metadata/deletePostById.hml new file mode 100644 index 0000000..dbc7139 --- /dev/null +++ b/tests/engine/app/metadata/deletePostById.hml @@ -0,0 +1,28 @@ +--- +kind: Command +version: v1 +definition: + name: deletePostById + outputType: Boolean + arguments: + - name: id + type: Int32! + description: The ID of the post to retrieve + source: + dataConnectorName: myapi + dataConnectorCommand: + procedure: deletePostById + graphql: + rootFieldName: deletePostById + rootFieldKind: Mutation + description: Delete specific post + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: deletePostById + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getAlbums.hml b/tests/engine/app/metadata/getAlbums.hml new file mode 100644 index 0000000..0c2a1ab --- /dev/null +++ b/tests/engine/app/metadata/getAlbums.hml @@ -0,0 +1,63 @@ +--- +kind: ObjectType +version: v1 +definition: + name: Album + fields: + - name: id + type: Int64 + - name: title + type: String + - name: userId + type: Int64 + graphql: + typeName: Album + inputTypeName: Album_input + dataConnectorTypeMapping: + - dataConnectorName: myapi + dataConnectorObjectType: Album + +--- +kind: TypePermissions +version: v1 +definition: + typeName: Album + permissions: + - role: admin + output: + allowedFields: + - id + - title + - userId + +--- +kind: Command +version: v1 +definition: + name: getAlbums + outputType: "[Album!]!" + arguments: + - name: id + type: Int32 + description: Filter by album ID + - name: userId + type: Int32 + description: Filter by user ID + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getAlbums + graphql: + rootFieldName: getAlbums + rootFieldKind: Query + description: Get all available albums + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getAlbums + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getAlbumsId.hml b/tests/engine/app/metadata/getAlbumsId.hml new file mode 100644 index 0000000..b09cd7a --- /dev/null +++ b/tests/engine/app/metadata/getAlbumsId.hml @@ -0,0 +1,28 @@ +--- +kind: Command +version: v1 +definition: + name: getAlbumsId + outputType: Album! + arguments: + - name: id + type: Int32! + description: The ID of the album to retrieve + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getAlbumsId + graphql: + rootFieldName: getAlbumsId + rootFieldKind: Query + description: Get specific album + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getAlbumsId + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getAlbumsIdPhotos.hml b/tests/engine/app/metadata/getAlbumsIdPhotos.hml new file mode 100644 index 0000000..76a9381 --- /dev/null +++ b/tests/engine/app/metadata/getAlbumsIdPhotos.hml @@ -0,0 +1,66 @@ +--- +kind: ObjectType +version: v1 +definition: + name: Photo + fields: + - name: albumId + type: Int64 + - name: id + type: Int64 + - name: thumbnailUrl + type: URI + - name: title + type: String + - name: url + type: URI + graphql: + typeName: Photo + inputTypeName: Photo_input + dataConnectorTypeMapping: + - dataConnectorName: myapi + dataConnectorObjectType: Photo + +--- +kind: TypePermissions +version: v1 +definition: + typeName: Photo + permissions: + - role: admin + output: + allowedFields: + - albumId + - id + - thumbnailUrl + - title + - url + +--- +kind: Command +version: v1 +definition: + name: getAlbumsIdPhotos + outputType: "[Photo!]!" + arguments: + - name: id + type: Int32! + description: post id + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getAlbumsIdPhotos + graphql: + rootFieldName: getAlbumsIdPhotos + rootFieldKind: Query + description: Get photos for a specific album + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getAlbumsIdPhotos + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getComment.hml b/tests/engine/app/metadata/getComment.hml new file mode 100644 index 0000000..4c7fa24 --- /dev/null +++ b/tests/engine/app/metadata/getComment.hml @@ -0,0 +1,66 @@ +--- +kind: ObjectType +version: v1 +definition: + name: Comment + fields: + - name: body + type: String + - name: email + type: String + - name: id + type: Int64 + - name: name + type: String + - name: postId + type: Int64 + graphql: + typeName: Comment + inputTypeName: Comment_input + dataConnectorTypeMapping: + - dataConnectorName: myapi + dataConnectorObjectType: Comment + +--- +kind: TypePermissions +version: v1 +definition: + typeName: Comment + permissions: + - role: admin + output: + allowedFields: + - body + - email + - id + - name + - postId + +--- +kind: Command +version: v1 +definition: + name: getComment + outputType: Comment! + arguments: + - name: id + type: Int32! + description: The ID of the comment to retrieve + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getComment + graphql: + rootFieldName: getComment + rootFieldKind: Query + description: Get specific comment + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getComment + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getComments.hml b/tests/engine/app/metadata/getComments.hml new file mode 100644 index 0000000..860d267 --- /dev/null +++ b/tests/engine/app/metadata/getComments.hml @@ -0,0 +1,31 @@ +--- +kind: Command +version: v1 +definition: + name: getComments + outputType: "[Comment!]!" + arguments: + - name: id + type: Int32 + description: Filter by comment ID + - name: postId + type: Int32 + description: Filter by post ID + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getComments + graphql: + rootFieldName: getComments + rootFieldKind: Query + description: Get all available comments + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getComments + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getPhoto.hml b/tests/engine/app/metadata/getPhoto.hml new file mode 100644 index 0000000..b04c2df --- /dev/null +++ b/tests/engine/app/metadata/getPhoto.hml @@ -0,0 +1,28 @@ +--- +kind: Command +version: v1 +definition: + name: getPhoto + outputType: Photo! + arguments: + - name: id + type: Int32! + description: The ID of the photo to retrieve + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getPhoto + graphql: + rootFieldName: getPhoto + rootFieldKind: Query + description: Get specific photo + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getPhoto + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getPhotos.hml b/tests/engine/app/metadata/getPhotos.hml new file mode 100644 index 0000000..5a28de4 --- /dev/null +++ b/tests/engine/app/metadata/getPhotos.hml @@ -0,0 +1,31 @@ +--- +kind: Command +version: v1 +definition: + name: getPhotos + outputType: "[Photo!]!" + arguments: + - name: albumId + type: Int32 + description: Filter by album ID + - name: id + type: Int32 + description: Filter by photo ID + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getPhotos + graphql: + rootFieldName: getPhotos + rootFieldKind: Query + description: Get all available photos + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getPhotos + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getPostById.hml b/tests/engine/app/metadata/getPostById.hml new file mode 100644 index 0000000..8813e67 --- /dev/null +++ b/tests/engine/app/metadata/getPostById.hml @@ -0,0 +1,63 @@ +--- +kind: ObjectType +version: v1 +definition: + name: Post + fields: + - name: body + type: String + - name: id + type: Int64 + - name: title + type: String + - name: userId + type: Int64 + graphql: + typeName: Post + inputTypeName: Post_input + dataConnectorTypeMapping: + - dataConnectorName: myapi + dataConnectorObjectType: Post + +--- +kind: TypePermissions +version: v1 +definition: + typeName: Post + permissions: + - role: admin + output: + allowedFields: + - body + - id + - title + - userId + +--- +kind: Command +version: v1 +definition: + name: getPostById + outputType: Post! + arguments: + - name: id + type: Int32! + description: The ID of the post to retrieve + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getPostById + graphql: + rootFieldName: getPostById + rootFieldKind: Query + description: Get specific post + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getPostById + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getPosts.hml b/tests/engine/app/metadata/getPosts.hml new file mode 100644 index 0000000..1f1e288 --- /dev/null +++ b/tests/engine/app/metadata/getPosts.hml @@ -0,0 +1,31 @@ +--- +kind: Command +version: v1 +definition: + name: getPosts + outputType: "[Post!]!" + arguments: + - name: id + type: Int32 + description: Filter by post ID + - name: userId + type: Int32 + description: Filter by user ID + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getPosts + graphql: + rootFieldName: getPosts + rootFieldKind: Query + description: Get all available posts + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getPosts + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getPostsIdComments.hml b/tests/engine/app/metadata/getPostsIdComments.hml new file mode 100644 index 0000000..4a8aeb9 --- /dev/null +++ b/tests/engine/app/metadata/getPostsIdComments.hml @@ -0,0 +1,28 @@ +--- +kind: Command +version: v1 +definition: + name: getPostsIdComments + outputType: "[Comment!]!" + arguments: + - name: id + type: Int32! + description: post id + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getPostsIdComments + graphql: + rootFieldName: getPostsIdComments + rootFieldKind: Query + description: Get comments for a specific post + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getPostsIdComments + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getTodo.hml b/tests/engine/app/metadata/getTodo.hml new file mode 100644 index 0000000..01cfdc1 --- /dev/null +++ b/tests/engine/app/metadata/getTodo.hml @@ -0,0 +1,63 @@ +--- +kind: ObjectType +version: v1 +definition: + name: Todo + fields: + - name: completed + type: Boolean + - name: id + type: Int64 + - name: title + type: String + - name: userId + type: Int64 + graphql: + typeName: Todo + inputTypeName: Todo_input + dataConnectorTypeMapping: + - dataConnectorName: myapi + dataConnectorObjectType: Todo + +--- +kind: TypePermissions +version: v1 +definition: + typeName: Todo + permissions: + - role: admin + output: + allowedFields: + - completed + - id + - title + - userId + +--- +kind: Command +version: v1 +definition: + name: getTodo + outputType: Todo! + arguments: + - name: id + type: Int32! + description: The ID of the todo to retrieve + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getTodo + graphql: + rootFieldName: getTodo + rootFieldKind: Query + description: Get specific todo + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getTodo + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getTodos.hml b/tests/engine/app/metadata/getTodos.hml new file mode 100644 index 0000000..050e1b9 --- /dev/null +++ b/tests/engine/app/metadata/getTodos.hml @@ -0,0 +1,31 @@ +--- +kind: Command +version: v1 +definition: + name: getTodos + outputType: "[Todo!]!" + arguments: + - name: id + type: Int32 + description: Filter by todo ID + - name: userId + type: Int32 + description: Filter by user ID + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getTodos + graphql: + rootFieldName: getTodos + rootFieldKind: Query + description: Get all available todos + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getTodos + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getUser.hml b/tests/engine/app/metadata/getUser.hml new file mode 100644 index 0000000..b1022de --- /dev/null +++ b/tests/engine/app/metadata/getUser.hml @@ -0,0 +1,174 @@ +--- +kind: ObjectType +version: v1 +definition: + name: UserAddressGeo + fields: + - name: lat + type: String + - name: lng + type: String + graphql: + typeName: UserAddressGeo + inputTypeName: UserAddressGeo_input + dataConnectorTypeMapping: + - dataConnectorName: myapi + dataConnectorObjectType: UserAddressGeo + +--- +kind: TypePermissions +version: v1 +definition: + typeName: UserAddressGeo + permissions: + - role: admin + output: + allowedFields: + - lat + - lng + +--- +kind: ObjectType +version: v1 +definition: + name: UserAddress + fields: + - name: city + type: String + - name: geo + type: UserAddressGeo + - name: street + type: String + - name: suite + type: String + - name: zipcode + type: String + graphql: + typeName: UserAddress + inputTypeName: UserAddress_input + dataConnectorTypeMapping: + - dataConnectorName: myapi + dataConnectorObjectType: UserAddress + +--- +kind: TypePermissions +version: v1 +definition: + typeName: UserAddress + permissions: + - role: admin + output: + allowedFields: + - city + - geo + - street + - suite + - zipcode + +--- +kind: ObjectType +version: v1 +definition: + name: UserCompany + fields: + - name: bs + type: String + - name: catchPhrase + type: String + - name: name + type: String + graphql: + typeName: UserCompany + inputTypeName: UserCompany_input + dataConnectorTypeMapping: + - dataConnectorName: myapi + dataConnectorObjectType: UserCompany + +--- +kind: TypePermissions +version: v1 +definition: + typeName: UserCompany + permissions: + - role: admin + output: + allowedFields: + - bs + - catchPhrase + - name + +--- +kind: ObjectType +version: v1 +definition: + name: User + fields: + - name: address + type: UserAddress + - name: company + type: UserCompany + - name: email + type: String + - name: id + type: Int64 + - name: name + type: String + - name: phone + type: String + - name: username + type: String + - name: website + type: String + graphql: + typeName: User + inputTypeName: User_input + dataConnectorTypeMapping: + - dataConnectorName: myapi + dataConnectorObjectType: User + +--- +kind: TypePermissions +version: v1 +definition: + typeName: User + permissions: + - role: admin + output: + allowedFields: + - address + - company + - email + - id + - name + - phone + - username + - website + +--- +kind: Command +version: v1 +definition: + name: getUser + outputType: User! + arguments: + - name: id + type: Int32! + description: The ID of the user to retrieve + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getUser + graphql: + rootFieldName: getUser + rootFieldKind: Query + description: Get specific user + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getUser + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/getUsers.hml b/tests/engine/app/metadata/getUsers.hml new file mode 100644 index 0000000..b1f3c32 --- /dev/null +++ b/tests/engine/app/metadata/getUsers.hml @@ -0,0 +1,31 @@ +--- +kind: Command +version: v1 +definition: + name: getUsers + outputType: "[User!]!" + arguments: + - name: email + type: Int32 + description: Filter by user email address + - name: id + type: Int32 + description: Filter by user ID + source: + dataConnectorName: myapi + dataConnectorCommand: + function: getUsers + graphql: + rootFieldName: getUsers + rootFieldKind: Query + description: Get all available users + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: getUsers + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/myapi-types.hml b/tests/engine/app/metadata/myapi-types.hml new file mode 100644 index 0000000..e46c3bc --- /dev/null +++ b/tests/engine/app/metadata/myapi-types.hml @@ -0,0 +1,114 @@ +--- +kind: ScalarType +version: v1 +definition: + name: Int32 + graphql: + typeName: Int32 + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: Int32_bool_exp + operand: + scalar: + type: Int32 + comparisonOperators: [] + dataConnectorOperatorMapping: + - dataConnectorName: myapi + dataConnectorScalarType: Int32 + operatorMapping: {} + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: Int32_bool_exp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: myapi + dataConnectorScalarType: Int32 + representation: Int32 + graphql: + comparisonExpressionTypeName: Int32_comparison_exp + +--- +kind: ScalarType +version: v1 +definition: + name: Int64 + graphql: + typeName: Int64 + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: Int64_bool_exp + operand: + scalar: + type: Int64 + comparisonOperators: [] + dataConnectorOperatorMapping: + - dataConnectorName: myapi + dataConnectorScalarType: Int64 + operatorMapping: {} + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: Int64_bool_exp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: myapi + dataConnectorScalarType: Int64 + representation: Int64 + graphql: + comparisonExpressionTypeName: Int64_comparison_exp + +--- +kind: ScalarType +version: v1 +definition: + name: URI + graphql: + typeName: URI + +--- +kind: BooleanExpressionType +version: v1 +definition: + name: URI_bool_exp + operand: + scalar: + type: URI + comparisonOperators: [] + dataConnectorOperatorMapping: + - dataConnectorName: myapi + dataConnectorScalarType: URI + operatorMapping: {} + logicalOperators: + enable: true + isNull: + enable: true + graphql: + typeName: URI_bool_exp + +--- +kind: DataConnectorScalarRepresentation +version: v1 +definition: + dataConnectorName: myapi + dataConnectorScalarType: URI + representation: URI + graphql: + comparisonExpressionTypeName: URI_comparison_exp + diff --git a/tests/engine/app/metadata/myapi.hml b/tests/engine/app/metadata/myapi.hml new file mode 100644 index 0000000..9602495 --- /dev/null +++ b/tests/engine/app/metadata/myapi.hml @@ -0,0 +1,588 @@ +kind: DataConnectorLink +version: v1 +definition: + name: myapi + url: + readWriteUrls: + read: + valueFromEnv: APP_MYAPI_READ_URL + write: + valueFromEnv: APP_MYAPI_WRITE_URL + headers: + X-Test-Header: + value: test + schema: + version: v0.1 + schema: + scalar_types: + Boolean: + representation: + type: boolean + aggregate_functions: {} + comparison_operators: {} + Int32: + representation: + type: int32 + aggregate_functions: {} + comparison_operators: {} + Int64: + representation: + type: int64 + aggregate_functions: {} + comparison_operators: {} + String: + representation: + type: string + aggregate_functions: {} + comparison_operators: {} + URI: + representation: + type: string + aggregate_functions: {} + comparison_operators: {} + object_types: + Album: + fields: + id: + type: + type: nullable + underlying_type: + type: named + name: Int64 + title: + type: + type: nullable + underlying_type: + type: named + name: String + userId: + type: + type: nullable + underlying_type: + type: named + name: Int64 + Comment: + fields: + body: + type: + type: nullable + underlying_type: + type: named + name: String + email: + type: + type: nullable + underlying_type: + type: named + name: String + id: + type: + type: nullable + underlying_type: + type: named + name: Int64 + name: + type: + type: nullable + underlying_type: + type: named + name: String + postId: + type: + type: nullable + underlying_type: + type: named + name: Int64 + Photo: + fields: + albumId: + type: + type: nullable + underlying_type: + type: named + name: Int64 + id: + type: + type: nullable + underlying_type: + type: named + name: Int64 + thumbnailUrl: + type: + type: nullable + underlying_type: + type: named + name: URI + title: + type: + type: nullable + underlying_type: + type: named + name: String + url: + type: + type: nullable + underlying_type: + type: named + name: URI + Post: + fields: + body: + type: + type: nullable + underlying_type: + type: named + name: String + id: + type: + type: nullable + underlying_type: + type: named + name: Int64 + title: + type: + type: nullable + underlying_type: + type: named + name: String + userId: + type: + type: nullable + underlying_type: + type: named + name: Int64 + Todo: + fields: + completed: + type: + type: nullable + underlying_type: + type: named + name: Boolean + id: + type: + type: nullable + underlying_type: + type: named + name: Int64 + title: + type: + type: nullable + underlying_type: + type: named + name: String + userId: + type: + type: nullable + underlying_type: + type: named + name: Int64 + User: + fields: + address: + type: + type: nullable + underlying_type: + type: named + name: UserAddress + company: + type: + type: nullable + underlying_type: + type: named + name: UserCompany + email: + type: + type: nullable + underlying_type: + type: named + name: String + id: + type: + type: nullable + underlying_type: + type: named + name: Int64 + name: + type: + type: nullable + underlying_type: + type: named + name: String + phone: + type: + type: nullable + underlying_type: + type: named + name: String + username: + type: + type: nullable + underlying_type: + type: named + name: String + website: + type: + type: nullable + underlying_type: + type: named + name: String + UserAddress: + fields: + city: + type: + type: nullable + underlying_type: + type: named + name: String + geo: + type: + type: nullable + underlying_type: + type: named + name: UserAddressGeo + street: + type: + type: nullable + underlying_type: + type: named + name: String + suite: + type: + type: nullable + underlying_type: + type: named + name: String + zipcode: + type: + type: nullable + underlying_type: + type: named + name: String + UserAddressGeo: + fields: + lat: + type: + type: nullable + underlying_type: + type: named + name: String + lng: + type: + type: nullable + underlying_type: + type: named + name: String + UserCompany: + fields: + bs: + type: + type: nullable + underlying_type: + type: named + name: String + catchPhrase: + type: + type: nullable + underlying_type: + type: named + name: String + name: + type: + type: nullable + underlying_type: + type: named + name: String + collections: [] + functions: + - name: getAlbums + description: Get all available albums + arguments: + id: + description: Filter by album ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + userId: + description: Filter by user ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + result_type: + type: array + element_type: + type: named + name: Album + - name: getAlbumsId + description: Get specific album + arguments: + id: + description: The ID of the album to retrieve + type: + type: named + name: Int32 + result_type: + type: named + name: Album + - name: getAlbumsIdPhotos + description: Get photos for a specific album + arguments: + id: + description: post id + type: + type: named + name: Int32 + result_type: + type: array + element_type: + type: named + name: Photo + - name: getComment + description: Get specific comment + arguments: + id: + description: The ID of the comment to retrieve + type: + type: named + name: Int32 + result_type: + type: named + name: Comment + - name: getComments + description: Get all available comments + arguments: + id: + description: Filter by comment ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + postId: + description: Filter by post ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + result_type: + type: array + element_type: + type: named + name: Comment + - name: getPhoto + description: Get specific photo + arguments: + id: + description: The ID of the photo to retrieve + type: + type: named + name: Int32 + result_type: + type: named + name: Photo + - name: getPhotos + description: Get all available photos + arguments: + albumId: + description: Filter by album ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + id: + description: Filter by photo ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + result_type: + type: array + element_type: + type: named + name: Photo + - name: getPostById + description: Get specific post + arguments: + id: + description: The ID of the post to retrieve + type: + type: named + name: Int32 + result_type: + type: named + name: Post + - name: getPosts + description: Get all available posts + arguments: + id: + description: Filter by post ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + userId: + description: Filter by user ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + result_type: + type: array + element_type: + type: named + name: Post + - name: getPostsIdComments + description: Get comments for a specific post + arguments: + id: + description: post id + type: + type: named + name: Int32 + result_type: + type: array + element_type: + type: named + name: Comment + - name: getTodo + description: Get specific todo + arguments: + id: + description: The ID of the todo to retrieve + type: + type: named + name: Int32 + result_type: + type: named + name: Todo + - name: getTodos + description: Get all available todos + arguments: + id: + description: Filter by todo ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + userId: + description: Filter by user ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + result_type: + type: array + element_type: + type: named + name: Todo + - name: getUser + description: Get specific user + arguments: + id: + description: The ID of the user to retrieve + type: + type: named + name: Int32 + result_type: + type: named + name: User + - name: getUsers + description: Get all available users + arguments: + email: + description: Filter by user email address + type: + type: nullable + underlying_type: + type: named + name: Int32 + id: + description: Filter by user ID + type: + type: nullable + underlying_type: + type: named + name: Int32 + result_type: + type: array + element_type: + type: named + name: User + procedures: + - name: createPost + description: Create a post + arguments: + body: + description: Post object that needs to be added + type: + type: named + name: Post + result_type: + type: named + name: Post + - name: deletePostById + description: Delete specific post + arguments: + id: + description: The ID of the post to retrieve + type: + type: named + name: Int32 + result_type: + type: nullable + underlying_type: + type: named + name: Boolean + - name: patchPostById + description: patch specific post + arguments: + body: + description: Post object that needs to be updated + type: + type: named + name: Post + id: + description: The ID of the post to retrieve + type: + type: named + name: Int32 + result_type: + type: named + name: Post + - name: updatePostById + description: Update specific post + arguments: + body: + description: Post object that needs to be updated + type: + type: named + name: Post + id: + description: The ID of the post to retrieve + type: + type: named + name: Int32 + result_type: + type: named + name: Post + capabilities: + version: 0.1.6 + capabilities: + query: + variables: {} + explain: {} + nested_fields: {} + exists: {} + mutation: + explain: {} diff --git a/tests/engine/app/metadata/patchPostById.hml b/tests/engine/app/metadata/patchPostById.hml new file mode 100644 index 0000000..6d957d1 --- /dev/null +++ b/tests/engine/app/metadata/patchPostById.hml @@ -0,0 +1,31 @@ +--- +kind: Command +version: v1 +definition: + name: patchPostById + outputType: Post! + arguments: + - name: body + type: Post! + description: Post object that needs to be updated + - name: id + type: Int32! + description: The ID of the post to retrieve + source: + dataConnectorName: myapi + dataConnectorCommand: + procedure: patchPostById + graphql: + rootFieldName: patchPostById + rootFieldKind: Mutation + description: patch specific post + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: patchPostById + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/metadata/updatePostById.hml b/tests/engine/app/metadata/updatePostById.hml new file mode 100644 index 0000000..50c6c57 --- /dev/null +++ b/tests/engine/app/metadata/updatePostById.hml @@ -0,0 +1,31 @@ +--- +kind: Command +version: v1 +definition: + name: updatePostById + outputType: Post! + arguments: + - name: body + type: Post! + description: Post object that needs to be updated + - name: id + type: Int32! + description: The ID of the post to retrieve + source: + dataConnectorName: myapi + dataConnectorCommand: + procedure: updatePostById + graphql: + rootFieldName: updatePostById + rootFieldKind: Mutation + description: Update specific post + +--- +kind: CommandPermissions +version: v1 +definition: + commandName: updatePostById + permissions: + - role: admin + allowExecution: true + diff --git a/tests/engine/app/subgraph.yaml b/tests/engine/app/subgraph.yaml new file mode 100644 index 0000000..1d7ebb0 --- /dev/null +++ b/tests/engine/app/subgraph.yaml @@ -0,0 +1,14 @@ +kind: Subgraph +version: v2 +definition: + name: app + generator: + rootPath: . + namingConvention: none + includePaths: + - metadata + envMapping: + APP_MYAPI_READ_URL: + fromEnv: APP_MYAPI_READ_URL + APP_MYAPI_WRITE_URL: + fromEnv: APP_MYAPI_WRITE_URL diff --git a/tests/engine/compose.yaml b/tests/engine/compose.yaml new file mode 100644 index 0000000..b9cd1ec --- /dev/null +++ b/tests/engine/compose.yaml @@ -0,0 +1,30 @@ +services: + engine: + build: + context: engine + dockerfile: Dockerfile.engine + pull: true + environment: + AUTHN_CONFIG_PATH: /md/auth_config.json + ENABLE_CORS: "true" + ENABLE_SQL_INTERFACE: "true" + INTROSPECTION_METADATA_FILE: /md/metadata.json + METADATA_PATH: /md/open_dd.json + OTLP_ENDPOINT: http://local.hasura.dev:4317 + extra_hosts: + - local.hasura.dev:host-gateway + labels: + io.hasura.ddn.service-name: engine + ports: + - 3280:3000 + otel-collector: + command: + - --config=/etc/otel-collector-config.yaml + environment: + HASURA_DDN_PAT: ${HASURA_DDN_PAT} + image: otel/opentelemetry-collector:0.104.0 + ports: + - 4317:4317 + - 4318:4318 + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml diff --git a/tests/engine/engine/Dockerfile.engine b/tests/engine/engine/Dockerfile.engine new file mode 100644 index 0000000..3613f0e --- /dev/null +++ b/tests/engine/engine/Dockerfile.engine @@ -0,0 +1,2 @@ +FROM ghcr.io/hasura/v3-engine +COPY ./build /md/ \ No newline at end of file diff --git a/tests/engine/globals/metadata/auth-config.hml b/tests/engine/globals/metadata/auth-config.hml new file mode 100644 index 0000000..62c27d7 --- /dev/null +++ b/tests/engine/globals/metadata/auth-config.hml @@ -0,0 +1,8 @@ +kind: AuthConfig +version: v2 +definition: + mode: + noAuth: + role: admin + sessionVariables: + X-Custom-Header: foo diff --git a/tests/engine/globals/metadata/compatibility-config.hml b/tests/engine/globals/metadata/compatibility-config.hml new file mode 100644 index 0000000..10d0471 --- /dev/null +++ b/tests/engine/globals/metadata/compatibility-config.hml @@ -0,0 +1,2 @@ +kind: CompatibilityConfig +date: "2024-10-01" diff --git a/tests/engine/globals/metadata/graphql-config.hml b/tests/engine/globals/metadata/graphql-config.hml new file mode 100644 index 0000000..f938ad0 --- /dev/null +++ b/tests/engine/globals/metadata/graphql-config.hml @@ -0,0 +1,34 @@ +kind: GraphqlConfig +version: v1 +definition: + query: + rootOperationTypeName: Query + argumentsInput: + fieldName: args + limitInput: + fieldName: limit + offsetInput: + fieldName: offset + filterInput: + fieldName: where + operatorNames: + and: _and + or: _or + not: _not + isNull: _is_null + orderByInput: + fieldName: order_by + enumDirectionValues: + asc: Asc + desc: Desc + enumTypeNames: + - directions: + - Asc + - Desc + typeName: OrderBy + aggregate: + filterInputFieldName: filter_input + countFieldName: _count + countDistinctFieldName: _count_distinct + mutation: + rootOperationTypeName: Mutation diff --git a/tests/engine/globals/subgraph.yaml b/tests/engine/globals/subgraph.yaml new file mode 100644 index 0000000..b21faca --- /dev/null +++ b/tests/engine/globals/subgraph.yaml @@ -0,0 +1,8 @@ +kind: Subgraph +version: v2 +definition: + name: globals + generator: + rootPath: . + includePaths: + - metadata diff --git a/tests/engine/hasura.yaml b/tests/engine/hasura.yaml new file mode 100644 index 0000000..7f8f5cc --- /dev/null +++ b/tests/engine/hasura.yaml @@ -0,0 +1 @@ +version: v3 diff --git a/tests/engine/otel-collector-config.yaml b/tests/engine/otel-collector-config.yaml new file mode 100644 index 0000000..e4bcd1e --- /dev/null +++ b/tests/engine/otel-collector-config.yaml @@ -0,0 +1,37 @@ +exporters: + otlp: + endpoint: https://gateway.otlp.hasura.io:443 + headers: + Authorization: pat ${env:HASURA_DDN_PAT} +processors: + batch: {} +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 +service: + pipelines: + logs: + exporters: + - otlp + processors: + - batch + receivers: + - otlp + metrics: + exporters: + - otlp + processors: + - batch + receivers: + - otlp + traces: + exporters: + - otlp + processors: + - batch + receivers: + - otlp diff --git a/tests/engine/supergraph.yaml b/tests/engine/supergraph.yaml new file mode 100644 index 0000000..0d9260e --- /dev/null +++ b/tests/engine/supergraph.yaml @@ -0,0 +1,6 @@ +kind: Supergraph +version: v2 +definition: + subgraphs: + - globals/subgraph.yaml + - app/subgraph.yaml