blob: 7eee953c044d56b80be2ebebb298a2f70e62f424 [file] [log] [blame]
# 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",
"urls" => "str-16",
"queues" => "str-8",
"prepared" => "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
@verbose=false
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
if @verbose
puts "Skipped #{@path} - unchanged" # Dont generate if unchanged
end
else
@path.open('w') { |f| f << @out }
if @verbose
puts "Generated #{@path}"
end
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