Как стать автором
Обновить

DSL для XML в C++

Время на прочтение6 мин
Количество просмотров4.9K

Что имеем


Начну с того, что расскажу немного о проекте, в котором работаю и как там все пишется. Может не у одних нас так…

Проект представляет из себя CRM систему, разрабатываемую специально для клиентов одного сегмента бизнеса. Проекту лет 6 и команда разработчиков состоит из 10 человек. Язык: C++ и PL/SQL.

Наша система исползует Plain Old XML, так уж повелось. И на используемые XML нет схем, по большей части. Что тут говорить, если юнит тесты прививаются здесь только второй год и менеджер до сих пор упрекает за время, потраченное на их написание. Да ладно…

По ходу дела все улучшения появляются тогда, когда текущее положение дел порядком надоедает и становится невмоготу. Так же произошло и сейчас.

Как и многие, я думаю, мы многое делаем неоптимально и не лучшим образом. Главное делаем. Пример с XML не исключение.


Стандартная практика


Вот пример кода построения XML документа в функции:

::std::string request =
    clearCreateNode("book",
      createNode("author", "", "name='Freddy' surname='Smith'") +
      createNode("author", "", "name='Bill' surname='Joe'") +
      clearCreateNode("quote",
        "This is the best unknown book I've ever quoted!" +
        createNode("author", "", "name='Mr. Bob'")),
      "isbn='123456789' name='Some book' year='2011'");


Стандартная практика. Почему не stream? — потому, что есть вложенность элементов. Хотя stream это первое, что приходит в голову и для императивного подхода вряд ли можно получить что-то отличное от «скармливания правого объекта левому».

Ладно, привыкли. Можно потерпеть запятую, но терпеть для каждого тега повторение названия функции clearNode или clearCreateNode — я уже не смог. И не важно, насколько коротким может быть название, хоть просто "T" — это маразм. Сродни тому, чтобы в диалоге с кем-то перед каждой репликой повторять: «я говорю», а перед каждым предложением говорить — «предложение:», а перед словом — «слово:» и так далее. Не маразм ли?

В чём проблема


Кто-то скажет, что XML сам по себе избыточен — хорошо, что в коде хоть не приходится тег закрывать…

Да, это так, но это не повод вести себя также. Мне больше нравится общаться с человеком, чем с идиотом. Когда мы оба понимаем контекст и семантику диалога. Когда можем использовать сленг и друг-друга понимаем. Или, когда можно только один раз объяснить правила конструкции и дальше общаться принимая их во внимание.

Например в Object Pascal есть такая конструкция:

with (Object)
begin
 a := 1;
 b := "foobar";
end;


Приятно, однако, когда можно сказать: «Сейчас работаем с объектом Object, поэтому его свойство 'a' установим в 1, а свойство 'b' в 'foobar'».

«Ни капли жира!»


Возвращаясь к XML, вот как-то так выглядит XML документ на DSL, например в Lisp:

((book :isbn "123456789" :name "Some book" :year "2011")
  ((author :name "Freddy" :surname "Smith"))
  ((author :name "Bill" :surname "Joe"))
  (quote "This is the best unknown book I've ever quoted!"
    (author :name "Mr. Bob")))


Если не обращать внимание на скобки, то здесь нет «ни капли жира!» Если в XML не обращать внимание на символы <,>,/,= и опустить избыточность закрывающего тега, то получатся одинаковые сообщения.

<book isbn='123456789' name='Some book' year='2011'><author name='Freddy' surname='Smith'/><author name='Bill' surname='Joe'/><quote>This is the best unknown book I&apos;ve ever quoted!<author name='Mr. Bob'/></quote></book>


Или с pretty print:

<book isbn='123456789' name='Some book' year='2011'>
  <author name='Freddy' surname='Smith'/>
  <author name='Bill' surname='Joe'/>
  <quote>This is the best unknown book I&apos;ve ever quoted!
    <author name='Mr. Bob'/>
  </quote>
</book>


Можно ли получить что-то похожее по смыслу в C++?

Результат


В общем, вот, что в итоге получилось:

  const xml::Tag book     ("book");
  const xml::Tag author   ("author");
  const xml::Tag quote    ("quote");

  xml::Element request = xml::Element()
    (book ("isbn", "123456789") ("name", "Some book") ("year", "2011"), xml::Element()
      (author ("name", "Freddy") ("surname", "Smith"))
      (author ("name", "Bill") ("surname", "Joe"))
      (quote, xml::Element()
        ("This is the best unknown book I've ever quoted!")
        (author ("name", "Mr. Bob"))));


Или с выделением списка тэгов для переиспользования в проекте:

namespace library
{
  namespace books
  {
    const xml::Tag book     ("book");
    const xml::Tag author   ("author");
    const xml::Tag quote    ("quote");
  }
}


