feat(BREAKING): new parser for the TDV
All checks were successful
Tests / test (pull_request) Successful in 18s
REUSE Compliance Check / check (push) Successful in 5s

This commit adds a new parser for the internal time duration flag value
(TimeDurationValue). Previously this used the parser from the time
package from the standard library but this was limited to parsing units
of time up to hours.

The new parser allows users to specify duration in days, hours, minutes,
seconds and a combination of the above. It is quite flexible in the way
users format their string input.

Additonal changes:

- Added unit tests for the command-line parsing of the
  TimeDurationValue type.
- Updated the unit tests for the BoolPtrValue type.
- Updated documentation.

PR: #55
This commit is contained in:
Dan Anglin 2024-08-20 03:32:54 +01:00
parent b558c5adff
commit 0ad02e0af4
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
7 changed files with 213 additions and 34 deletions

View file

@ -19,5 +19,4 @@ jobs:
with:
target: test
env:
ENBAS_TEST_VERBOSE: "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.
- **[Configuration reference](docs/configuration.md)**: The configuration reference documentation.
- **[User manual](docs/manual.md)**: The user manual.
- **[Tips and Tricks](docs/tips_and_tricks.md)**: Additional tips and tricks.
### Licensing

View file

@ -269,16 +269,21 @@ enbas show --type blocked
### Mute an account
```
enbas mute --type account --account-name @name@example.social --mute-notifications --mute-duration="1h"
```
- Mute an account indefinitely.
```
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 |
|------|------|----------|-------------|---------|
| `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. | |
| `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
@ -476,7 +481,7 @@ Creates a new status.
--content "The age-old question: which text editor do you prefer?" \
--add-poll \
--poll-allows-multiple-choices=false \
--poll-expires-in 168h \
--poll-expires-in "7 days" \
--poll-option "emacs" \
--poll-option "vim/neovim" \
--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-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-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

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
import (
"flag"
"slices"
"testing"
@ -10,52 +11,86 @@ import (
func TestBoolPtrValue(t *testing.T) {
tests := []struct {
input string
want bool
want string
}{
{
input: "True",
want: true,
want: "true",
},
{
input: "true",
want: "true",
},
{
input: "1",
want: "true",
},
{
input: "False",
want: "false",
},
{
input: "false",
want: false,
want: "false",
},
{
input: "0",
want: "false",
},
}
value := internalFlag.NewBoolPtrValue()
for _, test := range slices.All(tests) {
if err := value.Set(test.input); err != nil {
t.Fatalf(
"Unable to parse %s as a BoolPtrValue: %v",
test.input,
err,
)
args := []string{"--boolean-value=" + test.input}
t.Run("Flag parsing test: "+test.input, testBoolPtrValueParsing(args, test.want))
}
}
func testBoolPtrValueParsing(args []string, want string) func(t *testing.T) {
return func(t *testing.T) {
flagset := flag.NewFlagSet("test", flag.ExitOnError)
boolVal := internalFlag.NewBoolPtrValue()
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 := *value.Value
got := boolVal.String()
if got != test.want {
if got != want {
t.Errorf(
"Unexpected bool parsed from %s: want %t, got %t",
test.input,
test.want,
"Unexpected boolean value found after parsing BoolPtrValue: want %s, got %s",
want,
got,
)
} else {
t.Logf(
"Expected bool parsed from %s: got %t",
test.input,
"Expected boolean value found after parsing BoolPtrValue: got %s",
got,
)
}
}
}
func TestNilBoolPtrValue(t *testing.T) {
value := internalFlag.NewBoolPtrValue()
func TestNotSetBoolPtrValue(t *testing.T) {
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"
got := value.String()
got := boolVal.String()
if got != want {
t.Errorf("Unexpected string returned from the nil value; want %s, got %s", want, got)

View file

@ -2,16 +2,21 @@ package flag
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
const timeDurationRegexPattern string = `[0-9]{1,4}\s+(days?|hours?|minutes?|seconds?)`
type TimeDurationValue struct {
Duration time.Duration
}
func NewTimeDurationValue() TimeDurationValue {
return TimeDurationValue{
Duration: 0 * time.Second,
Duration: time.Duration(0),
}
}
@ -19,13 +24,56 @@ func (v TimeDurationValue) String() string {
return v.Duration.String()
}
func (v *TimeDurationValue) Set(text string) error {
duration, err := time.ParseDuration(text)
if err != nil {
return fmt.Errorf("unable to parse the value as time duration: %w", err)
func (v *TimeDurationValue) Set(value string) error {
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 {
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
}
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,
)
}
}
}