Good stuff
This commit is contained in:
parent
9da2db707f
commit
8032011708
32 changed files with 1052 additions and 374 deletions
|
|
@ -66,24 +66,41 @@ impl LruCache {
|
|||
}
|
||||
}
|
||||
|
||||
/// Strip a numeric prefix like "000000-" from a filename stem.
|
||||
/// "000000-bank-tube-station" → "bank-tube-station"
|
||||
fn strip_numeric_prefix(stem: &str) -> &str {
|
||||
if let Some(pos) = stem.find('-') {
|
||||
if stem[..pos].chars().all(|ch| ch.is_ascii_digit()) {
|
||||
return &stem[pos + 1..];
|
||||
}
|
||||
}
|
||||
stem
|
||||
}
|
||||
|
||||
/// Manages on-demand loading and caching of precomputed travel time parquet files.
|
||||
///
|
||||
/// Directory structure: `{base_dir}/{mode}/{slug}.parquet`
|
||||
/// Directory structure: `{base_dir}/{mode}/{NNNNNN-slug}.parquet`
|
||||
/// Files have a numeric prefix for uniqueness; lookups use the stripped slug.
|
||||
/// Each parquet file has columns: `pcds` (String), `travel_minutes` (Int16).
|
||||
pub struct TravelTimeStore {
|
||||
base_dir: PathBuf,
|
||||
/// Available transport modes (subdirectory names, e.g., "bicycle")
|
||||
pub available_modes: Vec<String>,
|
||||
/// mode → set of destination slugs (filenames without .parquet)
|
||||
/// mode → set of destination slugs (numeric prefix stripped)
|
||||
pub destinations: FxHashMap<String, FxHashSet<String>>,
|
||||
/// (mode, stripped_slug) → full filename stem (with numeric prefix)
|
||||
slug_to_file: FxHashMap<(String, String), String>,
|
||||
cache: Mutex<LruCache>,
|
||||
}
|
||||
|
||||
impl TravelTimeStore {
|
||||
/// Scan the travel-times directory to discover available modes and destinations.
|
||||
/// Filename stems have a numeric prefix (e.g., "000000-bank-tube-station") which
|
||||
/// is stripped for slug lookups but preserved for file loading.
|
||||
pub fn load(base_dir: &Path, cache_capacity: usize) -> anyhow::Result<Self> {
|
||||
let mut available_modes = Vec::new();
|
||||
let mut destinations: FxHashMap<String, FxHashSet<String>> = FxHashMap::default();
|
||||
let mut slug_to_file: FxHashMap<(String, String), String> = FxHashMap::default();
|
||||
|
||||
for entry in std::fs::read_dir(base_dir)
|
||||
.with_context(|| format!("Failed to read travel-times dir: {}", base_dir.display()))?
|
||||
|
|
@ -103,7 +120,12 @@ impl TravelTimeStore {
|
|||
let file_name = file_entry.file_name();
|
||||
let file_name = file_name.to_string_lossy();
|
||||
if file_name.ends_with(".parquet") {
|
||||
let slug = file_name.trim_end_matches(".parquet").to_string();
|
||||
let file_stem = file_name.trim_end_matches(".parquet");
|
||||
let slug = strip_numeric_prefix(file_stem).to_string();
|
||||
slug_to_file.insert(
|
||||
(mode.clone(), slug.clone()),
|
||||
file_stem.to_string(),
|
||||
);
|
||||
slugs.insert(slug);
|
||||
}
|
||||
}
|
||||
|
|
@ -125,6 +147,7 @@ impl TravelTimeStore {
|
|||
base_dir: base_dir.to_path_buf(),
|
||||
available_modes,
|
||||
destinations,
|
||||
slug_to_file,
|
||||
cache: Mutex::new(LruCache::new(cache_capacity)),
|
||||
})
|
||||
}
|
||||
|
|
@ -142,11 +165,16 @@ impl TravelTimeStore {
|
|||
}
|
||||
}
|
||||
|
||||
// Load from file (no lock held — harmless if two threads load the same file)
|
||||
// Resolve slug to actual filename (may have numeric prefix)
|
||||
let file_stem = self
|
||||
.slug_to_file
|
||||
.get(&key)
|
||||
.map(|val| val.as_str())
|
||||
.unwrap_or(slug);
|
||||
let path = self
|
||||
.base_dir
|
||||
.join(mode)
|
||||
.join(format!("{}.parquet", slug));
|
||||
.join(format!("{}.parquet", file_stem));
|
||||
if !path.exists() {
|
||||
bail!("Travel time file not found: {}", path.display());
|
||||
}
|
||||
|
|
@ -233,18 +261,15 @@ mod tests {
|
|||
#[test]
|
||||
fn slugify_basic() {
|
||||
assert_eq!(slugify("Abbey Hey"), "abbey-hey");
|
||||
assert_eq!(slugify("Abbots Bickington"), "abbots-bickington");
|
||||
assert_eq!(slugify("London"), "london");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slugify_special_chars() {
|
||||
assert_eq!(slugify("A'Bhuaile Ghlas"), "a-bhuaile-ghlas");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slugify_edges() {
|
||||
assert_eq!(slugify(" Hello "), "hello");
|
||||
assert_eq!(slugify("Abbey"), "abbey");
|
||||
fn strip_numeric_prefix_basic() {
|
||||
assert_eq!(strip_numeric_prefix("000000-bank-tube-station"), "bank-tube-station");
|
||||
assert_eq!(strip_numeric_prefix("000123-abbey-hey"), "abbey-hey");
|
||||
assert_eq!(strip_numeric_prefix("bank-tube-station"), "bank-tube-station");
|
||||
assert_eq!(strip_numeric_prefix("london"), "london");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ pub struct FeatureConfig {
|
|||
pub raw: bool,
|
||||
/// If true, the slider uses absolute min/max/step instead of percentile scaling
|
||||
pub absolute: bool,
|
||||
/// Listing modes this feature is available in (empty = all modes)
|
||||
pub modes: &'static [&'static str],
|
||||
/// Name of the linked feature that swaps when switching modes (empty = no link)
|
||||
pub linked: &'static str,
|
||||
}
|
||||
|
||||
/// Features whose histogram bins should be exactly 1 unit wide (one per integer).
|
||||
|
|
@ -61,7 +65,7 @@ pub struct EnumFeatureGroup {
|
|||
|
||||
pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
||||
FeatureGroup {
|
||||
name: "Property",
|
||||
name: "Properties in the area",
|
||||
features: &[
|
||||
FeatureConfig {
|
||||
name: "Last known price",
|
||||
|
|
@ -77,6 +81,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: true,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Estimated current price",
|
||||
|
|
@ -92,6 +98,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: true,
|
||||
modes: &["historical"],
|
||||
linked: "Asking price",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Price per sqm",
|
||||
|
|
@ -107,6 +115,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Est. price per sqm",
|
||||
|
|
@ -122,6 +132,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Total floor area (sqm)",
|
||||
|
|
@ -137,6 +149,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: " sqm",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Interior height (m)",
|
||||
|
|
@ -152,6 +166,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: " m",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Number of bedrooms & living rooms",
|
||||
|
|
@ -167,6 +183,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: " rooms",
|
||||
raw: false,
|
||||
absolute: true,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Estimated monthly rent",
|
||||
|
|
@ -179,6 +197,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/mo",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &["historical"],
|
||||
linked: "Asking rent (monthly)",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Date of last transaction",
|
||||
|
|
@ -194,6 +214,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: true,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Construction age",
|
||||
|
|
@ -209,6 +231,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: true,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Asking price",
|
||||
|
|
@ -224,6 +248,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: true,
|
||||
modes: &["buy"],
|
||||
linked: "Estimated current price",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Asking rent (monthly)",
|
||||
|
|
@ -239,6 +265,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/mo",
|
||||
raw: false,
|
||||
absolute: true,
|
||||
modes: &["rent"],
|
||||
linked: "Estimated monthly rent",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Bedrooms",
|
||||
|
|
@ -254,6 +282,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: true,
|
||||
modes: &["buy", "rent"],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Bathrooms",
|
||||
|
|
@ -269,6 +299,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: true,
|
||||
modes: &["buy", "rent"],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Listing date",
|
||||
|
|
@ -284,6 +316,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: true,
|
||||
absolute: false,
|
||||
modes: &["buy", "rent"],
|
||||
linked: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -304,6 +338,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: " mins",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Public transport to Fitzrovia (mins)",
|
||||
|
|
@ -319,6 +355,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: " mins",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Cycling to Bank (mins)",
|
||||
|
|
@ -334,6 +372,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: " mins",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Cycling to Fitzrovia (mins)",
|
||||
|
|
@ -349,6 +389,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: " mins",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Number of public transport stations within 2km",
|
||||
|
|
@ -364,6 +406,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -384,6 +428,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Good+ primary schools within 5km",
|
||||
|
|
@ -399,6 +445,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Good+ secondary schools within 5km",
|
||||
|
|
@ -414,6 +462,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -431,6 +481,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Employment Score (rate)",
|
||||
|
|
@ -443,6 +495,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Health Deprivation and Disability Score",
|
||||
|
|
@ -458,6 +512,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Living Environment Score",
|
||||
|
|
@ -473,6 +529,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Indoors Sub-domain Score",
|
||||
|
|
@ -488,6 +546,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Outdoors Sub-domain Score",
|
||||
|
|
@ -503,6 +563,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -524,6 +586,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Minor crime (avg/yr)",
|
||||
|
|
@ -539,6 +603,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -559,6 +625,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Violence and sexual offences (avg/yr)",
|
||||
|
|
@ -574,6 +642,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Criminal damage and arson (avg/yr)",
|
||||
|
|
@ -589,6 +659,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Burglary (avg/yr)",
|
||||
|
|
@ -604,6 +676,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Vehicle crime (avg/yr)",
|
||||
|
|
@ -619,6 +693,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Robbery (avg/yr)",
|
||||
|
|
@ -634,6 +710,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Other theft (avg/yr)",
|
||||
|
|
@ -649,6 +727,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Shoplifting (avg/yr)",
|
||||
|
|
@ -664,6 +744,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Drugs (avg/yr)",
|
||||
|
|
@ -679,6 +761,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Possession of weapons (avg/yr)",
|
||||
|
|
@ -694,6 +778,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Public order (avg/yr)",
|
||||
|
|
@ -709,6 +795,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Bicycle theft (avg/yr)",
|
||||
|
|
@ -724,6 +812,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Theft from the person (avg/yr)",
|
||||
|
|
@ -739,6 +829,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Other crime (avg/yr)",
|
||||
|
|
@ -754,6 +846,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "/yr",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -774,6 +868,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "%",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "% Asian",
|
||||
|
|
@ -789,6 +885,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "%",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "% Black",
|
||||
|
|
@ -804,6 +902,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "%",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "% Mixed",
|
||||
|
|
@ -819,6 +919,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "%",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "% Other",
|
||||
|
|
@ -834,6 +936,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "%",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -854,6 +958,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Number of grocery shops and supermarkets within 2km",
|
||||
|
|
@ -869,6 +975,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Number of parks within 2km",
|
||||
|
|
@ -884,6 +992,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: "",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -904,6 +1014,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: " dB",
|
||||
raw: false,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
FeatureConfig {
|
||||
name: "Max available download speed (Mbps)",
|
||||
|
|
@ -919,6 +1031,8 @@ pub static FEATURE_GROUPS: &[FeatureGroup] = &[
|
|||
suffix: " Mbps",
|
||||
raw: true,
|
||||
absolute: false,
|
||||
modes: &[],
|
||||
linked: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ fn is_false(val: &bool) -> bool {
|
|||
!val
|
||||
}
|
||||
|
||||
fn is_empty_slice(val: &&[&str]) -> bool {
|
||||
val.is_empty()
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum FeatureInfo {
|
||||
|
|
@ -37,6 +41,10 @@ pub enum FeatureInfo {
|
|||
raw: bool,
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
absolute: bool,
|
||||
#[serde(skip_serializing_if = "is_empty_slice")]
|
||||
modes: &'static [&'static str],
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
linked: &'static str,
|
||||
},
|
||||
#[serde(rename = "enum")]
|
||||
Enum {
|
||||
|
|
@ -102,6 +110,8 @@ pub fn build_features_response(data: &PropertyData) -> FeaturesResponse {
|
|||
suffix: feature_config.suffix,
|
||||
raw: feature_config.raw,
|
||||
absolute: feature_config.absolute,
|
||||
modes: feature_config.modes,
|
||||
linked: feature_config.linked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,16 +138,13 @@ pub async fn post_invites(
|
|||
}
|
||||
}
|
||||
|
||||
/// Validate an invite code. Requires authentication to prevent enumeration.
|
||||
/// Validate an invite code. Public endpoint — codes are 12-char random alphanumeric
|
||||
/// so enumeration is impractical, and the response only reveals valid/invalid + type.
|
||||
pub async fn get_invite(
|
||||
state: Arc<AppState>,
|
||||
Extension(user): Extension<OptionalUser>,
|
||||
Extension(_user): Extension<OptionalUser>,
|
||||
Path(code): Path<String>,
|
||||
) -> Response {
|
||||
if user.0.is_none() {
|
||||
return StatusCode::UNAUTHORIZED.into_response();
|
||||
}
|
||||
|
||||
if let Err(msg) = validate_invite_code(&code) {
|
||||
return (StatusCode::BAD_REQUEST, msg).into_response();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue