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] 🌻 Feature

## Issue(s)

#501 

## Test Plan

- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Keepers 2022-08-31 15:10:10 -06:00 committed by GitHub
parent 6de326a2c4
commit 5c59522fc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 181 additions and 30 deletions

View File

@ -15,6 +15,7 @@ func ContactInfo(contact models.Contactable) *details.ExchangeInfo {
}
return &details.ExchangeInfo{
ItemType: details.ExchangeContact,
ContactName: name,
}
}

View File

@ -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))
})
}
}

View File

@ -46,6 +46,7 @@ func EventInfo(evt models.Eventable) *details.ExchangeInfo {
}
return &details.ExchangeInfo{
ItemType: details.ExchangeEvent,
Organizer: organizer,
Subject: subject,
EventStart: start,

View File

@ -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
},
},
}

View File

@ -28,6 +28,7 @@ func MessageInfo(msg models.Messageable) *details.ExchangeInfo {
}
return &details.ExchangeInfo{
ItemType: details.ExchangeMail,
Sender: sender,
Subject: subject,
Received: received,

View File

@ -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
},
},
}

View File

@ -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)

View File

@ -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))
}
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 {
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}
}

View File

@ -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,