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