mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-19 05:06:56 +02:00
- ff::pg : portage complet de pg-ff-client.ts (mêmes requêtes SQL, fragments dynamiques via QueryBuilder, lectures tolérantes, caches process de la colonne parent de nomenclature et du lien mvtart → fournisseur). - ff::api : client de l'API FF Nancy (URL résolue Paramètres → env → défaut, mémorisée 30 s ; délai FF_API_TIMEOUT_MS), pagination, mensuel, référentiel, commandes, normalisation du statut de synchro. - ff::cache : getFournisseursCached / getDerniereReceptionCached. - ff::diagnostic : diagnostiquer_api_ff (DNS, refus, délai, code HTTP). - Routes /api/ff-status, /api/diag, /api/diag/stock-fournisseur, /api/test-db, /api/commandes-auto ; actions de la Grille et du cadencier ; pages dashboard, analytics, hit-parade, stock-negatif, commandes-auto, publicites. - Test d'intégration (FF_TEST_DATABASE_URL) avec fausse API FF : toutes les requêtes et tous les appels HTTP joués sur une base de test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LG2LLJuswrrTTPKJmunEVf
173 lines
5.1 KiB
Rust
173 lines
5.1 KiB
Rust
//! Conversions « à la JavaScript » sur des valeurs JSON brutes de l'API FF.
|
|
//!
|
|
//! Le code TS d'origine lisait les réponses de l'API sans schéma et convertissait
|
|
//! au fil de l'eau (`Number(x) || 0`, `String(x)`, tests de vérité). Ces aides
|
|
//! reproduisent ces conversions pour garder exactement les mêmes résultats.
|
|
|
|
use serde_json::Value;
|
|
|
|
/// `Number(v)` : `NaN` si la valeur n'est pas convertible.
|
|
pub fn js_number(v: &Value) -> f64 {
|
|
match v {
|
|
Value::Null => 0.0,
|
|
Value::Bool(b) => {
|
|
if *b {
|
|
1.0
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
Value::Number(n) => n.as_f64().unwrap_or(f64::NAN),
|
|
Value::String(s) => {
|
|
let t = s.trim();
|
|
if t.is_empty() {
|
|
0.0
|
|
} else {
|
|
// Rust accepte « inf » / « nan », pas JS (seul « Infinity » l'est, cas ignoré).
|
|
let l = t.to_ascii_lowercase();
|
|
if l.contains("inf") || l.contains("nan") {
|
|
f64::NAN
|
|
} else {
|
|
t.parse::<f64>().unwrap_or(f64::NAN)
|
|
}
|
|
}
|
|
}
|
|
Value::Array(a) => match a.len() {
|
|
0 => 0.0,
|
|
1 => js_number(&a[0]),
|
|
_ => f64::NAN,
|
|
},
|
|
Value::Object(_) => f64::NAN,
|
|
}
|
|
}
|
|
|
|
/// `Number(v) || 0` (NaN et 0 ramenés à 0).
|
|
pub fn js_number_or0(v: &Value) -> f64 {
|
|
let n = js_number(v);
|
|
if n.is_nan() {
|
|
0.0
|
|
} else {
|
|
n
|
|
}
|
|
}
|
|
|
|
/// `Number(v)` sur une valeur éventuellement absente (`undefined` → NaN).
|
|
pub fn js_number_opt(v: Option<&Value>) -> f64 {
|
|
match v {
|
|
None => f64::NAN,
|
|
Some(v) => js_number(v),
|
|
}
|
|
}
|
|
|
|
/// Formatage d'un nombre comme `String(n)` en JS (12 → "12", 1.5 → "1.5").
|
|
pub fn js_number_to_string(f: f64) -> String {
|
|
if f.is_nan() {
|
|
"NaN".into()
|
|
} else if f.is_infinite() {
|
|
if f > 0.0 {
|
|
"Infinity".into()
|
|
} else {
|
|
"-Infinity".into()
|
|
}
|
|
} else if f.fract() == 0.0 && f.abs() < 1e21 {
|
|
format!("{}", f as i128)
|
|
} else {
|
|
format!("{f}")
|
|
}
|
|
}
|
|
|
|
/// `String(v)`.
|
|
pub fn js_string(v: &Value) -> String {
|
|
match v {
|
|
Value::Null => "null".into(),
|
|
Value::Bool(b) => b.to_string(),
|
|
Value::Number(n) => match n.as_f64() {
|
|
Some(f) if n.is_f64() => js_number_to_string(f),
|
|
_ => n.to_string(),
|
|
},
|
|
Value::String(s) => s.clone(),
|
|
Value::Array(a) => a
|
|
.iter()
|
|
.map(|x| if x.is_null() { String::new() } else { js_string(x) })
|
|
.collect::<Vec<_>>()
|
|
.join(","),
|
|
Value::Object(_) => "[object Object]".into(),
|
|
}
|
|
}
|
|
|
|
/// Valeur « vraie » au sens JS (`if (v)`).
|
|
pub fn truthy(v: &Value) -> bool {
|
|
match v {
|
|
Value::Null => false,
|
|
Value::Bool(b) => *b,
|
|
Value::Number(n) => n.as_f64().map(|f| f != 0.0 && !f.is_nan()).unwrap_or(true),
|
|
Value::String(s) => !s.is_empty(),
|
|
Value::Array(_) | Value::Object(_) => true,
|
|
}
|
|
}
|
|
|
|
/// `v?.[key]` : `None` si absent (undefined).
|
|
pub fn get<'a>(v: &'a Value, key: &str) -> Option<&'a Value> {
|
|
v.as_object().and_then(|o| o.get(key))
|
|
}
|
|
|
|
/// `a ?? b ?? …` sur les clés d'un objet : première valeur ni absente ni `null`.
|
|
pub fn coalesce<'a>(v: &'a Value, keys: &[&str]) -> Option<&'a Value> {
|
|
keys.iter().filter_map(|k| get(v, k)).find(|x| !x.is_null())
|
|
}
|
|
|
|
/// Chaîne d'une valeur JSON, `None` si absente ou `null` (`x ?? undefined`).
|
|
pub fn opt_string(v: Option<&Value>) -> Option<String> {
|
|
match v {
|
|
None | Some(Value::Null) => None,
|
|
Some(x) => Some(js_string(x)),
|
|
}
|
|
}
|
|
|
|
/// Message d'une erreur et de ses causes, sans répéter une cause déjà incluse
|
|
/// dans le message précédent (les erreurs SQL recopient leur cause).
|
|
pub fn message_erreur(e: &anyhow::Error) -> String {
|
|
let mut parts: Vec<String> = vec![];
|
|
for cause in e.chain() {
|
|
let m = cause.to_string();
|
|
if !parts.iter().any(|p| p.contains(&m)) {
|
|
parts.push(m);
|
|
}
|
|
}
|
|
parts.join(": ")
|
|
}
|
|
|
|
/// `encodeURIComponent(String(v))`.
|
|
pub fn encode(v: &str) -> String {
|
|
urlencoding::encode(v).into_owned()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn number_conversions() {
|
|
assert_eq!(js_number(&json!("12.5")), 12.5);
|
|
assert_eq!(js_number(&json!(" ")), 0.0);
|
|
assert!(js_number(&json!("abc")).is_nan());
|
|
assert_eq!(js_number_or0(&json!("abc")), 0.0);
|
|
assert_eq!(js_number(&json!(true)), 1.0);
|
|
assert_eq!(js_number(&Value::Null), 0.0);
|
|
assert!(js_number_opt(None).is_nan());
|
|
}
|
|
|
|
#[test]
|
|
fn string_conversions() {
|
|
assert_eq!(js_string(&json!(12)), "12");
|
|
assert_eq!(js_string(&json!(12.0)), "12");
|
|
assert_eq!(js_string(&json!(1.5)), "1.5");
|
|
assert_eq!(js_string(&json!("x")), "x");
|
|
assert_eq!(js_number_to_string(2.0), "2");
|
|
assert!(truthy(&json!("a")));
|
|
assert!(!truthy(&json!("")));
|
|
assert!(!truthy(&json!(0)));
|
|
}
|
|
}
|