И тогда:

  namespace bks = ::library::books;

  xml::Element request = xml::Element()
    (bks::book ("isbn", "123456789") ("name", "Some book") ("year", "2011"), xml::Element()
      (bks::author ("name", "Freddy") ("surname", "Smith"))
      (bks::author ("name", "Bill") ("surname", "Joe"))
      (bks::quote, xml::Element()
        ("This is the best unknown book I've ever quoted!")
        (bks::author ("name", "Mr. Bob"))));


Полностью избавиться от избыточности не удалось (всё-таки указывать xml::Element() в точке вложения приходится). Но прогресс есть: уже не надо твердить для каждого тега, что это ТЕГ, и можно собирать аналитику для всего строящегося документа и делать на её основе например pretty print или ещё что-нибудь, так как имеем уже дело с объектом, точнее с деревом с общим корнем.

Выводы


Можно выделить интерфейс такого DSL, состоящий из:
  • семантических операторов класса Element:

  template <class T>
  void addContent(const T & content);

  void addEmptyTag(const Tag & tag);

  template <class T>
  void addTag(const Tag & tag, const T & content);

  void addTagElement(const Tag & tag, const Element & element);

  • DSL операторов класса Element:

  template <class T>
  inline
  Element & operator () (const T & content)
  {
    addContent(content);
    return *this;
  }

  inline
  Element & operator () (const Tag & tag)
  {
    addEmptyTag(tag);
    return *this;
  }

  template <class T>
  inline
  Element & operator () (const Tag & tag, const T & content)
  {
    addTag(tag, content);
    return *this;
  }

  template <>
  inline
  Element & operator () (const Tag & tag, const Element & element)
  {
    addTagElement(tag, element);
    return *this;
  }

  • интерфейса класса Tag, который разделять не так интересно, ввиду отсутствия вложенности у тэгов имён и атрибутов.


И теперь остаётся только придумывать различные реализации для компиляции кода написанного на DSL в XML. Вот например лабораторный вариант:

  template <class T>
  void addContent(const T & content)
  {
    ::std::ostringstream oss;
    oss << content;
    buf += http_encode(oss.str());
  }

  void addEmptyTag(const Tag & tag)
  {
    buf += createNode(tag.name, "", tag.args);
  }

  template <class T>
  void addTag(const Tag & tag, const T & content)
  {
    ::std::ostringstream oss;
    oss << content;
    buf += createNode(tag.name, oss.str(), tag.args);
  }

  void addTagElement(const Tag & tag, const Element & element)
  {
    buf += clearCreateNode(tag.name, element.str(), tag.args);
  }


Для полноты картины, приведу код класса Tag:

class Tag
{
public:
  // DSL операторы:

  template <class K>
  Tag & operator () (const K & attr_name)
  {
    ::std::ostringstream oss;
    oss << " " << attr_name;
    args += oss.str();
    return *this;
  }

  template <class K, class T>
  Tag & operator () (const K & attr_name, const T & value)
  {
    ::std::ostringstream oss;
    oss << " " << attr_name << "='" << value << "'";
    args += oss.str();
    return *this;
  }

  // Копирующие варианты DSL операторов:

  template <class K>
  Tag operator () (const K & attr_name) const
  {
    Tag copy(*this);
    return copy (attr_name);
  }

  template <class K, class T>
  Tag operator () (const K & attr_name, const T & value) const
  {
    Tag copy(*this);
    return copy (attr_name, value);
  }

  // Данные и инициализация:

  template <class T>
  Tag(const T & name) : name(name) {}

  Tag(const Tag & ref) : name(ref.name), args(ref.args) {}

  ::std::string name;
  ::std::string args;
};


… и пример XSLT для перевода простого XML документа в код на этом DSL:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
	<xsl:output method="text" version="1.0" encoding="windows-1251" indent="no"/>
	
	<xsl:template match="/">
		<xsl:apply-templates/> ;
	</xsl:template>
	
	<xsl:template match="text()">"<xsl:value-of select="."/>"</xsl:template>
	
	<xsl:template match="attribute::*"> ("<xsl:value-of select="name()"/>", "<xsl:value-of select="."/>")</xsl:template>

	<xsl:template match="*"><xsl:if test="child::node()">xml::Element()</xsl:if> (<xsl:value-of select="name()"/><xsl:apply-templates select="attribute::*"/><xsl:if test="child::node()">, <xsl:apply-templates/></xsl:if>)</xsl:template>
    
</xsl:stylesheet>


В итоге, можно сказать, что получилось-таки малой кровью написать DSL для С++. Теперь следующим пунктом будет включение SQL кода в C++…

Спасибо за потраченное время, надеюсь, что топик Вам понравился.
Теги:
Хабы:
+26
Комментарии27

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн