blob: 460df2ce4d0fea24ad4842b4e69b53d289088a5b [file] [log] [blame]
// Package paramtools provides Params and ParamSet.
package paramtools
import (
"encoding/json"
"sort"
"strings"
"go.skia.org/infra/go/sets"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/util"
)
// Params is a set of key,value pairs.
type Params map[string]string
// ParamSet is a set of keys and the possible values that the keys could have. I.e.
// the []string should contain no duplicates.
type ParamSet map[string][]string
// ReadOnlyParamSet is a ParamSet that doesn't allow offer any mutating methods.
//
// Note that you can still modify the map, but hopefully the name along with the
// removal of the mutating methods will catch most of the mis-uses.
type ReadOnlyParamSet map[string][]string
// NewParams returns the parsed structured key (see query) as Params.
//
// It presumes a valid key, i.e. something that passed query.IsValid.
func NewParams(key string) Params {
ret := Params{}
parts := strings.Split(key, ",")
parts = parts[1 : len(parts)-1]
for _, s := range parts {
pair := strings.SplitN(s, "=", 2)
ret[pair[0]] = pair[1]
}
return ret
}
// Add adds each set of Params in order to this Params.
//
// Values in p will be overwritten.
func (p Params) Add(b ...Params) {
for _, oneMap := range b {
for k, v := range oneMap {
p[k] = v
}
}
}
// Copy returns a copy of the Params.
func (p Params) Copy() Params {
ret := make(Params, len(p))
for k, v := range p {
ret[k] = v
}
return ret
}
// Equal returns true if this Params equals a.
func (p Params) Equal(a Params) bool {
if len(a) != len(p) {
return false
}
// Since they are the same size we only need to check from one side, i.e.
// compare a's values to p's values.
for k, v := range a {
if bv, ok := p[k]; !ok || bv != v {
return false
}
}
return true
}
// Keys returns the keys of the Params.
func (p Params) Keys() []string {
ret := make([]string, 0, len(p))
for v := range p {
ret = append(ret, v)
}
return ret
}
// NewParamSet returns a new ParamSet initialized with the given maps of parameters.
func NewParamSet(ps ...Params) ParamSet {
ret := ParamSet{}
for _, onePS := range ps {
ret.AddParams(onePS)
}
return ret
}
// NewReadOnlyParamSet returns a new ReadOnlyParamSet initialized with the given
// maps of parameters.
func NewReadOnlyParamSet(ps ...Params) ReadOnlyParamSet {
return ReadOnlyParamSet(NewParamSet(ps...))
}
// AddParams adds the Params to this ParamSet.
func (p ParamSet) AddParams(ps Params) {
for k, v := range ps {
// You might be tempted to replace this with
// sort.SearchStrings(), but that's actually slower for short
// slices. The breakpoint seems to around 50, and since most
// of our ParamSet lists are short that ends up being slower.
params := p[k]
if !util.In(v, params) {
p[k] = append(params, v)
}
}
}
// AddParamsFromKey is the same as calling
//
// paramset.AddParams(NewParams(key))
//
// but without creating the intermediate Params.
//
// It presumes a valid key, i.e. something that passed query.ValidateKey.
func (p ParamSet) AddParamsFromKey(key string) {
parts := strings.Split(key, ",")
parts = parts[1 : len(parts)-1]
for _, s := range parts {
pair := strings.SplitN(s, "=", 2)
params := p[pair[0]]
if !util.In(pair[1], params) {
p[pair[0]] = append(params, pair[1])
}
}
}
// AddParamSet adds the ParamSet or ReadOnlyParamSet to this ParamSet.
func (p ParamSet) AddParamSet(ps map[string][]string) {
for k, arr := range ps {
if _, ok := p[k]; !ok {
p[k] = append([]string{}, arr...)
} else {
for _, v := range arr {
if !util.In(v, p[k]) {
p[k] = append(p[k], v)
}
}
}
}
}
// Equal returns true if the given Paramset contain exactly the same keys and associated
// values as this one. Side Effect: both ParamSets will be normalized after this call (their
// values will be sorted) if they have the same number of keys.
func (p ParamSet) Equal(right map[string][]string) bool {
if len(p) != len(right) {
return false
}
p.Normalize()
ParamSet(right).Normalize()
for k, leftValues := range p {
rightValues, ok := right[k]
if !ok {
return false
}
// Due to normalize, we expect leftValues and rightValues to be in sorted order
if len(leftValues) != len(rightValues) {
return false
}
for i := range leftValues {
if leftValues[i] != rightValues[i] {
return false
}
}
}
return true
}
// Keys returns the keys of the ReadOnlyParamSet.
func (p ReadOnlyParamSet) Keys() []string {
ret := make([]string, 0, len(p))
for v := range p {
ret = append(ret, v)
}
return ret
}
// ToString returns a json string representation of the paramset.
func (p ReadOnlyParamSet) ToString() (string, error) {
b, err := json.Marshal(p)
if err != nil {
return "", err
}
return string(b), nil
}
// FromString creates a new paramset object from json string.
func FromString(jsonStr string) (ParamSet, error) {
var ps ParamSet
err := json.Unmarshal([]byte(jsonStr), &ps)
return ps, err
}
// Keys returns the keys of the ParamSet.
func (p ParamSet) Keys() []string {
return ReadOnlyParamSet(p).Keys()
}
// Copy returns a copy of the ParamSet.
func (p ParamSet) Copy() ParamSet {
ret := ParamSet{}
for k, v := range p {
newV := make([]string, len(v), len(v))
copy(newV, v)
ret[k] = newV
}
return ret
}
// FrozenCopy returns a copy of the ParamSet as a ReadOnlyParamSet.
func (p ParamSet) FrozenCopy() ReadOnlyParamSet {
return ReadOnlyParamSet(p.Copy())
}
// Freeze returns the ReadOnlyParamSet version of the ParamSet.
//
// It is up to the caller to make sure the original ParamSet is not modified, or
// call FrozenCopy() instead.
func (p ParamSet) Freeze() ReadOnlyParamSet {
return ReadOnlyParamSet(p)
}
// Normalize all the values by sorting them.
func (p ParamSet) Normalize() {
for _, arr := range p {
sort.Strings(arr)
}
}
// Matches returns true if the params in 'p' match the sets given in 'right'.
// For every key in 'p' there has to be a matching key in 'right' and the
// intersection of their values must be not empty.
func (p ReadOnlyParamSet) Matches(right ReadOnlyParamSet) bool {
for key, vals := range p {
rightVals, ok := right[key]
if !ok {
return false
}
found := false
for _, targetVal := range vals {
if util.In(targetVal, rightVals) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// Matches returns true if the params in 'p' match the sets given in 'right'.
// For every key in 'p' there has to be a matching key in 'right' and the
// intersection of their values must be not empty.
func (p ParamSet) Matches(right ParamSet) bool {
return ReadOnlyParamSet(p).Matches(ReadOnlyParamSet(right))
}
// CartesianProduct returns a channel of Params that represent the Cartesian
// Product of all the values for the given keys.
func (p ReadOnlyParamSet) CartesianProduct(keys []string) (<-chan Params, error) {
ret := make(chan Params)
counts := make([]int, len(keys))
for i, key := range keys {
counts[i] = len(p[key])
}
cpChan, err := sets.CartesianProduct(counts)
if err != nil {
close(ret)
return nil, skerr.Wrapf(err, "can not make cartesian product")
}
go func() {
for indices := range cpChan {
v := Params{}
for i, n := range indices {
v[keys[i]] = p[keys[i]][n]
}
ret <- v
}
close(ret)
}()
return ret, nil
}
// CartesianProduct returns a channel of Params that represent the Cartesian
// Product of all the values for the given keys.
func (p ParamSet) CartesianProduct(keys []string) (<-chan Params, error) {
return ReadOnlyParamSet(p).CartesianProduct(keys)
}
// MatchesParams returns true if the params in 'p' match the values given in
// 'right'. For every key in 'p' there has to be a matching key in 'right' and
// the intersection of their values must be not empty.
func (p ReadOnlyParamSet) MatchesParams(right Params) bool {
for key, vals := range p {
rightVal, ok := right[key]
if !ok {
return false
}
found := false
for _, targetVal := range vals {
if targetVal == rightVal {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// MatchesParams returns true if the params in 'p' match the values given in
// 'right'. For every key in 'p' there has to be a matching key in 'right' and
// the intersection of their values must be not empty.
func (p ParamSet) MatchesParams(right Params) bool {
return ReadOnlyParamSet(p).MatchesParams(right)
}
// Size returns the total number of values in the ReadOnlyParamSet.
func (p ReadOnlyParamSet) Size() int {
var ret int
for _, vals := range p {
ret += len(vals)
}
return ret
}
// Size returns the total number of values in the ParamSet.
func (p ParamSet) Size() int {
return ReadOnlyParamSet(p).Size()
}
// ParamMatcher is a list of Paramsets that can be matched against. The primary
// purpose is to match against a set of rules, e.g. ignore rules.
type ParamMatcher []ParamSet
// MatchAny returns true if the given ParamSet matches any of the rules in the matcher.
func (p ParamMatcher) MatchAny(params ParamSet) bool {
for _, oneRule := range p {
if oneRule.Matches(params) {
return true
}
}
return false
}
// MatchAnyParams returns true if the given Params matches any of the rules in the matcher.
func (p ParamMatcher) MatchAnyParams(params Params) bool {
for _, oneRule := range p {
if oneRule.MatchesParams(params) {
return true
}
}
return false
}