''' Serializers for conceptual schema API. ''' import json from typing import Optional, cast from rest_framework import serializers from django.db import transaction import pyconcept from cctext import Resolver, Reference, ReferenceType, EntityReference, SyntacticReference from .utils import fix_old_references from .models import Constituenta, LibraryItem, RSForm, Version from . import messages as msg _CST_TYPE = 'constituenta' _TRS_TYPE = 'rsform' _TRS_VERSION_MIN = 16 _TRS_VERSION = 16 _TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022' ConstituentaID = serializers.IntegerField NodeID = serializers.IntegerField class FileSerializer(serializers.Serializer): ''' Serializer: File input. ''' file = serializers.FileField(allow_empty_file=False) class ExpressionSerializer(serializers.Serializer): ''' Serializer: RSLang expression. ''' expression = serializers.CharField() class WordFormSerializer(serializers.Serializer): ''' Serializer: inflect request. ''' text = serializers.CharField() grams = serializers.CharField() class MultiFormSerializer(serializers.Serializer): ''' Serializer: inflect request. ''' items = serializers.ListField( child=WordFormSerializer() ) @staticmethod def from_list(data: list[tuple[str, str]]) -> dict: result: dict = {} result['items'] = [] for item in data: result['items'].append({ 'text': item[0], 'grams': item[1] }) return result class TextSerializer(serializers.Serializer): ''' Serializer: Text with references. ''' text = serializers.CharField() class LibraryItemSerializer(serializers.ModelSerializer): ''' Serializer: LibraryItem entry. ''' class Meta: ''' serializer metadata. ''' model = LibraryItem fields = '__all__' read_only_fields = ('owner', 'id', 'item_type') class FunctionArgSerializer(serializers.Serializer): ''' Serializer: RSLang function argument type. ''' alias = serializers.CharField() typification = serializers.CharField() class CstParseSerializer(serializers.Serializer): ''' Serializer: Constituenta parse result. ''' status = serializers.CharField() valueClass = serializers.CharField() typification = serializers.CharField() syntaxTree = serializers.CharField() args = serializers.ListField( child=FunctionArgSerializer() ) class ErrorDescriptionSerializer(serializers.Serializer): ''' Serializer: RSError description. ''' errorType = serializers.IntegerField() position = serializers.IntegerField() isCritical = serializers.BooleanField() params = serializers.ListField( child=serializers.CharField() ) class NodeDataSerializer(serializers.Serializer): ''' Serializer: Node data. ''' dataType = serializers.CharField() value = serializers.CharField() class ASTNodeSerializer(serializers.Serializer): ''' Serializer: Syntax tree node. ''' uid = NodeID() parent = serializers.IntegerField() # type: ignore typeID = serializers.IntegerField() start = serializers.IntegerField() finish = serializers.IntegerField() data = NodeDataSerializer() # type: ignore class ExpressionParseSerializer(serializers.Serializer): ''' Serializer: RSlang expression parse result. ''' parseResult = serializers.BooleanField() syntax = serializers.CharField() typification = serializers.CharField() valueClass = serializers.CharField() astText = serializers.CharField() ast = serializers.ListField( child=ASTNodeSerializer() ) errors = serializers.ListField( # type: ignore child=ErrorDescriptionSerializer() ) args = serializers.ListField( child=FunctionArgSerializer() ) class VersionSerializer(serializers.ModelSerializer): ''' Serializer: Version data. ''' class Meta: ''' serializer metadata. ''' model = Version fields = 'id', 'version', 'item', 'description', 'time_create' read_only_fields = ('id', 'item', 'time_create') class VersionInnerSerializer(serializers.ModelSerializer): ''' Serializer: Version data for list of versions. ''' class Meta: ''' serializer metadata. ''' model = Version fields = 'id', 'version', 'description', 'time_create' read_only_fields = ('id', 'item', 'time_create') class VersionCreateSerializer(serializers.ModelSerializer): ''' Serializer: Version create data. ''' class Meta: ''' serializer metadata. ''' model = Version fields = 'version', 'description' class LibraryItemDetailsSerializer(serializers.ModelSerializer): ''' Serializer: LibraryItem detailed data. ''' subscribers = serializers.SerializerMethodField() versions = serializers.SerializerMethodField() class Meta: ''' serializer metadata. ''' model = LibraryItem fields = '__all__' read_only_fields = ('owner', 'id', 'item_type') def get_subscribers(self, instance: LibraryItem) -> list[int]: return [item.pk for item in instance.subscribers()] def get_versions(self, instance: LibraryItem) -> list: return [VersionInnerSerializer(item).data for item in instance.versions()] class ConstituentaSerializer(serializers.ModelSerializer): ''' Serializer: Constituenta data. ''' class Meta: ''' serializer metadata. ''' model = Constituenta fields = '__all__' read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') def update(self, instance: Constituenta, validated_data) -> Constituenta: data = validated_data # Note: use alias for better code readability schema = RSForm(instance.schema) definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None term: Optional[str] = data['term_raw'] if 'term_raw' in data else None term_changed = 'term_forms' in data if definition is not None and definition != instance.definition_raw : data['definition_resolved'] = schema.resolver().resolve(definition) if term is not None and term != instance.term_raw: data['term_resolved'] = schema.resolver().resolve(term) if data['term_resolved'] != instance.term_resolved and 'term_forms' not in data: data['term_forms'] = [] term_changed = data['term_resolved'] != instance.term_resolved result: Constituenta = super().update(instance, data) if term_changed: schema.on_term_change([result.alias]) result.refresh_from_db() schema.item.save() return result class CstCreateSerializer(serializers.ModelSerializer): ''' Serializer: Constituenta creation. ''' insert_after = serializers.IntegerField(required=False, allow_null=True) class Meta: ''' serializer metadata. ''' model = Constituenta fields = \ 'alias', 'cst_type', 'convention', \ 'term_raw', 'definition_raw', 'definition_formal', \ 'insert_after', 'term_forms' class CstRenameSerializer(serializers.ModelSerializer): ''' Serializer: Constituenta renaming. ''' class Meta: ''' serializer metadata. ''' model = Constituenta fields = 'id', 'alias', 'cst_type' def validate(self, attrs): schema = cast(RSForm, self.context['schema']) old_cst = Constituenta.objects.get(pk=self.initial_data['id']) new_alias = self.initial_data['alias'] if old_cst.schema != schema.item: raise serializers.ValidationError({ 'id': msg.constituentaNotOwned(schema.item.title) }) if old_cst.alias == new_alias: raise serializers.ValidationError({ 'alias': msg.renameTrivial(new_alias) }) if schema.constituents().filter(alias=new_alias).exists(): raise serializers.ValidationError({ 'alias': msg.renameTaken(new_alias) }) self.instance = old_cst attrs['schema'] = schema.item attrs['id'] = self.initial_data['id'] return attrs class CstSubstituteSerializer(serializers.Serializer): ''' Serializer: Constituenta substitution. ''' original = ConstituentaID() substitution = ConstituentaID() transfer_term = serializers.BooleanField(required=False, default=False) def validate(self, attrs): schema = cast(RSForm, self.context['schema']) original_cst = Constituenta.objects.get(pk=self.initial_data['original']) substitution_cst = Constituenta.objects.get(pk=self.initial_data['substitution']) if original_cst.alias == substitution_cst.alias: raise serializers.ValidationError({ 'alias': msg.substituteTrivial(original_cst.alias) }) if original_cst.schema != schema.item: raise serializers.ValidationError({ 'original': msg.constituentaNotOwned(schema.item.title) }) if substitution_cst.schema != schema.item: raise serializers.ValidationError({ 'substitution': msg.constituentaNotOwned(schema.item.title) }) attrs['original'] = original_cst attrs['substitution'] = substitution_cst attrs['transfer_term'] = self.initial_data['transfer_term'] return attrs class CstListSerializer(serializers.Serializer): ''' Serializer: List of constituents from one origin. ''' items = serializers.ListField( child=serializers.IntegerField() ) def validate(self, attrs): schema = self.context['schema'] cstList = [] for item in attrs['items']: try: cst = Constituenta.objects.get(pk=item) except Constituenta.DoesNotExist as exception: raise serializers.ValidationError({ f'{item}': msg.constituentaNotExists }) from exception if cst.schema != schema.item: raise serializers.ValidationError({ f'{item}': msg.constituentaNotOwned(schema.item.title) }) cstList.append(cst) attrs['constituents'] = cstList return attrs class CstMoveSerializer(CstListSerializer): ''' Serializer: Change constituenta position. ''' move_to = serializers.IntegerField() class TextPositionSerializer(serializers.Serializer): ''' Serializer: Text position. ''' start = serializers.IntegerField() finish = serializers.IntegerField() class ReferenceDataSerializer(serializers.Serializer): ''' Serializer: Reference data - Union of all references. ''' offset = serializers.IntegerField() nominal = serializers.CharField() entity = serializers.CharField() form = serializers.CharField() class ReferenceSerializer(serializers.Serializer): ''' Serializer: Language reference. ''' type = serializers.CharField() data = ReferenceDataSerializer() # type: ignore pos_input = TextPositionSerializer() pos_output = TextPositionSerializer() class ResolverSerializer(serializers.Serializer): ''' Serializer: Resolver results serializer. ''' input = serializers.CharField() output = serializers.CharField() refs = serializers.ListField( child=ReferenceSerializer() ) def to_representation(self, instance: Resolver) -> dict: return { 'input': instance.input, 'output': instance.output, 'refs': [{ 'type': ref.ref.get_type().value, 'data': self._get_reference_data(ref.ref), 'resolved': ref.resolved, 'pos_input': { 'start': ref.pos_input.start, 'finish': ref.pos_input.finish }, 'pos_output': { 'start': ref.pos_output.start, 'finish': ref.pos_output.finish } } for ref in instance.refs] } @staticmethod def _get_reference_data(ref: Reference) -> dict: if ref.get_type() == ReferenceType.entity: return { 'entity': cast(EntityReference, ref).entity, 'form': cast(EntityReference, ref).form } else: return { 'offset': cast(SyntacticReference, ref).offset, 'nominal': cast(SyntacticReference, ref).nominal } class PyConceptAdapter: ''' RSForm adapter for interacting with pyconcept module. ''' def __init__(self, instance: RSForm): self.schema = instance self.data = self._prepare_request() self._checked_data: Optional[dict] = None def parse(self) -> dict: ''' Check RSForm and return check results. Warning! Does not include texts. ''' self._produce_response() if self._checked_data is None: raise ValueError(msg.pyconceptFailure()) return self._checked_data def _prepare_request(self) -> dict: result: dict = { 'items': [] } items = self.schema.constituents().order_by('order') for cst in items: result['items'].append({ 'entityUID': cst.pk, 'cstType': cst.cst_type, 'alias': cst.alias, 'definition': { 'formal': cst.definition_formal } }) return result def _produce_response(self): if self._checked_data is not None: return response = pyconcept.check_schema(json.dumps(self.data)) data = json.loads(response) self._checked_data = { 'items': [] } for cst in data['items']: self._checked_data['items'].append({ 'id': cst['entityUID'], 'cstType': cst['cstType'], 'alias': cst['alias'], 'definition': { 'formal': cst['definition']['formal'] }, 'parse': cst['parse'] }) class RSFormSerializer(serializers.ModelSerializer): ''' Serializer: Detailed data for RSForm. ''' subscribers = serializers.ListField( child=serializers.IntegerField() ) items = serializers.ListField( child=ConstituentaSerializer() ) class Meta: ''' serializer metadata. ''' model = LibraryItem fields = '__all__' def to_representation(self, instance: LibraryItem) -> dict: result = LibraryItemDetailsSerializer(instance).data schema = RSForm(instance) result['items'] = [] for cst in schema.constituents().order_by('order'): result['items'].append(ConstituentaSerializer(cst).data) return result def to_versioned_data(self) -> dict: ''' Create serializable version representation without redundant data. ''' result = self.to_representation(cast(LibraryItem, self.instance)) del result['versions'] del result['subscribers'] del result['owner'] del result['is_common'] del result['is_canonical'] del result['time_create'] del result['time_update'] return result def from_versioned_data(self, version: int, data: dict) -> dict: ''' Load data from version. ''' result = self.to_representation(cast(LibraryItem, self.instance)) result['version'] = version return result | data class CstDetailsSerializer(serializers.ModelSerializer): ''' Serializer: Constituenta data including parse. ''' parse = CstParseSerializer() class Meta: ''' serializer metadata. ''' model = Constituenta fields = '__all__' class RSFormParseSerializer(serializers.ModelSerializer): ''' Serializer: Detailed data for RSForm including parse. ''' subscribers = serializers.ListField( child=serializers.IntegerField() ) items = serializers.ListField( child=CstDetailsSerializer() ) class Meta: ''' serializer metadata. ''' model = LibraryItem fields = '__all__' def to_representation(self, instance: LibraryItem): result = RSFormSerializer(instance).data parse = PyConceptAdapter(RSForm(instance)).parse() for cst_data in result['items']: cst_data['parse'] = next( cst['parse'] for cst in parse['items'] if cst['id'] == cst_data['id'] ) return result class RSFormUploadSerializer(serializers.Serializer): ''' Upload data for RSForm serializer. ''' file = serializers.FileField() load_metadata = serializers.BooleanField() class RSFormTRSSerializer(serializers.Serializer): ''' Serializer: TRS file production and loading for RSForm. ''' def to_representation(self, instance: RSForm) -> dict: result = self._prepare_json_rsform(instance) items = instance.constituents().order_by('order') for cst in items: result['items'].append(self._prepare_json_constituenta(cst)) return result @staticmethod def _prepare_json_rsform(schema: RSForm) -> dict: return { 'type': _TRS_TYPE, 'title': schema.item.title, 'alias': schema.item.alias, 'comment': schema.item.comment, 'items': [], 'claimed': False, 'selection': [], 'version': _TRS_VERSION, 'versionInfo': _TRS_HEADER } @staticmethod def _prepare_json_constituenta(cst: Constituenta) -> dict: return { 'entityUID': cst.pk, 'type': _CST_TYPE, 'cstType': cst.cst_type, 'alias': cst.alias, 'convention': cst.convention, 'term': { 'raw': cst.term_raw, 'resolved': cst.term_resolved, 'forms': cst.term_forms }, 'definition': { 'formal': cst.definition_formal, 'text': { 'raw': cst.definition_raw, 'resolved': cst.definition_resolved }, }, } def to_internal_value(self, data): result = super().to_internal_value(data) if 'owner' in data: result['owner'] = data['owner'] if 'is_common' in data: result['is_common'] = data['is_common'] if 'is_canonical' in data: result['is_canonical'] = data['is_canonical'] result['items'] = data.get('items', []) if self.context['load_meta']: result['title'] = data.get('title', 'Без названия') result['alias'] = data.get('alias', '') result['comment']= data.get('comment', '') if 'id' in data: result['id'] = data['id'] self.instance = RSForm(LibraryItem.objects.get(pk=result['id'])) return result def validate(self, attrs: dict): if 'version' not in self.initial_data \ or self.initial_data['version'] < _TRS_VERSION_MIN \ or self.initial_data['version'] > _TRS_VERSION: raise serializers.ValidationError({ 'version': msg.exteorFileVersionNotSupported() }) return attrs @transaction.atomic def create(self, validated_data: dict) -> RSForm: self.instance: RSForm = RSForm.create( owner=validated_data.get('owner', None), alias=validated_data['alias'], title=validated_data['title'], comment=validated_data['comment'], is_common=validated_data['is_common'], is_canonical=validated_data['is_canonical'] ) self.instance.item.save() order = 1 for cst_data in validated_data['items']: cst = Constituenta( alias=cst_data['alias'], schema=self.instance.item, order=order, cst_type=cst_data['cstType'], ) self._load_cst_texts(cst, cst_data) cst.save() order += 1 self.instance.resolve_all_text() return self.instance @transaction.atomic def update(self, instance: RSForm, validated_data) -> RSForm: if 'alias' in validated_data: instance.item.alias = validated_data['alias'] if 'title' in validated_data: instance.item.title = validated_data['title'] if 'comment' in validated_data: instance.item.comment = validated_data['comment'] order = 1 prev_constituents = instance.constituents() loaded_ids = set() for cst_data in validated_data['items']: uid = int(cst_data['entityUID']) if prev_constituents.filter(pk=uid).exists(): cst: Constituenta = prev_constituents.get(pk=uid) cst.order = order cst.alias = cst_data['alias'] cst.cst_type = cst_data['cstType'] self._load_cst_texts(cst, cst_data) cst.save() else: cst = Constituenta( alias=cst_data['alias'], schema=instance.item, order=order, cst_type=cst_data['cstType'], ) self._load_cst_texts(cst, cst_data) cst.save() uid = cst.pk loaded_ids.add(uid) order += 1 for prev_cst in prev_constituents: if prev_cst.pk not in loaded_ids: prev_cst.delete() instance.resolve_all_text() instance.item.save() return instance @staticmethod def _load_cst_texts(cst: Constituenta, data: dict): cst.convention = data.get('convention', '') if 'definition' in data: cst.definition_formal = data['definition'].get('formal', '') if 'text' in data['definition']: cst.definition_raw = fix_old_references(data['definition']['text'].get('raw', '')) else: cst.definition_raw = '' if 'term' in data: cst.term_raw = fix_old_references(data['term'].get('raw', '')) cst.term_forms = data['term'].get('forms', []) else: cst.term_raw = '' cst.term_forms = [] class ResultTextResponse(serializers.Serializer): ''' Serializer: Text result of a function call. ''' result = serializers.CharField() class NewCstResponse(serializers.Serializer): ''' Serializer: Create cst response. ''' new_cst = ConstituentaSerializer() schema = RSFormParseSerializer() class NewVersionResponse(serializers.Serializer): ''' Serializer: Create cst response. ''' version = serializers.IntegerField() schema = RSFormParseSerializer()