XMLCoder icon indicating copy to clipboard operation
XMLCoder copied to clipboard

SOAP Usability

Open mickeyl opened this issue 3 years ago • 12 comments

I wonder whether XMLCoder can be used / improved to handle SOAP documents.

SOAP documents make excessive use of XML namespaces, e.g. consider the following SOAP document example:

<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
    xmlns:tds="http://www.onvif.org/ver10/device/wsdl"
    xmlns:tt="http://www.onvif.org/ver10/schema">
    <s:Header>
        <Security s:mustUnderstand="1"
            xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
            <UsernameToken>
                <Username>operator</Username>
                <Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">yDsyAGk5uWOaELjvFGKkG3xJHCU=</Password>
                <Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">K7y/yKFCjFbbCHHwMR6cBw==</Nonce>
                <Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2018-12-18T19:01:43.687Z</Created>
            </UsernameToken>
        </Security>
    </s:Header>
    <s:Body>
        <tds:GetCapabilities>
            <tds:Category>All</tds:Category>
        </tds:GetCapabilities>
    </s:Body>
</s:Envelope>

This example is used for ONVIF (which is a standard in the field of IP-camera surveillance devices) to gather the capabilities of a certain device. It is sent from the gathering device (client) to the camera device (server).

I think right now we can't produce such documents with XMLCoder, in particular because of the lack of namespace support for the encoder. Would something like a protocol Namespaced be feasible?

protocol Namespaced {
   var namespaces: [String: String]
}

Every node that is using namespaces could then comply to this protocol and upon generating the actual XML, the top node (Envelope in this case) could then use something like a pre-encoding hook that recursively travels through all the nodes, collects the namespaces, and inserts them as attributes into the Envelope.

Or am I missing something and we can already achieve that elegantly with XMLCoder?

mickeyl avatar Oct 02 '20 09:10 mickeyl

@mickeyl I have a similar use case. Did you ever figure this out?

itcohorts avatar Feb 21 '21 22:02 itcohorts

Unfortunately not – at the time I was in a hurry and so I couldn't dive deep enough in to XMLCoder to make it happen. Thus, I resorted to a bunch of my old SOAP-classes in Objective-C. Nowadays, that project is converting their SOAP-APIs into JSON, so I have no more immediate need.

I will have to come back to this sooner or later when I implement ONVIF for Swift, so I still have a need though. Perhaps we can tackle this together? What do you think about my namespace proposal?

mickeyl avatar Feb 22 '21 08:02 mickeyl

@mickeyl Sorry for the delay in response! I liked the namespace proposal. I got distracted by some other pressing work but I'm circling back to this project now. I've done a bit of Swift coding, but I still consider myself a noob with Swift. I'd be happy to work on this with you but would definitely need guidance.

itcohorts avatar Mar 04 '21 22:03 itcohorts

@MaxDesiatov Do you have any opinion on the namespace proposal?

mickeyl avatar May 31 '21 20:05 mickeyl

Sorry for the delayed reply. I'm not sure I'm following, how would a conformance to the Namespaced protocol look like for it to work with the XML example you provided? What are keys and values in the namespaces dictionary?

MaxDesiatov avatar Jun 01 '21 14:06 MaxDesiatov

@MaxDesiatov Please see the following (not working) demo which hopefully explains how I would imagine the look and feel at the call site:

import XMLCoder

/**
 Goal is to produce the following XML document:

 <?xml version="1.0" encoding="UTF-8"?>
 <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
     <s:Header>
         <Security s:mustUnderstand="1" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
             <UsernameToken>
                 <Username>operator</Username>
                 <Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">yDsyAGk5uWOaELjvFGKkG3xJHCU=</Password>
                 <Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">K7y/yKFCjFbbCHHwMR6cBw==</Nonce>
                 <Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2018-12-18T19:01:43.687Z</Created>
             </UsernameToken>
         </Security>
     </s:Header>
     <s:Body>
        <tds:GetCapabilities>
            <tds:Category>All</tds:Category>
        </tds:GetCapabilities>
     </s:Body>
 </s:Envelope>

And here is how I would create this:
*/


/**
 Let's introduce a namespace protocol that allows specifying which namespace(s) a node complies to.
 Namespaces are injected into the root node by the means of the typical xmlns:<prefix> = <localpart>
 The prefix of the first given namespace is injected into the node name.
 */
protocol Namespaced {

    typealias Prefix = String
    typealias LocalPart = String
    typealias Namespaces = [Prefix: LocalPart]

    var namespaces: Namespaces { get }
}

/**
 We might also (or alternatively) introduce a property wrapper instead that handles this.
 */


struct Envelope: Codable, Namespaced {

    var namespaces: Namespaces { ["s": "http://www.w3.org/2003/05/soap-envelope"] }

    let Header: Header
    let Body: Body

}

struct Header: Codable, Namespaced {

    var namespaces: Namespaces { ["s": "http://www.w3.org/2003/05/soap-envelope"] }

