lib/ruote/reader.rb in ruote-2.2.0 vs lib/ruote/reader.rb in ruote-2.3.0
- old
+ new
@@ -1,7 +1,7 @@
#--
-# Copyright (c) 2005-2011, John Mettraux, jmettraux@gmail.com
+# Copyright (c) 2005-2012, John Mettraux, jmettraux@gmail.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
@@ -20,16 +20,18 @@
# THE SOFTWARE.
#
# Made in Japan.
#++
-
require 'uri'
require 'open-uri'
require 'rufus/json'
-require 'ruote/reader/ruby_dsl' # just making sure it's loaded
require 'ruote/reader/xml'
+require 'ruote/reader/json'
+require 'ruote/reader/radial'
+require 'ruote/reader/ruby_dsl' # just making sure it's loaded
+require 'ruote/util/mpatch'
require 'ruote/util/subprocess'
module Ruote
@@ -38,10 +40,36 @@
#
# Can reader XML, JSON, Ruby (and more) process definition representations.
#
class Reader
+ # This error is emitted by the reader when it failed to read a process
+ # definition (passed as a string).
+ #
+ class Error < ArgumentError
+
+ attr_reader :definition
+ attr_reader :ruby, :radial, :xml, :json
+
+ def initialize(definition)
+ super('cannot read process definition')
+ @definition = definition
+ end
+
+ def <<(args)
+ type, error = args
+ type = type.to_s.match(/^Ruote::(.+)Reader$/)[1].downcase
+ instance_variable_set("@#{type}", error)
+ end
+
+ # Returns the most likely error cause...
+ #
+ def cause
+ @ruby || @radial || @xml || @json
+ end
+ end
+
def initialize(context)
@context = context
end
@@ -50,26 +78,45 @@
#
def read(definition)
return definition if Ruote.is_tree?(definition)
- (return XmlReader.read(definition)) rescue nil
- (return Rufus::Json.decode(definition)) rescue nil
- (return ruby_eval(definition)) rescue nil
+ raise ArgumentError.new(
+ "cannot read process definitions of class #{definition.class}"
+ ) unless definition.is_a?(String)
- if definition.index("\n").nil? && definition.index(' ').nil?
+ if is_uri?(definition)
- raise ArgumentError.new(
- "remote process definitions are not allowed"
- ) if Ruote::Reader.remote?(definition) && @context['remote_definition_allowed'] != true
+ if
+ Ruote::Reader.remote?(definition) &&
+ @context['remote_definition_allowed'] != true
+ then
+ raise ArgumentError.new('remote process definitions are not allowed')
+ end
return read(open(definition).read)
end
- raise ArgumentError.new(
- "doesn't know how to read definition (#{definition.class}) " +
- "or error in process definition")
+ tree = nil
+ error = Error.new(definition)
+
+ [
+ Ruote::RubyReader, Ruote::RadialReader,
+ Ruote::XmlReader, Ruote::JsonReader
+ ].each do |reader|
+
+ next if tree
+ next unless reader.understands?(definition)
+
+ begin
+ tree = reader.read(definition, @context.treechecker)
+ rescue => e
+ error << [ reader, e ]
+ end
+ end
+
+ tree || raise(error)
end
# Class method for parsing process definition (XML, Ruby, from file or
# from a string, ...) to syntax trees. Used by ruote-fluo for example.
#
@@ -92,59 +139,138 @@
#
# Mainly used by ruote-fluo.
#
def self.to_xml(tree, options={})
- require 'builder'
+ s = StringIO.new
+ s.puts('<?xml version="1.0" encoding="UTF-8"?>')
- # TODO : deal with "participant 'toto'"
+ _to_xml(tree, options[:indent], 0, s)
- builder(options) do |xml|
+ s.string
+ end
- atts = tree[1].dup
+ # Not as good as the builder gem, but at least doesn't come bundled with
+ # lib/blankslate.rb
+ #
+ def self._to_xml(tree, indent, level, s) # :nodoc:
- t = atts.find { |k, v| v == nil }
- if t
- atts.delete(t.first)
- key = tree[0] == 'if' ? 'test' : 'ref'
- atts[key] = t.first
- end
+ atts = tree[1].dup
- atts = atts.inject({}) { |h, (k, v)| h[k.to_s.gsub(/\_/, '-')] = v; h }
+ if t = atts.find { |k, v| v == nil }
+ atts.delete(t.first)
+ atts[tree[0] == 'if' ? 'test' : 'ref'] = t.first
+ end
- if tree[2].empty?
- xml.tag!(tree[0], atts)
- else
- xml.tag!(tree[0], atts) do
- tree[2].each { |child| to_xml(child, options) }
- end
- end
+ atts = atts.remap { |(k, v), h| h[k.to_s.gsub(/\_/, '-')] = v }
+ atts = atts.to_a.sort_by { |k, v| k }
+
+ s.print ' ' * level
+
+ s.print '<'
+ s.print tree[0]
+
+ if atts.any?
+ s.print ' '
+ s.print atts.collect { |k, v|
+ "#{k}=#{v.is_a?(String) ? v.inspect : v.inspect.inspect}"
+ }.join(' ')
end
+
+ if tree[2].empty?
+
+ s.puts '/>'
+
+ else
+
+ s.puts '>'
+
+ tree[2].each { |child| _to_xml(child, indent, level + (indent || 0), s) }
+
+ s.print ' ' * level
+ s.print '</'
+ s.print tree[0]
+ s.puts '>'
+ end
end
# Turns the given process definition tree (ruote syntax tree) to a Ruby
# process definition (a String containing that ruby process definition).
#
# Mainly used by ruote-fluo.
#
def self.to_ruby(tree, level=0)
expname = tree[0]
-
expname = 'Ruote.process_definition' if level == 0 && expname == 'define'
- s = "#{' ' * level}#{expname}#{atts_to_ruby(tree[1])}"
+ s = ' ' * level + expname + atts_to_ruby(tree[1])
return "#{s}\n" if tree[2].empty?
s << " do\n"
tree[2].each { |child| s << to_ruby(child, level + 1) }
s << "#{' ' * level}end\n"
s
end
+ # Turns the given tree into a radial process definition.
+ #
+ def self.to_radial(tree, level=0)
+
+ s = ' ' * level + tree[0] + atts_to_radial(tree[1]) + "\n"
+
+ return s if tree[2].empty?
+
+ tree[2].inject(s) { |ss, child| ss << to_radial(child, level + 1); ss }
+ end
+
+ # Produces an expid annotated radial version of the process definition,
+ # like:
+ #
+ # 0 define name: "nada"
+ # 0_0 sequence
+ # 0_0_0 alpha
+ # 0_0_1 participant "bravo", timeout: "2d", on_board: true
+ #
+ # Can be useful when debugging noisy engines.
+ #
+ def self.to_expid_radial(tree)
+
+ lines = to_raw_expid_radial(tree, '0')
+ max = lines.collect { |l| l[1].length }.max
+
+ lines.collect { |l|
+ "%#{max}s " % l[1] + " " * l[0] + l[2] + l[3]
+ }.join("\n")
+ end
+
+ # Used by .to_expid_radial. Outputs an array of 'lines'. Each line
+ # is a process definition line, represented as an array:
+ #
+ # [ level, expid, name, atts ]
+ #
+ # Like in:
+ #
+ # [[0, "0", "define", " name: \"nada\""],
+ # [1, "0_0", "sequence", ""],
+ # [2, "0_0_0", "alpha", ""],
+ # [2, "0_0_1", "participant", " \"bravo\", timeout: \"2d\"]]
+ #
+ def self.to_raw_expid_radial(tree, expid='0')
+
+ i = -1
+
+ [
+ [ expid.split('_').size - 1, expid, tree[0], atts_to_radial(tree[1]) ]
+ ] +
+ tree[2].collect { |t|
+ i = i + 1; to_raw_expid_radial(t, "#{expid}_#{i}")
+ }.flatten(1)
+ end
+
# Turns the process definition tree (ruote syntax tree) to a JSON String.
#
def self.to_json(tree)
tree.to_json
@@ -159,61 +285,72 @@
(u.scheme != nil) && ( ! ('A'..'Z').include?(u.scheme))
end
protected
- # Evaluates the ruby string in the code, but at fist, thanks to the
- # treechecker, makes sure it doesn't code malicious ruby code (at least
- # tries very hard).
+ # Minimal test. Used by #read.
#
- def ruby_eval(s)
+ def is_uri?(s)
- @context.treechecker.definition_check(s)
- eval(s)
+ return false if s.index("\n")
- rescue Exception => e
- #
- # have to catch everything (SyntaxError included)
+ ((URI.parse(s); true) rescue false)
+ end
- #puts '=' * 80
- #p s
- #puts '-' * 80
- #puts e
- #e.backtrace.each { |l| puts l }
+ def self.to_ra_string(o)
- raise ArgumentError.new('probably not ruby')
+ return 'nil' if o == nil
+
+ s = o.to_s
+
+ return s if [ true, false ].include?(o)
+
+ i = o.inspect
+
+ return i if %w[ true false nil ].include?(s)
+ return i if s.match(/[\s:]/)
+ return s if i == "\"#{o.to_s}\""
+
+ i
end
- # A convenience method when building XML
+ # split the txt => nil entry and sorts the rest of the attributes.
#
- def self.builder(options={}, &block)
+ def self.split_atts(atts)
- if b = options[:builder]
- block.call(b)
- else
- b = Builder::XmlMarkup.new(:indent => (options[:indent] || 0))
- options[:builder] = b
- b.instruct! unless options[:instruct] == false
- block.call(b)
- b.target!
- end
+ atts = atts.to_a.sort_by { |k, v| k }
+ txt = atts.find { |k, v| v == nil }
+ atts.delete(txt) if txt
+
+ [ txt ? txt.first : nil, atts ]
end
- # As used by to_ruby.
+ # As used by to_radial
#
- def self.atts_to_ruby(atts)
+ def self.atts_to_radial(atts, &block)
- return '' if atts.empty?
+ s = []
+ txt, atts = split_atts(atts)
+ s << to_ra_string(txt) if txt
+ s += atts.collect { |k, v| "#{to_ra_string(k)}: #{to_ra_string(v)}" }
+
+ s = s.join(', ')
+
+ s.length > 0 ? " #{s}" : s
+ end
+
+ # As used by to_ruby
+ #
+ def self.atts_to_ruby(atts, &block)
+
s = []
+ txt, atts = split_atts(atts)
- t = atts.find { |k, v| v == nil }
- s << t.first.inspect if t
+ s << txt.inspect if txt
+ s += atts.collect { |k, v| ":#{k} => #{v.inspect}" }
- s = atts.inject(s) { |a, (k, v)|
- a << ":#{k} => #{v.inspect}" if t.nil? || k != t.first
- a
- }.join(', ')
+ s = s.join(', ')
s.length > 0 ? " #{s}" : s
end
end
end