{"id":50989349,"url":"https://github.com/jathanism/okapi","last_synced_at":"2026-06-20T00:01:22.662Z","repository":{"id":356158988,"uuid":"1224895096","full_name":"jathanism/okapi","owner":"jathanism","description":"OpenAPI 3.x client and CLI generator for Go","archived":false,"fork":false,"pushed_at":"2026-05-14T00:19:44.000Z","size":147,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-14T00:51:04.842Z","etag":null,"topics":["cli","go","golang","openapi","openapi-codegen","openapi3","rest-api"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jathanism.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-29T18:33:11.000Z","updated_at":"2026-05-14T00:19:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jathanism/okapi","commit_stats":null,"previous_names":["jathanism/okapi"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/jathanism/okapi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jathanism%2Fokapi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jathanism%2Fokapi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jathanism%2Fokapi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jathanism%2Fokapi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jathanism","download_url":"https://codeload.github.com/jathanism/okapi/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jathanism%2Fokapi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34457745,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-17T02:00:05.408Z","response_time":127,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","go","golang","openapi","openapi-codegen","openapi3","rest-api"],"created_at":"2026-06-20T00:01:20.064Z","updated_at":"2026-06-20T00:01:22.651Z","avatar_url":"https://github.com/jathanism.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Okapi\n\nOkapi 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`).\n\nIt 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.\n\n## Features\n\n- **Spec-driven endpoints** — parse any OpenAPI 3.x spec (local file, `file://`, `http(s)://`) and call endpoints by name\n- **Request validation** — parameters are type-checked, required params are enforced, request bodies are validated against JSON Schema\n- **CLI generation** — `go 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](https://github.com/urfave/cli))\n- **Flexible API client** — bring your own HTTP client via the `OpenApiClient` interface; Okapi doesn't couple you to any specific HTTP library\n- **Glamour help output** — CLI help text is rendered with [glamour](https://github.com/charmbracelet/glamour) markdown styling, including inline request body schemas\n- **Zero runtime dependencies on your app's context** — `CliContext` interface lets you bridge Okapi into any CLI framework\n\n## Quick Start\n\n### As a library\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"net/http\"\n    \"io\"\n\n    \"github.com/jathanism/okapi\"\n    \"github.com/jathanism/okapi/request\"\n)\n\nfunc main() {\n    // Load an OpenAPI spec\n    api, err := (*openapi.OpenApi)(nil).NewFromSource(\"file://openapi.yaml\")\n    if err != nil {\n        panic(err)\n    }\n\n    // Bind an API client\n    myClient := \u0026MyHttpClient{}\n    api = api.WithClient(myClient)\n\n    // Call an endpoint\n    err = api.UsersList(\n        request.Param(\"limit\", 10),\n        request.Param(\"offset\", 0),\n        request.Result(\u0026result),\n    )\n}\n```\n\n### CLI generation\n\n```bash\n# Generate the OpenApi struct from a spec\nOKAPI_OPENAPI_SOURCE=https://api.example.com/openapi.json go generate ./...\n\n# Or use flags\ngo run ./gen/gen.go --source https://api.example.com/openapi.json\n```\n\nThis produces `openapi_gen.go` with typed endpoint fields:\n\n```go\ntype OpenApi struct {\n    internal\n\n    AccountsChangePassword OpenApiEndpoint\n    OrganizationsBySlug    OpenApiEndpoint\n    OrganizationsUsersCreate OpenApiEndpoint\n    OrganizationsUsersList OpenApiEndpoint\n    UsersCreate            OpenApiEndpoint\n    UsersList              OpenApiEndpoint\n    SchemasList            OpenApiEndpoint\n    // ...\n}\n```\n\n## Project Structure\n\n```\nokapi/\n├── openapi.go          # Core OpenApi struct — loads specs, builds endpoints, makes calls\n├── openapi_gen.go      # Generated OpenApi struct (do not edit — use go generate)\n├── openapi_test.go     # Integration tests (Ginkgo/Gomega)\n├── error/              # Error types and helpers (OpenApiError, OpenApiValidationError)\n├── request/            # Request building — params, body, headers, API client interface\n├── spec/               # OpenAPI spec parsing, endpoint/param/body types, validation\n├── cli/                # CLI generator — builds urfave/cli command tree from spec\n├── gen/                # Code generator — reads spec, writes openapi_gen.go\n├── internal/\n│   ├── log/            # Structured logging (charmbracelet/log) with trace support\n│   └── testutil/       # Test helpers\n└── testdata/\n    └── openapi.yaml    # Test fixture spec\n```\n\n## Packages\n\n### `openapi` (root)\n\nThe core package. `OpenApi` is the main type — it loads a spec, builds endpoint callables, and dispatches requests.\n\nKey types:\n- `OpenApi` — spec-loaded API client with typed endpoint fields\n- `OpenApiEndpoint` — `func(options ...RequestOption) error` — call an endpoint with options\n- `OpenApiSpec` — alias for `spec.OpenApiSpec`\n\nKey methods:\n- `NewFromSource(source string)` — load from file path, URL, or raw string (cached)\n- `NewFromBytes(source []byte)` — load from raw bytes (not cached)\n- `WithClient(client OpenApiClient)` — bind an HTTP client to all endpoints\n- `With(options ...RequestOption)` — clone with request options applied to all endpoints\n\n### `spec`\n\nParses OpenAPI 3.x specs using [pb33f/libopenapi](https://github.com/pb33f/libopenapi). Handles parameter parsing, request body JSON Schema compilation, and URL building.\n\n### `request`\n\nFunctional options for building requests. `Param()`, `Body()`, `Data()`, `Header()`, `Result()`. The `OpenApiClient` interface is what you implement to bring your own HTTP client.\n\n### `cli`\n\nGenerates a nested CLI command tree from an OpenAPI spec using [urfave/cli](https://github.com/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.\n\nThe `CliContext` interface bridges Okapi into your app's CLI runtime — implement it to provide stdin/stdout, host resolution, and JSON output formatting.\n\n### `gen`\n\nCode 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.\n\n### `error`\n\nCustom error types with `OpenApiError` and `OpenApiValidationError` sentinels. Use `Error()`, `Errorf()`, and `ErrorFrom()` to create wrapped errors.\n\n## Using as a Library\n\n### Loading a spec\n\n```go\n// From a file\napi, err := (*openapi.OpenApi)(nil).NewFromSource(\"file:///path/to/openapi.yaml\")\n\n// From a URL\napi, err := (*openapi.OpenApi)(nil).NewFromSource(\"https://api.example.com/openapi.json\")\n\n// From raw bytes\napi, err := (*openapi.OpenApi)(nil).NewFromBytes([]byte(yamlContent))\n```\n\n### Implementing the API client\n\nImplement the `OpenApiClient` interface to provide HTTP transport:\n\n```go\ntype OpenApiClient interface {\n    RequestJSON(method string, uri string, body io.Reader, result any, headers map[string][]string) (*http.Response, error)\n}\n```\n\nThen bind it:\n\n```go\napi = api.WithClient(myClient)\n```\n\n### Calling endpoints\n\n```go\nvar result map[string]any\n\n// Simple GET with query params\nerr := api.UsersList(\n    request.Param(\"limit\", 10),\n    request.Result(\u0026result),\n)\n\n// POST with body\nerr := api.UsersCreate(\n    request.Body(map[string]any{\"email\": \"user@example.com\", \"password\": \"secret\"}),\n    request.Result(\u0026result),\n)\n\n// Path params, headers, and combined options\nerr := api.OrganizationsUsersList(\n    request.Param(\"organization_id\", \"org-123\"),\n    request.Param(\"limit\", 50),\n    request.Header(\"Authorization\", \"Bearer token\"),\n    request.Result(\u0026result),\n)\n\n// Endpoint chaining with .With()\ncustomList := api.UsersList.With(\n    request.Param(\"limit\", 100),\n    request.Header(\"Authorization\", \"Bearer token\"),\n)\nerr := customList(request.Result(\u0026result))\n```\n\n### Dynamic dispatch (calling endpoints by name)\n\n`WithClient` only binds the typed `OpenApiEndpoint` fields on the generated\n`OpenApi` struct — it does **not** mutate the raw `*spec.Endpoint` values\nreturned by `api.Endpoints()`. If you fetch an endpoint from that map and try\nto call it directly with `openapi.CallEndpoint(ep, ...)`, you'll get:\n\n```\nNo ApiClient available, did you forget to call OpenApi.WithClient()?\n```\n\n`CallEndpoint` is the low-level dispatcher used internally — it doesn't know\nabout the client you bound to your `*OpenApi`. You have two pragmatic options:\n\n\u003e **Note:** `api.Endpoints()` is keyed by the spec's `operationId` (the raw\n\u003e name in the OpenAPI document, e.g. `usersList`). The matching field on the\n\u003e generated `*OpenApi` struct uses the CamelCased form returned by\n\u003e `(*spec.Endpoint).MethodName()` (e.g. `UsersList`). Use the raw name when\n\u003e looking up the endpoint, and `MethodName()` when looking up the field.\n\n**1. Pass the client through per-call** (recommended — no `reflect`, works for any spec):\n\n```go\nerr := openapi.CallEndpoint(ep,\n    request.WithClient(myClient),\n    request.Result(\u0026result),\n)\n```\n\nThis is the simplest form and works whether or not you have a bound\n`*OpenApi`. It's the right default for tooling that walks `api.Endpoints()`\nor for tests that drive specs the generated struct doesn't match. See\n[`examples/client-test`](examples/client-test) for a runnable end-to-end\nexample that uses this pattern.\n\n**2. Reflective field lookup** (when you specifically need the options bound to your `*OpenApi`):\n\nLook up the generated struct field by `MethodName()` and invoke it. This\ninherits everything bound via `WithClient` / `With`, so it's the form to\nreach for when you've layered on auth headers, defaults, etc., and want\neach dynamic call to pick those up:\n\n```go\napi = api.WithClient(myClient).With(request.Header(\"Authorization\", \"Bearer \"+token))\n\nep := api.Endpoints()[\"usersList\"]  // keyed by spec operationId\nname := ep.MethodName()             // CamelCased, e.g. \"UsersList\"\n\nfield := reflect.ValueOf(api).Elem().FieldByName(name)\nfn := field.Interface().(openapi.OpenApiEndpoint)\n\nerr := fn(request.Result(\u0026result))\n```\n\n### Params vs. headers\n\nOkapi separates spec-declared parameters by location:\n\n- `request.Param(name, value)` — for `path`, `query`, and `cookie` parameters\n- `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\n\nIf 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:\n\n```go\n// Spec: Idempotency-Key is declared as `in: header`\n\n// Correct:\nerr := api.UsersCreate(\n    request.Header(\"Idempotency-Key\", \"abc-123\"),\n    request.Body(payload),\n)\n\n// Wrong — Validate returns:\n//   \"Parameter Idempotency-Key is a header — pass it with\n//    request.Header(\\\"Idempotency-Key\\\", ...) instead of request.Param(...)\"\nerr := api.UsersCreate(\n    request.Param(\"Idempotency-Key\", \"abc-123\"),\n    request.Body(payload),\n)\n```\n\nHeaders that aren't declared in the spec (auth tokens, tracing IDs, etc.) pass through to the API client untouched.\n\n## Logging\n\nOkapi uses [charmbracelet/log](https://github.com/charmbracelet/log) internally. Enable debug or trace output with the `DEBUG` env var:\n\n```bash\nDEBUG=1      # Debug level\nDEBUG=trace  # Trace level (verbose request/response details)\n```\n\n## Requirements\n\n- Go 1.26.1+\n- An OpenAPI 3.x spec (JSON or YAML)\n\n## Development\n\n```bash\n# Run all tests (Ginkgo + Gomega)\ngo test ./...\n\n# Run a specific package\ngo test ./spec/...\ngo test ./request/...\n\n# Generate from a spec\nOKAPI_OPENAPI_SOURCE=file://testdata/openapi.yaml go generate ./...\n\n# Or with flags\ngo run ./gen/gen.go --source file://testdata/openapi.yaml\n```\n\n## Contributing\n\n1. Fork the repo\n2. Create a feature branch (`git checkout -b feat/my-feature`)\n3. Write tests for your changes (tests use [Ginkgo](https://onsi.github.io/ginkgo/) + [Gomega](https://onsi.github.io/gomega/))\n4. Make sure all tests pass (`go test ./...`)\n5. Commit with conventional commits (`feat:`, `fix:`, `chore:`, etc.)\n6. Push and open a pull request\n\n## License\n\n[Apache 2.0](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjathanism%2Fokapi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjathanism%2Fokapi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjathanism%2Fokapi/lists"}