1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2021 The Chromium Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li""" 6*9c5db199SXin LiFunctions to load config variables from JSON with transformation. 7*9c5db199SXin Li 8*9c5db199SXin Li* The config is a key-value dictionary. 9*9c5db199SXin Li* If the value is a list, then the list constitutes a list of conditions 10*9c5db199SXin Li to check. 11*9c5db199SXin Li* A condition is a key-value dictionary where the key is an external variable 12*9c5db199SXin Li name and the value is a case-insensitive regexp to match. If multiple 13*9c5db199SXin Li variables used, they all must match for the condition to succeed. 14*9c5db199SXin Li* A special key "value" is the value to assign if condition succeeds. 15*9c5db199SXin Li* The first matching condition wins. 16*9c5db199SXin Li* Condition with zero external vars always succeeds - it should be the last in 17*9c5db199SXin Li the list as a last resort case. 18*9c5db199SXin Li* If none of conditions match, it's an error. 19*9c5db199SXin Li* The value, in turn, can be a nested list of conditions. 20*9c5db199SXin Li* If the value is a boolean, the condition checks for the presence or absence 21*9c5db199SXin Li of an external variable. 22*9c5db199SXin Li 23*9c5db199SXin LiExample: 24*9c5db199SXin Li Python source: 25*9c5db199SXin Li config = TransformJsonFile( 26*9c5db199SXin Li "config.json", 27*9c5db199SXin Li extvars={ 28*9c5db199SXin Li "board": "board1", 29*9c5db199SXin Li "model": "model1", 30*9c5db199SXin Li }) 31*9c5db199SXin Li # config -> { 32*9c5db199SXin Li # "cuj_username": "user", 33*9c5db199SXin Li # "private_key": "SECRET", 34*9c5db199SXin Li # "some_var": "val for board1", 35*9c5db199SXin Li # "some_var2": "default val2", 36*9c5db199SXin Li # } 37*9c5db199SXin Li 38*9c5db199SXin Li config = TransformJsonFile( 39*9c5db199SXin Li "config.json", 40*9c5db199SXin Li extvars={ 41*9c5db199SXin Li "board": "board2", 42*9c5db199SXin Li "model": "model2", 43*9c5db199SXin Li }) 44*9c5db199SXin Li # config -> { 45*9c5db199SXin Li # "cuj_username": "user", 46*9c5db199SXin Li # "private_key": "SECRET", 47*9c5db199SXin Li # "some_var": "val for board2", 48*9c5db199SXin Li # "some_var2": "val2 for board2 model2", 49*9c5db199SXin Li # } 50*9c5db199SXin Li 51*9c5db199SXin Li config.json: 52*9c5db199SXin Li { 53*9c5db199SXin Li "cuj_username": "user", 54*9c5db199SXin Li "private_key": "SECRET", 55*9c5db199SXin Li "some_var": [ 56*9c5db199SXin Li { 57*9c5db199SXin Li "board": "board1.*", 58*9c5db199SXin Li "value": "val for board1", 59*9c5db199SXin Li }, 60*9c5db199SXin Li { 61*9c5db199SXin Li "board": "board2.*", 62*9c5db199SXin Li "value": "val for board2", 63*9c5db199SXin Li }, 64*9c5db199SXin Li { 65*9c5db199SXin Li "value": "default val", 66*9c5db199SXin Li } 67*9c5db199SXin Li ], 68*9c5db199SXin Li "some_var2": [ 69*9c5db199SXin Li { 70*9c5db199SXin Li "board": "board2.*", 71*9c5db199SXin Li "model": "model2.*", 72*9c5db199SXin Li "value": "val2 for board2 model2", 73*9c5db199SXin Li }, 74*9c5db199SXin Li { 75*9c5db199SXin Li "value": "default val2", 76*9c5db199SXin Li } 77*9c5db199SXin Li ], 78*9c5db199SXin Li } 79*9c5db199SXin Li 80*9c5db199SXin LiSee more examples in config_vars_unittest.py 81*9c5db199SXin Li 82*9c5db199SXin Li""" 83*9c5db199SXin Li 84*9c5db199SXin Li# Lint as: python2, python3 85*9c5db199SXin Li# pylint: disable=missing-docstring,bad-indentation 86*9c5db199SXin Lifrom __future__ import absolute_import 87*9c5db199SXin Lifrom __future__ import division 88*9c5db199SXin Lifrom __future__ import print_function 89*9c5db199SXin Li 90*9c5db199SXin Liimport json 91*9c5db199SXin Liimport logging 92*9c5db199SXin Liimport re 93*9c5db199SXin Li 94*9c5db199SXin Litry: 95*9c5db199SXin Li unicode 96*9c5db199SXin Liexcept NameError: 97*9c5db199SXin Li unicode = str 98*9c5db199SXin Li 99*9c5db199SXin LiVERBOSE = False 100*9c5db199SXin Li 101*9c5db199SXin Li 102*9c5db199SXin Liclass ConfigTransformError(ValueError): 103*9c5db199SXin Li pass 104*9c5db199SXin Li 105*9c5db199SXin Li 106*9c5db199SXin Lidef TransformConfig(data, extvars): 107*9c5db199SXin Li """Transforms data loaded from JSON to config variables. 108*9c5db199SXin Li 109*9c5db199SXin Li Args: 110*9c5db199SXin Li data (dict): input data dictionary from JSON parser 111*9c5db199SXin Li extvars (dict): external variables dictionary 112*9c5db199SXin Li 113*9c5db199SXin Li Returns: 114*9c5db199SXin Li dict: config variables 115*9c5db199SXin Li 116*9c5db199SXin Li Raises: 117*9c5db199SXin Li ConfigTransformError: transformation error 118*9c5db199SXin Li 're' errors 119*9c5db199SXin Li """ 120*9c5db199SXin Li if not isinstance(data, dict): 121*9c5db199SXin Li _Error('Top level configuration object must be a dictionary but got ' + 122*9c5db199SXin Li data.__class__.__name__) 123*9c5db199SXin Li 124*9c5db199SXin Li return {key: _GetVal(key, val, extvars) for key, val in data.items()} 125*9c5db199SXin Li 126*9c5db199SXin Li 127*9c5db199SXin Lidef TransformJsonText(text, extvars): 128*9c5db199SXin Li """Transforms JSON text to config variables. 129*9c5db199SXin Li 130*9c5db199SXin Li Args: 131*9c5db199SXin Li text (str): JSON input 132*9c5db199SXin Li extvars (dict): external variables dictionary 133*9c5db199SXin Li 134*9c5db199SXin Li Returns: 135*9c5db199SXin Li dict: config variables 136*9c5db199SXin Li 137*9c5db199SXin Li Raises: 138*9c5db199SXin Li ConfigTransformError: transformation error 139*9c5db199SXin Li 're' errors 140*9c5db199SXin Li 'json' errors 141*9c5db199SXin Li """ 142*9c5db199SXin Li data = json.loads(text) 143*9c5db199SXin Li return TransformConfig(data, extvars) 144*9c5db199SXin Li 145*9c5db199SXin Li 146*9c5db199SXin Lidef TransformJsonFile(file_name, extvars): 147*9c5db199SXin Li """Transforms JSON file to config variables. 148*9c5db199SXin Li 149*9c5db199SXin Li Args: 150*9c5db199SXin Li file_name (str): JSON file name 151*9c5db199SXin Li extvars (dict): external variables dictionary 152*9c5db199SXin Li 153*9c5db199SXin Li Returns: 154*9c5db199SXin Li dict: config variables 155*9c5db199SXin Li 156*9c5db199SXin Li Raises: 157*9c5db199SXin Li ConfigTransformError: transformation error 158*9c5db199SXin Li 're' errors 159*9c5db199SXin Li 'json' errors 160*9c5db199SXin Li IO errors 161*9c5db199SXin Li """ 162*9c5db199SXin Li with open(file_name, 'r') as f: 163*9c5db199SXin Li data = json.load(f) 164*9c5db199SXin Li return TransformConfig(data, extvars) 165*9c5db199SXin Li 166*9c5db199SXin Li 167*9c5db199SXin Lidef _GetVal(key, val, extvars): 168*9c5db199SXin Li """Calculates and returns the config variable value. 169*9c5db199SXin Li 170*9c5db199SXin Li Args: 171*9c5db199SXin Li key (str): key for error reporting 172*9c5db199SXin Li val (str | list): variable value or conditions list 173*9c5db199SXin Li extvars (dict): external variables dictionary 174*9c5db199SXin Li 175*9c5db199SXin Li Returns: 176*9c5db199SXin Li str: resolved variable value 177*9c5db199SXin Li 178*9c5db199SXin Li Raises: 179*9c5db199SXin Li ConfigTransformError: transformation error 180*9c5db199SXin Li """ 181*9c5db199SXin Li if (isinstance(val, str) or isinstance(val, unicode) 182*9c5db199SXin Li or isinstance(val, int) or isinstance(val, float)): 183*9c5db199SXin Li return val 184*9c5db199SXin Li 185*9c5db199SXin Li if not isinstance(val, list): 186*9c5db199SXin Li _Error('Conditions must be an array but got ' + val.__class__.__name__, 187*9c5db199SXin Li json.dumps(val), key) 188*9c5db199SXin Li 189*9c5db199SXin Li for cond in val: 190*9c5db199SXin Li if not isinstance(cond, dict): 191*9c5db199SXin Li _Error( 192*9c5db199SXin Li 'Condition must be a dictionary but got ' + 193*9c5db199SXin Li cond.__class__.__name__, json.dumps(cond), key) 194*9c5db199SXin Li if 'value' not in cond: 195*9c5db199SXin Li _Error('Missing mandatory "value" key from condition', 196*9c5db199SXin Li json.dumps(cond), key) 197*9c5db199SXin Li 198*9c5db199SXin Li for cond_key, cond_val in cond.items(): 199*9c5db199SXin Li if cond_key == 'value': 200*9c5db199SXin Li continue 201*9c5db199SXin Li 202*9c5db199SXin Li if isinstance(cond_val, bool): 203*9c5db199SXin Li # Boolean value -> check if variable exists 204*9c5db199SXin Li if (cond_key in extvars) == cond_val: 205*9c5db199SXin Li continue 206*9c5db199SXin Li else: 207*9c5db199SXin Li break 208*9c5db199SXin Li 209*9c5db199SXin Li if cond_key not in extvars: 210*9c5db199SXin Li logging.warning('Unknown external var: %s', cond_key) 211*9c5db199SXin Li break 212*9c5db199SXin Li if re.search(cond_val, extvars[cond_key], re.I) is None: 213*9c5db199SXin Li break 214*9c5db199SXin Li else: 215*9c5db199SXin Li return _GetVal(key, cond['value'], extvars) 216*9c5db199SXin Li 217*9c5db199SXin Li _Error('Condition did not match any external vars', 218*9c5db199SXin Li json.dumps(val, indent=4) + '\nvars: ' + extvars.__str__(), key) 219*9c5db199SXin Li 220*9c5db199SXin Li 221*9c5db199SXin Lidef _Error(text, extra='', key=''): 222*9c5db199SXin Li """Reports and raises an error. 223*9c5db199SXin Li 224*9c5db199SXin Li Args: 225*9c5db199SXin Li text (str): Error text 226*9c5db199SXin Li extra (str, optional): potentially sensitive error text for verbose output 227*9c5db199SXin Li key (str): key for error reporting or empty string if none 228*9c5db199SXin Li 229*9c5db199SXin Li Raises: 230*9c5db199SXin Li ConfigTransformError: error 231*9c5db199SXin Li """ 232*9c5db199SXin Li if key: 233*9c5db199SXin Li text = key + ': ' + text 234*9c5db199SXin Li if VERBOSE and extra: 235*9c5db199SXin Li text += ':\n' + extra 236*9c5db199SXin Li logging.error('%s', text) 237*9c5db199SXin Li raise ConfigTransformError(text) 238