gecko-dev/toolkit/components/featuregates/gen_feature_definitions.py

182 строки
5.6 KiB
Python
Executable File

#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import json
import pytoml
import re
import sys
import six
import voluptuous
import voluptuous.humanize
from voluptuous import Schema, Optional, Any, All, Required, Length, Range, Msg, Match
Text = Any(six.text_type, six.binary_type)
id_regex = re.compile(r'^[a-z0-9-]+$')
feature_schema = Schema({
Match(id_regex): {
Required('title'): All(Text, Length(min=1)),
Required('description'): All(Text, Length(min=1)),
Required('bug-numbers'): All(Length(min=1), [All(int, Range(min=1))]),
Required('restart-required'): bool,
Required('type'): 'boolean', # In the future this may include other types
Optional('preference'): Text,
Optional('default-value'): Any(bool, dict), # the types of the keys here should match the value of `type`
Optional('is-public'): Any(bool, dict),
},
})
EXIT_OK = 0
EXIT_ERROR = 1
def main(output, *filenames):
features = {}
errors = False
try:
features = process_files(filenames)
json.dump(features, output)
except ExceptionGroup as error_group:
print(str(error_group))
return EXIT_ERROR
return EXIT_OK
class ExceptionGroup(Exception):
def __init__(self, errors):
self.errors = errors
def __str__(self):
rv = ['There were errors while processing feature definitions:']
for error in self.errors:
# indent the message
s = '\n'.join(' ' + line for line in str(error).split('\n'))
# add a * at the beginning of the first line
s = ' * ' + s[4:]
rv.append(s)
return '\n'.join(rv)
class FeatureGateException(Exception):
def __init__(self, message, filename=None):
super(FeatureGateException, self).__init__(message)
self.filename = filename
def __str__(self):
message = super(FeatureGateException, self).__str__()
rv = ["In"]
if self.filename is None:
rv.append("unknown file:")
else:
rv.append('file "{}":'.format(self.filename))
rv.append(message)
return ' '.join(rv)
def __repr__(self):
# Turn "FeatureGateExcept(<message>,)" into "FeatureGateException(<message>, filename=<filename>)"
original = super(FeatureGateException, self).__repr__()
return original[:-1] + ' filename={!r})'.format(self.filename)
def process_files(filenames):
features = {}
errors = []
for filename in filenames:
try:
with open(filename, 'r') as f:
feature_data = pytoml.load(f)
voluptuous.humanize.validate_with_humanized_errors(feature_data, feature_schema)
for feature_id, feature in feature_data.items():
feature['id'] = feature_id
features[feature_id] = expand_feature(feature)
except (voluptuous.error.Error, IOError, FeatureGateException) as e:
# Wrap errors in enough information to know which file they came from
errors.append(FeatureGateException(e, filename))
except pytoml.TomlError as e:
# Toml errors have file information already
errors.append(e)
if errors:
raise ExceptionGroup(errors)
return features
def hyphens_to_camel_case(s):
"""Convert names-with-hyphens to namesInCamelCase"""
rv = ''
for part in s.split('-'):
if rv == '':
rv = part.lower()
else:
rv += part[0].upper() + part[1:].lower()
return rv
def expand_feature(feature):
"""Fill in default values for optional fields"""
# convert all names-with-hyphens to namesInCamelCase
key_changes = []
for key in feature.keys():
if '-' in key:
new_key = hyphens_to_camel_case(key)
key_changes.append((key, new_key))
for (old_key, new_key) in key_changes:
feature[new_key] = feature[old_key]
del feature[old_key]
if feature['type'] == 'boolean':
feature.setdefault('preference', 'features.{}.enabled'.format(feature['id']))
feature.setdefault('defaultValue', False)
elif 'preference' not in feature:
raise FeatureGateException(
'Features of type {} must specify an explicit preference name'.format(feature['type'])
)
feature.setdefault('isPublic', False)
try:
for key in ['defaultValue', 'isPublic']:
feature[key] = process_configured_value(key, feature[key])
except FeatureGateException as e:
raise FeatureGateException(
"Error when processing feature {}: {}".format(feature['id'], e.message))
return feature
def process_configured_value(name, value):
if not isinstance(value, dict):
return {'default': value}
if 'default' not in value:
raise FeatureGateException("Config for {} has no default: {}".format(name, value))
expected_keys = set({'default', 'win', 'mac', 'linux', 'android', 'nightly', 'beta', 'release', 'dev-edition', 'esr'})
for key in value.keys():
parts = [p.strip() for p in key.split(",")]
for part in parts:
if part not in expected_keys:
raise FeatureGateException(
"Unexpected target {}, expected any of {}".format(part, expected_keys)
)
# TODO Compute values at build time, so that it always returns only a single value.
return value
if __name__ == '__main__':
sys.exit(main(sys.stdout, *sys.argv[1:]))