Move to github.com/tkw1536/pkglib package

This commit removes various modules that can be migrated to the
github.com/tkw1536/pkglib package without any code changes (beyond
module renamings).
This commit is contained in:
Tom Wiesing 2023-02-26 09:53:25 +01:00
parent 30c25b8e2a
commit c3ca8e2974
No known key found for this signature in database
65 changed files with 103 additions and 1254 deletions

View file

@ -1,70 +0,0 @@
package cancel
import (
"context"
"time"
"github.com/FAU-CDI/wisski-distillery/pkg/timex"
)
// Anyways behaves like context.WithTimeout, except that if the Done() channel of ctx is closed before Anyways is called, the returned context's Done() channel is only closed after timeout.
func Anyways(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
// context is not yet cancelled => return as-is
if err := ctx.Err(); err == nil {
return context.WithTimeout(ctx, timeout)
}
// create a new anyways
any := &anyways{
done: make(chan struct{}),
parent: ctx,
deadline: time.Now().Add(timeout),
}
// start waiting for the timer (or the cancel to be called)
finish := make(chan struct{})
go func() {
t := timex.NewTimer()
t.Reset(timeout)
defer timex.ReleaseTimer(t)
defer close(any.done)
select {
case <-t.C:
case <-finish:
}
}()
return any, func() {
close(finish)
}
}
type anyways struct {
done chan struct{}
parent context.Context
deadline time.Time
}
func (a anyways) Deadline() (deadline time.Time, ok bool) {
return a.deadline, true
}
func (a anyways) Done() <-chan struct{} {
return a.done
}
func (a anyways) Err() error {
select {
case <-a.done:
return context.DeadlineExceeded
default:
return nil
}
}
func (a anyways) Value(key any) any {
return a.parent.Done()
}

View file

@ -1,56 +0,0 @@
package cancel
import (
"context"
)
// WithContext executes f and returns the returns the return value and nil.
//
// If the context is closed before f returns, invokes cancel and returns f(), ctx.Err().
//
// In general, WithContext always waits for f() to return even if cancel was called.
// As a special case if a closed context is passed, f is not invoked.
//
// allowcancel must be called by f exactly once, as soon as the cancel function may be invoked.
func WithContext[T any](ctx context.Context, f func(allowcancel func()) T, cancel func()) (t T, err error) {
t, _, err = WithContext2(ctx, func(start func()) (T, struct{}) {
return f(start), struct{}{}
}, cancel)
return
}
// WithContext2 is exactly like WithContext, but takes a function returning two parameters.
func WithContext2[T1, T2 any](ctx context.Context, f func(start func()) (T1, T2), cancel func()) (t1 T1, t2 T2, err error) {
// context is already closed, don't even try invoking it.
if err := ctx.Err(); err != nil {
return t1, t2, err
}
cancelable := make(chan struct{}, 1)
done := make(chan struct{})
go func() {
defer close(done)
defer close(cancelable)
t1, t2 = f(func() {
cancelable <- struct{}{}
})
}()
select {
case <-done:
// the function has exited regularly
// nothing to be done
case <-ctx.Done():
// context was cancelled
<-cancelable
cancel()
// still wait for it to be done!
<-done
err = ctx.Err()
}
return
}

View file

@ -1,57 +0,0 @@
package cancel
import (
"context"
"io"
"time"
)
type SetDeadline interface {
SetDeadline(t time.Time)
}
type SetReadDeadline interface {
SetReadDeadline(t time.Time) error
}
type SetWriteDeadline interface {
SetWriteDeadline(t time.Time) error
}
// Copy reads from src, and copies to dst.
//
// If the context is closed before src is closed, attempts to close the underlying reader and writer.
func Copy(ctx context.Context, dst io.Writer, src io.Reader) (written int64, err error) {
// if the context has a deadline, the propanate that deadline to the underyling file.
// this might cause the read call to not block.
if deadline, ok := ctx.Deadline(); ok {
var zero time.Time
if file, ok := src.(SetReadDeadline); ok {
file.SetReadDeadline(deadline)
defer file.SetReadDeadline(zero)
} else if file, ok := src.(SetDeadline); ok {
file.SetDeadline(deadline)
defer file.SetDeadline(zero)
}
if file, ok := dst.(SetWriteDeadline); ok {
file.SetWriteDeadline(deadline)
defer file.SetWriteDeadline(zero)
} else if file, ok := dst.(SetDeadline); ok {
file.SetDeadline(deadline)
defer file.SetDeadline(zero)
}
}
written, err, _ = WithContext2(ctx, func(start func()) (int64, error) {
start()
return io.Copy(dst, src)
}, func() {
if closer, ok := src.(io.Closer); ok {
closer.Close()
}
})
return written, err
}

