Cls-документы состоят всего из двух важных элементов – тегов и значений:
|| Это тег {tag_name || Это значение типа nonspace (:tok_nonspace) nonspace-value || Это значение типа string (:tok_string) "string-value" }
Значения делятся на два типа: nonspace, которые не могут содержать в себе пробельных символов, и string (которые заключаются в двойные кавычки и могут содержать пробелы). Значения разных типов в теге лучше не смешивать, т.е. пусть в теге будут либо только значения nonspace, либо только значения string.
Тег может иметь либо одно значение:
{name "my name" }
либо несколько значений:
{names "first" "second" ... "last" }
Пробелы и комментарии (которые приравниваются к пробелам) так же являются элементами, но обычно они не используются.
Предположим, что нужно представить в виде Cls документа описание некоторой сетевой службы (см. пример service_description). Служба описывается именем, набором IP-адресов, которые она будет использовать, и множеством сообщений, которые она способна обрабатывать. Каждое сообщение имеет имя и набор полей, где у каждого поля есть собственное имя и имя типа, значение которого будет находится в этом поле.
Пусть каждую службу описывает тег {service}:
{service ...}
дочерние теги которого будут содержать различные детали описания.
Имя службы может описываться тегом {name}, в котором допускается единственное значение типа string (в имя вполне могут входить пробелы):
{name <str> }
IP-адреса можно задавать двумя способами. Во-первых, в виде отдельного тега {ip} на каждый IP-адрес. Во-вторых, в виде одного тега {ips}, в котором будут несколько значений – все назначенные службе адреса. Пусть используется тег {ips} с несколькими значениями типа nonspace (поскольку в IP адресах не должно быть пробелов, то достаточно будет nonspace):
{ips <nonspace>+ }
Каждое сообщение описывается отдельным тегом {message} которое будет содержать одно значение типа nonspace – имя сообщения (поскольку это идентификатор, в котором пробелов не может быть) и необязательные теги {field} для описания полей сообщения:
{message <nonspace> {field ...}* }
Тег {field} в свою очередь будет иметь дочерние теги {name} и {type}:
{field {name <str>} {type <nonspace>}}
В результате получается структура:
{service {name <str> } {ips <nonspace>+ } {message <nonspace> {field {name <str> } {type <nonspace } }* }* }
Например, следующие описания соответствует этой структуре:
{service {name "Remote Service Manager" } {ips 0.0.0.0:3333 localhost:4444 } {message GetServicesList} {message StartService {field {name "service"} {type String}} {field {name "mode"} {type StartMode}} } {message StopService {field {name "service"} {type String}} } } {service {name "Test Service" } {ips localhost:5000 } }
Парсинг входного потока построен на том, что каждому тегу во входном потоке соответствует объект-обработчик, производный от типа ClsRuby::Tag. Парсеру (ClsRuby#parse_io, ClsRuby#parse_string, ClsRuby#parse_file) задается набор объектов-обработчиков верхнего уровня – соответствующие им теги являются самыми высокоуровневыми тегами входного потока (т.е. у них нет родителей).
Когда парсер находит во входном потоке имя тега, он просматривает все известные ему обработчики и ищет того обработчика, который способен обработать найденный тег (для этого используется ClsRuby::Tag#tag_compare_name). Если обработчик найден, то:
для этого обработчика вызывается метод ClsRuby::Tag#tag_on_start;
этот обработчик становится текущим контекстом парсинга.
Если обработчик не найден, то порождается ошибка (посредством исключения ClsRuby::ParsingErrorEx).
Далее парсер разбирает входной поток используя найденый тег в качестве текущего контекста. Когда парсер находит во входном потоке токен :tok_nonspace, он считает, что найдено значение типа nonspace и вызывает для текущего контекста метод ClsRuby::Tag#tag_on_tok_nonspace. Если парсер находит токен :tok_string, то для текущего контекста вызывается метод ClsRuby#tag_on_tok_string. Соответственно, обработчик может использовать эти методы для того, чтобы:
проверять значения на корректность;
сохранять полученные значения;
выполнять какие-то другие действия.
Если парсер обнаруживает имя тега, он запрашивает у текущего контекста список дочерних тегов (ClsRuby::Tag#tag_tags) и ищет среди них обработчик для найденого тега (посредством ClsRuby::Tag#tag_compare_name). Если обработчик найден, то:
для этого обработчика вызывается метод ClsRuby::Tag#tag_on_start;
текущий контекст сохраняется в стеке парсера;
найденый обработчик становится текущим контекстом парсера.
И т.д. до тех пор, пока парсер не найдет закрывающую фигурную скобку, которая означает завершение текущего тега. Тогда парсер:
для текущего контекста вызывает метод ClsRuby::Tag#tag_on_finsh;
извлекает из стека предыдущий текущий контекст и восстанавливает его в качестве текущего контекста;
для восстановленного текущего контекста вызывает метод ClsRuby::Tag#tag_on_tag.
Такой парсинг продолжается до тех пор, пока парсер не встретит завершение входного потока. При этом должно оказаться, что стек контекстов пуст и текущего контекста нет (т.к. количество открывающих фигурных скобок должно совпасть с количеством закрывающих фигурных скобок).
Когда для тега вызывается метод tag_on_finish, то считается, что тег стал определенным – т.е. выставляется признак, что тег встретился во входном потоке хотя бы один раз. Проверить, что тег был определен можно с помощью метода ClsRuby::Tag#tag_defined?, метод ClsRuby::Tag#tag_make_defined предназначен для того, чтобы указать тегу, что он определен (это нужно для того, чтобы тег был корректно отформатирован в выходной поток).
Теги делятся на обязательные (mandatory) и необязательные (optional). Если тег объявлен обязательным (т.е. его метод ClsRuby::Tag#tag_mandatory? возвращает true), то к окончанию разбора соответствующего фрагмента этот тег должен быть определен (т.е. его метод ClsRuby::Tag#tag_defined? должен возвращать true). Когда парсер обнаруживает завершение текущего тега (т.е. при нахождении во входном потоке закрывающей фигурной скобки) он просматривает все дочерние теги завершенного тега. Если среди них есть обязательные теги, которые не были определены, то парсер прекращает разбор порождая исключение. Аналогичную проверку парсер делает при завершении входного потока.
Как было показано выше, для разбора Cls-документов необходимо подготовить набор объектов, унаследованных от класса ClsRuby::Tag. ClsRuby предоставляет несколько готовых классов тегов, которые позволяют выполнять парсинг достаточно сложных Cls-документов без создания собственныхт классов тегов.
Но вопрос заключается в том, где и как сохранять извлекаемые из Cls-документа значения. Здесь возможно несколько подходов.
Можно реализовать собственных наследников класса ClsRuby::Tag таким образом, чтобы в методах Tag#tag_on_finish, Tag#tag_on_tok_nonspace и Tag#tag_on_tok_string сохранять извлеченные значения в каком-то объекте. Например, если необходимо получить список файлов, заданных тегами:
{include <str>}+
то можно сделать собственный тег:
require 'cls-ruby' class TagInclude < ClsRuby::Tag default_tag_params :name => 'include' def initialize( receiver, params = {} ) super( params ) # Это приемник имен файлов. @receiver = receiver end # При получении очередного значения сохраняем его в приемнике. def tag_on_tok_string( value ) @receiver << value end end files = [] tag = TagInclude.new( files ) ClsRuby::parse_io( ARGF, '-', tag ) puts files.join(', ')
Однако, такой путь не очень удобен. Поскольку требуется писать собственные теги, и следить за их корректностью. Так, данная реализация тега TagInclude не корректна из-за того, что она допускает несколько строковых значений в теге {include} и не сообщает об ошибках в случае обнаружения nonspace значений. Создание более корректной реализации вручную просто приведет к повторению функциональности уже существующего класса ClsRuby::TagStringScalar.
Можно использовать готовые теги ClsRuby и для парсинга и для хранения значений. Например, конструкцию:
{field {name <str>} {type <nonspace>}}
можно представить в виде следующего класса TagField:
require 'cls-ruby' require 'cls-ruby/tag_no_value' require 'cls-ruby/tag_scalar' class TagField < ClsRuby::TagNoValue mandatory_child_tag :name, ClsRuby::TagStringScalar mandatory_child_tag :type, ClsRuby::TagScalar, :format => ClsRuby::SCALAR_NONSPACE_STRING default_tag_params :name => 'field' # Getter-ы значений name и type. def name; @name.value; end def type; @type.value; end end tag = TagField.new ClsRuby::parse_io( ARGF, '-', tag ) puts "#{tag.name}:#{tag.type}"
Данный способ более предпочтителен, чем предыдущий, но при его использовании может потребоваться создание нетривиальных методов getter-ов для сложных конструкций тегов. Кроме того, происходит привязывание прикладной информации (т.е. значений из Cls-документа) к формату ее хранения – из-за того, что класс для хранения информации из тега {field} оказывается производным от ClsRuby::TagNoValue.
Использование Cls-формата в C++ показало, что выгодно создавать две иерархии классов. Одна используется для хранения прикладной информации, а вторая – для парсинга Cls-документов.
Так, можно создать структуру Field, которая будет хранить информацию из тега {field}:
Field = Struct.new( :name, :type )
и класс тег TagField, который будет отвечать за парсинг тега {field} и формирование объекта типа Field:
require 'cls-ruby' require 'cls-ruby/tag_no_value' require 'cls-ruby/tag_scalar' Field = Struct.new( :name, :type ) class TagField < ClsRuby::TagNoValue mandatory_child_tag :name, ClsRuby::TagStringScalar mandatory_child_tag :type, ClsRuby::TagScalar, :format => ClsRuby::SCALAR_NONSPACE_STRING default_tag_params :name => 'field' # Getter-ы значений name и type. def value Field.new( @name.value, @type.value ) end end tag = TagField.new ClsRuby::parse_io( ARGF, '-', tag ) field = tag.value puts "#{field.name}:#{field.type}"
Данный способ удобен тем, что прикладные классы независимы от соответствующих классов тегов. Поэтому, при необходимости можно менять структуру Cls документа или даже отказаться от использования Cls в пользу XML, YAML или JSON, но сами прикладные классы не изменятся.
Так же такой способ удобен при необходимости сохранения информации в Cls формат. В этом случае программист формирует нужные значения в прикладных классах, а соответствующие классы теги скрывают он него детали преобразования прикладной информации в Cls-формат.
Например, если требуется работать с форматом:
{message {field ...}* }
то это может выглядеть следующим образом:
require 'cls-ruby' require 'cls-ruby/default_formatter' require 'cls-ruby/tag_no_value' require 'cls-ruby/tag_scalar' require 'cls-ruby/tag_vector_of_tags' Field = Struct.new( :name, :type ) Message = Struct.new( :name, :fields ) class TagField < ClsRuby::TagNoValue mandatory_child_tag :name, ClsRuby::TagStringScalar mandatory_child_tag :type, ClsRuby::TagScalar, :format => ClsRuby::SCALAR_NONSPACE_STRING default_tag_params :name => 'field' # Если задан ключ :value, то сразу производится инициализация. def initialize( params = {} ) super( params ) tag_handle_opt_param( :value ) do |field| @name.value = field.name @type.value = field.type tag_make_defined end end # Getter-ы значений name и type. def value Field.new( @name.value, @type.value ) end end class TagMessage < ClsRuby::TagNoValue mandatory_child_tag :name, ClsRuby::TagScalar, :format => ClsRuby::SCALAR_NONSPACE_STRING child_tag :field, ClsRuby::TagVectorOfTags, :type => TagField default_tag_params :name => 'message' # Если задан ключ :value, то сразу производится инициализация. def initialize( params = {} ) super( params ) tag_handle_opt_param( :value ) do |message| @name.value = message.name message.fields.each do |field| @field.add_nested_tag( TagField.new( :value => field ) ) end tag_make_defined end end # Получение значения. def value r = Message.new r.name = @name.value r.fields = @field.collect_values_by( :value ) r end end # Считываем описание сообщения. in_tag = TagMessage.new ClsRuby::parse_io( ARGF, '-', in_tag ) message = in_tag.value # Дополняем сообщение еще несколькими полями. message.fields << Field.new( 'id', 'String' ) message.fields << Field.new( 'timestamp', 'TimeStamp' ) # Отображаем результирующее сообщение на стандартном потоке вывода. out_tag = TagMessage.new( :value => message ) out_tag.tag_format( ClsRuby::DefaultFormatter.new( STDOUT ) )
Главная причина – повторное использование. Когда тег оформлен в виде собственного класса его проще использовать повторно. Вот, например, класс тега, который позволяет задавать временной промежуток до одних суток. С его помощью можно легко указывать количество часов, минут и секунд. Формат тега:
{<name> [{hours <unit:1..24>}] [{minutes <uint:1..59>}] [{seconds <uint:1..59>}] }
И его реализация:
class TagTimePeriod < ClsRuby::TagNoValue child_tag :hours, ClsRuby::TagUintScalar, :constraint => 1..24 child_tag :minutes, ClsRuby::TagUintScalar, :constraint => 1...60 child_tag :seconds, ClsRuby::TagUintScalar, :constraint => 1...60 # Если задан ключ :value, то сразу делает тег определеным, # используюя его значение. При этом считается, что задано # значение в секундах. def initialize( params = {} ) super( params ) tag_handle_opt_param( :value ) do |v| self.value = v end end def tag_on_finish super fail 'at least one tag ({hours}/{minutes}/{seconds}) ' + "must be defined for {#{tag_name}}" unless @hours.tag_defined? || @minutes.tag_defined? || @seconds.tag_defined? end def value @hours.fetch( 0 ) * 3600 + @minutes.fetch( 0 ) * 60 + @seconds.fetch( 0 ) end def value=( seconds ) tag_reset h = seconds / 3600; seconds %= 3600 @hours.value = h if 0 != h m = seconds / 60; seconds %= 60 @minutes.value = m if 0 != m @seconds.value = seconds if 0 != seconds tag_make_defined if @hours.tag_defined? || @minutes.tag_defined? || @seconds.tag_defined? end end
Этот тег может использоваться в разных контекстах. Например:
{service {config-reload-period <TagTimePeriod> } {channel-params {ping-timeout <TagTimePeriod> } ... } {request-processing {max-wait-time <TagTimePeriod> } ... } }
теги {config-reload-period}, {ping-timeout} и {max-wait-time} могут быть реализованы с помощью TagTimePeriod:
class TagChannelParams < ClsRuby::TagNoValue mandatory_child_tag :ping_timeout, TagTimePeriod, :name => 'ping-timeout' ... end class TagRequestProcessing < ClsRuby::TagNoValue mandatory_child_tag :max_wait_time, TagTimePeriod, :name => 'max-wait-time' ... end class TagService < ClsRuby::TagNoValue mandatory_child_tag :config_reload_period, TagTimePeriod, :name => 'config-reload-period' mandatory_child_tag :channel_params, TagChannelParams, :name => 'channel-params' mandatory_child_tag :requiest_processing, TagRequestProcessing, :name => 'request-processing' ... end
Еще одна причина – упрощение сопровождения кода по разбору или формированию Cls-документа. Когда детали преобразования прикладных значений в Cls теги скрыты в реализации какого-то класса их проще изменять и расширять со временем. Кроме того, когда сложный тег оформлен в виде отдельного класса, то легче понять как он работает (даже при устаревших или отсутствующих комментариях).
И еще одна причина – упрощение работы с повторяющимися тегами. Как в показанном выше примере с тегами {field} внутри тега {message}. Каждый такой повторяющийся тег – это экземпляр TagField. Но класс TagMessage скрывает это от пользователя, создавая впечатление, что есть только набор объектов Field и больше ничего.
Входящие в состав ClsRuby штатные теги не расчитаны на сохранения строго порядка следования тегов во входном потоке. Так, показанная выше реализация тега TagTimePeriod не зависит от того, в каком порядке будут заданы значения тегов {hours}, {minutes} и {seconds}:
{config-reload-period {hours 3} {seconds 15} {minutes 40}} {config-reload-period {minutes 40} {seconds 15} {hours 3}} {config-reload-period {minutes 40} {hours 3} {seconds 15}}
все эти записи приведут к одному и тому же результату.
Такое поведение выбрано специально, поскольку основное назначение ClsRuby – разбор конфигурационных файлов, которые зачастую редактируются вручную. Если конфигурационный файл не налагает ограничений на взаимное расположение тегов одного уровня, то исправлять такой конфигурационный файл гораздо проще.
В некоторых случаях штатные теги ClsRuby являются чувствительными к порядку следования значений во входном потоке. Это относится к тегам ClsRuby::TagVectorOfTags (последовательность тегов с одинаковым именем) и ClsRuby::TagScalarVector (последовательность значений в одном теге). Это так же являются частью дизайна ClsRuby, поскольку данные теги предназначены для обработки последовательностей, в которых порядок следования элементов важен:
{compilation-params || Теги {include-path} обрабатываются с помощью TagVectorOfTags. || Их взаимный порядок следования важен, т.к. заданные первыми || значения имеют больший приоритет. {include-path "/home/eao197" } {include-path "/usr/share/include" } ... || Тег {copyright-info} обрабатывается с помощью TagScalarVector. || Порядок следования значений в нем важен, т.к. нарушение порядка || приведет к нарушению текста лицензионного соглашения. {copyright-info "Copyright (C) 2006, 2007 The ClsRuby Project. All rights reserved." "Redistribution and use in source and ..." } ... }
Особенность тега ClsRuby::TagVectorOfTags в том, что он сохраняет относительный порядок следования только тегов с одинаковым именем. Если же между этими тегами будут находится другие теги во входном потоке, то информация о взаимном расположении тегов с разными именами в ClsRuby::TagVectorOfTags не сохраняется – сохраняется только информация о взаимном расположении тегов с одинаковыми именами:
require 'cls-ruby' require 'cls-ruby/default_formatter' require 'cls-ruby/tag_no_value' require 'cls-ruby/tag_scalar' require 'cls-ruby/tag_vector_of_tags' # Тег, который использует TagVectorOfTags. # class TagCompilationParams < ClsRuby::TagNoValue child_tag :include_path, ClsRuby::TagVectorOfTags, :type => ClsRuby::TagStringScalar, :name => 'include-path' child_tag :define, ClsRuby::TagVectorOfTags, :type => ClsRuby::TagStringScalar default_tag_params :name => 'compilation-params' end # Входной поток, в котором теги {include-path} и {define} чередуются. # INPUT = <<END_OF_INPUT {compilation-params {include-path "/home/eao197" } {define "NDEBUG" } {include-path "/usr/share/include" } {define "X86_ARCH" } {include-path "/usr/local/include" } {define "STATIC_LIB" } } END_OF_INPUT # Разбор входного потока. # tag = TagCompilationParams.new ClsRuby::parse_string( INPUT, tag ) # Форматирование результата разбора в виде выходного потока. # tag.tag_format( ClsRuby::DefaultFormatter.new( STDOUT ) )
в результате работы этого скрипта получается:
{compilation-params {include-path "/home/eao197" } {include-path "/usr/share/include" } {include-path "/usr/local/include" } {define "NDEBUG" } {define "X86_ARCH" } {define "STATIC_LIB" } }
т.е. сохранился порядок следования тегов с одинаковыми именами, но не сохранилось их начальное взаимное расположение.
Как уже было сказано, подобное игнорирование взаимного расположения тегов с разными именами является частью дизайна ClsRuby. Если при разборе входного потока требуется сохранять этот оносительный порядок, то придется создавать собственные теги или использовать тег ClsRuby::TagAny.
Существует так же тег ClsRuby::TagVectorOfDifferentTags. Но он всего лишь является аналогом ClsRuby::TagVectorOfTags для случая, когда нужно сохранить относительный порядок следования повтояющихся тегов с разными именами.
# vim:ts=2:sts=2:sw=2:expandtab:ft=txt:tw=78
Generated with the Darkfish Rdoc Generator 2.