import base64 from datetime import datetime from typing import List from uuid import UUID from app.api.topics.schemas import Topic, TopicMessage from app.core.config import ( ENCODING_ASCII, ENCODING_UTF8, TABLE_PARTICIPANTS, TOPIC_EXPECTED_PARTS, TOPIC_INNER_SEPARATOR, TOPIC_LIST_SEPARATOR, TOPIC_MESSAGE_PART_SEPARATOR, TOPIC_PARSE_EMPTY_ERROR, TOPIC_PARSE_INVALID_ERROR, TOPIC_SEPARATOR, ) from app.core.link import Link, ParticipantLink class TopicLineParser: SEPARATOR = TOPIC_SEPARATOR EXPECTED_PARTS = TOPIC_EXPECTED_PARTS LIST_SEPARATOR = TOPIC_LIST_SEPARATOR INNER_SEPARATOR = TOPIC_INNER_SEPARATOR MESSAGE_PART_SEPARATOR = TOPIC_MESSAGE_PART_SEPARATOR @staticmethod def _decode_message(encoded: str) -> str: return base64.b64decode(encoded.encode(ENCODING_ASCII)).decode(ENCODING_UTF8) @staticmethod def _encode_message(value: str) -> str: return base64.b64encode(value.encode(ENCODING_UTF8)).decode(ENCODING_ASCII) @classmethod def _parse_participants(cls, raw: str) -> List[ParticipantLink]: if not raw: return [] links: List[ParticipantLink] = [] for value in raw.split(cls.INNER_SEPARATOR): if not value: continue links.append(ParticipantLink.from_raw(value)) return links @classmethod def _parse_messages(cls, raw: str) -> List[TopicMessage]: if not raw: return [] messages: List[TopicMessage] = [] for chunk in raw.split(cls.LIST_SEPARATOR): if not chunk: continue try: participant_raw, encoded_content = chunk.split(cls.MESSAGE_PART_SEPARATOR, 1) except ValueError: continue messages.append( TopicMessage( participant_id=ParticipantLink.from_raw(participant_raw), content=cls._decode_message(encoded_content), ) ) return messages @classmethod def _serialize_participants(cls, participants: List[ParticipantLink]) -> str: return cls.INNER_SEPARATOR.join(participant.as_path() for participant in participants) @classmethod def _serialize_messages(cls, messages: List[TopicMessage]) -> str: return cls.LIST_SEPARATOR.join( cls.MESSAGE_PART_SEPARATOR.join( [message.participant_id.as_path(), cls._encode_message(message.content)] ) for message in messages ) @classmethod def parse(cls, line: str) -> Topic: raw = line.strip() if not raw: raise ValueError(TOPIC_PARSE_EMPTY_ERROR) parts = raw.split(cls.SEPARATOR) if len(parts) != cls.EXPECTED_PARTS: raise ValueError(TOPIC_PARSE_INVALID_ERROR) ( topic_id, title, description, created_at_value, participants_raw, messages_raw, ) = parts participants = cls._parse_participants(participants_raw) messages = cls._parse_messages(messages_raw) return Topic( id=UUID(topic_id), title=title, description=description, created_at=datetime.fromisoformat(created_at_value), participants=participants, messages=messages, ) @classmethod def serialize(cls, topic: Topic) -> str: return cls.SEPARATOR.join( [ str(topic.id), topic.title, topic.description, topic.created_at.isoformat(), cls._serialize_participants(topic.participants), cls._serialize_messages(topic.messages), ] )