Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 0.0.22

- Replace default Swagger UI docs pages with Scalar API Reference for both RPC and legacy HTTP routers.
- Preserve auth prefix behavior in docs by applying guard prefixes to outgoing request headers.

## 0.0.21

- Treat `encoding/json.RawMessage` as arbitrary JSON in generated schemas and clients instead of byte arrays.

## 0.0.20

- Remove the unused `httpapi` lightweight JS client generator path.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.17
0.0.22
2 changes: 2 additions & 0 deletions docs/agents/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Virtuous is designed to be deterministic for agents. Keep project layout and han

```text
You are implementing a Virtuous RPC API.
- Target Virtuous version: read `VERSION` in the repo and pin it in the output (current: `0.0.21`).
- Create router.go with rpc.NewRouter(rpc.WithPrefix("/rpc")).
- Put handlers in package folders (states, users, admin).
- Use func(ctx, req) (Resp, int).
Expand All @@ -26,6 +27,7 @@ You are implementing a Virtuous RPC API.

```text
Migrate Swaggo routes to Virtuous using the canonical guide at docs/tutorials/migrate-swaggo.md.
- Target Virtuous version: read `VERSION` in the repo and pin it in the output (current: `0.0.21`).
- For Swaggo migrations, use httpapi first.
- Use rpc only for explicit phase-2 moves.
- Move field docs to doc struct tags.
Expand Down
3 changes: 3 additions & 0 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ deps/

```text
You are implementing a Virtuous RPC API.
- Target Virtuous version: read `VERSION` in the repo and pin it in the output (current: `0.0.21`).
- Create router.go with rpc.NewRouter(rpc.WithPrefix("/rpc")).
- Put handlers in package folders (states, users, admin).
- Use func(ctx, req) (Resp, int).
Expand All @@ -142,6 +143,7 @@ You are implementing a Virtuous RPC API.

```text
Use the canonical Swaggo migration prompt in docs/tutorials/migrate-swaggo.md.
- Target Virtuous version: read `VERSION` in the repo and pin it in the output (current: `0.0.21`).
- Default to httpapi for Swaggo routes.
- Use rpc only for phase-2 moves.
- Validate against the migration Definition of Done in that guide.
Expand Down Expand Up @@ -171,6 +173,7 @@ Agent prompt (porting legacy handlers):

```text
Port legacy handlers into Virtuous.
- Target Virtuous version: read `VERSION` in the repo and pin it in the output (current: `0.0.21`).
- For each handler, decide: RPC (new) or httpapi (legacy).
- For legacy: wrap http.HandlerFunc with httpapi.WrapFunc and register a method-prefixed route.
- For new: create an RPC handler and register with router.HandleRPC.
Expand Down
8 changes: 5 additions & 3 deletions docs/tutorials/migrate-swaggo.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ Use this prompt for migration automation:
You are migrating a Go API from Swaggo annotations to Virtuous.

Goal:
- Pin the target Virtuous version from `VERSION` and report it explicitly (current: `0.0.21`).
- Replace annotation-driven docs with Virtuous runtime docs/clients.
- For Swaggo migrations, migrate routes to httpapi first.
- Use RPC as an explicit phase-2 optimization after compatibility is preserved.
Expand All @@ -191,9 +192,10 @@ Rules:

Deliverables:
1) Code changes for migrated routes.
2) A migration completion checklist against the Definition of Done in docs/tutorials/migrate-swaggo.md.
3) List of routes intentionally deferred to phase-2 RPC and why.
4) Any routes blocked by feature mismatch, with concrete gap notes.
2) Reported target Virtuous version from `VERSION`.
3) A migration completion checklist against the Definition of Done in docs/tutorials/migrate-swaggo.md.
4) List of routes intentionally deferred to phase-2 RPC and why.
5) Any routes blocked by feature mismatch, with concrete gap notes.
```

## Known gaps vs Swaggo
Expand Down
155 changes: 101 additions & 54 deletions httpapi/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,65 @@ import (
"github.com/swetjen/virtuous/internal/textutil"
)

