Текстовый редактор моделей?

от автора

Всем привет! Мы добавили новую фичу в наш инструмент моделирования — текстовые представления для моделей. В статье я покажу что это такое и расскажу про детали реализации.

Волшебница превращает модель в код

Волшебница превращает модель в код

TL;DR покажите мне видео

Инструкция как попробовать самому

Вы можете войти в систему с учетной записью Google, GitHub или Яндекс:

Вход в систему

Вход в систему

После аутентификации вы можете создать новый проект:

Форма создания проекта

Форма создания проекта

Затем создать модель:

Форма создания модели

Форма создания модели

Можно добавить несколько классов:

Пример модели классов

Пример модели классов

Наконец создаём текстовое представление для модели:

Форма создания представления для модели

Форма создания представления для модели

И видим такой код:

Текстовый редактор для моделей

Текстовый редактор для моделей

Не знаю как вы, но я просто балдею от того как у нас сделана локализация моделей, мне всегда этого не хватало в инструментах моделирования.

Текстовый редактор поддерживает ожидаемые вещи:

  • Подсветка синтаксиса

  • Синтаксическая валидация (более сложная семантическая валидация пока планируется)

  • Автодолополнение

  • Переход к определению

  • Переименование объектов с обновлением ссылок

  • Автоформатирование (спорный вопрос баг это или фича)

  • Сворачивание блоков кода

  • Совместное редактирование несколькими пользователями

  • Синхронизация с другими представлениями модели (дерево, диаграмма, форма). При редактировании кода обновляются навигатор, форма свойств и диаграмма. Также это работает и в обратную сторону. Вы можете редактировать диаграмму, параметры объектов на форме свойств, перетаскивать объекты в навигаторе, при этом будет обновляться код в текстовом редакторе. Причём, и у вас (вы можете открыть диаграмму в другой вкладке браузера), и у других пользователей, которые работают с этой моделью

Что пока не сделано

Самая приоритетная фича, которой сейчас не хватает, это межмодельные связи. Для моделей классов у нас есть библиотека стандартных типов данных. Сейчас на них нельзя ссылаться в текстовом редакторе, все типы приходится объявлять в самой модели.

Вторая проблема. Если редактировать модель одновременно в диаграммном и в текстовом редакторах (открыв их в разных вкладках браузера или у разных пользователей), то могут откатываться некоторые изменения. Например, добавляю в диаграммном редакторе атрибут для класса, но ещё не успел задать для него название. Эта модель отправляется в текстовый редактор, который не может корректно вставить в текст безымянный атрибут. В итоге текстовый редактор удаляет этот атрибут из модели. Для разных редакторов разные ошибки имеют разную критичность, нужно будет это выравнять.

И ещё один баг или фича. Модели хранятся на сервере в виде JSON, а не текста. В них теряется информация о форматировании. С одной стороны, это хорошо, потому что кастомное форматирование и не особо нужно. Но с другой стороны, при редактировании это может приводить к неожиданным эффектам. Например, классы и типы данных в модели хранятся в двух отдельных коллекциях в JSON (это не обязательно, но автор метамодели решил сделать так). Соответственно в модели сохраняется информация отдельно о последовательности классов и отдельно о последовательности типов данных. При преобразовании модели в текст сначала выводятся все классы, а затем все типы данных. Если вы в коде будете писать вперемежку классы и типы данных, то классы будут автоматически перескакивать в начало файла, а типы данных — в конец. Это вроде и хорошо, потому что в коде будет порядок, а вроде и неожиданно для пользователя.

Как это реализовано

Работает это следующим образом (мне нереально лень рисовать картинки, плюс и статья про текстовый редактор, поэтому простите, дальше будут тонны текста, никаких алгоритмических схемок, потоков данных и т. д.):

  1. Модель хранится в JSON формате в базе данных

  2. Модель передаётся на фронтенд

  3. Модель преобразуется в абстрактное синтаксическое дерево

  4. Абстрактное синтаксическое дерево преобразуется в текст

  5. Текст отображается в текстовом редакторе и пользователь может его редактировать

  6. Текст преобразуется обратно в абстрактное синтаксическое дерево

  7. Абстрактное синтаксическое дерево преобразуется обратно в модель

  8. Модель сохраняется в базу данных

В общем архитектурный паттерн «Хоббит, или туда и обратно»…

Шаги 5 и 6 описаны в предыдущей статье. В этой статье более подробно остановимся на следующем:

  • Преобразование модели в абстрактное синтаксическое дерево и обратно (шаги 3 и 7)

  • Преобразование абстрактного синтаксического дерева в код (шаг 4)

  • Связь между метамоделями и грамматикой

