summaryrefslogtreecommitdiffstats
path: root/toolkit/components/featuregates/gen_feature_definitions.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/featuregates/gen_feature_definitions.py
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/featuregates/gen_feature_definitions.py')
-rwxr-xr-xtoolkit/components/featuregates/gen_feature_definitions.py216
1 files changed, 216 insertions, 0 deletions
diff --git a/toolkit/components/featuregates/gen_feature_definitions.py b/toolkit/components/featuregates/gen_feature_definitions.py
new file mode 100755
index 0000000000..da4a9fe177
--- /dev/null
+++ b/toolkit/components/featuregates/gen_feature_definitions.py
@@ -0,0 +1,216 @@
+#!/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:]))