xref: /aosp_15_r20/cts/tests/mediapc/requirements/templatefns.go (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1// Copyright (C) 2024 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package templatefns contains functions that are made available in genreqsrc templates.
16package templatefns
17
18import (
19	"fmt"
20	"strings"
21	"text/template"
22	"unicode"
23
24	pb "cts/test/mediapc/requirements/requirements_go_proto"
25)
26
27// Funcs returns a mapping from names of template helper functions to the
28// functions themselves.
29func Funcs() template.FuncMap {
30	// These function are made available in templates by calling their key values, e.g. {{SnakeCase "HelloWorld"}}.
31	return template.FuncMap{
32		// go/keep-sorted start
33		"Dict":             dict,
34		"HasConfigVariant": HasConfigVariant,
35		"KebabCase":        kebabCase,
36		"LowerCamelCase":   lowerCamelCase,
37		"LowerCase":        strings.ToLower,
38		"SafeReqID":        safeReqID,
39		"SafeTestConfigID": safeTestConfigID,
40		"SnakeCase":        snakeCase,
41		"TitleCase":        titleCase,
42		"UpperCamelCase":   upperCamelCase,
43		"UpperCase":        strings.ToUpper,
44		// go/keep-sorted end
45	}
46}
47
48// isDelimiter checks if a rune is some kind of whitespace, '_' or '-'.
49// helper function
50func isDelimiter(r rune) bool {
51	return r == '-' || r == '_' || unicode.IsSpace(r)
52}
53
54func shouldWriteDelimiter(r, prev, next rune) bool {
55	if isDelimiter(prev) {
56		// Don't delimit if we just delimited
57		return false
58	}
59	// Delimit before new uppercase letters and after acronyms
60	caseDelimit := unicode.IsUpper(r) && (unicode.IsLower(prev) || unicode.IsLower(next))
61	// Delimit after digits
62	digitDelimit := !unicode.IsDigit(r) && unicode.IsDigit(prev)
63	return isDelimiter(r) || caseDelimit || digitDelimit
64}
65
66// titleCase converts a string into Title Case.
67func titleCase(s string) string {
68	runes := []rune(s)
69	var out strings.Builder
70	for i, r := range runes {
71		prev, next := ' ', ' '
72		if i > 0 {
73			prev = runes[i-1]
74			if i+1 < len(runes) {
75				next = runes[i+1]
76			}
77		}
78
79		if shouldWriteDelimiter(r, prev, next) {
80			out.WriteRune(' ')
81		}
82
83		if !isDelimiter(r) {
84			// Output all non-delimiters unchanged
85			out.WriteRune(r)
86		}
87	}
88	return strings.Title(out.String())
89}
90
91// snakeCase converts a string into snake_case.
92func snakeCase(s string) string {
93	return strings.ReplaceAll(strings.ToLower(titleCase(s)), " ", "_")
94}
95
96// kebabCase converts a string into kebab-case.
97func kebabCase(s string) string {
98	return strings.ReplaceAll(strings.ToLower(titleCase(s)), " ", "-")
99}
100
101// upperCamelCase converts a string into UpperCamelCase.
102func upperCamelCase(s string) string {
103	return strings.ReplaceAll(titleCase(s), " ", "")
104}
105
106// lowerCamelCase converts a string into lowerCamelCase.
107func lowerCamelCase(s string) string {
108	if len(s) < 2 {
109		return strings.ToLower(s)
110	}
111	runes := []rune(upperCamelCase(s))
112	for i, r := range runes {
113		// Lowercase leading acronyms
114		if i > 1 && unicode.IsLower(r) {
115			return strings.ToLower(string(runes[:i-1])) + string(runes[i-1:])
116		}
117	}
118	return string(unicode.ToLower(runes[0])) + string(runes[1:])
119}
120
121// safeReqID converts a Media Performance Class (MPC) requirement id to a variable name safe string.
122func safeReqID(s string) string {
123	f := func(a, b, c string) string {
124		return strings.Replace(a, b, c, -1)
125	}
126	return "r" + strings.ToLower(f(f(f(s, "/", "__"), ".", "_"), "-", "_"))
127}
128
129// safeTestConfigID converts a group name to a variable name safe string to append onto a requirement id.
130func safeTestConfigID(s string) string {
131	if s == "" {
132		return ""
133	}
134	return "__" + snakeCase(s)
135}
136
137// dict converts a list of key-value pairs into a map.
138// If there is an odd number of values, the last value is nil.
139// The last key is preserved so in the template it can be referenced like {{$myDict.key}}.
140func dict(v ...any) map[string]any {
141	dict := map[string]any{}
142	lenv := len(v)
143	for i := 0; i < lenv; i += 2 {
144		key := toString(v[i])
145		if i+1 >= lenv {
146			dict[key] = nil
147			continue
148		}
149		dict[key] = v[i+1]
150	}
151	return dict
152}
153
154// HasConfigVariant checks if a requirement has a spec for a given test config and variant.
155func HasConfigVariant(r *pb.Requirement, configID string, variantID string) bool {
156	for _, spec := range r.GetSpecs() {
157		if configID == spec.GetTestConfigId() {
158			_, ok := spec.GetVariantSpecs()[variantID]
159			if ok {
160				return true
161			}
162		}
163	}
164	return false
165}
166
167// toString converts a value to a string.
168func toString(v any) string {
169	switch v := v.(type) {
170	case string:
171		return v
172	case []byte:
173		return string(v)
174	case error:
175		return v.Error()
176	case fmt.Stringer:
177		return v.String()
178	default:
179		return fmt.Sprintf("%v", v)
180	}
181}
182