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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
exported/
schemas/*.json
schemas/**/*.json
.idea/*
node_js/node_modules
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ COPY --from=builder /bin/autodoc /bin/autodoc
# Copy Node.js app + deps
COPY --from=node-builder /app /node_js
COPY ./html-swagger.sh /html-swagger.sh
COPY ./html-stoplight.sh /html-stoplight.sh
WORKDIR /
RUN chmod +x /node_js/deref.js

Expand Down
43 changes: 26 additions & 17 deletions api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ type ErrorResponse struct {
// ExportSuccess represents a successful export response.
// @description Returned when the OpenAPI schema has been successfully exported.
type ExportSuccess struct {
SwaggerUrl string `json:"url" example:"https://cdn.example.com/example-service/index.html"`
RedocUrl string `json:"redocUrl" example:"https://cdn.example.com/example-service/redoc.html"`
SwaggerUrl string `json:"url" example:"https://cdn.example.com/example-service/swagger.html"`
RedocUrl string `json:"redocUrl" example:"https://cdn.example.com/example-service/redoc.html"`
StoplightUrl string `json:"stoplightUrl" example:"https://cdn.example.com/example-service/stoplight.html"`
}

// returnError sends an error response to the client.
func returnError(w http.ResponseWriter, err error) {
slog.Error("Error handling request: %v", err)
slog.Error("Error handling request", "error", err)
w.WriteHeader(http.StatusBadRequest)
newError := ErrorResponse{Error: err.Error()}
_ = json.NewEncoder(w).Encode(newError)
Expand All @@ -72,41 +73,41 @@ func returnError(w http.ResponseWriter, err error) {
func openapiExport(w http.ResponseWriter, r *http.Request) {
jsonData, err := io.ReadAll(r.Body)
if err != nil {
slog.Error("Error reading body: %v", err)
slog.Error("Error reading body", "error", err)
returnError(w, err)
return
}

fullSchema := FullOpenAPI{}
if err := json.NewDecoder(bytes.NewReader(jsonData)).Decode(&fullSchema); err != nil {
slog.Error("Error decoding JSON: %v", err)
slog.Error("Error decoding JSON", "error", err)
returnError(w, err)
return
}

slog.Info("Accepted a new schema: %s", fullSchema.Info.Title)
slog.Info("Accepted a new schema", "title", fullSchema.Info.Title)
fullPth := "./schemas/" + fullSchema.Info.Title + ".json"
if err = os.MkdirAll(filepath.Dir(fullPth), 0777); err != nil {
slog.Error("Error creating directory %s: %v", filepath.Dir(fullPth), err)
slog.Error("Error creating directory", "path", filepath.Dir(fullPth), "error", err)
returnError(w, err)
return
}

err = os.WriteFile(fullPth, jsonData, 0644)
if err != nil {
slog.Error("Error writing file: %v", err)
slog.Error("Error writing file", "error", err)
returnError(w, err)
return
}

slog.Info("Wrote file: %s", fullSchema.Info.Title+".json")
slog.Info("File path: %s", fullPth)
slog.Info("Wrote file", "file", fullSchema.Info.Title+".json")
slog.Info("File path", "path", fullPth)

redocShortPath := fmt.Sprintf("./exported/%s", fullSchema.Info.Title)
fullPth, _ = filepath.Abs(fullPth)
redocPath, _ := filepath.Abs(redocShortPath)
makeUI := func(cmdCommand []string, cmdDir string) error {
slog.Info("Running command: %v", cmdCommand)
slog.Info("Running command", "command", cmdCommand)
_ = os.MkdirAll(redocPath, 0755)
var stderr bytes.Buffer
cmd := exec.Command(cmdCommand[0], cmdCommand[1:]...)
Expand Down Expand Up @@ -137,14 +138,22 @@ func openapiExport(w http.ResponseWriter, r *http.Request) {
returnError(w, err)
return
}
err = makeUI([]string{"/bin/bash", "html-stoplight.sh", fullPth, filepath.Join(redocPath, "stoplight.html")}, "")
if err != nil {
returnError(w, err)
return
}
success := ExportSuccess{
SwaggerUrl: fmt.Sprintf("%s%s/swagger.html", BaseCDNUrl, fullSchema.Info.Title),
RedocUrl: fmt.Sprintf("%s%s/redoc.html", BaseCDNUrl, fullSchema.Info.Title),
SwaggerUrl: fmt.Sprintf("%s%s/swagger.html", BaseCDNUrl, fullSchema.Info.Title),
RedocUrl: fmt.Sprintf("%s%s/redoc.html", BaseCDNUrl, fullSchema.Info.Title),
StoplightUrl: fmt.Sprintf("%s%s/stoplight.html", BaseCDNUrl, fullSchema.Info.Title),
}
_ = json.NewEncoder(w).Encode(success)

slog.Info("Exported: %s", fullSchema.Info.Title)
slog.Info("URL: %s", success.SwaggerUrl)
slog.Info("Exported", "title", fullSchema.Info.Title)
slog.Info("Swagger URL", "url", success.SwaggerUrl)
slog.Info("Redoc URL", "url", success.RedocUrl)
slog.Info("Stoplight URL", "url", success.StoplightUrl)
}

// expandedOpenapi handles OpenAPI export requests.
Expand All @@ -161,7 +170,7 @@ func openapiExport(w http.ResponseWriter, r *http.Request) {
func expandedOpenapi(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
schemaName := vars["name"]
slog.Info("Received request for schema: %s", schemaName)
slog.Info("Received request for schema", "name", schemaName)
schemaPath, err := filepath.Abs(
fmt.Sprintf("./schemas/%s.json", strings.TrimSuffix(schemaName, ".json")),
)
Expand All @@ -177,7 +186,7 @@ func expandedOpenapi(w http.ResponseWriter, r *http.Request) {
return
}
if !json.Valid(output) {
slog.Error("Failed to parse JSON: %s", output)
slog.Error("Failed to parse JSON", "output", string(output))
returnError(w, fmt.Errorf("failed to parse JSON: %s", output))
return
}
Expand Down
6 changes: 5 additions & 1 deletion docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,13 @@ const docTemplate = `{
"type": "string",
"example": "https://cdn.example.com/example-service/redoc.html"
},
"stoplightUrl": {
"type": "string",
"example": "https://cdn.example.com/example-service/stoplight.html"
},
"url": {
"type": "string",
"example": "https://cdn.example.com/example-service/index.html"
"example": "https://cdn.example.com/example-service/swagger.html"
}
}
},
Expand Down
6 changes: 5 additions & 1 deletion docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,13 @@
"type": "string",
"example": "https://cdn.example.com/example-service/redoc.html"
},
"stoplightUrl": {
"type": "string",
"example": "https://cdn.example.com/example-service/stoplight.html"
},
"url": {
"type": "string",
"example": "https://cdn.example.com/example-service/index.html"
"example": "https://cdn.example.com/example-service/swagger.html"
}
}
},
Expand Down
5 changes: 4 additions & 1 deletion docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ definitions:
redocUrl:
example: https://cdn.example.com/example-service/redoc.html
type: string
stoplightUrl:
example: https://cdn.example.com/example-service/stoplight.html
type: string
url:
example: https://cdn.example.com/example-service/index.html
example: https://cdn.example.com/example-service/swagger.html
type: string
type: object
api.FullOpenAPI:
Expand Down
65 changes: 65 additions & 0 deletions html-stoplight.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/bin/bash
set -e

INPUT_JSON="$1"
OUTPUT_HTML="$2"

if [[ -z "$INPUT_JSON" || -z "$OUTPUT_HTML" ]]; then
echo "Usage: $0 path/to/openapi.json output.html"
exit 1
fi

if [[ ! -f "$INPUT_JSON" ]]; then
echo "Error: File '$INPUT_JSON' does not exist"
exit 1
fi

python3 << PYEOF
import base64
import json

with open("$INPUT_JSON", "r") as f:
spec = json.load(f)

spec_json = json.dumps(spec)
spec_b64 = base64.b64encode(spec_json.encode()).decode()

html = f'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Documentation</title>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
<style>
body {{
margin: 0;
padding: 0;
}}
elements-api {{
display: block;
height: 100vh;
}}
</style>
</head>
<body>
<elements-api router="hash" layout="sidebar"></elements-api>
<script type="module">
import 'https://unpkg.com/@stoplight/elements/web-components.min.js?module';

const spec = JSON.parse(atob('{spec_b64}'));

customElements.whenDefined('elements-api').then(() => {{
const el = document.querySelector('elements-api');
el.apiDescriptionDocument = spec;
}});
</script>
</body>
</html>
'''

with open("$OUTPUT_HTML", "w") as f:
f.write(html)

print("✅ Generated $OUTPUT_HTML with Stoplight Elements", flush=True)
PYEOF
Loading