# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # Generic AMQP code generation library. # # TODO aconway 2008-02-21: # # The amqp_attr_reader and amqp_child_reader for each Amqp* class # should correspond exactly to ampq.dtd. Currently they are more # permissive so we can parse 0-10 preview and 0-10 final XML. # # Code marked with "# preview" should be removed/modified when final 0-10 # is complete and we are ready to remove preview-related code. # require 'delegate' require 'rexml/document' require 'pathname' require 'set' include REXML # Handy String functions for converting names. class String # Convert to CapitalizedForm. def caps() gsub( /(^|\W)(\w)/ ) { |m| $2.upcase } end # Convert to underbar_separated_form. def bars() tr('- .','_'); end # Convert to ALL_UPPERCASE_FORM def shout() bars.upcase; end # Convert to lowerCaseCapitalizedForm def lcaps() gsub( /\W(\w)/ ) { |m| $1.upcase } end def plural() self + (/[xs]$/ === self ? 'es' : 's'); end end # Sort an array by name. module Enumerable def sort_by_name() sort { |a,b| a.name <=> b.name }; end end # Add functions similar to attr_reader for AMQP attributes/children. # Symbols that are ruby Object function names (e.g. class) get # an "_" suffix. class Module # Add trailing _ to avoid conflict with Object methods. def mangle(sym) sym = (sym.to_s+"_").to_sym if (Object.method_defined?(sym) or sym == :type) sym end # Add attribute reader for XML attribute. def amqp_attr_reader(*attrs) attrs.each { |a| case a when Symbol define_method(mangle(a)) { @amqp_attr_reader||={ } @amqp_attr_reader[a] ||= xml.attributes[a.to_s] } when Hash a.each { |attr, default| define_method(mangle(attr)) { @amqp_attr_reader||={ } value = xml.attributes[attr.to_s] if value @amqp_attr_reader[attr] ||= value else @amqp_attr_reader[attr] ||= default end } } end } end # Add 2 child readers: # elname(name) == child('elname',name) # elnames() == children('elname') def amqp_child_reader(*element_names) element_names.each { |e| define_method(mangle(e)) { |name| child(e.to_s, name) } define_method(mangle(e.to_s.plural)) { children(e.to_s) } } end # When there can only be one child instance def amqp_single_child_reader(*element_names) element_names.each { |e| define_method(mangle(e)) { children(e.to_s)[0] } } end end # An AmqpElement contains an XML element and provides a convenient # API to access AMQP data. # # NB: AmqpElements cache values from XML, they assume that # the XML model does not change after the AmqpElement has # been created. class AmqpElement def wrap(xml) return nil if ["assert","rule"].include? xml.name eval("Amqp"+xml.name.caps).new(xml, self) or raise "nil wrapper" end public def initialize(xml, parent) @xml, @parent=xml, parent @children=xml.elements.map { |e| wrap e }.compact @cache_child={} @cache_child_named={} @cache_children={} @cache_children[nil]=@children end attr_reader :parent, :xml, :children, :doc amqp_attr_reader :name, :label # List of children of type elname, or all children if elname # not specified. def children(elname=nil) if elname @cache_children[elname] ||= @children.select { |c| elname==c.xml.name } else @children end end def each_descendant(&block) yield self @children.each { |c| c.each_descendant(&block) } end def collect_all(amqp_type) collect=[] each_descendant { |d| collect << d if d.is_a? amqp_type } collect end # Look up child of type elname with attribute name. def child(elname, name) @cache_child[[elname,name]] ||= children(elname).find { |c| c.name==name } end # Look up any child with name def child_named(name) @cache_child_named[name] ||= @children.find { |c| c.name==name } end # The root element. def root() @root ||=parent ? parent.root : self; end def to_s() "#<#{self.class}(#{fqname})>"; end def inspect() to_s; end # Text of doc child if there is one. def doc() d=xml.elements["doc"]; d and d.text; end def fqname() throw "fqname: #{self} #{parent.fqname} has no name" unless name p=parent && parent.fqname p ? p+"."+name : name; end def containing_class() return self if is_a? AmqpClass return parent && parent.containing_class end # 0-10 array domains are missing element type information, add it here. ArrayTypes={ "str16-array" => "str-16", "amqp-host-array" => "connection.amqp-host-url", "command-fragments" => "session.command-fragment", "in-doubt" => "dtx.xid", "tx-publish" => "str-8", "queues" => "str-8" } def array_type(name) return ArrayTypes[name] if ArrayTypes[name] raise "Missing ArrayType entry for " + name end end class AmqpResponse < AmqpElement def initialize(xml, parent) super; end def fqname() (parent ? parent.dotted_name+"." : "") + "response"; end end class AmqpDoc < AmqpElement def initialize(xml,parent) super; end def text() @xml.text end end class AmqpChoice < AmqpElement def initialize(xml,parent) super; end amqp_attr_reader :name, :value end class AmqpEnum < AmqpElement def initialize(xml,parent) super; end amqp_child_reader :choice end class AmqpDomain < AmqpElement def initialize(xml, parent) super root.used_by[uses].push(fqname) if uses and uses.index('.') end amqp_attr_reader :type amqp_single_child_reader :struct # preview amqp_single_child_reader :enum def uses() type_=="array" ? ArrayTypes[name] : type_; end end class AmqpException < AmqpElement def initialize(xml, amqp) super; end; amqp_attr_reader :error_code end class AmqpField < AmqpElement def initialize(xml, amqp) super; root.used_by[type_].push(parent.fqname) if type_ and type_.index('.') end amqp_single_child_reader :struct # preview amqp_child_reader :exception amqp_attr_reader :type, :default, :code, :required end class AmqpChassis < AmqpElement # preview def initialize(xml, parent) super; end amqp_attr_reader :implement end class AmqpConstant < AmqpElement def initialize(xml, parent) super; end amqp_attr_reader :value, :class end class AmqpResult < AmqpElement def initialize(xml, parent) super; end amqp_single_child_reader :struct # preview amqp_attr_reader :type def name() "result"; end end class AmqpEntry < AmqpElement def initialize(xml,parent) super; end amqp_attr_reader :type end class AmqpHeader < AmqpElement def initialize(xml,parent) super; end amqp_child_reader :entry amqp_attr_reader :required end class AmqpBody < AmqpElement def initialize(xml,parent) super; end amqp_attr_reader :required end class AmqpSegments < AmqpElement def initialize(xml,parent) super; end amqp_child_reader :header, :body end class AmqpStruct < AmqpElement def initialize(xml, parent) super; end amqp_attr_reader :type # preview amqp_attr_reader :size, :code, :pack amqp_child_reader :field def result?() parent.xml.name == "result"; end def domain?() parent.xml.name == "domain"; end end class AmqpMethod < AmqpElement def initialize(xml, parent) super; end amqp_attr_reader :content, :index, :synchronous amqp_child_reader :field, :chassis,:response amqp_single_child_reader :result def on_chassis?(chassis) child("chassis", chassis); end def on_client?() on_chassis? "client"; end def on_server?() on_chassis? "server"; end end # preview: Map command/control to preview method. class AmqpFakeMethod < AmqpMethod def initialize(action) super(action.xml, action.parent); @action=action end def content() return "1" if @action.is_a? AmqpCommand and @action.segments end def index() @action.code end def code() @action.code end def synchronous() end def on_chassis?(chassis) @action.received_by?(chassis) end def pack() "2" end # Encode pack=2, size=4 struct def size() "4" end end class AmqpImplement < AmqpElement def initialize(xml,amqp) super; end amqp_attr_reader :handle, :send end class AmqpRole < AmqpElement def initialize(xml,amqp) super; end amqp_attr_reader :implement end # Base class for command and control. class AmqpAction < AmqpElement def initialize(xml,amqp) super; end amqp_child_reader :implement, :field, :response amqp_attr_reader :code def implement?(role) # we can't use xpath for this because it triggers a bug in some # versions of ruby, including version 1.8.6.110 xml.elements.each {|el| return true if el.name == "implement" and el.attributes["role"] == role } return false end def received_by?(client_or_server) return (implement?(client_or_server) or implement?("sender") or implement?("receiver")) end def pack() "2" end def size() "4" end # Encoded as a size 4 Struct end class AmqpControl < AmqpAction def initialize(xml,amqp) super; end end class AmqpCommand < AmqpAction def initialize(xml,amqp) super; end amqp_child_reader :exception amqp_single_child_reader :result, :segments end class AmqpClass < AmqpElement def initialize(xml,amqp) super; end amqp_attr_reader :index # preview amqp_child_reader :struct, :domain, :control, :command, :role, :method amqp_attr_reader :code def actions() controls+commands; end # preview - command/control as methods def methods_() return (controls + commands).map { |a| AmqpFakeMethod.new(a) } end def method(name) a = (command(name) or control(name)) return AmqpFakeMethod.new(a) end # chassis should be "client" or "server" def methods_on(chassis) # preview @methods_on ||= { } @methods_on[chassis] ||= methods_.select { |m| m.on_chassis? chassis } end # FIXME aconway 2008-04-11: def l4?() # preview !["connection", "session", "execution"].include?(name) && !control? end # FIXME aconway 2008-04-11: def control?() ["connection", "session"].include?(name) end end class AmqpType < AmqpElement def initialize(xml,amqp) super; end amqp_attr_reader :code, :fixed_width, :variable_width end class AmqpXref < AmqpElement def initialize(xml,amqp) super; end end # AMQP root element. class AmqpRoot < AmqpElement amqp_attr_reader :major, :minor, :port, :comment amqp_child_reader :doc, :type, :struct, :domain, :constant, :class def get_root(x) case x when Element then x when Document then x.root else Document.new(x).root end end # Initialize with output directory and spec files from ARGV. def initialize(*specs) raise "No XML spec files." if specs.empty? xml=get_root(specs.shift) specs.each { |s| xml_merge(xml, get_root(s)) } @used_by=Hash.new{ |h,k| h[k]=[] } super(xml, nil) end attr_reader :used_by def merge(root) xml_merge(xml, root.xml); end def version() major + "-" + minor; end def methods_() classes.map { |c| c.methods_ }.flatten; end #preview # Return all methods on chassis for all classes. def methods_on(chassis) @methods_on ||= { } @methods_on[chassis] ||= classes.map { |c| c.methods_on(chassis) }.flatten end def fqname() nil; end private # Merge contents of elements. def xml_merge(to,from) from.elements.each { |from_child| tag,name = from_child.name, from_child.attributes["name"] to_child=to.elements["./#{tag}[@name='#{name}']"] to_child ? xml_merge(to_child, from_child) : to.add(from_child.deep_clone) } end end # Collect information about generated files. class GenFiles @@files = Set.new @@public_api = [] def GenFiles.add(f) @@files.add(f); end def GenFiles.get() @@files; end def GenFiles.public_api(file) @@public_api << file; end def GenFiles.public_api?(file) @@public_api.find { |f| f == file }; end end # Base class for code generators. # Supports setting a per-line prefix, useful for e.g. indenting code. # class Generator # Takes directory for output or "-", meaning print file names that # would be generated. def initialize (outdir, amqp) @outdir=outdir[0] @apidir=outdir[1] @amqp=amqp raise "outdir is not an array" unless outdir.class == Array @prefix=[''] # For indentation or comments. @indentstr=' ' # One indent level. @outdent=2 end # Declare next file to be public API def public_api(file) GenFiles.public_api(file); end # Create a new file, set @out. def file(file, &block) GenFiles.add(file) dir = GenFiles.public_api?(file) ? @apidir : @outdir if (dir != "-") @path=Pathname.new "#{dir}/#{file}" @path.parent.mkpath @out=String.new # Generate in memory first yield if block if @path.exist? and @path.read == @out puts "Skipped #{@path} - unchanged" # Dont generate if unchanged else @path.open('w') { |f| f << @out } puts "Generated #{@path}" end end end # Append multi-line string to generated code, prefixing each line. def gen(str) str.each_line { |line| @out << @prefix.last unless @midline @out << line @midline = nil } # Note if we stopped mid-line @midline = /[^\n]\z/ === str end # Append str + '\n' to generated code. def genl(str="") gen str+"\n"; end # Generate code with added prefix. def prefix(add, &block) @prefix.push @prefix.last+add if block then yield; endprefix; end end def endprefix() @prefix.pop end # Generate indented code def indent(n=1,&block) prefix(@indentstr * n,&block); end alias :endindent :endprefix # Generate outdented code def outdent(&block) @prefix.push @prefix.last[0...-2] if block then yield; endprefix; end end alias :endoutdent :endprefix attr_accessor :out end