diff options
Diffstat (limited to 'third_party/python/glean_parser/glean_parser/rust.py')
-rw-r--r-- | third_party/python/glean_parser/glean_parser/rust.py | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/third_party/python/glean_parser/glean_parser/rust.py b/third_party/python/glean_parser/glean_parser/rust.py new file mode 100644 index 0000000000..eb3355e382 --- /dev/null +++ b/third_party/python/glean_parser/glean_parser/rust.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- + +# 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/. + +""" +Outputter to generate Rust code for metrics. +""" + +import enum +import json +from pathlib import Path +from typing import Any, Dict, Optional, Union + +from . import __version__ +from . import metrics +from . import pings +from . import tags +from . import util + + +def rust_datatypes_filter(value): + """ + A Jinja2 filter that renders Rust literals. + + Based on Python's JSONEncoder, but overrides: + - dicts and sets to raise an error + - sets to vec![] (used in labels) + - enums to become Class::Value + - lists to vec![] (used in send_in_pings) + - null to None + - strings to "value".into() + - Rate objects to a CommonMetricData initializer + (for external Denominators' Numerators lists) + """ + + class RustEncoder(json.JSONEncoder): + def iterencode(self, value): + if isinstance(value, dict): + raise ValueError("RustEncoder doesn't know dicts {}".format(str(value))) + elif isinstance(value, enum.Enum): + yield (value.__class__.__name__ + "::" + util.Camelize(value.name)) + elif isinstance(value, set): + yield "vec![" + first = True + for subvalue in sorted(list(value)): + if not first: + yield ", " + yield from self.iterencode(subvalue) + first = False + yield "]" + elif isinstance(value, list): + yield "vec![" + first = True + for subvalue in list(value): + if not first: + yield ", " + yield from self.iterencode(subvalue) + first = False + yield "]" + elif value is None: + yield "None" + # `CowStr` is a `str`, so needs to be before next case + elif isinstance(value, metrics.CowString): + yield f'::std::borrow::Cow::from("{value.inner}")' + elif isinstance(value, str): + yield f'"{value}".into()' + elif isinstance(value, metrics.Rate): + yield "CommonMetricData(" + first = True + for arg_name in util.common_metric_args: + if hasattr(value, arg_name): + if not first: + yield ", " + yield f"{util.camelize(arg_name)} = " + yield from self.iterencode(getattr(value, arg_name)) + first = False + yield ")" + else: + yield from super().iterencode(value) + + return "".join(RustEncoder().iterencode(value)) + + +def ctor(obj): + """ + Returns the scope and name of the constructor to use for a metric object. + Necessary because LabeledMetric<T> is constructed using LabeledMetric::new + not LabeledMetric<T>::new + """ + if getattr(obj, "labeled", False): + return "LabeledMetric::new" + return class_name(obj.type) + "::new" + + +def type_name(obj): + """ + Returns the Rust type to use for a given metric or ping object. + """ + + if getattr(obj, "labeled", False): + return "LabeledMetric<{}>".format(class_name(obj.type)) + generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons? + if len(generate_enums): + generic = None + for name, suffix in generate_enums: + if len(getattr(obj, name)): + generic = util.Camelize(obj.name) + suffix + else: + if isinstance(obj, metrics.Event): + generic = "NoExtra" + else: + generic = "No" + suffix + + return "{}<{}>".format(class_name(obj.type), generic) + + return class_name(obj.type) + + +def extra_type_name(typ: str) -> str: + """ + Returns the corresponding Rust type for event's extra key types. + """ + + if typ == "boolean": + return "bool" + elif typ == "string": + return "String" + elif typ == "quantity": + return "u32" + else: + return "UNSUPPORTED" + + +def class_name(obj_type): + """ + Returns the Rust class name for a given metric or ping type. + """ + if obj_type == "ping": + return "Ping" + if obj_type.startswith("labeled_"): + obj_type = obj_type[8:] + return util.Camelize(obj_type) + "Metric" + + +def extra_keys(allowed_extra_keys): + """ + Returns the &'static [&'static str] ALLOWED_EXTRA_KEYS for impl ExtraKeys + """ + return "&[" + ", ".join([f'"{key}"' for key in allowed_extra_keys]) + "]" + + +class Category: + """ + Data struct holding information about a metric to be used in the template. + """ + + def __init__( + self, + name: str, + objs: Dict[str, Union[metrics.Metric, pings.Ping, tags.Tag]], + contains_pings: bool, + ): + self.name = name + self.objs = objs + self.contains_pings = contains_pings + + +def output_rust( + objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None +) -> None: + """ + Given a tree of objects, output Rust code to `output_dir`. + + :param objs: A tree of objects (metrics and pings) as returned from + `parser.parse_objects`. + :param output_dir: Path to an output directory to write to. + :param options: options dictionary, not currently used for Rust + """ + + if options is None: + options = {} + + template = util.get_jinja2_template( + "rust.jinja2", + filters=( + ("rust", rust_datatypes_filter), + ("snake_case", util.snake_case), + ("camelize", util.camelize), + ("type_name", type_name), + ("extra_type_name", extra_type_name), + ("ctor", ctor), + ("extra_keys", extra_keys), + ), + ) + + filename = "glean_metrics.rs" + filepath = output_dir / filename + categories = [] + + for category_key, category_val in objs.items(): + contains_pings = any( + isinstance(obj, pings.Ping) for obj in category_val.values() + ) + + cat = Category(category_key, category_val, contains_pings) + categories.append(cat) + + with filepath.open("w", encoding="utf-8") as fd: + fd.write( + template.render( + parser_version=__version__, + categories=categories, + extra_metric_args=util.extra_metric_args, + common_metric_args=util.common_metric_args, + ) + ) |