diff --git a/rsconcept/backend/apps/library/models/LibraryItem.py b/rsconcept/backend/apps/library/models/LibraryItem.py index bf0f6082..aca27641 100644 --- a/rsconcept/backend/apps/library/models/LibraryItem.py +++ b/rsconcept/backend/apps/library/models/LibraryItem.py @@ -107,6 +107,7 @@ class LibraryItem(Model): verbose_name = 'Схема' verbose_name_plural = 'Схемы' + # pylint: disable=invalid-str-returned def __str__(self) -> str: return f'{self.alias}' diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 6ab9fae6..a55a6923 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -400,7 +400,7 @@ class OperationSchema: if child_schema.change_cst_type(successor_id, ctype): self._cascade_change_cst_type(child_id, successor_id, ctype) - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, too-many-positional-arguments def _cascade_update_cst( self, operation: int, diff --git a/rsconcept/backend/apps/rsform/serializers/__init__.py b/rsconcept/backend/apps/rsform/serializers/__init__.py index 9792f640..ead9c317 100644 --- a/rsconcept/backend/apps/rsform/serializers/__init__.py +++ b/rsconcept/backend/apps/rsform/serializers/__init__.py @@ -2,6 +2,7 @@ from .basics import ( ASTNodeSerializer, + ConstituentaCheckSerializer, ExpressionParseSerializer, ExpressionSerializer, InheritanceDataSerializer, diff --git a/rsconcept/backend/apps/rsform/serializers/basics.py b/rsconcept/backend/apps/rsform/serializers/basics.py index d9ebe782..33f6c6a9 100644 --- a/rsconcept/backend/apps/rsform/serializers/basics.py +++ b/rsconcept/backend/apps/rsform/serializers/basics.py @@ -10,6 +10,13 @@ class ExpressionSerializer(serializers.Serializer): expression = serializers.CharField() +class ConstituentaCheckSerializer(serializers.Serializer): + ''' Serializer: RSLang expression. ''' + alias = serializers.CharField() + definition_formal = serializers.CharField(allow_blank=True) + cst_type = serializers.CharField() + + class WordFormSerializer(serializers.Serializer): ''' Serializer: inflect request. ''' text = serializers.CharField() @@ -85,6 +92,7 @@ class ASTNodeSerializer(serializers.Serializer): class ExpressionParseSerializer(serializers.Serializer): ''' Serializer: RSlang expression parse result. ''' parseResult = serializers.BooleanField() + prefixLen = serializers.IntegerField() syntax = serializers.CharField() typification = serializers.CharField() valueClass = serializers.CharField() diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py index d3a70547..4310ea79 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -113,8 +113,8 @@ class TestRSFormViewset(EndpointTester): self.executeForbidden(item=self.private_id) - @decl_endpoint('/api/rsforms/{item}/check', method='post') - def test_check(self): + @decl_endpoint('/api/rsforms/{item}/check-expression', method='post') + def test_check_expression(self): self.owned.insert_new('X1') data = {'expression': 'X1=X1'} response = self.executeOK(data=data, item=self.owned_id) @@ -127,6 +127,24 @@ class TestRSFormViewset(EndpointTester): self.executeOK(data=data, item=self.unowned_id) + @decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post') + def test_check_constituenta(self): + self.owned.insert_new('X1') + data = {'definition_formal': 'X1=X1', 'alias': 'A111', 'cst_type': CstType.AXIOM} + response = self.executeOK(data=data, item=self.owned_id) + self.assertEqual(response.data['parseResult'], True) + self.assertEqual(response.data['syntax'], 'math') + self.assertEqual(response.data['astText'], '[:==[A111][=[X1][X1]]]') + self.assertEqual(response.data['typification'], 'LOGIC') + self.assertEqual(response.data['valueClass'], 'value') + + @decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post') + def test_check_constituenta_error(self): + self.owned.insert_new('X1') + data = {'definition_formal': 'X1=X1', 'alias': 'D111', 'cst_type': CstType.TERM} + response = self.executeOK(data=data, item=self.owned_id) + self.assertEqual(response.data['parseResult'], False) + @decl_endpoint('/api/rsforms/{item}/resolve', method='post') def test_resolve(self): x1 = self.owned.insert_new( diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index 37d0b222..7cbf6294 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -56,7 +56,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr 'details', 'export_trs', 'resolve', - 'check' + 'check_expression', + 'check_constituenta' ]: permission_list = [permissions.ItemAnyone] else: @@ -424,8 +425,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr c.HTTP_404_NOT_FOUND: None }, ) - @action(detail=True, methods=['post'], url_path='check') - def check(self, request: Request, pk) -> HttpResponse: + @action(detail=True, methods=['post'], url_path='check-expression') + def check_expression(self, request: Request, pk) -> HttpResponse: ''' Endpoint: Check RSLang expression against schema context. ''' serializer = s.ExpressionSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -437,6 +438,31 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr data=json.loads(result) ) + @extend_schema( + summary='check expression for specific CstType', + tags=['RSForm', 'FormalLanguage'], + request=s.ConstituentaCheckSerializer, + responses={ + c.HTTP_200_OK: s.ExpressionParseSerializer, + c.HTTP_404_NOT_FOUND: None + }, + ) + @action(detail=True, methods=['post'], url_path='check-constituenta') + def check_constituenta(self, request: Request, pk) -> HttpResponse: + ''' Endpoint: Check RSLang expression against schema context. ''' + serializer = s.ConstituentaCheckSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + expression = serializer.validated_data['definition_formal'] + alias = serializer.validated_data['alias'] + cst_type = cast(m.CstType, serializer.validated_data['cst_type']) + + pySchema = s.PyConceptAdapter(m.RSForm(self.get_object())) + result = pyconcept.check_constituenta(json.dumps(pySchema.data), alias, expression, cst_type) + return Response( + status=c.HTTP_200_OK, + data=json.loads(result) + ) + @extend_schema( summary='resolve text with references', tags=['RSForm', 'NaturalLanguage'], diff --git a/rsconcept/backend/requirements-dev-lock.txt b/rsconcept/backend/requirements-dev-lock.txt index 4e705092..15ac8ccf 100644 --- a/rsconcept/backend/requirements-dev-lock.txt +++ b/rsconcept/backend/requirements-dev-lock.txt @@ -8,13 +8,12 @@ drf-spectacular-sidecar==2024.7.1 coreapi==2.3.3 django-rest-passwordreset==1.4.1 cctext==0.1.4 -pyconcept==0.1.6 +pyconcept==0.1.8 psycopg2-binary==2.9.9 gunicorn==23.0.0 - djangorestframework-stubs==3.15.1 django-extensions==3.2.3 mypy==1.11.2 -pylint==3.2.7 +pylint==3.3.0 coverage==7.6.1 \ No newline at end of file diff --git a/rsconcept/backend/requirements.txt b/rsconcept/backend/requirements.txt index 1a11e390..8296f987 100644 --- a/rsconcept/backend/requirements.txt +++ b/rsconcept/backend/requirements.txt @@ -8,7 +8,7 @@ drf-spectacular-sidecar==2024.7.1 coreapi==2.3.3 django-rest-passwordreset==1.4.1 cctext==0.1.4 -pyconcept==0.1.6 +pyconcept==0.1.8 psycopg2-binary==2.9.9 gunicorn==23.0.0 \ No newline at end of file diff --git a/rsconcept/frontend/src/backend/rsforms.ts b/rsconcept/frontend/src/backend/rsforms.ts index c9f3f0eb..bd2d689e 100644 --- a/rsconcept/frontend/src/backend/rsforms.ts +++ b/rsconcept/frontend/src/backend/rsforms.ts @@ -5,6 +5,7 @@ import { ILibraryCreateData, ILibraryItem } from '@/models/library'; import { ICstSubstituteData } from '@/models/oss'; import { + ICheckConstituentaData, IConstituentaList, IConstituentaMeta, ICstCreateData, @@ -18,7 +19,7 @@ import { IRSFormUploadData, ITargetCst } from '@/models/rsform'; -import { IExpressionParse, IRSExpression } from '@/models/rslang'; +import { IExpressionParse } from '@/models/rslang'; import { AxiosGet, AxiosPatch, AxiosPost, FrontExchange, FrontPull } from './apiTransport'; @@ -113,9 +114,12 @@ export function patchMoveConstituenta(schema: string, request: FrontExchange) { +export function postCheckConstituenta( + schema: string, + request: FrontExchange +) { AxiosPost({ - endpoint: `/api/rsforms/${schema}/check`, + endpoint: `/api/rsforms/${schema}/check-constituenta`, request: request }); } diff --git a/rsconcept/frontend/src/hooks/useCheckConstituenta.ts b/rsconcept/frontend/src/hooks/useCheckConstituenta.ts new file mode 100644 index 00000000..3c2c95dd --- /dev/null +++ b/rsconcept/frontend/src/hooks/useCheckConstituenta.ts @@ -0,0 +1,40 @@ +'use client'; + +import { useCallback, useState } from 'react'; + +import { DataCallback } from '@/backend/apiTransport'; +import { postCheckConstituenta } from '@/backend/rsforms'; +import { type ErrorData } from '@/components/info/InfoError'; +import { ICheckConstituentaData, IConstituenta, type IRSForm } from '@/models/rsform'; +import { IExpressionParse } from '@/models/rslang'; + +function useCheckConstituenta({ schema }: { schema?: IRSForm }) { + const [processing, setProcessing] = useState(false); + const [error, setError] = useState(undefined); + const [parseData, setParseData] = useState(undefined); + + const resetParse = useCallback(() => setParseData(undefined), []); + + function checkConstituenta(expression: string, activeCst: IConstituenta, onSuccess?: DataCallback) { + const data: ICheckConstituentaData = { + definition_formal: expression, + alias: activeCst.alias, + cst_type: activeCst.cst_type + }; + setError(undefined); + postCheckConstituenta(String(schema!.id), { + data: data, + showError: true, + setLoading: setProcessing, + onError: setError, + onSuccess: parse => { + setParseData(parse); + if (onSuccess) onSuccess(parse); + } + }); + } + + return { parseData, checkConstituenta, resetParse, error, setError, processing }; +} + +export default useCheckConstituenta; diff --git a/rsconcept/frontend/src/hooks/useCheckExpression.ts b/rsconcept/frontend/src/hooks/useCheckExpression.ts deleted file mode 100644 index 6a16c807..00000000 --- a/rsconcept/frontend/src/hooks/useCheckExpression.ts +++ /dev/null @@ -1,100 +0,0 @@ -'use client'; - -import { useCallback, useState } from 'react'; - -import { DataCallback } from '@/backend/apiTransport'; -import { postCheckExpression } from '@/backend/rsforms'; -import { type ErrorData } from '@/components/info/InfoError'; -import { CstType, IConstituenta, type IRSForm } from '@/models/rsform'; -import { getDefinitionPrefix } from '@/models/rsformAPI'; -import { IArgumentInfo, IExpressionParse } from '@/models/rslang'; -import { RSErrorType } from '@/models/rslang'; -import { PARAMETER } from '@/utils/constants'; - -function useCheckExpression({ schema }: { schema?: IRSForm }) { - const [processing, setProcessing] = useState(false); - const [error, setError] = useState(undefined); - const [parseData, setParseData] = useState(undefined); - - const resetParse = useCallback(() => setParseData(undefined), []); - - function checkExpression(expression: string, activeCst?: IConstituenta, onSuccess?: DataCallback) { - setError(undefined); - postCheckExpression(String(schema!.id), { - data: { expression: expression }, - showError: true, - setLoading: setProcessing, - onError: setError, - onSuccess: parse => { - if (activeCst) { - adjustResults(parse, expression.trim() === getDefinitionPrefix(activeCst), activeCst.cst_type); - } - setParseData(parse); - if (onSuccess) onSuccess(parse); - } - }); - } - - return { parseData, checkExpression, resetParse, error, setError, processing }; -} - -export default useCheckExpression; - -// ===== Internals ======== -function checkTypeConsistency(type: CstType, typification: string, args: IArgumentInfo[]): boolean { - switch (type) { - case CstType.BASE: - case CstType.CONSTANT: - case CstType.STRUCTURED: - case CstType.TERM: - return typification !== PARAMETER.logicLabel && args.length === 0; - - case CstType.AXIOM: - case CstType.THEOREM: - return typification === PARAMETER.logicLabel && args.length === 0; - - case CstType.FUNCTION: - return typification !== PARAMETER.logicLabel && args.length !== 0; - - case CstType.PREDICATE: - return typification === PARAMETER.logicLabel && args.length !== 0; - } -} - -function adjustResults(parse: IExpressionParse, emptyExpression: boolean, cstType: CstType) { - if (!parse.parseResult && parse.errors.length > 0) { - return; - } - if (cstType === CstType.BASE || cstType === CstType.CONSTANT) { - if (!emptyExpression) { - parse.parseResult = false; - parse.errors.push({ - errorType: RSErrorType.globalNonemptyBase, - isCritical: true, - params: [], - position: 0 - }); - return; - } - } else { - if (emptyExpression) { - parse.parseResult = false; - parse.errors.push({ - errorType: RSErrorType.globalEmptyDerived, - isCritical: true, - params: [], - position: 0 - }); - return; - } - } - if (!checkTypeConsistency(cstType, parse.typification, parse.args)) { - parse.parseResult = false; - parse.errors.push({ - errorType: RSErrorType.globalUnexpectedType, - isCritical: true, - params: [], - position: 0 - }); - } -} diff --git a/rsconcept/frontend/src/models/rsform.ts b/rsconcept/frontend/src/models/rsform.ts index 68c60614..7da75f9d 100644 --- a/rsconcept/frontend/src/models/rsform.ts +++ b/rsconcept/frontend/src/models/rsform.ts @@ -144,6 +144,11 @@ export interface IConstituentaList { items: ConstituentaID[]; } +/** + * Represents {@link IConstituenta} data, used for checking expression. + */ +export interface ICheckConstituentaData extends Pick {} + /** * Represents {@link IConstituenta} data, used in creation process. */ diff --git a/rsconcept/frontend/src/models/rslang.ts b/rsconcept/frontend/src/models/rslang.ts index ca7210c2..b888f8c1 100644 --- a/rsconcept/frontend/src/models/rslang.ts +++ b/rsconcept/frontend/src/models/rslang.ts @@ -7,13 +7,6 @@ */ export type AliasMapping = Record; -/** - * Represents formal expression. - */ -export interface IRSExpression { - expression: string; -} - /** * Represents syntax type. */ @@ -91,6 +84,7 @@ export interface IArgumentValue extends IArgumentInfo { */ export interface IExpressionParse { parseResult: boolean; + prefixLen: number; syntax: Syntax; typification: string; valueClass: ValueClass; @@ -248,16 +242,17 @@ export enum RSErrorType { typesNotCompatible = 34853, orderingNotSupported = 34854, - // !!!! Добавлены по сравнению с ConceptCore !!!!! - globalNonemptyBase = 34855, - globalUnexpectedType = 34856, - globalEmptyDerived = 34857, - - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! globalNoValue = 34880, invalidPropertyUsage = 34881, globalMissingAST = 34882, - globalFuncNoInterpretation = 34883 + globalFuncNoInterpretation = 34883, + + cstNonemptyBase = 34912, + cstEmptyDerived = 34913, + cstCallableNoArgs = 34914, + cstNonCallableHasArgs = 34915, + cstExpectedLogical = 34916, + cstExpectedTyped = 34917 } /** diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression/EditorRSExpression.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression/EditorRSExpression.tsx index 2f72ee5e..f06f1d00 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression/EditorRSExpression.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression/EditorRSExpression.tsx @@ -11,7 +11,7 @@ import { RSTextWrapper } from '@/components/RSInput/textEditing'; import Overlay from '@/components/ui/Overlay'; import { useRSForm } from '@/context/RSFormContext'; import DlgShowAST from '@/dialogs/DlgShowAST'; -import useCheckExpression from '@/hooks/useCheckExpression'; +import useCheckConstituenta from '@/hooks/useCheckConstituenta'; import useLocalStorage from '@/hooks/useLocalStorage'; import { HelpTopic } from '@/models/miscellaneous'; import { ConstituentaID, IConstituenta } from '@/models/rsform'; @@ -54,7 +54,7 @@ function EditorRSExpression({ const model = useRSForm(); const [isModified, setIsModified] = useState(false); - const parser = useCheckExpression({ schema: model.schema }); + const parser = useCheckConstituenta({ schema: model.schema }); const { resetParse } = parser; const rsInput = useRef(null); @@ -74,11 +74,9 @@ function EditorRSExpression({ } function handleCheckExpression(callback?: (parse: IExpressionParse) => void) { - const prefix = getDefinitionPrefix(activeCst); - const expression = prefix + value; - parser.checkExpression(expression, activeCst, parse => { + parser.checkConstituenta(value, activeCst, parse => { if (parse.errors.length > 0) { - onShowError(parse.errors[0]); + onShowError(parse.errors[0], parse.prefixLen); } else { rsInput.current?.view?.focus(); } @@ -95,12 +93,11 @@ function EditorRSExpression({ } const onShowError = useCallback( - (error: IRSErrorDescription) => { + (error: IRSErrorDescription, prefixLen: number) => { if (!rsInput.current) { return; } - const prefix = getDefinitionPrefix(activeCst); - let errorPosition = error.position - prefix.length; + let errorPosition = error.position - prefixLen; if (errorPosition < 0) errorPosition = 0; rsInput.current?.view?.dispatch({ selection: { @@ -133,6 +130,7 @@ function EditorRSExpression({ toast.error(errors.astFailed); } else { setSyntaxTree(parse.ast); + // TODO: return prefix from parser API instead of prefixLength setExpression(getDefinitionPrefix(activeCst) + value); setShowAST(true); } @@ -199,7 +197,7 @@ function EditorRSExpression({ isOpen={!!parser.parseData && parser.parseData.errors.length > 0} data={parser.parseData} disabled={disabled} - onShowError={onShowError} + onShowError={error => onShowError(error, parser.parseData?.prefixLen ?? 0)} /> ); diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts index 4017113c..8e4daca4 100644 --- a/rsconcept/frontend/src/utils/constants.ts +++ b/rsconcept/frontend/src/utils/constants.ts @@ -34,7 +34,7 @@ export const PARAMETER = { statSmallThreshold: 3, // characters - threshold for small labels - small font logicLabel: 'LOGIC', - exteorVersion: '4.9.4', + exteorVersion: '4.9.5', TOOLTIP_WIDTH: 'max-w-[29rem]' }; diff --git a/rsconcept/frontend/src/utils/labels.ts b/rsconcept/frontend/src/utils/labels.ts index 97849809..1f16d980 100644 --- a/rsconcept/frontend/src/utils/labels.ts +++ b/rsconcept/frontend/src/utils/labels.ts @@ -787,13 +787,20 @@ export function describeRSError(error: IRSErrorDescription): string { case RSErrorType.globalMissingAST: return `Не удалось получить дерево разбора для глобального идентификатора: ${error.params[0]}`; case RSErrorType.globalFuncNoInterpretation: - return `Функция не интерпретируется для данных аргументов`; - case RSErrorType.globalNonemptyBase: - return `Непустое выражение базисного/константного множества`; - case RSErrorType.globalUnexpectedType: - return `Типизация выражения не соответствует типу конституенты`; - case RSErrorType.globalEmptyDerived: - return `Пустое выражение для выводимого понятия или утверждения`; + return 'Функция не интерпретируется для данных аргументов'; + + case RSErrorType.cstNonemptyBase: + return 'Непустое выражение базисного/константного множества'; + case RSErrorType.cstEmptyDerived: + return 'Пустое выражение для сложного понятия или утверждения'; + case RSErrorType.cstCallableNoArgs: + return 'Отсутствуют аргументы для параметризованной конституенты'; + case RSErrorType.cstNonCallableHasArgs: + return 'Параметризованное выражение не подходит для данного типа конституенты'; + case RSErrorType.cstExpectedLogical: + return 'Данный тип конституенты требует логического выражения'; + case RSErrorType.cstExpectedTyped: + return 'Данный тип конституенты требует теоретико-множественного выражения'; } return 'UNKNOWN ERROR'; }