Class Index [+]

Quicksearch

Принципы работы с Cls. Введение.

Элементы из которых состоит Cls документ

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 документа

Предположим, что нужно представить в виде 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 }
}

Механизм разбора входного Cls документа

Парсинг входного потока построен на том, что каждому тегу во входном потоке соответствует объект-обработчик, производный от типа ClsRuby::Tag. Парсеру (ClsRuby#parse_io, ClsRuby#parse_string, ClsRuby#parse_file) задается набор объектов-обработчиков верхнего уровня – соответствующие им теги являются самыми высокоуровневыми тегами входного потока (т.е. у них нет родителей).

Когда парсер находит во входном потоке имя тега, он просматривает все известные ему обработчики и ищет того обработчика, который способен обработать найденный тег (для этого используется ClsRuby::Tag#tag_compare_name). Если обработчик найден, то:

Если обработчик не найден, то порождается ошибка (посредством исключения 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). Если обработчик найден, то:

И т.д. до тех пор, пока парсер не найдет закрывающую фигурную скобку, которая означает завершение текущего тега. Тогда парсер:

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

Когда для тега вызывается метод 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 для хранения значений

Можно использовать готовые теги 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

[Validate]

Generated with the Darkfish Rdoc Generator 2.