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