// DefaultDocsHTML returns a Swagger UI HTML page for the provided OpenAPI path.
// DefaultDocsHTML returns a Scalar API Reference HTML page for the provided OpenAPI path.
func DefaultDocsHTML(openAPIPath string) string {
return fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Virtuous API Docs</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
<style>
* {
box-sizing: border-box;
}

body {
margin: 0;
background: #f7f7f7;
min-height: 100vh;
background:
radial-gradient(circle at 10%% 10%%, #e5f4ff 0%%, rgba(229, 244, 255, 0) 45%%),
radial-gradient(circle at 90%% 0%%, #d9ffe9 0%%, rgba(217, 255, 233, 0) 35%%),
#f4f7fb;
}

.docs-shell {
padding: 12px;
}

#app {
min-height: calc(100vh - 24px);
border: 1px solid #c7d4e0;
border-radius: 14px;
overflow: hidden;
background: #ffffff;
box-shadow: 0 20px 60px rgba(17, 46, 79, 0.1);
}

@media (max-width: 800px) {
.docs-shell {
padding: 0;
}

#app {
min-height: 100vh;
border-radius: 0;
border: 0;
box-shadow: none;
}
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<main class="docs-shell">
<div id="app"></div>
</main>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
<script>
window.onload = function () {
(function () {
const VIRTUOUS_DEBUG_AUTH = false
const OPENAPI_URL = %q

function buildPrefixMap(spec) {
const map = {}
if (!spec || !spec.components || !spec.components.securitySchemes) {
Expand All @@ -53,71 +90,81 @@ func DefaultDocsHTML(openAPIPath string) string {
})
return map
}
function applyAuthPrefix(req, prefixMap) {
try {
if (!prefixMap || !req || !req.headers) {
return req
}
function findHeaderName(headers, target) {
if (!headers || !target) {
return ""
}
const targetLower = target.toLowerCase()
const keys = Object.keys(headers)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key.toLowerCase() === targetLower) {
return key
}
}
return ""

function installFetchAuthPrefixer(prefixMap) {
window.__virtuousDocsPrefixMap = prefixMap || {}
if (window.__virtuousDocsFetchWrapped) {
return
}
window.__virtuousDocsFetchWrapped = true
const originalFetch = window.fetch.bind(window)
window.fetch = function (input, init) {
const activePrefixMap = window.__virtuousDocsPrefixMap || {}
const headerNames = Object.keys(activePrefixMap)
if (headerNames.length === 0) {
return originalFetch(input, init)
}
Object.keys(prefixMap).forEach(function (headerName) {
const prefix = prefixMap[headerName]
const headerKey = findHeaderName(req.headers, headerName)
const current = headerKey ? req.headers[headerKey] : req.headers[headerName]
if (!current) {
return
}
const expected = prefix + " "
if (typeof current === "string" && !current.startsWith(expected)) {
if (headerKey) {
req.headers[headerKey] = expected + current
} else {
req.headers[headerName] = expected + current
try {
const sourceHeaders = init && init.headers ? init.headers : (input instanceof Request ? input.headers : undefined)
const headers = new Headers(sourceHeaders || {})
let changed = false
headerNames.forEach(function (headerName) {
const prefix = activePrefixMap[headerName]
const current = headers.get(headerName)
if (!prefix || !current) {
return
}
if (VIRTUOUS_DEBUG_AUTH && typeof console !== "undefined") {
console.log("virtuous docs auth prefix applied", headerName)
const expected = prefix + " "
if (!String(current).startsWith(expected)) {
headers.set(headerName, expected + current)
changed = true
if (VIRTUOUS_DEBUG_AUTH && typeof console !== "undefined") {
console.log("virtuous docs auth prefix applied", headerName)
}
}
})
if (!changed) {
return originalFetch(input, init)
}
if (input instanceof Request) {
const nextInit = init ? Object.assign({}, init) : {}
nextInit.headers = headers
return originalFetch(new Request(input, nextInit))
}
})
} catch (e) {
return req
const nextInit = init ? Object.assign({}, init) : {}
nextInit.headers = headers
return originalFetch(input, nextInit)
} catch (e) {
return originalFetch(input, init)
}
}
return req
}
function initUI(prefixMap) {
let ui
ui = SwaggerUIBundle({

function renderScalar(prefixMap) {
installFetchAuthPrefixer(prefixMap)
if (typeof Scalar === "undefined" || !Scalar.createApiReference) {
const app = document.getElementById("app")
if (app) {
app.innerHTML = "<pre style=\"padding: 24px; margin: 0;\">Unable to load Scalar API Reference.</pre>"
}
return
}
Scalar.createApiReference("#app", {
url: OPENAPI_URL,
dom_id: "#swagger-ui",
requestInterceptor: function (req) {
return applyAuthPrefix(req, prefixMap)
},
})
window.ui = ui
}

fetch(OPENAPI_URL)
.then(function (resp) {
return resp.json()
})
.then(function (spec) {
initUI(buildPrefixMap(spec))
renderScalar(buildPrefixMap(spec))
})
.catch(function () {
initUI({})
renderScalar({})
})
}
})()
</script>
</body>
</html>`, openAPIPath)
Expand Down
22 changes: 22 additions & 0 deletions httpapi/docs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package httpapi

import (
"strings"
"testing"
)

func TestDefaultDocsHTMLUsesScalar(t *testing.T) {
html := DefaultDocsHTML("/openapi.json")
if !strings.Contains(html, "https://cdn.jsdelivr.net/npm/@scalar/api-reference") {
t.Fatalf("expected Scalar script in docs HTML")
}
if !strings.Contains(html, "Scalar.createApiReference(\"#app\"") {
t.Fatalf("expected Scalar initialization in docs HTML")
}
if !strings.Contains(html, "const OPENAPI_URL = \"/openapi.json\"") {
t.Fatalf("expected openapi path in docs HTML")
}
if strings.Contains(html, "SwaggerUIBundle") {
t.Fatalf("unexpected Swagger UI bundle in docs HTML")
}
}
2 changes: 1 addition & 1 deletion python_loader/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "virtuous"
version = "0.0.17"
version = "0.0.22"
description = "Loader for Virtuous Python clients"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
Loading