2026-04-01 14:16:10 +00:00
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
|
|
|
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-04-03 04:42:14 +00:00
|
|
|
"iter"
|
2026-04-01 14:16:10 +00:00
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
2026-04-01 16:42:09 +00:00
|
|
|
"reflect"
|
2026-04-03 04:42:14 +00:00
|
|
|
"sort"
|
2026-04-01 14:16:10 +00:00
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
|
2026-04-03 04:42:14 +00:00
|
|
|
"slices"
|
|
|
|
|
|
2026-04-01 14:16:10 +00:00
|
|
|
"gopkg.in/yaml.v3"
|
2026-04-01 21:32:21 +00:00
|
|
|
|
|
|
|
|
coreerr "dappco.re/go/core/log"
|
2026-04-01 14:16:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// OpenAPIClient is a small runtime client that can call operations by their
|
|
|
|
|
// OpenAPI operationId. It loads the spec once, resolves the HTTP method and
|
|
|
|
|
// path for each operation, and performs JSON request/response handling.
|
2026-04-01 20:17:46 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// client := api.NewOpenAPIClient(api.WithSpec("./openapi.yaml"), api.WithBaseURL("https://api.example.com"))
|
|
|
|
|
// data, err := client.Call("get_health", nil)
|
2026-04-01 14:16:10 +00:00
|
|
|
type OpenAPIClient struct {
|
|
|
|
|
specPath string
|
2026-04-01 23:56:26 +00:00
|
|
|
specReader io.Reader
|
2026-04-01 14:16:10 +00:00
|
|
|
baseURL string
|
|
|
|
|
bearerToken string
|
|
|
|
|
httpClient *http.Client
|
|
|
|
|
|
|
|
|
|
once sync.Once
|
|
|
|
|
operations map[string]openAPIOperation
|
|
|
|
|
servers []string
|
|
|
|
|
loadErr error
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 04:42:14 +00:00
|
|
|
// OpenAPIOperation snapshots the public metadata for a single loaded OpenAPI
|
|
|
|
|
// operation.
|
2026-04-03 04:45:03 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// ops, err := client.Operations()
|
|
|
|
|
// if err == nil && len(ops) > 0 {
|
|
|
|
|
// fmt.Println(ops[0].OperationID, ops[0].PathTemplate)
|
|
|
|
|
// }
|
2026-04-03 04:42:14 +00:00
|
|
|
type OpenAPIOperation struct {
|
|
|
|
|
OperationID string
|
|
|
|
|
Method string
|
|
|
|
|
PathTemplate string
|
|
|
|
|
HasRequestBody bool
|
|
|
|
|
Parameters []OpenAPIParameter
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OpenAPIParameter snapshots a single OpenAPI parameter definition.
|
2026-04-03 04:45:03 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// op, err := client.Operations()
|
|
|
|
|
// if err == nil && len(op) > 0 && len(op[0].Parameters) > 0 {
|
|
|
|
|
// fmt.Println(op[0].Parameters[0].Name, op[0].Parameters[0].In)
|
|
|
|
|
// }
|
2026-04-03 04:42:14 +00:00
|
|
|
type OpenAPIParameter struct {
|
|
|
|
|
Name string
|
|
|
|
|
In string
|
|
|
|
|
Required bool
|
|
|
|
|
Schema map[string]any
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 14:16:10 +00:00
|
|
|
type openAPIOperation struct {
|
|
|
|
|
method string
|
|
|
|
|
pathTemplate string
|
|
|
|
|
hasRequestBody bool
|
2026-04-01 19:50:41 +00:00
|
|
|
parameters []openAPIParameter
|
2026-04-01 17:48:49 +00:00
|
|
|
requestSchema map[string]any
|
|
|
|
|
responseSchema map[string]any
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 19:50:41 +00:00
|
|
|
type openAPIParameter struct {
|
2026-04-02 00:00:39 +00:00
|
|
|
name string
|
|
|
|
|
in string
|
|
|
|
|
required bool
|
2026-04-02 08:27:25 +00:00
|
|
|
schema map[string]any
|
2026-04-01 19:50:41 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 14:16:10 +00:00
|
|
|
// OpenAPIClientOption configures a runtime OpenAPI client.
|
2026-04-02 07:51:21 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// client := api.NewOpenAPIClient(api.WithSpec("./openapi.yaml"))
|
2026-04-01 14:16:10 +00:00
|
|
|
type OpenAPIClientOption func(*OpenAPIClient)
|
|
|
|
|
|
|
|
|
|
// WithSpec sets the filesystem path to the OpenAPI document.
|
2026-04-01 20:23:41 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// client := api.NewOpenAPIClient(api.WithSpec("./openapi.yaml"))
|
2026-04-01 14:16:10 +00:00
|
|
|
func WithSpec(path string) OpenAPIClientOption {
|
|
|
|
|
return func(c *OpenAPIClient) {
|
|
|
|
|
c.specPath = path
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 23:56:26 +00:00
|
|
|
// WithSpecReader sets an in-memory or streamed OpenAPI document source.
|
|
|
|
|
// It is read once the first time the client loads its spec.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// client := api.NewOpenAPIClient(api.WithSpecReader(strings.NewReader(spec)))
|
|
|
|
|
func WithSpecReader(reader io.Reader) OpenAPIClientOption {
|
|
|
|
|
return func(c *OpenAPIClient) {
|
|
|
|
|
c.specReader = reader
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 14:16:10 +00:00
|
|
|
// WithBaseURL sets the base URL used for outgoing requests.
|
2026-04-01 20:23:41 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// client := api.NewOpenAPIClient(api.WithBaseURL("https://api.example.com"))
|
2026-04-01 14:16:10 +00:00
|
|
|
func WithBaseURL(baseURL string) OpenAPIClientOption {
|
|
|
|
|
return func(c *OpenAPIClient) {
|
|
|
|
|
c.baseURL = baseURL
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WithBearerToken sets the Authorization bearer token used for requests.
|
2026-04-01 20:23:41 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// client := api.NewOpenAPIClient(
|
|
|
|
|
// api.WithBaseURL("https://api.example.com"),
|
|
|
|
|
// api.WithBearerToken("secret-token"),
|
|
|
|
|
// )
|
2026-04-01 14:16:10 +00:00
|
|
|
func WithBearerToken(token string) OpenAPIClientOption {
|
|
|
|
|
return func(c *OpenAPIClient) {
|
|
|
|
|
c.bearerToken = token
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WithHTTPClient sets the HTTP client used to execute requests.
|
2026-04-01 20:23:41 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// client := api.NewOpenAPIClient(api.WithHTTPClient(http.DefaultClient))
|
2026-04-01 14:16:10 +00:00
|
|
|
func WithHTTPClient(client *http.Client) OpenAPIClientOption {
|
|
|
|
|
return func(c *OpenAPIClient) {
|
|
|
|
|
c.httpClient = client
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewOpenAPIClient constructs a runtime client for calling OpenAPI operations.
|
2026-04-01 20:17:46 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// client := api.NewOpenAPIClient(api.WithSpec("./openapi.yaml"))
|
2026-04-01 14:16:10 +00:00
|
|
|
func NewOpenAPIClient(opts ...OpenAPIClientOption) *OpenAPIClient {
|
|
|
|
|
c := &OpenAPIClient{
|
|
|
|
|
httpClient: http.DefaultClient,
|
|
|
|
|
}
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(c)
|
|
|
|
|
}
|
|
|
|
|
if c.httpClient == nil {
|
|
|
|
|
c.httpClient = http.DefaultClient
|
|
|
|
|
}
|
|
|
|
|
return c
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 04:42:14 +00:00
|
|
|
// Operations returns a snapshot of the operations loaded from the OpenAPI
|
|
|
|
|
// document.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// ops, err := client.Operations()
|
|
|
|
|
func (c *OpenAPIClient) Operations() ([]OpenAPIOperation, error) {
|
|
|
|
|
if err := c.load(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
operations := make([]OpenAPIOperation, 0, len(c.operations))
|
|
|
|
|
for operationID, op := range c.operations {
|
|
|
|
|
operations = append(operations, snapshotOpenAPIOperation(operationID, op))
|
|
|
|
|
}
|
|
|
|
|
sort.SliceStable(operations, func(i, j int) bool {
|
|
|
|
|
if operations[i].OperationID == operations[j].OperationID {
|
|
|
|
|
if operations[i].Method == operations[j].Method {
|
|
|
|
|
return operations[i].PathTemplate < operations[j].PathTemplate
|
|
|
|
|
}
|
|
|
|
|
return operations[i].Method < operations[j].Method
|
|
|
|
|
}
|
|
|
|
|
return operations[i].OperationID < operations[j].OperationID
|
|
|
|
|
})
|
|
|
|
|
return operations, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OperationsIter returns an iterator over the loaded OpenAPI operations.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
2026-04-03 04:50:54 +00:00
|
|
|
// ops, err := client.OperationsIter()
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// panic(err)
|
|
|
|
|
// }
|
|
|
|
|
// for op := range ops {
|
|
|
|
|
// fmt.Println(op.OperationID, op.PathTemplate)
|
2026-04-03 04:42:14 +00:00
|
|
|
// }
|
|
|
|
|
func (c *OpenAPIClient) OperationsIter() (iter.Seq[OpenAPIOperation], error) {
|
|
|
|
|
operations, err := c.Operations()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return func(yield func(OpenAPIOperation) bool) {
|
|
|
|
|
for _, op := range operations {
|
|
|
|
|
if !yield(op) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Servers returns a snapshot of the server URLs discovered from the OpenAPI
|
|
|
|
|
// document.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// servers, err := client.Servers()
|
|
|
|
|
func (c *OpenAPIClient) Servers() ([]string, error) {
|
|
|
|
|
if err := c.load(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return slices.Clone(c.servers), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ServersIter returns an iterator over the server URLs discovered from the
|
|
|
|
|
// OpenAPI document.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
2026-04-03 04:50:54 +00:00
|
|
|
// servers, err := client.ServersIter()
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// panic(err)
|
|
|
|
|
// }
|
|
|
|
|
// for server := range servers {
|
|
|
|
|
// fmt.Println(server)
|
2026-04-03 04:42:14 +00:00
|
|
|
// }
|
|
|
|
|
func (c *OpenAPIClient) ServersIter() (iter.Seq[string], error) {
|
|
|
|
|
servers, err := c.Servers()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return func(yield func(string) bool) {
|
|
|
|
|
for _, server := range servers {
|
|
|
|
|
if !yield(server) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 14:16:10 +00:00
|
|
|
// Call invokes the operation with the given operationId.
|
|
|
|
|
//
|
|
|
|
|
// The params argument may be a map, struct, or nil. For convenience, a map may
|
2026-04-01 19:50:41 +00:00
|
|
|
// include "path", "query", "header", "cookie", and "body" keys to explicitly
|
|
|
|
|
// control where the values are sent. When no explicit body is provided,
|
|
|
|
|
// requests with a declared requestBody send the remaining parameters as JSON.
|
2026-04-01 20:17:46 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// data, err := client.Call("create_item", map[string]any{"name": "alpha"})
|
2026-04-01 14:16:10 +00:00
|
|
|
func (c *OpenAPIClient) Call(operationID string, params any) (any, error) {
|
|
|
|
|
if err := c.load(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if c.httpClient == nil {
|
|
|
|
|
c.httpClient = http.DefaultClient
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
op, ok := c.operations[operationID]
|
|
|
|
|
if !ok {
|
2026-04-01 21:32:21 +00:00
|
|
|
return nil, coreerr.E("OpenAPIClient.Call", fmt.Sprintf("operation %q not found in OpenAPI spec", operationID), nil)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
merged, err := normaliseParams(params)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestURL, err := c.buildURL(op, merged)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body, err := c.buildBody(op, merged)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 17:48:49 +00:00
|
|
|
if op.requestSchema != nil && len(body) > 0 {
|
|
|
|
|
if err := validateOpenAPISchema(body, op.requestSchema, "request body"); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var bodyReader io.Reader
|
|
|
|
|
if len(body) > 0 {
|
|
|
|
|
bodyReader = bytes.NewReader(body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(op.method, requestURL, bodyReader)
|
2026-04-01 14:16:10 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-01 17:48:49 +00:00
|
|
|
if bodyReader != nil {
|
2026-04-01 14:16:10 +00:00
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
}
|
|
|
|
|
if c.bearerToken != "" {
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.bearerToken)
|
|
|
|
|
}
|
2026-04-01 19:50:41 +00:00
|
|
|
applyRequestParameters(req, op, merged)
|
2026-04-01 14:16:10 +00:00
|
|
|
|
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
payload, err := io.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
2026-04-01 21:32:21 +00:00
|
|
|
return nil, coreerr.E("OpenAPIClient.Call", fmt.Sprintf("openapi call %s returned %s: %s", operationID, resp.Status, strings.TrimSpace(string(payload))), nil)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 17:48:49 +00:00
|
|
|
if op.responseSchema != nil && len(bytes.TrimSpace(payload)) > 0 {
|
|
|
|
|
if err := validateOpenAPIResponse(payload, op.responseSchema, operationID); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 14:16:10 +00:00
|
|
|
if len(bytes.TrimSpace(payload)) == 0 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var decoded any
|
|
|
|
|
dec := json.NewDecoder(bytes.NewReader(payload))
|
|
|
|
|
dec.UseNumber()
|
|
|
|
|
if err := dec.Decode(&decoded); err != nil {
|
|
|
|
|
return string(payload), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if envelope, ok := decoded.(map[string]any); ok {
|
|
|
|
|
if success, ok := envelope["success"].(bool); ok {
|
|
|
|
|
if !success {
|
|
|
|
|
if errObj, ok := envelope["error"].(map[string]any); ok {
|
2026-04-01 21:32:21 +00:00
|
|
|
return nil, coreerr.E("OpenAPIClient.Call", fmt.Sprintf("openapi call %s failed: %v", operationID, errObj), nil)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
2026-04-01 21:32:21 +00:00
|
|
|
return nil, coreerr.E("OpenAPIClient.Call", fmt.Sprintf("openapi call %s failed", operationID), nil)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
if data, ok := envelope["data"]; ok {
|
|
|
|
|
return data, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return decoded, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *OpenAPIClient) load() error {
|
|
|
|
|
c.once.Do(func() {
|
|
|
|
|
c.loadErr = c.loadSpec()
|
|
|
|
|
})
|
|
|
|
|
return c.loadErr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *OpenAPIClient) loadSpec() error {
|
2026-04-01 23:56:26 +00:00
|
|
|
var (
|
|
|
|
|
data []byte
|
|
|
|
|
err error
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
case c.specReader != nil:
|
|
|
|
|
data, err = io.ReadAll(c.specReader)
|
|
|
|
|
case c.specPath != "":
|
|
|
|
|
f, openErr := os.Open(c.specPath)
|
|
|
|
|
if openErr != nil {
|
|
|
|
|
return coreerr.E("OpenAPIClient.loadSpec", "read spec", openErr)
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
data, err = io.ReadAll(f)
|
|
|
|
|
default:
|
|
|
|
|
return coreerr.E("OpenAPIClient.loadSpec", "spec path or reader is required", nil)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
2026-04-01 21:32:21 +00:00
|
|
|
return coreerr.E("OpenAPIClient.loadSpec", "read spec", err)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var spec map[string]any
|
|
|
|
|
if err := yaml.Unmarshal(data, &spec); err != nil {
|
2026-04-01 21:32:21 +00:00
|
|
|
return coreerr.E("OpenAPIClient.loadSpec", "parse spec", err)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
operations := make(map[string]openAPIOperation)
|
|
|
|
|
if paths, ok := spec["paths"].(map[string]any); ok {
|
|
|
|
|
for pathTemplate, rawPathItem := range paths {
|
|
|
|
|
pathItem, ok := rawPathItem.(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for method, rawOperation := range pathItem {
|
|
|
|
|
operation, ok := rawOperation.(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
operationID, _ := operation["operationId"].(string)
|
|
|
|
|
if operationID == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-04-01 19:50:41 +00:00
|
|
|
params := parseOperationParameters(operation)
|
2026-04-01 14:16:10 +00:00
|
|
|
operations[operationID] = openAPIOperation{
|
|
|
|
|
method: strings.ToUpper(method),
|
|
|
|
|
pathTemplate: pathTemplate,
|
|
|
|
|
hasRequestBody: operation["requestBody"] != nil,
|
2026-04-01 19:50:41 +00:00
|
|
|
parameters: params,
|
2026-04-01 17:48:49 +00:00
|
|
|
requestSchema: requestBodySchema(operation),
|
|
|
|
|
responseSchema: firstSuccessResponseSchema(operation),
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.operations = operations
|
|
|
|
|
if servers, ok := spec["servers"].([]any); ok {
|
|
|
|
|
for _, rawServer := range servers {
|
|
|
|
|
server, ok := rawServer.(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if u, _ := server["url"].(string); u != "" {
|
|
|
|
|
c.servers = append(c.servers, u)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-01 17:31:45 +00:00
|
|
|
c.servers = normaliseServers(c.servers)
|
2026-04-01 14:16:10 +00:00
|
|
|
|
2026-04-01 17:24:36 +00:00
|
|
|
if c.baseURL == "" {
|
|
|
|
|
for _, server := range c.servers {
|
|
|
|
|
if isAbsoluteBaseURL(server) {
|
|
|
|
|
c.baseURL = server
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 04:42:14 +00:00
|
|
|
func snapshotOpenAPIOperation(operationID string, op openAPIOperation) OpenAPIOperation {
|
|
|
|
|
parameters := make([]OpenAPIParameter, len(op.parameters))
|
|
|
|
|
for i, param := range op.parameters {
|
|
|
|
|
parameters[i] = OpenAPIParameter{
|
|
|
|
|
Name: param.name,
|
|
|
|
|
In: param.in,
|
|
|
|
|
Required: param.required,
|
|
|
|
|
Schema: cloneOpenAPIObject(param.schema),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return OpenAPIOperation{
|
|
|
|
|
OperationID: operationID,
|
|
|
|
|
Method: strings.ToUpper(op.method),
|
|
|
|
|
PathTemplate: op.pathTemplate,
|
|
|
|
|
HasRequestBody: op.hasRequestBody,
|
|
|
|
|
Parameters: parameters,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 14:16:10 +00:00
|
|
|
func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (string, error) {
|
|
|
|
|
base := strings.TrimRight(c.baseURL, "/")
|
|
|
|
|
if base == "" {
|
2026-04-01 21:32:21 +00:00
|
|
|
return "", coreerr.E("OpenAPIClient.buildURL", "base URL is required", nil)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
path := op.pathTemplate
|
|
|
|
|
pathKeys := pathParameterNames(path)
|
|
|
|
|
pathValues := map[string]any{}
|
|
|
|
|
if explicitPath, ok := nestedMap(params, "path"); ok {
|
|
|
|
|
pathValues = explicitPath
|
|
|
|
|
} else {
|
|
|
|
|
pathValues = params
|
|
|
|
|
}
|
2026-04-02 00:00:39 +00:00
|
|
|
|
|
|
|
|
if err := validateRequiredParameters(op, params, pathKeys); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2026-04-02 08:27:25 +00:00
|
|
|
if err := validateParameterValues(op, params); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2026-04-02 00:00:39 +00:00
|
|
|
|
2026-04-01 14:16:10 +00:00
|
|
|
for _, key := range pathKeys {
|
|
|
|
|
if value, ok := pathValues[key]; ok {
|
|
|
|
|
placeholder := "{" + key + "}"
|
|
|
|
|
path = strings.ReplaceAll(path, placeholder, url.PathEscape(fmt.Sprint(value)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.Contains(path, "{") {
|
2026-04-01 21:32:21 +00:00
|
|
|
return "", coreerr.E("OpenAPIClient.buildURL", fmt.Sprintf("missing path parameters for %q", op.pathTemplate), nil)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fullURL, err := url.JoinPath(base, path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query := url.Values{}
|
|
|
|
|
if explicitQuery, ok := nestedMap(params, "query"); ok {
|
|
|
|
|
for key, value := range explicitQuery {
|
2026-04-01 16:42:09 +00:00
|
|
|
appendQueryValue(query, key, value)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-01 21:19:45 +00:00
|
|
|
for key, value := range params {
|
|
|
|
|
if key == "path" || key == "body" || key == "query" || key == "header" || key == "cookie" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if containsString(pathKeys, key) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
location := operationParameterLocation(op, key)
|
|
|
|
|
if location != "query" && !(location == "" && (op.method == http.MethodGet || (op.method == http.MethodHead && !op.hasRequestBody))) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, exists := query[key]; exists {
|
|
|
|
|
continue
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
2026-04-01 21:19:45 +00:00
|
|
|
appendQueryValue(query, key, value)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if encoded := query.Encode(); encoded != "" {
|
|
|
|
|
fullURL += "?" + encoded
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fullURL, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 17:48:49 +00:00
|
|
|
func (c *OpenAPIClient) buildBody(op openAPIOperation, params map[string]any) ([]byte, error) {
|
2026-04-01 14:16:10 +00:00
|
|
|
if explicitBody, ok := params["body"]; ok {
|
|
|
|
|
return encodeJSONBody(explicitBody)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 16:37:13 +00:00
|
|
|
if op.method == http.MethodGet || (op.method == http.MethodHead && !op.hasRequestBody) {
|
2026-04-01 14:16:10 +00:00
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(params) == 0 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pathKeys := pathParameterNames(op.pathTemplate)
|
|
|
|
|
queryKeys := map[string]struct{}{}
|
|
|
|
|
if explicitQuery, ok := nestedMap(params, "query"); ok {
|
|
|
|
|
for key := range explicitQuery {
|
|
|
|
|
queryKeys[key] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
payload := make(map[string]any, len(params))
|
|
|
|
|
for key, value := range params {
|
2026-04-01 19:50:41 +00:00
|
|
|
if key == "path" || key == "query" || key == "body" || key == "header" || key == "cookie" {
|
2026-04-01 14:16:10 +00:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if containsString(pathKeys, key) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-04-01 19:50:41 +00:00
|
|
|
switch operationParameterLocation(op, key) {
|
|
|
|
|
case "header", "cookie", "query":
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-04-01 14:16:10 +00:00
|
|
|
if _, exists := queryKeys[key]; exists {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
payload[key] = value
|
|
|
|
|
}
|
|
|
|
|
if len(payload) == 0 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
return encodeJSONBody(payload)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 19:50:41 +00:00
|
|
|
func applyRequestParameters(req *http.Request, op openAPIOperation, params map[string]any) {
|
|
|
|
|
explicitHeaders, hasExplicitHeaders := nestedMap(params, "header")
|
|
|
|
|
explicitCookies, hasExplicitCookies := nestedMap(params, "cookie")
|
|
|
|
|
|
|
|
|
|
if hasExplicitHeaders {
|
|
|
|
|
applyHeaderValues(req.Header, explicitHeaders)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applyTopLevelHeaderParameters(req.Header, op, params, explicitHeaders, hasExplicitHeaders)
|
|
|
|
|
|
|
|
|
|
if hasExplicitCookies {
|
|
|
|
|
applyCookieValues(req, explicitCookies)
|
|
|
|
|
}
|
|
|
|
|
applyTopLevelCookieParameters(req, op, params, explicitCookies, hasExplicitCookies)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyTopLevelHeaderParameters(headers http.Header, op openAPIOperation, params, explicit map[string]any, hasExplicit bool) {
|
|
|
|
|
for key, value := range params {
|
|
|
|
|
if key == "path" || key == "query" || key == "body" || key == "header" || key == "cookie" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if operationParameterLocation(op, key) != "header" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if hasExplicit {
|
|
|
|
|
if _, ok := explicit[key]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
applyHeaderValue(headers, key, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyTopLevelCookieParameters(req *http.Request, op openAPIOperation, params, explicit map[string]any, hasExplicit bool) {
|
|
|
|
|
for key, value := range params {
|
|
|
|
|
if key == "path" || key == "query" || key == "body" || key == "header" || key == "cookie" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if operationParameterLocation(op, key) != "cookie" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if hasExplicit {
|
|
|
|
|
if _, ok := explicit[key]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
applyCookieValue(req, key, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyHeaderValues(headers http.Header, values map[string]any) {
|
|
|
|
|
for key, value := range values {
|
|
|
|
|
applyHeaderValue(headers, key, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyHeaderValue(headers http.Header, key string, value any) {
|
|
|
|
|
switch v := value.(type) {
|
|
|
|
|
case nil:
|
|
|
|
|
return
|
|
|
|
|
case []string:
|
|
|
|
|
for _, item := range v {
|
|
|
|
|
headers.Add(key, item)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
case []any:
|
|
|
|
|
for _, item := range v {
|
|
|
|
|
headers.Add(key, fmt.Sprint(item))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rv := reflect.ValueOf(value)
|
|
|
|
|
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) && !(rv.Type().Elem().Kind() == reflect.Uint8) {
|
|
|
|
|
for i := 0; i < rv.Len(); i++ {
|
|
|
|
|
headers.Add(key, fmt.Sprint(rv.Index(i).Interface()))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
headers.Set(key, fmt.Sprint(value))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyCookieValues(req *http.Request, values map[string]any) {
|
|
|
|
|
for key, value := range values {
|
|
|
|
|
applyCookieValue(req, key, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyCookieValue(req *http.Request, key string, value any) {
|
|
|
|
|
switch v := value.(type) {
|
|
|
|
|
case nil:
|
|
|
|
|
return
|
|
|
|
|
case []string:
|
|
|
|
|
for _, item := range v {
|
|
|
|
|
req.AddCookie(&http.Cookie{Name: key, Value: item})
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
case []any:
|
|
|
|
|
for _, item := range v {
|
|
|
|
|
req.AddCookie(&http.Cookie{Name: key, Value: fmt.Sprint(item)})
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rv := reflect.ValueOf(value)
|
|
|
|
|
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) && !(rv.Type().Elem().Kind() == reflect.Uint8) {
|
|
|
|
|
for i := 0; i < rv.Len(); i++ {
|
|
|
|
|
req.AddCookie(&http.Cookie{Name: key, Value: fmt.Sprint(rv.Index(i).Interface())})
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req.AddCookie(&http.Cookie{Name: key, Value: fmt.Sprint(value)})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseOperationParameters(operation map[string]any) []openAPIParameter {
|
|
|
|
|
rawParams, ok := operation["parameters"].([]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
params := make([]openAPIParameter, 0, len(rawParams))
|
|
|
|
|
for _, rawParam := range rawParams {
|
|
|
|
|
param, ok := rawParam.(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
name, _ := param["name"].(string)
|
|
|
|
|
in, _ := param["in"].(string)
|
|
|
|
|
if name == "" || in == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-04-02 00:00:39 +00:00
|
|
|
required, _ := param["required"].(bool)
|
2026-04-02 08:27:25 +00:00
|
|
|
schema, _ := param["schema"].(map[string]any)
|
|
|
|
|
params = append(params, openAPIParameter{name: name, in: in, required: required, schema: schema})
|
2026-04-01 19:50:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return params
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func operationParameterLocation(op openAPIOperation, name string) string {
|
|
|
|
|
for _, param := range op.parameters {
|
|
|
|
|
if param.name == name {
|
|
|
|
|
return param.in
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 08:27:25 +00:00
|
|
|
func validateParameterValues(op openAPIOperation, params map[string]any) error {
|
|
|
|
|
for _, param := range op.parameters {
|
|
|
|
|
if len(param.schema) == 0 {
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 00:00:39 +00:00
|
|
|
func validateRequiredParameters(op openAPIOperation, params map[string]any, pathKeys []string) error {
|
|
|
|
|
for _, param := range op.parameters {
|
|
|
|
|
if !param.required {
|
|
|
|
|
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 {
|
2026-04-02 08:30:44 +00:00
|
|
|
if value, exists := nested[name]; exists && value != nil {
|
2026-04-02 00:00:39 +00:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if value, exists := params[name]; exists {
|
|
|
|
|
if value != nil {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 17:48:49 +00:00
|
|
|
func encodeJSONBody(v any) ([]byte, error) {
|
2026-04-01 14:16:10 +00:00
|
|
|
data, err := json.Marshal(v)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-01 17:48:49 +00:00
|
|
|
return data, nil
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func normaliseParams(params any) (map[string]any, error) {
|
|
|
|
|
if params == nil {
|
|
|
|
|
return map[string]any{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if m, ok := params.(map[string]any); ok {
|
|
|
|
|
return m, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data, err := json.Marshal(params)
|
|
|
|
|
if err != nil {
|
2026-04-01 21:32:21 +00:00
|
|
|
return nil, coreerr.E("OpenAPIClient.normaliseParams", "marshal params", err)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var out map[string]any
|
|
|
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
2026-04-01 21:32:21 +00:00
|
|
|
return nil, coreerr.E("OpenAPIClient.normaliseParams", "decode params", err)
|
2026-04-01 14:16:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nestedMap(params map[string]any, key string) (map[string]any, bool) {
|
|
|
|
|
raw, ok := params[key]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m, ok := raw.(map[string]any)
|
|
|
|
|
if ok {
|
|
|
|
|
return m, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data, err := json.Marshal(raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
return m, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func pathParameterNames(pathTemplate string) []string {
|
|
|
|
|
var names []string
|
|
|
|
|
for i := 0; i < len(pathTemplate); i++ {
|
|
|
|
|
if pathTemplate[i] != '{' {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
end := strings.IndexByte(pathTemplate[i+1:], '}')
|
|
|
|
|
if end < 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
name := pathTemplate[i+1 : i+1+end]
|
|
|
|
|
if name != "" {
|
|
|
|
|
names = append(names, name)
|
|
|
|
|
}
|
|
|
|
|
i += end + 1
|
|
|
|
|
}
|
|
|
|
|
return names
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containsString(values []string, target string) bool {
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
if value == target {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-04-01 16:42:09 +00:00
|
|
|
|
|
|
|
|
func appendQueryValue(query url.Values, key string, value any) {
|
|
|
|
|
switch v := value.(type) {
|
|
|
|
|
case nil:
|
|
|
|
|
return
|
|
|
|
|
case []byte:
|
|
|
|
|
query.Add(key, string(v))
|
|
|
|
|
return
|
|
|
|
|
case []string:
|
|
|
|
|
for _, item := range v {
|
|
|
|
|
query.Add(key, item)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
case []any:
|
|
|
|
|
for _, item := range v {
|
|
|
|
|
appendQueryValue(query, key, item)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rv := reflect.ValueOf(value)
|
|
|
|
|
if !rv.IsValid() {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch rv.Kind() {
|
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
|
|
|
if rv.Type().Elem().Kind() == reflect.Uint8 {
|
|
|
|
|
query.Add(key, string(rv.Bytes()))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for i := 0; i < rv.Len(); i++ {
|
|
|
|
|
appendQueryValue(query, key, rv.Index(i).Interface())
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query.Add(key, fmt.Sprint(value))
|
|
|
|
|
}
|
2026-04-01 17:24:36 +00:00
|
|
|
|
|
|
|
|
func isAbsoluteBaseURL(raw string) bool {
|
|
|
|
|
u, err := url.Parse(raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return u.Scheme != "" && u.Host != ""
|
|
|
|
|
}
|
2026-04-01 17:48:49 +00:00
|
|
|
|
|
|
|
|
func requestBodySchema(operation map[string]any) map[string]any {
|
|
|
|
|
rawRequestBody, ok := operation["requestBody"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
content, ok := rawRequestBody["content"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rawJSON, ok := content["application/json"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
schema, _ := rawJSON["schema"].(map[string]any)
|
|
|
|
|
return schema
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func firstSuccessResponseSchema(operation map[string]any) map[string]any {
|
|
|
|
|
responses, ok := operation["responses"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, code := range []string{"200", "201", "202", "203", "204", "205", "206", "207", "208", "226"} {
|
|
|
|
|
rawResp, ok := responses[code].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
content, ok := rawResp["content"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
rawJSON, ok := content["application/json"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
schema, _ := rawJSON["schema"].(map[string]any)
|
|
|
|
|
if len(schema) > 0 {
|
|
|
|
|
return schema
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateOpenAPISchema(body []byte, schema map[string]any, label string) error {
|
|
|
|
|
if len(bytes.TrimSpace(body)) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var payload any
|
|
|
|
|
dec := json.NewDecoder(bytes.NewReader(body))
|
|
|
|
|
dec.UseNumber()
|
|
|
|
|
if err := dec.Decode(&payload); err != nil {
|
2026-04-01 21:32:21 +00:00
|
|
|
return coreerr.E("OpenAPIClient.validateOpenAPISchema", fmt.Sprintf("validate %s: invalid JSON", label), err)
|
2026-04-01 17:48:49 +00:00
|
|
|
}
|
|
|
|
|
var extra any
|
|
|
|
|
if err := dec.Decode(&extra); err != io.EOF {
|
2026-04-01 21:32:21 +00:00
|
|
|
return coreerr.E("OpenAPIClient.validateOpenAPISchema", fmt.Sprintf("validate %s: expected a single JSON value", label), nil)
|
2026-04-01 17:48:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := validateSchemaNode(payload, schema, ""); err != nil {
|
2026-04-01 21:32:21 +00:00
|
|
|
return coreerr.E("OpenAPIClient.validateOpenAPISchema", fmt.Sprintf("validate %s", label), err)
|
2026-04-01 17:48:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateOpenAPIResponse(payload []byte, schema map[string]any, operationID string) error {
|
|
|
|
|
var decoded any
|
|
|
|
|
dec := json.NewDecoder(bytes.NewReader(payload))
|
|
|
|
|
dec.UseNumber()
|
|
|
|
|
if err := dec.Decode(&decoded); err != nil {
|
2026-04-01 21:32:21 +00:00
|
|
|
return coreerr.E("OpenAPIClient.validateOpenAPIResponse", fmt.Sprintf("openapi call %s returned invalid JSON", operationID), err)
|
2026-04-01 17:48:49 +00:00
|
|
|
}
|
|
|
|
|
var extra any
|
|
|
|
|
if err := dec.Decode(&extra); err != io.EOF {
|
2026-04-01 21:32:21 +00:00
|
|
|
return coreerr.E("OpenAPIClient.validateOpenAPIResponse", fmt.Sprintf("openapi call %s returned multiple JSON values", operationID), nil)
|
2026-04-01 17:48:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := validateSchemaNode(decoded, schema, ""); err != nil {
|
2026-04-01 21:32:21 +00:00
|
|
|
return coreerr.E("OpenAPIClient.validateOpenAPIResponse", fmt.Sprintf("openapi call %s response does not match spec", operationID), err)
|
2026-04-01 17:48:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|