Files
hagfish/src/types.rs
T
2026-08-16 23:05:29 +01:00

279 lines
8.3 KiB
Rust

//! Type definitions for hagfish data structures.
//!
//! These types represent the PX-Web API contract and internal data models.
//! All public types derive Serialize/Deserialize for JSON (de)serialization.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Configuration loaded from config.json on startup.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub duckdb_path: String,
pub bind_address: String,
pub data_source_url: String,
pub log_file_path: Option<String>,
}
impl Default for Config {
fn default() -> Self {
Self {
duckdb_path: "hagfish.db".to_string(),
bind_address: "127.0.0.1:8090".to_string(),
data_source_url: "https://statbank.hagstova.fo/api/v1/fo/H2/VV/VV01/fisknv_md.px"
.to_string(),
log_file_path: Some("hagfish.log".to_string()),
}
}
}
// ---------------------------------------------------------------------------
// Metadata types (GET response)
// ---------------------------------------------------------------------------
/// Metadata response from PX-Web API GET request.
///
/// Confirmed structure from live API:
/// ```json
/// {
/// "title": "AVR01010 ...",
/// "variables": [
/// {
/// "code": "measure",
/// "text": "mát",
/// "role": null,
/// "values": ["MASS", "VALUE"],
/// "valueTexts": ["Nøgd", "Virði"]
/// }
/// ]
/// }
/// ```
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MetadataResponse {
#[serde(default)]
pub title: Option<String>,
pub variables: Vec<VariableMeta>,
}
/// Variable metadata describing a dimension in the PX-Web table.
///
/// PxWeb v1 uses parallel string arrays for codes and labels.
/// The `role` field is null on this endpoint — do not rely on it.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct VariableMeta {
/// Variable identifier used in queries (e.g. "month", "measure",
/// "Species (ASFIS2022)")
pub code: String,
/// Human-readable label in the queried language
#[serde(default)]
pub text: String,
/// Role classification — null on this endpoint
#[serde(default)]
pub role: Option<String>,
/// Code values for this variable (parallel with valueTexts)
#[serde(default)]
pub values: Vec<String>,
/// Display labels for each value (parallel with values)
#[serde(default, rename = "valueTexts")]
pub value_texts: Vec<String>,
/// Catch-all for unknown fields
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl VariableMeta {
/// Build a code→label lookup map from the parallel arrays.
pub fn lookup_map(&self) -> HashMap<String, String> {
self.values
.iter()
.zip(self.value_texts.iter())
.map(|(code, label)| (code.clone(), label.clone()))
.collect()
}
}
// ---------------------------------------------------------------------------
// Query types (POST request body)
// ---------------------------------------------------------------------------
/// Query body sent to PX-Web API POST endpoint.
///
/// Confirmed format:
/// ```json
/// {
/// "query": [{"code": "month", "selection": {"filter": "item", "values": [...]}}],
/// "response": {"format": "json-stat2"}
/// }
/// ```
#[derive(Debug, Clone, Serialize)]
pub struct Query {
pub query: Vec<QueryItem>,
pub response: QueryResponse,
}
/// Single query item representing a dimension selection.
#[derive(Debug, Clone, Serialize)]
pub struct QueryItem {
pub code: String,
pub selection: Selection,
}
/// Selection within a query item.
#[derive(Debug, Clone, Serialize)]
pub struct Selection {
/// "item" for explicit values, "all" for wildcard, "top" for top-N
pub filter: String,
/// Values to select. For "all" filter, use ["*"].
pub values: Vec<String>,
}
/// Response format specification.
#[derive(Debug, Clone, Serialize)]
pub struct QueryResponse {
pub format: String,
}
impl Default for QueryResponse {
fn default() -> Self {
Self {
format: "json-stat2".to_string(),
}
}
}
// ---------------------------------------------------------------------------
// Data response types (JSON-stat2)
// ---------------------------------------------------------------------------
/// Raw data response from PX-Web API POST request.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DataResponse {
pub dataset: Dataset,
}
/// Dataset wrapper containing dimensions and actual values.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Dataset {
pub dimension: DimInfo,
pub value: Vec<Option<f64>>,
/// Status codes per value (optional)
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub status: Vec<String>,
}
/// Dimension metadata in JSON-stat2 format.
///
/// Uses a flat `id` array for ordering and a `size` array for cardinality.
/// Each dimension is keyed by its code in the `dimensions` map.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DimInfo {
/// Ordered dimension IDs (defines cube layout)
#[serde(default)]
pub id: Vec<String>,
/// Size of each dimension
#[serde(default)]
pub size: Vec<usize>,
/// One entry per dimension, keyed by dimension code
#[serde(flatten)]
pub dimensions: HashMap<String, Dimension>,
}
/// A single dimension in the JSON-stat2 response.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Dimension {
/// Display label
#[serde(default)]
pub label: String,
/// Category info with index and label maps
pub category: CategoryInfo,
}
/// Category info containing index and label maps.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CategoryInfo {
/// Maps category code → numeric position in the dimension
pub index: HashMap<String, usize>,
/// Maps category code → display label
#[serde(default)]
pub label: HashMap<String, String>,
}
// ---------------------------------------------------------------------------
// Internal data models
// ---------------------------------------------------------------------------
/// Decoded row of landing data with labeled dimensions.
#[derive(Debug, Clone)]
pub struct DataRow {
pub month: String,
pub species_code: String,
pub species_label: String,
pub gear_code: String,
pub gear_label: String,
pub zone_code: String,
pub zone_label: String,
pub processing_code: String,
pub processing_label: String,
pub preservation_code: String,
pub preservation_label: String,
pub shipsize_code: String,
pub shipsize_label: String,
pub measure_code: String,
pub measure_label: String,
pub value: Option<f64>,
}
/// Structured representation of a single landing record for DB insertion.
#[derive(Debug, Clone)]
pub struct Landing {
pub month: String,
pub species_code: String,
pub species_label: String,
pub gear_code: String,
pub zone_code: String,
pub processing_code: String,
pub preservation_code: String,
pub shipsize_code: String,
pub measure_code: String,
pub value: Option<f64>,
}
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
/// Error types for ingestion module.
#[derive(Debug, thiserror::Error)]
pub enum IngestError {
#[error("HTTP request failed: {0}")]
HttpError(#[from] reqwest::Error),
#[error("JSON parsing failed: {0}")]
JsonError(#[from] serde_json::Error),
#[error("API returned status {status}: {body}")]
ApiStatus { status: u16, body: String },
#[error("Missing dimension in response: {0}")]
MissingDimension(String),
#[error("Invalid value code: {0}")]
InvalidValueCode(String),
#[error("API returned empty data set")]
EmptyDataset,
#[error("Unicode decode error: {0}")]
UnicodeError(String),
}
// ---------------------------------------------------------------------------
// Type aliases
// ---------------------------------------------------------------------------
/// Lookup map: dimension_code → (value_code → display_label).
pub type LookupMap = HashMap<String, HashMap<String, String>>;
/// Result alias using custom error type.
pub type Result<T> = std::result::Result<T, IngestError>;