From 6d66e77da8e2ee619f19e249182c0c03e7c1b29b Mon Sep 17 00:00:00 2001
From: Pierre-Olivier Simonard <pierre.olivier.simonard@gmail.com>
Date: Mon, 9 Oct 2023 15:23:19 +0200
Subject: [PATCH] ENH - Better handling of pydantic error msgs (#546)

* better handling of pydantic error msgs

* Better handling of spec validation errors

* Fix - forgotten return

* pydantic errors : custom exception

* linting
---
 .../conda_store_server/schema.py              | 40 ++++++++++++++++++-
 .../conda_store_server/server/views/api.py    |  2 +
 2 files changed, 40 insertions(+), 2 deletions(-)

diff --git a/conda-store-server/conda_store_server/schema.py b/conda-store-server/conda_store_server/schema.py
index 11dd685cb..173174249 100644
--- a/conda-store-server/conda_store_server/schema.py
+++ b/conda-store-server/conda_store_server/schema.py
@@ -6,8 +6,8 @@
 import sys
 from typing import Any, Callable, Dict, List, Optional, Union
 
-from conda_store_server import conda_utils
-from pydantic import BaseModel, Field, constr, validator
+from conda_store_server import conda_utils, utils
+from pydantic import BaseModel, Field, ValidationError, constr, validator
 
 
 def _datetime_factory(offset: datetime.timedelta):
@@ -406,6 +406,42 @@ def check_dependencies(cls, v):
 
         return v
 
+    @classmethod
+    def parse_obj(cls, specification):
+        try:
+            return super().parse_obj(specification)
+        except ValidationError as e:
+            # there can be multiple errors. Let's build a comprehensive summary
+            # to return to the end user.
+
+            # hr stands for "human readable"
+            all_errors_hr = []
+
+            for err in e.errors():
+                error_type = err["type"]
+                error_loc = err["loc"]
+
+                # fallback case : if we can't figure out the error, let's build a default
+                # one based on the data returned by Pydantic.
+                human_readable_error = (
+                    f"{err['msg']} (type={error_type}, loc={error_loc})"
+                )
+
+                if error_type == "type_error.none.not_allowed":
+                    if error_loc[0] == "name":
+                        human_readable_error = (
+                            "The name of the environment cannot be empty."
+                        )
+                    else:
+                        if len(error_loc) == 1:
+                            human_readable_error = f"Invalid YAML : A forbidden `None` value has been encountered in section {error_loc[0]}"
+                        elif len(error_loc) == 2:
+                            human_readable_error = f"Invalid YAML : A forbidden `None` value has been encountered in section `{error_loc[0]}`, line {error_loc[1]}"
+
+                all_errors_hr.append(human_readable_error)
+
+            raise utils.CondaStoreError(all_errors_hr)
+
 
 ###############################
 #  Docker Registry Schema
diff --git a/conda-store-server/conda_store_server/server/views/api.py b/conda-store-server/conda_store_server/server/views/api.py
index 2661c49e7..3e53097eb 100644
--- a/conda-store-server/conda_store_server/server/views/api.py
+++ b/conda-store-server/conda_store_server/server/views/api.py
@@ -560,6 +560,8 @@ async def api_post_specification(
         specification = schema.CondaSpecification.parse_obj(specification)
     except yaml.error.YAMLError:
         raise HTTPException(status_code=400, detail="Unable to parse. Invalid YAML")
+    except utils.CondaStoreError as e:
+        raise HTTPException(status_code=400, detail="\n".join(e.args[0]))
     except pydantic.ValidationError as e:
         raise HTTPException(status_code=400, detail=str(e))