1137 lines
37 KiB
HTML
1137 lines
37 KiB
HTML
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
<title>path: Go Coverage Report</title>
|
|
<style>
|
|
body {
|
|
background: black;
|
|
color: rgb(80, 80, 80);
|
|
}
|
|
body, pre, #legend span {
|
|
font-family: Menlo, monospace;
|
|
font-weight: bold;
|
|
}
|
|
#topbar {
|
|
background: black;
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0;
|
|
height: 42px;
|
|
border-bottom: 1px solid rgb(80, 80, 80);
|
|
}
|
|
#content {
|
|
margin-top: 50px;
|
|
}
|
|
#nav, #legend {
|
|
float: left;
|
|
margin-left: 10px;
|
|
}
|
|
#legend {
|
|
margin-top: 12px;
|
|
}
|
|
#nav {
|
|
margin-top: 10px;
|
|
}
|
|
#legend span {
|
|
margin: 0 5px;
|
|
}
|
|
.cov0 { color: rgb(192, 0, 0) }
|
|
.cov1 { color: rgb(128, 128, 128) }
|
|
.cov2 { color: rgb(116, 140, 131) }
|
|
.cov3 { color: rgb(104, 152, 134) }
|
|
.cov4 { color: rgb(92, 164, 137) }
|
|
.cov5 { color: rgb(80, 176, 140) }
|
|
.cov6 { color: rgb(68, 188, 143) }
|
|
.cov7 { color: rgb(56, 200, 146) }
|
|
.cov8 { color: rgb(44, 212, 149) }
|
|
.cov9 { color: rgb(32, 224, 152) }
|
|
.cov10 { color: rgb(20, 236, 155) }
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="topbar">
|
|
<div id="nav">
|
|
<select id="files">
|
|
|
|
<option value="file0">github.com/gopatchy/path/equal.go (100.0%)</option>
|
|
|
|
<option value="file1">github.com/gopatchy/path/greater.go (92.9%)</option>
|
|
|
|
<option value="file2">github.com/gopatchy/path/greaterequal.go (93.3%)</option>
|
|
|
|
<option value="file3">github.com/gopatchy/path/hasprefix.go (80.0%)</option>
|
|
|
|
<option value="file4">github.com/gopatchy/path/in.go (100.0%)</option>
|
|
|
|
<option value="file5">github.com/gopatchy/path/less.go (92.9%)</option>
|
|
|
|
<option value="file6">github.com/gopatchy/path/lessequal.go (93.3%)</option>
|
|
|
|
<option value="file7">github.com/gopatchy/path/merge.go (84.2%)</option>
|
|
|
|
<option value="file8">github.com/gopatchy/path/op.go (82.1%)</option>
|
|
|
|
<option value="file9">github.com/gopatchy/path/parse.go (94.6%)</option>
|
|
|
|
<option value="file10">github.com/gopatchy/path/path.go (87.0%)</option>
|
|
|
|
<option value="file11">github.com/gopatchy/path/slice.go (100.0%)</option>
|
|
|
|
<option value="file12">github.com/gopatchy/path/sort.go (73.5%)</option>
|
|
|
|
</select>
|
|
</div>
|
|
<div id="legend">
|
|
<span>not tracked</span>
|
|
|
|
<span class="cov0">no coverage</span>
|
|
<span class="cov1">low coverage</span>
|
|
<span class="cov2">*</span>
|
|
<span class="cov3">*</span>
|
|
<span class="cov4">*</span>
|
|
<span class="cov5">*</span>
|
|
<span class="cov6">*</span>
|
|
<span class="cov7">*</span>
|
|
<span class="cov8">*</span>
|
|
<span class="cov9">*</span>
|
|
<span class="cov10">high coverage</span>
|
|
|
|
</div>
|
|
</div>
|
|
<div id="content">
|
|
|
|
<pre class="file" id="file0" style="display: none">package path
|
|
|
|
import "time"
|
|
|
|
func Equal(obj any, path string, matchStr string) (bool, error) <span class="cov7" title="55">{
|
|
return op(obj, path, matchStr, equal)
|
|
}</span>
|
|
|
|
func equal(obj, match any, _ string) bool <span class="cov10" title="203">{
|
|
switch objt := obj.(type) </span>{
|
|
case time.Time:<span class="cov6" title="28">
|
|
tm := match.(*timeVal)
|
|
|
|
// TODO: Replace Truncate() with a timezone-aware version
|
|
return tm.time.Equal(objt.Truncate(tm.precision))</span>
|
|
|
|
default:<span class="cov9" title="175">
|
|
return obj == match</span>
|
|
}
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file1" style="display: none">package path
|
|
|
|
import (
|
|
"time"
|
|
|
|
"cloud.google.com/go/civil"
|
|
)
|
|
|
|
func Greater(obj any, path string, matchStr string) (bool, error) <span class="cov8" title="40">{
|
|
return op(obj, path, matchStr, greater)
|
|
}</span>
|
|
|
|
func greater(obj, match any, _ string) bool <span class="cov10" title="64">{
|
|
switch objt := obj.(type) </span>{
|
|
case int:<span class="cov5" title="8">
|
|
return objt > match.(int)</span>
|
|
|
|
case int64:<span class="cov5" title="8">
|
|
return objt > match.(int64)</span>
|
|
|
|
case uint:<span class="cov5" title="8">
|
|
return objt > match.(uint)</span>
|
|
|
|
case uint64:<span class="cov5" title="8">
|
|
return objt > match.(uint64)</span>
|
|
|
|
case float32:<span class="cov4" title="5">
|
|
return objt > match.(float32)</span>
|
|
|
|
case float64:<span class="cov4" title="5">
|
|
return objt > match.(float64)</span>
|
|
|
|
case string:<span class="cov4" title="5">
|
|
return objt > match.(string)</span>
|
|
|
|
case bool:<span class="cov4" title="5">
|
|
return objt && !match.(bool)</span>
|
|
|
|
case time.Time:<span class="cov4" title="6">
|
|
tm := match.(*timeVal)
|
|
|
|
return objt.Truncate(tm.precision).After(tm.time)</span>
|
|
|
|
case civil.Date:<span class="cov4" title="6">
|
|
return objt.After(match.(civil.Date))</span>
|
|
|
|
default:<span class="cov0" title="0">
|
|
panic(obj)</span>
|
|
}
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file2" style="display: none">package path
|
|
|
|
import (
|
|
"time"
|
|
|
|
"cloud.google.com/go/civil"
|
|
)
|
|
|
|
func GreaterEqual(obj any, path string, matchStr string) (bool, error) <span class="cov9" title="60">{
|
|
return op(obj, path, matchStr, greaterEqual)
|
|
}</span>
|
|
|
|
func greaterEqual(obj, match any, _ string) bool <span class="cov10" title="94">{
|
|
switch objt := obj.(type) </span>{
|
|
case int:<span class="cov5" title="12">
|
|
return objt >= match.(int)</span>
|
|
|
|
case int64:<span class="cov5" title="12">
|
|
return objt >= match.(int64)</span>
|
|
|
|
case uint:<span class="cov5" title="12">
|
|
return objt >= match.(uint)</span>
|
|
|
|
case uint64:<span class="cov5" title="12">
|
|
return objt >= match.(uint64)</span>
|
|
|
|
case float32:<span class="cov4" title="7">
|
|
return objt >= match.(float32)</span>
|
|
|
|
case float64:<span class="cov4" title="7">
|
|
return objt >= match.(float64)</span>
|
|
|
|
case string:<span class="cov4" title="7">
|
|
return objt >= match.(string)</span>
|
|
|
|
case bool:<span class="cov4" title="7">
|
|
return objt || objt == match.(bool)</span>
|
|
|
|
case time.Time:<span class="cov5" title="9">
|
|
tm := match.(*timeVal)
|
|
trunc := objt.Truncate(tm.precision)
|
|
|
|
return trunc.Equal(tm.time) || trunc.After(tm.time)</span>
|
|
|
|
case civil.Date:<span class="cov5" title="9">
|
|
return objt == match.(civil.Date) || objt.After(match.(civil.Date))</span>
|
|
|
|
default:<span class="cov0" title="0">
|
|
panic(obj)</span>
|
|
}
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file3" style="display: none">package path
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"cloud.google.com/go/civil"
|
|
)
|
|
|
|
func HasPrefix(obj any, path string, matchStr string) (bool, error) <span class="cov9" title="18">{
|
|
return op(obj, path, matchStr, hasPrefix)
|
|
}</span>
|
|
|
|
func hasPrefix(obj, match any, matchStr string) bool <span class="cov10" title="19">{
|
|
var objStr string
|
|
|
|
switch objt := obj.(type) </span>{
|
|
case int:<span class="cov3" title="2">
|
|
objStr = strconv.FormatInt(int64(objt), 10)</span>
|
|
|
|
case int64:<span class="cov3" title="2">
|
|
objStr = strconv.FormatInt(objt, 10)</span>
|
|
|
|
case uint:<span class="cov3" title="2">
|
|
objStr = strconv.FormatUint(uint64(objt), 10)</span>
|
|
|
|
case uint64:<span class="cov3" title="2">
|
|
objStr = strconv.FormatUint(objt, 10)</span>
|
|
|
|
case float32:<span class="cov3" title="2">
|
|
objStr = strconv.FormatFloat(float64(objt), 'f', -1, 32)</span>
|
|
|
|
case float64:<span class="cov3" title="2">
|
|
objStr = strconv.FormatFloat(objt, 'f', -1, 64)</span>
|
|
|
|
case string:<span class="cov5" title="5">
|
|
objStr = objt</span>
|
|
|
|
case bool:<span class="cov3" title="2">
|
|
objStr = strconv.FormatBool(objt)</span>
|
|
|
|
case time.Time:<span class="cov0" title="0">
|
|
objStr = objt.String()</span>
|
|
|
|
case civil.Date:<span class="cov0" title="0">
|
|
objStr = objt.String()</span>
|
|
|
|
default:<span class="cov0" title="0">
|
|
panic(obj)</span>
|
|
}
|
|
|
|
<span class="cov10" title="19">return strings.HasPrefix(objStr, matchStr)</span>
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file4" style="display: none">package path
|
|
|
|
func In(obj any, path string, matchStr string) (bool, error) <span class="cov10" title="40">{
|
|
return opList(obj, path, matchStr, equal)
|
|
}</span>
|
|
</pre>
|
|
|
|
<pre class="file" id="file5" style="display: none">package path
|
|
|
|
import (
|
|
"time"
|
|
|
|
"cloud.google.com/go/civil"
|
|
)
|
|
|
|
func Less(obj any, path string, matchStr string) (bool, error) <span class="cov9" title="40">{
|
|
return op(obj, path, matchStr, less)
|
|
}</span>
|
|
|
|
func less(obj, match any, _ string) bool <span class="cov10" title="58">{
|
|
switch objt := obj.(type) </span>{
|
|
case int:<span class="cov4" title="6">
|
|
return objt < match.(int)</span>
|
|
|
|
case int64:<span class="cov4" title="6">
|
|
return objt < match.(int64)</span>
|
|
|
|
case uint:<span class="cov4" title="6">
|
|
return objt < match.(uint)</span>
|
|
|
|
case uint64:<span class="cov4" title="6">
|
|
return objt < match.(uint64)</span>
|
|
|
|
case float32:<span class="cov4" title="6">
|
|
return objt < match.(float32)</span>
|
|
|
|
case float64:<span class="cov4" title="6">
|
|
return objt < match.(float64)</span>
|
|
|
|
case string:<span class="cov4" title="6">
|
|
return objt < match.(string)</span>
|
|
|
|
case bool:<span class="cov4" title="6">
|
|
return !objt && match.(bool)</span>
|
|
|
|
case time.Time:<span class="cov4" title="5">
|
|
tm := match.(*timeVal)
|
|
|
|
return objt.Truncate(tm.precision).Before(tm.time)</span>
|
|
|
|
case civil.Date:<span class="cov4" title="5">
|
|
return objt.Before(match.(civil.Date))</span>
|
|
|
|
default:<span class="cov0" title="0">
|
|
panic(obj)</span>
|
|
}
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file6" style="display: none">package path
|
|
|
|
import (
|
|
"time"
|
|
|
|
"cloud.google.com/go/civil"
|
|
)
|
|
|
|
func LessEqual(obj any, path string, matchStr string) (bool, error) <span class="cov9" title="60">{
|
|
return op(obj, path, matchStr, lessEqual)
|
|
}</span>
|
|
|
|
func lessEqual(obj, match any, _ string) bool <span class="cov10" title="81">{
|
|
switch objt := obj.(type) </span>{
|
|
case int:<span class="cov5" title="8">
|
|
return objt <= match.(int)</span>
|
|
|
|
case int64:<span class="cov5" title="8">
|
|
return objt <= match.(int64)</span>
|
|
|
|
case uint:<span class="cov5" title="8">
|
|
return objt <= match.(uint)</span>
|
|
|
|
case uint64:<span class="cov5" title="8">
|
|
return objt <= match.(uint64)</span>
|
|
|
|
case float32:<span class="cov5" title="9">
|
|
return objt <= match.(float32)</span>
|
|
|
|
case float64:<span class="cov5" title="9">
|
|
return objt <= match.(float64)</span>
|
|
|
|
case string:<span class="cov5" title="9">
|
|
return objt <= match.(string)</span>
|
|
|
|
case bool:<span class="cov5" title="8">
|
|
return !objt || objt == match.(bool)</span>
|
|
|
|
case time.Time:<span class="cov4" title="7">
|
|
tm := match.(*timeVal)
|
|
trunc := objt.Truncate(tm.precision)
|
|
|
|
return trunc.Equal(tm.time) || trunc.Before(tm.time)</span>
|
|
|
|
case civil.Date:<span class="cov4" title="7">
|
|
return objt == match.(civil.Date) || objt.Before(match.(civil.Date))</span>
|
|
|
|
default:<span class="cov0" title="0">
|
|
panic(obj)</span>
|
|
}
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file7" style="display: none">package path
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
|
|
"github.com/gopatchy/jsrest"
|
|
)
|
|
|
|
func Merge(to, from any) <span class="cov4" title="4">{
|
|
MergeValue(reflect.ValueOf(to), reflect.ValueOf(from))
|
|
}</span>
|
|
|
|
func MergeValue(to, from reflect.Value) <span class="cov5" title="6">{
|
|
to = reflect.Indirect(to)
|
|
from = reflect.Indirect(from)
|
|
|
|
for i := 0; i < to.NumField(); i++ </span><span class="cov10" title="28">{
|
|
toField := to.Field(i)
|
|
fromField := from.Field(i)
|
|
|
|
if fromField.IsZero() </span><span class="cov9" title="22">{
|
|
continue</span>
|
|
}
|
|
|
|
<span class="cov5" title="6">if reflect.Indirect(fromField).Kind() == reflect.Struct </span><span class="cov2" title="2">{
|
|
MergeValue(toField, fromField)
|
|
continue</span>
|
|
}
|
|
|
|
<span class="cov4" title="4">toField.Set(fromField)</span>
|
|
}
|
|
}
|
|
|
|
func MergeMap(to any, from map[string]any) error <span class="cov1" title="1">{
|
|
m, err := ToMap(to)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return jsrest.Errorf(jsrest.ErrInternalServerError, "converting to map failed (%w)", err)
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">MergeMaps(m, from)
|
|
|
|
return FromMap(to, m)</span>
|
|
}
|
|
|
|
func MergeMaps(to map[string]any, from map[string]any) <span class="cov2" title="2">{
|
|
for k, v := range from </span><span class="cov4" title="4">{
|
|
if vMap, isMap := v.(map[string]any); isMap </span><span class="cov1" title="1">{
|
|
if _, ok := to[k].(map[string]any); !ok </span><span class="cov0" title="0">{
|
|
// Either key doesn't exist or it's a different type
|
|
// If different type, error will happen during json decode
|
|
to[k] = map[string]any{}
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">MergeMaps(to[k].(map[string]any), vMap)</span>
|
|
} else<span class="cov3" title="3"> {
|
|
to[k] = v
|
|
}</span>
|
|
}
|
|
}
|
|
|
|
func ToMap(from any) (map[string]any, error) <span class="cov1" title="1">{
|
|
js, err := json.Marshal(from)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, jsrest.Errorf(jsrest.ErrInternalServerError, "json marshal failed (%w)", err)
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">ret := map[string]any{}
|
|
|
|
err = json.Unmarshal(js, &ret)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, jsrest.Errorf(jsrest.ErrInternalServerError, "json unmarshal failed (%w)", err)
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">return ret, nil</span>
|
|
}
|
|
|
|
func FromMap(to any, from map[string]any) error <span class="cov1" title="1">{
|
|
js, err := json.Marshal(from)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return jsrest.Errorf(jsrest.ErrInternalServerError, "json marshal failed (%w)", err)
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">err = json.Unmarshal(js, to)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return jsrest.Errorf(jsrest.ErrInternalServerError, "json unmarshal failed (%w)", err)
|
|
}</span>
|
|
|
|
<span class="cov1" title="1">return nil</span>
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file8" style="display: none">package path
|
|
|
|
import (
|
|
"strings"
|
|
)
|
|
|
|
func op(obj any, path string, matchStr string, cb func(any, any, string) bool) (bool, error) <span class="cov10" title="273">{
|
|
objVal, err := Get(obj, path)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return false, err
|
|
}</span>
|
|
|
|
<span class="cov10" title="273">matchVal, err := parse(matchStr, objVal)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return false, err
|
|
}</span>
|
|
|
|
<span class="cov10" title="273">if isSlice(objVal) </span><span class="cov8" title="124">{
|
|
return anyTrue(objVal, func(x any, _ int) bool </span><span class="cov9" title="245">{ return cb(x, matchVal, matchStr) }</span>), nil
|
|
}
|
|
|
|
<span class="cov9" title="149">return cb(objVal, matchVal, matchStr), nil</span>
|
|
}
|
|
|
|
func opList(obj any, path string, matchStr string, cb func(any, any, string) bool) (bool, error) <span class="cov6" title="40">{
|
|
objVal, err := Get(obj, path)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return false, err
|
|
}</span>
|
|
|
|
<span class="cov6" title="40">if objVal == nil </span><span class="cov0" title="0">{
|
|
return false, nil
|
|
}</span>
|
|
|
|
<span class="cov6" title="40">matchVal := []any{}
|
|
matchParts := strings.Split(matchStr, ",")
|
|
|
|
for _, matchPart := range matchParts </span><span class="cov8" title="98">{
|
|
matchTmp, err := parse(matchPart, objVal)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return false, err
|
|
}</span>
|
|
|
|
<span class="cov8" title="98">matchVal = append(matchVal, matchTmp)</span>
|
|
}
|
|
|
|
<span class="cov6" title="40">return anyTrue(matchVal, func(y any, i int) bool </span><span class="cov7" title="78">{
|
|
str := matchParts[i]
|
|
|
|
if isSlice(objVal) </span><span class="cov6" title="39">{
|
|
return anyTrue(objVal, func(x any, _ int) bool </span><span class="cov8" title="86">{ return cb(x, y, str) }</span>)
|
|
}
|
|
|
|
<span class="cov6" title="39">return cb(objVal, y, str)</span>
|
|
}), nil
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file9" style="display: none">package path
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"cloud.google.com/go/civil"
|
|
"github.com/gopatchy/jsrest"
|
|
)
|
|
|
|
type timeVal struct {
|
|
time time.Time
|
|
precision time.Duration
|
|
}
|
|
|
|
var (
|
|
ErrUnsupportedType = errors.New("unsupported type")
|
|
ErrUnknownTimeFormat = errors.New("unknown time format")
|
|
)
|
|
|
|
func parse(str string, t any) (any, error) <span class="cov10" title="380">{
|
|
typ := reflect.TypeOf(t)
|
|
|
|
if typ.Kind() == reflect.Slice </span><span class="cov8" title="173">{
|
|
typ = typ.Elem()
|
|
}</span>
|
|
|
|
<span class="cov10" title="380">if typ.Kind() == reflect.Pointer </span><span class="cov2" title="2">{
|
|
typ = typ.Elem()
|
|
}</span>
|
|
|
|
// TODO: Consider attempting to convert to string in default case
|
|
<span class="cov10" title="380">switch typ.Kind() </span>{ //nolint:exhaustive
|
|
case reflect.Int:<span class="cov6" title="40">
|
|
return parseInt(str)</span>
|
|
|
|
case reflect.Int64:<span class="cov6" title="37">
|
|
return strconv.ParseInt(str, 10, 64)</span>
|
|
|
|
case reflect.Uint:<span class="cov6" title="36">
|
|
return parseUint(str)</span>
|
|
|
|
case reflect.Uint64:<span class="cov6" title="36">
|
|
return strconv.ParseUint(str, 10, 64)</span>
|
|
|
|
case reflect.Float32:<span class="cov6" title="36">
|
|
return parseFloat32(str)</span>
|
|
|
|
case reflect.Float64:<span class="cov6" title="36">
|
|
return strconv.ParseFloat(str, 64)</span>
|
|
|
|
case reflect.String:<span class="cov6" title="39">
|
|
return str, nil</span>
|
|
|
|
case reflect.Bool:<span class="cov6" title="38">
|
|
return strconv.ParseBool(str)</span>
|
|
|
|
case reflect.Struct:<span class="cov7" title="82">
|
|
switch typ </span>{
|
|
case reflect.TypeOf(time.Time{}):<span class="cov6" title="48">
|
|
return parseTime(str)</span>
|
|
|
|
case reflect.TypeOf(civil.Date{}):<span class="cov6" title="34">
|
|
return civil.ParseDate(str)</span>
|
|
}
|
|
}
|
|
|
|
<span class="cov0" title="0">return nil, jsrest.Errorf(jsrest.ErrBadRequest, "%T (%w)", t, ErrUnsupportedType)</span>
|
|
}
|
|
|
|
func parseInt(str string) (int, error) <span class="cov6" title="40">{
|
|
val, err := strconv.ParseInt(str, 10, strconv.IntSize)
|
|
|
|
return int(val), err
|
|
}</span>
|
|
|
|
func parseUint(str string) (uint, error) <span class="cov6" title="36">{
|
|
val, err := strconv.ParseUint(str, 10, strconv.IntSize)
|
|
|
|
return uint(val), err
|
|
}</span>
|
|
|
|
func parseFloat32(str string) (float32, error) <span class="cov6" title="36">{
|
|
val, err := strconv.ParseFloat(str, 32)
|
|
|
|
return float32(val), err
|
|
}</span>
|
|
|
|
type timeFormat struct {
|
|
format string
|
|
precision time.Duration
|
|
}
|
|
|
|
var timeFormats = []timeFormat{
|
|
{
|
|
format: "2006-01-02-07:00",
|
|
precision: 24 * time.Hour,
|
|
// TODO: Support field annotation to change start vs end of day
|
|
// TODO: Support timezone context passed down to allow naked date
|
|
},
|
|
{
|
|
format: "2006-01-02T15:04:05Z",
|
|
precision: 1 * time.Second,
|
|
},
|
|
{
|
|
format: "2006-01-02T15:04:05-07:00",
|
|
precision: 1 * time.Second,
|
|
},
|
|
}
|
|
|
|
func parseTime(str string) (*timeVal, error) <span class="cov6" title="48">{
|
|
if strings.ToLower(str) == "now" </span><span class="cov1" title="1">{
|
|
return &timeVal{
|
|
time: time.Now(),
|
|
precision: 1 * time.Nanosecond,
|
|
}, nil
|
|
}</span>
|
|
|
|
<span class="cov6" title="47">for _, format := range timeFormats </span><span class="cov8" title="103">{
|
|
tm, err := time.Parse(format.format, str)
|
|
if err != nil </span><span class="cov7" title="63">{
|
|
continue</span>
|
|
}
|
|
|
|
<span class="cov6" title="40">return &timeVal{
|
|
time: tm,
|
|
precision: format.precision,
|
|
}, nil</span>
|
|
}
|
|
|
|
<span class="cov3" title="7">i, err := strconv.ParseInt(str, 10, 64)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", str, ErrUnknownTimeFormat)
|
|
}</span>
|
|
|
|
// UNIX Seconds: 2969-05-03
|
|
// UNIX Millis: 1971-01-01
|
|
// Intended to give us a wide range of useful values in both schemes
|
|
<span class="cov3" title="7">if i > 31536000000 </span><span class="cov3" title="4">{
|
|
return &timeVal{
|
|
time: time.UnixMilli(i),
|
|
precision: 1 * time.Millisecond,
|
|
}, nil
|
|
}</span>
|
|
|
|
<span class="cov2" title="3">return &timeVal{
|
|
time: time.Unix(i, 0),
|
|
precision: 1 * time.Second,
|
|
}, nil</span>
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file10" style="display: none">package path
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"cloud.google.com/go/civil"
|
|
"github.com/gopatchy/jsrest"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
type WalkCallback func(string, []string, reflect.StructField)
|
|
|
|
var (
|
|
TimeTimeType = reflect.TypeOf(time.Time{})
|
|
CivilDateType = reflect.TypeOf(civil.Date{})
|
|
|
|
ErrNotAStruct = errors.New("not a struct")
|
|
ErrUnknownFieldName = errors.New("unknown field name")
|
|
)
|
|
|
|
func Get(obj any, path string) (any, error) <span class="cov6" title="382">{
|
|
v, err := GetValue(reflect.ValueOf(obj), path)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return nil, err
|
|
}</span>
|
|
|
|
<span class="cov6" title="382">return v.Interface(), nil</span>
|
|
}
|
|
|
|
func GetValue(v reflect.Value, path string) (reflect.Value, error) <span class="cov6" title="382">{
|
|
parts := strings.Split(path, ".")
|
|
return getRecursive(v, parts, []string{})
|
|
}</span>
|
|
|
|
func getRecursive(v reflect.Value, parts []string, prev []string) (reflect.Value, error) <span class="cov7" title="771">{
|
|
if v.Kind() == reflect.Pointer </span><span class="cov6" title="388">{
|
|
if v.IsNil() </span><span class="cov1" title="2">{
|
|
v = reflect.Zero(v.Type().Elem())
|
|
}</span> else<span class="cov6" title="386"> {
|
|
v = reflect.Indirect(v)
|
|
}</span>
|
|
}
|
|
|
|
<span class="cov7" title="771">if len(parts) == 0 </span><span class="cov6" title="382">{
|
|
return v, nil
|
|
}</span>
|
|
|
|
<span class="cov6" title="389">if v.Kind() != reflect.Struct </span><span class="cov0" title="0">{
|
|
return reflect.Value{}, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", strings.Join(prev, "."), ErrNotAStruct)
|
|
}</span>
|
|
|
|
<span class="cov6" title="389">part := parts[0]
|
|
|
|
sub, found := getField(v, part)
|
|
if !found </span><span class="cov0" title="0">{
|
|
return reflect.Value{}, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", errorPath(prev, part), ErrUnknownFieldName)
|
|
}</span>
|
|
|
|
<span class="cov6" title="389">newPrev := []string{}
|
|
newPrev = append(newPrev, prev...)
|
|
newPrev = append(newPrev, part)
|
|
|
|
return getRecursive(sub, parts[1:], newPrev)</span>
|
|
}
|
|
|
|
func getField(v reflect.Value, name string) (reflect.Value, bool) <span class="cov6" title="398">{
|
|
field, found := getStructField(v.Type(), name)
|
|
if !found </span><span class="cov0" title="0">{
|
|
return reflect.Value{}, false
|
|
}</span>
|
|
|
|
<span class="cov6" title="398">return v.FieldByName(field.Name), true</span>
|
|
}
|
|
|
|
func Set(obj any, path string, val string) error <span class="cov2" title="6">{
|
|
return SetValue(reflect.ValueOf(obj), path, val)
|
|
}</span>
|
|
|
|
func SetValue(v reflect.Value, path string, val string) error <span class="cov2" title="6">{
|
|
parts := strings.Split(path, ".")
|
|
return setRecursive(v, parts, []string{}, val)
|
|
}</span>
|
|
|
|
func setRecursive(v reflect.Value, parts []string, prev []string, val string) error <span class="cov3" title="15">{
|
|
if v.Kind() == reflect.Pointer </span><span class="cov3" title="9">{
|
|
if v.IsNil() </span><span class="cov1" title="2">{
|
|
v.Set(reflect.New(v.Type().Elem()))
|
|
}</span>
|
|
|
|
<span class="cov3" title="9">v = reflect.Indirect(v)</span>
|
|
}
|
|
|
|
<span class="cov3" title="15">if len(parts) == 0 </span><span class="cov2" title="6">{
|
|
n, err := parse(val, v.Interface())
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
return err
|
|
}</span>
|
|
|
|
<span class="cov2" title="6">if _, ok := n.(*timeVal); ok </span><span class="cov1" title="1">{
|
|
n = n.(*timeVal).time
|
|
}</span>
|
|
|
|
<span class="cov2" title="6">v.Set(reflect.ValueOf(n))
|
|
|
|
return nil</span>
|
|
}
|
|
|
|
<span class="cov3" title="9">if v.Kind() != reflect.Struct </span><span class="cov0" title="0">{
|
|
return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", strings.Join(prev, "."), ErrNotAStruct)
|
|
}</span>
|
|
|
|
<span class="cov3" title="9">part := parts[0]
|
|
|
|
sub, found := getField(v, part)
|
|
if !found </span><span class="cov0" title="0">{
|
|
return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", errorPath(prev, part), ErrUnknownFieldName)
|
|
}</span>
|
|
|
|
<span class="cov3" title="9">newPrev := []string{}
|
|
newPrev = append(newPrev, prev...)
|
|
newPrev = append(newPrev, part)
|
|
|
|
return setRecursive(sub, parts[1:], newPrev, val)</span>
|
|
}
|
|
|
|
func List(obj any) []string <span class="cov1" title="1">{
|
|
return ListType(reflect.TypeOf(obj))
|
|
}</span>
|
|
|
|
func ListType(t reflect.Type) []string <span class="cov1" title="1">{
|
|
list := []string{}
|
|
|
|
WalkType(t, func(path string, _ []string, field reflect.StructField) </span><span class="cov2" title="7">{
|
|
t := MaybeIndirectType(field.Type)
|
|
if t.Kind() == reflect.Struct && t != TimeTimeType && t != CivilDateType </span><span class="cov1" title="1">{
|
|
return
|
|
}</span>
|
|
|
|
<span class="cov2" title="6">list = append(list, path)</span>
|
|
})
|
|
|
|
<span class="cov1" title="1">sort.Strings(list)
|
|
|
|
return list</span>
|
|
}
|
|
|
|
func GetFieldType(t reflect.Type, path string) reflect.Type <span class="cov1" title="2">{
|
|
parts := strings.Split(path, ".")
|
|
|
|
for _, part := range parts </span><span class="cov2" title="3">{
|
|
field, found := getStructField(MaybeIndirectType(t), part)
|
|
if !found </span><span class="cov0" title="0">{
|
|
return nil
|
|
}</span>
|
|
|
|
<span class="cov2" title="3">t = field.Type</span>
|
|
}
|
|
|
|
<span class="cov1" title="2">return t</span>
|
|
}
|
|
|
|
func FindTagValueType(t reflect.Type, key, value string) (string, bool) <span class="cov1" title="2">{
|
|
ret := ""
|
|
|
|
WalkType(t, func(path string, _ []string, field reflect.StructField) </span><span class="cov3" title="14">{
|
|
tag, found := field.Tag.Lookup(key)
|
|
if !found </span><span class="cov3" title="10">{
|
|
return
|
|
}</span>
|
|
|
|
<span class="cov2" title="4">parts := strings.Split(tag, ",")
|
|
|
|
if slices.Contains(parts, value) </span><span class="cov1" title="2">{
|
|
ret = path
|
|
}</span>
|
|
})
|
|
|
|
<span class="cov1" title="2">return ret, ret != ""</span>
|
|
}
|
|
|
|
func Walk(obj any, cb WalkCallback) <span class="cov0" title="0">{
|
|
WalkType(reflect.TypeOf(obj), cb)
|
|
}</span>
|
|
|
|
func WalkType(t reflect.Type, cb WalkCallback) <span class="cov2" title="3">{
|
|
walkRecursive(MaybeIndirectType(t), cb, []string{})
|
|
}</span>
|
|
|
|
func walkRecursive(t reflect.Type, cb WalkCallback, prev []string) <span class="cov3" title="9">{
|
|
for i := 0; i < t.NumField(); i++ </span><span class="cov4" title="24">{
|
|
sub := t.Field(i)
|
|
|
|
newPrev := []string{}
|
|
newPrev = append(newPrev, prev...)
|
|
|
|
if !sub.Anonymous </span><span class="cov4" title="21">{
|
|
newPrev = append(newPrev, FieldName(sub))
|
|
}</span>
|
|
|
|
<span class="cov4" title="24">t := MaybeIndirectType(sub.Type)
|
|
|
|
if len(newPrev) > 0 </span><span class="cov4" title="21">{
|
|
cb(strings.Join(newPrev, "."), newPrev, sub)
|
|
}</span>
|
|
|
|
<span class="cov4" title="24">if t.Kind() == reflect.Struct && t != TimeTimeType && t != CivilDateType </span><span class="cov2" title="6">{
|
|
walkRecursive(t, cb, newPrev)
|
|
}</span>
|
|
}
|
|
}
|
|
|
|
func getStructField(t reflect.Type, name string) (reflect.StructField, bool) <span class="cov6" title="401">{
|
|
name = strings.ToLower(name)
|
|
|
|
return t.FieldByNameFunc(func(iterName string) bool </span><span class="cov9" title="8955">{
|
|
iterField, iterOK := t.FieldByName(iterName)
|
|
if !iterOK </span><span class="cov0" title="0">{
|
|
panic(iterName)</span>
|
|
}
|
|
|
|
<span class="cov9" title="8955">return strings.ToLower(FieldName(iterField)) == name</span>
|
|
})
|
|
}
|
|
|
|
func errorPath(prev []string, part string) string <span class="cov0" title="0">{
|
|
if len(prev) == 0 </span><span class="cov0" title="0">{
|
|
return part
|
|
}</span>
|
|
|
|
<span class="cov0" title="0">return fmt.Sprintf("%s.%s", strings.Join(prev, "."), part)</span>
|
|
}
|
|
|
|
func FieldName(field reflect.StructField) string <span class="cov10" title="8976">{
|
|
tag := field.Tag.Get("json")
|
|
if tag != "" </span><span class="cov7" title="797">{
|
|
if tag == "-" </span><span class="cov0" title="0">{
|
|
return ""
|
|
}</span>
|
|
|
|
<span class="cov7" title="797">parts := strings.SplitN(tag, ",", 2)
|
|
|
|
return parts[0]</span>
|
|
}
|
|
|
|
<span class="cov9" title="8179">return field.Name</span>
|
|
}
|
|
|
|
func MaybeIndirectType(t reflect.Type) reflect.Type <span class="cov4" title="37">{
|
|
if t.Kind() == reflect.Pointer </span><span class="cov3" title="10">{
|
|
return t.Elem()
|
|
}</span>
|
|
|
|
<span class="cov4" title="27">return t</span>
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file11" style="display: none">package path
|
|
|
|
import "reflect"
|
|
|
|
func isSlice(v any) bool <span class="cov9" title="351">{
|
|
return reflect.TypeOf(v).Kind() == reflect.Slice
|
|
}</span>
|
|
|
|
func anyTrue(v any, cb func(any, int) bool) bool <span class="cov8" title="203">{
|
|
val := reflect.ValueOf(v)
|
|
|
|
for i := 0; i < val.Len(); i++ </span><span class="cov10" title="410">{
|
|
sub := val.Index(i)
|
|
|
|
if sub.Kind() == reflect.Pointer && sub.IsNil() </span><span class="cov1" title="1">{
|
|
continue</span>
|
|
}
|
|
|
|
<span class="cov9" title="409">sub = reflect.Indirect(sub)
|
|
|
|
if cb(sub.Interface(), i) </span><span class="cov7" title="102">{
|
|
return true
|
|
}</span>
|
|
}
|
|
|
|
<span class="cov7" title="101">return false</span>
|
|
}
|
|
</pre>
|
|
|
|
<pre class="file" id="file12" style="display: none">package path
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"sort"
|
|
"time"
|
|
|
|
"cloud.google.com/go/civil"
|
|
"github.com/gopatchy/jsrest"
|
|
)
|
|
|
|
func Sort(objs any, path string) error <span class="cov7" title="11">{
|
|
as := newAnySlice(objs, path)
|
|
sort.Stable(as)
|
|
|
|
return as.err
|
|
}</span>
|
|
|
|
func SortReverse(objs any, path string) error <span class="cov1" title="1">{
|
|
as := newAnySlice(objs, path)
|
|
sort.Stable(sort.Reverse(as))
|
|
|
|
return as.err
|
|
}</span>
|
|
|
|
type anySlice struct {
|
|
path string
|
|
slice reflect.Value
|
|
swapper func(i, j int)
|
|
err error
|
|
}
|
|
|
|
var ErrUnsupportedSortType = errors.New("unsupported _sort type")
|
|
|
|
func newAnySlice(objs any, path string) *anySlice <span class="cov7" title="12">{
|
|
return &anySlice{
|
|
path: path,
|
|
slice: reflect.ValueOf(objs),
|
|
swapper: reflect.Swapper(objs),
|
|
}
|
|
}</span>
|
|
|
|
func (as *anySlice) Len() int <span class="cov7" title="12">{
|
|
return as.slice.Len()
|
|
}</span>
|
|
|
|
func (as *anySlice) Less(i, j int) bool <span class="cov10" title="34">{
|
|
v1, err := Get(as.slice.Index(i).Interface(), as.path)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
as.err = err
|
|
// We have to obey the Less() contract even in error cases
|
|
return i < j
|
|
}</span>
|
|
|
|
<span class="cov10" title="34">v2, err := Get(as.slice.Index(j).Interface(), as.path)
|
|
if err != nil </span><span class="cov0" title="0">{
|
|
as.err = err
|
|
return i < j
|
|
}</span>
|
|
|
|
<span class="cov10" title="34">switch </span>{
|
|
case v1 == nil && v2 == nil:<span class="cov0" title="0">
|
|
return false</span>
|
|
case v1 == nil:<span class="cov0" title="0">
|
|
return true</span>
|
|
case v2 == nil:<span class="cov0" title="0">
|
|
return false</span>
|
|
}
|
|
|
|
<span class="cov10" title="34">switch t1 := v1.(type) </span>{
|
|
case int:<span class="cov6" title="8">
|
|
return t1 < v2.(int)</span>
|
|
|
|
case int64:<span class="cov3" title="3">
|
|
return t1 < v2.(int64)</span>
|
|
|
|
case uint:<span class="cov3" title="3">
|
|
return t1 < v2.(uint)</span>
|
|
|
|
case uint64:<span class="cov3" title="3">
|
|
return t1 < v2.(uint64)</span>
|
|
|
|
case float32:<span class="cov3" title="3">
|
|
return t1 < v2.(float32)</span>
|
|
|
|
case float64:<span class="cov3" title="3">
|
|
return t1 < v2.(float64)</span>
|
|
|
|
case string:<span class="cov3" title="3">
|
|
return t1 < v2.(string)</span>
|
|
|
|
case bool:<span class="cov2" title="2">
|
|
return !t1 && v2.(bool)</span>
|
|
|
|
case time.Time:<span class="cov3" title="3">
|
|
return t1.Before(v2.(time.Time))</span>
|
|
|
|
case civil.Date:<span class="cov3" title="3">
|
|
return t1.Before(v2.(civil.Date))</span>
|
|
|
|
default:<span class="cov0" title="0">
|
|
as.err = jsrest.Errorf(jsrest.ErrBadRequest, "%s: %T (%w)", as.path, t1, ErrUnsupportedSortType)
|
|
return i < j</span>
|
|
}
|
|
}
|
|
|
|
func (as *anySlice) Swap(i, j int) <span class="cov8" title="21">{
|
|
as.swapper(i, j)
|
|
}</span>
|
|
</pre>
|
|
|
|
</div>
|
|
</body>
|
|
<script>
|
|
(function() {
|
|
var files = document.getElementById('files');
|
|
var visible;
|
|
files.addEventListener('change', onChange, false);
|
|
function select(part) {
|
|
if (visible)
|
|
visible.style.display = 'none';
|
|
visible = document.getElementById(part);
|
|
if (!visible)
|
|
return;
|
|
files.value = part;
|
|
visible.style.display = 'block';
|
|
location.hash = part;
|
|
}
|
|
function onChange() {
|
|
select(files.value);
|
|
window.scrollTo(0, 0);
|
|
}
|
|
if (location.hash != "") {
|
|
select(location.hash.substr(1));
|
|
}
|
|
if (!visible) {
|
|
select("file0");
|
|
}
|
|
})();
|
|
</script>
|
|
</html>
|