# 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 sys from os import path import yaml __all__ = ["annotations_filename", "read_annotations"] annotations_filename = path.normpath( path.join(path.dirname(__file__), "..", "CrashAnnotations.yaml") ) def sort_annotations(annotations): """Return annotations in ascending alphabetical order ignoring case""" return sorted(annotations.items(), key=lambda annotation: str.lower(annotation[0])) # Convert CamelCase to snake_case. Also supports CAPCamelCase. def camel_to_snake(s): if s.islower(): return s lowers = [c.islower() for c in s] + [False] words = [] last = 0 for i in range(1, len(s)): if not lowers[i] and (lowers[i - 1] or lowers[i + 1]): words.append(s[last:i]) last = i words.append(s[last:]) return "_".join(words).lower() class AnnotationValidator: def __init__(self, name): self._name = name self.passed = True def validate(self, data): """ Ensure that the annotation has all the required fields, and elaborate default values. """ if "description" not in data: self._error("does not have a description") annotation_type = data.get("type") if annotation_type is None: self._error("does not have a type") valid_types = ["string", "boolean", "u32", "u64", "usize", "object"] if annotation_type and annotation_type not in valid_types: self._error(f"has an unknown type: {annotation_type}") annotation_type = None annotation_scope = data.setdefault("scope", "client") valid_scopes = ["client", "report", "ping", "ping-only"] if annotation_scope not in valid_scopes: self._error(f"has an unknown scope: {annotation_scope}") annotation_scope = None is_ping = annotation_scope and annotation_scope in ["ping", "ping-only"] if annotation_scope and "glean" in data and not is_ping: self._error("has a glean metric specification but does not have ping scope") if annotation_type and is_ping: self._glean(annotation_type, data.setdefault("glean", {})) def _error(self, message): print( f"{annotations_filename}: Annotation {self._name} {message}.", file=sys.stderr, ) self.passed = False def _glean(self, annotation_type, glean): if not isinstance(glean, dict): self._error("has invalid glean metric specification (expected a map)") return glean_metric_name = glean.setdefault("metric", "crash") # If only a category is given, derive the metric name from the annotation name. if "." not in glean_metric_name: glean_metric_name = glean["metric"] = ( f"{glean_metric_name}.{camel_to_snake(self._name)}" ) glean_default_type = ( annotation_type if annotation_type in ["string", "boolean"] else None ) glean_type = glean.setdefault("type", glean_default_type) if glean_type is None: self._error("must have a glean metric type specified") glean_types = [ "boolean", "datetime", "timespan", "string", "string_list", "quantity", "object", ] if glean_type and glean_type not in glean_types: self._error(f"has an invalid glean metric type ({glean_type})") glean_type = None metric_required_fields = { "datetime": ["time_unit"], "timespan": ["time_unit"], "quantity": ["unit"], "string_list": ["delimiter"], "object": ["structure"], } required_fields = metric_required_fields.get(glean_type, []) for field in required_fields: if field not in glean: self._error(f"requires a `{field}` field for glean {glean_type} metric") def read_annotations(): """Read the annotations from the YAML file. If an error is encountered quit the program.""" try: with open(annotations_filename) as annotations_file: annotations = sort_annotations(yaml.safe_load(annotations_file)) except (OSError, ValueError) as e: sys.exit("Error parsing " + annotations_filename + ":\n" + str(e) + "\n") valid = True for name, data in annotations: validator = AnnotationValidator(name) validator.validate(data) valid &= validator.passed if not valid: sys.exit(1) return annotations def main(output): yaml.safe_dump(read_annotations(), stream=output) return {annotations_filename}