Class: AceMQ::AMQP::XMLCodec
- Inherits:
-
Object
- Object
- AceMQ::AMQP::XMLCodec
- Defined in:
- lib/acemq/amqp/codec/xml.rb
Overview
Reads and writes XML.
Here because most estates have something that speaks XML and will not be rewritten, and a messaging library that cannot talk to it forces a translation layer nobody wants to own. New services should publish JSON; this exists so the ones that cannot are not a special case.
No document type declaration, ever
A body carrying a <!DOCTYPE> is refused before it is parsed. A DTD is
how an XML message reads files off the machine handling it, opens
connections on its behalf, and expands twelve bytes into a gigabyte of
heap — and a queue is exactly the sort of place a message from somewhere
unexpected arrives. This is not configurable, because the configuration
would only ever be wrong, and it costs nothing: the Java library disables
DTD support outright and neither Jackson nor Go's encoding/xml writes
one.
What a decoded message looks like
XML has no types, so every leaf comes back as a String and the caller
converts. The root element's name is dropped, because it names the
message rather than being part of it — which is also how Jackson and
encoding/xml read one into a class:
<order><id>A-1</id><line>widget</line><line>gasket</line></order>
# => { "id" => "A-1", "line" => ["widget", "gasket"] }
Elements repeated under one parent become an Array, which is what makes the Go library's lists readable here; Jackson wraps its lists in an element of their own instead, and that comes back as the nesting it is. Attributes become ordinary keys beside the elements, and an element holding both attributes and text puts the text under the empty key, which is the convention Jackson uses reading into a Map.
REXML
Ruby's own XML parser, and required lazily. It ships with Ruby, but it has been a bundled gem rather than a default one since Ruby 3.4, which means a Bundler process only has it if a Gemfile says so. The gem declares no runtime dependencies, so the failure is caught here and re-raised saying exactly that rather than leaving somebody with +cannot load such file -- rexml/document+.
Constant Summary collapse
- CONTENT_TYPE =
What this codec writes, and what Java and Go write.
"application/xml"- ALSO_READS =
Predates the registration and is still what a lot of tooling writes.
["text/xml"].freeze
- TEXT_KEY =
Where the text goes when an element has attributes as well.
""- ELEMENT_NAME =
A name XML will accept for an element.
/\A[A-Za-z_][\w.-]*\z/
Instance Method Summary collapse
-
#can_decode?(content_type) ⇒ Boolean
Accepts
application/xml,text/xmland any +...+xml+ media type. - #content_type ⇒ Object
- #decode(body) ⇒ Hash, String
- #encode(payload) ⇒ String
-
#initialize(root: "message") ⇒ XMLCodec
constructor
A new instance of XMLCodec.
Constructor Details
#initialize(root: "message") ⇒ XMLCodec
Returns a new instance of XMLCodec.
83 84 85 86 87 88 89 90 91 |
# File 'lib/acemq/amqp/codec/xml.rb', line 83 def initialize(root: "message") unless root.to_s.match?(ELEMENT_NAME) raise ArgumentError, "#{root.inspect} is not a name XML will take for an element" end @root = root.to_s load_parser! freeze end |
Instance Method Details
#can_decode?(content_type) ⇒ Boolean
Accepts application/xml, text/xml and any +...+xml+ media type.
Never a message whose sender set no content type. XML is rarely what arrives unannounced, and a codec that guessed wrong here would turn a readable message into a rejected one.
131 132 133 134 135 136 |
# File 'lib/acemq/amqp/codec/xml.rb', line 131 def can_decode?(content_type) lower = content_type.to_s.downcase return false if lower.empty? lower.start_with?(CONTENT_TYPE, *ALSO_READS) || lower.include?("+xml") end |
#content_type ⇒ Object
93 |
# File 'lib/acemq/amqp/codec/xml.rb', line 93 def content_type = CONTENT_TYPE |
#decode(body) ⇒ Hash, String
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/acemq/amqp/codec/xml.rb', line 109 def decode(body) text = body.to_s if text.match?(/<!DOCTYPE/i) raise DecodeError, "this message carries a document type declaration, which this codec refuses " \ "to parse: a DTD is how an XML body reads files off the machine handling it " \ "and expands a few bytes into a heap full of them." end root = REXML::Document.new(text).root raise DecodeError, "this message has no XML element in it" unless root read(root) rescue REXML::ParseException => e raise DecodeError, "this message is not XML: #{e..lines.first.to_s.strip}" end |
#encode(payload) ⇒ String
98 99 100 101 102 103 104 |
# File 'lib/acemq/amqp/codec/xml.rb', line 98 def encode(payload) document = REXML::Document.new fill(document.add_element(@root), payload) out = +"" REXML::Formatters::Default.new.write(document, out) out end |