Search code examples
powershellhashtabledata-conversionxml-rpc

How to convert a hashtable to XML-RPC format in PowerShell?


I have a hashtable with incoming parameters, for example (the number of parameters and the parameters themselves may vary):

$inParams = @{
  mode = "getevents"
  username = "vbgtut"
  password = "password"
  selecttype = "one"
  itemid = 148
  ver = 1
}

I need to convert the hashtable to the XML-RPC format described in the specification.

That is, for a hashtable, I have to use the struct data type described in the specification. The struct structure contains members corresponding to the key-value pairs of the hashtable.

The values in the members can be one of six scalar types (int, boolean, string, double, dateTime.iso8601 and base64) or one of two composite ones (struct and array). But for now, two scalar types are enough for me: string and int.

I wrote the following function for this:

function toXML($hashT) {
  $xml =  '<?xml version="1.0"?>'
  $xml += "<methodCall>"
  $xml += "<methodName>LJ.XMLRPC." + $hashT["mode"] + "</methodName>"
  $xml += "<params><param><value><struct>"
  foreach ($key in $hashT.Keys) {
    if ($key -ne "mode") {
      $xml += "<member>"
      $xml += "<name>" + $key + "</name>"
      $type = ($hashT[$key]).GetType().FullName
      if ($type -eq "System.Int32") {$type = "int"} else {$type = "string"}
      $xml += "<value><$type>" + $hashT[$key] + "</$type></value>"
      $xml += "</member>"
    }
  }
  $xml += "</struct></value></param></params>"
  $xml += "</methodCall>"
  return $xml
}

This function is doing its job successfully (I'm using PowerShell 7 on Windows 10):

PS C:\> $body = toXML($inParams)
PS C:\> $body
<?xml version="1.0"?><methodCall><methodName>LJ.XMLRPC.getevents</methodName><params><param><value><struct><member><name>ver</name><value><int>1</int></value></member><member><name>username</name><value><string>vbgtut</string></value></member><member><name>itemid</name><value><int>148</int></value></member><member><name>selecttype</name><value><string>one</string></value></member><member><name>password</name><value><string>password</string></value></member></struct></value></param></params></methodCall>

I do not need to receive XML in a readable form, but for this question I will bring XML in a readable form for an example:

PS C:\> [System.Xml.Linq.XDocument]::Parse($body).ToString()
<methodCall>
  <methodName>LJ.XMLRPC.getevents</methodName>
  <params>
    <param>
      <value>
        <struct>
          <member>
            <name>ver</name>
            <value>
              <int>1</int>
            </value>
          </member>
          <member>
            <name>username</name>
            <value>
              <string>vbgtut</string>
            </value>
          </member>
          <member>
            <name>itemid</name>
            <value>
              <int>148</int>
            </value>
          </member>
          <member>
            <name>selecttype</name>
            <value>
              <string>one</string>
            </value>
          </member>
          <member>
            <name>password</name>
            <value>
              <string>password</string>
            </value>
          </member>
        </struct>
      </value>
    </param>
  </params>
</methodCall>

My question is as follows: Is it possible in PowerShell to convert a hashtable to XML-RPC format in a more optimal way than in my ToXML function?

I tried using the cmdlet ConvertTo-Xml. It works well, but converts the hashtable to regular XML, not to XML-RPC format. Maybe this cmdlet can be configured somehow so that it works in XML-RPC format?

I also heard about the library xml-rpc.net, but her website is unavailable. It looks like this library is no longer being developed. Because of this, I am afraid to use it. Is it worth trying to use it?


Solution

  • As discussed, the XML-RPC protocol seems to be dying.

    On the plus side, given that the protocol isn't evolving anymore, you may be able to use even older libraries, as long as they're still technically compatible with your runtime environment.

    For instance, the third-party XmlRpc module, last updated in 2017, may still work for you (its data-type support doesn't include base64):

    # Install the module, if necessary
    if (-not (Get-Module -ListAvailable XmlRpc)) {
      Write-Verbose -Verbose "Installing module 'XmlRpc' in the current user's scope..."
      Install-Module -ErrorAction Stop -Scope CurrentUser XmlRpc
    }
    
    # Sample call
    $result = 
      ConvertTo-XmlRpcMethodCall -Name LJ.XMLRPC.getevents @{
        username = "vbgtut"
        password = "password"
        selecttype = "one"
        itemid = 148
        ver = 1
        # ... sample values to demonstrate full data-type support and XML escaping
        array = 'one & two', 3
        double = 3.14
        boolean = $true
        date = Get-Date
      } 
    
    # Post-process the results to make the data-type element
    # names conform to the spec.
    # ('Double' -> 'double', ..., and 'Int32' -> 'int')
    [regex]::Replace(
      $result,
      '(</?)([A-Z]\w+)',
      { 
        param($m) 
        $m.Groups[1].Value + ($m.Groups[2].Value.ToLower() -replace '32$')
      }
    )
    

    This produces the following output, but note:

    • As you've discovered, the data-type XML element names used by ConvertTo-XmlRpcMethodCall (the underlying ConvertTo-XmlRpcType) aren't spec-compliant, in that they start with a capital letter when they should all be all-lowercase (e.g. <String> instead of <string>), and <Int32> should be <int>. This problem is corrected in a post-processing step via [regex]::Replace(), which isn't ideal, but should work well enough in practice.

      • The problem has been reported in GitHub issue #4, though it's unclear if the module is still actively maintained.
    • For simplicity, I've removed the mode = "getevents" entry from the hashtable and have included its value as a literal part of the method name passed to -Name.

    • The real output isn't pretty-printed; it was done here for readability.

    • Because the command operates on - inherently unordered - hashtables, the entry-definition order isn't guaranteed - though that shouldn't matter. (Unfortunately, the module isn't designed to accept ordered hashtables ([ordered] @{ ... })).

    • Note how the & in 'one & two' was properly escaped as &amp;

    <?xml version="1.0"?>
    <methodCall>
      <methodName>LJ.XMLRPC.getevents</methodName>
      <params>
        <param>
          <value>
            <struct>
              <member>
                <name>date</name>
                <value>
                  <dateTime.iso8601>20230212T15:33:52</dateTime.iso8601>
                </value>
              </member>
              <member>
                <name>username</name>
                <value>
                  <string>vbgtut</string>
                </value>
              </member>
              <member>
                <name>selecttype</name>
                <value>
                  <string>one</string>
                </value>
              </member>
              <member>
                <name>double</name>
                <value>
                  <double>3.14</double>
                </value>
              </member>
              <member>
                <name>boolean</name>
                <value>
                  <boolean>True</boolean>
                </value>
              </member>
              <member>
                <name>array</name>
                <value>
                  <array>
                    <data>
                      <value>
                        <string>one &amp; two</string>
                      </value>
                      <value>
                        <int>3</int>
                      </value>
                    </data>
                  </array>
                </value>
              </member>
              <member>
                <name>itemid</name>
                <value>
                  <int>148</int>
                </value>
              </member>
              <member>
                <name>password</name>
                <value>
                  <string>password</string>
                </value>
              </member>
              <member>
                <name>ver</name>
                <value>
                  <int>1</int>
                </value>
              </member>
            </struct>
          </value>
        </param>
      </params>
    </methodCall>