1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 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)
|