| # 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 |
| |