Add "unique" option to validate lists of unique elements

Test: b test //build/bazel/utils:schema_validation_test_suite --test_output=errors
Change-Id: I72fdc5a258ccf245a907e61352b1671b3408564e
diff --git a/utils/schema_validation.bzl b/utils/schema_validation.bzl
index 9599dc5..db093fb 100644
--- a/utils/schema_validation.bzl
+++ b/utils/schema_validation.bzl
@@ -57,6 +57,7 @@
             ],
         },
         "of": {},  # to be filled in later
+        "unique": {"type": "bool"},
         "length": {"or": [
             {"type": "string"},
             {"type": "int"},
@@ -177,6 +178,24 @@
                     ret = "Expected %s, got %s" % (schema["value"], obj)
                     stack.pop()
                     continue
+            if schema.get("unique", False):
+                if ty != "list" and ty != "tuple":
+                    fail("'unique' is only valid for lists or tuples, got: " + ty)
+                l = sorted(obj)
+                done = False
+                for i in range(len(l) - 1):
+                    if type(l[i]) not in ["string", "int", "float", "bool", "NoneType", "bytes"]:
+                        ret = "'unique' only works on lists/tuples of scalar types, got: " + type(l[i])
+                        stack.pop()
+                        done = True
+                        break
+                    if l[i] == l[i + 1]:
+                        ret = "Expected all elements to be unique, but saw '%s' twice" % str(l[i])
+                        stack.pop()
+                        done = True
+                        break
+                if done:
+                    continue
             if "of" in schema:
                 if ty != "list" and ty != "tuple":
                     fail("'of' is only valid for lists or tuples, got: " + ty)
diff --git a/utils/schema_validation_test.bzl b/utils/schema_validation_test.bzl
index 9a5c772..131e643 100644
--- a/utils/schema_validation_test.bzl
+++ b/utils/schema_validation_test.bzl
@@ -298,6 +298,38 @@
     )
     return test_name
 
+def _unique_list_of_strings_success():
+    test_name = "unique_list_of_strings_success"
+    data = ["a", "b"]
+    schema = {
+        "type": "list",
+        "of": {"type": "string"},
+        "unique": True,
+    }
+    message = validate(data, schema, fail_on_error = False)
+    _string_comparison_test(
+        name = test_name,
+        expected = "",
+        actual = message,
+    )
+    return test_name
+
+def _unique_list_of_strings_failure():
+    test_name = "unique_list_of_strings_failure"
+    data = ["a", "b", "a"]
+    schema = {
+        "type": "list",
+        "of": {"type": "string"},
+        "unique": True,
+    }
+    message = validate(data, schema, fail_on_error = False)
+    _string_comparison_test(
+        name = test_name,
+        expected = "Expected all elements to be unique, but saw 'a' twice",
+        actual = message,
+    )
+    return test_name
+
 def _dict_success():
     test_name = "dict_success"
     data = {
@@ -504,6 +536,8 @@
             _list_of_strings_failure(),
             _tuple_of_strings_success(),
             _tuple_of_strings_failure(),
+            _unique_list_of_strings_success(),
+            _unique_list_of_strings_failure(),
             _dict_success(),
             _dict_missing_required_key(),
             _dict_extra_keys(),