From cc3d38781b7c55cd2d795162197ce23c5b62710e Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 18 Oct 2026 21:39:56 +0200 Subject: [PATCH] The links written as HTML, which the local build could not see MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 12 of the 85 sites write their card grids as `` (md_in_html), which a Markdown-link rewrite cannot see. Hugo does not mind — an attribute is an attribute — so this reached CI, where the link checker found three dead links on go-aiquota/docs' front page. Re-converted with the fixed mkdocs2hextra, and the local gate now reads the BUILT HTML for .md hrefs as well as building it. Co-Authored-By: Claude Opus 5 --- .github/workflows/docs.yml | 172 ++++++++++++++++++-------- .gitmodules | 3 + README.md | 22 ++-- docs/index.md => content/en/_index.md | 38 +++--- {docs => content/en}/api.md | 30 +++-- {docs => content/en}/contributing.md | 15 ++- {docs => content/en}/performance.md | 105 +++++++++------- {docs => content/en}/roadmap.md | 11 +- {docs => content/en}/why.md | 15 ++- hugo.yaml | 87 +++++++++++++ mkdocs.yml | 61 --------- requirements.txt | 2 - {docs => static}/assets/logo.svg | 0 themes/hextra | 1 + 14 files changed, 354 insertions(+), 208 deletions(-) create mode 100644 .gitmodules rename docs/index.md => content/en/_index.md (75%) rename {docs => content/en}/api.md (87%) rename {docs => content/en}/contributing.md (87%) rename {docs => content/en}/performance.md (56%) rename {docs => content/en}/roadmap.md (90%) rename {docs => content/en}/why.md (85%) create mode 100644 hugo.yaml delete mode 100644 mkdocs.yml delete mode 100644 requirements.txt rename {docs => static}/assets/logo.svg (100%) create mode 160000 themes/hextra diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a4e3ba7..e98a8e9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,59 +1,105 @@ name: docs +# Every documentation version is a separate Hugo build, published under +# https://go-ruby-net-http.github.io/docs// on the gh-pages branch, where is +# the minor version of the code the pages describe (params.docs.version in +# hugo.yaml: v0.3.1 -> 0.3), or "main" while no release is tagged. Each push to +# main replaces that version's path and leaves the others alone. Pull requests +# build and check, and publish nothing. +# +# ⛔ This replaced MkDocs Material plus mike: a Python toolchain, a +# requirements.txt and a pip install to render a static site for a pure-Go +# organisation. + on: push: branches: [main] pull_request: workflow_dispatch: -# The build needs nothing beyond the source. Write access to the repository -# is granted to the deploy job alone, below, so a pull request build cannot -# push to gh-pages. permissions: contents: read +env: + HUGO_VERSION: "0.167.0" + # sha256 of hugo_${HUGO_VERSION}_linux-amd64.tar.gz, from the release's checksums.txt. + HUGO_SHA256: "4d84519b9f619e6d4c3fb45a50157abeabeb724f859c60605f44c23def6e1169" + DOCS_URL: "https://go-ruby-net-http.github.io/docs" + jobs: - # Build on every pull request as well as on main, so a change to the docs or to - # the toolchain in requirements.txt has to prove it still builds before it - # lands. Without this the workflow ran only on main, which meant a dependency - # bump had no signal at all and nothing could say whether it was safe to merge. build: runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} steps: - name: Checkout uses: actions/checkout@v7 - - - name: Set up Python - uses: actions/setup-python@v7 with: - python-version: "3.15" - cache: pip - cache-dependency-path: requirements.txt + # themes/hextra is the tannevaled/hextra fork, pinned to a release tag. + submodules: recursive + # The whole history: each page's date comes from git. + fetch-depth: 0 + + - name: Install Hugo ${{ env.HUGO_VERSION }} + run: | + curl -fsSL -o /tmp/hugo.tar.gz \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_linux-amd64.tar.gz" + echo "${HUGO_SHA256} /tmp/hugo.tar.gz" | sha256sum -c - + sudo tar -xzf /tmp/hugo.tar.gz -C /usr/local/bin hugo + hugo version + + - name: Version + id: version + run: | + documented=$(hugo config --format json | jq -r '.params.docs.version') + if [ "${documented}" = main ]; then + version=main + else + version=$(printf '%s\n' "${documented}" | sed -nE 's/^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/\1.\2/p') + fi + if [ -z "${version}" ]; then + echo "::error::params.docs.version in hugo.yaml is \"${documented}\", not vMAJOR.MINOR.PATCH or main" + exit 1 + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "documented ${documented}: documentation version ${version}" + + # --panicOnWarning: a relref to a page that is not there, a deprecated + # setting or a template warning fails the build rather than scrolling by. + - name: Build + env: + HUGO_ENVIRONMENT: production + run: >- + hugo --gc --minify --panicOnWarning --printPathWarnings + --baseURL "${DOCS_URL}/${{ steps.version.outputs.version }}/" - - name: Install MkDocs Material + mike + - name: Lay out the site for the link check run: | - pip install --upgrade pip - pip install -r requirements.txt + mkdir -p _check/docs + cp -R public "_check/docs/${{ steps.version.outputs.version }}" - # --strict turns a broken link or an unknown config key into a failure - # rather than a warning nobody reads. - - name: Build the site (strict) - run: mkdocs build --strict + - name: Check links + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --offline --no-progress --include-fragments + --index-files index.html + --root-dir ${{ github.workspace }}/_check + '_check/docs/**/*.html' + fail: true + + - uses: actions/upload-artifact@v7 + with: + name: site + path: public + retention-days: 7 deploy: - # Only main publishes. A pull request stops after build above: it has no - # business pushing to gh-pages, and `permissions: contents: write` is not - # granted to a fork's PR anyway. - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' needs: build - # Two deploys must never run at once: `mike deploy --push` pushes gh-pages, - # and a second job doing the same is rejected outright -- - # - # ! [rejected] gh-pages -> gh-pages (fetch first) - # - # cancel-in-progress stays FALSE deliberately: killing a publish half way - # through is worse than making it wait, and the queued run then fetches the - # gh-pages the previous one just wrote. + # Never two deploys at once: both would push gh-pages and the second would + # be rejected. cancel-in-progress stays false — killing a publish half way + # through is worse than making it wait. concurrency: group: gh-pages-deploy cancel-in-progress: false @@ -61,32 +107,54 @@ jobs: permissions: contents: write steps: - - name: Checkout - uses: actions/checkout@v7 + - uses: actions/checkout@v7 with: + path: gh-pages fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v7 + - name: Switch to gh-pages + working-directory: gh-pages + run: | + if git ls-remote --exit-code --heads origin gh-pages >/dev/null; then + git fetch -q origin gh-pages + git checkout -q -B gh-pages FETCH_HEAD + else + echo "no gh-pages branch yet: starting one" + git checkout -q --orphan gh-pages + git rm -rqf . + git clean -fdxq + fi + + - uses: actions/download-artifact@v8 with: - python-version: "3.15" + name: site + path: site - - name: Install MkDocs Material + mike + # Only the version being built is replaced; every other version stays as + # it is, and /docs/latest/ follows the newest. + - name: Publish ${{ needs.build.outputs.version }} + working-directory: gh-pages run: | - pip install --upgrade pip - pip install -r requirements.txt + version="${{ needs.build.outputs.version }}" + rm -rf "docs/${version}" + mkdir -p "docs/${version}" + cp -R ../site/. "docs/${version}/" + newest=$(ls docs | grep -E '^[0-9]+\.[0-9]+$' | sort -t. -k1,1n -k2,2n | tail -1 || true) + if [ -n "${newest}" ]; then + rm -rf docs/latest + cp -R "docs/${newest}" docs/latest + fi + touch .nojekyll - - name: Configure git for mike + - name: Push gh-pages + working-directory: gh-pages run: | - git config user.name "github-actions[bot]" + git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - # mike publishes versioned docs to the gh-pages branch; GitHub Pages serves - # that branch at https://go-ruby-set.github.io/docs/. The version label - # tracks main as "0.1" aliased "latest"; tag-driven versions can be added - # later. /docs/ redirects to the default ("latest"). - - name: Deploy versioned docs - run: | - git fetch origin gh-pages --depth=1 || true - mike deploy --push --update-aliases 0.1 latest - mike set-default --push latest + git add -A + if git diff --cached --quiet; then + echo "nothing changed" + exit 0 + fi + git commit -q -m "Deployed ${GITHUB_SHA::7} to ${{ needs.build.outputs.version }} with Hugo ${HUGO_VERSION}" + git push origin HEAD:gh-pages diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..966ed98 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "themes/hextra"] + path = themes/hextra + url = https://github.com/tannevaled/hextra.git diff --git a/README.md b/README.md index 42dfba0..5ebbce9 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ # go-ruby-net-http/docs Versioned documentation for [go-ruby-net-http](https://github.com/go-ruby-net-http), -built with [MkDocs Material](https://squidfunk.github.io/mkdocs-material/) and -versioned with [mike](https://github.com/jimporter/mike). Published to the +built with [Hugo](https://gohugo.io/) and this fleet's [Hextra fork](https://github.com/tannevaled/hextra) — upstream Hextra plus the opt-in features the fleet's sites use. Published to the `gh-pages` branch and served at . The organization landing page ([go-ruby-net-http.github.io](https://go-ruby-net-http.github.io)) @@ -13,10 +12,8 @@ links here. ## Local preview ```bash -python -m venv .venv && . .venv/bin/activate -pip install -r requirements.txt -mkdocs serve # http://localhost:8000 (current sources) -mike serve # preview the versioned site +git submodule update --init --recursive # themes/hextra, pinned at a release tag +hugo server # http://localhost:1313 ``` ## Benchmarks @@ -34,7 +31,12 @@ OUTER=60 WARM=5 bash benchmarks/run.sh ## Releasing a new docs version -```bash -mike deploy --push --update-aliases latest -mike set-default --push latest -``` +Each documented version is its own build under `/docs//`, and +`/docs/latest/` follows the newest — which is what `mike` did before. The version +comes from `params.docs.version` in `hugo.yaml`: set it to the release the pages +describe (`v0.16.0` → `/docs/0.16/`), or leave it at `main` while no release is +tagged. The `docs` workflow reads it, builds with that `--baseURL`, and replaces +**only** that version's path. + +⛔ The build runs with `--panicOnWarning`, so a link to a page that is not there +fails the lane instead of 404ing for a reader. diff --git a/docs/index.md b/content/en/_index.md similarity index 75% rename from docs/index.md rename to content/en/_index.md index 27bdde0..51f16ac 100644 --- a/docs/index.md +++ b/content/en/_index.md @@ -1,4 +1,9 @@ -# go-ruby-net-http documentation +--- +title: "Home" +weight: 10 +--- + +# go-ruby-net-http documentation {#go-ruby-net-http-documentation} **Ruby's `Net::HTTP` HTTP/1.1 message codec — in pure Go, no cgo, no I/O.** @@ -17,13 +22,16 @@ HTTP-message backend intended for [go-embedded-ruby](https://github.com/go-embed [go-ruby-yaml](https://github.com/go-ruby-yaml/yaml). The dependency runs one way: this library has **no dependency on the Ruby runtime**. -!!! success "Status: complete — MRI byte-exact codec" - A faithful pure-Go port of `Net::HTTP`'s message codec, validated by a - **differential oracle** against the system `ruby` — request bytes and parsed - responses compared byte-for-byte — at 100% coverage, `gofmt` + `go vet` clean, - CI green across the six 64-bit Go targets and three OSes. +{{< callout type="info" >}} +**Status: complete — MRI byte-exact codec** + +A faithful pure-Go port of `Net::HTTP`'s message codec, validated by a +**differential oracle** against the system `ruby` — request bytes and parsed +responses compared byte-for-byte — at 100% coverage, `gofmt` + `go vet` clean, +CI green across the six 64-bit Go targets and three OSes. +{{< /callout >}} -## The socket / TLS is a host-side seam +## The socket / TLS is a host-side seam {#the-socket-tls-is-a-host-side-seam} Building HTTP/1.1 request bytes and parsing a response byte stream is fully deterministic and needs no interpreter, so it lives here as pure Go. Opening the @@ -36,7 +44,7 @@ deterministic and needs no interpreter, so it lives here as pure Go. Opening the | write / read bytes on the socket | host (rbgo) | — | | parse the response | this library | `ParseResponse([]byte) (*Response, error)` | -## Quick taste +## Quick taste {#quick-taste} ```go req, _ := nethttp.NewRequest("POST", "/submit", "example.com", nil) @@ -50,7 +58,7 @@ res.IsSuccess() // true res.Body() // "Wikipedia" (chunked-decoded) ``` -## Repositories +## Repositories {#repositories} | Repo | What it is | | --- | --- | @@ -59,7 +67,7 @@ res.Body() // "Wikipedia" (chunked-decoded) | [`go-ruby-net-http.github.io`](https://github.com/go-ruby-net-http/go-ruby-net-http.github.io) | the organization landing page (Hugo) | | [`brand`](https://github.com/go-ruby-net-http/brand) | logo and brand assets | -## Principles +## Principles {#principles} - **Pure Go, `CGO_ENABLED=0`** — trivial cross-compilation, a single static binary, no C toolchain. @@ -72,12 +80,12 @@ res.Body() // "Wikipedia" (chunked-decoded) runs the other way. - **100% test coverage**, enforced as a CI gate, across 6 arches and 3 OSes. -## Where to go next +## Where to go next {#where-to-go-next} -- [Why pure Go](why.md) — why this slice of `Net::HTTP` is deterministic enough to +- [Why pure Go]({{< relref "why.md" >}}) — why this slice of `Net::HTTP` is deterministic enough to live as a standalone, interpreter-independent Go library. -- [Usage & API](api.md) — the public surface and worked examples. -- [Roadmap](roadmap.md) — what is done and what is downstream (the transport) by design. -- [Performance](performance.md) — the offline, cross-runtime codec benchmark. +- [Usage & API]({{< relref "api.md" >}}) — the public surface and worked examples. +- [Roadmap]({{< relref "roadmap.md" >}}) — what is done and what is downstream (the transport) by design. +- [Performance]({{< relref "performance.md" >}}) — the offline, cross-runtime codec benchmark. Source lives at [github.com/go-ruby-net-http/net-http](https://github.com/go-ruby-net-http/net-http). diff --git a/docs/api.md b/content/en/api.md similarity index 87% rename from docs/api.md rename to content/en/api.md index 8780122..b2a1235 100644 --- a/docs/api.md +++ b/content/en/api.md @@ -1,4 +1,9 @@ -# Usage & API +--- +title: "Usage & API" +weight: 30 +--- + +# Usage & API {#usage-api} The public API lives at the module root (`github.com/go-ruby-net-http/net-http`, package `nethttp`). It is **Ruby-shaped but Go-idiomatic**: the names mirror @@ -7,17 +12,20 @@ package `nethttp`). It is **Ruby-shaped but Go-idiomatic**: the names mirror Go conventions — explicit byte slices, returned errors, no global state, and no hidden I/O. -!!! success "Status: implemented" - The library is built and importable as `github.com/go-ruby-net-http/net-http`; - see [Roadmap](roadmap.md). +{{< callout type="info" >}} +**Status: implemented** + +The library is built and importable as `github.com/go-ruby-net-http/net-http`; +see [Roadmap]({{< relref "roadmap.md" >}}). +{{< /callout >}} -## Install +## Install {#install} ```sh go get github.com/go-ruby-net-http/net-http ``` -## Building a request +## Building a request {#building-a-request} `NewRequest` mirrors `Net::HTTPGenericRequest#initialize` for the `Get` / `Post` / `Put` / `Delete` / `Head` / `Patch` / `Options` subclasses: it seeds MRI's default @@ -43,7 +51,7 @@ wire, _ := req.Bytes("1.1") // exact MRI socket empty-body default for body-permitting methods, and Basic / Proxy-Basic auth (`req.BasicAuth("user", "pass")`). -## Parsing a response +## Parsing a response {#parsing-a-response} `ParseResponse` mirrors `Net::HTTPResponse.read_new` + `read_body`: the status line, folded multi-value headers, and the body decoded by `Content-Length` **or** @@ -67,7 +75,7 @@ selects — `HTTPOK`, `HTTPNotFound`, `HTTPMovedPermanently`, … with the `HTTPUnknownResponse` / category fallbacks — exposed as `Class()` / `Category()` and the `Is*` predicates rather than a distinct Go type per code. -## The `Net::HTTPHeader` mixin +## The `Net::HTTPHeader` mixin {#the-nethttpheader-mixin} Both `Request` and `Response` embed `*Header`, the pure-Go port of the `Net::HTTPHeader` mixin (keys stored downcased, first-insertion order preserved): @@ -84,7 +92,7 @@ h.ConnectionClose() // connection_close? h.EachCapitalized(fn) // each_capitalized (wire order + casing) ``` -## Public surface +## Public surface {#public-surface} ```go // Request building (Net::HTTPGenericRequest + the Get/Post/... subclasses). @@ -110,5 +118,5 @@ func EncodeWWWFormComponent(s string) string func EncodeWWWForm(pairs [][2]string) string ``` -See [Roadmap](roadmap.md) for the codec-vs-transport boundary and -[Performance](performance.md) for the offline benchmark. +See [Roadmap]({{< relref "roadmap.md" >}}) for the codec-vs-transport boundary and +[Performance]({{< relref "performance.md" >}}) for the offline benchmark. diff --git a/docs/contributing.md b/content/en/contributing.md similarity index 87% rename from docs/contributing.md rename to content/en/contributing.md index df277fa..59db0ea 100644 --- a/docs/contributing.md +++ b/content/en/contributing.md @@ -1,10 +1,15 @@ -# Contributing +--- +title: "Contributing" +weight: 60 +--- + +# Contributing {#contributing} Contributions are welcome. `go-ruby-net-http/net-http` is built to a small set of non-negotiable rules — they are what keep it pure-Go, correct, and MRI-compatible. Please read these before opening a pull request. -## Hard rules +## Hard rules {#hard-rules} - **Build from source — no vendoring.** Everything compiles from source. Being able to compile from source is a guarantee of independence. @@ -22,7 +27,7 @@ MRI-compatible. Please read these before opening a pull request. performs **no I/O** — no socket, no TLS, no clock. Anything that needs the network belongs in the host (rbgo), not here. -## Workflow +## Workflow {#workflow} 1. Pick or open an issue describing the change. 2. Work test-first: add the differential / unit tests, then make them pass. @@ -36,11 +41,11 @@ MRI-compatible. Please read these before opening a pull request. 4. Open a PR in English, referencing the issue. -## Where things live +## Where things live {#where-things-live} The library is in [`github.com/go-ruby-net-http/net-http`](https://github.com/go-ruby-net-http/net-http). This documentation site (and the offline benchmark harness) is in [`github.com/go-ruby-net-http/docs`](https://github.com/go-ruby-net-http/docs). -Start from the [Usage & API](api.md) page and the [Roadmap](roadmap.md) to find +Start from the [Usage & API]({{< relref "api.md" >}}) page and the [Roadmap]({{< relref "roadmap.md" >}}) to find the right place for your change. diff --git a/docs/performance.md b/content/en/performance.md similarity index 56% rename from docs/performance.md rename to content/en/performance.md index 516816a..68b5582 100644 --- a/docs/performance.md +++ b/content/en/performance.md @@ -1,4 +1,9 @@ -# Performance +--- +title: "Performance" +weight: 50 +--- + +# Performance {#performance} `go-ruby-net-http/net-http` is the pure-Go library that [`rbgo`](https://github.com/go-embedded-ruby/ruby) can bind for Ruby's @@ -6,16 +11,19 @@ benchmark** of that codec against the reference Ruby runtimes, part of the ecosystem-wide per-module parity suite. -!!! warning "Offline scope — this is the codec, not the network" - `net-http` is a **message codec with no I/O**: DNS, the `TCPSocket`, the TLS - handshake and the byte transfer are the host's job (rbgo's), by design. This - benchmark therefore measures **only the deterministic, network-free surface** — - building request bytes, parsing a response buffer (including chunked decode), - URI form encoding, and header canonicalization. It is **not** an end-to-end - HTTP latency comparison; the wire, the kernel and the TLS stack dominate any - real request and are identical regardless of who builds the bytes. +{{< callout type="warning" >}} +**Offline scope — this is the codec, not the network** + +`net-http` is a **message codec with no I/O**: DNS, the `TCPSocket`, the TLS +handshake and the byte transfer are the host's job (rbgo's), by design. This +benchmark therefore measures **only the deterministic, network-free surface** — +building request bytes, parsing a response buffer (including chunked decode), +URI form encoding, and header canonicalization. It is **not** an end-to-end +HTTP latency comparison; the wire, the kernel and the TLS stack dominate any +real request and are identical regardless of who builds the bytes. +{{< /callout >}} -## What is measured +## What is measured {#what-is-measured} The **same** four network-free operations run through the pure-Go library (via its Go API) and through each reference runtime's own `Net::HTTP` / `URI`. Inputs @@ -40,7 +48,7 @@ driver**; a runtime that diverges is skipped, not timed. So each column is the Interpreter start-up is outside the timed region, so these are operation costs, not `ruby file.rb` process costs. -## Summary — the pure-Go codec beats MRI *and* YJIT on all four +## Summary — the pure-Go codec beats MRI *and* YJIT on all four {#summary-the-pure-go-codec-beats-mri-and-yjit-on-all-four} | Op | go-ruby (ns/op) | vs MRI | vs YJIT | verdict | | --- | ---: | ---: | ---: | --- | @@ -57,9 +65,9 @@ per-header `String`/`MatchData` allocation is a structural win here, so there is cherry-pick: the numbers below are every op measured, and the cross-runtime CHECK gate guarantees each column computed the identical bytes. -## Per-operation results (best of 60) +## Per-operation results (best of 60) {#per-operation-results-best-of-60} -#### request-serialize +#### request-serialize {#request-serialize} | Runtime | ns/op | vs MRI | | --- | ---: | ---: | @@ -69,7 +77,7 @@ gate guarantees each column computed the identical bytes. | JRuby | 3534.8 | 0.75× | | TruffleRuby | 8707.9 | 1.84× | -#### response-parse +#### response-parse {#response-parse} | Runtime | ns/op | vs MRI | | --- | ---: | ---: | @@ -79,7 +87,7 @@ gate guarantees each column computed the identical bytes. | JRuby | 11425.6 | 0.47× | | TruffleRuby | 17964.2 | 0.74× | -#### form-encode +#### form-encode {#form-encode} | Runtime | ns/op | vs MRI | | --- | ---: | ---: | @@ -89,7 +97,7 @@ gate guarantees each column computed the identical bytes. | JRuby | 12307.7 | 0.42× | | TruffleRuby | 21444.2 | 0.74× | -#### header-canonicalize +#### header-canonicalize {#header-canonicalize} | Runtime | ns/op | vs MRI | | --- | ---: | ---: | @@ -99,31 +107,40 @@ gate guarantees each column computed the identical bytes. | JRuby | 5366.0 | 0.36× | | TruffleRuby | 12570.6 | 0.84× | -!!! note "Reproduce" - The harness is committed under - [`benchmarks/`](https://github.com/go-ruby-net-http/docs/tree/main/benchmarks): - a self-contained Go driver (`go/`, which pins this library's published - pseudo-version via `go.mod`), the equivalent `ruby/net_http.rb` workload, and - `run.sh`. Run `OUTER=60 WARM=5 bash benchmarks/run.sh`; env `OUTER`/`WARM` - tune the pass budget and `RUBY`/`JRUBY`/`TRUFFLERUBY` select the runtime - binaries. `run.sh` verifies every runtime's `sha256` CHECK against the Go - driver before timing it. - -!!! warning "Warm-up budget & noise — honest framing" - Numbers reflect a **fixed warm-process budget** (5 warm-up + 60 timed passes - in one process, best pass reported). The JVM/GraalVM JITs (JRuby, TruffleRuby) - may need a larger warm-up to reach steady state, so their columns can - **understate** peak throughput — TruffleRuby's `request-serialize` (1.84×) is - the clearest case of a cold, short-loop outlier. Every number here is a **real - measured value** from the dated run above — nothing is fabricated, estimated, - or cherry-picked. The go-ruby column is the pure-Go library; every other column - is that interpreter's own `Net::HTTP` / `URI` doing the equivalent work, proven - to produce identical output. - -!!! note "What this does *not* claim" - This measures the **codec**, not HTTP. A real request is dominated by DNS, the - TCP round-trips and the TLS handshake — none of which this library performs, and - all of which are identical no matter which runtime formats the bytes. The result - to take away is narrow and honest: **formatting and parsing HTTP/1.1 messages is - materially cheaper in the pure-Go codec than in any Ruby runtime's `Net::HTTP`**, - so binding it costs the interpreter nothing on the compute side. +{{< callout type="info" >}} +**Reproduce** + +The harness is committed under +[`benchmarks/`](https://github.com/go-ruby-net-http/docs/tree/main/benchmarks): +a self-contained Go driver (`go/`, which pins this library's published +pseudo-version via `go.mod`), the equivalent `ruby/net_http.rb` workload, and +`run.sh`. Run `OUTER=60 WARM=5 bash benchmarks/run.sh`; env `OUTER`/`WARM` +tune the pass budget and `RUBY`/`JRUBY`/`TRUFFLERUBY` select the runtime +binaries. `run.sh` verifies every runtime's `sha256` CHECK against the Go +driver before timing it. +{{< /callout >}} + +{{< callout type="warning" >}} +**Warm-up budget & noise — honest framing** + +Numbers reflect a **fixed warm-process budget** (5 warm-up + 60 timed passes +in one process, best pass reported). The JVM/GraalVM JITs (JRuby, TruffleRuby) +may need a larger warm-up to reach steady state, so their columns can +**understate** peak throughput — TruffleRuby's `request-serialize` (1.84×) is +the clearest case of a cold, short-loop outlier. Every number here is a **real +measured value** from the dated run above — nothing is fabricated, estimated, +or cherry-picked. The go-ruby column is the pure-Go library; every other column +is that interpreter's own `Net::HTTP` / `URI` doing the equivalent work, proven +to produce identical output. +{{< /callout >}} + +{{< callout type="info" >}} +**What this does *not* claim** + +This measures the **codec**, not HTTP. A real request is dominated by DNS, the +TCP round-trips and the TLS handshake — none of which this library performs, and +all of which are identical no matter which runtime formats the bytes. The result +to take away is narrow and honest: **formatting and parsing HTTP/1.1 messages is +materially cheaper in the pure-Go codec than in any Ruby runtime's `Net::HTTP`**, +so binding it costs the interpreter nothing on the compute side. +{{< /callout >}} diff --git a/docs/roadmap.md b/content/en/roadmap.md similarity index 90% rename from docs/roadmap.md rename to content/en/roadmap.md index bd8de47..d9e4d37 100644 --- a/docs/roadmap.md +++ b/content/en/roadmap.md @@ -1,4 +1,9 @@ -# Roadmap +--- +title: "Roadmap" +weight: 40 +--- + +# Roadmap {#roadmap} `go-ruby-net-http/net-http` is grown **test-first**, each capability differential-tested against MRI rather than built in isolation. The @@ -13,7 +18,7 @@ codec — is **complete**. | `Net::HTTPHeader` mixin | `[]` / `[]=` / `key?` / `delete` / `add_field` / `get_fields` / `each_header` / `each_capitalized`, plus `content_type` / `content_length` / `chunked?` / `connection_close?` and the `URI.encode_www_form` component encoder. | **Done** | | Differential oracle & coverage | Request bytes, response parse, subclass over MRI's whole table, and form encoding checked byte-for-byte against `ruby` (≥ 4.0); 100% coverage, green across 6 arches and 3 OSes. | **Done** | -## Documented out-of-scope boundaries +## Documented out-of-scope boundaries {#documented-out-of-scope-boundaries} These are **deliberate**, recorded so the module's surface is unambiguous: @@ -28,5 +33,5 @@ These are **deliberate**, recorded so the module's surface is unambiguous: - **Reference is reference Ruby (MRI).** Byte-for-byte conformance targets MRI 4.0.5's `Net::HTTP`, as pinned by the differential oracle. -See [Usage & API](api.md) for the surface and [Why pure Go](why.md) for the +See [Usage & API]({{< relref "api.md" >}}) for the surface and [Why pure Go]({{< relref "why.md" >}}) for the codec/transport split. diff --git a/docs/why.md b/content/en/why.md similarity index 85% rename from docs/why.md rename to content/en/why.md index 38f953a..dfa302b 100644 --- a/docs/why.md +++ b/content/en/why.md @@ -1,4 +1,9 @@ -# Why pure Go +--- +title: "Why pure Go" +weight: 20 +--- + +# Why pure Go {#why-pure-go} `go-ruby-net-http/net-http` reimplements the core of Ruby's `Net::HTTP` in **pure Go, with cgo disabled**. The slice of `Net::HTTP` it covers is **deterministic and @@ -9,7 +14,7 @@ bytes, no live binding, no evaluation of arbitrary Ruby. That is precisely the part that can — and should — live as a standalone Go library, separate from both the interpreter and the network. -## The transport is a separate concern +## The transport is a separate concern {#the-transport-is-a-separate-concern} `Net::HTTP` bundles two very different things: a **message codec** (format the request, parse the response) and a **transport** (DNS, `TCPSocket`, TLS, timeouts, @@ -25,7 +30,7 @@ library implements exactly that half: Keeping the transport out is what makes the codec **testable in isolation** and **reusable** by any Go program that already has a byte transport. -## Extracted for reuse, bound the other way +## Extracted for reuse, bound the other way {#extracted-for-reuse-bound-the-other-way} This library is designed as a reusable standalone module so that: @@ -38,7 +43,7 @@ This library is designed as a reusable standalone module so that: - the behaviour is pinned by a **differential oracle** against the system `ruby`, independent of any one consumer. -## Why pure Go matters here +## Why pure Go matters here {#why-pure-go-matters-here} Because the library is CGO-free, it: @@ -49,5 +54,5 @@ Because the library is CGO-free, it: `PATH`, while the cross-arch lanes (where `ruby` is absent) still validate the codec itself. -See [Usage & API](api.md) for the surface and [Roadmap](roadmap.md) for the +See [Usage & API]({{< relref "api.md" >}}) for the surface and [Roadmap]({{< relref "roadmap.md" >}}) for the codec-vs-transport boundary. diff --git a/hugo.yaml b/hugo.yaml new file mode 100644 index 0000000..aad1a75 --- /dev/null +++ b/hugo.yaml @@ -0,0 +1,87 @@ +# go-ruby-net-http docs documentation — Hugo + Hextra, built like go-plot/docs and +# go-ndarray/docs. +# +# Each documentation version is a separate build under +# https://go-ruby-net-http.github.io/docs//, where is the minor version of the +# code the pages describe, or "main" while no release is tagged. The CI sets it +# through --baseURL, from params.docs.version below; this default is the same +# build. +# +# ⛔ What this replaced was MkDocs Material plus mike, which needed a Python +# toolchain, a requirements.txt and a pip install in CI to render a static site +# for a pure-Go organisation. +baseURL: "https://go-ruby-net-http.github.io/docs/main/" +title: "go-ruby-net-http docs" +# ⛔ No languageCode: deprecated in Hugo 0.158 in favour of locale, and the +# languages block below already says which language this is. The second +# deprecation the pilot build refused, after cascade._target — both inherited +# from copying an older site in this fleet. + +# themes/hextra is the tannevaled/hextra fork, pinned at a release tag as a +# submodule: upstream Hextra plus opt-in features (partial menu items, combos, +# related-sites menu, page subtitle and history, darkreaderLock). +theme: hextra + +defaultContentLanguage: en +defaultContentLanguageInSubdir: false + +languages: + en: + label: "English" + contentDir: content/en + weight: 1 + +enableRobotsTXT: true +# Git history dates every page: "Last updated on" at the foot of each one. +enableGitInfo: true + +markup: + goldmark: + renderer: + unsafe: true + parser: + attribute: + title: true + # ⛔ Block attributes, because every converted heading carries an + # explicit id — "## Heading {#the-mkdocs-slug}". The two slug rules + # disagree: Python-Markdown drops an em dash and collapses the gap it + # leaves into ONE hyphen, goldmark keeps one hyphen per space. An + # anchor is a URL somebody may have saved, so the old spelling is + # kept. Without this setting the braces are printed as text. + block: true + highlight: + noClasses: false + +# ⛔ The content is NOT under content/en/docs. This site is served AT /docs/, +# so a "docs" section would make every page /docs/docs/… . Hextra picks its +# documentation layout from the section being called "docs", so with the +# content at the root the type is cascaded instead. +# ⛔ "target", not "_target": the underscored spelling was deprecated in Hugo +# 0.156 and the fleet's older Hextra sites still carry it, so copying one of +# them is how a converted site arrives with a deprecation. The build runs with +# --panicOnWarning, which is what caught this on the first pilot. +cascade: + - target: + path: "/**" + params: + type: docs + +params: + docs: + version: "main" + description: "A pure-Go reimplementation of Ruby's Net::HTTP HTTP/1.1 message codec — MRI-faithful, no cgo, no I/O." + navbar: + displayTitle: true + displayLogo: false + theme: + default: system + displayToggle: true + footer: + enable: true + displayCopyright: false + displayPoweredBy: false + page: + width: normal + editURL: + enable: true + base: "https://github.com/go-ruby-net-http/edit/main/content/en" diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index d9d9388..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,61 +0,0 @@ -site_name: go-ruby-net-http docs -site_description: "A pure-Go reimplementation of Ruby's Net::HTTP HTTP/1.1 message codec — MRI-faithful, no cgo, no I/O." -site_url: https://go-ruby-net-http.github.io/docs/ -repo_url: https://github.com/go-ruby-net-http -repo_name: go-ruby-net-http -edit_uri: "" - -theme: - name: material - logo: assets/logo.svg - favicon: assets/logo.svg - features: - - navigation.sections - - navigation.top - - navigation.instant - - content.code.copy - - search.suggest - palette: - # System default (follows the OS preference), then explicit light and dark. - - media: "(prefers-color-scheme)" - toggle: - icon: material/brightness-auto - name: Switch to light mode - - media: "(prefers-color-scheme: light)" - scheme: default - primary: red - toggle: - icon: material/brightness-7 - name: Switch to dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: red - toggle: - icon: material/brightness-4 - name: Switch to system preference - -extra: - version: - provider: mike - default: latest - -nav: - - Home: index.md - - Why pure Go: why.md - - Usage & API: api.md - - Roadmap: roadmap.md - - Performance: performance.md - - Contributing: contributing.md - -markdown_extensions: - - admonition - - attr_list - - def_list - - footnotes - - tables - - toc: - permalink: true - - pymdownx.highlight: - anchor_linenums: true - - pymdownx.superfences - - pymdownx.inlinehilite diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 34eb296..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -mkdocs-material==9.7.7 -mike==2.2.0 diff --git a/docs/assets/logo.svg b/static/assets/logo.svg similarity index 100% rename from docs/assets/logo.svg rename to static/assets/logo.svg diff --git a/themes/hextra b/themes/hextra new file mode 160000 index 0000000..1d8daa4 --- /dev/null +++ b/themes/hextra @@ -0,0 +1 @@ +Subproject commit 1d8daa42d0406250fa00b7e803db2ab104a7fcab