Рассмотрим каждый пункт более детально.

Преобразование модели в абстрактное синтаксическое дерево (AST) и обратно

Это самая важная, сложная и интересная часть, практически квинтэссенция программирования — перекладывание JSONов! То ради чего джедаи от разработки годами оттачивают своё мастерство и получают тысячи денег…

И AST, и модель — это JSON, причём очень похожие по структуре, но всё‑таки не идентичные. Самое существенное отличие в том, что в модели у каждого объекта есть идентификатор (который нужен для межмодельных ссылок, для ссылок на модель из диаграмм). А в текстовом редакторе никаких идентификаторов у объектов может и не быть (будет просто название и всё), а значит их не будет и в AST.

Пример кода
classModel OnlineStore  @name('ru-RU', 'пользователь') class User {      @name('ru-RU', 'имя')     attribute firstName String[0..1]  }  string String {  }
Пример AST
{   "$type": "ClassModel",   "name": "OnlineStore",   "classes": [     {       "$type": "Class",       "localizedName": [         {           "$type": "Ecore_EStringToStringMapEntry",           "key": "ru-RU",           "value": "пользователь"         }       ],       "kind": {         "$type": "ClassKind__Regular"       },       "name": "User",       "properties": [         {           "$type": "Attribute",           "localizedName": [             {               "$type": "Ecore_EStringToStringMapEntry",               "key": "ru-RU",               "value": "имя"             }           ],           "name": "firstName",           "dataType": {             "$ref": "#/dataTypes@0",             "$refText": "String"           },           "lower": 0,           "upper": 1         }       ]     }   ],   "dataTypes": [     {       "$type": "StringType",       "name": "String"     }   ] }
Пример модели
{   "json": {     "version": "1.0",     "encoding": "utf-8"   },   "ns": {     "classmodel": "https://example.com/class-model",     "ecore": "http://www.eclipse.org/emf/2002/Ecore"   },   "content": [     {       "id": "0197f429-e5e0-743b-9330-d755229cecbb",       "eClass": "classmodel:ClassModel",       "data": {         "name": "OnlineStore",         "classes": [           {             "id": "0197f429-e5e0-743b-9330-dfccbe50ea8a",             "eClass": "classmodel:Class",             "data": {               "localizedName": [                 {                   "id": "0197f429-e5e0-743b-9330-e355d52c727d",                   "eClass": "ecore:EStringToStringMapEntry",                   "data": {                     "key": "ru-RU",                     "value": "пользователь"                   }                 }               ],               "name": "User",               "properties": [                 {                   "id": "0197f429-e5e1-777f-9508-9562d8686ac3",                   "eClass": "classmodel:Attribute",                   "data": {                     "localizedName": [                       {                         "id": "0197f429-e5e1-777f-9508-9940a4401452",                         "eClass": "ecore:EStringToStringMapEntry",                         "data": {                           "key": "ru-RU",                           "value": "имя"                         }                       }                     ],                     "name": "firstName",                     "dataType": "0197f429-e5e1-777f-9508-fecb083bb4df",                     "lower": 0,                     "upper": 1                   }                 }               ]             }           }         ],         "dataTypes": [           {             "id": "0197f429-e5e1-777f-9508-fecb083bb4df",             "eClass": "classmodel:StringType",             "data": {               "name": "String"             }           }         ]       }     }   ] }

При преобразовании кода в AST и затем в модель мы должны каким‑то образом восстановить исходные идентификаторы всех объектов, а для добавленных в коде объектов сгенерировать новые идентификаторы. Делается это следующим образом:

  • Преобразуем модель в AST, при этом копируем идентификаторы объектов. Текстовому редактору они не нужны, но нам понадобятся позже. Сохраняем AST в кэш

  • Преобразуем AST в код, здесь идентификаторы уже теряются

  • Пользователь редактирует код, преобразуем его в новый AST уже без идентификаторов

  • Сравниваем исходный AST (с идентификаторами) и новый AST (без идентификаторов), получаем список изменений (diff). Мы используем для этого JsonDiffPatch. Вообще, такое сравнение — это не самая тривиальная задача. Например, если вы в коде поменяете два атрибута местами, то как понять, что это именно обмен местами, а не удаление одного атрибута в начале и затем создание такого же в конце? Можно определить это по названию, т. е. если удалён и создан атрибут с таким же названием, значит это перемещение, но что если между этими действиями было ещё редактирование? Или если мы используем название в качестве идентификатора для объектов, то как отслеживать переименования? Это решается с помощью разных эвристик, которых нет в JsonDiffPatch, здесь есть что улучшить, но сейчас это как‑то работает

  • Применяем к исходному AST изменения. И теперь оно и с идентификаторами, и актуальное

  • Проходим по AST, генерируем идентификаторы для новых объектов

  • Преобразуем AST в модель

