Tester la résolution des chemins JSON Pointer iOS sur un Mac cloud
Le serveur situe une erreur de validation à l’emplacement /items/0/name, mais le client iOS met en évidence le mauvais champ. Le problème ne vient pas forcément de la liaison avec l’interface : bien souvent, le résolveur de chemins supprime les clés vides, interprète 01 comme un indice de tableau ou traite de la même manière un champ absent et une valeur null valide. Sur un Mac cloud, un script Swift autonome permet d’abord de fixer ces règles de résolution, puis de transférer les cas de test dans la cible de tests de l’application, sans lancer tout le parcours d’interface.
1. Définir d’abord le contrat de lecture
L’implémentation présentée ici prend en charge la forme chaîne JSON définie par la RFC 6901 : une chaîne vide désigne le document entier, et tout autre chemin doit commencer par /. Elle n’accepte pas la forme fragment d’URI commençant par #, ne modifie pas les données et ne constitue pas un langage de requête JSONPath.
Le découpage doit conserver les segments vides. Le chemin / accède à la clé vide ; /a/ accède à la clé vide de l’objet a. Il ne faut donc pas supprimer la barre oblique finale. Les noms des membres d’un objet sont toujours comparés comme des chaînes ; un indice numérique n’est interprété comme tel que si le nœud courant est effectivement un tableau.
Un chemin qui existe et dont la valeur est null correspond à une lecture réussie. Un chemin inexistant est un échec. N’utilisez pas une même valeur optionnelle pour représenter ces deux résultats.
Le message affiché peut se résumer à « Impossible de localiser le champ », mais les tests doivent distinguer les erreurs de syntaxe, les membres absents, les erreurs d’indice et les erreurs de type de nœud. Ces distinctions facilitent le diagnostic des évolutions du contrat d’API.
2. Implémenter un décodage strict des échappements et le parcours des nœuds
Enregistrez le code suivant dans PointerCheck.swift. Il dépend uniquement de Foundation et s’exécute avec la chaîne d’outils actuellement sélectionnée, sans installer de paquet supplémentaire. Le décodage des échappements accepte uniquement ~0 et ~1, qui deviennent respectivement ~ et /.
import Foundation
enum PointerError: Error {
case syntax
case missing
case index
case scalar
}
func decodeToken(_ token: Substring) throws -> String {
var result = ""
var cursor = token.startIndex
while cursor < token.endIndex {
let character = token[cursor]
cursor = token.index(after: cursor)
if character != "~" {
result.append(character)
continue
}
guard cursor < token.endIndex else {
throw PointerError.syntax
}
switch token[cursor] {
case "0": result.append("~")
case "1": result.append("/")
default: throw PointerError.syntax
}
cursor = token.index(after: cursor)
}
return result
}
func resolve(_ pointer: String, in document: Any) throws -> Any {
if pointer.isEmpty {
return document
}
guard pointer.first == "/" else {
throw PointerError.syntax
}
let tokens = try pointer.dropFirst()
.split(separator: "/", omittingEmptySubsequences: false)
.map { try decodeToken($0) }
var current = document
for token in tokens {
if let object = current as? [String: Any] {
guard let value = object[token] else {
throw PointerError.missing
}
current = value
} else if let array = current as? [Any] {
let digits = token.utf8
guard !digits.isEmpty,
digits.allSatisfy({ $0 >= 48 && $0 <= 57 }),
token == "0" || !token.hasPrefix("0"),
let index = Int(token),
index < array.count else {
throw PointerError.index
}
current = array[index]
} else {
throw PointerError.scalar
}
}
return current
}
Il ne faut pas remplacer d’abord ~0, puis ~1 : la séquence d’origine ~01 doit devenir le texte littéral ~1, et non une barre oblique. L’analyse de chaque segment caractère par caractère permet aussi de rejeter un tilde isolé ou ~2, au lieu de « corriger » silencieusement un chemin invalide.
La branche consacrée aux tableaux vérifie que les chiffres sont des caractères ASCII et rejette les signes, les espaces et les autres caractères numériques. Un échec de conversion en Int est également une erreur d’indice ; il ne faut pas forcer le déballage de l’optionnel. En revanche, "01" est un nom de clé valide dans un objet et ne doit pas être soumis aux règles des tableaux.
3. Couvrir les cas limites ambigus avec un petit jeu de données
Ajoutez le code de validation suivant à la suite. Le jeu de données contient une clé vide, des clés avec des caractères spéciaux, un tableau, des valeurs scalaires et null. Chaque assertion ne vérifie qu’une seule propriété.
let source = #"{"a/b":{"~key":[null,7]},"":9,"~1":11,"01":12}"#
let document = try JSONSerialization.jsonObject(
with: Data(source.utf8),
options: [.fragmentsAllowed]
)
let nullValue = try resolve("/a~1b/~0key/0", in: document)
precondition(nullValue is NSNull)
let emptyKey = try resolve("/", in: document)
precondition((emptyKey as? NSNumber)?.intValue == 9)
let escapedKey = try resolve("/~01", in: document)
precondition((escapedKey as? NSNumber)?.intValue == 11)
let numericKey = try resolve("/01", in: document)
precondition((numericKey as? NSNumber)?.intValue == 12)
let root = try resolve("", in: document)
precondition(root is [String: Any])
let rejected = [
"a", "#/a", "/bad~2", "/bad~",
"/absent", "/a~1b/~0key/01",
"/a~1b/~0key/-", "/a~1b/~0key/2",
"/a~1b/~0key/999999999999999999999999999999",
"/a~1b/~0key/1/child"
]
for pointer in rejected {
var failed = false
do {
_ = try resolve(pointer, in: document)
} catch is PointerError {
failed = true
}
precondition(failed, "Unexpected resolution success")
}
Complétez ces tests par des cas dont la racine est un tableau ou une valeur scalaire. Une racine scalaire associée à un chemin vide doit être lue avec succès ; toute tentative de poursuivre le parcours doit échouer. La comparaison des chaînes doit rester telle quelle, sans ignorer automatiquement la casse ni appliquer de normalisation Unicode, sous peine de lire un autre membre.
4. Intégrer la vérification au build cloud, au-delà du simple exemple
Dans une session SSH, exécutez :
set -eu
xcrun --find swift
xcrun swift PointerCheck.swift
Le script se termine sans rien afficher si tout réussit et renvoie un code de sortie non nul si une assertion échoue. N’exécutez pas ce script de validation dans un mode de compilation optimisé susceptible de supprimer les vérifications. Pour l’intégration dans l’application, placez le résolveur dans un module partagé, convertissez les assertions en assertions de la cible de tests et exécutez-les via le point d’entrée de test déjà utilisé par le projet.
Le script autonome valide les règles de lecture sur l’arbre de données Foundation ; il ne remplace pas les tests d’intégration de la cible iOS. Si l’application parcourt directement des modèles de décodage personnalisés, préparez pour eux les mêmes cas de test, sans supposer que les deux représentations se comportent naturellement de la même façon.
Lorsque l’entrée provient d’une API externe, limitez aussi la taille du document, la longueur du chemin et le nombre de segments avant l’appel au résolveur. Définissez les seuils selon la taille des données métier et testez aussi bien les valeurs limites que celles qui les dépassent. Les chemins courts de l’exemple ne constituent pas une limite de consommation des ressources.
5. En cas d’échec, vérifier l’entrée plutôt que deviner
Si la résolution échoue, consignez d’abord la catégorie d’erreur, la position du segment et le type du nœud courant, sans enregistrer le document métier complet. Le chemin lui-même peut contenir des données utilisateur : il ne doit être joint aux artefacts de build qu’après suppression ou masquage des données sensibles.
Vérifiez ensuite si le serveur fournit un JSON Pointer ou une autre syntaxe de chemin, puis si ce chemin a subi un décodage d’URL supplémentaire. Dans la forme chaîne prise en charge ici, le signe pourcentage est un caractère ordinaire ; il ne faut pas appliquer de décodage supplémentaire de sa propre initiative.
Enfin, ajoutez chaque entrée ayant provoqué un échec corrigé au jeu de tests permanent. Le critère de validation doit être une lecture exacte pour les chemins valides et un échec explicite pour les chemins invalides, et non une tentative de « trouver un champ qui ressemble ». Les messages d’erreur, la lecture de configuration et le mappage des champs peuvent ainsi partager un contrat de chemin stable.
Questions fréquentes
Le chemin vide équivaut-il à une barre oblique seule ?
Non. Le chemin vide renvoie le document entier. Une barre oblique seule désigne le membre dont la clé est vide dans l'objet racine ; son absence doit provoquer un échec.
Peut-on lire un tableau avec l'indice 01 ou un tiret ?
Ce résolveur les refuse. Il accepte 0 ou un entier décimal sans zéro initial. Un tiret ne désigne aucun élément lisible ; les dépassements de capacité et les indices hors limites échouent aussi.
Validez votre pipeline sur un Mac dans le cloud
ArmRDP met à votre disposition des Mac mini physiques dédiés, et non des machines virtuelles. Louez à la journée : vérifiez d’abord votre chaîne d’outils et les résultats de compilation, puis choisissez la durée adaptée à votre équipe.
Louer un Mac dans le cloud