feat(api): validate required openapi parameters

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 00:00:39 +00:00
parent 1f43f019b1
commit f67e3fe5de
2 changed files with 86 additions and 3 deletions

View file

@ -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 {

View file

@ -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()