From f67e3fe5de68ba20964aa1a876b863a9a7f95772 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 00:00:39 +0000 Subject: [PATCH] feat(api): validate required openapi parameters Co-Authored-By: Virgil --- client.go | 45 ++++++++++++++++++++++++++++++++++++++++++--- client_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 7c68afa..4ef503f 100644 --- a/client.go +++ b/client.go @@ -50,8 +50,9 @@ type openAPIOperation struct { } type openAPIParameter struct { - name string - in string + name string + in string + required bool } // OpenAPIClientOption configures a runtime OpenAPI client. @@ -349,6 +350,11 @@ func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (st } else { pathValues = params } + + if err := validateRequiredParameters(op, params, pathKeys); err != nil { + return "", err + } + for _, key := range pathKeys { if value, ok := pathValues[key]; ok { placeholder := "{" + key + "}" @@ -572,7 +578,8 @@ func parseOperationParameters(operation map[string]any) []openAPIParameter { if name == "" || in == "" { continue } - params = append(params, openAPIParameter{name: name, in: in}) + required, _ := param["required"].(bool) + params = append(params, openAPIParameter{name: name, in: in, required: required}) } return params @@ -587,6 +594,38 @@ func operationParameterLocation(op openAPIOperation, name string) string { return "" } +func validateRequiredParameters(op openAPIOperation, params map[string]any, pathKeys []string) error { + for _, param := range op.parameters { + if !param.required { + continue + } + if containsString(pathKeys, param.name) { + continue + } + if parameterProvided(params, param.name, param.in) { + continue + } + return coreerr.E("OpenAPIClient.buildURL", fmt.Sprintf("missing required %s parameter %q", param.in, param.name), nil) + } + return nil +} + +func parameterProvided(params map[string]any, name, location string) bool { + if nested, ok := nestedMap(params, location); ok { + if _, exists := nested[name]; exists { + return true + } + } + + if value, exists := params[name]; exists { + if value != nil { + return true + } + } + + return false +} + func encodeJSONBody(v any) ([]byte, error) { data, err := json.Marshal(v) if err != nil { diff --git a/client_test.go b/client_test.go index ed9e2f5..2e6c4e1 100644 --- a/client_test.go +++ b/client_test.go @@ -384,6 +384,50 @@ paths: } } +func TestOpenAPIClient_Bad_MissingRequiredQueryParameter(t *testing.T) { + called := make(chan struct{}, 1) + mux := http.NewServeMux() + mux.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) { + called <- struct{}{} + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"success":true,"data":{"ok":true}}`)) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + specPath := writeTempSpec(t, `openapi: 3.1.0 +info: + title: Test API + version: 1.0.0 +paths: + /submit: + post: + operationId: submit_item + parameters: + - name: verbose + in: query + required: true +`) + + client := api.NewOpenAPIClient( + api.WithSpec(specPath), + api.WithBaseURL(srv.URL), + ) + + if _, err := client.Call("submit_item", map[string]any{ + "name": "Ada", + }); err == nil { + t.Fatal("expected required query parameter validation error, got nil") + } + + select { + case <-called: + t.Fatal("expected validation to fail before the HTTP call") + default: + } +} + func TestOpenAPIClient_Good_UsesHeaderAndCookieParameters(t *testing.T) { errCh := make(chan error, 1) mux := http.NewServeMux()