19466: Add an API documentation page about properties
[arvados.git] / sdk / go / arvados / vocabulary.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 package arvados
6
7 import (
8         "bytes"
9         "encoding/json"
10         "errors"
11         "fmt"
12         "reflect"
13         "strconv"
14         "strings"
15 )
16
17 type Vocabulary struct {
18         reservedTagKeys map[string]bool          `json:"-"`
19         StrictTags      bool                     `json:"strict_tags"`
20         Tags            map[string]VocabularyTag `json:"tags"`
21 }
22
23 type VocabularyTag struct {
24         Strict bool                          `json:"strict"`
25         Labels []VocabularyLabel             `json:"labels"`
26         Values map[string]VocabularyTagValue `json:"values"`
27 }
28
29 // Cannot have a constant map in Go, so we have to use a function
30 func (v *Vocabulary) systemTagKeys() map[string]bool {
31         return map[string]bool{
32                 "type":                  true,
33                 "template_uuid":         true,
34                 "groups":                true,
35                 "username":              true,
36                 "image_timestamp":       true,
37                 "docker-image-repo-tag": true,
38                 "filters":               true,
39                 "container_request":     true,
40                 "cwl_inputs":            true,
41                 "cwl_outputs":           true,
42         }
43 }
44
45 type VocabularyLabel struct {
46         Label string `json:"label"`
47 }
48
49 type VocabularyTagValue struct {
50         Labels []VocabularyLabel `json:"labels"`
51 }
52
53 // NewVocabulary creates a new Vocabulary from a JSON definition and a list
54 // of reserved tag keys that will get special treatment when strict mode is
55 // enabled.
56 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
57         if r := bytes.Compare(data, []byte("")); r == 0 {
58                 return &Vocabulary{}, nil
59         }
60         err = json.Unmarshal(data, &voc)
61         if err != nil {
62                 var serr *json.SyntaxError
63                 if errors.As(err, &serr) {
64                         offset := serr.Offset
65                         errorMsg := string(data[:offset])
66                         line := 1 + strings.Count(errorMsg, "\n")
67                         column := offset - int64(strings.LastIndex(errorMsg, "\n")+len("\n"))
68                         return nil, fmt.Errorf("invalid JSON format: %q (line %d, column %d)", err, line, column)
69                 }
70                 return nil, fmt.Errorf("invalid JSON format: %q", err)
71         }
72         if reflect.DeepEqual(voc, &Vocabulary{}) {
73                 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
74         }
75
76         shouldReportErrors := false
77         errors := []string{}
78
79         // json.Unmarshal() doesn't error out on duplicate keys.
80         dupedKeys := []string{}
81         err = checkJSONDupedKeys(json.NewDecoder(bytes.NewReader(data)), nil, &dupedKeys)
82         if err != nil {
83                 shouldReportErrors = true
84                 for _, dk := range dupedKeys {
85                         errors = append(errors, fmt.Sprintf("duplicate JSON key %q", dk))
86                 }
87         }
88         voc.reservedTagKeys = make(map[string]bool)
89         for _, managedKey := range managedTagKeys {
90                 voc.reservedTagKeys[managedKey] = true
91         }
92         for systemKey := range voc.systemTagKeys() {
93                 voc.reservedTagKeys[systemKey] = true
94         }
95         validationErrs, err := voc.validate()
96         if err != nil {
97                 shouldReportErrors = true
98                 errors = append(errors, validationErrs...)
99         }
100         if shouldReportErrors {
101                 return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
102         }
103         return voc, nil
104 }
105
106 func checkJSONDupedKeys(d *json.Decoder, path []string, errors *[]string) error {
107         t, err := d.Token()
108         if err != nil {
109                 return err
110         }
111         delim, ok := t.(json.Delim)
112         if !ok {
113                 return nil
114         }
115         switch delim {
116         case '{':
117                 keys := make(map[string]bool)
118                 for d.More() {
119                         t, err := d.Token()
120                         if err != nil {
121                                 return err
122                         }
123                         key := t.(string)
124
125                         if keys[key] {
126                                 *errors = append(*errors, strings.Join(append(path, key), "."))
127                         }
128                         keys[key] = true
129
130                         if err := checkJSONDupedKeys(d, append(path, key), errors); err != nil {
131                                 return err
132                         }
133                 }
134                 // consume closing '}'
135                 if _, err := d.Token(); err != nil {
136                         return err
137                 }
138         case '[':
139                 i := 0
140                 for d.More() {
141                         if err := checkJSONDupedKeys(d, append(path, strconv.Itoa(i)), errors); err != nil {
142                                 return err
143                         }
144                         i++
145                 }
146                 // consume closing ']'
147                 if _, err := d.Token(); err != nil {
148                         return err
149                 }
150         }
151         if len(path) == 0 && len(*errors) > 0 {
152                 return fmt.Errorf("duplicate JSON key(s) found")
153         }
154         return nil
155 }
156
157 func (v *Vocabulary) validate() ([]string, error) {
158         if v == nil {
159                 return nil, nil
160         }
161         tagKeys := map[string]string{}
162         // Checks for Vocabulary strictness
163         if v.StrictTags && len(v.Tags) == 0 {
164                 return nil, fmt.Errorf("vocabulary is strict but no tags are defined")
165         }
166         // Checks for collisions between tag keys, reserved tag keys
167         // and tag key labels.
168         errors := []string{}
169         for key := range v.Tags {
170                 if v.reservedTagKeys[key] {
171                         errors = append(errors, fmt.Sprintf("tag key %q is reserved", key))
172                 }
173                 lcKey := strings.ToLower(key)
174                 if tagKeys[lcKey] != "" {
175                         errors = append(errors, fmt.Sprintf("duplicate tag key %q", key))
176                 }
177                 tagKeys[lcKey] = key
178                 for _, lbl := range v.Tags[key].Labels {
179                         label := strings.ToLower(lbl.Label)
180                         if tagKeys[label] != "" {
181                                 errors = append(errors, fmt.Sprintf("tag label %q for key %q already seen as a tag key or label", lbl.Label, key))
182                         }
183                         tagKeys[label] = lbl.Label
184                 }
185                 // Checks for value strictness
186                 if v.Tags[key].Strict && len(v.Tags[key].Values) == 0 {
187                         errors = append(errors, fmt.Sprintf("tag key %q is configured as strict but doesn't provide values", key))
188                 }
189                 // Checks for collisions between tag values and tag value labels.
190                 tagValues := map[string]string{}
191                 for val := range v.Tags[key].Values {
192                         lcVal := strings.ToLower(val)
193                         if tagValues[lcVal] != "" {
194                                 errors = append(errors, fmt.Sprintf("duplicate tag value %q for tag %q", val, key))
195                         }
196                         // Checks for collisions between labels from different values.
197                         tagValues[lcVal] = val
198                         for _, tagLbl := range v.Tags[key].Values[val].Labels {
199                                 label := strings.ToLower(tagLbl.Label)
200                                 if tagValues[label] != "" && tagValues[label] != val {
201                                         errors = append(errors, fmt.Sprintf("tag value label %q for pair (%q:%q) already seen on value %q", tagLbl.Label, key, val, tagValues[label]))
202                                 }
203                                 tagValues[label] = val
204                         }
205                 }
206         }
207         if len(errors) > 0 {
208                 return errors, fmt.Errorf("invalid vocabulary")
209         }
210         return nil, nil
211 }
212
213 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
214         if v == nil {
215                 return
216         }
217         labels = make(map[string]string)
218         for key, val := range v.Tags {
219                 for _, lbl := range val.Labels {
220                         label := strings.ToLower(lbl.Label)
221                         labels[label] = key
222                 }
223         }
224         return labels
225 }
226
227 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
228         if v == nil {
229                 return
230         }
231         labels = make(map[string]string)
232         if _, ok := v.Tags[key]; ok {
233                 for val := range v.Tags[key].Values {
234                         labels[strings.ToLower(val)] = val
235                         for _, tagLbl := range v.Tags[key].Values[val].Labels {
236                                 label := strings.ToLower(tagLbl.Label)
237                                 labels[label] = val
238                         }
239                 }
240         }
241         return labels
242 }
243
244 func (v *Vocabulary) checkValue(key, val string) error {
245         if _, ok := v.Tags[key].Values[val]; !ok {
246                 lcVal := strings.ToLower(val)
247                 correctValue, ok := v.getLabelsToValues(key)[lcVal]
248                 if ok {
249                         return fmt.Errorf("tag value %q for key %q is an alias, must be provided as %q", val, key, correctValue)
250                 } else if v.Tags[key].Strict {
251                         return fmt.Errorf("tag value %q is not valid for key %q", val, key)
252                 }
253         }
254         return nil
255 }
256
257 // Check validates the given data against the vocabulary.
258 func (v *Vocabulary) Check(data map[string]interface{}) error {
259         if v == nil {
260                 return nil
261         }
262         for key, val := range data {
263                 // Checks for key validity
264                 if v.reservedTagKeys[key] {
265                         // Allow reserved keys to be used even if they are not defined in
266                         // the vocabulary no matter its strictness.
267                         continue
268                 }
269                 if _, ok := v.Tags[key]; !ok {
270                         lcKey := strings.ToLower(key)
271                         correctKey, ok := v.getLabelsToKeys()[lcKey]
272                         if ok {
273                                 return fmt.Errorf("tag key %q is an alias, must be provided as %q", key, correctKey)
274                         } else if v.StrictTags {
275                                 return fmt.Errorf("tag key %q is not defined in the vocabulary", key)
276                         }
277                         // If the key is not defined, we don't need to check the value
278                         continue
279                 }
280                 // Checks for value validity -- key is defined
281                 switch val := val.(type) {
282                 case string:
283                         err := v.checkValue(key, val)
284                         if err != nil {
285                                 return err
286                         }
287                 case []interface{}:
288                         for _, singleVal := range val {
289                                 switch singleVal := singleVal.(type) {
290                                 case string:
291                                         err := v.checkValue(key, singleVal)
292                                         if err != nil {
293                                                 return err
294                                         }
295                                 default:
296                                         return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
297                                 }
298                         }
299                 default:
300                         return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)
301                 }
302         }
303         return nil
304 }