Los de producto: Pues si un usuario hace click en el call to action del email, no quiero que se abra la web, se tiene que abrir la app. En X pantalla con Y datos.

Así empieza la pequeña historia de hoy. O algo parecido, porque en Parkfy no hay gente de producto. Somos una startup de esas que aun son startups de verdad. No es famosa, no tiene millones y aún carecemos de cierto personal, cómo los de producto 😂. Pero intentamos hacer lo mejor que podemos con lo que tenemos, y mola.

Te pones manos a la obra. Lees un tutorial de que fichero había que subir al servidor (por que aún que lo hayas hecho una vez, no te acuerdas y lo sabes) para abrir una URL de esas. Subes el fichero, compruebas que funciona. La app se abre. Bien. Mapea la URL.

En iOS, en caso de que la URL nos traiga los datos en el query, no hay mucho problema. URLComponents tiene algo así como un queryItems y podemos acceder via clave -> valor. Pero por todos es sabido que esas horribles cosas no molan. Un usuario prefiere no verlas. Prefiere ver /helado/34/cucurucho/12, que lo entiende mejor no?. En realidad el usuario no, pero google sí.

De todos modos la URL de un mail, dudo mucho que cuente en el SEO. El botón tendrá estilos por lo que la URL no se ve. ¿No te olvidas de algo?. Los esmeses (SMSs). Un SMS también tiene que ser bonito no?. Cómo lo son los de movistar 😂😂😂

Bueno, que hay que mapear los paths de la URL.

Dando vueltas al asunto. ¿Como coño mapeo un path?, entonces pienso. Los servidores tienen que hacerlo bién, al fin y al cabo, dada cierta URL te llevan a cierto controlador con ciertos datos. Vease /helado/:helado_id/cucurucho/:cucurucho_id. ¿Como lo hacen los servidores?, pues no tengo ni idea. La verdad es que no lo miré, pero me supuse que algo de expresiones regulares tendria sentido.

Hacemos el match por expresiones regulares, dado esto, cubre cualquier URL y si no, pues tiramos un error.

Lo primero que busqué estaba relacionado con expresiones regulares en un switch. Articulazo el que encontré, en serio. Una cosa completita para de un modo sencillo, matchear Strings.

Mi idea era, meter expresiones en un switch para diferenciar la URL a la que accedemos. Sencillo, y si no, hay un default tirando error o devolviendo nil whatever u want.

Una vez entendemos el articulo, vemos que para machear con el Regex hacemos un overload del operador ~=. Como funciona el Regex es simple. Lo creamos con una expresión regular, él nos gestiona todo el rollo de compararlo y nos devuelve si encaja o no. O los grupos que han encajado.

Resulta que volviendo a leer como funciona una expresión regular, hay una cosa llamada grupos. En una expresión regular defines un grupo con ( paréntesis ). Quiero un grupo, el de la URL.

Pruebo que funciona aquí. Esa web no solo te dice si pasa o no, te dice que haces. Porque en cuanto metes 4 cosas, vete tu a saber qué hace tu expresion regular. Para manejar esas mierdas tienes que ser ingeniero bioquímico especializado en la fotosítesis de las plantas para extraer energía del sol y crear paneles fotovoltaicos a partir de cactus (que no hay que regarlos mucho).

