1# -*- coding: utf-8 -*- 2 3# This Source Code Form is subject to the terms of the Mozilla Public 4# License, v. 2.0. If a copy of the MPL was not distributed with this 5# file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7""" 8Outputter to generate Javascript code for metrics. 9""" 10 11import enum 12import json 13from pathlib import Path 14from typing import Any, Dict, Optional, Callable 15 16from . import metrics 17from . import util 18 19 20def javascript_datatypes_filter(value: util.JSONType) -> str: 21 """ 22 A Jinja2 filter that renders Javascript literals. 23 """ 24 25 class JavascriptEncoder(json.JSONEncoder): 26 def iterencode(self, value): 27 if isinstance(value, enum.Enum): 28 yield from super().iterencode(util.camelize(value.name)) 29 elif isinstance(value, set): 30 yield "[" 31 first = True 32 for subvalue in sorted(list(value)): 33 if not first: 34 yield ", " 35 yield from self.iterencode(subvalue) 36 first = False 37 yield "]" 38 else: 39 yield from super().iterencode(value) 40 41 return "".join(JavascriptEncoder().iterencode(value)) 42 43 44def class_name_factory(platform: str) -> Callable[[str], str]: 45 """ 46 Returns a function that receives an obj_type and 47 returns the correct class name for that type in the current platform. 48 """ 49 50 def class_name(obj_type: str) -> str: 51 if obj_type == "ping": 52 class_name = "PingType" 53 else: 54 if obj_type.startswith("labeled_"): 55 obj_type = obj_type[8:] 56 class_name = util.Camelize(obj_type) + "MetricType" 57 58 if platform == "qt": 59 return "Glean.Glean._private." + class_name 60 61 return class_name 62 63 return class_name 64 65 66def extra_type_name(extra_type: str) -> str: 67 """ 68 Returns the equivalent TypeScript type to an extra type. 69 """ 70 if extra_type == "quantity": 71 return "number" 72 73 return extra_type 74 75 76def import_path(obj_type: str) -> str: 77 """ 78 Returns the import path of the given object inside the @mozilla/glean package. 79 """ 80 if obj_type == "ping": 81 import_path = "ping" 82 else: 83 if obj_type.startswith("labeled_"): 84 obj_type = obj_type[8:] 85 import_path = "metrics/" + obj_type 86 87 return import_path 88 89 90def args(obj_type: str) -> Dict[str, object]: 91 """ 92 Returns the list of arguments for each object type. 93 """ 94 if obj_type == "ping": 95 return {"common": util.ping_args, "extra": []} 96 97 return {"common": util.common_metric_args, "extra": util.extra_metric_args} 98 99 100def output( 101 lang: str, 102 objs: metrics.ObjectTree, 103 output_dir: Path, 104 options: Optional[Dict[str, Any]] = None, 105) -> None: 106 """ 107 Given a tree of objects, output Javascript or Typescript code to `output_dir`. 108 109 :param lang: Either "javascript" or "typescript"; 110 :param objects: A tree of objects (metrics and pings) as returned from 111 `parser.parse_objects`. 112 :param output_dir: Path to an output directory to write to. 113 :param options: options dictionary, with the following optional keys: 114 - `platform`: Which platform are we building for. Options are `webext` and `qt`. 115 Default is `webext`. 116 - `version`: The version of the Glean.js Qt library being used. 117 This option is mandatory when targeting Qt. Note that the version 118 string must only contain the major and minor version i.e. 0.14. 119 120 """ 121 122 if options is None: 123 options = {} 124 125 platform = options.get("platform", "webext") 126 accepted_platforms = ["qt", "webext", "node"] 127 if platform not in accepted_platforms: 128 raise ValueError( 129 f"Unknown platform: {platform}. Accepted platforms are: {accepted_platforms}." # noqa 130 ) 131 version = options.get("version") 132 if platform == "qt" and version is None: 133 raise ValueError( 134 "'version' option is required when building for the 'qt' platform." 135 ) 136 137 template = util.get_jinja2_template( 138 "javascript.jinja2", 139 filters=( 140 ("class_name", class_name_factory(platform)), 141 ("extra_type_name", extra_type_name), 142 ("import_path", import_path), 143 ("js", javascript_datatypes_filter), 144 ("args", args), 145 ), 146 ) 147 148 for category_key, category_val in objs.items(): 149 extension = ".js" if lang == "javascript" else ".ts" 150 filename = util.camelize(category_key) + extension 151 filepath = output_dir / filename 152 153 types = set( 154 [ 155 # This takes care of the regular metric type imports 156 # as well as the labeled metric subtype imports, 157 # thus the removal of the `labeled_` substring. 158 # 159 # The actual LabeledMetricType import is conditioned after 160 # the `has_labeled_metrics` boolean. 161 obj.type if not obj.type.startswith("labeled_") else obj.type[8:] 162 for obj in category_val.values() 163 ] 164 ) 165 has_labeled_metrics = any( 166 getattr(metric, "labeled", False) for metric in category_val.values() 167 ) 168 with filepath.open("w", encoding="utf-8") as fd: 169 fd.write( 170 template.render( 171 category_name=category_key, 172 objs=category_val, 173 extra_args=util.extra_args, 174 platform=platform, 175 version=version, 176 has_labeled_metrics=has_labeled_metrics, 177 types=types, 178 lang=lang, 179 ) 180 ) 181 # Jinja2 squashes the final newline, so we explicitly add it 182 fd.write("\n") 183 184 if platform == "qt": 185 # Explicitly create a qmldir file when building for Qt 186 template = util.get_jinja2_template("qmldir.jinja2") 187 filepath = output_dir / "qmldir" 188 189 with filepath.open("w", encoding="utf-8") as fd: 190 fd.write(template.render(categories=objs.keys(), version=version)) 191 # Jinja2 squashes the final newline, so we explicitly add it 192 fd.write("\n") 193 194 195def output_javascript( 196 objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None 197) -> None: 198 """ 199 Given a tree of objects, output Javascript code to `output_dir`. 200 201 :param objects: A tree of objects (metrics and pings) as returned from 202 `parser.parse_objects`. 203 :param output_dir: Path to an output directory to write to. 204 :param options: options dictionary, with the following optional keys: 205 206 - `namespace`: The identifier of the global variable to assign to. 207 This will only have and effect for Qt and static web sites. 208 Default is `Glean`. 209 - `platform`: Which platform are we building for. Options are `webext` and `qt`. 210 Default is `webext`. 211 """ 212 213 output("javascript", objs, output_dir, options) 214 215 216def output_typescript( 217 objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None 218) -> None: 219 """ 220 Given a tree of objects, output Typescript code to `output_dir`. 221 222 # Note 223 224 The only difference between the typescript and javascript templates, 225 currently is the file extension. 226 227 :param objects: A tree of objects (metrics and pings) as returned from 228 `parser.parse_objects`. 229 :param output_dir: Path to an output directory to write to. 230 :param options: options dictionary, with the following optional keys: 231 232 - `namespace`: The identifier of the global variable to assign to. 233 This will only have and effect for Qt and static web sites. 234 Default is `Glean`. 235 - `platform`: Which platform are we building for. Options are `webext` and `qt`. 236 Default is `webext`. 237 """ 238 239 output("typescript", objs, output_dir, options) 240