Introduction
La plupart des formats de données exigent de différencier plusieurs structures possibles au même endroit : une valeur peut être un objet, un tableau, une chaîne ou un nombre, et un fichier de configuration peut contenir des options dans un ordre quelconque. Le module nom::branch propose deux combinateurs pour traiter ces cas sans conditionnelles explicites : alt et permutation.
alt : sélectionner la première branche qui réussit
alt prend une liste de parseurs et renvoie le résultat du premier qui réussit. C’est l’outil naturel pour des alternativse exclusives.
Exemple de base
use nom::branch::alt;
use nom::bytes::complete::tag;
fn parse_keyword(i: &str) -> nom::IResult<&str, &str> {
alt((tag("fn"), tag("let"), tag("use"))).parse(i)
}
assert_eq!(parse_keyword("let x = 1"), Ok((" x = 1", "let")));
assert_eq!(parse_keyword("use std::io"), Ok((" std::io", "use")));
Différentes formes d’entrée
alt accepte un n-uplet, un tableau ou une tranche mutable :
// N-uplet
alt((tag("red"), tag("green"), tag("blue"))).parse(i);
// Tableau
const COLORS: [fn(&str) -> nom::IResult<&str, &str>; 3] =
[tag("red"), tag("green"), tag("blue")];
alt(COLORS).parse(i);
// Tranche mutable
let mut parsers = [tag("one"), tag("two"), tag("three")];
alt(&mut parsers[..]).parse(i);
permutation : toutes les branches, dans n’importe quel ordre
Contrairement à alt, permutation exige que tous les parseurs réusissent, mais ils peuvent apparaître dans n’importe quel ordre.
use nom::branch::permutation;
use nom::bytes::complete::tag;
fn parse_flags(i: &[u8]) -> nom::IResult<&[u8], (&[u8], &[u8], &[u8])> {
permutation((
tag(b"fast"),
tag(b"safe"),
tag(b"quiet"),
))
.parse(i)
}
assert_eq!(parse_flags(b"fastsafequiet"), Ok((&b""[..], (b"fast", b"safe", b"quiet"))));
assert_eq!(parse_flags(b"quietfastsafe"), Ok((&b""[..], (b"fast", b"safe", b"quiet"))));
Affiner les erreurs avec un type personnalisé
Par défaut, alt renvoie l’erreur de la dernière branche testée. Un type d’erreur personnalisé permet d’accumuler les échecs.
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::error::{ErrorKind, ParseError};
#[derive(Debug, PartialEq)]
struct BranchError(String);
impl<'a> ParseError<&'a str> for BranchError {
fn from_error_kind(input: &'a str, kind: ErrorKind) -> Self {
BranchError(format!("échec sur {:?} ({:?})", input, kind))
}
fn append(input: &'a str, kind: ErrorKind, other: Self) -> Self {
BranchError(format!("{} ; aussi {:?} ({:?})", other.0, input, kind))
}
}
fn parse_color(i: &str) -> nom::IResult<&str, &str, BranchError> {
alt((tag("rouge"), tag("vert"), tag("bleu"))).parse(i)
}
Exemples concrets
Valeurs JSON
use nom::branch::alt;
use nom::combinator::map;
fn json_value(i: &str) -> nom::IResult<&str, JsonValue> {
alt((
map(parse_object, JsonValue::Object),
map(parse_array, JsonValue::Array),
map(parse_string, JsonValue::String),
map(parse_number, JsonValue::Number),
map(parse_bool, JsonValue::Bool),
map(parse_null, |_| JsonValue::Null),
))
.parse(i)
}
Atome d’un S-expression
fn expr_atom(i: &str) -> nom::IResult<&str, Expr> {
alt((
map(parse_symbol, Expr::Symbol),
map(parse_string, Expr::String),
map(parse_number, Expr::Number),
))
.parse(i)
}
Options de configuration sans ordre fixe
use nom::branch::permutation;
use nom::bytes::complete::{tag, take_until};
use nom::character::complete::digit1;
use nom::combinator::map;
use nom::sequence::preceded;
fn parse_bind(i: &str) -> nom::IResult<&str, &str> {
preceded(tag("bind="), take_until(";")).parse(i)
}
fn parse_timeout(i: &str) -> nom::IResult<&str, u64> {
map(
preceded(tag("timeout="), digit1),
|n: &str| n.parse().unwrap(),
)
.parse(i)
}
fn parse_settings(i: &str) -> nom::IResult<&str, (&str, u64)> {
permutation((parse_bind, parse_timeout)).parse(i)
}
assert_eq!(
parse_settings("bind=127.0.0.1;timeout=30;"),
Ok((";", ("127.0.0.1", 30)))
);
assert_eq!(
parse_settings("timeout=30;bind=127.0.0.1;"),
Ok((";", ("127.0.0.1", 30)))
);
Stratégies de test
Il est utile de vérifier chaque branche isolément, mais aussi les interactions :
- tester le succès de chaque branche de
alt; - vérifier que
permutationaccepte tous les ordres possibles ; - contrôler le comportement face à une entrée partielle ;
- s’assurer que l’erreur finale contient l’information attendue.
#[test]
fn alt_returns_first_match() {
assert_eq!(parse_keyword("fn main()"), Ok((" main()", "fn")));
assert_eq!(parse_keyword("use std"), Ok((" std", "use")));
}
Bonnes pratiques
- Ordre des branches : placez en premier les cas les plus fréquents ou ceux qui échouent le plus vite.
- Regroupement : quand
altcontient trop de branches, décomposez-le en sous-combinateurs. - Erreurs : utilisez un type d’erreur personnalisé pour conserver le contexte de chaque échec.
- Copies : évitez de cloner l’entrée dans vos parseurs personnalisés ;
altrestitue l’entrée originale lorsqu’une branche échoue. - Grand nombre de branches : préférez un tableau à un très grand n-uplet.
Pièges courants
- Une branche de
altconsomme partiellement l’entrée puis échoue : l’erreur est renvoyée immédiatement. Utilisezpeekoucutselon que l’erreur est récupérable ou non. permutationrenvoie un tuple ordonné selon la déclaration, pas selon l’ordre d’apparition dans l’entrée.- Un parseur récursif dans
altsans borne peut provoquer un débordement de pile ; utilisez un combinateur de récursion ou un mode itératif.