View file

@ -1,24 +0,0 @@
package cancel
import "context"
// ValuesOf returns a new context that has the same deadline and cancelation behviour as parent.
// However when requesting values from the context, checks the values in context first.
func ValuesOf(parent, values context.Context) context.Context {
return &valuesOf{
Context: parent,
values: values,
}
}
type valuesOf struct {
context.Context
values context.Context
}
func (vv *valuesOf) Value(key any) any {
if value := vv.values.Value(key); value != nil {
return value
}
return vv.Context.Value(key)
}

View file

@ -1,62 +0,0 @@
package countwriter
import (
"io"
)
// CountWriter wraps an io.Writer, see [NewCountWriter].
//
// It is intended to be used to count different writes to an underlying writer.
// Once an error occurs, no more writes are passed through, and the underlying error is returned instead.
// This means that in practice, calls to write can be continued and are ignored silently.
//
// The underlying sum of bytes written and error can be seen using [Sum].
type CountWriter struct {
w io.Writer
n int
err error
}
// NewCountWriter creates a new [CountWriter] that delegates to w.
func NewCountWriter(w io.Writer) *CountWriter {
return &CountWriter{w: w}
}
// write performs the write operation w on this writer.
func (cw *CountWriter) write(w func() (int, error)) (int, error) {
// if there was an error, return it and don't do a write
if cw.err != nil {
return 0, cw.err
}
// call the writer
n, err := w()
// update the underling state
cw.n += n
cw.err = err
// and return
return n, err
}
// Write implements [io.Writer]
func (cw *CountWriter) Write(p []byte) (int, error) {
return cw.write(func() (int, error) {
return cw.w.Write(p)
})
}
// WriteString implements [io.WriteString].
// See [Write].
func (cw *CountWriter) WriteString(s string) (int, error) {
return cw.write(func() (int, error) {
return io.WriteString(cw.w, s)
})
}
// Sum returns the state, that is the total number of bytes written and any error
func (cw *CountWriter) Sum() (int, error) {
return cw.n, cw.err
}

View file

@ -6,8 +6,8 @@ import (
"io/fs"
"os"
"github.com/FAU-CDI/wisski-distillery/pkg/pools"
"github.com/tkw1536/goprogram/stream"
"github.com/tkw1536/pkglib/pools"
)
// ExecCommandError is returned by Exec when a command could not be executed.

View file

@ -1,8 +0,0 @@
package errorx
import "github.com/tkw1536/goprogram/lib/collection"
// First returns the first non-nil error, or nil otherwise.
func First(errors ...error) error {
return collection.First(errors, func(err error) bool { return err != nil })
}

View file

@ -6,8 +6,8 @@ import (
"io/fs"
"path/filepath"
"github.com/FAU-CDI/wisski-distillery/pkg/cancel"
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
"github.com/tkw1536/pkglib/contextx"
)
var ErrCopySameFile = errors.New("src and dst must be different")
@ -47,7 +47,7 @@ func CopyFile(ctx context.Context, env environment.Environment, dst, src string)
defer dstFile.Close()
// and do the copy!
_, err = cancel.Copy(ctx, dstFile, srcFile)
_, err = contextx.Copy(ctx, dstFile, srcFile)
return err
}

View file

@ -1,34 +0,0 @@
// Package hostname provides the hostname.
package hostname
import (
"os"
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
"github.com/Showmax/go-fqdn"
)
// FQDN attempts to return the fully qualified domain name of the host system.
// If an error occurs, may fall back to the empty string.
func FQDN(env environment.Environment) string {
// TODO: Pass this through!
// try the hostname function
{
fqdn, err := fqdn.FqdnHostname()
if err == nil {
return fqdn
}
}
// fallback to os hostname
{
hostname, err := os.Hostname()
if err == nil {
return hostname
}
}
// use the empty string
return ""
}

