Skip to content

Commit 6f0cd72

Browse files
committed
feat: property cardinality
1 parent 28138d5 commit 6f0cd72

File tree

5 files changed

+126
-7
lines changed

5 files changed

+126
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ While AI assisted in development, all code was reviewed and tested.
3131
* **Code Generation:** Creates `msgspec.Struct` definitions from Schema.org types, including type hints and docstrings.
3232
* **Proper Inheritance:** Preserves the Schema.org class hierarchy using Python inheritance (`Book` inherits from `CreativeWork`, which inherits from `Thing`).
3333
* **JSON-LD Compatibility:** All models support JSON-LD fields (`@id`, `@type`, `@context`) that serialize correctly.
34+
* **Property Cardinality:** Implements Schema.org's multiple-value property model, where properties can take both single values and lists of values.
3435
* **Category Organization:** Organizes generated classes into subdirectories (CreativeWork, Person, etc.).
3536
* **Circular Dependency Resolution:** Uses forward references (`"TypeName"`) and `TYPE_CHECKING` imports.
3637
* **Python Compatibility:** Handles reserved keywords.

msgspec_schemaorg/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
- Maintains type safety with modern Python type annotations
1212
"""
1313

14-
__version__ = "0.1.4"
14+
__version__ = "0.1.5"
1515

1616
# Import the key functions and classes to expose at the package level
1717
from .generate import fetch_and_generate, SchemaProcessor

msgspec_schemaorg/generate.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -634,15 +634,18 @@ def generate_struct_code(self, schema_class_id: str) -> tuple[str, list[str]]:
634634

635635
# Create type annotation string
636636
if len(types) > 1:
637-
# Use Union for multiple types
638-
type_str = " | ".join(self._get_string_type_annotation(t) for t in types)
637+
# Use Union for multiple types, ensuring proper quotes for forward references
638+
type_parts = [self._get_string_type_annotation(t) for t in types]
639+
type_str = ", ".join(type_parts)
640+
# Use Union[] syntax instead of | for compatibility
641+
type_str = f"Union[{type_str}]"
639642
elif len(types) == 1:
640643
type_str = self._get_string_type_annotation(types[0])
641644
else:
642645
type_str = "str" # Default
643646

644-
# Make all fields optional for now
645-
code.append(f" {prop_name}: {type_str} | None = None")
647+
# Add support for multiple values (cardinality) - in Schema.org, properties can have multiple values by default
648+
code.append(f" {prop_name}: Union[List[{type_str}], {type_str}, None] = None")
646649

647650
# Mark if we need date handling
648651
if has_date_type:
@@ -700,7 +703,15 @@ def _get_string_type_annotation(self, type_obj: Union[type, str]) -> str:
700703
# Check if it's a Schema.org type (one of our model classes)
701704
for norm_name in self.normalized_class_names.values():
702705
if norm_name == type_obj:
703-
return f"'{type_obj}'" # Use string literal for all Schema.org types
706+
# Already a string (likely a class name for forward reference)
707+
# Strip quotes if already quoted to prevent double quoting
708+
clean_type = str(type_obj).strip("'\"")
709+
return f"'{clean_type}'"
710+
711+
# Handle special case for Union and List types
712+
if str(type_obj).startswith(('Union[', 'List[')):
713+
return str(type_obj)
714+
704715
# It's a primitive type or unknown
705716
return str(type_obj)
706717

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "msgspec-schemaorg"
7-
version = "0.1.4"
7+
version = "0.1.5"
88
description = "Generate Python msgspec.Struct classes from the Schema.org vocabulary"
99
readme = "README.md"
1010
requires-python = ">=3.10"

tests/test_cardinality.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""
2+
Test that Schema.org properties correctly support multiple values (cardinality).
3+
"""
4+
import json
5+
import unittest
6+
import msgspec
7+
from pathlib import Path
8+
9+
# This will only work after models are regenerated
10+
from msgspec_schemaorg.models import Book, Person, CreativeWork
11+
12+
13+
class TestCardinality(unittest.TestCase):
14+
"""Test case for Schema.org property cardinality."""
15+
16+
def test_single_value(self):
17+
"""Test that properties work with single values."""
18+
# Create a book with a single author
19+
author = Person(name="Jane Doe")
20+
book = Book(
21+
name="The Example",
22+
author=author,
23+
isbn="123456789X"
24+
)
25+
26+
# Serialize and deserialize
27+
json_data = msgspec.json.encode(book)
28+
# Load as dict for checking values
29+
data = json.loads(json_data)
30+
31+
# Check values
32+
self.assertEqual(data["name"], "The Example")
33+
self.assertEqual(data["author"]["name"], "Jane Doe")
34+
self.assertEqual(data["isbn"], "123456789X")
35+
36+
def test_multiple_values(self):
37+
"""Test that properties work with multiple values (list)."""
38+
# Create a book with multiple authors
39+
authors = [
40+
Person(name="Jane Doe"),
41+
Person(name="John Smith")
42+
]
43+
44+
book = Book(
45+
name="The Co-Authored Example",
46+
author=authors,
47+
isbn="987654321X"
48+
)
49+
50+
# Serialize and check the JSON
51+
json_data = msgspec.json.encode(book)
52+
data = json.loads(json_data)
53+
54+
# Check values
55+
self.assertEqual(data["name"], "The Co-Authored Example")
56+
self.assertEqual(len(data["author"]), 2)
57+
self.assertEqual(data["author"][0]["name"], "Jane Doe")
58+
self.assertEqual(data["author"][1]["name"], "John Smith")
59+
self.assertEqual(data["isbn"], "987654321X")
60+
61+
def test_mixed_property_types(self):
62+
"""Test properties that accept multiple types."""
63+
# CreativeWork.about can be any Thing
64+
work1 = CreativeWork(name="About a Person", about=Person(name="Subject Person"))
65+
work2 = CreativeWork(name="About a Book", about=Book(name="Subject Book"))
66+
67+
# Serialize and check
68+
json_data1 = msgspec.json.encode(work1)
69+
json_data2 = msgspec.json.encode(work2)
70+
71+
data1 = json.loads(json_data1)
72+
data2 = json.loads(json_data2)
73+
74+
# Check values
75+
self.assertEqual(data1["about"]["name"], "Subject Person")
76+
self.assertEqual(data2["about"]["name"], "Subject Book")
77+
78+
def test_mixed_cardinality_and_types(self):
79+
"""Test properties with both multiple types and multiple values."""
80+
# Book can have multiple authors that can be either Person or Organization
81+
from msgspec_schemaorg.models import Organization
82+
83+
book = Book(
84+
name="Complex Example",
85+
author=[
86+
Person(name="First Author"),
87+
Organization(name="Publishing Group"),
88+
Person(name="Second Author")
89+
],
90+
isbn="555555555X"
91+
)
92+
93+
# Serialize and check
94+
json_data = msgspec.json.encode(book)
95+
data = json.loads(json_data)
96+
97+
# Check values
98+
self.assertEqual(data["name"], "Complex Example")
99+
self.assertEqual(len(data["author"]), 3)
100+
self.assertEqual(data["author"][0]["name"], "First Author")
101+
self.assertEqual(data["author"][1]["name"], "Publishing Group")
102+
self.assertEqual(data["author"][2]["name"], "Second Author")
103+
self.assertEqual(data["isbn"], "555555555X")
104+
105+
106+
if __name__ == "__main__":
107+
unittest.main()

0 commit comments

Comments
 (0)