    let Security: Security
}

struct Security: Codable {

    let UsernameToken: UsernameToken
}

struct UsernameToken: Codable {

    let Username: String
    let Password: String
    let Nonce: String
    let Created: String
}

struct Body: Codable, Namespaced {

    var namespaces: Namespaces { ["s": "http://www.w3.org/2003/05/soap-envelope"] }

    let GetCapabilities: GetCapabilities
}

struct GetCapabilities: Codable, Namespaced {

    var namespaces: Namespaces { [
        "tds": "http://www.onvif.org/ver10/device/wsdl",
        "tt": "http://www.onvif.org/ver10/schema",
    ] }

    @Namespaced(["tds": "http://www.onvif.org/ver10/device/wsdl"])
    let Category: String

}

let getCapabilities = GetCapabilities(Category: "All")
let body = Body(GetCapabilities: getCapabilities)
let usernameToken = UsernameToken(Username: "operator", Password: "yDsyAGk5uWOaELjvFGKkG3xJHCU", Nonce: "K7y/yKFCjFbbCHHwMR6cBw==", Created: "2018-12-18T19:01:43.687Z")
let security = Security(UsernameToken: usernameToken)
let header = Header(Security: security)
let envelope = Envelope(Header: header, Body: body)

/**
 Here comes the magic… The XML encoder now
  * traverses through the all the nodes and scans for namespace compliance,
  * rewrites the node names (if necessary),
  * inserts them into a set, and
  * dumps them as xmlns attributes into the root node.
 */

let data = try! XMLEncoder().encode(envelope)
print(String(data: data, encoding: .utf8)!)


mickeyl avatar Jun 03 '21 10:06 mickeyl

I'm a bit worried about the approach with the Namespaced protocol, it doesn't allow customizing namespaces for a specific property or to avoid using namespaces for some properties altogether. @Namespaced property wrapper looks to be much more localized, although I'm unsure if passing a dictionary is better than passing just a simple pair of strings? Can a property ever have more than a single namespace?

MaxDesiatov avatar Jun 13 '21 07:06 MaxDesiatov

Well, in that case we could make it an (optional?) non-static property in the protocol and have it set as part of the element construction.

The dictionary conveys more semantics, but yes, a pair of strings would also do. In fact, strictly spoken, only the local parts are a necessity. If you wanted, you could compute the prefixes on-demand (although well-known prefix parts are kind of recommended, since they give meaning).

Unfortunately right now I don't have the ONVIF infrastructure handy to do some tests, but I think I remember that although one property only has a single namespace, some namespaces need to be present in the envelope else the devices don't accept the requests – we could solve this in another way though, which would lead to the namespaced protocol to just carry one string (or a tuple, if you want to allow setting the prefix) instead of a dict.

And it's not just ONVIF btw., all kind of SOAP protocols are so convoluted. There are dozens of SOAP protocols (CALDav, CardDAV, …) still out there, for which there is no canonical way to access them from Swift yet – due to the namespace issue.

mickeyl avatar Jun 13 '21 10:06 mickeyl

Let me add: Of course, I can implement all that on top of XMLCoder, it just feels more natural — to me at least — to have builtin support for the creation of namespaced documents.

mickeyl avatar Jun 13 '21 12:06 mickeyl

Well, in that case we could make it an (optional?) non-static property in the protocol and have it set as part of the element construction.

The dictionary conveys more semantics, but yes, a pair of strings would also do.

I hope we can avoid introducing more than a single way to achieve the same thing. What would be a benefit of providing a protocol and a property wrapper at the same time? Of these two, the property wrapper so far seems to be the most flexible option.

Additionally, in the case of a dictionary passed in the parameter, what would happen if two prefixes are supplied?

@Namespaced(prefix: "a", uri: "http://some.tld/path")
let property: String

seems to be quite unambiguous, while with dictionaries it's unclear to me what would happen in this case?

@Namespaced(["a": "http://some.tld/path", "b": "http://some.other.tld/path"])
let property: String

MaxDesiatov avatar Jun 14 '21 21:06 MaxDesiatov

Well, in that case we could make it an (optional?) non-static property in the protocol and have it set as part of the element construction.

The dictionary conveys more semantics, but yes, a pair of strings would also do.

I hope we can avoid introducing more than a single way to achieve the same thing. What would be a benefit of providing a protocol and a property wrapper at the same time? Of these two, the property wrapper so far seems to be the most flexible option.

The property wrapper can't be assigned to a top-level object, but that's all. Since the envelope needs special treatment anyways, we could probably do without the protocol.

@Namespaced(prefix: "a", uri: "http://some.tld/path")
let property: String

Looks good.

mickeyl avatar Jun 18 '21 08:06 mickeyl

Awesome, thanks for the reviewing the approach! One could start with a property wrapper, and then we could consider additional approaches if needed. 👍

MaxDesiatov avatar Jun 18 '21 12:06 MaxDesiatov