Class: XmlStruct

Inherits:
Object
  • Object
show all
Defined in:
lib/xml_struct.rb

Overview

Purpose

XMLStruct is a specialised version of OpenStruct that allows you to create XML layout automagically using Ruby assignment. The class supports XML tag attributes and element content.

The class has been designed so that you can rapidly change a flat dataset into an XML layout without all that tedious messing about with Xpaths, tree traversal, nested blocks and the like.

Usage

Create a blank document using the usual #new method.

xml = XmlStruct.new

Add items as required and output using the #to_s command

xml.GovTalkMessage = {:xmlns => "http://www.govtalk.gov.uk/CM/envelope"}
md = xml.GovTalkMessage.Header.MessageDetails
md.Class="IR-PAYE-EOY"
md.Transformation="XML"
xml.GovTalkMessage.GovTalkDetails
xml.GovTalkMessage.Body
xml.to_s

gives

<GovTalkMessage xmlns="http://www.govtalk.gov.uk/CM/envelope">
<Header>
<MessageDetails>
<Class>IR-PAYE-EOY</Class>
<Transformation>XML</Transformation>
</MessageDetails>
</Header>
<GovTalkDetails>
</GovTalkDetails>
<Body>
</Body>
</GovTalkMessage>

Content

Xml content is added using standard Ruby assign. Any wrapper tags are created automatically.

xml.GovTalkMessage.Header.MessageDetails.Class="IR-PAYE-EOY"

XML is output using the #to_s method. The above would output as:

<GovTalkMessage>
<Header>
<MessageDetails>
<Class>IR-PAYE-EOY</Class>
</MessageDetails>
</Header>
</GovTalkMessage>

Blank elements with no content are created by simply referencing the element.

xml.GovTalkMessage.Body

Content is stored in an array like manner within the structure so that you can have multiple elements with the same tag, e.g.

xml.Address.Line[0] = "1 Some Street"
xml.Address.Line[1] = "Somewhere"
xml.Address.Line[2] = "Sometown"
xml.to_s

gives

<Address>
<Line>1 Some Street</Line>
<Line>Somewhere</Line>
<Line>Sometown</Line>
</Address>

Any element can be referenced to a variable as a shortcut. That address example again.

ln = xml.Address.Line
ln[0] = "1 Some Street"
ln[1] = "Somewhere"
ln[2] = "Sometown"

To make things really simple assigning an array of values to an element creates multiple elements with the same tag.

xml.Address.Line = ["1 Some Street", "Somewhere", "Sometown"]

Attributes

You set attributes for elements by assigning a hash of attributes to the element, e.g

xml.GovTalkMessage = {:xmlns => "http://www.govtalk.gov.uk/CM/envelope"}
xml.to_s

gives

<GovTalkMessage xmlns="http://www.govtalk.gov.uk/CM/envelope">
</GovTalkMessage>

Each element can have its own set of attributes - even those with the same element name, eg.

xml.Keys.Key[0]={:Type => "VendorNumber"}
xml.Keys.Key[0]=275687
xml.Keys.Key[1]={:Type => "MainCPH"}
xml.Keys.Key[1]="14/02/0327"
xml.to_s

gives

<Keys>
<Key Type="VendorNumber">275687</Key>
<Key Type="MainCPH">14/02/0327</Key>
</Keys>

Multiple attributes are simply a matter of enumerating all the attributes in the hash, eg.

xml.Keys.Key = {:Type => "Unique Tax Reference", :Length => "10"}

gives

<Keys>
<Key Type="Unique Tax Reference" Length="10">
</Key>
</Keys

Caveats

XMLStruct undefines all the instance methods that may clash with XML tags except for inspect. So if you have a file with an <inspect> XML tag then the class won’t work without modification.

If you need to add methods to this class or any subclass, then use the ! and ? suffixes. Otherwise the method will clash with the XML tag of the same name. (Fortunately ! and ? are not valid XML tag characters).

There is no ‘delete’ or ‘reorder’ methods within this class. The class is designed to take flat data and impose an XML structure on it statically. So the original purpose doesn’t require anything other than assign and output facilities. Still I hope you find it useful.

Instance Method Summary collapse

Constructor Details

#initialize(tag = "") ⇒ XmlStruct

Creates a new XmlStruct document. The optional tag can be used to set the initial tag for the document. However normally it is omitted.

xml = XmlStruct.new
xml.FirstTag
xml.to_s

and

xml = XmlStruct.new("FirstTag")
xml.to_s

both give the same output, viz:

<FirstTag>
</FirstTag>


188
189
190
191
192
193
194
# File 'lib/xml_struct.rb', line 188

def initialize (tag = "")
  @my_tag = tag
  @content = []
  @attributes = []
  @children = {}
  @create_order = []
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_id, *args) ⇒ Object

The method_missing method implements the Proxy pattern for this class. It automatically creates any attribute if it is missing and handles the default assignment by passing the assignment onto the [0] element.

Methods that cannot be XML tags (essentially those ending in ? and !) are raised as errors.



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/xml_struct.rb', line 244

def method_missing(method_id, *args) # :nodoc:
  method_name = method_id.id2name
  len = args.length
  if method_name[-1] == ?=
    unless len == 1
	raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
    end
    if frozen?
	raise TypeError, "can't modify frozen object", caller(1)
    end
    method_name.chop!
    unless method_name =~ /^[a-z:_][\w:\.-]*$/i
	raise NoMethodError, "undefined method '#{method_name}'", caller(1)
    end
    child = obtain_accessor!(method_name)
    child[0] = args[0]
  elsif method_name =~ /^[a-z:_][\w:\.-]*$/i
    if len == 0
	obtain_accessor!(method_name)
    else
	raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
    end
  else
    raise NoMethodError, "undefined method '#{method_name}'", caller(1)
  end
end

Instance Method Details

#[](index) ⇒ Object

Allows you to access the content values for a particular element. The default assign places content in [0], and you use that index to read the content back, e.g.

xml.Address.PostCode = "EC1A 5SW"
xml.Address.PostCode[0]
=> "EC1A 5SW"


206
207
208
# File 'lib/xml_struct.rb', line 206

def [](index)
  @content[index]
end

#[]=(index, rval) ⇒ Object

Assigning to [0] is the same as the default assign. The following are equivalent

xml.Address.PostCode = "EC1A 5SW"
xml.Address.PostCode[0] = "EC1A 5SW"

Assigning to indexes greater than zero creates an additional element with the same tag but different content

xml.Address.Line[0] = "1 Some Street"
xml.Address.Line[1] = "Somewhere"

However the default assign with an array is easier to use.

xml.Address.Line = [ "1 Some Street", "Somewhere" ]


226
227
228
229
230
231
232
233
234
235
# File 'lib/xml_struct.rb', line 226

def []=(index, rval)
  case
    when rval.kind_of?(Hash)
	@attributes[index] = rval
    when rval.kind_of?(Array)
	rval.each_index { |i| @content[i] = rval[i] }
    else
	@content[index] = rval
  end
end

#to_sObject

Converts the XMLStruct into XML format. Returns an empty string if the XMLStruct is empty.



275
276
277
278
279
280
# File 'lib/xml_struct.rb', line 275

def to_s
  %{#{open_my_tags!}#{
    @create_order.collect do |tag_id|
    @children[tag_id].to_s
    end.join}#{end_my_tags!}}
end