mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-13 22:36:27 +03:00
Add caddyhttp.Ratio
type
This commit is contained in:
parent
cf69cd7b27
commit
c4b934f232
2 changed files with 154 additions and 1 deletions
|
@ -17,6 +17,7 @@ package caddyhttp
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
@ -164,7 +165,7 @@ func (ws *WeakString) UnmarshalJSON(b []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON marshals was a boolean if true or false,
|
||||
// MarshalJSON marshals as a boolean if true or false,
|
||||
// a number if an integer, or a string otherwise.
|
||||
func (ws WeakString) MarshalJSON() ([]byte, error) {
|
||||
if ws == "true" {
|
||||
|
@ -204,6 +205,82 @@ func (ws WeakString) String() string {
|
|||
return string(ws)
|
||||
}
|
||||
|
||||
// Ratio is a type that unmarshals a valid numerical ratio string.
|
||||
// Valid formats are:
|
||||
// - a/b as a fraction (a / b)
|
||||
// - a:b as a ratio (a / a+b)
|
||||
// - a floating point number
|
||||
type Ratio float64
|
||||
|
||||
// UnmarshalJSON satisfies json.Unmarshaler according to
|
||||
// this type's documentation.
|
||||
func (r *Ratio) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 0 {
|
||||
return io.EOF
|
||||
}
|
||||
if b[0] == byte('"') && b[len(b)-1] == byte('"') {
|
||||
if !strings.Contains(string(b), "/") && !strings.Contains(string(b), ":") {
|
||||
return fmt.Errorf("ratio string '%s' did not contain a slash '/' or colon ':'", string(b[1:len(b)-1]))
|
||||
}
|
||||
if strings.Contains(string(b), "/") {
|
||||
left, right, _ := strings.Cut(string(b[1:len(b)-1]), "/")
|
||||
num, err := strconv.Atoi(left)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing numerator as integer %s: %v", left, err)
|
||||
}
|
||||
denom, err := strconv.Atoi(right)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing denominator as integer %s: %v", right, err)
|
||||
}
|
||||
*r = Ratio(float64(num) / float64(denom))
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(string(b), ":") {
|
||||
left, right, _ := strings.Cut(string(b[1:len(b)-1]), ":")
|
||||
num, err := strconv.Atoi(left)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing numerator as integer %s: %v", left, err)
|
||||
}
|
||||
denom, err := strconv.Atoi(right)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing denominator as integer %s: %v", right, err)
|
||||
}
|
||||
*r = Ratio(float64(num) / (float64(num) + float64(denom)))
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid ratio string '%s'", string(b[1:len(b)-1]))
|
||||
}
|
||||
if bytes.Equal(b, []byte("null")) {
|
||||
return nil
|
||||
}
|
||||
float, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing ratio as float %s: %v", b, err)
|
||||
}
|
||||
*r = Ratio(float)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseRatio(r string) (Ratio, error) {
|
||||
if strings.Contains(r, "/") {
|
||||
left, right, _ := strings.Cut(r, "/")
|
||||
num, err := strconv.Atoi(left)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed parsing numerator as integer %s: %v", left, err)
|
||||
}
|
||||
denom, err := strconv.Atoi(right)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed parsing denominator as integer %s: %v", right, err)
|
||||
}
|
||||
return Ratio(float64(num) / float64(denom)), nil
|
||||
}
|
||||
float, err := strconv.ParseFloat(r, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed parsing ratio as float %s: %v", r, err)
|
||||
}
|
||||
return Ratio(float), nil
|
||||
}
|
||||
|
||||
// StatusCodeMatches returns true if a real HTTP status code matches
|
||||
// the configured status code, which may be either a real HTTP status
|
||||
// code or an integer representing a class of codes (e.g. 4 for all
|
||||
|
|
|
@ -149,3 +149,79 @@ func TestCleanPath(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalRatio(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
input []byte
|
||||
expect float64
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
input: []byte("null"),
|
||||
expect: 0,
|
||||
},
|
||||
{
|
||||
input: []byte(`"1/3"`),
|
||||
expect: float64(1) / float64(3),
|
||||
},
|
||||
{
|
||||
input: []byte(`"1/100"`),
|
||||
expect: float64(1) / float64(100),
|
||||
},
|
||||
{
|
||||
input: []byte(`"3:2"`),
|
||||
expect: 0.6,
|
||||
},
|
||||
{
|
||||
input: []byte(`"99:1"`),
|
||||
expect: 0.99,
|
||||
},
|
||||
{
|
||||
input: []byte(`"1/100"`),
|
||||
expect: float64(1) / float64(100),
|
||||
},
|
||||
{
|
||||
input: []byte(`0.1`),
|
||||
expect: 0.1,
|
||||
},
|
||||
{
|
||||
input: []byte(`0.005`),
|
||||
expect: 0.005,
|
||||
},
|
||||
{
|
||||
input: []byte(`0`),
|
||||
expect: 0,
|
||||
},
|
||||
{
|
||||
input: []byte(`"0"`),
|
||||
errMsg: `ratio string '0' did not contain a slash '/' or colon ':'`,
|
||||
},
|
||||
{
|
||||
input: []byte(`a`),
|
||||
errMsg: `failed parsing ratio as float a: strconv.ParseFloat: parsing "a": invalid syntax`,
|
||||
},
|
||||
{
|
||||
input: []byte(`"a/1"`),
|
||||
errMsg: `failed parsing numerator as integer a: strconv.Atoi: parsing "a": invalid syntax`,
|
||||
},
|
||||
{
|
||||
input: []byte(`"1/a"`),
|
||||
errMsg: `failed parsing denominator as integer a: strconv.Atoi: parsing "a": invalid syntax`,
|
||||
},
|
||||
} {
|
||||
ratio := Ratio(0)
|
||||
err := ratio.UnmarshalJSON(tc.input)
|
||||
if err != nil {
|
||||
if tc.errMsg != "" {
|
||||
if tc.errMsg != err.Error() {
|
||||
t.Fatalf("Test %d: expected error: %v, got: %v", i, tc.errMsg, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
t.Fatalf("Test %d: invalid ratio: %v", i, err)
|
||||
}
|
||||
if ratio != Ratio(tc.expect) {
|
||||
t.Fatalf("Test %d: expected %v, got %v", i, tc.expect, ratio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue