chore(repo): reinitialize repository
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type statusError struct {
|
||||
status int
|
||||
err error
|
||||
}
|
||||
|
||||
func (e statusError) Error() string {
|
||||
if e.err == nil {
|
||||
return ""
|
||||
}
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e statusError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func ErrorWithStatus(status int, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return statusError{status: status, err: err}
|
||||
}
|
||||
|
||||
func StatusFromError(err error) (int, bool) {
|
||||
var target statusError
|
||||
if errors.As(err, &target) {
|
||||
return target.status, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func WriteJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(v)
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, status int, msg string) {
|
||||
WriteJSON(w, status, map[string]string{"error": msg})
|
||||
}
|
||||
|
||||
func DecodeJSON(r *http.Request, dst any) error {
|
||||
dec := json.NewDecoder(r.Body)
|
||||
dec.DisallowUnknownFields()
|
||||
return dec.Decode(dst)
|
||||
}
|
||||
|
||||
func CORSMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package idgen
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"inbox/internal/base/slug"
|
||||
"inbox/internal/base/timeutil"
|
||||
)
|
||||
|
||||
type Generator struct {
|
||||
Clock timeutil.Clock
|
||||
Random io.Reader
|
||||
SuffixSize int
|
||||
}
|
||||
|
||||
func NewGenerator(clock timeutil.Clock, random io.Reader) Generator {
|
||||
if clock == nil {
|
||||
clock = timeutil.SystemClock{}
|
||||
}
|
||||
if random == nil {
|
||||
random = rand.Reader
|
||||
}
|
||||
return Generator{
|
||||
Clock: clock,
|
||||
Random: random,
|
||||
SuffixSize: 6,
|
||||
}
|
||||
}
|
||||
|
||||
func (g Generator) New(kind string) (string, error) {
|
||||
prefix := slug.NormalizeWithLimit(kind, 24)
|
||||
if prefix == "" {
|
||||
prefix = "id"
|
||||
}
|
||||
size := g.SuffixSize
|
||||
if size <= 0 {
|
||||
size = 6
|
||||
}
|
||||
buf := make([]byte, size)
|
||||
if _, err := io.ReadFull(g.Random, buf); err != nil {
|
||||
return "", fmt.Errorf("generate id suffix: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("%s-%s-%s", prefix, timeutil.TimestampIDPart(g.Clock.Now()), hex.EncodeToString(buf)), nil
|
||||
}
|
||||
|
||||
func New(kind string) (string, error) {
|
||||
return NewGenerator(timeutil.SystemClock{}, rand.Reader).New(kind)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package idgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inbox/internal/base/timeutil"
|
||||
)
|
||||
|
||||
func TestGeneratorNew(t *testing.T) {
|
||||
gen := NewGenerator(
|
||||
timeutil.FixedClock{Time: time.Date(2026, 3, 13, 10, 11, 12, 0, time.UTC)},
|
||||
bytes.NewReader([]byte{0, 1, 2, 3, 4, 5}),
|
||||
)
|
||||
got, err := gen.New("Role Prompt")
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
if got != "role-prompt-20260313T101112Z-000102030405" {
|
||||
t.Fatalf("New() = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package slug
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultLimit = 80
|
||||
)
|
||||
|
||||
var invalidChars = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
func Normalize(s string) string {
|
||||
return NormalizeWithLimit(s, DefaultLimit)
|
||||
}
|
||||
|
||||
func NormalizeWithLimit(s string, limit int) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
s = invalidChars.ReplaceAllString(s, "-")
|
||||
s = strings.Trim(s, "-")
|
||||
if limit > 0 && len(s) > limit {
|
||||
s = strings.Trim(s[:limit], "-")
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package slug
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalize(t *testing.T) {
|
||||
got := Normalize(" Product Prompt V2 / Main ")
|
||||
if got != "product-prompt-v2-main" {
|
||||
t.Fatalf("Normalize() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeWithLimit(t *testing.T) {
|
||||
got := NormalizeWithLimit("abcdefghijklmnopqrstuvwxyz", 10)
|
||||
if got != "abcdefghij" {
|
||||
t.Fatalf("NormalizeWithLimit() = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package timeutil
|
||||
|
||||
import "time"
|
||||
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
type SystemClock struct{}
|
||||
|
||||
func (SystemClock) Now() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
type FixedClock struct {
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
func (c FixedClock) Now() time.Time {
|
||||
return c.Time.UTC()
|
||||
}
|
||||
|
||||
func NowUTC() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func FormatRFC3339(t time.Time) string {
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func FormatRFC3339Nano(t time.Time) string {
|
||||
return t.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
func TimestampIDPart(t time.Time) string {
|
||||
return t.UTC().Format("20060102T150405Z")
|
||||
}
|
||||
Reference in New Issue
Block a user