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