Repository: Informasjonsforvaltning/datacatalogtordf

Issue:
Validate content of lists
When user set a list-property, the library should validate the items of the list where this is feasible.

E.g. formats of Distribution where list should be a list of URIs: https://github.com/Informasjonsforvaltning/datacatalogtordf/blob/3eb00ad2b92cfdf51138e301f87cdc5bea279834/src/datacatalogtordf/distribution.py#L264

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/Informasjonsforvaltning/datacatalogtordf/pull/78?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning) Report
> Merging [#78](https://codecov.io/gh/Informasjonsforvaltning/datacatalogtordf/pull/78?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning) (c05eaaf) into [main](https://codecov.io/gh/Informasjonsforvaltning/datacatalogtordf/commit/3eb00ad2b92cfdf51138e301f87cdc5bea279834?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning) (3eb00ad) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff            @@
##              main       #78   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           15        15           
  Lines         1520      1544   +24     
=========================================
+ Hits          1520      1544   +24     
```


| [Impacted Files](https://codecov.io/gh/Informasjonsforvaltning/datacatalogtordf/pull/78?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning) | Coverage Δ | |
|---|---|---|
| [src/datacatalogtordf/uri.py](https://codecov.io/gh/Informasjonsforvaltning/datacatalogtordf/pull/78/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning#diff-c3JjL2RhdGFjYXRhbG9ndG9yZGYvdXJpLnB5) | `100.00% <ø> (ø)` | |
| [src/datacatalogtordf/catalog.py](https://codecov.io/gh/Informasjonsforvaltning/datacatalogtordf/pull/78/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning#diff-c3JjL2RhdGFjYXRhbG9ndG9yZGYvY2F0YWxvZy5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/datacatalogtordf/catalogrecord.py](https://codecov.io/gh/Informasjonsforvaltning/datacatalogtordf/pull/78/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning#diff-c3JjL2RhdGFjYXRhbG9ndG9yZGYvY2F0YWxvZ3JlY29yZC5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/datacatalogtordf/dataset.py](https://codecov.io/gh/Informasjonsforvaltning/datacatalogtordf/pull/78/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning#diff-c3JjL2RhdGFjYXRhbG9ndG9yZGYvZGF0YXNldC5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/datacatalogtordf/distribution.py](https://codecov.io/gh/Informasjonsforvaltning/datacatalogtordf/pull/78/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning#diff-c3JjL2RhdGFjYXRhbG9ndG9yZGYvZGlzdHJpYnV0aW9uLnB5) | `100.00% <100.00%> (ø)` | |
| [src/datacatalogtordf/resource.py](https://codecov.io/gh/Informasjonsforvaltning/datacatalogtordf/pull/78/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning#diff-c3JjL2RhdGFjYXRhbG9ndG9yZGYvcmVzb3VyY2UucHk=) | `100.00% <100.00%> (ø)` | |

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Informasjonsforvaltning)


Patch:
diff --git a/pyproject.toml b/pyproject.toml
index a551582..80ee9b3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,7 @@ license = "Apache-2.0"
 name = "datacatalogtordf"
 readme = "README.md"
 repository = "https://github.com/Informasjonsforvaltning/datacatalogtordf"
-version = "2.1.1"
+version = "2.1.2"
 
 [tool.poetry.dependencies]
 concepttordf = "^2.0.0"
diff --git a/src/datacatalogtordf/catalog.py b/src/datacatalogtordf/catalog.py
index 2f0e172..51d4019 100644
--- a/src/datacatalogtordf/catalog.py
+++ b/src/datacatalogtordf/catalog.py
@@ -130,6 +130,11 @@ class Catalog(Dataset):
 
     @models.setter
     def models(self: Catalog, models: List[Any]) -> None:
+        # Validate model URIs:
+        for model in models:
+            if type(model) is str:
+                URI(model)
+
         self._models = models
 
     @property
@@ -139,6 +144,11 @@ class Catalog(Dataset):
 
     @contains_services.setter
     def contains_services(self: Catalog, contains_services: List[Any]) -> None:
+        # Validate contains_service URIs:
+        for contains_service in contains_services:
+            if type(contains_service) is str:
+                URI(contains_service)
+
         self._contains_services = contains_services
 
     @property
diff --git a/src/datacatalogtordf/catalogrecord.py b/src/datacatalogtordf/catalogrecord.py
index 8fa0500..fbc6661 100644
--- a/src/datacatalogtordf/catalogrecord.py
+++ b/src/datacatalogtordf/catalogrecord.py
@@ -128,6 +128,10 @@ class CatalogRecord:
 
     @conforms_to.setter
     def conforms_to(self: CatalogRecord, conforms_to: List[str]) -> None:
+        # Validate conforms_to URIs:
+        for string in conforms_to:
+            URI(string)
+
         self._conforms_to = conforms_to
 
     # -
diff --git a/src/datacatalogtordf/dataset.py b/src/datacatalogtordf/dataset.py
index 0101c51..43323fd 100644
--- a/src/datacatalogtordf/dataset.py
+++ b/src/datacatalogtordf/dataset.py
@@ -162,6 +162,10 @@ class Dataset(Resource):
     def access_rights_comments(
         self: Dataset, access_rights_comments: List[str]
     ) -> None:
+        # Validate access_rights_comment URIs:
+        for access_rights_comment in access_rights_comments:
+            URI(access_rights_comment)
+
         self._access_rights_comments = access_rights_comments
 
     @property
diff --git a/src/datacatalogtordf/distribution.py b/src/datacatalogtordf/distribution.py
index 87c0d73..65ef533 100644
--- a/src/datacatalogtordf/distribution.py
+++ b/src/datacatalogtordf/distribution.py
@@ -243,6 +243,10 @@ class Distribution:
 
     @conforms_to.setter
     def conforms_to(self: Distribution, conforms_to: List[str]) -> None:
+        # Validate conforms_to URIs:
+        for string in conforms_to:
+            URI(string)
+
         self._conforms_to = conforms_to
 
     @property
@@ -252,6 +256,9 @@ class Distribution:
 
     @media_types.setter
     def media_types(self: Distribution, media_types: List[str]) -> None:
+        # Validate media_type URIs:
+        for media_type in media_types:
+            URI(media_type)
         self._media_types = media_types
 
     @property
@@ -261,6 +268,9 @@ class Distribution:
 
     @formats.setter
     def formats(self: Distribution, formats: List[str]) -> None:
+        # Validate format URIs:
+        for format in formats:
+            URI(format)
         self._formats = formats
 
     @property
diff --git a/src/datacatalogtordf/resource.py b/src/datacatalogtordf/resource.py
index a4db415..836f761 100644
--- a/src/datacatalogtordf/resource.py
+++ b/src/datacatalogtordf/resource.py
@@ -157,6 +157,9 @@ class Resource(ABC):
 
     @conformsTo.setter
     def conformsTo(self: Resource, conformsTo: List[str]) -> None:
+        # Validate conforms_to URIs:
+        for string in conformsTo:
+            URI(string)
         self._conformsTo = conformsTo
 
     @property
@@ -166,6 +169,9 @@ class Resource(ABC):
 
     @theme.setter
     def theme(self: Resource, theme: List[str]) -> None:
+        # Validate theme URIs:
+        for string in theme:
+            URI(string)
         self._theme = theme
 
     @property
@@ -249,6 +255,9 @@ class Resource(ABC):
 
     @landing_page.setter
     def landing_page(self: Resource, landing_page: List[str]) -> None:
+        # Validate landing_page URIs:
+        for string in landing_page:
+            URI(string)
         self._landing_page = landing_page
 
     @property
@@ -276,6 +285,9 @@ class Resource(ABC):
 
     @resource_relation.setter
     def resource_relation(self: Resource, resource_relation: List[str]) -> None:
+        # Validate resource_relation URIs:
+        for string in resource_relation:
+            URI(string)
         self._resource_relation = resource_relation
 
     @property
diff --git a/src/datacatalogtordf/uri.py b/src/datacatalogtordf/uri.py
index bb266ea..2924741 100644
--- a/src/datacatalogtordf/uri.py
+++ b/src/datacatalogtordf/uri.py
@@ -14,6 +14,12 @@ class URI(str):
 
     _invalid_uri_chars = '<>" {}|\\^`'
 
+    Args:
+        link: The string to validate as URI.
+
+    Raises:
+        InvalidURIError: If the string is not a valid URI.
+
     Ref: https://github.com/RDFLib/rdflib/blob/master/rdflib/term.py#L75
 
     Example:


Test patch:
diff --git a/tests/test_catalog.py b/tests/test_catalog.py
index 0b3e620..1a9315e 100644
--- a/tests/test_catalog.py
+++ b/tests/test_catalog.py
@@ -1,13 +1,20 @@
 """Test cases for the catalog module."""
 
-from typing import Any
+from typing import Any, List
 
+import pytest
 from pytest_mock import MockFixture
 from rdflib import Graph, Literal, Namespace, RDF, URIRef
 from rdflib.compare import graph_diff, isomorphic
 from skolemizer.testutils import skolemization, SkolemUtils
 
-from datacatalogtordf import Catalog, CatalogRecord, DataService, Dataset
+from datacatalogtordf import (
+    Catalog,
+    CatalogRecord,
+    DataService,
+    Dataset,
+    InvalidURIError,
+)
 from tests.testutils import assert_isomorphic
 
 DCT = Namespace("http://purl.org/dc/terms/")
@@ -446,6 +453,69 @@ def test_to_graph_should_return_model_as_graph() -> None:
     assert _isomorphic
 
 
+def test_to_graph_should_return_model_as_graph_when_setting_list() -> None:
+    """It returns a model graph isomorphic to spec."""
+    catalog = Catalog()
+    catalog.identifier = "http://example.com/catalogs/1"
+    # Creating a class as a placeholder for real InformationModel class
+    InformationModel = type(
+        "InformationModel",
+        (object,),
+        {
+            "title": "",
+            "identifier": "",
+            "_to_graph": lambda self: model_to_graph(self),
+        },
+    )
+
+    _models: List[Any] = []
+    model_1 = InformationModel()
+    model_1.identifier = "http://example.com/models/1"  # type: ignore
+    model_1.title = {"en": "My first model"}  # type: ignore
+    _models.append(model_1)
+
+    model_2 = InformationModel()
+    model_2.identifier = "http://example.com/models/2"  # type: ignore
+    model_2.title = {"en": "My second model"}  # type: ignore
+    _models.append(model_2)
+    catalog.models = _models
+
+    src = """
+    @prefix dct: <http://purl.org/dc/terms/> .
+    @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+    @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+    @prefix dcat: <http://www.w3.org/ns/dcat#> .
+    @prefix modelldcatno: <https://data.norge.no/vocabulary/modelldcatno#> .
+
+    <http://example.com/catalogs/1> a dcat:Catalog ;
+        modelldcatno:model  <http://example.com/models/1> ,
+                            <http://example.com/models/2> ;
+    .
+    <http://example.com/models/1> a modelldcatno:InformationModel ;
+        dct:title   "My first model"@en ;
+    .
+    <http://example.com/models/2> a modelldcatno:InformationModel ;
+        dct:title   "My second model"@en ;
+    .
+    """
+    g1 = Graph().parse(data=catalog.to_rdf(), format="turtle")
+    g2 = Graph().parse(data=src, format="turtle")
+
+    _isomorphic = isomorphic(g1, g2)
+    if not _isomorphic:
+        _dump_diff(g1, g2)
+        pass
+    assert _isomorphic
+
+
+def test_set_models_to_list_of_invalid_formats() -> None:
+    """Should raise InvalidURIError."""
+    catalog = Catalog()
+    catalog.identifier = "http://example.com/catalogs/1"
+    with pytest.raises(InvalidURIError):
+        catalog.models = ["http://invalid^.uri.com/format"]
+
+
 def service_to_graph(model: Any) -> Graph:
     """Helper function to create a service graph to test with."""
     g = Graph()
@@ -518,6 +588,71 @@ def test_to_graph_should_return_service_as_graph() -> None:
     assert _isomorphic
 
 
+def test_to_graph_should_return_service_as_graph_when_setting_list() -> None:
+    """It returns a service graph isomorphic to spec."""
+    catalog = Catalog()
+    catalog.identifier = "http://example.com/catalogs/1"
+    # Creating a class as a placeholder for real Service class
+    Service = type(
+        "Service",
+        (object,),
+        {
+            "title": "",
+            "identifier": "",
+            "_to_graph": lambda self: service_to_graph(self),
+        },
+    )
+
+    _contains_services: List[Any] = []
+    service_1 = Service()
+    service_1.identifier = "http://example.com/services/1"  # type: ignore
+    service_1.title = {"en": "My first service"}  # type: ignore
+    _contains_services.append(service_1)
+
+    service_2 = Service()
+    service_2.identifier = "http://example.com/services/2"  # type: ignore
+    service_2.title = {"en": "My second service"}  # type: ignore
+    _contains_services.append(service_2)
+
+    catalog.contains_services = _contains_services
+
+    src = """
+    @prefix dct: <http://purl.org/dc/terms/> .
+    @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+    @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+    @prefix dcat: <http://www.w3.org/ns/dcat#> .
+    @prefix dcatno: <https://data.norge.no/vocabulary/dcatno#> .
+    @prefix cpsvno: <https://data.norge.no/vocabulary/cpsvno#> .
+
+    <http://example.com/catalogs/1> a dcat:Catalog ;
+        dcatno:containsService  <http://example.com/services/1> ,
+                            <http://example.com/services/2> ;
+    .
+    <http://example.com/services/1> a cpsvno:Service ;
+        dct:title   "My first service"@en ;
+    .
+    <http://example.com/services/2> a cpsvno:Service ;
+        dct:title   "My second service"@en ;
+    .
+    """
+    g1 = Graph().parse(data=catalog.to_rdf(), format="turtle")
+    g2 = Graph().parse(data=src, format="turtle")
+
+    _isomorphic = isomorphic(g1, g2)
+    if not _isomorphic:
+        _dump_diff(g1, g2)
+        pass
+    assert _isomorphic
+
+
+def test_set_contains_services_to_list_of_invalid_formats() -> None:
+    """Should raise InvalidURIError."""
+    catalog = Catalog()
+    catalog.identifier = "http://example.com/catalogs/1"
+    with pytest.raises(InvalidURIError):
+        catalog.contains_services = ["http://invalid^.uri.com/format"]
+
+
 def test_to_graph_should_return_dct_identifier_as_graph() -> None:
     """It returns a dct_identifier graph isomorphic to spec."""
     catalog = Catalog()
diff --git a/tests/test_catalogrecord.py b/tests/test_catalogrecord.py
index e8ff1e0..231bc21 100644
--- a/tests/test_catalogrecord.py
+++ b/tests/test_catalogrecord.py
@@ -1,11 +1,11 @@
 """Test cases for the dataset module."""
+import pytest
 from pytest_mock import MockFixture
 from rdflib import Graph
 from rdflib.compare import graph_diff, isomorphic
 from skolemizer.testutils import skolemization
 
-from datacatalogtordf import CatalogRecord
-from datacatalogtordf import Dataset
+from datacatalogtordf import CatalogRecord, Dataset, InvalidURIError
 from tests.testutils import assert_isomorphic
 
 
@@ -285,6 +285,14 @@ def test_to_graph_should_return_conforms_to() -> None:
     assert _isomorphic
 
 
+def test_set_conforms_to_list_of_invalid_formats() -> None:
+    """Should raise InvalidURIError."""
+    catalogrecord = CatalogRecord()
+    catalogrecord.identifier = "http://example.com/catalogrecords/1"
+    with pytest.raises(InvalidURIError):
+        catalogrecord.conforms_to = ["http://invalid^.uri.com/format"]
+
+
 # ---------------------------------------------------------------------- #
 # Utils for displaying debug information
 
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
index 16d47c9..32db373 100644
--- a/tests/test_dataset.py
+++ b/tests/test_dataset.py
@@ -1,12 +1,19 @@
 """Test cases for the dataset module."""
 from decimal import Decimal
 
+import pytest
 from pytest_mock import MockFixture
 from rdflib import Graph
 from rdflib.compare import graph_diff, isomorphic
 from skolemizer.testutils import skolemization
 
-from datacatalogtordf import Dataset, Distribution, Location, PeriodOfTime
+from datacatalogtordf import (
+    Dataset,
+    Distribution,
+    InvalidURIError,
+    Location,
+    PeriodOfTime,
+)
 
 
 def test_to_graph_should_return_identifier_set_at_constructor() -> None:
@@ -390,6 +397,15 @@ def test_to_graph_should_return_access_rights_comment() -> None:
     assert _isomorphic
 
 
+def test_set_access_rights_with_list_of_invalid_uris() -> None:
+    """Should raise InvalidURIError."""
+    dataset = Dataset()
+    dataset.identifier = "http://example.com/datasets/1"
+
+    with pytest.raises(InvalidURIError):
+        dataset.access_rights_comments = ["http://invalid^.uri.com/format"]
+
+
 def test_to_graph_should_return_link_to_spatial() -> None:
     """It returns a spatial coverage graph isomorphic to spec."""
     dataset = Dataset()
diff --git a/tests/test_distribution.py b/tests/test_distribution.py
index 4b10dbb..17d23dd 100644
--- a/tests/test_distribution.py
+++ b/tests/test_distribution.py
@@ -1,13 +1,13 @@
 """Test cases for the distribution module."""
 from decimal import Decimal
 
+import pytest
 from pytest_mock import MockFixture
 from rdflib import Graph
 from rdflib.compare import graph_diff, isomorphic
 from skolemizer.testutils import skolemization
 
-from datacatalogtordf import DataService
-from datacatalogtordf import Distribution
+from datacatalogtordf import DataService, Distribution, InvalidURIError
 
 
 def test_to_graph_should_return_identifier_set_at_constructor() -> None:
@@ -503,6 +503,14 @@ def test_to_graph_should_return_conforms_to() -> None:
     assert _isomorphic
 
 
+def test_set_conforms_to_list_of_invalid_uris() -> None:
+    """Should raise InvalidURIError."""
+    distribution = Distribution()
+    distribution.identifier = "http://example.com/distributions/1"
+    with pytest.raises(InvalidURIError):
+        distribution.conforms_to = ["http://invalid^.uri.com/format"]
+
+
 def test_to_graph_should_return_media_type() -> None:
     """It returns a media type graph isomorphic to spec."""
     distribution = Distribution()
@@ -532,6 +540,14 @@ def test_to_graph_should_return_media_type() -> None:
     assert _isomorphic
 
 
+def test_set_media_types_list_of_invalid_uris() -> None:
+    """Should raise InvalidURIError."""
+    distribution = Distribution()
+    distribution.identifier = "http://example.com/distributions/1"
+    with pytest.raises(InvalidURIError):
+        distribution.media_types = ["http://invalid^.uri.com/format"]
+
+
 def test_to_graph_should_return_format() -> None:
     """It returns a format graph isomorphic to spec."""
     distribution = Distribution()
@@ -618,6 +634,14 @@ def test_to_graph_should_return_packaging_format() -> None:
     assert _isomorphic
 
 
+def test_set_format_with_list_of_invalid_formats() -> None:
+    """Should raise InvalidURIError."""
+    distribution = Distribution()
+    distribution.identifier = "http://example.com/distributions/1"
+    with pytest.raises(InvalidURIError):
+        distribution.formats = ["http://invalid^.uri.com/format"]
+
+
 # ---------------------------------------------------------------------- #
 # Utils for displaying debug information
 
diff --git a/tests/test_resource.py b/tests/test_resource.py
index 18b07c4..37628b8 100644
--- a/tests/test_resource.py
+++ b/tests/test_resource.py
@@ -5,7 +5,7 @@ import pytest
 from rdflib import Graph
 from rdflib.compare import graph_diff, isomorphic
 
-from datacatalogtordf import Agent, Dataset, Relationship, Resource
+from datacatalogtordf import Agent, Dataset, InvalidURIError, Relationship, Resource
 
 """
 A test class for testing the _abstract_ class Resource.
@@ -216,6 +216,14 @@ def test_to_graph_should_return_conformsTo() -> None:
     assert _isomorphic
 
 
+def test_set_conformsTo_list_of_invalid_uris() -> None:
+    """Should raise InvalidURIError."""
+    resource = Dataset()
+    resource.identifier = "http://example.com/datasets/1"
+    with pytest.raises(InvalidURIError):
+        resource.conformsTo = ["http://invalid^.uri.com/format"]
+
+
 def test_to_graph_should_return_contactpoint() -> None:
     """It returns a contactpoint graph isomorphic to spec."""
     resource = Dataset()
@@ -429,6 +437,14 @@ def test_to_graph_should_return_landingPage() -> None:
     assert _isomorphic
 
 
+def test_set_conforms_to_list_of_landing_page() -> None:
+    """Should raise InvalidURIError."""
+    resource = Dataset()
+    resource.identifier = "http://example.com/datasets/1"
+    with pytest.raises(InvalidURIError):
+        resource.landing_page = ["http://invalid^.uri.com/format"]
+
+
 def test_to_graph_should_return_license() -> None:
     """It returns a license graph isomorphic to spec."""
     resource = Dataset()
@@ -511,6 +527,14 @@ def test_to_graph_should_return_relation() -> None:
     assert _isomorphic
 
 
+def test_set_resource_relation_list_of_uris() -> None:
+    """Should raise InvalidURIError."""
+    resource = Dataset()
+    resource.identifier = "http://example.com/datasets/1"
+    with pytest.raises(InvalidURIError):
+        resource.resource_relation = ["http://invalid^.uri.com/format"]
+
+
 def test_to_graph_should_return_rights() -> None:
     """It returns a rights graph isomorphic to spec."""
     resource = Dataset()
@@ -631,6 +655,14 @@ def test_to_graph_should_return_theme() -> None:
     assert _isomorphic
 
 
+def test_set_theme_list_of_invalid_uris() -> None:
+    """Should raise InvalidURIError."""
+    resource = Dataset()
+    resource.identifier = "http://example.com/datasets/1"
+    with pytest.raises(InvalidURIError):
+        resource.theme = ["http://invalid^.uri.com/format"]
+
+
 def test_to_graph_should_return_type() -> None:
     """It returns a type graph isomorphic to spec."""
     resource = Dataset()