From 152645489bc2f692d3c6e32a5503b3ff9e4563d4 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 08:27:25 +0000 Subject: [PATCH] feat(api): validate openapi parameter values Co-Authored-By: Virgil --- client.go | 50 ++++++++++++++++++++++++++- client_test.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index 59922e3..5e4b36a 100644 --- a/client.go +++ b/client.go @@ -53,6 +53,7 @@ type openAPIParameter struct { name string in string required bool + schema map[string]any } // OpenAPIClientOption configures a runtime OpenAPI client. @@ -358,6 +359,9 @@ func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (st if err := validateRequiredParameters(op, params, pathKeys); err != nil { return "", err } + if err := validateParameterValues(op, params); err != nil { + return "", err + } for _, key := range pathKeys { if value, ok := pathValues[key]; ok { @@ -583,7 +587,8 @@ func parseOperationParameters(operation map[string]any) []openAPIParameter { continue } required, _ := param["required"].(bool) - params = append(params, openAPIParameter{name: name, in: in, required: required}) + schema, _ := param["schema"].(map[string]any) + params = append(params, openAPIParameter{name: name, in: in, required: required, schema: schema}) } return params @@ -598,6 +603,49 @@ func operationParameterLocation(op openAPIOperation, name string) string { return "" } +func validateParameterValues(op openAPIOperation, params map[string]any) error { + pathKeys := pathParameterNames(op.pathTemplate) + for _, param := range op.parameters { + if len(param.schema) == 0 { + continue + } + if containsString(pathKeys, param.name) { + continue + } + + if nested, ok := nestedMap(params, param.in); ok { + if value, exists := nested[param.name]; exists { + if err := validateParameterValue(param, value); err != nil { + return err + } + continue + } + } + + if value, exists := params[param.name]; exists { + if err := validateParameterValue(param, value); err != nil { + return err + } + } + } + return nil +} + +func validateParameterValue(param openAPIParameter, value any) error { + if value == nil { + return nil + } + + data, err := json.Marshal(value) + if err != nil { + return coreerr.E("OpenAPIClient.validateParameterValue", fmt.Sprintf("marshal %s parameter %q", param.in, param.name), err) + } + if err := validateOpenAPISchema(data, param.schema, fmt.Sprintf("%s parameter %q", param.in, param.name)); err != nil { + return err + } + return nil +} + func validateRequiredParameters(op openAPIOperation, params map[string]any, pathKeys []string) error { for _, param := range op.parameters { if !param.required { diff --git a/client_test.go b/client_test.go index 2e6c4e1..e793f66 100644 --- a/client_test.go +++ b/client_test.go @@ -428,6 +428,99 @@ paths: } } +func TestOpenAPIClient_Bad_ValidatesQueryParameterAgainstSchema(t *testing.T) { + called := make(chan struct{}, 1) + mux := http.NewServeMux() + mux.HandleFunc("/search", 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: + /search: + get: + operationId: search_items + parameters: + - name: page + in: query + schema: + type: integer +`) + + client := api.NewOpenAPIClient( + api.WithSpec(specPath), + api.WithBaseURL(srv.URL), + ) + + if _, err := client.Call("search_items", map[string]any{ + "page": "two", + }); err == nil { + t.Fatal("expected query parameter validation error, got nil") + } + + select { + case <-called: + t.Fatal("expected validation to fail before the HTTP call") + default: + } +} + +func TestOpenAPIClient_Bad_ValidatesPathParameterAgainstSchema(t *testing.T) { + called := make(chan struct{}, 1) + mux := http.NewServeMux() + mux.HandleFunc("/users/123", 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: + /users/{id}: + get: + operationId: get_user + parameters: + - name: id + in: path + required: true + schema: + type: integer +`) + + client := api.NewOpenAPIClient( + api.WithSpec(specPath), + api.WithBaseURL(srv.URL), + ) + + if _, err := client.Call("get_user", map[string]any{ + "path": map[string]any{ + "id": "abc", + }, + }); err == nil { + t.Fatal("expected path 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()