summaryrefslogtreecommitdiffstats
path: root/third_party/python/glean_parser/glean_parser/kotlin.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/glean_parser/glean_parser/kotlin.py')
-rw-r--r--third_party/python/glean_parser/glean_parser/kotlin.py356
1 files changed, 356 insertions, 0 deletions
diff --git a/third_party/python/glean_parser/glean_parser/kotlin.py b/third_party/python/glean_parser/glean_parser/kotlin.py
new file mode 100644
index 0000000000..82cc63d237
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/kotlin.py
@@ -0,0 +1,356 @@
+# -*- 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 Kotlin code for metrics.
+"""
+
+from collections import OrderedDict
+import enum
+import json
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Union # noqa
+
+from . import __version__
+from . import metrics
+from . import pings
+from . import tags
+from . import util
+from .util import DictWrapper
+
+
+def kotlin_datatypes_filter(value: util.JSONType) -> str:
+ """
+ A Jinja2 filter that renders Kotlin literals.
+
+ Based on Python's JSONEncoder, but overrides:
+ - lists to use listOf
+ - dicts to use mapOf
+ - sets to use setOf
+ - enums to use the like-named Kotlin enum
+ - Rate objects to a CommonMetricData initializer
+ (for external Denominators' Numerators lists)
+ """
+
+ class KotlinEncoder(json.JSONEncoder):
+ def iterencode(self, value):
+ if isinstance(value, list):
+ yield "listOf("
+ first = True
+ for subvalue in value:
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield ")"
+ elif isinstance(value, dict):
+ yield "mapOf("
+ first = True
+ for key, subvalue in value.items():
+ if not first:
+ yield ", "
+ yield from self.iterencode(key)
+ yield " to "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield ")"
+ elif isinstance(value, enum.Enum):
+ # UniFFI generates SCREAMING_CASE enum variants.
+ yield (value.__class__.__name__ + "." + util.screaming_case(value.name))
+ elif isinstance(value, set):
+ yield "setOf("
+ first = True
+ for subvalue in sorted(list(value)):
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield ")"
+ 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(KotlinEncoder().iterencode(value))
+
+
+def type_name(obj: Union[metrics.Metric, pings.Ping]) -> str:
+ """
+ Returns the Kotlin type to use for a given metric or ping object.
+ """
+ generate_enums = getattr(obj, "_generate_enums", [])
+ if len(generate_enums):
+ generic = None
+ for member, suffix in generate_enums:
+ if len(getattr(obj, member)):
+ if isinstance(obj, metrics.Event):
+ generic = util.Camelize(obj.name) + suffix
+ else:
+ generic = util.camelize(obj.name) + suffix
+ else:
+ if isinstance(obj, metrics.Event):
+ generic = "NoExtras"
+ 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 Kotlin type for event's extra key types.
+ """
+
+ if typ == "boolean":
+ return "Boolean"
+ elif typ == "string":
+ return "String"
+ elif typ == "quantity":
+ return "Int"
+ else:
+ return "UNSUPPORTED"
+
+
+def class_name(obj_type: str) -> str:
+ """
+ Returns the Kotlin class name for a given metric or ping type.
+ """
+ if obj_type == "ping":
+ return "PingType"
+ if obj_type.startswith("labeled_"):
+ obj_type = obj_type[8:]
+ return util.Camelize(obj_type) + "MetricType"
+
+
+def generate_build_date(date: Optional[str]) -> str:
+ """
+ Generate the build timestamp.
+ """
+
+ ts = util.build_date(date)
+
+ data = [
+ str(ts.year),
+ # In Java the first month of the year in calendars is JANUARY which is 0.
+ # In Python it's 1-based
+ str(ts.month - 1),
+ str(ts.day),
+ str(ts.hour),
+ str(ts.minute),
+ str(ts.second),
+ ]
+ components = ", ".join(data)
+
+ # DatetimeMetricType takes a `Calendar` instance.
+ return f'Calendar.getInstance(TimeZone.getTimeZone("GMT+0")).also {{ cal -> cal.set({components}) }}' # noqa
+
+
+def output_gecko_lookup(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, generate a Kotlin map between Gecko histograms and
+ Glean SDK metric types.
+
+ :param objects: 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, with the following optional keys:
+
+ - `namespace`: The package namespace to declare at the top of the
+ generated files. Defaults to `GleanMetrics`.
+ - `glean_namespace`: The package namespace of the glean library itself.
+ This is where glean objects will be imported from in the generated
+ code.
+ """
+ if options is None:
+ options = {}
+
+ template = util.get_jinja2_template(
+ "kotlin.geckoview.jinja2",
+ filters=(
+ ("kotlin", kotlin_datatypes_filter),
+ ("type_name", type_name),
+ ("class_name", class_name),
+ ),
+ )
+
+ namespace = options.get("namespace", "GleanMetrics")
+ glean_namespace = options.get("glean_namespace", "mozilla.components.service.glean")
+
+ # Build a dictionary that contains data for metrics that are
+ # histogram-like/scalar-like and contain a gecko_datapoint, with this format:
+ #
+ # {
+ # "histograms": {
+ # "category": [
+ # {"gecko_datapoint": "the-datapoint", "name": "the-metric-name"},
+ # ...
+ # ],
+ # ...
+ # },
+ # "other-type": {}
+ # }
+ gecko_metrics: Dict[str, Dict[str, List[Dict[str, str]]]] = DictWrapper()
+
+ # Define scalar-like types.
+ SCALAR_LIKE_TYPES = ["boolean", "string", "quantity"]
+
+ for category_key, category_val in objs.items():
+ # Support exfiltration of Gecko metrics from products using both the
+ # Glean SDK and GeckoView. See bug 1566356 for more context.
+ for metric in category_val.values():
+ # This is not a Gecko metric, skip it.
+ if (
+ isinstance(metric, pings.Ping)
+ or isinstance(metric, tags.Tag)
+ or not getattr(metric, "gecko_datapoint", False)
+ ):
+ continue
+
+ # Put scalars in their own categories, histogram-like in "histograms" and
+ # categorical histograms in "categoricals".
+ type_category = "histograms"
+ if metric.type in SCALAR_LIKE_TYPES:
+ type_category = metric.type
+ elif metric.type == "labeled_counter":
+ # Labeled counters with a 'gecko_datapoint' property
+ # are categorical histograms.
+ type_category = "categoricals"
+
+ gecko_metrics.setdefault(type_category, OrderedDict())
+ gecko_metrics[type_category].setdefault(category_key, [])
+
+ gecko_metrics[type_category][category_key].append(
+ {"gecko_datapoint": metric.gecko_datapoint, "name": metric.name}
+ )
+
+ if not gecko_metrics:
+ # Bail out and don't create a file if no gecko metrics
+ # are found.
+ return
+
+ filepath = output_dir / "GleanGeckoMetricsMapping.kt"
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ gecko_metrics=gecko_metrics,
+ namespace=namespace,
+ glean_namespace=glean_namespace,
+ )
+ )
+ # Jinja2 squashes the final newline, so we explicitly add it
+ fd.write("\n")
+
+
+def output_kotlin(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Kotlin code to `output_dir`.
+
+ :param objects: 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, with the following optional keys:
+
+ - `namespace`: The package namespace to declare at the top of the
+ generated files. Defaults to `GleanMetrics`.
+ - `glean_namespace`: The package namespace of the glean library itself.
+ This is where glean objects will be imported from in the generated
+ code.
+ - `with_buildinfo`: If "true" a `GleanBuildInfo.kt` file is generated.
+ Otherwise generation of that file is skipped.
+ Defaults to "true".
+ - `build_date`: If set to `0` a static unix epoch time will be used.
+ If set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
+ it will use that date.
+ Other values will throw an error.
+ If not set it will use the current date & time.
+ """
+ if options is None:
+ options = {}
+
+ namespace = options.get("namespace", "GleanMetrics")
+ glean_namespace = options.get("glean_namespace", "mozilla.components.service.glean")
+ namespace_package = namespace[: namespace.rfind(".")]
+ with_buildinfo = options.get("with_buildinfo", "true").lower() == "true"
+ build_date = options.get("build_date", None)
+
+ # Write out the special "build info" object
+ template = util.get_jinja2_template(
+ "kotlin.buildinfo.jinja2",
+ )
+
+ if with_buildinfo:
+ build_date = generate_build_date(build_date)
+ # This filename needs to start with "Glean" so it can never clash with a
+ # metric category
+ with (output_dir / "GleanBuildInfo.kt").open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ namespace=namespace,
+ namespace_package=namespace_package,
+ glean_namespace=glean_namespace,
+ build_date=build_date,
+ )
+ )
+ fd.write("\n")
+
+ template = util.get_jinja2_template(
+ "kotlin.jinja2",
+ filters=(
+ ("kotlin", kotlin_datatypes_filter),
+ ("type_name", type_name),
+ ("extra_type_name", extra_type_name),
+ ("class_name", class_name),
+ ),
+ )
+
+ for category_key, category_val in objs.items():
+ filename = util.Camelize(category_key) + ".kt"
+ filepath = output_dir / filename
+
+ obj_types = sorted(
+ list(set(class_name(obj.type) for obj in category_val.values()))
+ )
+ has_labeled_metrics = any(
+ getattr(metric, "labeled", False) for metric in category_val.values()
+ )
+
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ category_name=category_key,
+ objs=category_val,
+ obj_types=obj_types,
+ common_metric_args=util.common_metric_args,
+ extra_metric_args=util.extra_metric_args,
+ ping_args=util.ping_args,
+ namespace=namespace,
+ has_labeled_metrics=has_labeled_metrics,
+ glean_namespace=glean_namespace,
+ )
+ )
+ # Jinja2 squashes the final newline, so we explicitly add it
+ fd.write("\n")
+
+ # TODO: Maybe this should just be a separate outputter?
+ output_gecko_lookup(objs, output_dir, options)