Source code

Revision control

Copy as Markdown

Other Tools

#!/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
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("group"): All(Text, Length(min=1)),
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-jexl"): Text,
Optional("is-public-jexl"): Text,
Optional("description-links"): dict,
def main(output, *filenames):
features = {}
errors = False
features = process_files(filenames)
json.dump(features, output)
except ExceptionGroup as error_group:
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:]
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:")
rv.append('file "{}":\n'.format(self.filename))
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:
with open(filename, "r") as f:
feature_data = toml.load(f)
feature_data, feature_schema
for feature_id, feature in feature_data.items():
feature["id"] = feature_id
features[feature_id] = expand_feature(feature)
except (
) 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()
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 preferences where we forgot to set the default value
feature.setdefault("defaultValueJexl", None)
elif "preference" not in feature:
raise FeatureGateException(
"Features of type {} must specify an explicit preference name".format(
feature.setdefault("isPublicJexl", "false")
return feature
if __name__ == "__main__":
sys.exit(main(sys.stdout, *sys.argv[1:]))