feat(api): allow openapi specs from readers

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 23:56:26 +00:00
parent 799de22d4d
commit 1f43f019b1
2 changed files with 79 additions and 9 deletions

View file

@ -29,6 +29,7 @@ import (
// data, err := client.Call("get_health", nil)
type OpenAPIClient struct {
specPath string
specReader io.Reader
baseURL string
bearerToken string
httpClient *http.Client
@ -67,6 +68,18 @@ func WithSpec(path string) OpenAPIClientOption {
}
}
// 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
}
}
// WithBaseURL sets the base URL used for outgoing requests.
//
// Example:
@ -239,17 +252,25 @@ func (c *OpenAPIClient) load() error {
}
func (c *OpenAPIClient) loadSpec() error {
if c.specPath == "" {
return coreerr.E("OpenAPIClient.loadSpec", "spec path is required", nil)
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)
}
f, err := os.Open(c.specPath)
if err != nil {
return coreerr.E("OpenAPIClient.loadSpec", "read spec", err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return coreerr.E("OpenAPIClient.loadSpec", "read spec", err)
}

View file

@ -9,6 +9,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
api "dappco.re/go/core/api"
@ -125,6 +126,54 @@ paths:
}
}
func TestOpenAPIClient_Good_LoadsSpecFromReader(t *testing.T) {
errCh := make(chan error, 1)
mux := http.NewServeMux()
mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
errCh <- fmt.Errorf("expected GET, got %s", r.Method)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"message":"pong"}}`))
})
srv := httptest.NewServer(mux)
defer srv.Close()
client := api.NewOpenAPIClient(
api.WithSpecReader(strings.NewReader(`openapi: 3.1.0
info:
title: Test API
version: 1.0.0
paths:
/ping:
get:
operationId: ping
`)),
api.WithBaseURL(srv.URL),
)
result, err := client.Call("ping", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
select {
case err := <-errCh:
t.Fatal(err)
default:
}
ping, ok := result.(map[string]any)
if !ok {
t.Fatalf("expected map result, got %T", result)
}
if ping["message"] != "pong" {
t.Fatalf("expected message=pong, got %#v", ping["message"])
}
}
func TestOpenAPIClient_Good_CallHeadOperationWithRequestBody(t *testing.T) {
errCh := make(chan error, 1)
mux := http.NewServeMux()