1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2024 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Helper classes and functions for formatting results into API responses.
10 from typing import Type, TypeVar, Dict, List, Callable, Any, Mapping, Optional, cast
11 from collections import defaultdict
12 from pathlib import Path
15 from .server.content_types import CONTENT_JSON
17 T = TypeVar('T') # pylint: disable=invalid-name
18 FormatFunc = Callable[[T, Mapping[str, Any]], str]
19 ErrorFormatFunc = Callable[[str, str, int], str]
22 class FormatDispatcher:
23 """ Helper class to conveniently create formatting functions in
24 a module using decorators.
27 def __init__(self, content_types: Optional[Mapping[str, str]] = None) -> None:
28 self.error_handler: ErrorFormatFunc = lambda ct, msg, status: f"ERROR {status}: {msg}"
29 self.content_types: Dict[str, str] = {}
31 self.content_types.update(content_types)
32 self.format_functions: Dict[Type[Any], Dict[str, FormatFunc[Any]]] = defaultdict(dict)
35 def format_func(self, result_class: Type[T],
36 fmt: str) -> Callable[[FormatFunc[T]], FormatFunc[T]]:
37 """ Decorator for a function that formats a given type of result into the
40 def decorator(func: FormatFunc[T]) -> FormatFunc[T]:
41 self.format_functions[result_class][fmt] = func
47 def error_format_func(self, func: ErrorFormatFunc) -> ErrorFormatFunc:
48 """ Decorator for a function that formats error messges.
49 There is only one error formatter per dispatcher. Using
50 the decorator repeatedly will overwrite previous functions.
52 self.error_handler = func
56 def list_formats(self, result_type: Type[Any]) -> List[str]:
57 """ Return a list of formats supported by this formatter.
59 return list(self.format_functions[result_type].keys())
62 def supports_format(self, result_type: Type[Any], fmt: str) -> bool:
63 """ Check if the given format is supported by this formatter.
65 return fmt in self.format_functions[result_type]
68 def format_result(self, result: Any, fmt: str, options: Mapping[str, Any]) -> str:
69 """ Convert the given result into a string using the given format.
71 The format is expected to be in the list returned by
74 return self.format_functions[type(result)][fmt](result, options)
77 def format_error(self, content_type: str, msg: str, status: int) -> str:
78 """ Convert the given error message into a response string
79 taking the requested content_type into account.
81 Change the format using the error_format_func decorator.
83 return self.error_handler(content_type, msg, status)
86 def set_content_type(self, fmt: str, content_type: str) -> None:
87 """ Set the content type for the given format. This is the string
88 that will be returned in the Content-Type header of the HTML
89 response, when the given format is choosen.
91 self.content_types[fmt] = content_type
94 def get_content_type(self, fmt: str) -> str:
95 """ Return the content type for the given format.
97 If no explicit content type has been defined, then
98 JSON format is assumed.
100 return self.content_types.get(fmt, CONTENT_JSON)
103 def load_format_dispatcher(api_name: str, project_dir: Optional[Path]) -> FormatDispatcher:
104 """ Load the dispatcher for the given API.
106 The function first tries to find a module api/<api_name>/format.py
107 in the project directory. This file must export a single variable
110 If the function does not exist, the default formatter is loaded.
112 if project_dir is not None:
113 priv_module = project_dir / 'api' / api_name / 'format.py'
114 if priv_module.is_file():
115 spec = importlib.util.spec_from_file_location(f'api.{api_name},format',
118 module = importlib.util.module_from_spec(spec)
119 # Do not add to global modules because there is no standard
120 # module name that Python can resolve.
121 assert spec.loader is not None
122 spec.loader.exec_module(module)
124 return cast(FormatDispatcher, module.dispatch)
126 return cast(FormatDispatcher,
127 importlib.import_module(f'nominatim_api.{api_name}.format').dispatch)