View file

@ -7,8 +7,8 @@ import (
_ "embed"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
"github.com/FAU-CDI/wisski-distillery/pkg/pools"
"github.com/gorilla/csrf"
"github.com/tkw1536/pkglib/pools"
)
// Form provides a form that a user can submit via a http POST method call.

View file

@ -5,8 +5,8 @@ import (
"net/http"
"time"
"github.com/FAU-CDI/wisski-distillery/pkg/timex"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/timex"
)
const HTMLFlushInterval = time.Second / 10

View file

@ -3,7 +3,7 @@ package lazy
import (
"reflect"
"github.com/tkw1536/goprogram/lib/reflectx"
"github.com/tkw1536/pkglib/reflectx"
)
// Pool represents a pool of laziliy initialized and potentially referencing Component instances.

View file

@ -3,7 +3,7 @@ package lazy
import (
"reflect"
"github.com/tkw1536/goprogram/lib/collection"
"github.com/tkw1536/pkglib/collection"
"golang.org/x/exp/slices"
)

View file

@ -3,8 +3,8 @@ package lazy
import (
"reflect"
"github.com/tkw1536/goprogram/lib/collection"
"github.com/tkw1536/goprogram/lib/reflectx"
"github.com/tkw1536/pkglib/collection"
"github.com/tkw1536/pkglib/reflectx"
)
// getMeta gets the component belonging to a component type

View file

