describe time value standards, validate time flags (#1066)
## Description Adds validation of cli inputs for time-based flags. All time values that can be parsed by the time handling in common count as supported, even though official support includes a smaller set. Also adds some clean-up to time.go and adds a design document describing standard time value formats, and the maintenance thereof, in corso. ## Type of change - [x] 🌻 Feature - [x] 🗺️ Documentation - [x] 🤖 Test ## Issue(s) * #943 * #924 ## Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
cffb00c44b
commit
89668ed164
25
design/time_standards.md
Normal file
25
design/time_standards.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Corso Standard Time Format
|
||||||
|
|
||||||
|
Since Corso primarily deals with with historical point-in-time events (created-at, delivered-at, started-on, etc), as opposed to scheduled future events, we can safely represent all time with point-in-time, timezoned values.
|
||||||
|
|
||||||
|
The standard string format uses [iso-8601](https://en.wikipedia.org/wiki/ISO_8601) date & time + [rfc-3339](https://datatracker.ietf.org/doc/html/rfc3339) compliant formats, with milliseconds. All time values should be stored in the UTC timezone.
|
||||||
|
|
||||||
|
ex:
|
||||||
|
* `2022-07-11T20:07:59.00000Z`
|
||||||
|
* `2022-07-11T20:07:59Z`
|
||||||
|
|
||||||
|
In golang implementation, use the time package format `time.RFC3339Nano`.
|
||||||
|
|
||||||
|
## Deviation From the Standard
|
||||||
|
|
||||||
|
The above standard helps ensure clean transformations when serializing to and from time.Time structs and strings. In certain cases time values will need to be displayed in a non-standard format. These variations are acceptable as long as the application has no intent to consume that format again in a future process.
|
||||||
|
|
||||||
|
Examples: the date-time suffix in restoration destination root folders or the human-readable format displayed in CLI outputs.
|
||||||
|
|
||||||
|
## Input Leniency
|
||||||
|
|
||||||
|
End users are not required to utilize the standard time format when calling inputs. In practice, all formats recognized in `/internal/common/time.go` can be used as a valid input value. Official format support is detailed in the public Corso Documentation.
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
All supported time formats should appear as consts in `/internal/common/time.go`. Usage of `time.Parse()` and `time.Format()` should be kept to that package.
|
||||||
@ -382,22 +382,6 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := utils.ValidateExchangeRestoreFlags(backupID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
|
|
||||||
if err != nil {
|
|
||||||
return Only(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := repository.Connect(ctx, acct, s, options.Control())
|
|
||||||
if err != nil {
|
|
||||||
return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider))
|
|
||||||
}
|
|
||||||
|
|
||||||
defer utils.CloseRepo(ctx, r)
|
|
||||||
|
|
||||||
opts := utils.ExchangeOpts{
|
opts := utils.ExchangeOpts{
|
||||||
Contacts: contact,
|
Contacts: contact,
|
||||||
ContactFolders: contactFolder,
|
ContactFolders: contactFolder,
|
||||||
@ -418,6 +402,22 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
EventSubject: eventSubject,
|
EventSubject: eventSubject,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := utils.ValidateExchangeRestoreFlags(backupID, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return Only(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := repository.Connect(ctx, acct, s, options.Control())
|
||||||
|
if err != nil {
|
||||||
|
return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer utils.CloseRepo(ctx, r)
|
||||||
|
|
||||||
ds, err := runDetailsExchangeCmd(ctx, r, backupID, opts)
|
ds, err := runDetailsExchangeCmd(ctx, r, backupID, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(ctx, err)
|
return Only(ctx, err)
|
||||||
|
|||||||
@ -176,22 +176,6 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := utils.ValidateExchangeRestoreFlags(backupID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s, a, err := config.GetStorageAndAccount(ctx, true, nil)
|
|
||||||
if err != nil {
|
|
||||||
return Only(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := repository.Connect(ctx, a, s, options.Control())
|
|
||||||
if err != nil {
|
|
||||||
return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider))
|
|
||||||
}
|
|
||||||
|
|
||||||
defer utils.CloseRepo(ctx, r)
|
|
||||||
|
|
||||||
opts := utils.ExchangeOpts{
|
opts := utils.ExchangeOpts{
|
||||||
Contacts: contact,
|
Contacts: contact,
|
||||||
ContactFolders: contactFolder,
|
ContactFolders: contactFolder,
|
||||||
@ -212,6 +196,22 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
EventSubject: eventSubject,
|
EventSubject: eventSubject,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := utils.ValidateExchangeRestoreFlags(backupID, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s, a, err := config.GetStorageAndAccount(ctx, true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return Only(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := repository.Connect(ctx, a, s, options.Control())
|
||||||
|
if err != nil {
|
||||||
|
return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer utils.CloseRepo(ctx, r)
|
||||||
|
|
||||||
sel := selectors.NewExchangeRestore()
|
sel := selectors.NewExchangeRestore()
|
||||||
utils.IncludeExchangeRestoreDataSelectors(sel, opts)
|
utils.IncludeExchangeRestoreDataSelectors(sel, opts)
|
||||||
utils.FilterExchangeRestoreInfoSelectors(sel, opts)
|
utils.FilterExchangeRestoreInfoSelectors(sel, opts)
|
||||||
@ -221,7 +221,7 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
sel.Include(sel.Users(selectors.Any()))
|
sel.Include(sel.Users(selectors.Any()))
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreDest := control.DefaultRestoreDestination(common.SimpleDateTimeFormat)
|
restoreDest := control.DefaultRestoreDestination(common.SimpleDateTime)
|
||||||
|
|
||||||
ro, err := r.NewRestore(ctx, backupID, sel.Selector, restoreDest)
|
ro, err := r.NewRestore(ctx, backupID, sel.Selector, restoreDest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -140,3 +140,35 @@ func (suite *RestoreExchangeIntegrationSuite) TestExchangeRestoreCmd() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *RestoreExchangeIntegrationSuite) TestExchangeRestoreCmd_badTimeFlags() {
|
||||||
|
for _, set := range backupDataSets {
|
||||||
|
if set == contacts {
|
||||||
|
suite.T().Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.T().Run(set.String(), func(t *testing.T) {
|
||||||
|
ctx := config.SetViper(tester.NewContext(), suite.vpr)
|
||||||
|
ctx, _ = logger.SeedLevel(ctx, logger.Development)
|
||||||
|
defer logger.Flush(ctx)
|
||||||
|
|
||||||
|
var timeFilter string
|
||||||
|
switch set {
|
||||||
|
case email:
|
||||||
|
timeFilter = "--email-received-after"
|
||||||
|
case events:
|
||||||
|
timeFilter = "--event-starts-after"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := tester.StubRootCmd(
|
||||||
|
"restore", "exchange",
|
||||||
|
"--config-file", suite.cfgFP,
|
||||||
|
"--backup", string(suite.backupOps[set].Results.BackupID),
|
||||||
|
timeFilter, "smarf")
|
||||||
|
cli.BuildCommandTree(cmd)
|
||||||
|
|
||||||
|
// run the command
|
||||||
|
require.Error(t, cmd.ExecuteContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -123,7 +123,17 @@ func restoreOneDriveCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := utils.ValidateOneDriveRestoreFlags(backupID); err != nil {
|
opts := utils.OneDriveOpts{
|
||||||
|
Users: user,
|
||||||
|
Paths: folderPaths,
|
||||||
|
Names: fileNames,
|
||||||
|
CreatedAfter: fileCreatedAfter,
|
||||||
|
CreatedBefore: fileCreatedBefore,
|
||||||
|
ModifiedAfter: fileModifiedAfter,
|
||||||
|
ModifiedBefore: fileModifiedBefore,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.ValidateOneDriveRestoreFlags(backupID, opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,16 +149,6 @@ func restoreOneDriveCmd(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
defer utils.CloseRepo(ctx, r)
|
defer utils.CloseRepo(ctx, r)
|
||||||
|
|
||||||
opts := utils.OneDriveOpts{
|
|
||||||
Users: user,
|
|
||||||
Paths: folderPaths,
|
|
||||||
Names: fileNames,
|
|
||||||
CreatedAfter: fileCreatedAfter,
|
|
||||||
CreatedBefore: fileCreatedBefore,
|
|
||||||
ModifiedAfter: fileModifiedAfter,
|
|
||||||
ModifiedBefore: fileModifiedBefore,
|
|
||||||
}
|
|
||||||
|
|
||||||
sel := selectors.NewOneDriveRestore()
|
sel := selectors.NewOneDriveRestore()
|
||||||
utils.IncludeOneDriveRestoreDataSelectors(sel, opts)
|
utils.IncludeOneDriveRestoreDataSelectors(sel, opts)
|
||||||
utils.FilterOneDriveRestoreInfoSelectors(sel, opts)
|
utils.FilterOneDriveRestoreInfoSelectors(sel, opts)
|
||||||
@ -158,7 +158,7 @@ func restoreOneDriveCmd(cmd *cobra.Command, args []string) error {
|
|||||||
sel.Include(sel.Users(selectors.Any()))
|
sel.Include(sel.Users(selectors.Any()))
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreDest := control.DefaultRestoreDestination(common.SimpleDateTimeFormatOneDrive)
|
restoreDest := control.DefaultRestoreDestination(common.SimpleDateTimeOneDrive)
|
||||||
|
|
||||||
ro, err := r.NewRestore(ctx, backupID, sel.Selector, restoreDest)
|
ro, err := r.NewRestore(ctx, backupID, sel.Selector, restoreDest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -72,11 +72,27 @@ func AddExchangeFilter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateExchangeRestoreFlags checks common flags for correctness and interdependencies
|
// ValidateExchangeRestoreFlags checks common flags for correctness and interdependencies
|
||||||
func ValidateExchangeRestoreFlags(backupID string) error {
|
func ValidateExchangeRestoreFlags(backupID string, opts ExchangeOpts) error {
|
||||||
if len(backupID) == 0 {
|
if len(backupID) == 0 {
|
||||||
return errors.New("a backup ID is required")
|
return errors.New("a backup ID is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !IsValidTimeFormat(opts.EmailReceivedAfter) {
|
||||||
|
return errors.New("invalid time format for email-received-after")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidTimeFormat(opts.EmailReceivedBefore) {
|
||||||
|
return errors.New("invalid time format for email-received-before")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidTimeFormat(opts.EventStartsAfter) {
|
||||||
|
return errors.New("invalid time format for event-starts-after")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidTimeFormat(opts.EventStartsBefore) {
|
||||||
|
return errors.New("invalid time format for event-starts-before")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/cli/utils"
|
"github.com/alcionai/corso/src/cli/utils"
|
||||||
|
"github.com/alcionai/corso/src/internal/common"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,21 +23,35 @@ func (suite *ExchangeUtilsSuite) TestValidateBackupDetailFlags() {
|
|||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
backupID string
|
backupID string
|
||||||
|
opts utils.ExchangeOpts
|
||||||
expect assert.ErrorAssertionFunc
|
expect assert.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "with backupid",
|
name: "with backupid",
|
||||||
backupID: "bid",
|
backupID: "bid",
|
||||||
|
opts: utils.ExchangeOpts{},
|
||||||
expect: assert.NoError,
|
expect: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no backupid",
|
name: "no backupid",
|
||||||
|
opts: utils.ExchangeOpts{},
|
||||||
|
expect: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid time",
|
||||||
|
backupID: "bid",
|
||||||
|
opts: utils.ExchangeOpts{EmailReceivedAfter: common.Now()},
|
||||||
|
expect: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid time",
|
||||||
|
opts: utils.ExchangeOpts{EmailReceivedAfter: "fnords"},
|
||||||
expect: assert.Error,
|
expect: assert.Error,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.T().Run(test.name, func(t *testing.T) {
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
test.expect(t, utils.ValidateExchangeRestoreFlags(test.backupID))
|
test.expect(t, utils.ValidateExchangeRestoreFlags(test.backupID, test.opts))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,11 +17,27 @@ type OneDriveOpts struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateOneDriveRestoreFlags checks common flags for correctness and interdependencies
|
// ValidateOneDriveRestoreFlags checks common flags for correctness and interdependencies
|
||||||
func ValidateOneDriveRestoreFlags(backupID string) error {
|
func ValidateOneDriveRestoreFlags(backupID string, opts OneDriveOpts) error {
|
||||||
if len(backupID) == 0 {
|
if len(backupID) == 0 {
|
||||||
return errors.New("a backup ID is required")
|
return errors.New("a backup ID is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !IsValidTimeFormat(opts.CreatedAfter) {
|
||||||
|
return errors.New("invalid time format for created-after")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidTimeFormat(opts.CreatedBefore) {
|
||||||
|
return errors.New("invalid time format for created-before")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidTimeFormat(opts.ModifiedAfter) {
|
||||||
|
return errors.New("invalid time format for modified-after")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidTimeFormat(opts.ModifiedBefore) {
|
||||||
|
return errors.New("invalid time format for modified-before")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common"
|
||||||
"github.com/alcionai/corso/src/pkg/repository"
|
"github.com/alcionai/corso/src/pkg/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,3 +58,20 @@ func AddCommand(parent, c *cobra.Command) (*cobra.Command, *pflag.FlagSet) {
|
|||||||
|
|
||||||
return c, c.Flags()
|
return c, c.Flags()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValidTimeFormat returns true if the input is regonized as a
|
||||||
|
// supported format by the common time parser. Returns true if
|
||||||
|
// the input is zero valued, which indicates that the flag was not
|
||||||
|
// called.
|
||||||
|
func IsValidTimeFormat(in string) bool {
|
||||||
|
if len(in) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := common.ParseTime(in)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@ -7,26 +7,47 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TimeFormat string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// the clipped format occurs when m365 removes the :00 second suffix
|
// StandardTime is the canonical format used for all data storage in corso
|
||||||
ClippedSimpleTimeFormat = "02-Jan-2006_15:04"
|
StandardTime TimeFormat = time.RFC3339Nano
|
||||||
ClippedSimpleTimeFormatOneDrive = "02-Jan-2006_15-04"
|
|
||||||
LegacyTimeFormat = time.RFC3339
|
// DateOnly is accepted by the CLI as a valid input for timestamp-based
|
||||||
SimpleDateTimeFormat = "02-Jan-2006_15:04:05"
|
// filters. Time and timezone are assumed to be 00:00:00 and UTC.
|
||||||
// SimpleDateTimeFormatOneDrive is similar to `SimpleDateTimeFormat`
|
DateOnly TimeFormat = "2006-01-02"
|
||||||
// but uses `-` instead of `:` which is a reserved character in
|
|
||||||
// OneDrive
|
// TabularOutput is used when displaying time values to the user in
|
||||||
SimpleDateTimeFormatOneDrive = "02-Jan-2006_15-04-05"
|
// non-json cli outputs.
|
||||||
StandardTimeFormat = time.RFC3339Nano
|
TabularOutput TimeFormat = "2006-01-02T15:04:05Z"
|
||||||
TabularOutputTimeFormat = "2006-01-02T15:04:05Z"
|
|
||||||
// Format used for test restore destination folders. Microsecond granularity.
|
// LegacyTime is used in /exchange/service_restore to comply with certain
|
||||||
SimpleDateTimeFormatTests = SimpleDateTimeFormatOneDrive + ".000000"
|
// graphAPI time format requirements.
|
||||||
|
LegacyTime TimeFormat = time.RFC3339
|
||||||
|
|
||||||
|
// SimpleDateTime is the default value appended to the root restoration folder name.
|
||||||
|
SimpleDateTime TimeFormat = "02-Jan-2006_15:04:05"
|
||||||
|
// SimpleDateTimeOneDrive modifies SimpleDateTimeFormat to comply with onedrive folder
|
||||||
|
// restrictions: primarily swapping `-` instead of `:` which is a reserved character.
|
||||||
|
SimpleDateTimeOneDrive TimeFormat = "02-Jan-2006_15-04-05"
|
||||||
|
|
||||||
|
// m365 will remove the :00 second suffix on folder names, resulting in the following formats.
|
||||||
|
ClippedSimple TimeFormat = "02-Jan-2006_15:04"
|
||||||
|
ClippedSimpleOneDrive TimeFormat = "02-Jan-2006_15-04"
|
||||||
|
|
||||||
|
// SimpleTimeTesting is used for testing restore destination folders.
|
||||||
|
// Microsecond granularity prevents collisions in parallel package or workflow runs.
|
||||||
|
SimpleTimeTesting TimeFormat = SimpleDateTimeOneDrive + ".000000"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// these regexes are used to extract time formats from strings. Their primary purpose is to
|
||||||
|
// identify the folders produced in external data during automated testing. For safety, each
|
||||||
|
// time format described above should have a matching regexp.
|
||||||
var (
|
var (
|
||||||
clippedSimpleTimeRE = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}:\d{2}).*`)
|
clippedSimpleRE = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}:\d{2}).*`)
|
||||||
clippedSimpleTimeOneDriveRE = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}-\d{2}).*`)
|
clippedSimpleOneDriveRE = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}-\d{2}).*`)
|
||||||
legacyTimeRE = regexp.MustCompile(
|
dateOnlyRE = regexp.MustCompile(`.*(\d{4}-\d{2}-\d{2}).*`)
|
||||||
|
legacyTimeRE = regexp.MustCompile(
|
||||||
`.*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?([Zz]|[a-zA-Z]{2}|([\+|\-]([01]\d|2[0-3])))).*`)
|
`.*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?([Zz]|[a-zA-Z]{2}|([\+|\-]([01]\d|2[0-3])))).*`)
|
||||||
simpleDateTimeRE = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}:\d{2}:\d{2}).*`)
|
simpleDateTimeRE = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}:\d{2}:\d{2}).*`)
|
||||||
simpleDateTimeOneDriveRE = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}-\d{2}-\d{2}).*`)
|
simpleDateTimeOneDriveRE = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}-\d{2}-\d{2}).*`)
|
||||||
@ -36,15 +57,17 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// clipped formats must appear last, else they take priority over the regular Simple format.
|
// shortened formats (clipped*, DateOnly) must follow behind longer formats, otherwise they'll
|
||||||
formats = []string{
|
// get eagerly chosen as the parsable format, slicing out some data.
|
||||||
StandardTimeFormat,
|
formats = []TimeFormat{
|
||||||
SimpleDateTimeFormat,
|
StandardTime,
|
||||||
SimpleDateTimeFormatOneDrive,
|
SimpleDateTime,
|
||||||
LegacyTimeFormat,
|
SimpleDateTimeOneDrive,
|
||||||
TabularOutputTimeFormat,
|
LegacyTime,
|
||||||
ClippedSimpleTimeFormat,
|
TabularOutput,
|
||||||
ClippedSimpleTimeFormatOneDrive,
|
ClippedSimple,
|
||||||
|
ClippedSimpleOneDrive,
|
||||||
|
DateOnly,
|
||||||
}
|
}
|
||||||
regexes = []*regexp.Regexp{
|
regexes = []*regexp.Regexp{
|
||||||
standardTimeRE,
|
standardTimeRE,
|
||||||
@ -52,47 +75,53 @@ var (
|
|||||||
simpleDateTimeOneDriveRE,
|
simpleDateTimeOneDriveRE,
|
||||||
legacyTimeRE,
|
legacyTimeRE,
|
||||||
tabularOutputTimeRE,
|
tabularOutputTimeRE,
|
||||||
clippedSimpleTimeRE,
|
clippedSimpleRE,
|
||||||
clippedSimpleTimeOneDriveRE,
|
clippedSimpleOneDriveRE,
|
||||||
|
dateOnlyRE,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNoTimeString = errors.New("no substring contains a known time format")
|
var ErrNoTimeString = errors.New("no substring contains a known time format")
|
||||||
|
|
||||||
|
// Now produces the current time as a string in the standard format.
|
||||||
|
func Now() string {
|
||||||
|
return FormatNow(StandardTime)
|
||||||
|
}
|
||||||
|
|
||||||
// FormatNow produces the current time in UTC using the provided
|
// FormatNow produces the current time in UTC using the provided
|
||||||
// time format.
|
// time format.
|
||||||
func FormatNow(fmt string) string {
|
func FormatNow(fmt TimeFormat) string {
|
||||||
return time.Now().UTC().Format(fmt)
|
return FormatTimeWith(time.Now(), fmt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatTimeWith produces the a datetime with the given format.
|
// FormatTimeWith produces the a datetime with the given format.
|
||||||
func FormatTimeWith(t time.Time, fmt string) string {
|
func FormatTimeWith(t time.Time, fmt TimeFormat) string {
|
||||||
return t.UTC().Format(fmt)
|
return t.UTC().Format(string(fmt))
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatTime produces the standard format for corso time values.
|
// FormatTime produces the standard format for corso time values.
|
||||||
// Always formats into the UTC timezone.
|
// Always formats into the UTC timezone.
|
||||||
func FormatTime(t time.Time) string {
|
func FormatTime(t time.Time) string {
|
||||||
return FormatTimeWith(t, StandardTimeFormat)
|
return FormatTimeWith(t, StandardTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatSimpleDateTime produces a simple datetime of the format
|
// FormatSimpleDateTime produces a simple datetime of the format
|
||||||
// "02-Jan-2006_15:04:05"
|
// "02-Jan-2006_15:04:05"
|
||||||
func FormatSimpleDateTime(t time.Time) string {
|
func FormatSimpleDateTime(t time.Time) string {
|
||||||
return FormatTimeWith(t, SimpleDateTimeFormat)
|
return FormatTimeWith(t, SimpleDateTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatTabularDisplayTime produces the standard format for displaying
|
// FormatTabularDisplayTime produces the standard format for displaying
|
||||||
// a timestamp as part of user-readable cli output.
|
// a timestamp as part of user-readable cli output.
|
||||||
// "2016-01-02T15:04:05Z"
|
// "2016-01-02T15:04:05Z"
|
||||||
func FormatTabularDisplayTime(t time.Time) string {
|
func FormatTabularDisplayTime(t time.Time) string {
|
||||||
return FormatTimeWith(t, TabularOutputTimeFormat)
|
return FormatTimeWith(t, TabularOutput)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatLegacyTime produces standard format for string values
|
// FormatLegacyTime produces standard format for string values
|
||||||
// that are placed in SingleValueExtendedProperty tags
|
// that are placed in SingleValueExtendedProperty tags
|
||||||
func FormatLegacyTime(t time.Time) string {
|
func FormatLegacyTime(t time.Time) string {
|
||||||
return FormatTimeWith(t, LegacyTimeFormat)
|
return FormatTimeWith(t, LegacyTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseTime makes a best attempt to produce a time value from
|
// ParseTime makes a best attempt to produce a time value from
|
||||||
@ -103,7 +132,7 @@ func ParseTime(s string) (time.Time, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, form := range formats {
|
for _, form := range formats {
|
||||||
t, err := time.Parse(form, s)
|
t, err := time.Parse(string(form), s)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return t.UTC(), nil
|
return t.UTC(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ func (suite *CommonTimeUnitSuite) TestFormatTabularDisplayTime() {
|
|||||||
t := suite.T()
|
t := suite.T()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
result := common.FormatTabularDisplayTime(now)
|
result := common.FormatTabularDisplayTime(now)
|
||||||
assert.Equal(t, now.UTC().Format(common.TabularOutputTimeFormat), result)
|
assert.Equal(t, now.UTC().Format(string(common.TabularOutput)), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *CommonTimeUnitSuite) TestParseTime() {
|
func (suite *CommonTimeUnitSuite) TestParseTime() {
|
||||||
@ -57,11 +57,11 @@ func (suite *CommonTimeUnitSuite) TestParseTime() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *CommonTimeUnitSuite) TestExtractTime() {
|
func (suite *CommonTimeUnitSuite) TestExtractTime() {
|
||||||
comparable := func(t *testing.T, tt time.Time, clippedFormat string) time.Time {
|
comparable := func(t *testing.T, tt time.Time, shortFormat common.TimeFormat) time.Time {
|
||||||
ts := common.FormatLegacyTime(tt.UTC())
|
ts := common.FormatLegacyTime(tt.UTC())
|
||||||
|
|
||||||
if len(clippedFormat) > 0 {
|
if len(shortFormat) > 0 {
|
||||||
ts = tt.UTC().Format(clippedFormat)
|
ts = tt.UTC().Format(string(shortFormat))
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := common.ParseTime(ts)
|
c, err := common.ParseTime(ts)
|
||||||
@ -91,15 +91,16 @@ func (suite *CommonTimeUnitSuite) TestExtractTime() {
|
|||||||
|
|
||||||
type timeFormatter func(time.Time) string
|
type timeFormatter func(time.Time) string
|
||||||
|
|
||||||
formats := []string{
|
formats := []common.TimeFormat{
|
||||||
common.ClippedSimpleTimeFormat,
|
common.ClippedSimple,
|
||||||
common.ClippedSimpleTimeFormatOneDrive,
|
common.ClippedSimpleOneDrive,
|
||||||
common.LegacyTimeFormat,
|
common.LegacyTime,
|
||||||
common.SimpleDateTimeFormat,
|
common.SimpleDateTime,
|
||||||
common.SimpleDateTimeFormatOneDrive,
|
common.SimpleDateTimeOneDrive,
|
||||||
common.StandardTimeFormat,
|
common.StandardTime,
|
||||||
common.TabularOutputTimeFormat,
|
common.TabularOutput,
|
||||||
common.SimpleDateTimeFormatTests,
|
common.SimpleTimeTesting,
|
||||||
|
common.DateOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
type presuf struct {
|
type presuf struct {
|
||||||
@ -116,7 +117,7 @@ func (suite *CommonTimeUnitSuite) TestExtractTime() {
|
|||||||
|
|
||||||
type testable struct {
|
type testable struct {
|
||||||
input string
|
input string
|
||||||
clippedFormat string
|
clippedFormat common.TimeFormat
|
||||||
expect time.Time
|
expect time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,10 +126,12 @@ func (suite *CommonTimeUnitSuite) TestExtractTime() {
|
|||||||
// test matrix: for each input, in each format, with each prefix/suffix, run the test.
|
// test matrix: for each input, in each format, with each prefix/suffix, run the test.
|
||||||
for _, in := range inputs {
|
for _, in := range inputs {
|
||||||
for _, f := range formats {
|
for _, f := range formats {
|
||||||
clippedFormat := f
|
shortFormat := f
|
||||||
|
|
||||||
if f != common.ClippedSimpleTimeFormat && f != common.ClippedSimpleTimeFormatOneDrive {
|
if f != common.ClippedSimple &&
|
||||||
clippedFormat = ""
|
f != common.ClippedSimpleOneDrive &&
|
||||||
|
f != common.DateOnly {
|
||||||
|
shortFormat = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
v := common.FormatTimeWith(in, f)
|
v := common.FormatTimeWith(in, f)
|
||||||
@ -136,8 +139,8 @@ func (suite *CommonTimeUnitSuite) TestExtractTime() {
|
|||||||
for _, ps := range pss {
|
for _, ps := range pss {
|
||||||
table = append(table, testable{
|
table = append(table, testable{
|
||||||
input: ps.prefix + v + ps.suffix,
|
input: ps.prefix + v + ps.suffix,
|
||||||
expect: comparable(suite.T(), in, clippedFormat),
|
expect: comparable(suite.T(), in, shortFormat),
|
||||||
clippedFormat: clippedFormat,
|
clippedFormat: shortFormat,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ func TestEventSuite(t *testing.T) {
|
|||||||
func (suite *EventSuite) TestEventInfo() {
|
func (suite *EventSuite) TestEventInfo() {
|
||||||
initial := time.Now()
|
initial := time.Now()
|
||||||
|
|
||||||
now := initial.Format(common.StandardTimeFormat)
|
now := common.FormatTime(initial)
|
||||||
suite.T().Logf("Initial: %v\nFormatted: %v\n", initial, now)
|
suite.T().Logf("Initial: %v\nFormatted: %v\n", initial, now)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -48,7 +48,7 @@ func (suite *EventSuite) TestEventInfo() {
|
|||||||
dateTime := models.NewDateTimeTimeZone()
|
dateTime := models.NewDateTimeTimeZone()
|
||||||
dateTime.SetDateTime(&now)
|
dateTime.SetDateTime(&now)
|
||||||
event.SetStart(dateTime)
|
event.SetStart(dateTime)
|
||||||
full, err := time.Parse(common.StandardTimeFormat, now)
|
full, err := common.ParseTime(now)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
i := &details.ExchangeInfo{
|
i := &details.ExchangeInfo{
|
||||||
ItemType: details.ExchangeEvent,
|
ItemType: details.ExchangeEvent,
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const (
|
|||||||
// Contents verified as working with sample data from kiota-serialization-json-go v0.5.5
|
// Contents verified as working with sample data from kiota-serialization-json-go v0.5.5
|
||||||
func GetMockMessageBytes(subject string) []byte {
|
func GetMockMessageBytes(subject string) []byte {
|
||||||
userID := "foobar@8qzvrj.onmicrosoft.com"
|
userID := "foobar@8qzvrj.onmicrosoft.com"
|
||||||
timestamp := " " + common.FormatNow(common.SimpleDateTimeFormat)
|
timestamp := " " + common.FormatNow(common.SimpleDateTime)
|
||||||
|
|
||||||
message := fmt.Sprintf(
|
message := fmt.Sprintf(
|
||||||
messageTmpl,
|
messageTmpl,
|
||||||
|
|||||||
@ -7,5 +7,5 @@ import (
|
|||||||
|
|
||||||
func DefaultTestRestoreDestination() control.RestoreDestination {
|
func DefaultTestRestoreDestination() control.RestoreDestination {
|
||||||
// Use microsecond granularity to help reduce collisions.
|
// Use microsecond granularity to help reduce collisions.
|
||||||
return control.DefaultRestoreDestination(common.SimpleDateTimeFormatTests)
|
return control.DefaultRestoreDestination(common.SimpleTimeTesting)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ func TestDetailsUnitSuite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() {
|
func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() {
|
||||||
nowStr := common.FormatNow(common.TabularOutputTimeFormat)
|
nowStr := common.FormatNow(common.TabularOutput)
|
||||||
now, err := common.ParseTime(nowStr)
|
now, err := common.ParseTime(nowStr)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@ type RestoreDestination struct {
|
|||||||
ContainerName string
|
ContainerName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultRestoreDestination(timeFormat string) RestoreDestination {
|
func DefaultRestoreDestination(timeFormat common.TimeFormat) RestoreDestination {
|
||||||
return RestoreDestination{
|
return RestoreDestination{
|
||||||
ContainerName: defaultRestoreLocation + common.FormatNow(timeFormat),
|
ContainerName: defaultRestoreLocation + common.FormatNow(timeFormat),
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user