| // 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 |
| } |