From 5c59522fc24ec08c45a730f9b88a6503db353ce4 Mon Sep 17 00:00:00 2001 From: Keepers Date: Wed, 31 Aug 2022 15:10:10 -0600 Subject: [PATCH] print separate tables per entry type (#709) ## Description Since a backup can encapsulate multiple data types (ex: mail, contacts, and events), it doesn't make sense to print one table with all disjoint values. This change splits up the print output so that each data type in the details gets its own table. ## Type of change Please check the type of change your PR introduces: - [x] :sunflower: Feature ## Issue(s) #501 ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/connector/exchange/contact.go | 1 + .../connector/exchange/contact_test.go | 12 ++- src/internal/connector/exchange/event.go | 1 + src/internal/connector/exchange/event_test.go | 19 +++- src/internal/connector/exchange/message.go | 1 + .../connector/exchange/message_test.go | 29 ++++- src/internal/connector/onedrive/collection.go | 6 +- src/pkg/backup/details/details.go | 100 +++++++++++++++--- src/pkg/backup/details/details_test.go | 42 +++++++- 9 files changed, 181 insertions(+), 30 deletions(-) diff --git a/src/internal/connector/exchange/contact.go b/src/internal/connector/exchange/contact.go index c51cda98f..d6e401d20 100644 --- a/src/internal/connector/exchange/contact.go +++ b/src/internal/connector/exchange/contact.go @@ -15,6 +15,7 @@ func ContactInfo(contact models.Contactable) *details.ExchangeInfo { } return &details.ExchangeInfo{ + ItemType: details.ExchangeContact, ContactName: name, } } diff --git a/src/internal/connector/exchange/contact_test.go b/src/internal/connector/exchange/contact_test.go index 468229419..bbc6fe9d5 100644 --- a/src/internal/connector/exchange/contact_test.go +++ b/src/internal/connector/exchange/contact_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/alcionai/corso/pkg/backup/details" @@ -25,7 +26,8 @@ func (suite *ContactSuite) TestContactInfo() { { name: "Empty Contact", contactAndRP: func() (models.Contactable, *details.ExchangeInfo) { - return models.NewContact(), &details.ExchangeInfo{} + i := &details.ExchangeInfo{ItemType: details.ExchangeContact} + return models.NewContact(), i }, }, { name: "Only Name", @@ -33,14 +35,18 @@ func (suite *ContactSuite) TestContactInfo() { aPerson := "Whole Person" contact := models.NewContact() contact.SetDisplayName(&aPerson) - return contact, &details.ExchangeInfo{ContactName: aPerson} + i := &details.ExchangeInfo{ + ItemType: details.ExchangeContact, + ContactName: aPerson, + } + return contact, i }, }, } for _, test := range tests { suite.T().Run(test.name, func(t *testing.T) { contact, expected := test.contactAndRP() - suite.Equal(expected, ContactInfo(contact)) + assert.Equal(t, expected, ContactInfo(contact)) }) } } diff --git a/src/internal/connector/exchange/event.go b/src/internal/connector/exchange/event.go index c30a5ec76..98836058f 100644 --- a/src/internal/connector/exchange/event.go +++ b/src/internal/connector/exchange/event.go @@ -46,6 +46,7 @@ func EventInfo(evt models.Eventable) *details.ExchangeInfo { } return &details.ExchangeInfo{ + ItemType: details.ExchangeEvent, Organizer: organizer, Subject: subject, EventStart: start, diff --git a/src/internal/connector/exchange/event_test.go b/src/internal/connector/exchange/event_test.go index 5879ff87b..d3e7a97fc 100644 --- a/src/internal/connector/exchange/event_test.go +++ b/src/internal/connector/exchange/event_test.go @@ -37,7 +37,8 @@ func (suite *EventSuite) TestEventInfo() { { name: "Empty event", evtAndRP: func() (models.Eventable, *details.ExchangeInfo) { - return models.NewEvent(), &details.ExchangeInfo{} + i := &details.ExchangeInfo{ItemType: details.ExchangeEvent} + return models.NewEvent(), i }, }, { @@ -49,7 +50,11 @@ func (suite *EventSuite) TestEventInfo() { event.SetStart(dateTime) full, err := time.Parse(common.StandardTimeFormat, now) require.NoError(suite.T(), err) - return event, &details.ExchangeInfo{Received: full} + i := &details.ExchangeInfo{ + ItemType: details.ExchangeEvent, + Received: full, + } + return event, i }, }, { @@ -58,7 +63,11 @@ func (suite *EventSuite) TestEventInfo() { subject := "Hello Corso" event := models.NewEvent() event.SetSubject(&subject) - return event, &details.ExchangeInfo{Subject: subject} + i := &details.ExchangeInfo{ + ItemType: details.ExchangeEvent, + Subject: subject, + } + return event, i }, }, { @@ -70,11 +79,13 @@ func (suite *EventSuite) TestEventInfo() { subject := " Test MockReview + Lunch" organizer := "foobar3@8qzvrj.onmicrosoft.com" eventTime := time.Date(2022, time.April, 28, 3, 41, 58, 0, time.UTC) - return event, &details.ExchangeInfo{ + i := &details.ExchangeInfo{ + ItemType: details.ExchangeEvent, Subject: subject, Organizer: organizer, EventStart: eventTime, } + return event, i }, }, } diff --git a/src/internal/connector/exchange/message.go b/src/internal/connector/exchange/message.go index 6b0322124..1bf2bc394 100644 --- a/src/internal/connector/exchange/message.go +++ b/src/internal/connector/exchange/message.go @@ -28,6 +28,7 @@ func MessageInfo(msg models.Messageable) *details.ExchangeInfo { } return &details.ExchangeInfo{ + ItemType: details.ExchangeMail, Sender: sender, Subject: subject, Received: received, diff --git a/src/internal/connector/exchange/message_test.go b/src/internal/connector/exchange/message_test.go index 231707b8e..96a4f53d9 100644 --- a/src/internal/connector/exchange/message_test.go +++ b/src/internal/connector/exchange/message_test.go @@ -26,7 +26,8 @@ func (suite *MessageSuite) TestMessageInfo() { { name: "Empty message", msgAndRP: func() (models.Messageable, *details.ExchangeInfo) { - return models.NewMessage(), &details.ExchangeInfo{} + i := &details.ExchangeInfo{ItemType: details.ExchangeMail} + return models.NewMessage(), i }, }, { @@ -39,7 +40,11 @@ func (suite *MessageSuite) TestMessageInfo() { sea.SetAddress(&sender) sr.SetEmailAddress(sea) msg.SetSender(sr) - return msg, &details.ExchangeInfo{Sender: sender} + i := &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + Sender: sender, + } + return msg, i }, }, { @@ -48,7 +53,11 @@ func (suite *MessageSuite) TestMessageInfo() { subject := "Hello world" msg := models.NewMessage() msg.SetSubject(&subject) - return msg, &details.ExchangeInfo{Subject: subject} + i := &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + Subject: subject, + } + return msg, i }, }, { @@ -57,7 +66,11 @@ func (suite *MessageSuite) TestMessageInfo() { now := time.Now() msg := models.NewMessage() msg.SetReceivedDateTime(&now) - return msg, &details.ExchangeInfo{Received: now} + i := &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + Received: now, + } + return msg, i }, }, { @@ -74,7 +87,13 @@ func (suite *MessageSuite) TestMessageInfo() { msg.SetSender(sr) msg.SetSubject(&subject) msg.SetReceivedDateTime(&now) - return msg, &details.ExchangeInfo{Sender: sender, Subject: subject, Received: now} + i := &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + Sender: sender, + Subject: subject, + Received: now, + } + return msg, i }, }, } diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index 7c129bee8..5db4a5c0e 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -119,7 +119,11 @@ func (oc *Collection) populateItems(ctx context.Context) { oc.data <- &Item{ id: itemID, data: itemData, - info: &details.OneDriveInfo{ItemName: itemName, ParentPath: oc.folderPath}, + info: &details.OneDriveInfo{ + ItemType: details.OneDriveItem, + ItemName: itemName, + ParentPath: oc.folderPath, + }, } } close(oc.data) diff --git a/src/pkg/backup/details/details.go b/src/pkg/backup/details/details.go index 42cbb7f8f..9cc2fe927 100644 --- a/src/pkg/backup/details/details.go +++ b/src/pkg/backup/details/details.go @@ -2,10 +2,12 @@ package details import ( "context" + "strconv" "sync" "time" "github.com/alcionai/corso/cli/print" + "github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/internal/model" ) @@ -22,12 +24,22 @@ type DetailsModel struct { // Print writes the DetailModel Entries to StdOut, in the format // requested by the caller. func (dm DetailsModel) PrintEntries(ctx context.Context) { - ps := []print.Printable{} + perType := map[itemType][]print.Printable{} + for _, de := range dm.Entries { - ps = append(ps, de) + it := de.infoType() + ps, ok := perType[it] + + if !ok { + ps = []print.Printable{} + } + + perType[it] = append(ps, print.Printable(de)) } - print.All(ctx, ps...) + for _, ps := range perType { + print.All(ctx, ps...) + } } // Paths returns the list of Paths extracted from the Entries slice. @@ -126,6 +138,21 @@ func (de DetailsEntry) Values() []string { return vs } +type itemType int + +const ( + UnknownType itemType = iota + + // separate each service by a factor of 100 for padding + ExchangeContact + ExchangeEvent + ExchangeMail + + SharepointItem itemType = iota + 100 + + OneDriveItem itemType = iota + 200 +) + // ItemInfo is a oneOf that contains service specific // information about the item it tracks type ItemInfo struct { @@ -134,8 +161,30 @@ type ItemInfo struct { OneDrive *OneDriveInfo `json:"oneDrive,omitempty"` } +// typedInfo should get embedded in each sesrvice type to track +// the type of item it stores for multi-item service support. + +// infoType provides internal categorization for collecting like-typed ItemInfos. +// It should return the most granular value type (ex: "event" for an exchange +// calendar event). +func (i ItemInfo) infoType() itemType { + switch { + case i.Exchange != nil: + return i.Exchange.ItemType + + case i.Sharepoint != nil: + return i.Sharepoint.ItemType + + case i.OneDrive != nil: + return i.OneDrive.ItemType + } + + return UnknownType +} + // ExchangeInfo describes an exchange item type ExchangeInfo struct { + ItemType itemType Sender string `json:"sender,omitempty"` Subject string `json:"subject,omitempty"` Received time.Time `json:"received,omitempty"` @@ -147,47 +196,72 @@ type ExchangeInfo struct { // Headers returns the human-readable names of properties in an ExchangeInfo // for printing out to a terminal in a columnar display. -func (e ExchangeInfo) Headers() []string { - return []string{"Sender", "Subject", "Received"} +func (i ExchangeInfo) Headers() []string { + switch i.ItemType { + case ExchangeEvent: + return []string{"Organizer", "Subject", "Starts", "Recurring"} + + case ExchangeContact: + return []string{"Contact Name"} + + case ExchangeMail: + return []string{"Sender", "Subject", "Received"} + } + + return []string{} } // Values returns the values matching the Headers list for printing // out to a terminal in a columnar display. -func (e ExchangeInfo) Values() []string { - return []string{e.Sender, e.Subject, e.Received.Format(time.RFC3339Nano)} +func (i ExchangeInfo) Values() []string { + switch i.ItemType { + case ExchangeEvent: + return []string{i.Organizer, i.Subject, common.FormatTime(i.EventStart), strconv.FormatBool(i.EventRecurs)} + + case ExchangeContact: + return []string{i.ContactName} + + case ExchangeMail: + return []string{i.Sender, i.Subject, common.FormatTime(i.Received)} + } + + return []string{} } // SharepointInfo describes a sharepoint item // TODO: Implement this. This is currently here // just to illustrate usage -type SharepointInfo struct{} +type SharepointInfo struct { + ItemType itemType +} // Headers returns the human-readable names of properties in a SharepointInfo // for printing out to a terminal in a columnar display. -func (s SharepointInfo) Headers() []string { +func (i SharepointInfo) Headers() []string { return []string{} } // Values returns the values matching the Headers list for printing // out to a terminal in a columnar display. -func (s SharepointInfo) Values() []string { +func (i SharepointInfo) Values() []string { return []string{} } // OneDriveInfo describes a oneDrive item type OneDriveInfo struct { + ItemType itemType ParentPath string `json:"parentPath"` ItemName string `json:"itemName"` } // Headers returns the human-readable names of properties in a OneDriveInfo // for printing out to a terminal in a columnar display. -func (oi OneDriveInfo) Headers() []string { +func (i OneDriveInfo) Headers() []string { return []string{"ItemName", "ParentPath"} } // Values returns the values matching the Headers list for printing // out to a terminal in a columnar display. -func (oi OneDriveInfo) Values() []string { - return []string{oi.ItemName, oi.ParentPath} +func (i OneDriveInfo) Values() []string { + return []string{i.ItemName, i.ParentPath} } diff --git a/src/pkg/backup/details/details_test.go b/src/pkg/backup/details/details_test.go index 5053a9b83..91dd3ce3e 100644 --- a/src/pkg/backup/details/details_test.go +++ b/src/pkg/backup/details/details_test.go @@ -2,11 +2,12 @@ package details_test import ( "testing" - "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/pkg/backup/details" ) @@ -23,8 +24,9 @@ func TestDetailsUnitSuite(t *testing.T) { } func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() { - now := time.Now() - nowStr := now.Format(time.RFC3339Nano) + nowStr := common.FormatNow(common.StandardTimeFormat) + now, err := common.ParseTime(nowStr) + require.NoError(suite.T(), err) table := []struct { name string @@ -41,11 +43,43 @@ func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() { expectVs: []string{"reporef"}, }, { - name: "exhange info", + name: "exchange event info", entry: details.DetailsEntry{ RepoRef: "reporef", ItemInfo: details.ItemInfo{ Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeEvent, + EventStart: now, + Organizer: "organizer", + EventRecurs: true, + Subject: "subject", + }, + }, + }, + expectHs: []string{"Repo Ref", "Organizer", "Subject", "Starts", "Recurring"}, + expectVs: []string{"reporef", "organizer", "subject", nowStr, "true"}, + }, + { + name: "exchange contact info", + entry: details.DetailsEntry{ + RepoRef: "reporef", + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeContact, + ContactName: "contactName", + }, + }, + }, + expectHs: []string{"Repo Ref", "Contact Name"}, + expectVs: []string{"reporef", "contactName"}, + }, + { + name: "exchange mail info", + entry: details.DetailsEntry{ + RepoRef: "reporef", + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeMail, Sender: "sender", Subject: "subject", Received: now,