summaryrefslogtreecommitdiffstats
path: root/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/glean/build_scripts/glean_parser_ext/rust.py')
-rw-r--r--toolkit/components/glean/build_scripts/glean_parser_ext/rust.py283
1 files changed, 283 insertions, 0 deletions
diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py b/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py
new file mode 100644
index 0000000000..615784b481
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py
@@ -0,0 +1,283 @@
+# -*- 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
+
+import jinja2
+from glean_parser import util
+from glean_parser.metrics import CowString, Event, Rate
+from util import generate_metric_ids, generate_ping_ids, get_metrics
+
+from js import ID_BITS, ID_SIGNAL_BITS
+
+# The list of all args to CommonMetricData.
+# No particular order is required, but I have these in common_metric_data.rs
+# order just to be organized.
+common_metric_data_args = [
+ "name",
+ "category",
+ "send_in_pings",
+ "lifetime",
+ "disabled",
+ "dynamic_label",
+]
+
+
+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 from self.iterencode(sorted(list(value)))
+ 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"
+ # CowString is also a 'str' but is a special case.
+ # Ensure its case is handled before str's (below).
+ elif isinstance(value, CowString):
+ yield f'::std::borrow::Cow::from("{value.inner}")'
+ elif isinstance(value, str):
+ yield '"' + value + '".into()'
+ elif isinstance(value, Rate):
+ yield "CommonMetricData {"
+ for arg_name in common_metric_data_args:
+ if hasattr(value, arg_name):
+ yield f"{arg_name}: "
+ yield from self.iterencode(getattr(value, arg_name))
+ yield ", "
+ yield " ..Default::default()}"
+ 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):
+ label_enum = "super::DynamicLabel"
+ if obj.labels and len(obj.labels):
+ label_enum = f"{util.Camelize(obj.name)}Label"
+ return f"LabeledMetric<Labeled{class_name(obj.type)}, {label_enum}>"
+ generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons?
+ if len(generate_enums):
+ for name, _ in generate_enums:
+ if not len(getattr(obj, name)) and isinstance(obj, Event):
+ return class_name(obj.type) + "<NoExtraKeys>"
+ else:
+ # we always use the `extra` suffix,
+ # because we only expose the new event API
+ suffix = "Extra"
+ return "{}<{}>".format(
+ class_name(obj.type), util.Camelize(obj.name) + suffix
+ )
+ 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(map(lambda key: '"' + key + '"', allowed_extra_keys)) + "]"
+
+
+def output_rust(objs, output_fd, ping_names_by_app_id, options={}):
+ """
+ Given a tree of objects, output Rust code to the file-like object `output_fd`.
+
+ :param objs: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_fd: Writeable file to write the output to.
+ :param ping_names_by_app_id: A map of app_ids to lists of ping names.
+ Used to determine which custom pings to register.
+ :param options: options dictionary, presently unused.
+ """
+
+ # Monkeypatch util.snake_case for the templates to use
+ util.snake_case = lambda value: value.replace(".", "_").replace("-", "_")
+ # Monkeypatch util.get_jinja2_template to find templates nearby
+
+ def get_local_template(template_name, filters=()):
+ env = jinja2.Environment(
+ loader=jinja2.PackageLoader("rust", "templates"),
+ trim_blocks=True,
+ lstrip_blocks=True,
+ )
+ env.filters["camelize"] = util.camelize
+ env.filters["Camelize"] = util.Camelize
+ for filter_name, filter_func in filters:
+ env.filters[filter_name] = filter_func
+ return env.get_template(template_name)
+
+ util.get_jinja2_template = get_local_template
+ get_metric_id = generate_metric_ids(objs)
+ get_ping_id = generate_ping_ids(objs)
+
+ # Map from a tuple (const, typ) to an array of tuples (id, path)
+ # where:
+ # const: The Rust constant name to be used for the lookup map
+ # typ: The metric type to be stored in the lookup map
+ # id: The numeric metric ID
+ # path: The fully qualified path to the metric object in Rust
+ #
+ # This map is only filled for metrics, not for pings.
+ #
+ # Example:
+ #
+ # ("COUNTERS", "CounterMetric") -> [(1, "test_only::clicks"), ...]
+ objs_by_type = {}
+
+ # Map from a metric ID to the fully qualified path of the event object in Rust.
+ # Required for the special handling of event lookups.
+ #
+ # Example:
+ #
+ # 17 -> "test_only::an_event"
+ events_by_id = {}
+
+ # Map from a labeled type (e.g. "counter") to a map from metric ID to the
+ # fully qualified path of the labeled metric object in Rust paired with
+ # whether the labeled metric has an enum.
+ # Required for the special handling of labeled metric lookups.
+ #
+ # Example:
+ #
+ # "counter" -> 42 -> ("test_only::mabels_kitchen_counters", false)
+ labeleds_by_id_by_type = {}
+
+ if "pings" in objs:
+ template_filename = "rust_pings.jinja2"
+ objs = {"pings": objs["pings"]}
+ else:
+ template_filename = "rust.jinja2"
+ objs = get_metrics(objs)
+ for category_name, category_value in objs.items():
+ for metric in category_value.values():
+ # The constant is all uppercase and suffixed by `_MAP`
+ const_name = util.snake_case(metric.type).upper() + "_MAP"
+ typ = type_name(metric)
+ key = (const_name, typ)
+
+ metric_name = util.snake_case(metric.name)
+ category_snake = util.snake_case(category_name)
+ full_path = f"{category_snake}::{metric_name}"
+
+ if metric.type == "event":
+ events_by_id[get_metric_id(metric)] = full_path
+ continue
+
+ if getattr(metric, "labeled", False):
+ labeled_type = metric.type[8:]
+ if labeled_type not in labeleds_by_id_by_type:
+ labeleds_by_id_by_type[labeled_type] = {}
+ labeleds_by_id_by_type[labeled_type][get_metric_id(metric)] = (
+ full_path,
+ metric.labels and len(metric.labels),
+ )
+ continue
+
+ if key not in objs_by_type:
+ objs_by_type[key] = []
+ objs_by_type[key].append((get_metric_id(metric), full_path))
+
+ # Now for the modules for each category.
+ template = util.get_jinja2_template(
+ template_filename,
+ filters=(
+ ("rust", rust_datatypes_filter),
+ ("snake_case", util.snake_case),
+ ("type_name", type_name),
+ ("extra_type_name", extra_type_name),
+ ("ctor", ctor),
+ ("extra_keys", extra_keys),
+ ("metric_id", get_metric_id),
+ ("ping_id", get_ping_id),
+ ),
+ )
+
+ output_fd.write(
+ template.render(
+ all_objs=objs,
+ common_metric_data_args=common_metric_data_args,
+ metric_by_type=objs_by_type,
+ extra_args=util.extra_args,
+ events_by_id=events_by_id,
+ labeleds_by_id_by_type=labeleds_by_id_by_type,
+ submetric_bit=ID_BITS - ID_SIGNAL_BITS,
+ ping_names_by_app_id=ping_names_by_app_id,
+ )
+ )
+ output_fd.write("\n")