python/moz/l10n/formats/mf2/from_json.py (115 lines of code) (raw):

# Copyright Mozilla Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from typing import Any, Literal, cast from ...model import ( CatchallKey, Expression, Markup, Message, Pattern, PatternMessage, SelectMessage, VariableRef, ) from .validate import MF2ValidationError def mf2_from_json(json: dict[str, Any]) -> Message: """ Marshal a MessageFormat 2 data model [JSON Schema](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/message.json) object into a parsed `moz.l10n.message.data.Message`. May raise `MF2ValidationError`. """ try: msg_type = json["type"] if msg_type not in {"message", "select"}: raise MF2ValidationError(f"Invalid JSON message: {json}") declarations: dict[str, Expression] = {} for decl in json["declarations"]: decl_type = decl["type"] if decl_type not in {"input", "local"}: raise MF2ValidationError(f"Invalid JSON declaration type: {decl}") decl_name = _string(decl, "name") decl_expr = _expression(decl["value"]) if decl_type == "input": if ( not isinstance(decl_expr.arg, VariableRef) or decl_expr.arg.name != decl_name ): raise MF2ValidationError(f"Invalid JSON .input declaration: {decl}") if decl_name in declarations: raise MF2ValidationError(f"Duplicate JSON declaration for ${decl_name}") declarations[decl_name] = decl_expr if msg_type == "message": pattern = _pattern(json["pattern"]) return PatternMessage(pattern, declarations) assert msg_type == "select" selectors = tuple(_variable(sel) for sel in json["selectors"]) variants = { tuple(_key(key) for key in vari["keys"]): _pattern(vari["value"]) for vari in json["variants"] } return SelectMessage(declarations, selectors, variants) except (IndexError, KeyError, TypeError) as err: raise MF2ValidationError(f"Invalid JSON: {err!r}") def _pattern(json: list[Any]) -> Pattern: return [ part if isinstance(part, str) else _markup(part) if part["type"] == "markup" else _expression(part) for part in json ] def _expression(json: dict[str, Any]) -> Expression: if json["type"] != "expression": raise MF2ValidationError(f"Invalid JSON expression type: {json}") arg = _value(json["arg"]) if "arg" in json else None json_func = json.get("function", None) if json_func: if json_func["type"] != "function": raise MF2ValidationError(f"Invalid JSON function type: {json_func}") function = _string(json_func, "name") options = _options(json_func["options"]) if "options" in json_func else {} else: function = None options = {} if arg is None and function is None: raise MF2ValidationError( f"Invalid JSON expression with no operand and no function: {json}" ) attributes = _attributes(json["attributes"]) if "attributes" in json else {} return Expression(arg, function, options, attributes) def _markup(json: dict[str, Any]) -> Markup: assert json["type"] == "markup" kind = cast(Literal["open", "standalone", "close"], _string(json, "kind")) if kind not in {"open", "standalone", "close"}: raise MF2ValidationError(f"Invalid JSON markup kind: {json}") name = _string(json, "name") options = _options(json["options"]) if "options" in json else {} attributes = _attributes(json["attributes"]) if "attributes" in json else {} return Markup(kind, name, options, attributes) def _options(json: dict[str, Any]) -> dict[str, str | VariableRef]: return {name: _value(json_value) for name, json_value in json.items()} def _attributes(json: dict[str, Any]) -> dict[str, str | Literal[True]]: return { name: True if json_value is True else _literal(json_value) for name, json_value in json.items() } def _key(json: dict[str, Any]) -> str | CatchallKey: type = json["type"] if type == "literal": return _string(json, "value") elif json["type"] == "*": value = _string(json, "value") if "value" in json else None return CatchallKey(value) else: raise MF2ValidationError(f"Invalid JSON variant key: {json}") def _value(json: dict[str, Any]) -> str | VariableRef: return _string(json, "value") if json["type"] == "literal" else _variable(json) def _literal(json: dict[str, Any]) -> str: if json["type"] != "literal": raise MF2ValidationError(f"Invalid JSON literal: {json}") return _string(json, "value") def _variable(json: dict[str, Any]) -> VariableRef: if json["type"] != "variable": raise MF2ValidationError(f"Invalid JSON variable: {json}") return VariableRef(_string(json, "name")) def _string(obj: dict[str, Any], key: str | None = None) -> str: value = obj if key is None else obj.get(key, None) if isinstance(value, str): return value else: raise MF2ValidationError(f"Expected a string value for {key} in {obj}")