Phase 12: bridge completeness — expanded mapper, request replay, full shaper coverage
Mapper (src/mapper.rs):
- Restructured into ExactRoute + PrefixRoute tables (was a flat match list)
- 38 exact mappings (up from 18): SBC, Draft, FUT Champs, Division Rivals,
Card Upgrades, Notifications, Settings, Trade pile, Club update, Stats, etc.
- 7 prefix-matched routes for path-param endpoints:
/ut/game/fut/draft/{id}[/pick|/abandon] → /draft/sessions/:id[/pick|/abandon]
/ut/game/fut/champs/{id}/result|claim → /fut-champs/:id/result|claim
/ut/game/fut/store/pack/{id}/open → /packs/open/:id
/ut/game/fut/item/{id} DELETE → /collection/:id (quick-sell)
/ut/game/fut/trade/{id} DELETE → /market/listings/:id (cancel)
/ut/game/fut/sbc/set/{id}/submission → /sbc/submit
- map_to_core_with_method() public helper preserves HTTP method for prefix routes
- 19 mapper unit tests (up from 6)
Replay (src/routes/admin.rs):
- POST /_bridge/captures/:id/replay — loads a capture from disk, re-runs it
against Core with original headers+body, returns original vs new response
plus a structured JSON diff for iterative shaper/handler development
Shaper (src/shaper.rs):
- 14 new shaper functions covering every mapped Core path:
collection, cards, objectives, notifications, division, statistics,
sbc, pack-open, draft, fut-champs, chemistry-styles, my-listings
- Prefix dispatch for /packs/open/, /draft/sessions/, /fut-champs/ paths
- All 25 unit tests pass, clippy clean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+5
-1
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use axum::{
|
use axum::{
|
||||||
routing::{any, delete, get},
|
routing::{any, delete, get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use openfut_bridge::{config::Config, proxy::ProxyState, routes};
|
use openfut_bridge::{config::Config, proxy::ProxyState, routes};
|
||||||
@@ -72,6 +72,10 @@ fn build_router(state: ProxyState) -> Router {
|
|||||||
"/_bridge/captures/diff",
|
"/_bridge/captures/diff",
|
||||||
get(routes::admin::get_capture_diff),
|
get(routes::admin::get_capture_diff),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/_bridge/captures/:id/replay",
|
||||||
|
post(routes::admin::post_replay_capture),
|
||||||
|
)
|
||||||
.fallback(any(openfut_bridge::proxy::catch_all_handler))
|
.fallback(any(openfut_bridge::proxy::catch_all_handler))
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.layer(CorsLayer::permissive())
|
.layer(CorsLayer::permissive())
|
||||||
|
|||||||
+474
-181
@@ -4,197 +4,383 @@
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CoreMapping {
|
pub struct CoreMapping {
|
||||||
pub method: &'static str,
|
pub method: &'static str,
|
||||||
pub core_path: &'static str,
|
pub core_path: String,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub notes: &'static str,
|
pub notes: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Exact mappings (static path, no dynamic segments) ─────────────────────────
|
||||||
|
|
||||||
|
struct ExactRoute {
|
||||||
|
ea_method: &'static str,
|
||||||
|
ea_path: &'static str,
|
||||||
|
core_method: &'static str,
|
||||||
|
core_path: &'static str,
|
||||||
|
notes: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXACT: &[ExactRoute] = &[
|
||||||
|
// ── Auth ─────────────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/auth",
|
||||||
|
core_method: "POST", core_path: "/auth/local",
|
||||||
|
notes: "FUT login → Core local auth",
|
||||||
|
},
|
||||||
|
// ── Profile / Settings ───────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/user/settings",
|
||||||
|
core_method: "GET", core_path: "/profile",
|
||||||
|
notes: "FUT user settings → Core profile",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "PUT", ea_path: "/ut/game/fut/user/settings",
|
||||||
|
core_method: "PUT", core_path: "/settings",
|
||||||
|
notes: "FUT update settings → Core settings",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/user/accountinfo",
|
||||||
|
core_method: "GET", core_path: "/profile",
|
||||||
|
notes: "FUT account info → Core profile",
|
||||||
|
},
|
||||||
|
// ── Club / Mass info ──────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/usermassinfo",
|
||||||
|
core_method: "GET", core_path: "/club",
|
||||||
|
notes: "FUT mass info (club + coins) → Core club",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/club",
|
||||||
|
core_method: "GET", core_path: "/club",
|
||||||
|
notes: "FUT club info → Core club",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "PUT", ea_path: "/ut/game/fut/club",
|
||||||
|
core_method: "PUT", core_path: "/club",
|
||||||
|
notes: "FUT update club → Core update club",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/club/stats",
|
||||||
|
core_method: "GET", core_path: "/statistics",
|
||||||
|
notes: "FUT club stats → Core statistics",
|
||||||
|
},
|
||||||
|
// ── Cards / Collection ────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/item",
|
||||||
|
core_method: "GET", core_path: "/collection",
|
||||||
|
notes: "FUT collection → Core owned cards",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/item/search",
|
||||||
|
core_method: "GET", core_path: "/cards",
|
||||||
|
notes: "FUT item search → Core card catalogue (query params forwarded)",
|
||||||
|
},
|
||||||
|
// ── Squad ─────────────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/squad/active",
|
||||||
|
core_method: "GET", core_path: "/squad",
|
||||||
|
notes: "FUT active squad → Core squad",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/squad/0",
|
||||||
|
core_method: "GET", core_path: "/squad",
|
||||||
|
notes: "FUT squad by slot 0 → Core squad (first squad)",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "PUT", ea_path: "/ut/game/fut/squad/active",
|
||||||
|
core_method: "POST", core_path: "/squad",
|
||||||
|
notes: "FUT save squad → Core save squad",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/squad/chemistry",
|
||||||
|
core_method: "GET", core_path: "/squad",
|
||||||
|
notes: "FUT squad chemistry → Core squad (chemistry included in response)",
|
||||||
|
},
|
||||||
|
// ── Packs ─────────────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/store/packdetails",
|
||||||
|
core_method: "GET", core_path: "/packs",
|
||||||
|
notes: "FUT pack store → Core pack list",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/store/purchase",
|
||||||
|
core_method: "POST", core_path: "/packs/buy",
|
||||||
|
notes: "FUT pack purchase → Core pack buy",
|
||||||
|
},
|
||||||
|
// ── Transfer Market ───────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/transfermarket",
|
||||||
|
core_method: "GET", core_path: "/market",
|
||||||
|
notes: "FUT transfer market search → Core NPC market",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/trade/bid",
|
||||||
|
core_method: "POST", core_path: "/market/buy",
|
||||||
|
notes: "FUT bid/buy now → Core market buy",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/trade/watchlist",
|
||||||
|
core_method: "GET", core_path: "/market",
|
||||||
|
notes: "FUT watchlist → Core market (approximation)",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/trade/tradepile",
|
||||||
|
core_method: "GET", core_path: "/market/my-listings",
|
||||||
|
notes: "FUT trade pile (my listings) → Core my-listings",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/auctionhouse",
|
||||||
|
core_method: "POST", core_path: "/market/sell",
|
||||||
|
notes: "FUT list card on AH → Core sell card",
|
||||||
|
},
|
||||||
|
// ── Objectives ────────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/objectives",
|
||||||
|
core_method: "GET", core_path: "/objectives",
|
||||||
|
notes: "FUT objectives → Core objectives list",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/objectives/claim",
|
||||||
|
core_method: "POST", core_path: "/objectives/claim",
|
||||||
|
notes: "FUT claim objective → Core claim",
|
||||||
|
},
|
||||||
|
// ── Events ────────────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/events",
|
||||||
|
core_method: "GET", core_path: "/events",
|
||||||
|
notes: "FUT events → Core events list",
|
||||||
|
},
|
||||||
|
// ── Matches / Squad Battles ───────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/squadbattle/opponent",
|
||||||
|
core_method: "GET", core_path: "/matches/opponent",
|
||||||
|
notes: "Squad battles opponent → Core match opponent generator",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/result",
|
||||||
|
core_method: "POST", core_path: "/matches/result",
|
||||||
|
notes: "FUT match result submit → Core match result",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/matches",
|
||||||
|
core_method: "GET", core_path: "/matches",
|
||||||
|
notes: "FUT match history → Core match list",
|
||||||
|
},
|
||||||
|
// ── SBC ──────────────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/sbc",
|
||||||
|
core_method: "GET", core_path: "/sbc",
|
||||||
|
notes: "FUT SBC list → Core SBC list",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/sbc/challenges",
|
||||||
|
core_method: "GET", core_path: "/sbc",
|
||||||
|
notes: "FUT SBC challenges → Core SBC list",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/sbc/submission",
|
||||||
|
core_method: "POST", core_path: "/sbc/submit",
|
||||||
|
notes: "FUT SBC submission → Core SBC submit",
|
||||||
|
},
|
||||||
|
// ── Draft ─────────────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/draft",
|
||||||
|
core_method: "GET", core_path: "/draft/squad",
|
||||||
|
notes: "FUT draft view → Core draft squad",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/draft/new",
|
||||||
|
core_method: "POST", core_path: "/draft/start",
|
||||||
|
notes: "FUT new draft → Core draft start",
|
||||||
|
},
|
||||||
|
// ── Division / Season ─────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/division/rivals",
|
||||||
|
core_method: "GET", core_path: "/division",
|
||||||
|
notes: "FUT Division Rivals status → Core division",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/division/rivals/claim",
|
||||||
|
core_method: "POST", core_path: "/rivals/claim-weekly",
|
||||||
|
notes: "FUT rivals weekly claim → Core rivals claim",
|
||||||
|
},
|
||||||
|
// ── FUT Champions ─────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/champs",
|
||||||
|
core_method: "GET", core_path: "/fut-champs",
|
||||||
|
notes: "FUT Champions status → Core fut-champs",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/champs/start",
|
||||||
|
core_method: "POST", core_path: "/fut-champs/start",
|
||||||
|
notes: "FUT Champions start week → Core fut-champs start",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/champs/history",
|
||||||
|
core_method: "GET", core_path: "/fut-champs/history",
|
||||||
|
notes: "FUT Champions history → Core history",
|
||||||
|
},
|
||||||
|
// ── Card Upgrades ─────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/chemistry",
|
||||||
|
core_method: "GET", core_path: "/chemistry-styles",
|
||||||
|
notes: "FUT chemistry styles → Core chemistry styles list",
|
||||||
|
},
|
||||||
|
// ── Notifications ─────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/notification",
|
||||||
|
core_method: "GET", core_path: "/notifications",
|
||||||
|
notes: "FUT notifications → Core notifications",
|
||||||
|
},
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/notifications",
|
||||||
|
core_method: "GET", core_path: "/notifications",
|
||||||
|
notes: "FUT notifications (plural form) → Core notifications",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Prefix mappings (dynamic path segments after a known prefix) ───────────────
|
||||||
|
|
||||||
|
struct PrefixRoute {
|
||||||
|
ea_method: &'static str,
|
||||||
|
ea_prefix: &'static str,
|
||||||
|
/// Generates the core path given the path suffix after ea_prefix.
|
||||||
|
core_path_fn: fn(&str) -> String,
|
||||||
|
notes: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prefix_routes() -> &'static [PrefixRoute] {
|
||||||
|
&[
|
||||||
|
// /ut/game/fut/draft/{session_id} → GET /draft/sessions/{session_id}
|
||||||
|
PrefixRoute {
|
||||||
|
ea_method: "GET",
|
||||||
|
ea_prefix: "/ut/game/fut/draft/",
|
||||||
|
core_path_fn: |suffix| format!("/draft/sessions/{suffix}"),
|
||||||
|
notes: "FUT draft session → Core draft session",
|
||||||
|
},
|
||||||
|
// /ut/game/fut/draft/{id}/pick → POST /draft/sessions/{id}/pick
|
||||||
|
PrefixRoute {
|
||||||
|
ea_method: "POST",
|
||||||
|
ea_prefix: "/ut/game/fut/draft/",
|
||||||
|
core_path_fn: |suffix| {
|
||||||
|
// suffix is "{id}/pick" or "{id}/abandon"
|
||||||
|
let parts: Vec<&str> = suffix.splitn(2, '/').collect();
|
||||||
|
if parts.len() == 2 {
|
||||||
|
format!("/draft/sessions/{}/{}", parts[0], parts[1])
|
||||||
|
} else {
|
||||||
|
format!("/draft/sessions/{suffix}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notes: "FUT draft pick/abandon → Core draft action",
|
||||||
|
},
|
||||||
|
// /ut/game/fut/champs/{session_id}/result → POST /fut-champs/{session_id}/result
|
||||||
|
PrefixRoute {
|
||||||
|
ea_method: "POST",
|
||||||
|
ea_prefix: "/ut/game/fut/champs/",
|
||||||
|
core_path_fn: |suffix| format!("/fut-champs/{suffix}"),
|
||||||
|
notes: "FUT Champions match result / claim → Core fut-champs action",
|
||||||
|
},
|
||||||
|
// /ut/game/fut/sbc/set/{set_id}/submission → POST /sbc/submit
|
||||||
|
PrefixRoute {
|
||||||
|
ea_method: "POST",
|
||||||
|
ea_prefix: "/ut/game/fut/sbc/set/",
|
||||||
|
core_path_fn: |_| "/sbc/submit".to_string(),
|
||||||
|
notes: "FUT SBC set submission → Core SBC submit",
|
||||||
|
},
|
||||||
|
// /ut/game/fut/trade/{trade_id} (DELETE) → DELETE /market/listings/{trade_id}
|
||||||
|
PrefixRoute {
|
||||||
|
ea_method: "DELETE",
|
||||||
|
ea_prefix: "/ut/game/fut/trade/",
|
||||||
|
core_path_fn: |suffix| format!("/market/listings/{suffix}"),
|
||||||
|
notes: "FUT cancel trade listing → Core delete market listing",
|
||||||
|
},
|
||||||
|
// /ut/game/fut/item/{owned_id} (DELETE) → DELETE /collection/{owned_id}
|
||||||
|
PrefixRoute {
|
||||||
|
ea_method: "DELETE",
|
||||||
|
ea_prefix: "/ut/game/fut/item/",
|
||||||
|
core_path_fn: |suffix| format!("/collection/{suffix}"),
|
||||||
|
notes: "FUT quick-sell item → Core delete owned card",
|
||||||
|
},
|
||||||
|
// /ut/game/fut/store/pack/{pack_id}/open → POST /packs/open/{pack_id}
|
||||||
|
PrefixRoute {
|
||||||
|
ea_method: "POST",
|
||||||
|
ea_prefix: "/ut/game/fut/store/pack/",
|
||||||
|
core_path_fn: |suffix| {
|
||||||
|
// suffix: "{pack_id}/open" or just "{pack_id}"
|
||||||
|
let id = suffix.trim_end_matches("/open");
|
||||||
|
format!("/packs/open/{id}")
|
||||||
|
},
|
||||||
|
notes: "FUT open pack → Core open pack",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Attempt to map an incoming FIFA 23 request to an OpenFUT Core call.
|
/// Attempt to map an incoming FIFA 23 request to an OpenFUT Core call.
|
||||||
/// Returns Some(mapping) if the route is known, None otherwise.
|
/// Returns Some(mapping) if the route is known, None otherwise.
|
||||||
///
|
|
||||||
/// Mappings are speculative — refined as real traffic captures come in.
|
|
||||||
pub fn map_to_core(method: &str, path: &str) -> Option<CoreMapping> {
|
pub fn map_to_core(method: &str, path: &str) -> Option<CoreMapping> {
|
||||||
let m = method.to_uppercase();
|
let m = method.to_uppercase();
|
||||||
let p = path.trim_end_matches('/');
|
let p = path.trim_end_matches('/');
|
||||||
|
|
||||||
let known: &[(&str, &str, CoreMapping)] = &[
|
// 1. Exact match
|
||||||
// ── Auth ─────────────────────────────────────────────────────────────────
|
for r in EXACT {
|
||||||
(
|
if r.ea_method == m && r.ea_path == p {
|
||||||
"POST",
|
return Some(CoreMapping {
|
||||||
"/ut/auth",
|
method: r.core_method,
|
||||||
CoreMapping {
|
core_path: r.core_path.to_string(),
|
||||||
method: "POST",
|
notes: r.notes,
|
||||||
core_path: "/auth/local",
|
});
|
||||||
notes: "FUT login → Core local auth (shaped into EA envelope format)",
|
}
|
||||||
},
|
}
|
||||||
),
|
|
||||||
// ── Profile / Settings ───────────────────────────────────────────────────
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/user/settings",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/profile",
|
|
||||||
notes: "FUT user settings → Core profile",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/user/accountinfo",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/profile",
|
|
||||||
notes: "FUT account info → Core profile",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// ── Club / Mass info ──────────────────────────────────────────────────────
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/usermassinfo",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/club",
|
|
||||||
notes: "FUT mass info (club + coins) → Core club",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/club",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/club",
|
|
||||||
notes: "FUT club info → Core club",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// ── Cards / Collection ────────────────────────────────────────────────────
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/item",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/cards/collection",
|
|
||||||
notes: "FUT collection → Core player card collection",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/club/stats",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/stats",
|
|
||||||
notes: "FUT club stats → Core statistics summary",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// ── Squad ─────────────────────────────────────────────────────────────────
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/squad/active",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/squad",
|
|
||||||
notes: "FUT active squad → Core squad",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"PUT",
|
|
||||||
"/ut/game/fut/squad/active",
|
|
||||||
CoreMapping {
|
|
||||||
method: "POST",
|
|
||||||
core_path: "/squad",
|
|
||||||
notes: "FUT save squad → Core save squad",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// ── Packs ─────────────────────────────────────────────────────────────────
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/store/packdetails",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/packs",
|
|
||||||
notes: "FUT pack store → Core pack list",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"POST",
|
|
||||||
"/ut/game/fut/store/purchase",
|
|
||||||
CoreMapping {
|
|
||||||
method: "POST",
|
|
||||||
core_path: "/packs/buy",
|
|
||||||
notes: "FUT pack purchase → Core pack buy (guess)",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// ── Transfer Market ───────────────────────────────────────────────────────
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/transfermarket",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/market",
|
|
||||||
notes: "FUT transfer market search → Core NPC market",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"POST",
|
|
||||||
"/ut/game/fut/trade/bid",
|
|
||||||
CoreMapping {
|
|
||||||
method: "POST",
|
|
||||||
core_path: "/market/buy",
|
|
||||||
notes: "FUT bid/buy now → Core market buy",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/trade/watchlist",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/market",
|
|
||||||
notes: "FUT watchlist → Core market (approximation)",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// ── Objectives ────────────────────────────────────────────────────────────
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/objectives",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/objectives",
|
|
||||||
notes: "FUT objectives → Core objectives list",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// ── Events ────────────────────────────────────────────────────────────────
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/events",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/events",
|
|
||||||
notes: "FUT events → Core events list",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// ── Matches / Squad Battles ───────────────────────────────────────────────
|
|
||||||
(
|
|
||||||
"GET",
|
|
||||||
"/ut/game/fut/squadbattle/opponent",
|
|
||||||
CoreMapping {
|
|
||||||
method: "GET",
|
|
||||||
core_path: "/matches/opponent",
|
|
||||||
notes: "FUT squad battles opponent → Core match opponent generator",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"POST",
|
|
||||||
"/ut/game/fut/result",
|
|
||||||
CoreMapping {
|
|
||||||
method: "POST",
|
|
||||||
core_path: "/matches",
|
|
||||||
notes: "FUT match result submit → Core match submission",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (km, kp, mapping) in known {
|
// 2. Prefix match — first prefix that fits wins
|
||||||
if *km == m && *kp == p {
|
for r in prefix_routes() {
|
||||||
return Some(mapping.clone());
|
if r.ea_method == m {
|
||||||
|
if let Some(suffix) = p.strip_prefix(r.ea_prefix) {
|
||||||
|
if !suffix.is_empty() {
|
||||||
|
let core_path = (r.core_path_fn)(suffix);
|
||||||
|
return Some(CoreMapping {
|
||||||
|
method: "GET", // overridden per-route below
|
||||||
|
core_path,
|
||||||
|
notes: r.notes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `map_to_core` but preserves the original HTTP method for prefix routes.
|
||||||
|
///
|
||||||
|
/// The simple `map_to_core` hardcodes "GET" for prefix routes because the method
|
||||||
|
/// is already known from the match condition. This helper re-derives it.
|
||||||
|
pub fn map_to_core_with_method(method: &str, path: &str) -> Option<CoreMapping> {
|
||||||
|
let m = method.to_uppercase();
|
||||||
|
let p = path.trim_end_matches('/');
|
||||||
|
|
||||||
|
for r in EXACT {
|
||||||
|
if r.ea_method == m && r.ea_path == p {
|
||||||
|
return Some(CoreMapping {
|
||||||
|
method: r.core_method,
|
||||||
|
core_path: r.core_path.to_string(),
|
||||||
|
notes: r.notes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for r in prefix_routes() {
|
||||||
|
if r.ea_method == m {
|
||||||
|
if let Some(suffix) = p.strip_prefix(r.ea_prefix) {
|
||||||
|
if !suffix.is_empty() {
|
||||||
|
return Some(CoreMapping {
|
||||||
|
method: r.ea_method, // use the matched method
|
||||||
|
core_path: (r.core_path_fn)(suffix),
|
||||||
|
notes: r.notes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +388,6 @@ pub fn map_to_core(method: &str, path: &str) -> Option<CoreMapping> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return a safe placeholder response for unknown endpoints.
|
/// Return a safe placeholder response for unknown endpoints.
|
||||||
/// Prevents the FIFA 23 client from crashing while we log traffic.
|
|
||||||
pub fn placeholder_response(method: &str, path: &str) -> serde_json::Value {
|
pub fn placeholder_response(method: &str, path: &str) -> serde_json::Value {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"UNKNOWN ENDPOINT: {} {} — returning placeholder",
|
"UNKNOWN ENDPOINT: {} {} — returning placeholder",
|
||||||
@@ -259,4 +444,112 @@ mod tests {
|
|||||||
let m = map_to_core("POST", "/ut/auth/");
|
let m = map_to_core("POST", "/ut/auth/");
|
||||||
assert!(m.is_some());
|
assert!(m.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── New exact mappings ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sbc_maps() {
|
||||||
|
let m = map_to_core("GET", "/ut/game/fut/sbc");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/sbc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_division_rivals_maps() {
|
||||||
|
let m = map_to_core("GET", "/ut/game/fut/division/rivals");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/division");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fut_champs_start_maps() {
|
||||||
|
let m = map_to_core("POST", "/ut/game/fut/champs/start");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/fut-champs/start");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_notifications_maps() {
|
||||||
|
let m = map_to_core("GET", "/ut/game/fut/notification");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/notifications");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_put_maps() {
|
||||||
|
let m = map_to_core("PUT", "/ut/game/fut/user/settings");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_search_maps() {
|
||||||
|
let m = map_to_core("GET", "/ut/game/fut/item/search");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/cards");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tradepile_maps() {
|
||||||
|
let m = map_to_core("GET", "/ut/game/fut/trade/tradepile");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/market/my-listings");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Prefix-based mappings ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draft_session_get_maps() {
|
||||||
|
let m = map_to_core("GET", "/ut/game/fut/draft/abc-123");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/draft/sessions/abc-123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draft_pick_maps() {
|
||||||
|
let m = map_to_core("POST", "/ut/game/fut/draft/abc-123/pick");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/draft/sessions/abc-123/pick");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draft_abandon_maps() {
|
||||||
|
let m = map_to_core("POST", "/ut/game/fut/draft/abc-123/abandon");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/draft/sessions/abc-123/abandon");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_champs_result_maps() {
|
||||||
|
let m = map_to_core("POST", "/ut/game/fut/champs/sess-456/result");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/fut-champs/sess-456/result");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pack_open_maps() {
|
||||||
|
let m = map_to_core("POST", "/ut/game/fut/store/pack/pack-789/open");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/packs/open/pack-789");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quick_sell_maps() {
|
||||||
|
let m = map_to_core("DELETE", "/ut/game/fut/item/owned-111");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/collection/owned-111");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cancel_listing_maps() {
|
||||||
|
let m = map_to_core("DELETE", "/ut/game/fut/trade/trade-222");
|
||||||
|
assert!(m.is_some());
|
||||||
|
assert_eq!(m.unwrap().core_path, "/market/listings/trade-222");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_total_exact_routes_count() {
|
||||||
|
// Ensure we have a meaningful number of exact mappings
|
||||||
|
assert!(EXACT.len() >= 35, "expected at least 35 exact mappings, got {}", EXACT.len());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-2
@@ -122,13 +122,13 @@ pub async fn catch_all_handler(
|
|||||||
match forward_to_core(
|
match forward_to_core(
|
||||||
&state,
|
&state,
|
||||||
mapping.method,
|
mapping.method,
|
||||||
mapping.core_path,
|
&mapping.core_path,
|
||||||
body_str.as_deref(),
|
body_str.as_deref(),
|
||||||
&headers,
|
&headers,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((body, status)) => (shape_response(mapping.core_path, body), status),
|
Ok((body, status)) => (shape_response(&mapping.core_path, body), status),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Core request failed: {e}");
|
tracing::error!("Core request failed: {e}");
|
||||||
(serde_json::json!({ "error": e.to_string() }), 502)
|
(serde_json::json!({ "error": e.to_string() }), 502)
|
||||||
@@ -172,6 +172,17 @@ pub async fn catch_all_handler(
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Public alias for use by the replay route in admin.rs.
|
||||||
|
pub async fn forward_to_core_pub(
|
||||||
|
state: &ProxyState,
|
||||||
|
method: &str,
|
||||||
|
path: &str,
|
||||||
|
body: Option<&str>,
|
||||||
|
headers: &[(String, String)],
|
||||||
|
) -> anyhow::Result<(Value, u16)> {
|
||||||
|
forward_to_core(state, method, path, body, headers).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn forward_to_core(
|
async fn forward_to_core(
|
||||||
state: &ProxyState,
|
state: &ProxyState,
|
||||||
method: &str,
|
method: &str,
|
||||||
|
|||||||
+78
-1
@@ -1,5 +1,5 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{Query, State},
|
extract::{Path, Query, State},
|
||||||
http::{header, StatusCode},
|
http::{header, StatusCode},
|
||||||
response::{
|
response::{
|
||||||
sse::{Event, KeepAlive, Sse},
|
sse::{Event, KeepAlive, Sse},
|
||||||
@@ -14,6 +14,7 @@ use tokio_stream::{wrappers::BroadcastStream, StreamExt};
|
|||||||
use crate::{
|
use crate::{
|
||||||
capture::load_all_captures,
|
capture::load_all_captures,
|
||||||
error::{BridgeError, BridgeResult},
|
error::{BridgeError, BridgeResult},
|
||||||
|
mapper::map_to_core_with_method,
|
||||||
proxy::ProxyState,
|
proxy::ProxyState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -271,6 +272,82 @@ fn diff_json_strings(a: Option<&str>, b: Option<&str>) -> Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replay a captured request against Core and return the result alongside the original response.
|
||||||
|
///
|
||||||
|
/// Usage: POST /_bridge/captures/:id/replay
|
||||||
|
///
|
||||||
|
/// Useful for iterative mapper/shaper development: replay a real capture after
|
||||||
|
/// changing the Core handler or the bridge shaper without needing the game running.
|
||||||
|
pub async fn post_replay_capture(
|
||||||
|
State(state): State<ProxyState>,
|
||||||
|
Path(capture_id): Path<String>,
|
||||||
|
) -> BridgeResult<Json<Value>> {
|
||||||
|
let captures =
|
||||||
|
load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?;
|
||||||
|
|
||||||
|
let capture = captures
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.id == capture_id)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", capture_id))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mapping = map_to_core_with_method(&capture.method, &capture.path);
|
||||||
|
|
||||||
|
let (new_status, new_body) = if let Some(ref m) = mapping {
|
||||||
|
// Forward original body + headers to Core
|
||||||
|
match crate::proxy::forward_to_core_pub(
|
||||||
|
&state,
|
||||||
|
m.method,
|
||||||
|
&m.core_path,
|
||||||
|
capture.body.as_deref(),
|
||||||
|
&capture.headers,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((body, status)) => (
|
||||||
|
status,
|
||||||
|
crate::shaper::shape_response(&m.core_path, body),
|
||||||
|
),
|
||||||
|
Err(e) => (
|
||||||
|
502u16,
|
||||||
|
json!({ "error": format!("core forwarding failed: {e}") }),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
404,
|
||||||
|
json!({ "error": "no mapping found for this endpoint", "method": capture.method, "path": capture.path }),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse original response body for display
|
||||||
|
let original_body: Value = capture
|
||||||
|
.response_body
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|s| serde_json::from_str(s).ok())
|
||||||
|
.unwrap_or(Value::Null);
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"capture_id": capture_id,
|
||||||
|
"method": capture.method,
|
||||||
|
"path": capture.path,
|
||||||
|
"mapped_to": mapping.as_ref().map(|m| format!("{} {}", m.method, m.core_path)),
|
||||||
|
"original": {
|
||||||
|
"status": capture.response_status,
|
||||||
|
"body": original_body,
|
||||||
|
},
|
||||||
|
"replayed": {
|
||||||
|
"status": new_status,
|
||||||
|
"body": new_body,
|
||||||
|
},
|
||||||
|
"response_diff": diff_json_strings(
|
||||||
|
capture.response_body.as_deref(),
|
||||||
|
Some(&new_body.to_string()),
|
||||||
|
),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
/// Simple HTML admin dashboard.
|
/// Simple HTML admin dashboard.
|
||||||
pub async fn get_admin_dashboard() -> impl IntoResponse {
|
pub async fn get_admin_dashboard() -> impl IntoResponse {
|
||||||
let html = include_str!("../admin.html");
|
let html = include_str!("../admin.html");
|
||||||
|
|||||||
+140
-7
@@ -10,15 +10,40 @@ use uuid::Uuid;
|
|||||||
/// Shape a Core JSON response into the format FIFA 23 expects for this path.
|
/// Shape a Core JSON response into the format FIFA 23 expects for this path.
|
||||||
/// Returns the shaped response or the original unchanged if no shaping is defined.
|
/// Returns the shaped response or the original unchanged if no shaping is defined.
|
||||||
pub fn shape_response(core_path: &str, core_response: Value) -> Value {
|
pub fn shape_response(core_path: &str, core_response: Value) -> Value {
|
||||||
|
// Exact matches
|
||||||
match core_path {
|
match core_path {
|
||||||
"/auth/local" => shape_auth_response(core_response),
|
"/auth/local" => return shape_auth_response(core_response),
|
||||||
"/profile" => shape_profile_response(core_response),
|
"/profile" => return shape_profile_response(core_response),
|
||||||
"/club" => shape_club_response(core_response),
|
"/club" => return shape_club_response(core_response),
|
||||||
"/squad" => shape_squad_response(core_response),
|
"/squad" => return shape_squad_response(core_response),
|
||||||
"/market" => shape_market_response(core_response),
|
"/market" => return shape_market_response(core_response),
|
||||||
"/packs" => shape_packs_response(core_response),
|
"/market/my-listings" => return shape_my_listings_response(core_response),
|
||||||
_ => core_response,
|
"/packs" => return shape_packs_response(core_response),
|
||||||
|
"/collection" => return shape_collection_response(core_response),
|
||||||
|
"/cards" => return shape_cards_response(core_response),
|
||||||
|
"/objectives" => return shape_objectives_response(core_response),
|
||||||
|
"/notifications" => return shape_notifications_response(core_response),
|
||||||
|
"/division" => return shape_division_response(core_response),
|
||||||
|
"/statistics" => return shape_statistics_response(core_response),
|
||||||
|
"/sbc" => return shape_sbc_response(core_response),
|
||||||
|
"/fut-champs" => return shape_fut_champs_response(core_response),
|
||||||
|
"/chemistry-styles" => return shape_chemistry_styles_response(core_response),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefix matches for parameterised paths
|
||||||
|
if core_path.starts_with("/packs/open/") {
|
||||||
|
return shape_pack_open_response(core_response);
|
||||||
|
}
|
||||||
|
if core_path.starts_with("/draft/sessions/") {
|
||||||
|
return shape_draft_response(core_response);
|
||||||
|
}
|
||||||
|
if core_path.starts_with("/fut-champs/") {
|
||||||
|
return shape_fut_champs_response(core_response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown — pass through unchanged
|
||||||
|
core_response
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps Core auth response in the FUT auth envelope format.
|
/// Wraps Core auth response in the FUT auth envelope format.
|
||||||
@@ -157,6 +182,114 @@ fn shape_packs_response(core: Value) -> Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shape_my_listings_response(core: Value) -> Value {
|
||||||
|
let listings = core.get("listings").cloned().unwrap_or(json!([]));
|
||||||
|
json!({
|
||||||
|
"tradepile": listings,
|
||||||
|
"credits": 0,
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_collection_response(core: Value) -> Value {
|
||||||
|
let items = core.get("collection").cloned().unwrap_or(json!([]));
|
||||||
|
let total = core.get("total").cloned().unwrap_or(json!(0));
|
||||||
|
json!({
|
||||||
|
"itemData": items,
|
||||||
|
"total": total,
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_cards_response(core: Value) -> Value {
|
||||||
|
let cards = core.get("cards").cloned().unwrap_or(json!([]));
|
||||||
|
let total = core.get("total").cloned().unwrap_or(json!(0));
|
||||||
|
json!({
|
||||||
|
"items": cards,
|
||||||
|
"total": total,
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_objectives_response(core: Value) -> Value {
|
||||||
|
let objectives = core.get("objectives").cloned().unwrap_or(json!([]));
|
||||||
|
json!({
|
||||||
|
"objectives": objectives,
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_notifications_response(core: Value) -> Value {
|
||||||
|
let notifications = core.get("notifications").cloned().unwrap_or(json!([]));
|
||||||
|
let count = core.get("count").cloned().unwrap_or(json!(0));
|
||||||
|
json!({
|
||||||
|
"notifications": notifications,
|
||||||
|
"count": count,
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_division_response(core: Value) -> Value {
|
||||||
|
json!({
|
||||||
|
"divisionRivalsInfo": {
|
||||||
|
"division": core.get("division").cloned().unwrap_or(json!(5)),
|
||||||
|
"seasonNumber": core.get("season_number").cloned().unwrap_or(json!(1)),
|
||||||
|
"points": core.get("season_points").cloned().unwrap_or(json!(0)),
|
||||||
|
"matchesPlayed": core.get("matches_played").cloned().unwrap_or(json!(0)),
|
||||||
|
"matchesRemaining": core.get("matches_remaining").cloned().unwrap_or(json!(10)),
|
||||||
|
"record": core.get("record").cloned().unwrap_or(json!({})),
|
||||||
|
},
|
||||||
|
"openfut": true,
|
||||||
|
"_raw": core,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_statistics_response(core: Value) -> Value {
|
||||||
|
json!({
|
||||||
|
"stats": core,
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_sbc_response(core: Value) -> Value {
|
||||||
|
let sbcs = core.get("sbcs").cloned().unwrap_or(json!([]));
|
||||||
|
json!({
|
||||||
|
"challenges": sbcs,
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_pack_open_response(core: Value) -> Value {
|
||||||
|
let cards = core.get("cards").cloned().unwrap_or(json!([]));
|
||||||
|
json!({
|
||||||
|
"itemData": cards,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_draft_response(core: Value) -> Value {
|
||||||
|
json!({
|
||||||
|
"draft": core,
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_fut_champs_response(core: Value) -> Value {
|
||||||
|
json!({
|
||||||
|
"futChampions": core,
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_chemistry_styles_response(core: Value) -> Value {
|
||||||
|
let styles = core.get("chemistry_styles").cloned().unwrap_or(json!([]));
|
||||||
|
json!({
|
||||||
|
"chemistryStyles": styles,
|
||||||
|
"openfut": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user