forked from mirrors/gecko-dev
Backed out changeset 9654aaf21d82 (bug 1896187) Backed out changeset 0a74adb44160 (bug 1896187)
216 lines
6.3 KiB
Python
Executable file
216 lines
6.3 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 re
|
|
import sys
|
|
import toml
|
|
import voluptuous
|
|
import voluptuous.humanize
|
|
from voluptuous import Schema, Optional, Any, All, Required, Length, Range, Msg, Match
|
|
|
|
|
|
Text = Any(str, bytes)
|
|
|
|
|
|
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),
|
|
Optional("description-links"): dict,
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
EXIT_OK = 0
|
|
EXIT_ERROR = 1
|
|
|
|
|
|
def main(output, *filenames):
|
|
features = {}
|
|
errors = False
|
|
try:
|
|
features = process_files(filenames)
|
|
json.dump(features, output, sort_keys=True)
|
|
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 "{}":\n'.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__()
|
|
with_comma = original[:-1]
|
|
# python 2 adds a trailing comma and python 3 does not, so we need to conditionally reinclude it
|
|
if len(with_comma) > 0 and with_comma[-1] != ",":
|
|
with_comma = with_comma + ","
|
|
return with_comma + " filename={!r})".format(self.filename)
|
|
|
|
|
|
def process_files(filenames):
|
|
features = {}
|
|
errors = []
|
|
|
|
for filename in filenames:
|
|
try:
|
|
with open(filename, "r") as f:
|
|
feature_data = toml.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,
|
|
toml.TomlDecodeError,
|
|
) as e:
|
|
# Wrap errors in enough information to know which file they came from
|
|
errors.append(FeatureGateException(e, filename))
|
|
|
|
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"]))
|
|
# set default value to None so that we can test for perferences where we forgot to set the default value
|
|
feature.setdefault("defaultValue", None)
|
|
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)
|
|
)
|
|
|
|
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",
|
|
"early_beta_or_earlier",
|
|
"beta",
|
|
"release",
|
|
"dev-edition",
|
|
"esr",
|
|
"thunderbird",
|
|
}
|
|
)
|
|
|
|
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:]))
|