CoreOffice / XMLCoder

Easy XML parsing using Codable protocols in Swift
https://coreoffice.github.io/XMLCoder/
MIT License
797 stars 109 forks source link

SOAP Usability #203

Open mickeyl opened 4 years ago

mickeyl commented 4 years ago

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?

itcohorts commented 3 years ago

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

mickeyl commented 3 years ago

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?

itcohorts commented 3 years ago

@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.

mickeyl commented 3 years ago

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

MaxDesiatov commented 3 years ago

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?

mickeyl commented 3 years ago

@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)!)
MaxDesiatov commented 3 years ago

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?

mickeyl commented 3 years ago

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 commented 3 years ago

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.

MaxDesiatov commented 3 years ago

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
mickeyl commented 3 years ago

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.

MaxDesiatov commented 3 years ago

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