Defino el grupo de la URL así ^(\/helado\/[0-9]+\/*). A la derecha se te pinta un Full Match y veo una cosa que me pinta Group 1. Entonces pasa ese extraño fenomeno donde la electricidad encuentra un medio por el cual desplazarse a capricho y tu cabeza luce.

Hagamos un segundo grupo para sacar solo el id. Lo unico a añadir es esto: (). PARÉNTESIS. Sí, unos simples y llanos paréntesis haran que no sólo se me pinte la URL a la derecha, sino también el id que buscaré luego en la app. La expresión regular queda así ^(\/helado\/([0-9]+)\/*).

Si una web de 4 frikis es capaz de pintar un grupo dentro de una expresión regular, seguro que iOS puede darme los grupos que encajan en una expresión regular. Así saco los ids del path sin partir el string en substrings, contar posiciones y cosas de esas…

El problema.

Cuando todo pinta tan bién, algo pintará mal. Eso me ha pasado el 90% de las veces que programo. Tal y cómo explica el artículo anterior, tenemos esto:

switch path {
  case Regex(""):
    //Pues encaja aqui...
  default:
    //Tu verás, pero encajar no encaja...
}

¿Donde están mis grupos queridos?

Los switch en swift pueden hacer un bind de variables. No se como hacer un bind de una variable que no está hasta que se pasa el Regex. De hecho, aún no se como sacar esa variable. No he podido hacer el bind para sacarla como se hace con los enums. Queria algo así:

switch path {
  case Regex("", let groups):
    //Pues encaja aqui...
  default:
    //Tu verás, pero encajar no encaja...
}

Eso con los enums lo tienes gratis. No sé si se puede hacer, si sabeis como hacerlo, pues un mail o un tweet diciendome como se agradecería un monton (o un comentario el gist, que hay un link abajo). Despues de horas, en serio, horas tratando de averiguar eso, llego a la conclusión de que no se puede. En tal caso, tengo un problema.

El Regex se crea para hacer la comparación. No está asignado en ninguna variable. No puedo guardar en el Regex los grupos encontrados. Quiero evitar declarar los 8 Regex en una variable local para tener acceso a ellos. Además, en cuanto pasemos un string distinto, esos grupos se van. No es una buena solucíon.

Podemos volver a hacer la expresión regular y sacarlos. Otra mierda de solución. Si ya hemos pasado la expresion regular, eso ya está hecho. No quiero hacer lo mismo dos veces para sacar dos variables.

Lo único común en ese switch, lo que viaja por todas las comparaciones es el String. El String es RegexMatchable porque lo hemos hecho nosotros. Hemos extendido el String para que pueda utilizar el operador del switch con un Regex.

¿Y si en lugar de usar un String usamos una clase propia?

Creamos una clase que almacene un String, además tiene una variable opcional que son los grupos encontrados (cuando se encuentren). Cuando vayan pasando los cases del switch, tendremos esa clase propia a nuestra entera disposición y con los datos rellenados.

Y sí, funciona. Una vez tenemos los grupos, extraer los ids tampoco es que sea muy limpio. Es un array de rangos, que nos dá mas de un rango. Hay que comprobar si ese rango es todo numeros y en ese caso, ese es el rango que buscamos. Si esperamos 2 o 3, pues irán en posiciones seguidas.

Al final todo queda reducido a esto:

import Foundation

extension String {

    var isNumber: Bool {
        return !self.isEmpty && CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: self))
    }

}

struct Regex {

    let pattern: String
    let options: NSRegularExpression.Options!

    private var matcher: NSRegularExpression {
        return try! NSRegularExpression(pattern: self.pattern, options: self.options)

    }

    init(_ pattern: String, options: NSRegularExpression.Options = [.caseInsensitive]) {
        self.pattern = pattern
        self.options = options
    }

    //Devuelve el array de grupos
    func match(_ string: String, options: NSRegularExpression.MatchingOptions = []) -> [NSTextCheckingResult] {
        return self.matcher.matches(in: string, options: [], range: NSMakeRange(0, string.characters.count))
    }

}

protocol RegexMatchable {
    func match(regex: Regex) -> Bool
}

final class Matcher: RegexMatchable {

    let matching: String
    var groups: [NSTextCheckingResult]?

    var matchingGroups: [String] {
        var matches = [String]()
        if let groups = groups {
            let string = matching as NSString
            for group in groups {
                for index in 1..<group.numberOfRanges {
                    matches.append(string.substring(with: group.rangeAt(index)))
                }
            }
        }
        return matches
    }

    var intGroups: [Int] {
        return self.matchingGroups.flatMap { Int($0) }
    }

    init(_ matching: String) {
        self.matching = matching
    }

    func match(regex: Regex) -> Bool {
        let matches = regex.match(self.matching)
        if !matches.isEmpty {
            self.groups = matches
            return true
        }
        return false
    }

}

extension String: RegexMatchable {
    func match(regex: Regex) -> Bool {
        return !regex.match(self).isEmpty
    }
}

//Overload patter matching operator
func ~=<T>(pattern: Regex, matchable: T) -> Bool where T: RegexMatchable {
    return matchable.match(regex: pattern)
}


enum DeepLinking {
    case helado(id: Int)
}

func linking(_ path: String) -> DeepLinking? {

    let matching = Matcher(path)

    switch matching {
    case Regex("^(\\/helado\\/([0-9]+)\\/*)"):
        guard let id = matching.intGroups.first else { return nil }
        //Aquí tienes tu id, en tipo Int.
        return .helado(id: id)
    default:
        //No hay coincidencias
        return nil
    }
}

let link = linking("/helado/43")

Tampoco es tanto código, creo que queda más o menos limpio y fácil de entender cómo se sacan los datos de la URL, y además, cómo es con expresiones regulares, es muy versátil. Para cualquier idea o cualquier mejora, aquí hay un Gist donde puedes comentar cualquier cosa.