@ -1,154 +0,0 @@
// Package mux provides mux
package mux
import (
"context"
"net/http"
)
// Mux represents a mux that can handle different requests
type Mux[C any] struct {
prefixes map[string][]handler
exacts map[string][]handler
Context func(r *http.Request) C // called to set context on the given request
Panic func(panic any, w http.ResponseWriter, r *http.Request) // called on panic
NotFound http.Handler // optional handler to be called in case of a not found
}
type contextKey struct{}
var theContextKey = contextKey{}
type handler struct {
Predicate Predicate
http.Handler
}
func (mux *Mux[T]) Prepare(r *http.Request) *http.Request {
if mux == nil || mux.Context == nil {
return r
}
ctx := context.WithValue(r.Context(), theContextKey, mux.Context(r))
return r.WithContext(ctx)
}
func (mux *Mux[T]) ContextOf(r *http.Request) (t T) {
value, ok := r.Context().Value(theContextKey).(T)
if !ok {
return t
}
return value
}
// Add adds a handler for the given path
func (mux *Mux[T]) Add(path string, predicate Predicate, exact bool, h http.Handler) {
if mux.exacts == nil {
mux.exacts = make(map[string][]handler)
}
if mux.prefixes == nil {
mux.prefixes = make(map[string][]handler)
}
mPath := NormalizePath(path)
mHandler := handler{Predicate: predicate, Handler: h}
if exact {
mux.exacts[mPath] = append(mux.exacts[mPath], mHandler)
} else {
mux.prefixes[mPath] = append(mux.prefixes[mPath], mHandler)
}
}
// Match returns the handler to be applied for the given request.
func (mux *Mux[T]) Match(r *http.Request, prepare bool) (http.Handler, bool) {
if mux == nil {
return nil, false
}
if prepare {
r = mux.Prepare(r)
}
candidate := NormalizePath(r.URL.Path)
// match the exact path first
for _, h := range mux.exacts[candidate] {
if h.Predicate.Call(r) {
return h.Handler, true
}
}
// iterate over path segment candidates
for {
// check the current candidate
for _, h := range mux.prefixes[candidate] {
if h.Predicate.Call(r) {
return h.Handler, true
}
}
// if the candidate is the root url, we can bail out now
if len(candidate) == 0 || candidate == "/" {
return nil, false
}
// move to the parent segment
candidate = parentSegment(candidate)
}
}
func (mux *Mux[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// handle panics with the panic handler
defer func() {
caught := recover()
if caught == nil {
return
}
if mux == nil || mux.Panic == nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// silently ignore any panic()s in the panic handler
defer func() {
recover()
}()
// call the panic handler
mux.Panic(caught, w, r)
}()
// prepare the request
r = mux.Prepare(r)
// find the right handler
// or go into 404 mode
handler, ok := mux.Match(r, false)
if !ok {
if mux == nil || mux.NotFound == nil {
http.NotFound(w, r)
return
}
mux.NotFound.ServeHTTP(w, r)
return
}
// call the actual handling
handler.ServeHTTP(w, r)
}
// Predicate represents a matching predicate for a given request.
// The nil predicate always matches
type Predicate func(r *http.Request) bool
// Call checks if this predicate matches the given request.
func (p Predicate) Call(r *http.Request) bool {
if p == nil {
return true
}
return p(r)
}

View file

@ -1,28 +0,0 @@
package mux
import (
"path"
)
// NormalizePath normalizes the provided path.
// It ensures that there is both a leading and trailing slash.
func NormalizePath(value string) string {
value = path.Clean(value)
if value != "/" {
value = value + "/"
}
return value
}
// parentSegment returns the parent segment of the provided path
// it assumes that normalizePath has been called on value.
func parentSegment(value string) string {
if value == "" || value == "/" {
return "/"
}
parent := path.Dir(value[:len(value)-1])
if parent != "/" {
parent = parent + "/"
}
return parent
}

View file

@ -5,7 +5,7 @@ import (
"crypto/rand"
"math/big"
"github.com/FAU-CDI/wisski-distillery/pkg/pools"
"github.com/tkw1536/pkglib/pools"
)
// NOTE(twiesing): A bunch of scripts cannot properly handle the extra characters in the password.

View file

@ -1,34 +0,0 @@
// Package pools holds various pools for reuse
package pools
import (
"bytes"
"strings"
"sync"
)
var builders = sync.Pool{
New: func() any { return new(strings.Builder) },
}
func GetBuilder() *strings.Builder {
return builders.Get().(*strings.Builder)
}
func ReleaseBuilder(builder *strings.Builder) {
builder.Reset()
builders.Put(builder)
}
var buffers = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
func GetBuffer() *bytes.Buffer {
return buffers.Get().(*bytes.Buffer)
}
func ReleaseBuffer(buffer *bytes.Buffer) {
buffer.Reset()
buffers.Put(buffer)
}

View file

@ -1,108 +0,0 @@
package spawner
import (
"io"
"log"
"sync"
)
type Spawner[T io.Closer] struct {
Spawn func() T
Alive func(t T) bool
Limit int
initOnce sync.Once
wg sync.WaitGroup
tasks chan task[T]
instances []T
alive []bool
}
type task[T any] struct {
f func(t T)
done chan<- struct{}
}
func (tt task[T]) Do(t T) {
defer close(tt.done)
tt.f(t)
}
func (spawner *Spawner[T]) worker(i int) {
spawner.wg.Add(1)
go func() {
defer spawner.wg.Done()
var zero T
for task := range spawner.tasks {
// spawn a new instance once finished
if !spawner.alive[i] {
log.Println("respawning instance ", i)
spawner.instances[i] = spawner.Spawn()
spawner.alive[i] = true
}
task.Do(spawner.instances[i])
// if the instance has died during execution
// then do a close (to deallocate)
if !spawner.Alive(spawner.instances[i]) {
spawner.instances[i].Close()
spawner.instances[i] = zero
spawner.alive[i] = false
}
}
}()
}
func (spawner *Spawner[T]) start() {
spawner.initOnce.Do(func() {
limit := spawner.Limit
if limit < 1 {
limit = 1
}
spawner.tasks = make(chan task[T], limit)
spawner.alive = make([]bool, limit)
spawner.instances = make([]T, limit)
for i := 0; i < limit; i++ {
spawner.worker(i)
}
})
}
// Do performs f on an unspecified object from the spawner.
// If no objects are available within time.Duration, a new object is spawned as there are at most limit objects.
func (spawner *Spawner[T]) Do(f func(t T)) {
spawner.start()
done := make(chan struct{})
spawner.tasks <- task[T]{
f: f,
done: done,
}
<-done
}
func (spawner *Spawner[T]) Close() {
close(spawner.tasks)
spawner.wg.Wait()
spawner.wg.Add(len(spawner.alive))
var zero T
for i := range spawner.alive {
go func(i int) {
defer spawner.wg.Done()
if spawner.alive[i] {
spawner.instances[i].Close()
spawner.instances[i] = zero
}
}(i)
}
spawner.wg.Wait()
}

File diff suppressed because one or more lines are too long

View file

@ -1,12 +0,0 @@
package sqle
import (
"github.com/feiin/sqlstring"
)
// TODO: This is really unsafe and shouldn't be used at all.
// Format formats the provided query with the given parameters.
func Format(query string, params ...interface{}) string {
return sqlstring.Format(query, params...)
}

View file

@ -1,17 +0,0 @@
package sshx
import "github.com/gliderlabs/ssh"
// ParseAllKeys parses all keys from the list of bytes
func ParseAllKeys(bytes []byte) (keys []ssh.PublicKey) {
var key ssh.PublicKey
var err error
for {
key, _, _, bytes, err = ssh.ParseAuthorizedKey(bytes)
if err != nil {
break
}
keys = append(keys, key)
}
return
}

View file

@ -1,91 +0,0 @@
// Package timex provides Interval and Wait
package timex
import (
"context"
"sync"
"time"
)
var tPool = sync.Pool{
New: func() any {
timer := time.NewTimer(time.Second)
StopTimer(timer)
return timer
},
}
// NewTimer returns an unusued timer from an internal timer pool.
// The timer is guaranteed to be stopped; meaning a call to timer.Reset() should be made before using it.
func NewTimer() *time.Timer {
return tPool.Get().(*time.Timer)
}
// StopTimer stops the given timer and drains the underlying channel.
// This prevents it from firing, until a call to Reset() is made.
//
// If the timer is not running, StopTimer does nothing.
func StopTimer(t *time.Timer) {
t.Stop()
// try to stop
select {
case <-t.C:
default:
}
}
// ReleaseTimer stops t and returns it to the pool of timers.
func ReleaseTimer(t *time.Timer) {
StopTimer(t)
tPool.Put(t)
}
// TickContext is like [time.Tick], but closes the returned channel once the context closes.
// As such it can be recovered by the garbage collector; see [time.TickContext].
//
// Unlike [time.Tick], immediatly send the current time on the given channel.
func TickContext(c context.Context, d time.Duration) <-chan time.Time {
if d < 0 {
return nil
}
ticker := make(chan time.Time, 1)
ticker <- time.Now()
go func() {
defer close(ticker)
timer := NewTimer()
defer ReleaseTimer(timer)
for {
timer.Reset(d)
select {
case tick := <-timer.C:
ticker <- tick
case <-c.Done():
return
}
}
}()
return ticker
}
// TickUntilFunc invokes f every d until either context is closed, or f returns true.
// f is invoked once immediatly when the timer starts.
//
// TickUntilFunc blocks until f is no longer invoked.
//
// Returns the error of the context (if any).
func TickUntilFunc(f func(t time.Time) bool, c context.Context, d time.Duration) error {
context, cancel := context.WithCancel(c)
defer cancel()
for t := range TickContext(context, d) {
if f(t) {
break
}
}
return c.Err()
}

View file

@ -1,88 +0,0 @@
package validator
import (
"errors"
"fmt"
"reflect"
)
const (
validateTag = "validate"
recurseTag = "recurse"
dfltTag = "default"
)
// Validate validates an object of type T, setting defaults where appropriate.
//
// T must be a struct type, when this is not the case, returns ErrNotAStruct.
// validators should contain a set of validators.
//
// Validate iterates over the fields and tags of those fields as follows:
// - If the 'validate' tag is not the empty string, read the appropriate validator from the map, and call the function.
// If the element in the validators map does not exist, returns an error that unwraps to type UnknownValidator.
// If the element in the validators map is not a validator, returns an error that unwraps to type NotAValidator.
// If the type of validator function does not match the field type, returns an error that unwraps to type IncompatibleValidator.
// - If the 'recurse' tag is not the empty string, recurse into the struct type by calling Validate on it.
// If the annotated field is not a struct, return an error.
//
// Any error is wrapped in a FieldError, indicating the field they occured in.
// Recursive validate calls may result in FieldError wraps.
// For a description of struct tags, see [reflect.StructTag].
func Validate[T any](data *T, validators map[string]any) error {
return validate(reflect.ValueOf(data).Elem(), validators)
}
// FieldError wraps an error to indicate which field it occured in.
type FieldError struct {
Field string
Err error
}
func (fe FieldError) Error() string {
return fmt.Sprintf("field %q: %s", fe.Field, fe.Err)
}
func (fe FieldError) Unwrap() error {
return fe.Err
}
var ErrNotAStruct = errors.New("validate called on non-struct type")
func validate(datum reflect.Value, validators Collection) error {
// make sure that we have a struct type
typ := datum.Type()
if typ.Kind() != reflect.Struct {
return ErrNotAStruct
}
fieldC := typ.NumField()
for i := 0; i < fieldC; i++ {
field := typ.Field(i)
// if the recurse tag is set, do the recursion!
if field.Tag.Get(recurseTag) != "" {
if err := validate(datum.FieldByName(field.Name), validators); err != nil {
return FieldError{Field: field.Name, Err: err}
}
continue
}
// check if there is a validator associated with this tag
// and if not, skip it!
validator := field.Tag.Get(validateTag)
if validator == "" {
continue
}
// call the actual validator
if err := validators.Call(
validator,
datum.FieldByName(field.Name),
field.Tag.Get(dfltTag),
); err != nil {
return FieldError{Field: field.Name, Err: err}
}
}
return nil
}

View file

@ -1,123 +0,0 @@
package validator
import (
"errors"
"fmt"
"strconv"
)
func ExampleValidate() {
var value struct {
Number int `validate:"positive" default:"234"`
String string `validate:"nonempty" default:"stuff"`
Recursive struct {
Number int `validate:"positive" default:"45"`
String string `validate:"nonempty" default:"more"`
} `recurse:"true"`
}
collection := make(Collection, 2)
Add(collection, "positive", func(value *int, dflt string) error {
if *value == 0 {
i, err := strconv.ParseInt(dflt, 10, 64)
if err != nil {
return err
}
*value = int(i)
return nil
}
if *value < 0 {
return errors.New("not positive")
}
return nil
})
Add(collection, "nonempty", func(value *string, dflt string) error {
if *value == "" {
*value = dflt
}
if *value == "" {
return errors.New("empty string")
}
return nil
})
err := Validate(&value, collection)
fmt.Printf("%v\n", value)
fmt.Println(err)
// Output: {234 stuff {45 more}}
// <nil>
}
func ExampleValidate_fail() {
var value struct {
Number int `validate:"positive" default:"12"`
String string `validate:"nonempty" default:"stuff"`
Recursive struct {
Number int `validate:"positive" default:"12"`
String string `validate:"nonempty"`
} `recurse:"true"`
}
collection := make(Collection, 2)
Add(collection, "positive", func(value *int, dflt string) error {
if *value == 0 {
i, err := strconv.ParseInt(dflt, 10, 64)
if err != nil {
return err
}
*value = int(i)
return nil
}
if *value < 0 {
return errors.New("not positive")
}
return nil
})
Add(collection, "nonempty", func(value *string, dflt string) error {
if *value == "" {
*value = dflt
}
if *value == "" {
return errors.New("empty string")
}
return nil
})
err := Validate(&value, collection)
fmt.Printf("%v\n", value)
fmt.Println(err)
// Output: {12 stuff {12 }}
// field "Recursive": field "String": empty string
}
func ExampleValidate_notastruct() {
var value int
err := Validate(&value, nil)
fmt.Println(err)
}
func ExampleValidate_notavalidator() {
var value struct {
Field int `validate:"generic"`
}
collection := make(Collection, 2)
collection["generic"] = func(x, y int) error {
panic("never reached")
}
err := Validate(&value, collection)
fmt.Println(err)
// Output: field "Field": entry "generic" in validators is not a valiator
}
func ExampleValidate_invalid() {
var value struct {
Field int `validate:"string"`
}
collection := make(Collection, 2)
collection["string"] = func(value *string, dflt string) error {
panic("never reached")
}
err := Validate(&value, collection)
fmt.Println(err)
// Output: field "Field": validator "string": got type string, expected type int
}

View file

@ -1,131 +0,0 @@
package validator
import (
"fmt"
"reflect"
"strings"
"github.com/tkw1536/goprogram/lib/reflectx"
)
// Collection represents a set of validators.
// The zero value is not ready to use; it should be created using make().
//
// A validator is a non-nil function with signature func(value *F, dflt string) error.
// Here F is the type of a value of a field.
// The value is the initialized value to be validated.
// The validator may perform abitrary normalization on the value.
// dflt is the default value (read from the 'default' tag).
// error should be an appropriate error that occured.
//
// A validator function is applied by calling it.
type Collection map[string]any
// Add adds a Validator to the provided collection of validators.
// Any previously validator of the same name is overwritten.
func Add[F any](coll Collection, name string, validator func(value *F, dflt string) error) {
coll[name] = validator
}
// AddSlice adds a Validator to the provided collection of validators that validates a slice of the given type. The default is seperated by seperator.
func AddSlice[F any](coll Collection, name string, sep string, validator func(value *F, dflt string) error) {
Add(coll, name, func(value *[]F, dflt string) error {
// some value is set, so we do not need to set the default!
if *value != nil {
for i := range *value {
if err := validator(&(*value)[i], ""); err != nil {
return err
}
}
}
// no default provided => set if to an empty slice
if dflt == "" {
*value = make([]F, 0)
return nil
}
// some default provided => iterate over the underlying validator
dflts := strings.Split(dflt, sep)
*value = make([]F, len(dflts))
for i := range *value {
if err := validator(&(*value)[i], dflts[i]); err != nil {
return err
}
}
return nil
})
}
var (
errTyp = reflectx.TypeOf[error]()
strTyp = reflectx.TypeOf[string]()
)
// UnknownValidator is an error returned from Validate if a validator does not exist
type UnknownValidator string
func (uv UnknownValidator) Error() string {
return fmt.Sprintf("unknown validator %q", string(uv))
}
// NotAValidator is an error returned from Validate if an entry in the validators map is not a validator
type NotAValidator string
func (nv NotAValidator) Error() string {
return fmt.Sprintf("entry %q in validators is not a valiator", string(nv))
}
// IncompatibleValidator is returned when a validator in the validators map is incompatible
type IncompatibleValidator struct {
Validator string
GotType reflect.Type
ExpectedType reflect.Type
}
func (iv IncompatibleValidator) Error() string {
return fmt.Sprintf("validator %q: got type %s, expected type %s", iv.Validator, iv.GotType, iv.ExpectedType)
}
// Call calls the validator with the given name, on the given value, and with the provided default.
// See documentation of [Validate] for details.
func (coll Collection) Call(name string, field reflect.Value, dflt string) error {
validator, ok := coll[name]
if !ok {
return UnknownValidator(name)
}
// get the type of the validator
vFunc := reflect.ValueOf(validator)
vTyp := vFunc.Type()
// ensure that vTyp is of type func(*F,string) error
// where T is the type of the field
//
// - the first if assumes checks for some type F
// - the second if checks if the F is the right one
if validator == nil || vTyp.Kind() != reflect.Func || // func
vTyp.NumIn() != 2 || vTyp.In(0).Kind() != reflect.Pointer || vTyp.In(1) != strTyp || // (*F,string)
vTyp.NumOut() != 1 || vTyp.Out(0) != errTyp { // error
return NotAValidator(name)
}
if vTyp.In(0).Elem() != field.Type() { // the correct *F
return IncompatibleValidator{
Validator: name,
GotType: vTyp.In(0).Elem(),
ExpectedType: field.Type(),
}
}
// call the validator function, and return an error
results := vFunc.Call([]reflect.Value{field.Addr(), reflect.ValueOf(dflt)})
// turn the result into an error
// NOTE: We can't just .(error) here because that panic()s on err == nil
err := results[0].Interface()
if err, ok := err.(error); ok {
return err
}
return nil
}