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
 
  18 FormatFunc = Callable[[T, Mapping[str, Any]], str]
 
  19 ErrorFormatFunc = Callable[[str, str, int], str]
 
  22 class FormatDispatcher:
 
  23     """ Container for formatting functions for results.
 
  24         Functions can conveniently be added by using decorated functions.
 
  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)
 
  34     def format_func(self, result_class: Type[T],
 
  35                     fmt: str) -> Callable[[FormatFunc[T]], FormatFunc[T]]:
 
  36         """ Decorator for a function that formats a given type of result into the
 
  39         def decorator(func: FormatFunc[T]) -> FormatFunc[T]:
 
  40             self.format_functions[result_class][fmt] = func
 
  45     def error_format_func(self, func: ErrorFormatFunc) -> ErrorFormatFunc:
 
  46         """ Decorator for a function that formats error messges.
 
  47             There is only one error formatter per dispatcher. Using
 
  48             the decorator repeatedly will overwrite previous functions.
 
  50         self.error_handler = func
 
  53     def list_formats(self, result_type: Type[Any]) -> List[str]:
 
  54         """ Return a list of formats supported by this formatter.
 
  56         return list(self.format_functions[result_type].keys())
 
  58     def supports_format(self, result_type: Type[Any], fmt: str) -> bool:
 
  59         """ Check if the given format is supported by this formatter.
 
  61         return fmt in self.format_functions[result_type]
 
  63     def format_result(self, result: Any, fmt: str, options: Mapping[str, Any]) -> str:
 
  64         """ Convert the given result into a string using the given format.
 
  66             The format is expected to be in the list returned by
 
  69         return self.format_functions[type(result)][fmt](result, options)
 
  71     def format_error(self, content_type: str, msg: str, status: int) -> str:
 
  72         """ Convert the given error message into a response string
 
  73             taking the requested content_type into account.
 
  75             Change the format using the error_format_func decorator.
 
  77         return self.error_handler(content_type, msg, status)
 
  79     def set_content_type(self, fmt: str, content_type: str) -> None:
 
  80         """ Set the content type for the given format. This is the string
 
  81             that will be returned in the Content-Type header of the HTML
 
  82             response, when the given format is choosen.
 
  84         self.content_types[fmt] = content_type
 
  86     def get_content_type(self, fmt: str) -> str:
 
  87         """ Return the content type for the given format.
 
  89             If no explicit content type has been defined, then
 
  90             JSON format is assumed.
 
  92         return self.content_types.get(fmt, CONTENT_JSON)
 
  95 def load_format_dispatcher(api_name: str, project_dir: Optional[Path]) -> FormatDispatcher:
 
  96     """ Load the dispatcher for the given API.
 
  98         The function first tries to find a module api/<api_name>/format.py
 
  99         in the project directory. This file must export a single variable
 
 102         If the function does not exist, the default formatter is loaded.
 
 104     if project_dir is not None:
 
 105         priv_module = project_dir / 'api' / api_name / 'format.py'
 
 106         if priv_module.is_file():
 
 107             spec = importlib.util.spec_from_file_location(f'api.{api_name},format',
 
 110                 module = importlib.util.module_from_spec(spec)
 
 111                 # Do not add to global modules because there is no standard
 
 112                 # module name that Python can resolve.
 
 113                 assert spec.loader is not None
 
 114                 spec.loader.exec_module(module)
 
 116                 return cast(FormatDispatcher, module.dispatch)
 
 118     return cast(FormatDispatcher,
 
 119                 importlib.import_module(f'nominatim_api.{api_name}.format').dispatch)