add json converter

Changed files
+149
src
atpasser
+3
src/atpasser/model/__init__.py
···
ProcedureModel,
SubscriptionModel
)
__all__ = [
"DataModel",
···
"QueryModel",
"ProcedureModel",
"SubscriptionModel",
# Exceptions
"AtprotoModelError",
"ValidationError",
···
ProcedureModel,
SubscriptionModel
)
+
from .converter import LexiconConverter
__all__ = [
"DataModel",
···
"QueryModel",
"ProcedureModel",
"SubscriptionModel",
+
# Converter
+
"LexiconConverter",
# Exceptions
"AtprotoModelError",
"ValidationError",
+146
src/atpasser/model/converter.py
···
···
+
"""
+
Lexicon JSON to Data Model Converter
+
+
Provides bidirectional conversion between Lexicon JSON schemas and Python data models.
+
"""
+
import json
+
from typing import Any, Dict, Type, Union, TypeVar
+
from pydantic import BaseModel
+
from .exceptions import ValidationError, SerializationError
+
from .types.primitive import NullModel, BooleanModel, IntegerModel
+
from .types.string import StringModel
+
from .types.complex import ArrayModel, ObjectModel, ParamsModel
+
from .types.reference import TokenModel, RefModel, UnionModel
+
from .types.special import (
+
UnknownModel, RecordModel, QueryModel,
+
ProcedureModel, SubscriptionModel
+
)
+
+
class LexiconConverter:
+
"""
+
Bidirectional converter between Lexicon JSON and Python data models.
+
+
Attributes:
+
modelMap (Dict[str, Type[BaseModel]]): Mapping from Lexicon types to model classes
+
"""
+
+
modelMap = {
+
# Primitive types
+
"null": NullModel,
+
"boolean": BooleanModel,
+
"integer": IntegerModel,
+
"string": StringModel,
+
+
# Complex types
+
"array": ArrayModel,
+
"object": ObjectModel,
+
"params": ParamsModel,
+
+
# Reference types
+
"token": TokenModel,
+
"ref": RefModel,
+
"union": UnionModel,
+
+
# Special types
+
"unknown": UnknownModel,
+
"record": RecordModel,
+
"query": QueryModel,
+
"procedure": ProcedureModel,
+
"subscription": SubscriptionModel,
+
}
+
+
@classmethod
+
def fromLexicon(cls, lexiconJson: Union[str, Dict[str, Any]]) -> BaseModel:
+
"""
+
Convert Lexicon JSON to Python data model instance.
+
+
Args:
+
lexiconJson: Lexicon JSON string or dict
+
+
Returns:
+
Instance of appropriate Pydantic model
+
+
Raises:
+
ValidationError: If JSON is invalid or doesn't match schema
+
SerializationError: If conversion fails
+
"""
+
if isinstance(lexiconJson, str):
+
try:
+
data = json.loads(lexiconJson)
+
except json.JSONDecodeError as e:
+
raise ValidationError("lexiconJson", f"Invalid JSON: {e}")
+
else:
+
data = lexiconJson
+
+
if not isinstance(data, dict):
+
raise ValidationError("lexiconJson", "Lexicon must be a JSON object")
+
+
if "type" not in data:
+
raise ValidationError("type", "Lexicon definition must have 'type' field")
+
+
typeName = data["type"]
+
if typeName not in cls.modelMap:
+
raise ValidationError("type", f"Unknown Lexicon type: {typeName}")
+
+
try:
+
modelClass = cls.modelMap[typeName]
+
return modelClass(**data)
+
except Exception as e:
+
raise SerializationError(typeName, f"Failed to create model: {e}")
+
+
@classmethod
+
def toLexicon(cls, modelInstance: BaseModel) -> Dict[str, Any]:
+
"""
+
Convert Python model instance back to Lexicon JSON format.
+
+
Args:
+
modelInstance: Instance of a Lexicon model
+
+
Returns:
+
Dictionary representing Lexicon JSON
+
+
Raises:
+
SerializationError: If conversion fails
+
"""
+
if not isinstance(modelInstance, BaseModel):
+
raise SerializationError("modelInstance", "Input must be a Pydantic model")
+
+
try:
+
# Get the model's type name by reverse lookup
+
modelType = None
+
for typeName, modelClass in cls.modelMap.items():
+
if isinstance(modelInstance, modelClass):
+
modelType = typeName
+
break
+
+
if modelType is None:
+
raise SerializationError("modelInstance", "Unknown model type")
+
+
# Convert to dict and add type field
+
result = modelInstance.model_dump(exclude_unset=True)
+
result["type"] = modelType
+
+
# Handle special cases
+
if modelType == "record" and "type" in result:
+
result["$type"] = result.pop("type")
+
+
return result
+
except Exception as e:
+
raise SerializationError("toLexicon", f"Failed to serialize model: {e}")
+
+
@classmethod
+
def validateLexicon(cls, lexiconJson: Union[str, Dict[str, Any]]) -> bool:
+
"""
+
Validate Lexicon JSON against schema definitions.
+
+
Args:
+
lexiconJson: Lexicon JSON to validate
+
+
Returns:
+
True if valid, False otherwise
+
"""
+
try:
+
cls.fromLexicon(lexiconJson)
+
return True
+
except (ValidationError, SerializationError):
+
return False