Swift: Deconstruct SPF: Strings and SubStrings

Learning Swift: Passing substrings to functions

I have been working on my SPF project in Swift of late. As part of this I have started to deal with substrings. “A slice of a string” according to Apple Documentation. In dealing with this, I have found myself quite often converting substrings back to normal strings. Today we will look at how we can pass substrings directly by making our functions generic.

Reference

According to the documentation listed above. There are two options when dealing with substrings.

  1. Create a new string by calling String() on the substring.
  2. Create a function which is generic over the StringProtocol.

Initially I was taking option 1. As I am new to Swift. But It just seemed a bit excessive to keep creating a new String from a substring.

How am creating all these substrings

I am doing a lot of split() calls as I am parsing a given string and breaking it down into its parts.

The documentation for split. Note there are several flavours of split.

func split(separator: Character, maxSplits: Int = Int.max,
           omittingEmptySubsequences: Bool = true) -> [Substring]

Original Code Example

Here is some of the original parse() function code.

mutating func parse() {
    let splitString = self.source.split(separator: " ")
    for subString in splitString {
        if (subString.starts(with: "v=") || subString.starts(with: "spf2")) {
            self.version = String(subString)
        }
        --snip--
        else if (subString.range(of: "redirect=") != nil) {
            let qmSplit = subString.split(separator: "=")
            self.redirect = Mechanism(k: MechanismKind.Redirect,
                                      q: identifyQualifier(prefixStr: String(qmSplit[0])),
                                      m: String(qmSplit[1]))
            self.is_redirect = true
        }
        else if (subString.range(of: "ip4:") != nil) {
            self.ip4 = processIp(ipStr: String(subString), kind: MechanismKind.Ip4)
        }
        else if (subString.range(of: "ip6:") != nil) {
                self.ip6 = genericProcessIp(subString: subString, kind: MechanismKind.Ip6)
        }
        --snip---
    }
}

private func processIp(ipStr: String, kind: MechanismKind) -> Mechanism {
    let qmSplit = ipStr.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true)
    let q = identifyQualifier(prefixStr: String(qmSplit[0]))
    return Mechanism(k: kind, q: q, m: String(qmSplit[1]))
}

Take note that the ip6: block is using a genericProcessIp() function.
This should become clear later in the article.

Let’s look at the code block starting with else if (subString.range(of: "ip4:") != nil).

range()

This function is being used to check if the strings contains the given string. From what I have seen. There is no contains() function. I could well be wrong.

Within this block I have a function processIp(). Its job is to take the given string and create a Mechanism struct of MechanismKind.Ip4.

processIp()

private func processIp(ipStr: String, kind: MechanismKind) -> Mechanism {
    let qmSplit = ipStr.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true)
    let q = identifyQualifier(prefixStr: String(qmSplit[0]))
    return Mechanism(k: kind, q: q, m: String(qmSplit[1]))
}

It takes two arguments, a String and a MechanismKind. But if you look above in the block for ip4 I am passing in String(subString). So I am first needing to make my substring into a new String and pass that to processIp()

Within processIp(), I again split the string. And once more I am calling String() to make a new string before passing it to identifyQualifier().
This all seems like a lot of converting / splitting.

Making processIp Generic

Let’s see the two together to make things easier to compare.

private func processIp(ipStr: String, kind: MechanismKind) -> Mechanism {
    let qmSplit = ipStr.split(separator: ":",
                              maxSplits: 1,
                              omittingEmptySubsequences: true)
    let q = identifyQualifier(prefixStr: String(qmSplit[0]))
    return Mechanism(k: kind, q: q, m: String(qmSplit[1]))
}

private func genericProcessIp<S: StringProtocol>(subString s: S, kind: MechanismKind) -> Mechanism {
    let qm = s.split(separator: ":",
                     maxSplits: 1,
                     omittingEmptySubsequences: true)
    let q = genericIdentifyQualifier(prefix: qm[0])
    return Mechanism(k: kind, q: q, m: String(qm[1]))
    
}

The only real difference is the function declaration.
<S: StringProtocol>(subString s: S, kind: MechanismKind)
This function conforms to or supports the StringProtocol
In the documentation it is declared without a label.
<S: StringProtocol>(_ s: S, ......)
This function conforms to or supports the StringProtocol

You might also notice that identifyQualifier() has also been made generic.
There is the code for these two version of the function.

private func identifyQualifier(prefixStr: String)  -> Qualifier {
    let char = String(prefixStr.prefix(1))
    let c = Character(char)
    if !c.isLetter {
        if char == "+" {
            return Qualifier.Pass
        } else if char == "-" {
            return Qualifier.Fail
        } else if char == "~" {
            return Qualifier.SoftFail
        } else if char == "?" {
            return Qualifier.Neutral
        }
    }
    return Qualifier.None
}

private func genericIdentifyQualifier<S: StringProtocol>(prefix s: S)  -> Qualifier {
    let char = String(s.prefix(1))
    let c = Character(char)
    if !c.isLetter {
        switch char {
        case "+":
            return Qualifier.Pass
        case "-":
            return Qualifier.Fail
        case "~":
            return Qualifier.SoftFail
        case "?":
            return Qualifier.Neutral
        default:
            return Qualifier.None
        }
    }
    return Qualifier.None
}

prefix()

This function also returns substring Documentation.

Closing

I suspect I can refactor some of the split() calls on substring out of the code, by finding the index(of: ) and then the end of the substring.

I am not sure this is overly useful for others, But it was an interesting find for me. I hope it does help others.


See also