//! 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, } 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, pub variables: Vec, } /// 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, /// Code values for this variable (parallel with valueTexts) #[serde(default)] pub values: Vec, /// Display labels for each value (parallel with values) #[serde(default, rename = "valueTexts")] pub value_texts: Vec, /// Catch-all for unknown fields #[serde(flatten)] pub extra: HashMap, } impl VariableMeta { /// Build a code→label lookup map from the parallel arrays. pub fn lookup_map(&self) -> HashMap { 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, 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, } /// 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>, /// Status codes per value (optional) #[serde(default, skip_serializing_if = "Vec::is_empty")] pub status: Vec, } /// 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, /// Size of each dimension #[serde(default)] pub size: Vec, /// One entry per dimension, keyed by dimension code #[serde(flatten)] pub dimensions: HashMap, } /// 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, /// Maps category code → display label #[serde(default)] pub label: HashMap, } // --------------------------------------------------------------------------- // 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, } /// 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, } // --------------------------------------------------------------------------- // 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>; /// Result alias using custom error type. pub type Result = std::result::Result;