From e03e4b8ecfbcdafddfad19c322f47732c4ed62fd Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 2 Sep 2026 21:01:19 -0400 Subject: [PATCH] enhance openapi docs --- app/controllers/openapi_controller.rb | 47 ++++++++ app/views/layouts/inertia.html.erb | 2 + config/routes.rb | 9 ++ docs/index.mdx | 2 +- public/robots.txt | 7 ++ .../api/hackatime/v1/compatibility_spec.rb | 4 +- spec/requests/api/openapi_spec.rb | 57 ++++++++++ spec/requests/api/summary_spec.rb | 1 + spec/requests/api/v1/badges_spec.rb | 1 + .../requests/api/v1/currently_hacking_spec.rb | 1 + spec/requests/api/v1/leaderboards_spec.rb | 3 + spec/requests/api/v1/stats_spec.rb | 6 ++ spec/swagger_helper.rb | 28 ++++- swagger/admin/swagger.yaml | 9 ++ swagger/v1/swagger.yaml | 101 ++++++++++++++++-- test/controllers/openapi_controller_test.rb | 84 +++++++++++++++ 16 files changed, 351 insertions(+), 11 deletions(-) create mode 100644 app/controllers/openapi_controller.rb create mode 100644 spec/requests/api/openapi_spec.rb create mode 100644 test/controllers/openapi_controller_test.rb diff --git a/app/controllers/openapi_controller.rb b/app/controllers/openapi_controller.rb new file mode 100644 index 000000000..115125b4c --- /dev/null +++ b/app/controllers/openapi_controller.rb @@ -0,0 +1,47 @@ +# Publishes the OpenAPI description of Hackatime's public API at the +# conventional, machine-discoverable URLs (`/openapi.json`, `/openapi.yaml` +# and their `/api` aliases). The Scalar reference at `/api-docs` is the human +# entry point; these endpoints are what API clients, SDK generators and agents +# fetch. Both formats render the same document that `rswag` generates into +# `swagger/v1/swagger.yaml`, so there is a single source of truth. +class OpenapiController < ApplicationController + SPEC_PATH = Rails.root.join("swagger", "v1", "swagger.yaml").freeze + CACHE_MAX_AGE = 1.hour + + class << self + def json_document + @json_document ||= "#{JSON.pretty_generate(parsed_document)}\n" + end + + def yaml_document + @yaml_document ||= SPEC_PATH.read + end + + private + + def parsed_document + YAML.safe_load(yaml_document, aliases: true) + end + end + + def show_json + render_spec self.class.json_document, "application/json" + end + + def show_yaml + # RFC 9512 registers `application/yaml` as the media type for YAML. + render_spec self.class.yaml_document, "application/yaml" + end + + private + + def render_spec(body, content_type) + # Overrides ApplicationController's `no-store` default: the document is + # public, identical for every caller and only changes on deploy. + response.headers["Cache-Control"] = "public, max-age=#{CACHE_MAX_AGE.to_i}" + # Browser-based API consoles need to fetch the spec cross-origin. + response.headers["Access-Control-Allow-Origin"] = "*" + + render plain: body, content_type: content_type + end +end diff --git a/app/views/layouts/inertia.html.erb b/app/views/layouts/inertia.html.erb index bbdd9fc75..3771beffa 100644 --- a/app/views/layouts/inertia.html.erb +++ b/app/views/layouts/inertia.html.erb @@ -12,6 +12,8 @@ + + diff --git a/config/routes.rb b/config/routes.rb index 43371fdcc..d7b069b76 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,15 @@ def matches?(request) get "api-docs", to: "api_docs#show", as: :api_docs get "api-docs/admin", to: "api_docs#admin", as: :admin_api_docs mount Rswag::Api::Engine => "/api-docs" + + # Machine-readable description of the public API. /openapi.json is the + # canonical location; the others are conventional aliases that API clients + # and agents probe for. + get "openapi.json", to: "openapi#show_json", as: :openapi, format: false + get "openapi.yaml", to: "openapi#show_yaml", as: :openapi_yaml, format: false + get "api/openapi.json", to: "openapi#show_json", as: :api_openapi, format: false + get "api/openapi.yaml", to: "openapi#show_yaml", as: :api_openapi_yaml, format: false + defaults export: true do use_doorkeeper do controllers authorizations: "custom_doorkeeper/authorizations" diff --git a/docs/index.mdx b/docs/index.mdx index 26eb0fbe9..0105f19a1 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -23,7 +23,7 @@ Get Hackatime running in your editor, customise your setup or build an integrati Build an OAuth integration.