summaryrefslogtreecommitdiffstats
path: root/third_party/python/glean_parser/glean_parser/javascript.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/glean_parser/glean_parser/javascript.py')
-rw-r--r--third_party/python/glean_parser/glean_parser/javascript.py322
1 files changed, 322 insertions, 0 deletions
diff --git a/third_party/python/glean_parser/glean_parser/javascript.py b/third_party/python/glean_parser/glean_parser/javascript.py
new file mode 100644
index 0000000000..1473065beb
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/javascript.py
@@ -0,0 +1,322 @@
+# -*- 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 Javascript code for metrics.
+"""
+
+import enum
+import json
+from pathlib import Path
+from typing import Any, Dict, Optional, Callable
+
+from . import __version__
+from . import metrics
+from . import util
+
+
+def javascript_datatypes_filter(value: util.JSONType) -> str:
+ """
+ A Jinja2 filter that renders Javascript literals.
+
+ Based on Python's JSONEncoder, but overrides:
+ - lists to use listOf
+ - sets to use setOf
+ - Rate objects to a CommonMetricData initializer
+ (for external Denominators' Numerators lists)
+ """
+
+ class JavascriptEncoder(json.JSONEncoder):
+ def iterencode(self, value):
+ if isinstance(value, enum.Enum):
+ yield from super().iterencode(util.camelize(value.name))
+ elif isinstance(value, list):
+ yield "["
+ first = True
+ for subvalue in value:
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield "]"
+ elif isinstance(value, set):
+ yield "["
+ 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(JavascriptEncoder().iterencode(value))
+
+
+def class_name_factory(platform: str) -> Callable[[str], str]:
+ """
+ Returns a function that receives an obj_type and
+ returns the correct class name for that type in the current platform.
+ """
+
+ def class_name(obj_type: str) -> str:
+ if obj_type == "ping":
+ class_name = "PingType"
+ else:
+ if obj_type.startswith("labeled_"):
+ obj_type = obj_type[8:]
+ class_name = util.Camelize(obj_type) + "MetricType"
+
+ if platform == "qt":
+ return "Glean.Glean._private." + class_name
+
+ return class_name
+
+ return class_name
+
+
+def extra_type_name(extra_type: str) -> str:
+ """
+ Returns the equivalent TypeScript type to an extra type.
+ """
+ if extra_type == "quantity":
+ return "number"
+
+ return extra_type
+
+
+def import_path(obj_type: str) -> str:
+ """
+ Returns the import path of the given object inside the @mozilla/glean package.
+ """
+ if obj_type == "ping":
+ import_path = "ping"
+ else:
+ if obj_type.startswith("labeled_"):
+ obj_type = obj_type[8:]
+ import_path = "metrics/" + obj_type
+
+ return import_path
+
+
+def args(obj_type: str) -> Dict[str, object]:
+ """
+ Returns the list of arguments for each object type.
+ """
+ if obj_type == "ping":
+ return {"common": util.ping_args, "extra": []}
+
+ return {"common": util.common_metric_args, "extra": util.extra_metric_args}
+
+
+def generate_build_date(date: Optional[str]) -> str:
+ """
+ Generate the build Date object.
+ """
+
+ ts = util.build_date(date)
+
+ data = [
+ str(ts.year),
+ # In JavaScript 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 `Date` instance.
+ return f"new Date({components})" # noqa
+
+
+def output(
+ lang: str,
+ objs: metrics.ObjectTree,
+ output_dir: Path,
+ options: Optional[Dict[str, Any]] = None,
+) -> None:
+ """
+ Given a tree of objects, output Javascript or Typescript code to `output_dir`.
+
+ :param lang: Either "javascript" or "typescript";
+ :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:
+ - `platform`: Which platform are we building for. Options are `webext` and `qt`.
+ Default is `webext`.
+ - `version`: The version of the Glean.js Qt library being used.
+ This option is mandatory when targeting Qt. Note that the version
+ string must only contain the major and minor version i.e. 0.14.
+ - `with_buildinfo`: If "true" a `gleanBuildInfo.(js|ts)` file is generated.
+ Otherwise generation of that file is skipped. Defaults to "false".
+ - `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 = {}
+
+ platform = options.get("platform", "webext")
+ accepted_platforms = ["qt", "webext", "node"]
+ if platform not in accepted_platforms:
+ raise ValueError(
+ f"Unknown platform: {platform}. Accepted platforms are: {accepted_platforms}." # noqa
+ )
+ version = options.get("version")
+ if platform == "qt" and version is None:
+ raise ValueError(
+ "'version' option is required when building for the 'qt' platform."
+ )
+
+ template = util.get_jinja2_template(
+ "javascript.jinja2",
+ filters=(
+ ("class_name", class_name_factory(platform)),
+ ("extra_type_name", extra_type_name),
+ ("import_path", import_path),
+ ("js", javascript_datatypes_filter),
+ ("args", args),
+ ),
+ )
+
+ for category_key, category_val in objs.items():
+ extension = ".js" if lang == "javascript" else ".ts"
+ filename = util.camelize(category_key) + extension
+ filepath = output_dir / filename
+
+ types = set(
+ [
+ # This takes care of the regular metric type imports
+ # as well as the labeled metric subtype imports,
+ # thus the removal of the `labeled_` substring.
+ #
+ # The actual LabeledMetricType import is conditioned after
+ # the `has_labeled_metrics` boolean.
+ obj.type if not obj.type.startswith("labeled_") else obj.type[8:]
+ 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,
+ extra_args=util.extra_args,
+ platform=platform,
+ version=version,
+ has_labeled_metrics=has_labeled_metrics,
+ types=types,
+ lang=lang,
+ )
+ )
+ # Jinja2 squashes the final newline, so we explicitly add it
+ fd.write("\n")
+
+ with_buildinfo = options.get("with_buildinfo", "").lower() == "true"
+ build_date = options.get("build_date", None)
+ if with_buildinfo:
+ # Write out the special "build info" file
+ template = util.get_jinja2_template(
+ "javascript.buildinfo.jinja2",
+ )
+ # This filename needs to start with "glean" so it can never
+ # clash with a metric category
+ filename = "gleanBuildInfo" + extension
+ filepath = output_dir / filename
+
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ platform=platform,
+ build_date=generate_build_date(build_date),
+ )
+ )
+ fd.write("\n")
+
+ if platform == "qt":
+ # Explicitly create a qmldir file when building for Qt
+ template = util.get_jinja2_template("qmldir.jinja2")
+ filepath = output_dir / "qmldir"
+
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__, categories=objs.keys(), version=version
+ )
+ )
+ # Jinja2 squashes the final newline, so we explicitly add it
+ fd.write("\n")
+
+
+def output_javascript(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Javascript 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 identifier of the global variable to assign to.
+ This will only have and effect for Qt and static web sites.
+ Default is `Glean`.
+ - `platform`: Which platform are we building for. Options are `webext` and `qt`.
+ Default is `webext`.
+ """
+
+ output("javascript", objs, output_dir, options)
+
+
+def output_typescript(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Typescript code to `output_dir`.
+
+ # Note
+
+ The only difference between the typescript and javascript templates,
+ currently is the file extension.
+
+ :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 identifier of the global variable to assign to.
+ This will only have and effect for Qt and static web sites.
+ Default is `Glean`.
+ - `platform`: Which platform are we building for. Options are `webext` and `qt`.
+ Default is `webext`.
+ """
+
+ output("typescript", objs, output_dir, options)