Repository: coverahealth/dataspec

Issue:
Blankable and nilable spec conformers can return INVALID even when validation passes
Due to some changes in #81 to `s.blankable` and `s.nilable`, default conformers for both spec types can return `INVALID` even if the validation logic considers the input value valid.

```
>>> from dataspec import s
>>> spec = s.blankable(s.all("8_digit_date", s.str(length=8), s.date("date", format_="%Y%m%d", conformer=str)))
>>> spec.validate_all("")
[]
>>> spec.conform("")
<dataspec.base.Invalid object at 0x103f4dd68>
```

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e2d058..e1705c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Fixed
+- Fixed a bug where nilable and blankable Spec conformers would return `INVALID` even
+  if their validation passed (#87)
 
 ## [v0.3.post1]
 Fixed `README.rst` reference and content-type in `setup.py` for PyPI description.
diff --git a/src/dataspec/__version__.py b/src/dataspec/__version__.py
index 8365ceb..085fffc 100644
--- a/src/dataspec/__version__.py
+++ b/src/dataspec/__version__.py
@@ -1,3 +1,3 @@
-VERSION = (0, 3, "post1")
+VERSION = (0, 3, "dev2")
 
 __version__ = ".".join(map(str, VERSION))
diff --git a/src/dataspec/factories.py b/src/dataspec/factories.py
index a42822f..c8518f0 100644
--- a/src/dataspec/factories.py
+++ b/src/dataspec/factories.py
@@ -92,10 +92,15 @@ def blankable_spec(
                 message=f"Value '{e}' is not blank", pred=blank_or_pred, value=e,
             )
 
+    def conform_blankable(e):
+        if e == "":
+            return e
+        return spec.conform(e)
+
     return ValidatorSpec(
         tag or "blankable",
         blank_or_pred,
-        conformer=compose_conformers(spec.conformer, conformer),
+        conformer=compose_conformers(conform_blankable, conformer),
     )
 
 
@@ -873,10 +878,15 @@ def nilable_spec(
                 message=f"Value '{e}' is not None", pred=nil_or_pred, value=e,
             )
 
+    def conform_nilable(e):
+        if e is None:
+            return e
+        return spec.conform(e)
+
     return ValidatorSpec(
         tag or "nilable",
         nil_or_pred,
-        conformer=compose_conformers(spec.conformer, conformer),
+        conformer=compose_conformers(conform_nilable, conformer),
     )
 
 


Test patch:
diff --git a/tests/test_factories.py b/tests/test_factories.py
index b532cfe..6b49918 100644
--- a/tests/test_factories.py
+++ b/tests/test_factories.py
@@ -53,6 +53,17 @@ class TestBlankableSpecValidation:
         assert blankable_spec.validate_all(v)
 
 
+class TestBlankableSpecConformation:
+    @pytest.fixture
+    def blankable_spec(self) -> Spec:
+        return s.blankable(s.date("date", format_="%Y%m%d", conformer=str))
+
+    @pytest.mark.parametrize("v,conformed", [("", ""), ("20200314", "2020-03-14")])
+    def test_blankable_conformation(self, blankable_spec: Spec, v, conformed):
+        assert blankable_spec.is_valid(v)
+        assert conformed == blankable_spec.conform(v)
+
+
 class TestBoolValidation:
     @pytest.mark.parametrize("v", [True, False])
     def test_bool(self, v):
@@ -1008,6 +1019,17 @@ class TestNilableSpecValidation:
         assert nilable_spec.validate_all(v)
 
 
+class TestNilableSpecConformation:
+    @pytest.fixture
+    def nilable_spec(self) -> Spec:
+        return s.nilable(s.date("date", format_="%Y%m%d", conformer=str))
+
+    @pytest.mark.parametrize("v,conformed", [(None, None), ("20200314", "2020-03-14")])
+    def test_blankable_conformation(self, nilable_spec: Spec, v, conformed):
+        assert nilable_spec.is_valid(v)
+        assert conformed == nilable_spec.conform(v)
+
+
 class TestNumSpecValidation:
     @pytest.mark.parametrize("v", [-3, 25, 3.14, -2.72, -33])
     def test_is_num(self, v):