golib/database/csvx/http.go

131 lines
3.3 KiB
Go

// Copyright 2025 Cogburn Bros
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain a copy at https://mozilla.org/MPL/2.0/.
package csvx
import (
"fmt"
"net/http"
"strconv"
"strings"
)
// Format is a canonical output format.
type Format string
const (
// DefaultFormat is csv — project rule: all new routes default to CSV.
DefaultFormat Format = "csv"
FormatCSV Format = "csv"
FormatTSV Format = "tab"
FormatJSON Format = "json"
FormatJSONL Format = "jsonl"
)
// ContentType maps canonical format to MIME type.
func ContentType(f Format) string {
switch f {
case FormatCSV:
return "text/csv"
case FormatTSV:
return "text/tab-separated-values"
case FormatJSONL:
return "application/x-ndjson"
default:
return "application/json"
}
}
// normalize maps ?format= values to canonical format names.
func normalize(s string) (Format, bool) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "csv":
return FormatCSV, true
case "tsv", "tab":
return FormatTSV, true
case "json":
return FormatJSON, true
case "ndjson", "jsonl":
return FormatJSONL, true
}
return "", false
}
// mediaFormat maps Accept media types to canonical format names.
func mediaFormat(media string) (Format, bool) {
switch strings.ToLower(strings.TrimSpace(media)) {
case "text/csv":
return FormatCSV, true
case "text/tab-separated-values":
return FormatTSV, true
case "application/json", "text/json":
return FormatJSON, true
case "application/x-ndjson", "application/jsonl":
return FormatJSONL, true
}
return "", false
}
// ResolveFormat returns the output format for a request. ?format= has
// priority. Otherwise the Accept header's best q-matched media type is used.
// Per project rule, the default (empty Accept, */*, or no match) is CSV.
// Returns an error only for an explicit unknown ?format= value.
func ResolveFormat(r *http.Request) (Format, error) {
if f := r.URL.Query().Get("format"); f != "" {
format, ok := normalize(f)
if !ok {
return "", fmt.Errorf("unsupported format %q (accepted: csv, tsv, json, jsonl)", f)
}
return format, nil
}
return acceptFormat(r.Header.Get("Accept"))
}
// acceptFormat inspects the Accept header's quality values and returns the
// best-matching format. Defaults to CSV when the header is empty, wildcarded,
// or has no matching media type.
func acceptFormat(accept string) (Format, error) {
best, bestQ := DefaultFormat, -1.0
found := false
for part := range strings.SplitSeq(accept, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
media := part
q := 1.0
if before, after, ok := strings.Cut(part, ";"); ok {
media = before
for param := range strings.SplitSeq(after, ";") {
param = strings.TrimSpace(param)
if strings.HasPrefix(strings.ToLower(param), "q=") {
if parsed, err := strconv.ParseFloat(strings.TrimSpace(param[2:]), 64); err == nil {
q = parsed
}
}
}
}
media = strings.TrimSpace(media)
var f Format
var ok bool
if media == "*/*" {
f, ok = DefaultFormat, true
} else {
f, ok = mediaFormat(media)
}
if ok && q > bestQ {
best, bestQ, found = f, q, true
}
}
if !found {
return DefaultFormat, nil
}
return best, nil
}