introduce printable version of selectors (#424)

This commit is contained in:
Keepers 2022-07-27 14:14:53 -06:00 committed by GitHub
parent e50728c0d6
commit cc810fa3ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 230 additions and 2 deletions

View File

@ -168,7 +168,10 @@ func makeExchangeScope(granularity string, cat exchangeCategory, vs []string) Ex
}
func makeExchangeUserScope(user, granularity string, cat exchangeCategory, vs []string) ExchangeScope {
return makeExchangeScope(granularity, cat, vs).set(ExchangeUser, user)
es := makeExchangeScope(granularity, cat, vs).set(ExchangeUser, user)
es[scopeKeyResource] = user
es[scopeKeyDataType] = cat.dataType()
return es
}
// Produces one or more exchange contact scopes.
@ -288,6 +291,8 @@ func makeExchangeFilterScope(cat, filterCat exchangeCategory, vs []string) Excha
scopeKeyGranularity: Filter,
scopeKeyCategory: cat.String(),
scopeKeyInfoFilter: filterCat.String(),
scopeKeyResource: Filter,
scopeKeyDataType: cat.dataType(),
filterCat.String(): join(vs...),
}
}
@ -436,6 +441,19 @@ func exchangeCatAtoI(s string) exchangeCategory {
}
}
// exchangeDataType returns the human-readable name of the core data type.
// Ex: ExchangeContactFolder.dataType() => ExchangeContact.String()
// Ex: ExchangeEvent.dataType() => ExchangeEvent.String().
func (ec exchangeCategory) dataType() string {
switch ec {
case ExchangeContact, ExchangeContactFolder:
return ExchangeContact.String()
case ExchangeMail, ExchangeMailFolder:
return ExchangeMail.String()
}
return ec.String()
}
// Granularity describes the granularity (directory || item)
// of the data in scope
func (s ExchangeScope) Granularity() string {

View File

@ -2,6 +2,7 @@ package selectors
import (
"encoding/json"
"fmt"
"strings"
"github.com/pkg/errors"
@ -21,6 +22,8 @@ const (
scopeKeyCategory = "category"
scopeKeyGranularity = "granularity"
scopeKeyInfoFilter = "info_filter"
scopeKeyResource = "resource"
scopeKeyDataType = "type"
)
// The granularity exprerssed by the scope. Groups imply non-item granularity,
@ -60,7 +63,7 @@ type Selector struct {
Service service `json:"service,omitempty"` // The service scope of the data. Exchange, Teams, Sharepoint, etc.
Excludes []map[string]string `json:"exclusions,omitempty"` // A slice of exclusion scopes. Exclusions apply globally to all inclusions/filters, with any-match behavior.
Filters []map[string]string `json:"filters,omitempty"` // A slice of filter scopes. All inclusions must also match ALL filters.
Includes []map[string]string `json:"scopes,omitempty"` // A slice of inclusion scopes. Comparators must match either one of these, or all filters, to be included.
Includes []map[string]string `json:"includes,omitempty"` // A slice of inclusion scopes. Comparators must match either one of these, or all filters, to be included.
}
// helper for specific selector instance constructors.
@ -148,6 +151,94 @@ func appendIncludes[T baseScope](
}
}
// ---------------------------------------------------------------------------
// Printing Selectors for Human Reading
// ---------------------------------------------------------------------------
type Printable struct {
Service string `json:"service"`
Excludes map[string][]string `json:"excludes,omitempty"`
Filters map[string][]string `json:"filters,omitempty"`
Includes map[string][]string `json:"includes,omitempty"`
}
// Printable is the minimized display of a selector, formatted for human readability.
// This transformer assumes that the scopeKeyResource and scopeKeyDataType have been
// added to all scopes as they were created. It is unable to infer resource or data
// type values from existing scope values.
func (s Selector) Printable() Printable {
return Printable{
Service: s.Service.String(),
Excludes: toResourceTypeMap(s.Excludes),
Filters: toResourceTypeMap(s.Filters),
Includes: toResourceTypeMap(s.Includes),
}
}
// Resources generates a tabular-readable output of the resources in Printable.
// Only the first (arbitrarily picked) resource is displayed. All others are
// simply counted. If no inclusions exist, uses Filters. If no filters exist,
// defaults to "All".
// Resource refers to the top-level entity in the service. User for Exchange,
// Site for sharepoint, etc.
func (p Printable) Resources() string {
s := resourcesShortFormat(p.Includes)
if len(s) == 0 {
s = resourcesShortFormat(p.Filters)
}
if len(s) == 0 {
s = "All"
}
return s
}
// returns a string with the resources in the map. Shortened to the first resource key,
// plus, if more exist, " (len-1 more)"
func resourcesShortFormat(m map[string][]string) string {
var s string
for k := range m {
s = k
break
}
if len(s) > 0 && len(m) > 1 {
s = fmt.Sprintf("%s (%d more)", s, len(m)-1)
}
return s
}
// Transforms the slice to a single map.
// Keys are each map's scopeKeyResource value.
// Values are the set of all scopeKeyDataTypes for a given resource.
func toResourceTypeMap(ms []map[string]string) map[string][]string {
if len(ms) == 0 {
return nil
}
r := make(map[string][]string)
for _, m := range ms {
res := m[scopeKeyResource]
if res == AnyTgt {
res = "All"
}
r[res] = addToSet(r[res], m[scopeKeyDataType])
}
return r
}
// returns [v] if set is empty,
// returns self if set contains v,
// appends v to self, otherwise.
func addToSet(set []string, v string) []string {
if len(set) == 0 {
return []string{v}
}
for _, s := range set {
if s == v {
return set
}
}
return append(set, v)
}
// ---------------------------------------------------------------------------
// Destination
// ---------------------------------------------------------------------------

View File

@ -1,12 +1,41 @@
package selectors
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
const (
user = "me@my.onmicrosoft.com"
)
var (
dataType = ExchangeEvent.String()
)
func stubScope() map[string]string {
return map[string]string{
ExchangeEvent.String(): AnyTgt,
ExchangeUser.String(): user,
scopeKeyCategory: dataType,
scopeKeyGranularity: Group,
scopeKeyResource: user,
scopeKeyDataType: dataType,
}
}
func stubSelector() Selector {
return Selector{
Service: ServiceExchange,
Excludes: []map[string]string{stubScope()},
Filters: []map[string]string{stubScope()},
Includes: []map[string]string{stubScope()},
}
}
type SelectorSuite struct {
suite.Suite
}
@ -32,3 +61,93 @@ func (suite *SelectorSuite) TestExistingDestinationErr() {
err := existingDestinationErr("foo", "bar")
assert.Error(suite.T(), err)
}
func (suite *SelectorSuite) TestPrintable() {
t := suite.T()
sel := stubSelector()
p := sel.Printable()
assert.Equal(t, sel.Service.String(), p.Service)
assert.Equal(t, 1, len(p.Excludes))
assert.Equal(t, 1, len(p.Filters))
assert.Equal(t, 1, len(p.Includes))
}
func (suite *SelectorSuite) TestPrintable_IncludedResources() {
t := suite.T()
sel := stubSelector()
p := sel.Printable()
res := p.Resources()
assert.Equal(t, user, res, "resource should state only the user")
sel.Includes = []map[string]string{
stubScope(),
{scopeKeyResource: "smarf", scopeKeyDataType: dataType},
{scopeKeyResource: "smurf", scopeKeyDataType: dataType}}
p = sel.Printable()
res = p.Resources()
assert.True(t, strings.HasSuffix(res, "(2 more)"), "resource '"+res+"' should have (2 more) suffix")
p.Includes = nil
res = p.Resources()
assert.Equal(t, user, res, "resource on filters should state only the user")
p.Filters = nil
res = p.Resources()
assert.Equal(t, "All", res, "resource with no Includes or Filters should state All")
}
func (suite *SelectorSuite) TestToResourceTypeMap() {
table := []struct {
name string
input []map[string]string
expect map[string][]string
}{
{
name: "single scope",
input: []map[string]string{stubScope()},
expect: map[string][]string{
user: {dataType},
},
},
{
name: "disjoint resources",
input: []map[string]string{
stubScope(),
{
scopeKeyResource: "smarf",
scopeKeyDataType: dataType,
},
},
expect: map[string][]string{
user: {dataType},
"smarf": {dataType},
},
},
{
name: "disjoint types",
input: []map[string]string{
stubScope(),
{
scopeKeyResource: user,
scopeKeyDataType: "other",
},
},
expect: map[string][]string{
user: {dataType, "other"},
},
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
rtm := toResourceTypeMap(test.input)
assert.Equal(t, test.expect, rtm)
})
}
}