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,