diff options
Diffstat (limited to 'qpid/cpp/rubygen/amqpgen.rb')
-rwxr-xr-x | qpid/cpp/rubygen/amqpgen.rb | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/qpid/cpp/rubygen/amqpgen.rb b/qpid/cpp/rubygen/amqpgen.rb new file mode 100755 index 0000000000..20aac35194 --- /dev/null +++ b/qpid/cpp/rubygen/amqpgen.rb @@ -0,0 +1,550 @@ +# 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 <amqp> 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 + |