feat(BREAKING): new parser for the time duration value #55

Manually merged
dananglin merged 1 commit from custom-time-duration-parser into main 2024-08-20 03:41:47 +01:00
7 changed files with 213 additions and 34 deletions

View file

@ -19,5 +19,4 @@ jobs:
with: with:
target: test target: test
env: env:
ENBAS_TEST_VERBOSE: "1"
ENBAS_TEST_COVER: "1" ENBAS_TEST_COVER: "1"

View file

@ -24,6 +24,7 @@ the `main` branch mirrored to the following forges:
- **[Getting started guide](docs/getting_started.md)**: A guide to help you get started on using Enbas. - **[Getting started guide](docs/getting_started.md)**: A guide to help you get started on using Enbas.
- **[Configuration reference](docs/configuration.md)**: The configuration reference documentation. - **[Configuration reference](docs/configuration.md)**: The configuration reference documentation.
- **[User manual](docs/manual.md)**: The user manual. - **[User manual](docs/manual.md)**: The user manual.
- **[Tips and Tricks](docs/tips_and_tricks.md)**: Additional tips and tricks.
### Licensing ### Licensing

View file

@ -269,8 +269,13 @@ enbas show --type blocked
### Mute an account ### Mute an account
- Mute an account indefinitely.
``` ```
enbas mute --type account --account-name @name@example.social --mute-notifications --mute-duration="1h" enbas mute --type account --account-name @name@example.social --mute-notifications"
```
- Mute an account for 1 and a half hours.
```
enbas mute --type account --account-name @name@example.social --mute-notifications --mute-duration="1 hour and 30 minutes"
``` ```
| flag | type | required | description | default | | flag | type | required | description | default |
@ -278,7 +283,7 @@ enbas mute --type account --account-name @name@example.social --mute-notificatio
| `type` | string | true | The resource you want to mute.<br>Here this should be `account`. | | | `type` | string | true | The resource you want to mute.<br>Here this should be `account`. | |
| `account-name` | string | true | The name of the account to mute. | | | `account-name` | string | true | The name of the account to mute. | |
| `mute-notifications` | boolean | false | Set to `true` to mute notifications as well as statuses. | false | | `mute-notifications` | boolean | false | Set to `true` to mute notifications as well as statuses. | false |
| `mute-duration` | string | false | Specify how long the account should be muted for.<br>Set to `0s` to mute indefinitely | 0s (indefinitely). | | `mute-duration` | [time duration value](tips_and_tricks.md#the-time-duration-value) | false | Specify how long the account should be muted for.<br>Set to `0 seconds` to mute indefinitely | 0 seconds (indefinitely). |
### Unmute an account ### Unmute an account
@ -476,7 +481,7 @@ Creates a new status.
--content "The age-old question: which text editor do you prefer?" \ --content "The age-old question: which text editor do you prefer?" \
--add-poll \ --add-poll \
--poll-allows-multiple-choices=false \ --poll-allows-multiple-choices=false \
--poll-expires-in 168h \ --poll-expires-in "7 days" \
--poll-option "emacs" \ --poll-option "emacs" \
--poll-option "vim/neovim" \ --poll-option "vim/neovim" \
--poll-option "nano" \ --poll-option "nano" \
@ -528,7 +533,7 @@ Additional flags for polls.
| `poll-allows-multiple-choices` | boolean | false | Set to `true` to allow users to make multiple choices. | false | | `poll-allows-multiple-choices` | boolean | false | Set to `true` to allow users to make multiple choices. | false |
| `poll-hides-vote-counts` | boolean | false | Set to `true` to hide the vote count until the poll is closed. | false | | `poll-hides-vote-counts` | boolean | false | Set to `true` to hide the vote count until the poll is closed. | false |
| `poll-option` | string | true | An option in the poll. Use this flag multiple times to set multiple options. | | | `poll-option` | string | true | An option in the poll. Use this flag multiple times to set multiple options. | |
| `poll-expires-in` | string | false | The duration in which the poll is open for. | | | `poll-expires-in` | [time duration value](tips_and_tricks.md#the-time-duration-value) | false | The duration in which the poll is open for. | |
### Delete a status ### Delete a status

24
docs/tips_and_tricks.md Normal file
View file

@ -0,0 +1,24 @@
# Tips and Tricks
## The time duration value
The time duration value is a custom [flag value](https://pkg.go.dev/flag#Value) that converts a string input into a duration of time.
A typical string input would be in the form of something like `"3 days, 12 hours and 39 minutes"`.
The value can convert units in days, hours, minutes and seconds.
To ensure that your string input is converted correctly there are simple rules to follow.
- The input must be wrapped in quotes.
- Use `day` or `days` to convert the number of days.
- Use `hour` or `hours` to convert the number of hours.
- Use `minute` or `minutes` to convert the number of minutes.
- Use `second` or `seconds` to convert the number of seconds.
- There must be at least one space between the number and the unit of time.<br>
E.g. `"7 days"` is valid, but `"7days"` is invalid.
### Example valid string inputs
- `"3 days"`
- `"6 hours, 45 minutes and 1 second"`
- `"1 day, 15 hours 31 minutes and 12 seconds"`
- `"(7 days) (1 hour) (21 minutes) (35 seconds)"`

View file

@ -1,6 +1,7 @@
package flag_test package flag_test
import ( import (
"flag"
"slices" "slices"
"testing" "testing"
@ -10,52 +11,86 @@ import (
func TestBoolPtrValue(t *testing.T) { func TestBoolPtrValue(t *testing.T) {
tests := []struct { tests := []struct {
input string input string
want bool want string
}{ }{
{ {
input: "True", input: "True",
want: true, want: "true",
},
{
input: "true",
want: "true",
},
{
input: "1",
want: "true",
},
{
input: "False",
want: "false",
}, },
{ {
input: "false", input: "false",
want: false, want: "false",
},
{
input: "0",
want: "false",
}, },
} }
value := internalFlag.NewBoolPtrValue()
for _, test := range slices.All(tests) { for _, test := range slices.All(tests) {
if err := value.Set(test.input); err != nil { args := []string{"--boolean-value=" + test.input}
t.Fatalf(
"Unable to parse %s as a BoolPtrValue: %v", t.Run("Flag parsing test: "+test.input, testBoolPtrValueParsing(args, test.want))
test.input, }
err,
)
} }
got := *value.Value func testBoolPtrValueParsing(args []string, want string) func(t *testing.T) {
return func(t *testing.T) {
flagset := flag.NewFlagSet("test", flag.ExitOnError)
boolVal := internalFlag.NewBoolPtrValue()
if got != test.want { flagset.Var(&boolVal, "boolean-value", "Boolean value")
if err := flagset.Parse(args); err != nil {
t.Fatalf("Received an error parsing the flag: %v", err)
}
got := boolVal.String()
if got != want {
t.Errorf( t.Errorf(
"Unexpected bool parsed from %s: want %t, got %t", "Unexpected boolean value found after parsing BoolPtrValue: want %s, got %s",
test.input, want,
test.want,
got, got,
) )
} else { } else {
t.Logf( t.Logf(
"Expected bool parsed from %s: got %t", "Expected boolean value found after parsing BoolPtrValue: got %s",
test.input,
got, got,
) )
} }
} }
} }
func TestNilBoolPtrValue(t *testing.T) { func TestNotSetBoolPtrValue(t *testing.T) {
value := internalFlag.NewBoolPtrValue() flagset := flag.NewFlagSet("test", flag.ExitOnError)
boolVal := internalFlag.NewBoolPtrValue()
var otherVal string
flagset.Var(&boolVal, "boolean-value", "Boolean value")
flagset.StringVar(&otherVal, "other-value", "", "Another value")
args := []string{"--other-value", "other-value"}
if err := flagset.Parse(args); err != nil {
t.Fatalf("Received an error parsing the flag: %v", err)
}
want := "NOT SET" want := "NOT SET"
got := value.String() got := boolVal.String()
if got != want { if got != want {
t.Errorf("Unexpected string returned from the nil value; want %s, got %s", want, got) t.Errorf("Unexpected string returned from the nil value; want %s, got %s", want, got)

View file

@ -2,16 +2,21 @@ package flag
import ( import (
"fmt" "fmt"
"regexp"
"strconv"
"strings"
"time" "time"
) )
const timeDurationRegexPattern string = `[0-9]{1,4}\s+(days?|hours?|minutes?|seconds?)`
type TimeDurationValue struct { type TimeDurationValue struct {
Duration time.Duration Duration time.Duration
} }
func NewTimeDurationValue() TimeDurationValue { func NewTimeDurationValue() TimeDurationValue {
return TimeDurationValue{ return TimeDurationValue{
Duration: 0 * time.Second, Duration: time.Duration(0),
} }
} }
@ -19,13 +24,56 @@ func (v TimeDurationValue) String() string {
return v.Duration.String() return v.Duration.String()
} }
func (v *TimeDurationValue) Set(text string) error { func (v *TimeDurationValue) Set(value string) error {
duration, err := time.ParseDuration(text) pattern := regexp.MustCompile(timeDurationRegexPattern)
matches := pattern.FindAllString(value, -1)
days, hours, minutes, seconds := 0, 0, 0, 0
var err error
for ind := range len(matches) {
switch {
case strings.Contains(matches[ind], "day"):
days, err = parseInt(matches[ind])
if err != nil { if err != nil {
return fmt.Errorf("unable to parse the value as time duration: %w", err) return fmt.Errorf("unable to parse the number of days from %s: %w", matches[ind], err)
}
case strings.Contains(matches[ind], "hour"):
hours, err = parseInt(matches[ind])
if err != nil {
return fmt.Errorf("unable to parse the number of hours from %s: %w", matches[ind], err)
}
case strings.Contains(matches[ind], "minute"):
minutes, err = parseInt(matches[ind])
if err != nil {
return fmt.Errorf("unable to parse the number of minutes from %s: %w", matches[ind], err)
}
case strings.Contains(matches[ind], "second"):
seconds, err = parseInt(matches[ind])
if err != nil {
return fmt.Errorf("unable to parse the number of seconds from %s: %w", matches[ind], err)
}
}
} }
v.Duration = duration durationValue := (days * 86400) + (hours * 3600) + (minutes * 60) + seconds
v.Duration = time.Duration(durationValue) * time.Second
return nil return nil
} }
func parseInt(text string) (int, error) {
split := strings.SplitN(text, " ", 2)
if len(split) != 2 {
return 0, fmt.Errorf("unexpected number of split for %s: want 2, got %d", text, len(split))
}
output, err := strconv.Atoi(split[0])
if err != nil {
return 0, fmt.Errorf("unable to convert %s to an integer: %w", text, err)
}
return output, nil
}

View file

@ -0,0 +1,67 @@
package flag_test
import (
"flag"
"slices"
"testing"
internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag"
)
func TestTimeDurationValue(t *testing.T) {
parsingTests := []struct {
input string
want string
}{
{
input: `"1 day"`,
want: "24h0m0s",
},
{
input: `"3 days, 5 hours, 39 minutes and 6 seconds"`,
want: "77h39m6s",
},
{
input: `"1 minute and 30 seconds"`,
want: "1m30s",
},
{
input: `"(7 seconds) (21 hours) (41 days)"`,
want: "1005h0m7s",
},
}
for _, test := range slices.All(parsingTests) {
args := []string{"--duration", test.input}
t.Run("Flag parsing test: "+test.input, testTimeDurationValueParsing(args, test.want))
}
}
func testTimeDurationValueParsing(args []string, want string) func(t *testing.T) {
return func(t *testing.T) {
flagset := flag.NewFlagSet("test", flag.ExitOnError)
duration := internalFlag.NewTimeDurationValue()
flagset.Var(&duration, "duration", "Duration value")
if err := flagset.Parse(args); err != nil {
t.Fatalf("Received an error parsing the flag: %v", err)
}
got := duration.String()
if got != want {
t.Errorf(
"Unexpected duration parsed from the flag: want %s, got %s",
want,
got,
)
} else {
t.Logf(
"Expected duration parsed from the flag: got %s",
got,
)
}
}
}