| # Lint as: python2, python3 |
| # Copyright (c) 2021 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """ |
| Functions to load config variables from JSON with transformation. |
| |
| * The config is a key-value dictionary. |
| * If the value is a list, then the list constitutes a list of conditions |
| to check. |
| * A condition is a key-value dictionary where the key is an external variable |
| name and the value is a case-insensitive regexp to match. If multiple |
| variables used, they all must match for the condition to succeed. |
| * A special key "value" is the value to assign if condition succeeds. |
| * The first matching condition wins. |
| * Condition with zero external vars always succeeds - it should be the last in |
| the list as a last resort case. |
| * If none of conditions match, it's an error. |
| * The value, in turn, can be a nested list of conditions. |
| * If the value is a boolean, the condition checks for the presence or absence |
| of an external variable. |
| |
| Example: |
| Python source: |
| config = TransformJsonFile( |
| "config.json", |
| extvars={ |
| "board": "board1", |
| "model": "model1", |
| }) |
| # config -> { |
| # "cuj_username": "user", |
| # "private_key": "SECRET", |
| # "some_var": "val for board1", |
| # "some_var2": "default val2", |
| # } |
| |
| config = TransformJsonFile( |
| "config.json", |
| extvars={ |
| "board": "board2", |
| "model": "model2", |
| }) |
| # config -> { |
| # "cuj_username": "user", |
| # "private_key": "SECRET", |
| # "some_var": "val for board2", |
| # "some_var2": "val2 for board2 model2", |
| # } |
| |
| config.json: |
| { |
| "cuj_username": "user", |
| "private_key": "SECRET", |
| "some_var": [ |
| { |
| "board": "board1.*", |
| "value": "val for board1", |
| }, |
| { |
| "board": "board2.*", |
| "value": "val for board2", |
| }, |
| { |
| "value": "default val", |
| } |
| ], |
| "some_var2": [ |
| { |
| "board": "board2.*", |
| "model": "model2.*", |
| "value": "val2 for board2 model2", |
| }, |
| { |
| "value": "default val2", |
| } |
| ], |
| } |
| |
| See more examples in config_vars_unittest.py |
| |
| """ |
| |
| # Lint as: python2, python3 |
| # pylint: disable=missing-docstring,bad-indentation |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import json |
| import logging |
| import re |
| |
| try: |
| unicode |
| except NameError: |
| unicode = str |
| |
| VERBOSE = False |
| |
| |
| class ConfigTransformError(ValueError): |
| pass |
| |
| |
| def TransformConfig(data, extvars): |
| """Transforms data loaded from JSON to config variables. |
| |
| Args: |
| data (dict): input data dictionary from JSON parser |
| extvars (dict): external variables dictionary |
| |
| Returns: |
| dict: config variables |
| |
| Raises: |
| ConfigTransformError: transformation error |
| 're' errors |
| """ |
| if not isinstance(data, dict): |
| _Error('Top level configuration object must be a dictionary but got ' + |
| data.__class__.__name__) |
| |
| return {key: _GetVal(key, val, extvars) for key, val in data.items()} |
| |
| |
| def TransformJsonText(text, extvars): |
| """Transforms JSON text to config variables. |
| |
| Args: |
| text (str): JSON input |
| extvars (dict): external variables dictionary |
| |
| Returns: |
| dict: config variables |
| |
| Raises: |
| ConfigTransformError: transformation error |
| 're' errors |
| 'json' errors |
| """ |
| data = json.loads(text) |
| return TransformConfig(data, extvars) |
| |
| |
| def TransformJsonFile(file_name, extvars): |
| """Transforms JSON file to config variables. |
| |
| Args: |
| file_name (str): JSON file name |
| extvars (dict): external variables dictionary |
| |
| Returns: |
| dict: config variables |
| |
| Raises: |
| ConfigTransformError: transformation error |
| 're' errors |
| 'json' errors |
| IO errors |
| """ |
| with open(file_name, 'r') as f: |
| data = json.load(f) |
| return TransformConfig(data, extvars) |
| |
| |
| def _GetVal(key, val, extvars): |
| """Calculates and returns the config variable value. |
| |
| Args: |
| key (str): key for error reporting |
| val (str | list): variable value or conditions list |
| extvars (dict): external variables dictionary |
| |
| Returns: |
| str: resolved variable value |
| |
| Raises: |
| ConfigTransformError: transformation error |
| """ |
| if (isinstance(val, str) or isinstance(val, unicode) |
| or isinstance(val, int) or isinstance(val, float)): |
| return val |
| |
| if not isinstance(val, list): |
| _Error('Conditions must be an array but got ' + val.__class__.__name__, |
| json.dumps(val), key) |
| |
| for cond in val: |
| if not isinstance(cond, dict): |
| _Error( |
| 'Condition must be a dictionary but got ' + |
| cond.__class__.__name__, json.dumps(cond), key) |
| if 'value' not in cond: |
| _Error('Missing mandatory "value" key from condition', |
| json.dumps(cond), key) |
| |
| for cond_key, cond_val in cond.items(): |
| if cond_key == 'value': |
| continue |
| |
| if isinstance(cond_val, bool): |
| # Boolean value -> check if variable exists |
| if (cond_key in extvars) == cond_val: |
| continue |
| else: |
| break |
| |
| if cond_key not in extvars: |
| logging.warning('Unknown external var: %s', cond_key) |
| break |
| if re.search(cond_val, extvars[cond_key], re.I) is None: |
| break |
| else: |
| return _GetVal(key, cond['value'], extvars) |
| |
| _Error('Condition did not match any external vars', |
| json.dumps(val, indent=4) + '\nvars: ' + extvars.__str__(), key) |
| |
| |
| def _Error(text, extra='', key=''): |
| """Reports and raises an error. |
| |
| Args: |
| text (str): Error text |
| extra (str, optional): potentially sensitive error text for verbose output |
| key (str): key for error reporting or empty string if none |
| |
| Raises: |
| ConfigTransformError: error |
| """ |
| if key: |
| text = key + ': ' + text |
| if VERBOSE and extra: |
| text += ':\n' + extra |
| logging.error('%s', text) |
| raise ConfigTransformError(text) |