fix(fifa17): implement rare=SP 'Special' club filter via rareflag
The club search 'Quality = Special' sends rare=SP, which was a deliberate no-op
('semantics UNKNOWN'), so it returned every card — base golds included. The
rareflag work now grounds it: a special is rareflag > 1 (base rare = 1),
evidence-backed by the FIFA17 taxonomy + the observed profile (base Ronaldo/Messi
rareflag 1; their informs 11/24).
rareflag lives in the FIFA catalog, not Core, so Core cannot filter it:
- map_to_core: rare=SP now sets CoreOwnedQuery.special (host-applied), not
'unsupported'; any OTHER rare value stays unsupported. special is NEVER a Core
/collection param. is_special_rareflag(rf)=rf>1 lives in the adapter.
- handle_club special path: fetch all items matching the OTHER filters (offset/
limit stripped), shape (resolves rareflag), then special_filter_page() keeps
rareflag>1 and paginates the FILTERED set locally (start/count over specials,
not Core's unfiltered page) — no base leakage, no post-pagination drops.
Verified live on the real staged club: rare=SP -> 1665 items (=1949-284 base),
rareflag distribution all >1, zero base leaked; pagination page0==full[0:50],
page1==full[50:100], no overlap. Tests: adapter map rare=SP->special, unknown
rare stays unsupported, is_special_rareflag predicate; host special_filter_page
filter+paginate. adapter 113 + host 25 + importer 25 green; clippy -D clean.
This commit is contained in:
@@ -193,6 +193,10 @@ pub struct CoreOwnedQuery {
|
||||
pub club: Option<String>,
|
||||
pub offset: Option<i64>,
|
||||
pub limit: Option<i64>,
|
||||
/// "Special" filter (`rare=SP`): keep only special cards. Applied by the
|
||||
/// HOST via the FIFA `rareflag` (which lives in the catalog, not Core) — so
|
||||
/// it is NEVER a Core `/collection` param. See [`is_special_rareflag`].
|
||||
pub special: bool,
|
||||
/// Wire filters that were parsed but deliberately NOT applied because their
|
||||
/// semantics are unproven (currently: `rare`/Special). Recorded, never guessed.
|
||||
pub unsupported: Vec<&'static str>,
|
||||
@@ -245,9 +249,13 @@ pub fn map_to_core(
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// rare=SP: semantics UNKNOWN. Recorded, never turned into a filter.
|
||||
// rare=SP → "Special" quality. Now GROUNDED via the observed FIFA rareflag
|
||||
// (carried in the catalog): a special is rareflag > 1 (base rare = 1). The
|
||||
// host applies it post-shape; Core never sees it. Any OTHER `rare` value
|
||||
// stays genuinely unsupported (recorded, never guessed).
|
||||
let special = matches!(q.rare.as_deref(), Some(s) if s.eq_ignore_ascii_case("SP"));
|
||||
let mut unsupported = Vec::new();
|
||||
if q.rare.is_some() {
|
||||
if q.rare.is_some() && !special {
|
||||
unsupported.push("rare");
|
||||
}
|
||||
|
||||
@@ -283,10 +291,19 @@ pub fn map_to_core(
|
||||
club,
|
||||
offset: q.start.map(|s| s as i64),
|
||||
limit: q.count.map(|c| c as i64),
|
||||
special,
|
||||
unsupported,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a FIFA `rareflag` denotes a SPECIAL card (in-form/programme), as
|
||||
/// opposed to a base card. Grounded in the observed profile + the FIFA 17
|
||||
/// taxonomy: 0 = common, 1 = rare (both BASE gold/silver/bronze); every value
|
||||
/// above 1 is a special programme (3 = TOTW, 21..=24 = programmes, etc.).
|
||||
pub fn is_special_rareflag(rareflag: i64) -> bool {
|
||||
rareflag > 1
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -365,18 +382,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_rare_sp_is_unsupported_not_a_filter() {
|
||||
fn map_rare_sp_sets_special_and_never_a_core_param() {
|
||||
let core = map_to_core(&parse_club_query("level=any&rare=SP"), &resolver()).unwrap();
|
||||
assert!(
|
||||
core.unsupported.contains(&"rare"),
|
||||
"rare must be recorded unsupported"
|
||||
);
|
||||
// never guessed into a Core predicate
|
||||
// rare=SP is now GROUNDED: a host-applied special flag, not "unsupported".
|
||||
assert!(core.special, "rare=SP must set the special flag");
|
||||
assert!(!core.unsupported.contains(&"rare"));
|
||||
// still NEVER a Core predicate (Core has no rareflag) and no quality guess.
|
||||
assert_eq!(core.quality, None);
|
||||
let keys: Vec<&str> = core.to_query_pairs().into_iter().map(|(k, _)| k).collect();
|
||||
assert!(!keys.contains(&"rare") && !keys.contains(&"special"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_unknown_rare_value_stays_unsupported() {
|
||||
let core = map_to_core(&parse_club_query("rare=WAT"), &resolver()).unwrap();
|
||||
assert!(!core.special);
|
||||
assert!(core.unsupported.contains(&"rare"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn special_predicate_base_vs_special() {
|
||||
assert!(!is_special_rareflag(0)); // common
|
||||
assert!(!is_special_rareflag(1)); // rare gold (base)
|
||||
assert!(is_special_rareflag(3)); // TOTW
|
||||
assert!(is_special_rareflag(24)); // programme
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_resolves_ids_to_semantic_names() {
|
||||
let core =
|
||||
@@ -456,6 +487,7 @@ mod tests {
|
||||
club: Some("Chelsea".into()),
|
||||
offset: Some(10),
|
||||
limit: Some(11),
|
||||
special: false,
|
||||
unsupported: vec![],
|
||||
};
|
||||
let keys: Vec<&str> = core.to_query_pairs().into_iter().map(|(k, _)| k).collect();
|
||||
@@ -485,6 +517,7 @@ mod tests {
|
||||
club: Some("Chelsea".into()),
|
||||
offset: Some(10),
|
||||
limit: Some(11),
|
||||
special: false,
|
||||
unsupported: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user