Friday, February 5, 2010

slightly better kludge for suppressing ruby soap warnings

code i ended up with, followed by a description of the journey:

#monkeypatch kludge to get Parser to suppress "ignored element" warnings
#generated when mumboe-soap4r consumes wsdl that asp.net serves
#TODO: find a cleaner way to do this
class WSDL::Parser
old_initialize = instance_method(:initialize)
define_method(:initialize) do |*args, &block|
old_initialize.bind(self).call(*args, &block)

#set up QNames to ignore
names = ["binding", "operation", "body", "header", "address"]
namespace = "http://schemas.xmlsoap.org/wsdl/soap12/"
ignored = names.inject({}) do |ignored, name|
elename = XSD::QName.new(namespace, name)
ignored.update(elename => elename)
end

instance_variable_set(:@ignored, ignored)
end
end

how i got there


yesterday i implemented a kludge to suppress these warnings, but it was a bit broad. it would suppress everything printed to stderr during decode_tag. i refined that method to get to the code above. to make yesterday's kludge, i had to read some of the code in /var/lib/gems/1.9.0/gems/mumboe-soap4r-1.5.8.3/lib/wsdl/parser.rb. i noticed the use of @ignored to prevent duplicate warnings. i figured i could populate it with the things i don't care about before doing any wsdl consumption. turns out that worked.

what warnings to suppress


so i needed to print out the contents of @ignored. at first, i tried monkeypatching decode_tag to print out stuff as it was added to @ignored but that resulted in many duplicates. ironically, avoiding this duplication is exactly the reason for having @ignored. instead i went to the place where decode_tag was first called and monkeypatched that method (start_element).

deeper


i found that @ignored was populated with XSD::QNames. so i went to .../lib/xsd/qname.rb to find out how to create them and how they were compared. QName creation requires a namespace and a name. so i needed to have each key and value in @ignore print its namespace and name. fortunately these were readable attributes, so i just had to modify start_element in wsdl/parser.rb

back to wsdl/parser.rb


so i landed on this monkeypatch, which is the slickness:

class WSDL::Parser
def parse(string_or_readable)
@parsestack = []
@lastnode = nil
@textbuf = ''
@parser.do_parse(string_or_readable)
puts
puts "ignored = {"
@ignored.each do |key, value|
puts "XSD::QName.new(\"#{key.namespace}\", \"#{key.name}\") =>"
puts "XSD::QName.new(\"#{value.namespace}\", \"#{value.name}\"),"
end
puts "}"
@lastnode
end
end

that's right, it prints out the information i need formatted as ruby code so i just copy-pasta'd the output back into my code and massaged it a little.

kinda sorta almost


this result was almost what i wanted. here's the code:

class WSDL::Parser
old_initialize = instance_method(:initialize)
define_method(:initialize) do |*args, &block|
old_initialize.bind(self).call(*args, &block)
ignored = {
XSD::QName.new("http://schemas.xmlsoap.org/wsdl/soap12/", "binding") =>
XSD::QName.new("http://schemas.xmlsoap.org/wsdl/soap12/", "binding"),
XSD::QName.new("http://schemas.xmlsoap.org/wsdl/soap12/", "operation") =>
XSD::QName.new("http://schemas.xmlsoap.org/wsdl/soap12/", "operation"),
XSD::QName.new("http://schemas.xmlsoap.org/wsdl/soap12/", "body") =>
XSD::QName.new("http://schemas.xmlsoap.org/wsdl/soap12/", "body"),
XSD::QName.new("http://schemas.xmlsoap.org/wsdl/soap12/", "header") =>
XSD::QName.new("http://schemas.xmlsoap.org/wsdl/soap12/", "header"),
XSD::QName.new("http://schemas.xmlsoap.org/wsdl/soap12/", "address") =>
XSD::QName.new("http://schemas.xmlsoap.org/wsdl/soap12/", "address"),
}
instance_variable_set(:@ignored, ignored)
end
end

this was nearly good enough for me to stop. it suppressed exactly what i knew about. nothing more, nothing less. but that's a lot of text. and a lot of it is repepetetetivive. and every QName is created twice, so technically it wasn't populating @ignored in exactly the same way as decode_tag does. and what if the namespace needs changing, or i want to add another name? i felt this would be annoying to maintain.

another step


so i used a variable to store the namespace and used a variable to store each QName.
here's that code:

class WSDL::Parser
old_initialize = instance_method(:initialize)
define_method(:initialize) do |*args, &block|
old_initialize.bind(self).call(*args, &block)
namespace = "http://schemas.xmlsoap.org/wsdl/soap12/"
binding = XSD::QName.new(namespace, "binding")
operation = XSD::QName.new(namespace, "operation")
body = XSD::QName.new(namespace, "body")
header = XSD::QName.new(namespace, "header")
address = XSD::QName.new(namespace, "address")
ignored = {
binding => binding,
operation => operation,
body => body,
header => header,
address => address,
}
instance_variable_set(:@ignored, ignored)
end
end

