openapi

package module
v0.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

README

Okapi

Okapi is a standalone OpenAPI 3.x client library for Go. Give it an OpenAPI spec, and it gives you typed Go endpoints — no code generation step required at runtime (unless you want compile-time safety with go generate).

It parses your spec, builds a OpenApi struct with endpoint methods, validates request params and bodies against the schema, and makes the HTTP call. It also ships with a CLI generator that turns your spec into a nested command tree automatically.

Features

  • Spec-driven endpoints — parse any OpenAPI 3.x spec (local file, file://, http(s)://) and call endpoints by name
  • Request validation — parameters are type-checked, required params are enforced, request bodies are validated against JSON Schema
  • CLI generationgo generate produces a typed OpenApi struct with endpoint methods as fields; the cli package builds a full command tree from the spec (using urfave/cli)
  • Flexible API client — bring your own HTTP client via the OpenApiClient interface; Okapi doesn't couple you to any specific HTTP library
  • Glamour help output — CLI help text is rendered with glamour markdown styling, including inline request body schemas
  • Zero runtime dependencies on your app's contextCliContext interface lets you bridge Okapi into any CLI framework

Quick Start

As a library
package main

import (
    "fmt"
    "net/http"
    "io"

    "github.com/jathanism/okapi"
    "github.com/jathanism/okapi/request"
)

func main() {
    // Load an OpenAPI spec
    api, err := (*openapi.OpenApi)(nil).NewFromSource("file://openapi.yaml")
    if err != nil {
        panic(err)
    }

    // Bind an API client
    myClient := &MyHttpClient{}
    api = api.WithClient(myClient)

    // Call an endpoint
    err = api.UsersList(
        request.Param("limit", 10),
        request.Param("offset", 0),
        request.Result(&result),
    )
}
CLI generation
# Generate the OpenApi struct from a spec
OKAPI_OPENAPI_SOURCE=https://api.example.com/openapi.json go generate ./...

# Or use flags
go run ./gen/gen.go --source https://api.example.com/openapi.json

This produces openapi_gen.go with typed endpoint fields:

type OpenApi struct {
    internal

    AccountsChangePassword OpenApiEndpoint
    OrganizationsBySlug    OpenApiEndpoint
    OrganizationsUsersCreate OpenApiEndpoint
    OrganizationsUsersList OpenApiEndpoint
    UsersCreate            OpenApiEndpoint
    UsersList              OpenApiEndpoint
    SchemasList            OpenApiEndpoint
    // ...
}

Project Structure

okapi/
├── openapi.go          # Core OpenApi struct — loads specs, builds endpoints, makes calls
├── openapi_gen.go      # Generated OpenApi struct (do not edit — use go generate)
├── openapi_test.go     # Integration tests (Ginkgo/Gomega)
├── error/              # Error types and helpers (OpenApiError, OpenApiValidationError)
├── request/            # Request building — params, body, headers, API client interface
├── spec/               # OpenAPI spec parsing, endpoint/param/body types, validation
├── cli/                # CLI generator — builds urfave/cli command tree from spec
├── gen/                # Code generator — reads spec, writes openapi_gen.go
├── internal/
│   ├── log/            # Structured logging (charmbracelet/log) with trace support
│   └── testutil/       # Test helpers
└── testdata/
    └── openapi.yaml    # Test fixture spec

Packages

openapi (root)

The core package. OpenApi is the main type — it loads a spec, builds endpoint callables, and dispatches requests.

Key types:

  • OpenApi — spec-loaded API client with typed endpoint fields
  • OpenApiEndpointfunc(options ...RequestOption) error — call an endpoint with options
  • OpenApiSpec — alias for spec.OpenApiSpec

Key methods:

  • NewFromSource(source string) — load from file path, URL, or raw string (cached)
  • NewFromBytes(source []byte) — load from raw bytes (not cached)
  • WithClient(client OpenApiClient) — bind an HTTP client to all endpoints
  • With(options ...RequestOption) — clone with request options applied to all endpoints
spec

Parses OpenAPI 3.x specs using pb33f/libopenapi. Handles parameter parsing, request body JSON Schema compilation, and URL building.

request

Functional options for building requests. Param(), Body(), Data(), Header(), Result(). The OpenApiClient interface is what you implement to bring your own HTTP client.

cli

Generates a nested CLI command tree from an OpenAPI spec using urfave/cli. Commands mirror the spec's operationId structure (e.g., users list, organizations users create). Help text includes inline JSON body schemas rendered with glamour.

The CliContext interface bridges Okapi into your app's CLI runtime — implement it to provide stdin/stdout, host resolution, and JSON output formatting.

gen

Code generator invoked via go generate. Reads a spec and writes openapi_gen.go with the typed struct. Configure with --source, --host, or the OKAPI_OPENAPI_SOURCE env var.

error

Custom error types with OpenApiError and OpenApiValidationError sentinels. Use Error(), Errorf(), and ErrorFrom() to create wrapped errors.

Using as a Library

Loading a spec
// From a file
api, err := (*openapi.OpenApi)(nil).NewFromSource("file:///path/to/openapi.yaml")

// From a URL
api, err := (*openapi.OpenApi)(nil).NewFromSource("https://api.example.com/openapi.json")

// From raw bytes
api, err := (*openapi.OpenApi)(nil).NewFromBytes([]byte(yamlContent))
Implementing the API client

Implement the OpenApiClient interface to provide HTTP transport:

type OpenApiClient interface {
    RequestJSON(method string, uri string, body io.Reader, result any, headers map[string][]string) (*http.Response, error)
}

Then bind it:

api = api.WithClient(myClient)
Calling endpoints
var result map[string]any

// Simple GET with query params
err := api.UsersList(
    request.Param("limit", 10),
    request.Result(&result),
)

// POST with body
err := api.UsersCreate(
    request.Body(map[string]any{"email": "user@example.com", "password": "secret"}),
    request.Result(&result),
)

// Path params, headers, and combined options
err := api.OrganizationsUsersList(
    request.Param("organization_id", "org-123"),
    request.Param("limit", 50),
    request.Header("Authorization", "Bearer token"),
    request.Result(&result),
)

// Endpoint chaining with .With()
customList := api.UsersList.With(
    request.Param("limit", 100),
    request.Header("Authorization", "Bearer token"),
)
err := customList(request.Result(&result))
Dynamic dispatch (calling endpoints by name)

WithClient only binds the typed OpenApiEndpoint fields on the generated OpenApi struct — it does not mutate the raw *spec.Endpoint values returned by api.Endpoints(). If you fetch an endpoint from that map and try to call it directly with openapi.CallEndpoint(ep, ...), you'll get:

No ApiClient available, did you forget to call OpenApi.WithClient()?

CallEndpoint is the low-level dispatcher used internally — it doesn't know about the client you bound to your *OpenApi. You have two pragmatic options:

Note: api.Endpoints() is keyed by the spec's operationId (the raw name in the OpenAPI document, e.g. usersList). The matching field on the generated *OpenApi struct uses the CamelCased form returned by (*spec.Endpoint).MethodName() (e.g. UsersList). Use the raw name when looking up the endpoint, and MethodName() when looking up the field.

1. Pass the client through per-call (recommended — no reflect, works for any spec):

err := openapi.CallEndpoint(ep,
    request.WithClient(myClient),
    request.Result(&result),
)

This is the simplest form and works whether or not you have a bound *OpenApi. It's the right default for tooling that walks api.Endpoints() or for tests that drive specs the generated struct doesn't match. See examples/client-test for a runnable end-to-end example that uses this pattern.

2. Reflective field lookup (when you specifically need the options bound to your *OpenApi):

Look up the generated struct field by MethodName() and invoke it. This inherits everything bound via WithClient / With, so it's the form to reach for when you've layered on auth headers, defaults, etc., and want each dynamic call to pick those up:

api = api.WithClient(myClient).With(request.Header("Authorization", "Bearer "+token))

ep := api.Endpoints()["usersList"]  // keyed by spec operationId
name := ep.MethodName()             // CamelCased, e.g. "UsersList"

field := reflect.ValueOf(api).Elem().FieldByName(name)
fn := field.Interface().(openapi.OpenApiEndpoint)

err := fn(request.Result(&result))
Params vs. headers

Okapi separates spec-declared parameters by location:

  • request.Param(name, value) — for path, query, and cookie parameters
  • request.Header(name, value) — for header parameters declared in the spec, and for ad-hoc HTTP headers (Authorization, Content-Type, etc.) that aren't in the spec at all

If a spec declares a parameter with in: header (e.g. Idempotency-Key), pass it through request.Header(...). Passing it through request.Param(...) will fail validation with a message pointing you at the right helper, and vice versa:

// Spec: Idempotency-Key is declared as `in: header`

// Correct:
err := api.UsersCreate(
    request.Header("Idempotency-Key", "abc-123"),
    request.Body(payload),
)

// Wrong — Validate returns:
//   "Parameter Idempotency-Key is a header — pass it with
//    request.Header(\"Idempotency-Key\", ...) instead of request.Param(...)"
err := api.UsersCreate(
    request.Param("Idempotency-Key", "abc-123"),
    request.Body(payload),
)

Headers that aren't declared in the spec (auth tokens, tracing IDs, etc.) pass through to the API client untouched.

Logging

Okapi uses charmbracelet/log internally. Enable debug or trace output with the DEBUG env var:

DEBUG=1      # Debug level
DEBUG=trace  # Trace level (verbose request/response details)

Requirements

  • Go 1.26.1+
  • An OpenAPI 3.x spec (JSON or YAML)

Development

# Run all tests (Ginkgo + Gomega)
go test ./...

# Run a specific package
go test ./spec/...
go test ./request/...

# Generate from a spec
OKAPI_OPENAPI_SOURCE=file://testdata/openapi.yaml go generate ./...

# Or with flags
go run ./gen/gen.go --source file://testdata/openapi.yaml

Contributing

  1. Fork the repo
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Write tests for your changes (tests use Ginkgo + Gomega)
  4. Make sure all tests pass (go test ./...)
  5. Commit with conventional commits (feat:, fix:, chore:, etc.)
  6. Push and open a pull request

License

Apache 2.0

Documentation

Overview

This file is generated by gen/gen.go - DO NOT EDIT

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CallEndpoint

func CallEndpoint(endpoint *spec.Endpoint, options ...request.RequestOption) error

CallEndpoint builds a request and makes an API call using the request's ApiClient.

func NilEndpoint

func NilEndpoint(options ...request.RequestOption) error

NilEndpoint is an OpenApiEndpoint that returns an error when called.

Types

type ApiClientCallable

type ApiClientCallable = request.ApiClientCallable

type OpenApi

type OpenApi struct {
	// contains filtered or unexported fields
}

func (*OpenApi) EndpointNames

func (o *OpenApi) EndpointNames() []string

EndpointNames returns a list of all the CamelCase endpoint names in the OpenApi struct.

func (*OpenApi) EndpointOperationNames

func (o *OpenApi) EndpointOperationNames() []string

EndpointOperationNames returns a list of all the operation names in the OpenApi spec.

func (*OpenApi) Endpoints

func (o *OpenApi) Endpoints() map[string]*spec.Endpoint

Endpoints returns a map of all the endpoints in the OpenApi spec.

func (*OpenApi) NewFromBytes

func (o *OpenApi) NewFromBytes(source []byte) (*OpenApi, error)

func (*OpenApi) NewFromSource

func (o *OpenApi) NewFromSource(source string) (*OpenApi, error)

func (*OpenApi) With

func (o *OpenApi) With(options ...request.RequestOption) *OpenApi

func (*OpenApi) WithClient

func (o *OpenApi) WithClient(client request.OpenApiClient) *OpenApi

type OpenApiEndpoint

type OpenApiEndpoint func(options ...request.RequestOption) error

func MakeOpenApiEndpoint

func MakeOpenApiEndpoint(endpoint *spec.Endpoint) OpenApiEndpoint

MakeOpenApiEndpoint returns an OpenApiEndpoint from a spec.Endpoint.

func (OpenApiEndpoint) With

type OpenApiSpec

type OpenApiSpec = spec.OpenApiSpec

Directories

Path Synopsis
cmd
okapi-gen-typed command
Command okapi-gen-typed generates a statically typed Go client from an OpenAPI 3.x spec.
Command okapi-gen-typed generates a statically typed Go client from an OpenAPI 3.x spec.
examples
client-test command
Package main demonstrates how to drive every endpoint in an OpenAPI spec dynamically through okapi, against a real HTTP round-trip.
Package main demonstrates how to drive every endpoint in an OpenAPI spec dynamically through okapi, against a real HTTP round-trip.
typed-client command
Command typed-client is the runnable example for okapi's statically typed client generator (`gen/typed`).
Command typed-client is the runnable example for okapi's statically typed client generator (`gen/typed`).
gen
typed
Package typed generates a statically typed Go client (types + methods) from an OpenAPI 3.x spec.
Package typed generates a statically typed Go client (types + methods) from an OpenAPI 3.x spec.
internal
log

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL