xref: /aosp_15_r20/build/bazel/utils/schema_validation.scl (revision 7594170e27e0732bc44b93d1440d87a54b6ffe7c)
1*7594170eSAndroid Build Coastguard Worker# Copyright (C) 2023 The Android Open Source Project
2*7594170eSAndroid Build Coastguard Worker#
3*7594170eSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*7594170eSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*7594170eSAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*7594170eSAndroid Build Coastguard Worker#
7*7594170eSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*7594170eSAndroid Build Coastguard Worker#
9*7594170eSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*7594170eSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*7594170eSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*7594170eSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*7594170eSAndroid Build Coastguard Worker# limitations under the License.
14*7594170eSAndroid Build Coastguard Worker"""
15*7594170eSAndroid Build Coastguard WorkerThis module provides a function for validating starlark data against a schema.
16*7594170eSAndroid Build Coastguard WorkerSee validate() for more information.
17*7594170eSAndroid Build Coastguard Worker"""
18*7594170eSAndroid Build Coastguard Worker
19*7594170eSAndroid Build Coastguard Worker_schema_schema = {
20*7594170eSAndroid Build Coastguard Worker    "type": "dict",
21*7594170eSAndroid Build Coastguard Worker    "optional_keys": {
22*7594170eSAndroid Build Coastguard Worker        "or": {
23*7594170eSAndroid Build Coastguard Worker            "type": "list",
24*7594170eSAndroid Build Coastguard Worker            "length": ">=2",
25*7594170eSAndroid Build Coastguard Worker        },
26*7594170eSAndroid Build Coastguard Worker        "noneable": {"type": "bool"},
27*7594170eSAndroid Build Coastguard Worker        "type": {
28*7594170eSAndroid Build Coastguard Worker            "type": "string",
29*7594170eSAndroid Build Coastguard Worker            "choices": [
30*7594170eSAndroid Build Coastguard Worker                "NoneType",
31*7594170eSAndroid Build Coastguard Worker                "bool",
32*7594170eSAndroid Build Coastguard Worker                "int",
33*7594170eSAndroid Build Coastguard Worker                "float",
34*7594170eSAndroid Build Coastguard Worker                "string",
35*7594170eSAndroid Build Coastguard Worker                "bytes",
36*7594170eSAndroid Build Coastguard Worker                "list",
37*7594170eSAndroid Build Coastguard Worker                "tuple",
38*7594170eSAndroid Build Coastguard Worker                "dict",
39*7594170eSAndroid Build Coastguard Worker                "struct",
40*7594170eSAndroid Build Coastguard Worker            ],
41*7594170eSAndroid Build Coastguard Worker        },
42*7594170eSAndroid Build Coastguard Worker        "choices": {
43*7594170eSAndroid Build Coastguard Worker            "type": "list",
44*7594170eSAndroid Build Coastguard Worker            "of": {
45*7594170eSAndroid Build Coastguard Worker                "or": [
46*7594170eSAndroid Build Coastguard Worker                    {"type": "string"},
47*7594170eSAndroid Build Coastguard Worker                    {"type": "int"},
48*7594170eSAndroid Build Coastguard Worker                    {"type": "float"},
49*7594170eSAndroid Build Coastguard Worker                ],
50*7594170eSAndroid Build Coastguard Worker            },
51*7594170eSAndroid Build Coastguard Worker        },
52*7594170eSAndroid Build Coastguard Worker        "value": {
53*7594170eSAndroid Build Coastguard Worker            "or": [
54*7594170eSAndroid Build Coastguard Worker                {"type": "string"},
55*7594170eSAndroid Build Coastguard Worker                {"type": "int"},
56*7594170eSAndroid Build Coastguard Worker                {"type": "float"},
57*7594170eSAndroid Build Coastguard Worker            ],
58*7594170eSAndroid Build Coastguard Worker        },
59*7594170eSAndroid Build Coastguard Worker        "of": {},  # to be filled in later
60*7594170eSAndroid Build Coastguard Worker        "unique": {"type": "bool"},
61*7594170eSAndroid Build Coastguard Worker        "length": {"or": [
62*7594170eSAndroid Build Coastguard Worker            {"type": "string"},
63*7594170eSAndroid Build Coastguard Worker            {"type": "int"},
64*7594170eSAndroid Build Coastguard Worker        ]},
65*7594170eSAndroid Build Coastguard Worker        "required_keys": {
66*7594170eSAndroid Build Coastguard Worker            "type": "dict",
67*7594170eSAndroid Build Coastguard Worker            "values": {},  # to be filled in later
68*7594170eSAndroid Build Coastguard Worker        },
69*7594170eSAndroid Build Coastguard Worker        "optional_keys": {
70*7594170eSAndroid Build Coastguard Worker            "type": "dict",
71*7594170eSAndroid Build Coastguard Worker            "values": {},  # to be filled in later
72*7594170eSAndroid Build Coastguard Worker        },
73*7594170eSAndroid Build Coastguard Worker        "keys": {},  # to be filled in later
74*7594170eSAndroid Build Coastguard Worker        "values": {},  # to be filled in later
75*7594170eSAndroid Build Coastguard Worker        "required_fields": {
76*7594170eSAndroid Build Coastguard Worker            "type": "dict",
77*7594170eSAndroid Build Coastguard Worker            "keys": {"type": "string"},
78*7594170eSAndroid Build Coastguard Worker            "values": {},  # to be filled in later
79*7594170eSAndroid Build Coastguard Worker        },
80*7594170eSAndroid Build Coastguard Worker        "optional_fields": {
81*7594170eSAndroid Build Coastguard Worker            "type": "dict",
82*7594170eSAndroid Build Coastguard Worker            "keys": {"type": "string"},
83*7594170eSAndroid Build Coastguard Worker            "values": {},  # to be filled in later
84*7594170eSAndroid Build Coastguard Worker        },
85*7594170eSAndroid Build Coastguard Worker    },
86*7594170eSAndroid Build Coastguard Worker}
87*7594170eSAndroid Build Coastguard Worker
88*7594170eSAndroid Build Coastguard Worker_schema_schema["optional_keys"]["of"] = _schema_schema
89*7594170eSAndroid Build Coastguard Worker_schema_schema["optional_keys"]["required_keys"]["values"] = _schema_schema
90*7594170eSAndroid Build Coastguard Worker_schema_schema["optional_keys"]["optional_keys"]["values"] = _schema_schema
91*7594170eSAndroid Build Coastguard Worker_schema_schema["optional_keys"]["keys"] = _schema_schema
92*7594170eSAndroid Build Coastguard Worker_schema_schema["optional_keys"]["values"] = _schema_schema
93*7594170eSAndroid Build Coastguard Worker_schema_schema["optional_keys"]["required_fields"]["values"] = _schema_schema
94*7594170eSAndroid Build Coastguard Worker_schema_schema["optional_keys"]["optional_fields"]["values"] = _schema_schema
95*7594170eSAndroid Build Coastguard Worker
96*7594170eSAndroid Build Coastguard Workerdef _check_len(obj, length):
97*7594170eSAndroid Build Coastguard Worker    if type(length) == "int":
98*7594170eSAndroid Build Coastguard Worker        return len(obj) == length
99*7594170eSAndroid Build Coastguard Worker    if length.startswith("<="):
100*7594170eSAndroid Build Coastguard Worker        return len(obj) <= int(length[2:])
101*7594170eSAndroid Build Coastguard Worker    if length.startswith(">="):
102*7594170eSAndroid Build Coastguard Worker        return len(obj) >= int(length[2:])
103*7594170eSAndroid Build Coastguard Worker    ln = int(length[1:])
104*7594170eSAndroid Build Coastguard Worker    if length[0] == "=":
105*7594170eSAndroid Build Coastguard Worker        return len(obj) == ln
106*7594170eSAndroid Build Coastguard Worker    if length[0] == "<":
107*7594170eSAndroid Build Coastguard Worker        return len(obj) < ln
108*7594170eSAndroid Build Coastguard Worker    if length[0] == ">":
109*7594170eSAndroid Build Coastguard Worker        return len(obj) > ln
110*7594170eSAndroid Build Coastguard Worker    fail("Unexpected length format")
111*7594170eSAndroid Build Coastguard Worker
112*7594170eSAndroid Build Coastguard Workerdef _validate_impl(obj, schema):
113*7594170eSAndroid Build Coastguard Worker    stack = []
114*7594170eSAndroid Build Coastguard Worker
115*7594170eSAndroid Build Coastguard Worker    def newStackFrame(obj, schema):
116*7594170eSAndroid Build Coastguard Worker        stack.append({
117*7594170eSAndroid Build Coastguard Worker            "obj": obj,
118*7594170eSAndroid Build Coastguard Worker            "schema": schema,
119*7594170eSAndroid Build Coastguard Worker            "state": "start",
120*7594170eSAndroid Build Coastguard Worker        })
121*7594170eSAndroid Build Coastguard Worker
122*7594170eSAndroid Build Coastguard Worker    newStackFrame(obj, schema)
123*7594170eSAndroid Build Coastguard Worker    ret = ""
124*7594170eSAndroid Build Coastguard Worker
125*7594170eSAndroid Build Coastguard Worker    # Because bazel doesn't allow infinite loops/recursion, just make a loop
126*7594170eSAndroid Build Coastguard Worker    # with an arbitrarily large number of iterations.
127*7594170eSAndroid Build Coastguard Worker    for _ in range(100000):
128*7594170eSAndroid Build Coastguard Worker        if not stack:
129*7594170eSAndroid Build Coastguard Worker            break
130*7594170eSAndroid Build Coastguard Worker        frame = stack[-1]
131*7594170eSAndroid Build Coastguard Worker        obj = frame["obj"]
132*7594170eSAndroid Build Coastguard Worker        schema = frame["schema"]
133*7594170eSAndroid Build Coastguard Worker        state = frame["state"]
134*7594170eSAndroid Build Coastguard Worker
135*7594170eSAndroid Build Coastguard Worker        if state == "start":
136*7594170eSAndroid Build Coastguard Worker            if len(schema) == 0:
137*7594170eSAndroid Build Coastguard Worker                ret = ""
138*7594170eSAndroid Build Coastguard Worker                stack.pop()
139*7594170eSAndroid Build Coastguard Worker                continue
140*7594170eSAndroid Build Coastguard Worker            if "or" in schema:
141*7594170eSAndroid Build Coastguard Worker                if len(schema) != 1:
142*7594170eSAndroid Build Coastguard Worker                    fail("an 'or' schema must not be accompanied by any other keys")
143*7594170eSAndroid Build Coastguard Worker                frame["i"] = 0
144*7594170eSAndroid Build Coastguard Worker                frame["state"] = "or_loop"
145*7594170eSAndroid Build Coastguard Worker                frame["failures"] = []
146*7594170eSAndroid Build Coastguard Worker                newStackFrame(obj, schema["or"][0])
147*7594170eSAndroid Build Coastguard Worker                continue
148*7594170eSAndroid Build Coastguard Worker            if "type" not in schema:
149*7594170eSAndroid Build Coastguard Worker                fail("a non-empty/non-or schema must have a 'type' key: " + str(schema))
150*7594170eSAndroid Build Coastguard Worker            if schema.get("noneable", False):
151*7594170eSAndroid Build Coastguard Worker                if obj == None:
152*7594170eSAndroid Build Coastguard Worker                    ret = ""
153*7594170eSAndroid Build Coastguard Worker                    stack.pop()
154*7594170eSAndroid Build Coastguard Worker                    continue
155*7594170eSAndroid Build Coastguard Worker            ty = schema["type"]
156*7594170eSAndroid Build Coastguard Worker            if type(obj) != ty:
157*7594170eSAndroid Build Coastguard Worker                ret = "Expected %s, got %s" % (ty, type(obj))
158*7594170eSAndroid Build Coastguard Worker                stack.pop()
159*7594170eSAndroid Build Coastguard Worker                continue
160*7594170eSAndroid Build Coastguard Worker            if "length" in schema:
161*7594170eSAndroid Build Coastguard Worker                if ty not in ["string", "bytes", "list", "tuple"]:
162*7594170eSAndroid Build Coastguard Worker                    fail("'len' is only valid for string, bytes, lists, or tuples, got: " + ty)
163*7594170eSAndroid Build Coastguard Worker                if not _check_len(obj, schema["length"]):
164*7594170eSAndroid Build Coastguard Worker                    ret = "Expected length %s, got %d" % (schema["length"], len(obj))
165*7594170eSAndroid Build Coastguard Worker                    stack.pop()
166*7594170eSAndroid Build Coastguard Worker                    continue
167*7594170eSAndroid Build Coastguard Worker            if "choices" in schema:
168*7594170eSAndroid Build Coastguard Worker                if ty not in ["string", "int", "float"]:
169*7594170eSAndroid Build Coastguard Worker                    fail("'choices' is only valid for string, int, or float, got: " + ty)
170*7594170eSAndroid Build Coastguard Worker                if obj not in schema["choices"]:
171*7594170eSAndroid Build Coastguard Worker                    ret = "Expected one of %s, got %s" % (schema["choices"], obj)
172*7594170eSAndroid Build Coastguard Worker                    stack.pop()
173*7594170eSAndroid Build Coastguard Worker                    continue
174*7594170eSAndroid Build Coastguard Worker            if "value" in schema:
175*7594170eSAndroid Build Coastguard Worker                if ty not in ["string", "int", "float"]:
176*7594170eSAndroid Build Coastguard Worker                    fail("'value' is only valid for string, int, or float, got: " + ty)
177*7594170eSAndroid Build Coastguard Worker                if obj != schema["value"]:
178*7594170eSAndroid Build Coastguard Worker                    ret = "Expected %s, got %s" % (schema["value"], obj)
179*7594170eSAndroid Build Coastguard Worker                    stack.pop()
180*7594170eSAndroid Build Coastguard Worker                    continue
181*7594170eSAndroid Build Coastguard Worker            if schema.get("unique", False):
182*7594170eSAndroid Build Coastguard Worker                if ty != "list" and ty != "tuple":
183*7594170eSAndroid Build Coastguard Worker                    fail("'unique' is only valid for lists or tuples, got: " + ty)
184*7594170eSAndroid Build Coastguard Worker                sorted_list = sorted(obj)
185*7594170eSAndroid Build Coastguard Worker                done = False
186*7594170eSAndroid Build Coastguard Worker                for i in range(len(sorted_list) - 1):
187*7594170eSAndroid Build Coastguard Worker                    if type(sorted_list[i]) not in ["string", "int", "float", "bool", "NoneType", "bytes"]:
188*7594170eSAndroid Build Coastguard Worker                        ret = "'unique' only works on lists/tuples of scalar types, got: " + type(sorted_list[i])
189*7594170eSAndroid Build Coastguard Worker                        stack.pop()
190*7594170eSAndroid Build Coastguard Worker                        done = True
191*7594170eSAndroid Build Coastguard Worker                        break
192*7594170eSAndroid Build Coastguard Worker                    if sorted_list[i] == sorted_list[i + 1]:
193*7594170eSAndroid Build Coastguard Worker                        ret = "Expected all elements to be unique, but saw '%s' twice" % str(sorted_list[i])
194*7594170eSAndroid Build Coastguard Worker                        stack.pop()
195*7594170eSAndroid Build Coastguard Worker                        done = True
196*7594170eSAndroid Build Coastguard Worker                        break
197*7594170eSAndroid Build Coastguard Worker                if done:
198*7594170eSAndroid Build Coastguard Worker                    continue
199*7594170eSAndroid Build Coastguard Worker            if "of" in schema:
200*7594170eSAndroid Build Coastguard Worker                if ty != "list" and ty != "tuple":
201*7594170eSAndroid Build Coastguard Worker                    fail("'of' is only valid for lists or tuples, got: " + ty)
202*7594170eSAndroid Build Coastguard Worker                if obj:
203*7594170eSAndroid Build Coastguard Worker                    frame["i"] = 0
204*7594170eSAndroid Build Coastguard Worker                    frame["state"] = "of_loop"
205*7594170eSAndroid Build Coastguard Worker                    newStackFrame(obj[0], schema["of"])
206*7594170eSAndroid Build Coastguard Worker                    continue
207*7594170eSAndroid Build Coastguard Worker            if ty == "dict":
208*7594170eSAndroid Build Coastguard Worker                if "required_fields" in schema or "optional_fields" in schema:
209*7594170eSAndroid Build Coastguard Worker                    fail("a dict schema can't contain required_fields/optional_fields")
210*7594170eSAndroid Build Coastguard Worker                schema_names_keys = bool(schema.get("required_keys", {})) or bool(schema.get("optional_keys", {}))
211*7594170eSAndroid Build Coastguard Worker                schema_enforces_generic_keys = bool(schema.get("keys", {})) or bool(schema.get("values", {}))
212*7594170eSAndroid Build Coastguard Worker                if schema_names_keys and schema_enforces_generic_keys:
213*7594170eSAndroid Build Coastguard Worker                    fail("Only required_keys/optional_keys or keys/values may be used, but not both")
214*7594170eSAndroid Build Coastguard Worker                if schema_names_keys:
215*7594170eSAndroid Build Coastguard Worker                    all_keys = {}
216*7594170eSAndroid Build Coastguard Worker                    done = False
217*7594170eSAndroid Build Coastguard Worker                    for key, subSchema in schema.get("required_keys", {}).items():
218*7594170eSAndroid Build Coastguard Worker                        if key not in obj:
219*7594170eSAndroid Build Coastguard Worker                            ret = "required key '" + key + "' not found"
220*7594170eSAndroid Build Coastguard Worker                            stack.pop()
221*7594170eSAndroid Build Coastguard Worker                            done = True
222*7594170eSAndroid Build Coastguard Worker                            break
223*7594170eSAndroid Build Coastguard Worker                        all_keys[key] = subSchema
224*7594170eSAndroid Build Coastguard Worker                    if done:
225*7594170eSAndroid Build Coastguard Worker                        continue
226*7594170eSAndroid Build Coastguard Worker                    for key, subSchema in schema.get("optional_keys", {}).items():
227*7594170eSAndroid Build Coastguard Worker                        if key in all_keys:
228*7594170eSAndroid Build Coastguard Worker                            fail("A key cannot be both required and optional: " + key)
229*7594170eSAndroid Build Coastguard Worker                        if key in obj:
230*7594170eSAndroid Build Coastguard Worker                            all_keys[key] = subSchema
231*7594170eSAndroid Build Coastguard Worker                    extra_keys = [
232*7594170eSAndroid Build Coastguard Worker                        key
233*7594170eSAndroid Build Coastguard Worker                        for key in obj.keys()
234*7594170eSAndroid Build Coastguard Worker                        if key not in all_keys
235*7594170eSAndroid Build Coastguard Worker                    ]
236*7594170eSAndroid Build Coastguard Worker                    if extra_keys:
237*7594170eSAndroid Build Coastguard Worker                        ret = "keys " + str(extra_keys) + " not allowed, valid keys: " + str(all_keys.keys())
238*7594170eSAndroid Build Coastguard Worker                        stack.pop()
239*7594170eSAndroid Build Coastguard Worker                        continue
240*7594170eSAndroid Build Coastguard Worker                    if all_keys:
241*7594170eSAndroid Build Coastguard Worker                        frame["all_keys"] = all_keys.items()
242*7594170eSAndroid Build Coastguard Worker                        frame["i"] = 0
243*7594170eSAndroid Build Coastguard Worker                        frame["state"] = "dict_individual_keys_loop"
244*7594170eSAndroid Build Coastguard Worker                        k, v = frame["all_keys"][0]
245*7594170eSAndroid Build Coastguard Worker                        newStackFrame(obj[k], v)
246*7594170eSAndroid Build Coastguard Worker                        continue
247*7594170eSAndroid Build Coastguard Worker                elif schema_enforces_generic_keys:
248*7594170eSAndroid Build Coastguard Worker                    frame["items"] = obj.items()
249*7594170eSAndroid Build Coastguard Worker                    if frame["items"]:
250*7594170eSAndroid Build Coastguard Worker                        frame["i"] = 0
251*7594170eSAndroid Build Coastguard Worker                        frame["state"] = "dict_generic_keys_loop"
252*7594170eSAndroid Build Coastguard Worker                        frame["checking_key"] = True
253*7594170eSAndroid Build Coastguard Worker                        continue
254*7594170eSAndroid Build Coastguard Worker            if ty == "struct":
255*7594170eSAndroid Build Coastguard Worker                if "required_keys" in schema or "optional_keys" in schema or "keys" in schema or "values" in schema:
256*7594170eSAndroid Build Coastguard Worker                    fail("a struct schema can't contain required_keys/optional_keys/keys/values")
257*7594170eSAndroid Build Coastguard Worker                all_fields = {}
258*7594170eSAndroid Build Coastguard Worker                original_fields = {f: True for f in dir(obj)}
259*7594170eSAndroid Build Coastguard Worker                done = False
260*7594170eSAndroid Build Coastguard Worker                for field, subSchema in schema.get("required_fields", {}).items():
261*7594170eSAndroid Build Coastguard Worker                    if field not in original_fields:
262*7594170eSAndroid Build Coastguard Worker                        ret = "required field '" + field + "' not found"
263*7594170eSAndroid Build Coastguard Worker                        stack.pop()
264*7594170eSAndroid Build Coastguard Worker                        done = True
265*7594170eSAndroid Build Coastguard Worker                        break
266*7594170eSAndroid Build Coastguard Worker                    all_fields[field] = subSchema
267*7594170eSAndroid Build Coastguard Worker                if done:
268*7594170eSAndroid Build Coastguard Worker                    continue
269*7594170eSAndroid Build Coastguard Worker                for field, subSchema in schema.get("optional_fields", {}).items():
270*7594170eSAndroid Build Coastguard Worker                    if field in all_fields:
271*7594170eSAndroid Build Coastguard Worker                        fail("A field cannot be both required and optional: " + key)
272*7594170eSAndroid Build Coastguard Worker                    if field in original_fields:
273*7594170eSAndroid Build Coastguard Worker                        all_fields[field] = subSchema
274*7594170eSAndroid Build Coastguard Worker                for field in all_fields:
275*7594170eSAndroid Build Coastguard Worker                    if field == "to_json" or field == "to_proto":
276*7594170eSAndroid Build Coastguard Worker                        fail("don't use deprecated fields to_json or to_proto")
277*7594170eSAndroid Build Coastguard Worker                extra_fields = [
278*7594170eSAndroid Build Coastguard Worker                    field
279*7594170eSAndroid Build Coastguard Worker                    for field in original_fields.keys()
280*7594170eSAndroid Build Coastguard Worker                    if field not in all_fields and field != "to_json" and field != "to_proto"
281*7594170eSAndroid Build Coastguard Worker                ]
282*7594170eSAndroid Build Coastguard Worker                if extra_fields:
283*7594170eSAndroid Build Coastguard Worker                    ret = "fields " + str(extra_fields) + " not allowed, valid keys: " + str(all_fields.keys())
284*7594170eSAndroid Build Coastguard Worker                    stack.pop()
285*7594170eSAndroid Build Coastguard Worker                    continue
286*7594170eSAndroid Build Coastguard Worker                if all_fields:
287*7594170eSAndroid Build Coastguard Worker                    frame["all_fields"] = all_fields.items()
288*7594170eSAndroid Build Coastguard Worker                    frame["i"] = 0
289*7594170eSAndroid Build Coastguard Worker                    frame["state"] = "struct_individual_fields_loop"
290*7594170eSAndroid Build Coastguard Worker                    k, v = frame["all_fields"][0]
291*7594170eSAndroid Build Coastguard Worker                    newStackFrame(getattr(obj, k), v)
292*7594170eSAndroid Build Coastguard Worker                    continue
293*7594170eSAndroid Build Coastguard Worker        elif state == "or_loop":
294*7594170eSAndroid Build Coastguard Worker            if ret != "":
295*7594170eSAndroid Build Coastguard Worker                frame["failures"].append("  " + ret)
296*7594170eSAndroid Build Coastguard Worker                frame["i"] += 1
297*7594170eSAndroid Build Coastguard Worker                if frame["i"] >= len(schema["or"]):
298*7594170eSAndroid Build Coastguard Worker                    ret = "did not match any schemas in 'or' list, errors:\n" + "\n".join(frame["failures"])
299*7594170eSAndroid Build Coastguard Worker                    stack.pop()
300*7594170eSAndroid Build Coastguard Worker                    continue
301*7594170eSAndroid Build Coastguard Worker                else:
302*7594170eSAndroid Build Coastguard Worker                    newStackFrame(obj, schema["or"][frame["i"]])
303*7594170eSAndroid Build Coastguard Worker                    continue
304*7594170eSAndroid Build Coastguard Worker        elif state == "of_loop":
305*7594170eSAndroid Build Coastguard Worker            frame["i"] += 1
306*7594170eSAndroid Build Coastguard Worker            if ret != "" or frame["i"] >= len(obj):
307*7594170eSAndroid Build Coastguard Worker                stack.pop()
308*7594170eSAndroid Build Coastguard Worker                continue
309*7594170eSAndroid Build Coastguard Worker            newStackFrame(obj[frame["i"]], schema["of"])
310*7594170eSAndroid Build Coastguard Worker            continue
311*7594170eSAndroid Build Coastguard Worker        elif state == "dict_individual_keys_loop":
312*7594170eSAndroid Build Coastguard Worker            frame["i"] += 1
313*7594170eSAndroid Build Coastguard Worker            if ret != "" or frame["i"] >= len(frame["all_keys"]):
314*7594170eSAndroid Build Coastguard Worker                stack.pop()
315*7594170eSAndroid Build Coastguard Worker                continue
316*7594170eSAndroid Build Coastguard Worker            k, v = frame["all_keys"][frame["i"]]
317*7594170eSAndroid Build Coastguard Worker            newStackFrame(obj[k], v)
318*7594170eSAndroid Build Coastguard Worker            continue
319*7594170eSAndroid Build Coastguard Worker        elif state == "dict_generic_keys_loop":
320*7594170eSAndroid Build Coastguard Worker            if ret != "" or frame["i"] >= len(frame["items"]):
321*7594170eSAndroid Build Coastguard Worker                stack.pop()
322*7594170eSAndroid Build Coastguard Worker                continue
323*7594170eSAndroid Build Coastguard Worker            k, v = frame["items"][frame["i"]]
324*7594170eSAndroid Build Coastguard Worker            if frame["checking_key"]:
325*7594170eSAndroid Build Coastguard Worker                frame["checking_key"] = False
326*7594170eSAndroid Build Coastguard Worker                newStackFrame(k, schema.get("keys", {}))
327*7594170eSAndroid Build Coastguard Worker                continue
328*7594170eSAndroid Build Coastguard Worker            else:
329*7594170eSAndroid Build Coastguard Worker                frame["checking_key"] = True
330*7594170eSAndroid Build Coastguard Worker                frame["i"] += 1
331*7594170eSAndroid Build Coastguard Worker                newStackFrame(v, schema.get("values", {}))
332*7594170eSAndroid Build Coastguard Worker                continue
333*7594170eSAndroid Build Coastguard Worker        elif state == "struct_individual_fields_loop":
334*7594170eSAndroid Build Coastguard Worker            frame["i"] += 1
335*7594170eSAndroid Build Coastguard Worker            if ret != "" or frame["i"] >= len(frame["all_fields"]):
336*7594170eSAndroid Build Coastguard Worker                stack.pop()
337*7594170eSAndroid Build Coastguard Worker                continue
338*7594170eSAndroid Build Coastguard Worker            k, v = frame["all_fields"][frame["i"]]
339*7594170eSAndroid Build Coastguard Worker            newStackFrame(getattr(obj, k), v)
340*7594170eSAndroid Build Coastguard Worker            continue
341*7594170eSAndroid Build Coastguard Worker
342*7594170eSAndroid Build Coastguard Worker        # by default return success
343*7594170eSAndroid Build Coastguard Worker        ret = ""
344*7594170eSAndroid Build Coastguard Worker        stack.pop()
345*7594170eSAndroid Build Coastguard Worker    if stack:
346*7594170eSAndroid Build Coastguard Worker        fail("Schema validation took too many iterations")
347*7594170eSAndroid Build Coastguard Worker    return ret
348*7594170eSAndroid Build Coastguard Worker
349*7594170eSAndroid Build Coastguard Workerdef validate(obj, schema, *, validate_schema = True, fail_on_error = True):
350*7594170eSAndroid Build Coastguard Worker    """Validates the given starlark object against a schema.
351*7594170eSAndroid Build Coastguard Worker
352*7594170eSAndroid Build Coastguard Worker    A schema is a dictionary that describes the format of obj. Currently,
353*7594170eSAndroid Build Coastguard Worker    recursive objects cannot be validated because there's no cycle detection.
354*7594170eSAndroid Build Coastguard Worker
355*7594170eSAndroid Build Coastguard Worker    An empty dictionary describes "any object".
356*7594170eSAndroid Build Coastguard Worker
357*7594170eSAndroid Build Coastguard Worker    A dictionary with an "or" key must not have any other keys, and its
358*7594170eSAndroid Build Coastguard Worker    value is a list of other schema objects. If any of those schema objects
359*7594170eSAndroid Build Coastguard Worker    match, the "or" schema is considered a success.
360*7594170eSAndroid Build Coastguard Worker
361*7594170eSAndroid Build Coastguard Worker    Any schemas that are not empty or "or" schemas must have a "type" key.
362*7594170eSAndroid Build Coastguard Worker    This type must match the result of type(obj).
363*7594170eSAndroid Build Coastguard Worker
364*7594170eSAndroid Build Coastguard Worker    The "noneable" key can be set to true to act as an alias for:
365*7594170eSAndroid Build Coastguard Worker    `{"or": [{"type": "NoneType"}, ...the rest of the schema...]}`
366*7594170eSAndroid Build Coastguard Worker
367*7594170eSAndroid Build Coastguard Worker    The "value" key contains a value that must match the object exactly.
368*7594170eSAndroid Build Coastguard Worker    Only applies to strings, ints, and floats.
369*7594170eSAndroid Build Coastguard Worker
370*7594170eSAndroid Build Coastguard Worker    The "choices" key is a list of values that the object could match.
371*7594170eSAndroid Build Coastguard Worker    If the object is equal to any one of them then validation succeeds.
372*7594170eSAndroid Build Coastguard Worker
373*7594170eSAndroid Build Coastguard Worker    The "length" key applies to strings, bytes, lists, or tuples.
374*7594170eSAndroid Build Coastguard Worker    Its value can either be an integer length that the object must have,
375*7594170eSAndroid Build Coastguard Worker    or a string in that starts with <, >, <=, >=, or =, followed by a number.
376*7594170eSAndroid Build Coastguard Worker
377*7594170eSAndroid Build Coastguard Worker    The "of" key is a schema to match against the elements of a list/tuple.
378*7594170eSAndroid Build Coastguard Worker
379*7594170eSAndroid Build Coastguard Worker    Dictionaries and structs have "required_keys"/"required_fields" and
380*7594170eSAndroid Build Coastguard Worker    "optional_keys"/"optional_fields". (keys for dictionaries, fields for
381*7594170eSAndroid Build Coastguard Worker    structs). The value of each of these fields is a dictionary mapping from
382*7594170eSAndroid Build Coastguard Worker    the key/field value to a schema object to validate the value of the
383*7594170eSAndroid Build Coastguard Worker    key/field. Any keys/fields that are not listed in the schema will cause
384*7594170eSAndroid Build Coastguard Worker    the validation to fail. Any keys/fields in the required_ schemas must
385*7594170eSAndroid Build Coastguard Worker    be present in the input object.
386*7594170eSAndroid Build Coastguard Worker
387*7594170eSAndroid Build Coastguard Worker    Dictionaries have two additional fields over structs, "keys" and "values".
388*7594170eSAndroid Build Coastguard Worker    These fields cannot be mixed with required_keys/optional_keys. They provide
389*7594170eSAndroid Build Coastguard Worker    a single schema object each to apply to all the keys/values in the dictionary.
390*7594170eSAndroid Build Coastguard Worker
391*7594170eSAndroid Build Coastguard Worker    Args:
392*7594170eSAndroid Build Coastguard Worker        obj: The object to be validated against the schema
393*7594170eSAndroid Build Coastguard Worker        schema: The schema. (See above)
394*7594170eSAndroid Build Coastguard Worker        validate_schema: Also check that the schema itself is valid. This
395*7594170eSAndroid Build Coastguard Worker            can be disabled for performance. However, some of the checks
396*7594170eSAndroid Build Coastguard Worker            about the schema are hardcoded and cannot be disabled.
397*7594170eSAndroid Build Coastguard Worker        fail_on_error: If this function should fail() when the object doesn't
398*7594170eSAndroid Build Coastguard Worker            conform to the schema. Note that if the schema itself is invalid,
399*7594170eSAndroid Build Coastguard Worker            validate() fails regardless of the value of this argument.
400*7594170eSAndroid Build Coastguard Worker    Returns:
401*7594170eSAndroid Build Coastguard Worker        If fail_on_error is True, validate() doesn't return anything.
402*7594170eSAndroid Build Coastguard Worker        If fail_on_error is False, validate() returns a string that describes
403*7594170eSAndroid Build Coastguard Worker        the reason why the object doesn't match the schema, or an empty string
404*7594170eSAndroid Build Coastguard Worker        if it does match.
405*7594170eSAndroid Build Coastguard Worker    """
406*7594170eSAndroid Build Coastguard Worker    if validate_schema:
407*7594170eSAndroid Build Coastguard Worker        schema_validation_results = _validate_impl(schema, _schema_schema)
408*7594170eSAndroid Build Coastguard Worker        if schema_validation_results:
409*7594170eSAndroid Build Coastguard Worker            fail("Schema is invalid: " + schema_validation_results)
410*7594170eSAndroid Build Coastguard Worker    result = _validate_impl(obj, schema)
411*7594170eSAndroid Build Coastguard Worker    if not fail_on_error:
412*7594170eSAndroid Build Coastguard Worker        return result
413*7594170eSAndroid Build Coastguard Worker    if result:
414*7594170eSAndroid Build Coastguard Worker        fail(result)
415*7594170eSAndroid Build Coastguard Worker    return None
416