all right. better. but ... augh, i can't let it be that way. i mean, come on! for every ignored thing, the name of the ignored thing shows up 4 times, on 2 different lines that aren't right next to each other. still not DRY enough. i figured i could create an array with symbols and do something with that. turns out that 3 of the 4 times a name showed up, it was just a temporary variable and the true essence could be summed up more generically so that any particular name was only given once! so i messed with Enumerable#each and finally landed on a workable solution using Enumberable#inject.

final code again (so you don't have to scroll up):



#monkeypatch kludge to get Parser to suppress "ignored element" warnings
#generated when mumboe-soap4r consumes wsdl that asp.net serves
#TODO: find a cleaner way to do this
class WSDL::Parser
old_initialize = instance_method(:initialize)
define_method(:initialize) do |*args, &block|
old_initialize.bind(self).call(*args, &block)

#set up QNames to ignore
names = ["binding", "operation", "body", "header", "address"]
namespace = "http://schemas.xmlsoap.org/wsdl/soap12/"
ignored = names.inject({}) do |ignored, name|
elename = XSD::QName.new(namespace, name)
ignored.update(elename => elename)
end

instance_variable_set(:@ignored, ignored)
end
end

now when someone wants to add a new ignored name, they just add it to the list of names that get ignored! one place! <comma> <space> <quotation mark> <name> <quotation mark>. the rest of the work is done generically, automatically. and i think reading this is easier than reading the intermediate steps. there's less to read. less dense. easier to update. a good kludge.
so i'm glad that's done. ... or is it?

it's still a kludge


ultimately, this is neat and all, but it's still a kludge. i need to figure out why these warnings are generated at all. i don't want to have to read a wsdl, or read more code involved in the process of parsing a wsdl. a brief look tells me it must be that parent.parse_element(elename) returns nil in some case. i don't know what type parent is and i don't feel like searching any further right now.

Thursday, February 4, 2010

kludge: suppress mumboe-soap4r warnings

first the code, then the explanation:

#monkeypatch kludge to get Parser to suppress "ignored element" warnings
#generated when mumboe-soap4r consumes wsdl that asp.net serves
#TODO: find a cleaner way to do this
require 'stringio'
class WSDL::Parser
private
old_decode_tag = instance_method(:decode_tag)
ignored_string = StringIO.new
define_method(:decode_tag) do |*args, &block|
old_stderr, $stderr = $stderr, ignored_string
result = old_decode_tag.bind(self).call(*args, &block)
$stderr = old_stderr
result
end
end

situation


recently i've been dealing with soap. T_T. but i'm getting paid. ^_^. i'm writing the client in ruby 1.9 using mumboe-soap4r lib (gem1.9 install -r mumboe-soap4r). asp.net serves up the web services. the wsdl that asp.net serves has elements that the ruby lib ignores. mumboe-soap4r generates warnings printed to stderr.
the warnings (modified to be more generic):

ignored element: {http://schemas.xmlsoap.org/wsdl/soap12/}binding : #<WSDL::Binding:0xd2c490 {http://example.com/ExampleServices}ExampleSoap12>
ignored element: {http://schemas.xmlsoap.org/wsdl/soap12/}operation : #<WSDL::OperationBinding:0xd21a54 CheckLogOnCredentials>
ignored element: {http://schemas.xmlsoap.org/wsdl/soap12/}body : #<WSDL::Param:0xd20a78 >
ignored element: {http://schemas.xmlsoap.org/wsdl/soap12/}header : #<WSDL::Param:0xd20a78 >
ignored element: {http://schemas.xmlsoap.org/wsdl/soap12/}address : #<WSDL::Port:0x1083bfc {http://example.com/ExampleServices}ExampleSoap12>

motivation


this was printed out to stderr every time the client grabbed the wsdl and parsed it. reading unit test results was quite annoying. and running the client was annoying. i could just redirect stderr to /dev/null, but then i wouldn't get any errors at all. this code diagnoses problems and uses stderr to report those problems. so i need something more precise.

solution


so instead i redirect stderr but only for the duration of the method that actually generates warnings. the file /var/lib/gems/1.9.0/gems/mumboe-soap4r-1.5.8.3/lib/wsdl/xmlSchema/parser.rb (on ubuntu 9.10) defines the decode_tag method. i found it like this:

% gem1.9 environment|grep INSTALLATION
- INSTALLATION DIRECTORY: /var/lib/gems/1.9.0
% cd /var/lib/gems/1.9.0/
% ls
% cd mumboe-soap4r-1.5.8.3/lib
% grep -r "ignored element" *
wsdl/parser.rb
wsdl/xmlSchema/parser.rb

then i went back to a blog post i read earlier: jay fields alias method alternative. i also noticed there a link to another post by martin on method replacing. then i did a bit of trial and error to get it working for me in this case, where the class is defined in a module. once i was able to replace the method with my code, written in my file, i just needed to write code that would suppress stderr, call the original method, then resurrect stderr. temporarily redirecting stdout (or stderr) is common enough that someone had written nearly exactly what i needed. i didn't need to search for long.

Where am I?