mirror of
https://github.com/therootcompany/golib.git
synced 2026-11-04 06:55:00 +00:00
131 lines
3.3 KiB
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
|
|
}
|