По ссылке минимальный демо пример преобразования кода в модель и обратно (исходный код):

Пример текстового редактора моделей

Пример текстового редактора моделей

Преобразование абстрактного синтаксического дерева в код

В движках для создания предметно‑ориентированных языков программирования обычно уделяется внимание парсингу кода в AST, но не так часто в них поддерживается обратное преобразование из AST в код. Мы используем Langium, и в текущей версии это не поддерживается. Хотя идейно он основан на Xtext, где эта фича есть, и вроде Langium делают те же люди и можно было бы дождаться когда они это запилят. Но в Xtext это сделано на столько заморочено, что мы решили не ждать и навайбкодить это сами.

Преобразование AST в код включает следующее:

  • Рекурсивно пройти по грамматике, параллельно пройти по AST и при этом генерить текст

  • Либо сразу, либо после форматировать код: добавлять пробелы и переносы строк где требуется

Первая часть относительно простая. А как задать правила форматирования кода — это нетривиальный вопрос. В Xtext для этого просто пишется код, и в Langium авторы пошли той же тропинкой в пропасть. Если бы нам нужно было реализовать один DSL, например, для моделей классов, то мы могли бы написать этот код. Но в нашем инструменте моделирования поддерживаются в том числе и пользовательские DSL, поэтому мы решили описывать правила форматирования в самой грамматике, подглядев эту идею в канувшем в лету EMFText.

Фрагмент грамматики с «аннотациями» для форматирования кода:

grammar CM  entry ClassModel:     _NL? Localization* _NL?     'classModel' name=ID     (classes+=Class | dataTypes+=DataType)*;  Class:     _NL? Localization* _NL?     kind=ClassKind name=ID     ('extends' generals+=[Class:ID] (',' generals+=[Class:ID])*)? ('{'         properties+=Property*     _NL? _NL? '}')?;  _NL returns string: '__NL__';

Там, где в грамматике указан _NL?, при генерации кода будет добавляться перевод строки. По хорошему мы должны были бы расширить язык описания грамматик новой конструкцией для описания правил форматирования, а не использовать для этого фейковое правило с захардкоженым названием. Но решили применить архитектурный паттерн «Фигак‑фигак и в продакшн».

Кроме переводов строк нужно задавать ещё правила добавления отступов и пробелов внутри текста. Но их мы просто захардкодили. Например, до или после скобок пробелы никогда не добавляются, до запятой пробел не ставим, а после запятой ставим. Для текста в фигурных скобках добавляется отступ. В общем случае для произвольных DSL это работать не будет и придётся всё‑таки расширять язык описания грамматики нормальными аннотациями для кодогенератора.

По ссылке минимальный демо пример преобразования кода в AST и обратно (исходный код):

Пример преобразования AST в код

Пример преобразования AST в код

Связь между метамоделями и грамматикой

Выше были ссылки на минимальные демо примеры. А как использовать всё это уже в реальном инструменте моделирования?

Для каждого языка моделирования (модели классов, C4, VAD, EPC, …) у нас есть метамодель, которая описывает структуру соответствующих моделей (модель классов состоит из классов и типов данных, классы содержат свойства двух типов: атрибуты и ссылки и т. д.):

Метамодель для моделей классов

Метамодель для моделей классов

Чтобы добавить DSL (текстовую нотацию) для вашего языка моделирования достаточно добавить грамматику в поле Grammar на форме свойств. В будущих версиях мы сделаем адекватный редактор грамматики типа такого (исходный код):

Пример редактора грамматики и расширений для неё

Пример редактора грамматики и расширений для неё

Плюс я думаю, что с большой вероятностью мы сделаем несколько автогенерируемых грамматик по умолчанию, чтобы любую модель можно было редактировать в виде HUTN или YAML.

Заключение

В нашем инструменте моделирования размывается граница между подходами «something as code» и «something as model», размывается граница между текстовыми DSL и визуальными языками моделирования. В будущих версиях немного причешем табличные представления для моделей и размоем границу между инструментами моделирования и учётными системами.

Что вы думаете по поводу всего этого?


ссылка на оригинал статьи https://habr.com/ru/articles/938340/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *