diff --git a/.forgejo/workflows/Tests.yaml b/.forgejo/workflows/Tests.yaml index 07be59d..6da9f3c 100644 --- a/.forgejo/workflows/Tests.yaml +++ b/.forgejo/workflows/Tests.yaml @@ -19,5 +19,4 @@ jobs: with: target: test env: - ENBAS_TEST_VERBOSE: "1" ENBAS_TEST_COVER: "1" diff --git a/README.md b/README.md index 4bf9ea0..fbebdd0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/manual.md b/docs/manual.md index 116ddb7..4562f77 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -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.
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.
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.
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 diff --git a/docs/tips_and_tricks.md b/docs/tips_and_tricks.md new file mode 100644 index 0000000..afc430a --- /dev/null +++ b/docs/tips_and_tricks.md @@ -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.
+ 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)"` diff --git a/internal/flag/boolptrvalue_test.go b/internal/flag/boolptrvalue_test.go index d136f55..30bef18 100644 --- a/internal/flag/boolptrvalue_test.go +++ b/internal/flag/boolptrvalue_test.go @@ -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) diff --git a/internal/flag/timedurationvalue.go b/internal/flag/timedurationvalue.go index cdf02ac..6524372 100644 --- a/internal/flag/timedurationvalue.go +++ b/internal/flag/timedurationvalue.go @@ -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 +} diff --git a/internal/flag/timedurationvalue_test.go b/internal/flag/timedurationvalue_test.go new file mode 100644 index 0000000..95103b8 --- /dev/null +++ b/internal/flag/timedurationvalue_test.go @@ -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, + ) + } + } +}