fix(api): expose OpenAPI client snapshots
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
76acb4534b
commit
8b5e572d1c
2 changed files with 226 additions and 0 deletions
128
client.go
128
client.go
|
|
@ -7,13 +7,17 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"slices"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -40,6 +44,24 @@ type OpenAPIClient struct {
|
|||
loadErr error
|
||||
}
|
||||
|
||||
// OpenAPIOperation snapshots the public metadata for a single loaded OpenAPI
|
||||
// operation.
|
||||
type OpenAPIOperation struct {
|
||||
OperationID string
|
||||
Method string
|
||||
PathTemplate string
|
||||
HasRequestBody bool
|
||||
Parameters []OpenAPIParameter
|
||||
}
|
||||
|
||||
// OpenAPIParameter snapshots a single OpenAPI parameter definition.
|
||||
type OpenAPIParameter struct {
|
||||
Name string
|
||||
In string
|
||||
Required bool
|
||||
Schema map[string]any
|
||||
}
|
||||
|
||||
type openAPIOperation struct {
|
||||
method string
|
||||
pathTemplate string
|
||||
|
|
@ -140,6 +162,92 @@ func NewOpenAPIClient(opts ...OpenAPIClientOption) *OpenAPIClient {
|
|||
return c
|
||||
}
|
||||
|
||||
// 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:
|
||||
//
|
||||
// for op := range client.OperationsIter() {
|
||||
// _ = op
|
||||
// }
|
||||
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:
|
||||
//
|
||||
// for server := range client.ServersIter() {
|
||||
// _ = server
|
||||
// }
|
||||
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
|
||||
}
|
||||
|
||||
// Call invokes the operation with the given operationId.
|
||||
//
|
||||
// The params argument may be a map, struct, or nil. For convenience, a map may
|
||||
|
|
@ -341,6 +449,26 @@ func (c *OpenAPIClient) loadSpec() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (string, error) {
|
||||
base := strings.TrimRight(c.baseURL, "/")
|
||||
if base == "" {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"slices"
|
||||
|
||||
api "dappco.re/go/core/api"
|
||||
)
|
||||
|
||||
|
|
@ -174,6 +176,102 @@ paths:
|
|||
}
|
||||
}
|
||||
|
||||
func TestOpenAPIClient_Good_ExposesOperationSnapshots(t *testing.T) {
|
||||
specPath := writeTempSpec(t, `openapi: 3.1.0
|
||||
info:
|
||||
title: Test API
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: https://api.example.com
|
||||
paths:
|
||||
/users/{id}:
|
||||
post:
|
||||
operationId: update_user
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
`)
|
||||
|
||||
client := api.NewOpenAPIClient(api.WithSpec(specPath))
|
||||
|
||||
operations, err := client.Operations()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(operations) != 1 {
|
||||
t.Fatalf("expected 1 operation, got %d", len(operations))
|
||||
}
|
||||
|
||||
op := operations[0]
|
||||
if op.OperationID != "update_user" {
|
||||
t.Fatalf("expected operationId update_user, got %q", op.OperationID)
|
||||
}
|
||||
if op.Method != http.MethodPost {
|
||||
t.Fatalf("expected method POST, got %q", op.Method)
|
||||
}
|
||||
if op.PathTemplate != "/users/{id}" {
|
||||
t.Fatalf("expected path template /users/{id}, got %q", op.PathTemplate)
|
||||
}
|
||||
if !op.HasRequestBody {
|
||||
t.Fatal("expected operation to report a request body")
|
||||
}
|
||||
if len(op.Parameters) != 1 || op.Parameters[0].Name != "id" {
|
||||
t.Fatalf("expected one path parameter snapshot, got %+v", op.Parameters)
|
||||
}
|
||||
|
||||
op.Parameters[0].Schema["type"] = "integer"
|
||||
operations[0].PathTemplate = "/mutated"
|
||||
|
||||
again, err := client.Operations()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error on re-read: %v", err)
|
||||
}
|
||||
if again[0].PathTemplate != "/users/{id}" {
|
||||
t.Fatalf("expected snapshot to remain immutable, got %q", again[0].PathTemplate)
|
||||
}
|
||||
if got := again[0].Parameters[0].Schema["type"]; got != "string" {
|
||||
t.Fatalf("expected cloned parameter schema, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAPIClient_Good_ExposesServerSnapshots(t *testing.T) {
|
||||
client := api.NewOpenAPIClient(api.WithSpecReader(strings.NewReader(`openapi: 3.1.0
|
||||
info:
|
||||
title: Test API
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: https://api.example.com
|
||||
- url: /relative
|
||||
paths: {}
|
||||
`)))
|
||||
|
||||
servers, err := client.Servers()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !slices.Equal(servers, []string{"https://api.example.com", "/relative"}) {
|
||||
t.Fatalf("expected server snapshot to preserve order, got %v", servers)
|
||||
}
|
||||
|
||||
servers[0] = "https://mutated.example.com"
|
||||
again, err := client.Servers()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error on re-read: %v", err)
|
||||
}
|
||||
if !slices.Equal(again, []string{"https://api.example.com", "/relative"}) {
|
||||
t.Fatalf("expected server snapshot to be cloned, got %v", again)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAPIClient_Good_CallHeadOperationWithRequestBody(t *testing.T) {
|
||||
errCh := make(chan error, 1)
|
||||
mux := http.NewServeMux()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue