1cb93a386Sopenharmony_ci// Copyright 2020 The Chromium Authors. All rights reserved.
2cb93a386Sopenharmony_ci// Use of this source code is governed by a BSD-style license that can be
3cb93a386Sopenharmony_ci// found in the LICENSE file.
4cb93a386Sopenharmony_cipackage gen_tasks_logic
5cb93a386Sopenharmony_ci
6cb93a386Sopenharmony_ci/*
7cb93a386Sopenharmony_ci   This file contains logic related to task/job name schemas.
8cb93a386Sopenharmony_ci*/
9cb93a386Sopenharmony_ci
10cb93a386Sopenharmony_ciimport (
11cb93a386Sopenharmony_ci	"encoding/json"
12cb93a386Sopenharmony_ci	"fmt"
13cb93a386Sopenharmony_ci	"log"
14cb93a386Sopenharmony_ci	"os"
15cb93a386Sopenharmony_ci	"regexp"
16cb93a386Sopenharmony_ci	"strings"
17cb93a386Sopenharmony_ci)
18cb93a386Sopenharmony_ci
19cb93a386Sopenharmony_ci// parts represents the key/value pairs which make up task and job names.
20cb93a386Sopenharmony_citype parts map[string]string
21cb93a386Sopenharmony_ci
22cb93a386Sopenharmony_ci// equal returns true if the given part of this job's name equals any of the
23cb93a386Sopenharmony_ci// given values. Panics if no values are provided.
24cb93a386Sopenharmony_cifunc (p parts) equal(part string, eq ...string) bool {
25cb93a386Sopenharmony_ci	if len(eq) == 0 {
26cb93a386Sopenharmony_ci		log.Fatal("No values provided for equal!")
27cb93a386Sopenharmony_ci	}
28cb93a386Sopenharmony_ci	v := p[part]
29cb93a386Sopenharmony_ci	for _, e := range eq {
30cb93a386Sopenharmony_ci		if v == e {
31cb93a386Sopenharmony_ci			return true
32cb93a386Sopenharmony_ci		}
33cb93a386Sopenharmony_ci	}
34cb93a386Sopenharmony_ci	return false
35cb93a386Sopenharmony_ci}
36cb93a386Sopenharmony_ci
37cb93a386Sopenharmony_ci// role returns true if the role for this job equals any of the given values.
38cb93a386Sopenharmony_cifunc (p parts) role(eq ...string) bool {
39cb93a386Sopenharmony_ci	return p.equal("role", eq...)
40cb93a386Sopenharmony_ci}
41cb93a386Sopenharmony_ci
42cb93a386Sopenharmony_ci// os returns true if the OS for this job equals any of the given values.
43cb93a386Sopenharmony_cifunc (p parts) os(eq ...string) bool {
44cb93a386Sopenharmony_ci	return p.equal("os", eq...)
45cb93a386Sopenharmony_ci}
46cb93a386Sopenharmony_ci
47cb93a386Sopenharmony_ci// compiler returns true if the compiler for this job equals any of the given
48cb93a386Sopenharmony_ci// values.
49cb93a386Sopenharmony_cifunc (p parts) compiler(eq ...string) bool {
50cb93a386Sopenharmony_ci	return p.equal("compiler", eq...)
51cb93a386Sopenharmony_ci}
52cb93a386Sopenharmony_ci
53cb93a386Sopenharmony_ci// model returns true if the model for this job equals any of the given values.
54cb93a386Sopenharmony_cifunc (p parts) model(eq ...string) bool {
55cb93a386Sopenharmony_ci	return p.equal("model", eq...)
56cb93a386Sopenharmony_ci}
57cb93a386Sopenharmony_ci
58cb93a386Sopenharmony_ci// frequency returns true if the frequency for this job equals any of the given
59cb93a386Sopenharmony_ci// values.
60cb93a386Sopenharmony_cifunc (p parts) frequency(eq ...string) bool {
61cb93a386Sopenharmony_ci	return p.equal("frequency", eq...)
62cb93a386Sopenharmony_ci}
63cb93a386Sopenharmony_ci
64cb93a386Sopenharmony_ci// cpu returns true if the task's cpu_or_gpu is "CPU" and the CPU for this
65cb93a386Sopenharmony_ci// task equals any of the given values. If no values are provided, cpu returns
66cb93a386Sopenharmony_ci// true if this task runs on CPU.
67cb93a386Sopenharmony_cifunc (p parts) cpu(eq ...string) bool {
68cb93a386Sopenharmony_ci	if p["cpu_or_gpu"] == "CPU" {
69cb93a386Sopenharmony_ci		if len(eq) == 0 {
70cb93a386Sopenharmony_ci			return true
71cb93a386Sopenharmony_ci		}
72cb93a386Sopenharmony_ci		return p.equal("cpu_or_gpu_value", eq...)
73cb93a386Sopenharmony_ci	}
74cb93a386Sopenharmony_ci	return false
75cb93a386Sopenharmony_ci}
76cb93a386Sopenharmony_ci
77cb93a386Sopenharmony_ci// gpu returns true if the task's cpu_or_gpu is "GPU" and the GPU for this task
78cb93a386Sopenharmony_ci// equals any of the given values. If no values are provided, gpu returns true
79cb93a386Sopenharmony_ci// if this task runs on GPU.
80cb93a386Sopenharmony_cifunc (p parts) gpu(eq ...string) bool {
81cb93a386Sopenharmony_ci	if p["cpu_or_gpu"] == "GPU" {
82cb93a386Sopenharmony_ci		if len(eq) == 0 {
83cb93a386Sopenharmony_ci			return true
84cb93a386Sopenharmony_ci		}
85cb93a386Sopenharmony_ci		return p.equal("cpu_or_gpu_value", eq...)
86cb93a386Sopenharmony_ci	}
87cb93a386Sopenharmony_ci	return false
88cb93a386Sopenharmony_ci}
89cb93a386Sopenharmony_ci
90cb93a386Sopenharmony_ci// arch returns true if the architecture for this job equals any of the
91cb93a386Sopenharmony_ci// given values.
92cb93a386Sopenharmony_cifunc (p parts) arch(eq ...string) bool {
93cb93a386Sopenharmony_ci	return p.equal("arch", eq...) || p.equal("target_arch", eq...)
94cb93a386Sopenharmony_ci}
95cb93a386Sopenharmony_ci
96cb93a386Sopenharmony_ci// extraConfig returns true if any of the extra_configs for this job equals
97cb93a386Sopenharmony_ci// any of the given values. If the extra_config starts with "SK_",
98cb93a386Sopenharmony_ci// it is considered to be a single config.
99cb93a386Sopenharmony_cifunc (p parts) extraConfig(eq ...string) bool {
100cb93a386Sopenharmony_ci	if len(eq) == 0 {
101cb93a386Sopenharmony_ci		log.Fatal("No values provided for extraConfig()!")
102cb93a386Sopenharmony_ci	}
103cb93a386Sopenharmony_ci	ec := p["extra_config"]
104cb93a386Sopenharmony_ci	if ec == "" {
105cb93a386Sopenharmony_ci		return false
106cb93a386Sopenharmony_ci	}
107cb93a386Sopenharmony_ci	var cfgs []string
108cb93a386Sopenharmony_ci	if strings.HasPrefix(ec, "SK_") {
109cb93a386Sopenharmony_ci		cfgs = []string{ec}
110cb93a386Sopenharmony_ci	} else {
111cb93a386Sopenharmony_ci		cfgs = strings.Split(ec, "_")
112cb93a386Sopenharmony_ci	}
113cb93a386Sopenharmony_ci	for _, c := range cfgs {
114cb93a386Sopenharmony_ci		for _, e := range eq {
115cb93a386Sopenharmony_ci			if c == e {
116cb93a386Sopenharmony_ci				return true
117cb93a386Sopenharmony_ci			}
118cb93a386Sopenharmony_ci		}
119cb93a386Sopenharmony_ci	}
120cb93a386Sopenharmony_ci	return false
121cb93a386Sopenharmony_ci}
122cb93a386Sopenharmony_ci
123cb93a386Sopenharmony_ci// noExtraConfig returns true if there are no extra_configs for this job.
124cb93a386Sopenharmony_cifunc (p parts) noExtraConfig(eq ...string) bool {
125cb93a386Sopenharmony_ci	ec := p["extra_config"]
126cb93a386Sopenharmony_ci	return ec == ""
127cb93a386Sopenharmony_ci}
128cb93a386Sopenharmony_ci
129cb93a386Sopenharmony_ci// matchPart returns true if the given part of this job's name matches any of
130cb93a386Sopenharmony_ci// the given regular expressions. Note that a regular expression might match any
131cb93a386Sopenharmony_ci// substring, so if you need an exact match on the entire string you'll need to
132cb93a386Sopenharmony_ci// use `^` and `$`. Panics if no regular expressions are provided.
133cb93a386Sopenharmony_cifunc (p parts) matchPart(part string, re ...string) bool {
134cb93a386Sopenharmony_ci	if len(re) == 0 {
135cb93a386Sopenharmony_ci		log.Fatal("No regular expressions provided for matchPart()!")
136cb93a386Sopenharmony_ci	}
137cb93a386Sopenharmony_ci	v := p[part]
138cb93a386Sopenharmony_ci	for _, r := range re {
139cb93a386Sopenharmony_ci		if regexp.MustCompile(r).MatchString(v) {
140cb93a386Sopenharmony_ci			return true
141cb93a386Sopenharmony_ci		}
142cb93a386Sopenharmony_ci	}
143cb93a386Sopenharmony_ci	return false
144cb93a386Sopenharmony_ci}
145cb93a386Sopenharmony_ci
146cb93a386Sopenharmony_ci// matchRole returns true if the role for this job matches any of the given
147cb93a386Sopenharmony_ci// regular expressions.
148cb93a386Sopenharmony_cifunc (p parts) matchRole(re ...string) bool {
149cb93a386Sopenharmony_ci	return p.matchPart("role", re...)
150cb93a386Sopenharmony_ci}
151cb93a386Sopenharmony_ci
152cb93a386Sopenharmony_cifunc (p parts) project(re ...string) bool {
153cb93a386Sopenharmony_ci	return p.matchPart("project", re...)
154cb93a386Sopenharmony_ci}
155cb93a386Sopenharmony_ci
156cb93a386Sopenharmony_ci// matchOs returns true if the OS for this job matches any of the given regular
157cb93a386Sopenharmony_ci// expressions.
158cb93a386Sopenharmony_cifunc (p parts) matchOs(re ...string) bool {
159cb93a386Sopenharmony_ci	return p.matchPart("os", re...)
160cb93a386Sopenharmony_ci}
161cb93a386Sopenharmony_ci
162cb93a386Sopenharmony_ci// matchCompiler returns true if the compiler for this job matches any of the
163cb93a386Sopenharmony_ci// given regular expressions.
164cb93a386Sopenharmony_cifunc (p parts) matchCompiler(re ...string) bool {
165cb93a386Sopenharmony_ci	return p.matchPart("compiler", re...)
166cb93a386Sopenharmony_ci}
167cb93a386Sopenharmony_ci
168cb93a386Sopenharmony_ci// matchModel returns true if the model for this job matches any of the given
169cb93a386Sopenharmony_ci// regular expressions.
170cb93a386Sopenharmony_cifunc (p parts) matchModel(re ...string) bool {
171cb93a386Sopenharmony_ci	return p.matchPart("model", re...)
172cb93a386Sopenharmony_ci}
173cb93a386Sopenharmony_ci
174cb93a386Sopenharmony_ci// matchCpu returns true if the task's cpu_or_gpu is "CPU" and the CPU for this
175cb93a386Sopenharmony_ci// task matches any of the given regular expressions. If no regular expressions
176cb93a386Sopenharmony_ci// are provided, cpu returns true if this task runs on CPU.
177cb93a386Sopenharmony_cifunc (p parts) matchCpu(re ...string) bool {
178cb93a386Sopenharmony_ci	if p["cpu_or_gpu"] == "CPU" {
179cb93a386Sopenharmony_ci		if len(re) == 0 {
180cb93a386Sopenharmony_ci			return true
181cb93a386Sopenharmony_ci		}
182cb93a386Sopenharmony_ci		return p.matchPart("cpu_or_gpu_value", re...)
183cb93a386Sopenharmony_ci	}
184cb93a386Sopenharmony_ci	return false
185cb93a386Sopenharmony_ci}
186cb93a386Sopenharmony_ci
187cb93a386Sopenharmony_ci// matchGpu returns true if the task's cpu_or_gpu is "GPU" and the GPU for this task
188cb93a386Sopenharmony_ci// matches any of the given regular expressions. If no regular expressions are
189cb93a386Sopenharmony_ci// provided, gpu returns true if this task runs on GPU.
190cb93a386Sopenharmony_cifunc (p parts) matchGpu(re ...string) bool {
191cb93a386Sopenharmony_ci	if p["cpu_or_gpu"] == "GPU" {
192cb93a386Sopenharmony_ci		if len(re) == 0 {
193cb93a386Sopenharmony_ci			return true
194cb93a386Sopenharmony_ci		}
195cb93a386Sopenharmony_ci		return p.matchPart("cpu_or_gpu_value", re...)
196cb93a386Sopenharmony_ci	}
197cb93a386Sopenharmony_ci	return false
198cb93a386Sopenharmony_ci}
199cb93a386Sopenharmony_ci
200cb93a386Sopenharmony_ci// matchArch returns true if the architecture for this job matches any of the
201cb93a386Sopenharmony_ci// given regular expressions.
202cb93a386Sopenharmony_cifunc (p parts) matchArch(re ...string) bool {
203cb93a386Sopenharmony_ci	return p.matchPart("arch", re...) || p.matchPart("target_arch", re...)
204cb93a386Sopenharmony_ci}
205cb93a386Sopenharmony_ci
206cb93a386Sopenharmony_ci// matchExtraConfig returns true if any of the extra_configs for this job matches
207cb93a386Sopenharmony_ci// any of the given regular expressions. If the extra_config starts with "SK_",
208cb93a386Sopenharmony_ci// it is considered to be a single config.
209cb93a386Sopenharmony_cifunc (p parts) matchExtraConfig(re ...string) bool {
210cb93a386Sopenharmony_ci	if len(re) == 0 {
211cb93a386Sopenharmony_ci		log.Fatal("No regular expressions provided for matchExtraConfig()!")
212cb93a386Sopenharmony_ci	}
213cb93a386Sopenharmony_ci	ec := p["extra_config"]
214cb93a386Sopenharmony_ci	if ec == "" {
215cb93a386Sopenharmony_ci		return false
216cb93a386Sopenharmony_ci	}
217cb93a386Sopenharmony_ci	var cfgs []string
218cb93a386Sopenharmony_ci	if strings.HasPrefix(ec, "SK_") {
219cb93a386Sopenharmony_ci		cfgs = []string{ec}
220cb93a386Sopenharmony_ci	} else {
221cb93a386Sopenharmony_ci		cfgs = strings.Split(ec, "_")
222cb93a386Sopenharmony_ci	}
223cb93a386Sopenharmony_ci	compiled := make([]*regexp.Regexp, 0, len(re))
224cb93a386Sopenharmony_ci	for _, r := range re {
225cb93a386Sopenharmony_ci		compiled = append(compiled, regexp.MustCompile(r))
226cb93a386Sopenharmony_ci	}
227cb93a386Sopenharmony_ci	for _, c := range cfgs {
228cb93a386Sopenharmony_ci		for _, r := range compiled {
229cb93a386Sopenharmony_ci			if r.MatchString(c) {
230cb93a386Sopenharmony_ci				return true
231cb93a386Sopenharmony_ci			}
232cb93a386Sopenharmony_ci		}
233cb93a386Sopenharmony_ci	}
234cb93a386Sopenharmony_ci	return false
235cb93a386Sopenharmony_ci}
236cb93a386Sopenharmony_ci
237cb93a386Sopenharmony_ci// debug returns true if this task runs in debug mode.
238cb93a386Sopenharmony_cifunc (p parts) debug() bool {
239cb93a386Sopenharmony_ci	return p["configuration"] == "Debug"
240cb93a386Sopenharmony_ci}
241cb93a386Sopenharmony_ci
242cb93a386Sopenharmony_ci// release returns true if this task runs in release mode.
243cb93a386Sopenharmony_cifunc (p parts) release() bool {
244cb93a386Sopenharmony_ci	return p["configuration"] == "Release"
245cb93a386Sopenharmony_ci}
246cb93a386Sopenharmony_ci
247cb93a386Sopenharmony_ci// isLinux returns true if the task runs on Linux.
248cb93a386Sopenharmony_cifunc (p parts) isLinux() bool {
249cb93a386Sopenharmony_ci	return p.matchOs("Debian", "Ubuntu")
250cb93a386Sopenharmony_ci}
251cb93a386Sopenharmony_ci
252cb93a386Sopenharmony_ci// TODO(borenet): The below really belongs in its own file, probably next to the
253cb93a386Sopenharmony_ci// builder_name_schema.json file.
254cb93a386Sopenharmony_ci
255cb93a386Sopenharmony_ci// schema is a sub-struct of JobNameSchema.
256cb93a386Sopenharmony_citype schema struct {
257cb93a386Sopenharmony_ci	Keys         []string `json:"keys"`
258cb93a386Sopenharmony_ci	OptionalKeys []string `json:"optional_keys"`
259cb93a386Sopenharmony_ci	RecurseRoles []string `json:"recurse_roles"`
260cb93a386Sopenharmony_ci}
261cb93a386Sopenharmony_ci
262cb93a386Sopenharmony_ci// JobNameSchema is a struct used for (de)constructing Job names in a
263cb93a386Sopenharmony_ci// predictable format.
264cb93a386Sopenharmony_citype JobNameSchema struct {
265cb93a386Sopenharmony_ci	Schema map[string]*schema `json:"builder_name_schema"`
266cb93a386Sopenharmony_ci	Sep    string             `json:"builder_name_sep"`
267cb93a386Sopenharmony_ci}
268cb93a386Sopenharmony_ci
269cb93a386Sopenharmony_ci// NewJobNameSchema returns a JobNameSchema instance based on the given JSON
270cb93a386Sopenharmony_ci// file.
271cb93a386Sopenharmony_cifunc NewJobNameSchema(jsonFile string) (*JobNameSchema, error) {
272cb93a386Sopenharmony_ci	var rv JobNameSchema
273cb93a386Sopenharmony_ci	f, err := os.Open(jsonFile)
274cb93a386Sopenharmony_ci	if err != nil {
275cb93a386Sopenharmony_ci		return nil, err
276cb93a386Sopenharmony_ci	}
277cb93a386Sopenharmony_ci	defer func() {
278cb93a386Sopenharmony_ci		if err := f.Close(); err != nil {
279cb93a386Sopenharmony_ci			log.Println(fmt.Sprintf("Failed to close %s: %s", jsonFile, err))
280cb93a386Sopenharmony_ci		}
281cb93a386Sopenharmony_ci	}()
282cb93a386Sopenharmony_ci	if err := json.NewDecoder(f).Decode(&rv); err != nil {
283cb93a386Sopenharmony_ci		return nil, err
284cb93a386Sopenharmony_ci	}
285cb93a386Sopenharmony_ci	return &rv, nil
286cb93a386Sopenharmony_ci}
287cb93a386Sopenharmony_ci
288cb93a386Sopenharmony_ci// ParseJobName splits the given Job name into its component parts, according
289cb93a386Sopenharmony_ci// to the schema.
290cb93a386Sopenharmony_cifunc (s *JobNameSchema) ParseJobName(n string) (map[string]string, error) {
291cb93a386Sopenharmony_ci	popFront := func(items []string) (string, []string, error) {
292cb93a386Sopenharmony_ci		if len(items) == 0 {
293cb93a386Sopenharmony_ci			return "", nil, fmt.Errorf("Invalid job name: %s (not enough parts)", n)
294cb93a386Sopenharmony_ci		}
295cb93a386Sopenharmony_ci		return items[0], items[1:], nil
296cb93a386Sopenharmony_ci	}
297cb93a386Sopenharmony_ci
298cb93a386Sopenharmony_ci	result := map[string]string{}
299cb93a386Sopenharmony_ci
300cb93a386Sopenharmony_ci	var parse func(int, string, []string) ([]string, error)
301cb93a386Sopenharmony_ci	parse = func(depth int, role string, parts []string) ([]string, error) {
302cb93a386Sopenharmony_ci		s, ok := s.Schema[role]
303cb93a386Sopenharmony_ci		if !ok {
304cb93a386Sopenharmony_ci			return nil, fmt.Errorf("Invalid job name; %q is not a valid role.", role)
305cb93a386Sopenharmony_ci		}
306cb93a386Sopenharmony_ci		if depth == 0 {
307cb93a386Sopenharmony_ci			result["role"] = role
308cb93a386Sopenharmony_ci		} else {
309cb93a386Sopenharmony_ci			result[fmt.Sprintf("sub-role-%d", depth)] = role
310cb93a386Sopenharmony_ci		}
311cb93a386Sopenharmony_ci		var err error
312cb93a386Sopenharmony_ci		for _, key := range s.Keys {
313cb93a386Sopenharmony_ci			var value string
314cb93a386Sopenharmony_ci			value, parts, err = popFront(parts)
315cb93a386Sopenharmony_ci			if err != nil {
316cb93a386Sopenharmony_ci				return nil, err
317cb93a386Sopenharmony_ci			}
318cb93a386Sopenharmony_ci			result[key] = value
319cb93a386Sopenharmony_ci		}
320cb93a386Sopenharmony_ci		for _, subRole := range s.RecurseRoles {
321cb93a386Sopenharmony_ci			if len(parts) > 0 && parts[0] == subRole {
322cb93a386Sopenharmony_ci				parts, err = parse(depth+1, parts[0], parts[1:])
323cb93a386Sopenharmony_ci				if err != nil {
324cb93a386Sopenharmony_ci					return nil, err
325cb93a386Sopenharmony_ci				}
326cb93a386Sopenharmony_ci			}
327cb93a386Sopenharmony_ci		}
328cb93a386Sopenharmony_ci		for _, key := range s.OptionalKeys {
329cb93a386Sopenharmony_ci			if len(parts) > 0 {
330cb93a386Sopenharmony_ci				var value string
331cb93a386Sopenharmony_ci				value, parts, err = popFront(parts)
332cb93a386Sopenharmony_ci				if err != nil {
333cb93a386Sopenharmony_ci					return nil, err
334cb93a386Sopenharmony_ci				}
335cb93a386Sopenharmony_ci				result[key] = value
336cb93a386Sopenharmony_ci			}
337cb93a386Sopenharmony_ci		}
338cb93a386Sopenharmony_ci		if len(parts) > 0 {
339cb93a386Sopenharmony_ci			return nil, fmt.Errorf("Invalid job name: %s (too many parts)", n)
340cb93a386Sopenharmony_ci		}
341cb93a386Sopenharmony_ci		return parts, nil
342cb93a386Sopenharmony_ci	}
343cb93a386Sopenharmony_ci
344cb93a386Sopenharmony_ci	split := strings.Split(n, s.Sep)
345cb93a386Sopenharmony_ci	if len(split) < 2 {
346cb93a386Sopenharmony_ci		return nil, fmt.Errorf("Invalid job name: %s (not enough parts)", n)
347cb93a386Sopenharmony_ci	}
348cb93a386Sopenharmony_ci	role := split[0]
349cb93a386Sopenharmony_ci	split = split[1:]
350cb93a386Sopenharmony_ci	_, err := parse(0, role, split)
351cb93a386Sopenharmony_ci	return result, err
352cb93a386Sopenharmony_ci}
353cb93a386Sopenharmony_ci
354cb93a386Sopenharmony_ci// MakeJobName assembles the given parts of a Job name, according to the schema.
355cb93a386Sopenharmony_cifunc (s *JobNameSchema) MakeJobName(parts map[string]string) (string, error) {
356cb93a386Sopenharmony_ci	rvParts := make([]string, 0, len(parts))
357cb93a386Sopenharmony_ci
358cb93a386Sopenharmony_ci	var process func(int, map[string]string) (map[string]string, error)
359cb93a386Sopenharmony_ci	process = func(depth int, parts map[string]string) (map[string]string, error) {
360cb93a386Sopenharmony_ci		roleKey := "role"
361cb93a386Sopenharmony_ci		if depth != 0 {
362cb93a386Sopenharmony_ci			roleKey = fmt.Sprintf("sub-role-%d", depth)
363cb93a386Sopenharmony_ci		}
364cb93a386Sopenharmony_ci		role, ok := parts[roleKey]
365cb93a386Sopenharmony_ci		if !ok {
366cb93a386Sopenharmony_ci			return nil, fmt.Errorf("Invalid job parts; missing key %q", roleKey)
367cb93a386Sopenharmony_ci		}
368cb93a386Sopenharmony_ci
369cb93a386Sopenharmony_ci		s, ok := s.Schema[role]
370cb93a386Sopenharmony_ci		if !ok {
371cb93a386Sopenharmony_ci			return nil, fmt.Errorf("Invalid job parts; unknown role %q", role)
372cb93a386Sopenharmony_ci		}
373cb93a386Sopenharmony_ci		rvParts = append(rvParts, role)
374cb93a386Sopenharmony_ci		delete(parts, roleKey)
375cb93a386Sopenharmony_ci
376cb93a386Sopenharmony_ci		for _, key := range s.Keys {
377cb93a386Sopenharmony_ci			value, ok := parts[key]
378cb93a386Sopenharmony_ci			if !ok {
379cb93a386Sopenharmony_ci				return nil, fmt.Errorf("Invalid job parts; missing %q", key)
380cb93a386Sopenharmony_ci			}
381cb93a386Sopenharmony_ci			rvParts = append(rvParts, value)
382cb93a386Sopenharmony_ci			delete(parts, key)
383cb93a386Sopenharmony_ci		}
384cb93a386Sopenharmony_ci
385cb93a386Sopenharmony_ci		if len(s.RecurseRoles) > 0 {
386cb93a386Sopenharmony_ci			subRoleKey := fmt.Sprintf("sub-role-%d", depth+1)
387cb93a386Sopenharmony_ci			subRole, ok := parts[subRoleKey]
388cb93a386Sopenharmony_ci			if !ok {
389cb93a386Sopenharmony_ci				return nil, fmt.Errorf("Invalid job parts; missing %q", subRoleKey)
390cb93a386Sopenharmony_ci			}
391cb93a386Sopenharmony_ci			rvParts = append(rvParts, subRole)
392cb93a386Sopenharmony_ci			delete(parts, subRoleKey)
393cb93a386Sopenharmony_ci			found := false
394cb93a386Sopenharmony_ci			for _, recurseRole := range s.RecurseRoles {
395cb93a386Sopenharmony_ci				if recurseRole == subRole {
396cb93a386Sopenharmony_ci					found = true
397cb93a386Sopenharmony_ci					var err error
398cb93a386Sopenharmony_ci					parts, err = process(depth+1, parts)
399cb93a386Sopenharmony_ci					if err != nil {
400cb93a386Sopenharmony_ci						return nil, err
401cb93a386Sopenharmony_ci					}
402cb93a386Sopenharmony_ci					break
403cb93a386Sopenharmony_ci				}
404cb93a386Sopenharmony_ci			}
405cb93a386Sopenharmony_ci			if !found {
406cb93a386Sopenharmony_ci				return nil, fmt.Errorf("Invalid job parts; unknown sub-role %q", subRole)
407cb93a386Sopenharmony_ci			}
408cb93a386Sopenharmony_ci		}
409cb93a386Sopenharmony_ci		for _, key := range s.OptionalKeys {
410cb93a386Sopenharmony_ci			if value, ok := parts[key]; ok {
411cb93a386Sopenharmony_ci				rvParts = append(rvParts, value)
412cb93a386Sopenharmony_ci				delete(parts, key)
413cb93a386Sopenharmony_ci			}
414cb93a386Sopenharmony_ci		}
415cb93a386Sopenharmony_ci		if len(parts) > 0 {
416cb93a386Sopenharmony_ci			return nil, fmt.Errorf("Invalid job parts: too many parts: %v", parts)
417cb93a386Sopenharmony_ci		}
418cb93a386Sopenharmony_ci		return parts, nil
419cb93a386Sopenharmony_ci	}
420cb93a386Sopenharmony_ci
421cb93a386Sopenharmony_ci	// Copy the parts map, so that we can modify at will.
422cb93a386Sopenharmony_ci	partsCpy := make(map[string]string, len(parts))
423cb93a386Sopenharmony_ci	for k, v := range parts {
424cb93a386Sopenharmony_ci		partsCpy[k] = v
425cb93a386Sopenharmony_ci	}
426cb93a386Sopenharmony_ci	if _, err := process(0, partsCpy); err != nil {
427cb93a386Sopenharmony_ci		return "", err
428cb93a386Sopenharmony_ci	}
429cb93a386Sopenharmony_ci	return strings.Join(rvParts, s.Sep), nil
430cb93a386Sopenharmony_ci}
431