diff --git a/src/server/BreCal/impl/shipcalls.py b/src/server/BreCal/impl/shipcalls.py index 83385cf..65bb000 100644 --- a/src/server/BreCal/impl/shipcalls.py +++ b/src/server/BreCal/impl/shipcalls.py @@ -10,6 +10,7 @@ from ..services.auth_guard import check_jwt from BreCal.database.update_database import evaluate_shipcall_state from BreCal.database.sql_queries import create_sql_query_shipcall_get, create_sql_query_shipcall_post, create_sql_query_shipcall_put, create_sql_query_history_post, create_sql_query_history_put, SQLQuery from marshmallow import Schema, fields, ValidationError +from BreCal.validators.validation_error import create_validation_error_response def GetShipcalls(options): """ @@ -152,9 +153,7 @@ def PostShipcalls(schemaModel): return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'} except ValidationError as ex: - logging.error(ex) - print(ex) - return json.dumps({"message":f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"}), 400 + return create_validation_error_response(ex, status_code=400, create_log=True) except Exception as ex: logging.error(traceback.format_exc()) @@ -265,9 +264,7 @@ def PutShipcalls(schemaModel): return json.dumps({"id" : schemaModel["id"]}), 200 except ValidationError as ex: - logging.error(ex) - print(ex) - return json.dumps({"message":f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"}), 400 + return create_validation_error_response(ex, status_code=400, create_log=True) except Exception as ex: logging.error(traceback.format_exc()) diff --git a/src/server/BreCal/validators/validation_error.py b/src/server/BreCal/validators/validation_error.py index e108222..4ee0c7e 100644 --- a/src/server/BreCal/validators/validation_error.py +++ b/src/server/BreCal/validators/validation_error.py @@ -1,58 +1,59 @@ import logging import typing import json +import sys from marshmallow import ValidationError -import werkzeug from werkzeug.exceptions import Forbidden -def create_default_error_format(error_description): +def create_default_json_response_format(error_field:str, error_description:str): + """ + The default format of a JSON response for exceptions is: + { + "message":str, + "errors":typing.Optional[ + list[dict[str,list[str]]] + ] + } + """ json_response = { - "message":f"{error_description}", - "errors":[], - "valid_data":{} + "error_field":error_field, + "error_description":error_description } return json_response +def unbundle_(errors, unbundled=[]): + """ + unbundles a dictionary entry into separate items and appends them to the list {unbundled}. + Example: + errors = {"key1":{"keya":"keyb","keyc":{"keyc12":12}}} + Returns: + [{'error_field':'keya', 'error_description':['keyb']}, {'error_field':'keyc12', 'error_description':[12]}] + As can be seen, only the subkeys and their respective value are received. Each value is *always* a list. + """ + {k:unbundle_(v,unbundled=unbundled) if isinstance(v,dict) else unbundled.append({"error_field":k, "error_description":v[0] if isinstance(v,list) else str(v)}) for k,v in errors.items()} + return + +def unbundle_validation_error_message(message): + """ + inputs: + message: ValidationError.messages object. A str, list or dictionary + """ + unbundled = [] + unbundle_(message, unbundled=unbundled) + if len(unbundled)>0: + error_field = "ValidationError in the following field(s): " + " & ".join([unb["error_field"] for unb in unbundled]) + error_description = "Error Description(s): " + " & ".join([unb["error_description"] for unb in unbundled]) + else: + error_field = "ValidationError" + error_description = "unknown validation error" + return (error_field, error_description) def create_validation_error_response(ex:ValidationError, status_code:int=400, create_log:bool=True)->typing.Tuple[str,int]: - # generate an overview the errors - #example: - # {'lock_time': ['The provided value must be in the future. Current Time: 2024-09-02 08:23:32.600791, Value: 2024-09-01 08:20:41.853000']} - # when the model schema returns an error, 'messages' is by default a dictionary. - # e.g., loadedModel = model.TimesSchema().load(data=content, many=False, partial=True) - # returns: {'eta_berth': ['The provided value must be in the future} - - # when raising a custom ValidationError, it can return a string, list or dict. - # we would like to ensure, that the content of the .messages is a dictionary. This can be accomplished by calling - # raise ValidationError({"example_key_which_fails":"the respective error message"}) - errors = ex.messages - - # raise ValidationError("example error") - # creates a .messages object, which is an array. e.g., ex.messages = ["example error"] - # the following conversion snipped ensures a dictionary output - if isinstance(errors, (str,list)): - errors = {"undefined_schema":errors} - errors = {k:v if isinstance(v,list) else [v] for k,v in errors.items()} - - # hence, errors always has the following type: list[dict[str, list[str]]] - errors = [errors] - - - # example: - # "Valid Data": { - # "id": 2894, - # "eta_berth": "datetime.datetime(2024, 9, 2, 11, 11, 43)", - # "eta_berth_fixed": false - # } - valid_data = ex.valid_data - - message = "ValidationError" - json_response = create_default_error_format(error_description=message) - json_response.update({ - "errors":errors, - "valid_data":valid_data - }) + # unbundles ValidationError into a dictionary of {'error_field':str, 'error_description':str}-format + message = ex.messages + (error_field, error_description) = unbundle_validation_error_message(message) + json_response = create_default_json_response_format(error_field=error_field, error_description=error_description) # json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not) # natively serializable are properly serialized. @@ -67,7 +68,7 @@ def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log # json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not) # natively serializable are properly serialized. message = ex.description - json_response = create_default_error_format(error_description=message) + json_response = create_default_json_response_format(error_field=str(repr(ex)), error_description=message) serialized_response = json.dumps(json_response, default=str) if create_log: @@ -77,7 +78,8 @@ def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Optional[str]=None, create_log:bool=True): message = repr(ex) if message is None else message - json_response = create_default_error_format(error_description=message) + json_response = create_default_json_response_format(error_field="Exception", error_description=message) + json_response["message"] = "call failed" serialized_response = json.dumps(json_response, default=str)