From 042993fafc97e728d8e0b50da0994046184c57cb Mon Sep 17 00:00:00 2001 From: Priec Date: Mon, 7 Sep 2026 10:17:06 +0200 Subject: [PATCH] strings not used internally10 --- AGENTS.md | 6 + client | 2 +- client-gui2 | 2 +- common/proto/ecb.proto | 17 ++- common/proto/table_script.proto | 42 +++++-- common/src/lib.rs | 1 + common/src/proto/descriptor.bin | Bin 250444 -> 252865 bytes common/src/proto/komp_ac.ecb.rs | 71 ++++++++++- common/src/proto/komp_ac.table_script.rs | 147 +++++++++++++++++++++-- common/src/script_operations.rs | 134 +++++++++++++++++++++ server | 2 +- web/src/pages/admin/ecb/loader.rs | 13 +- web/src/pages/admin/ecb/state.rs | 8 +- web/src/pages/admin/ecb/ui.rs | 16 +-- 14 files changed, 417 insertions(+), 44 deletions(-) create mode 100644 common/src/script_operations.rs diff --git a/AGENTS.md b/AGENTS.md index 991b1524..51dd9027 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,12 @@ - Do not run builds, checks, linters, or pre-existing tests. Only run tests that an agent wrote or modified as part of the current task; if the agent did not write or modify any tests, run no tests. - Check if what you are doing is running. Server can be running, tauri app might be running. No need to turn on redundant systems. +## Typed domain values + +- Use enums/domain types for states, kinds, policies, and operations. Parse strings and protobuf integers at boundaries, reject invalid values, and keep internal logic typed. +- Use typed protobuf contracts too; update affected clients, bindings, and descriptors together. Breaking changes are acceptable; add compatibility only when explicitly requested. +- Keep strings for free text, names, storage, language syntax, and display. Diagnostic labels must never control business logic. + ## Disk constraints This workspace is heavily disk constrained. Do not run any command that may create a new or substantially different artifact graph unless the user explicitly authorizes the disk cost. diff --git a/client b/client index dc88add4..0ab21a07 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit dc88add49e3cdb51f832d6768465a90b94a8ad51 +Subproject commit 0ab21a0764728c47baa6c5ebf2fc51ea7d35f1e7 diff --git a/client-gui2 b/client-gui2 index 965938b5..e7151989 160000 --- a/client-gui2 +++ b/client-gui2 @@ -1 +1 @@ -Subproject commit 965938b510f238bd848fcccf68686ad1b98a0ad3 +Subproject commit e715198949162cf08be9570995036931c81c67a7 diff --git a/common/proto/ecb.proto b/common/proto/ecb.proto index 826fd3c1..d50daa16 100644 --- a/common/proto/ecb.proto +++ b/common/proto/ecb.proto @@ -11,9 +11,22 @@ service EcbService { message GetEcbPipelineStatusRequest { int32 batch_limit = 1; } +enum ImportBatchStatus { + IMPORT_BATCH_STATUS_UNSPECIFIED = 0; + IMPORT_BATCH_STATUS_RUNNING = 1; + IMPORT_BATCH_STATUS_SUCCEEDED = 2; + IMPORT_BATCH_STATUS_FAILED = 3; +} + +enum CurrencyCoverageStatus { + CURRENCY_COVERAGE_STATUS_UNSPECIFIED = 0; + CURRENCY_COVERAGE_STATUS_PENDING = 1; + CURRENCY_COVERAGE_STATUS_COMPLETE = 2; +} + message EcbImportBatch { int64 batch_id = 1; - string status = 2; + ImportBatchStatus status = 2; string requested_from = 3; string requested_through = 4; string endpoint = 5; @@ -27,7 +40,7 @@ message EcbImportBatch { message EcbCurrencyCoverage { string currency = 1; - string status = 2; + CurrencyCoverageStatus status = 2; optional string verified_from_date = 3; optional string verified_through_date = 4; } diff --git a/common/proto/table_script.proto b/common/proto/table_script.proto index fe6901b5..eaa1bbca 100644 --- a/common/proto/table_script.proto +++ b/common/proto/table_script.proto @@ -167,16 +167,44 @@ message StoredTableScript { repeated ScriptDependency dependencies = 6; } +enum ScriptDependencyKind { + SCRIPT_DEPENDENCY_KIND_UNSPECIFIED = 0; + SCRIPT_DEPENDENCY_KIND_COLUMN_ACCESS = 1; + SCRIPT_DEPENDENCY_KIND_RELATED_AGGREGATE = 2; + SCRIPT_DEPENDENCY_KIND_LEDGER_EFFECT = 3; +} + +enum AggregateOperation { + AGGREGATE_OPERATION_UNSPECIFIED = 0; + AGGREGATE_OPERATION_SUM = 1; + AGGREGATE_OPERATION_MIN = 2; + AGGREGATE_OPERATION_MAX = 3; + AGGREGATE_OPERATION_COUNT = 4; + AGGREGATE_OPERATION_COUNT_DISTINCT = 5; + AGGREGATE_OPERATION_ANY = 6; + AGGREGATE_OPERATION_ALL = 7; + AGGREGATE_OPERATION_COUNT_ROWS = 8; + AGGREGATE_OPERATION_EXISTS = 9; +} + +enum LedgerEffectOperation { + LEDGER_EFFECT_OPERATION_UNSPECIFIED = 0; + LEDGER_EFFECT_OPERATION_ADD = 1; + LEDGER_EFFECT_OPERATION_SUBTRACT = 2; + LEDGER_EFFECT_OPERATION_BOOLEAN = 3; +} + message ScriptDependency { // Logical table name referenced by the script. string target_table = 1; - // Normalized dependency kind, such as column_access or related_aggregate. - string dependency_type = 2; + ScriptDependencyKind dependency_type = 2; // Logical column name. Empty for aggregates that operate on rows only. string column = 3; - // Aggregate operation name, such as sum, count_rows, or exists; empty for - // column_access dependencies. - string operation = 4; + // Absent for column access; otherwise matches the dependency kind. + oneof operation { + AggregateOperation aggregate_operation = 4; + LedgerEffectOperation ledger_operation = 8; + } // Relationship table used to match the owner row to the related collection. string via_table = 5; // Column of the current row holding the referenced row's id: the link this @@ -224,9 +252,7 @@ message HydratedColumnValue { // One declared related-collection aggregate input for the client Steel context. message HydratedAggregateValue { - // Normalized aggregate operation: sum, min, max, count, count_distinct, - // any, all, count_rows, or exists. - string operation = 1; + AggregateOperation operation = 1; // Logical table whose related rows were aggregated. string target_table = 2; // Logical aggregated column; empty for count_rows and exists. diff --git a/common/src/lib.rs b/common/src/lib.rs index c0ea8548..5ae9ede4 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -7,6 +7,7 @@ pub mod decimal; pub mod grpc_error; pub mod money; pub mod relationship; +pub mod script_operations; pub mod system_column; pub mod typst_contract; diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 37f665a61508926e44ca426bc3bcfcbe25120fae..091b44a1bdb8cf59f7f848df1511015335aa033e 100644 GIT binary patch delta 6040 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, #[prost(string, optional, tag = "4")] @@ -73,6 +73,69 @@ pub struct GetEcbPipelineStatusResponse { #[prost(message, repeated, tag = "11")] pub currency_coverage: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ImportBatchStatus { + Unspecified = 0, + Running = 1, + Succeeded = 2, + Failed = 3, +} +impl ImportBatchStatus { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "IMPORT_BATCH_STATUS_UNSPECIFIED", + Self::Running => "IMPORT_BATCH_STATUS_RUNNING", + Self::Succeeded => "IMPORT_BATCH_STATUS_SUCCEEDED", + Self::Failed => "IMPORT_BATCH_STATUS_FAILED", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "IMPORT_BATCH_STATUS_UNSPECIFIED" => Some(Self::Unspecified), + "IMPORT_BATCH_STATUS_RUNNING" => Some(Self::Running), + "IMPORT_BATCH_STATUS_SUCCEEDED" => Some(Self::Succeeded), + "IMPORT_BATCH_STATUS_FAILED" => Some(Self::Failed), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum CurrencyCoverageStatus { + Unspecified = 0, + Pending = 1, + Complete = 2, +} +impl CurrencyCoverageStatus { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "CURRENCY_COVERAGE_STATUS_UNSPECIFIED", + Self::Pending => "CURRENCY_COVERAGE_STATUS_PENDING", + Self::Complete => "CURRENCY_COVERAGE_STATUS_COMPLETE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "CURRENCY_COVERAGE_STATUS_UNSPECIFIED" => Some(Self::Unspecified), + "CURRENCY_COVERAGE_STATUS_PENDING" => Some(Self::Pending), + "CURRENCY_COVERAGE_STATUS_COMPLETE" => Some(Self::Complete), + _ => None, + } + } +} /// Generated client implementations. pub mod ecb_service_client { #![allow( diff --git a/common/src/proto/komp_ac.table_script.rs b/common/src/proto/komp_ac.table_script.rs index 8fb2c81f..9690dd09 100644 --- a/common/src/proto/komp_ac.table_script.rs +++ b/common/src/proto/komp_ac.table_script.rs @@ -117,16 +117,11 @@ pub struct ScriptDependency { /// Logical table name referenced by the script. #[prost(string, tag = "1")] pub target_table: ::prost::alloc::string::String, - /// Normalized dependency kind, such as column_access or related_aggregate. - #[prost(string, tag = "2")] - pub dependency_type: ::prost::alloc::string::String, + #[prost(enumeration = "ScriptDependencyKind", tag = "2")] + pub dependency_type: i32, /// Logical column name. Empty for aggregates that operate on rows only. #[prost(string, tag = "3")] pub column: ::prost::alloc::string::String, - /// Aggregate operation name, such as sum, count_rows, or exists; empty for - /// column_access dependencies. - #[prost(string, tag = "4")] - pub operation: ::prost::alloc::string::String, /// Relationship table used to match the owner row to the related collection. #[prost(string, tag = "5")] pub via_table: ::prost::alloc::string::String, @@ -142,6 +137,21 @@ pub struct ScriptDependency { /// runtime looks its inputs up by this text. #[prost(string, tag = "7")] pub name_in_script: ::prost::alloc::string::String, + /// Absent for column access; otherwise matches the dependency kind. + #[prost(oneof = "script_dependency::Operation", tags = "4, 8")] + pub operation: ::core::option::Option, +} +/// Nested message and enum types in `ScriptDependency`. +pub mod script_dependency { + /// Absent for column access; otherwise matches the dependency kind. + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum Operation { + #[prost(enumeration = "super::AggregateOperation", tag = "4")] + AggregateOperation(i32), + #[prost(enumeration = "super::LedgerEffectOperation", tag = "8")] + LedgerOperation(i32), + } } /// Identifies the active form row whose external Steel inputs must be hydrated. #[derive(serde::Serialize, serde::Deserialize)] @@ -193,10 +203,8 @@ pub struct HydratedColumnValue { #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct HydratedAggregateValue { - /// Normalized aggregate operation: sum, min, max, count, count_distinct, - /// any, all, count_rows, or exists. - #[prost(string, tag = "1")] - pub operation: ::prost::alloc::string::String, + #[prost(enumeration = "AggregateOperation", tag = "1")] + pub operation: i32, /// Logical table whose related rows were aggregated. #[prost(string, tag = "2")] pub target_table: ::prost::alloc::string::String, @@ -227,6 +235,123 @@ pub struct HydrateScriptDependenciesResponse { #[prost(message, repeated, tag = "2")] pub aggregates: ::prost::alloc::vec::Vec, } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ScriptDependencyKind { + Unspecified = 0, + ColumnAccess = 1, + RelatedAggregate = 2, + LedgerEffect = 3, +} +impl ScriptDependencyKind { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "SCRIPT_DEPENDENCY_KIND_UNSPECIFIED", + Self::ColumnAccess => "SCRIPT_DEPENDENCY_KIND_COLUMN_ACCESS", + Self::RelatedAggregate => "SCRIPT_DEPENDENCY_KIND_RELATED_AGGREGATE", + Self::LedgerEffect => "SCRIPT_DEPENDENCY_KIND_LEDGER_EFFECT", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "SCRIPT_DEPENDENCY_KIND_UNSPECIFIED" => Some(Self::Unspecified), + "SCRIPT_DEPENDENCY_KIND_COLUMN_ACCESS" => Some(Self::ColumnAccess), + "SCRIPT_DEPENDENCY_KIND_RELATED_AGGREGATE" => Some(Self::RelatedAggregate), + "SCRIPT_DEPENDENCY_KIND_LEDGER_EFFECT" => Some(Self::LedgerEffect), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum AggregateOperation { + Unspecified = 0, + Sum = 1, + Min = 2, + Max = 3, + Count = 4, + CountDistinct = 5, + Any = 6, + All = 7, + CountRows = 8, + Exists = 9, +} +impl AggregateOperation { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "AGGREGATE_OPERATION_UNSPECIFIED", + Self::Sum => "AGGREGATE_OPERATION_SUM", + Self::Min => "AGGREGATE_OPERATION_MIN", + Self::Max => "AGGREGATE_OPERATION_MAX", + Self::Count => "AGGREGATE_OPERATION_COUNT", + Self::CountDistinct => "AGGREGATE_OPERATION_COUNT_DISTINCT", + Self::Any => "AGGREGATE_OPERATION_ANY", + Self::All => "AGGREGATE_OPERATION_ALL", + Self::CountRows => "AGGREGATE_OPERATION_COUNT_ROWS", + Self::Exists => "AGGREGATE_OPERATION_EXISTS", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "AGGREGATE_OPERATION_UNSPECIFIED" => Some(Self::Unspecified), + "AGGREGATE_OPERATION_SUM" => Some(Self::Sum), + "AGGREGATE_OPERATION_MIN" => Some(Self::Min), + "AGGREGATE_OPERATION_MAX" => Some(Self::Max), + "AGGREGATE_OPERATION_COUNT" => Some(Self::Count), + "AGGREGATE_OPERATION_COUNT_DISTINCT" => Some(Self::CountDistinct), + "AGGREGATE_OPERATION_ANY" => Some(Self::Any), + "AGGREGATE_OPERATION_ALL" => Some(Self::All), + "AGGREGATE_OPERATION_COUNT_ROWS" => Some(Self::CountRows), + "AGGREGATE_OPERATION_EXISTS" => Some(Self::Exists), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum LedgerEffectOperation { + Unspecified = 0, + Add = 1, + Subtract = 2, + Boolean = 3, +} +impl LedgerEffectOperation { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "LEDGER_EFFECT_OPERATION_UNSPECIFIED", + Self::Add => "LEDGER_EFFECT_OPERATION_ADD", + Self::Subtract => "LEDGER_EFFECT_OPERATION_SUBTRACT", + Self::Boolean => "LEDGER_EFFECT_OPERATION_BOOLEAN", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "LEDGER_EFFECT_OPERATION_UNSPECIFIED" => Some(Self::Unspecified), + "LEDGER_EFFECT_OPERATION_ADD" => Some(Self::Add), + "LEDGER_EFFECT_OPERATION_SUBTRACT" => Some(Self::Subtract), + "LEDGER_EFFECT_OPERATION_BOOLEAN" => Some(Self::Boolean), + _ => None, + } + } +} /// Generated client implementations. pub mod table_script_client { #![allow( diff --git a/common/src/script_operations.rs b/common/src/script_operations.rs new file mode 100644 index 00000000..b261706d --- /dev/null +++ b/common/src/script_operations.rs @@ -0,0 +1,134 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AggregateOperation { + Sum, + Min, + Max, + Count, + CountDistinct, + Any, + All, + CountRows, + Exists, +} + +impl AggregateOperation { + pub fn as_str(self) -> &'static str { + match self { + Self::Sum => "sum", + Self::Min => "min", + Self::Max => "max", + Self::Count => "count", + Self::CountDistinct => "count_distinct", + Self::Any => "any", + Self::All => "all", + Self::CountRows => "count_rows", + Self::Exists => "exists", + } + } +} + +impl std::str::FromStr for AggregateOperation { + type Err = serde::de::value::Error; + + fn from_str(value: &str) -> Result { + Self::deserialize(serde::de::value::StrDeserializer::new(value)) + } +} + +impl std::fmt::Display for AggregateOperation { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum LedgerEffectOperation { + Add, + #[serde(rename = "sub")] + Subtract, + Boolean, +} + +impl LedgerEffectOperation { + pub fn as_str(self) -> &'static str { + match self { + Self::Add => "add", + Self::Subtract => "sub", + Self::Boolean => "boolean", + } + } +} + +impl std::fmt::Display for LedgerEffectOperation { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +impl From for crate::proto::komp_ac::table_script::AggregateOperation { + fn from(operation: AggregateOperation) -> Self { + match operation { + AggregateOperation::Sum => Self::Sum, + AggregateOperation::Min => Self::Min, + AggregateOperation::Max => Self::Max, + AggregateOperation::Count => Self::Count, + AggregateOperation::CountDistinct => Self::CountDistinct, + AggregateOperation::Any => Self::Any, + AggregateOperation::All => Self::All, + AggregateOperation::CountRows => Self::CountRows, + AggregateOperation::Exists => Self::Exists, + } + } +} + +impl TryFrom for AggregateOperation { + type Error = serde::de::value::Error; + + fn try_from(value: i32) -> Result { + use crate::proto::komp_ac::table_script::AggregateOperation as ProtoOperation; + match ProtoOperation::try_from(value) { + Ok(ProtoOperation::Sum) => Ok(Self::Sum), + Ok(ProtoOperation::Min) => Ok(Self::Min), + Ok(ProtoOperation::Max) => Ok(Self::Max), + Ok(ProtoOperation::Count) => Ok(Self::Count), + Ok(ProtoOperation::CountDistinct) => Ok(Self::CountDistinct), + Ok(ProtoOperation::Any) => Ok(Self::Any), + Ok(ProtoOperation::All) => Ok(Self::All), + Ok(ProtoOperation::CountRows) => Ok(Self::CountRows), + Ok(ProtoOperation::Exists) => Ok(Self::Exists), + _ => Err(serde::de::Error::custom(format!( + "Invalid AggregateOperation: {value}" + ))), + } + } +} + +impl From for crate::proto::komp_ac::table_script::LedgerEffectOperation { + fn from(operation: LedgerEffectOperation) -> Self { + match operation { + LedgerEffectOperation::Add => Self::Add, + LedgerEffectOperation::Subtract => Self::Subtract, + LedgerEffectOperation::Boolean => Self::Boolean, + } + } +} + +impl TryFrom for LedgerEffectOperation { + type Error = serde::de::value::Error; + + fn try_from(value: i32) -> Result { + use crate::proto::komp_ac::table_script::LedgerEffectOperation as ProtoOperation; + match ProtoOperation::try_from(value) { + Ok(ProtoOperation::Add) => Ok(Self::Add), + Ok(ProtoOperation::Subtract) => Ok(Self::Subtract), + Ok(ProtoOperation::Boolean) => Ok(Self::Boolean), + _ => Err(serde::de::Error::custom(format!( + "Invalid LedgerEffectOperation: {value}" + ))), + } + } +} diff --git a/server b/server index 8dc36863..6d6f241b 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 8dc3686366c04119f37a896eddace84c8be97808 +Subproject commit 6d6f241b0b290e0fed5522be0c61a6d1dd4ff8a5 diff --git a/web/src/pages/admin/ecb/loader.rs b/web/src/pages/admin/ecb/loader.rs index eb1fc765..3f5935e7 100644 --- a/web/src/pages/admin/ecb/loader.rs +++ b/web/src/pages/admin/ecb/loader.rs @@ -71,9 +71,14 @@ pub(crate) async fn load_ecb_page( batches: status .batches .into_iter() - .map(|batch| ImportBatchView { + .map(|batch| Ok(ImportBatchView { batch_id: batch.batch_id, - status: batch.status, + status: match crate::ecb::ImportBatchStatus::try_from(batch.status) { + Ok(status @ (crate::ecb::ImportBatchStatus::Running + | crate::ecb::ImportBatchStatus::Succeeded + | crate::ecb::ImportBatchStatus::Failed)) => status, + _ => return Err(LoadError::Backend(format!("Invalid import batch status: {}", batch.status))), + }, requested_from: batch.requested_from, requested_through: batch.requested_through, endpoint: batch.endpoint, @@ -83,7 +88,7 @@ pub(crate) async fn load_ecb_page( observation_count: batch.observation_count, inserted_observation_count: batch.inserted_observation_count, error_message: batch.error_message, - }) - .collect(), + })) + .collect::, LoadError>>()?, }) } diff --git a/web/src/pages/admin/ecb/state.rs b/web/src/pages/admin/ecb/state.rs index cdb69bf9..3ee2a5a3 100644 --- a/web/src/pages/admin/ecb/state.rs +++ b/web/src/pages/admin/ecb/state.rs @@ -77,7 +77,7 @@ fn from_now(locale: Locale, raw: &str) -> Option { /// One import attempt, as the audit log recorded it. pub(crate) struct ImportBatchView { pub batch_id: i64, - pub status: String, + pub status: crate::ecb::ImportBatchStatus, pub requested_from: String, pub requested_through: String, pub endpoint: String, @@ -91,15 +91,15 @@ pub(crate) struct ImportBatchView { impl ImportBatchView { pub(crate) fn succeeded(&self) -> bool { - self.status == "succeeded" + self.status == crate::ecb::ImportBatchStatus::Succeeded } pub(crate) fn running(&self) -> bool { - self.status == "running" + self.status == crate::ecb::ImportBatchStatus::Running } pub(crate) fn failed(&self) -> bool { - self.status == "failed" + self.status == crate::ecb::ImportBatchStatus::Failed } pub(crate) fn started(&self) -> String { diff --git a/web/src/pages/admin/ecb/ui.rs b/web/src/pages/admin/ecb/ui.rs index bbf80e73..9f5f4436 100644 --- a/web/src/pages/admin/ecb/ui.rs +++ b/web/src/pages/admin/ecb/ui.rs @@ -63,10 +63,10 @@ mod tests { use super::*; use crate::pages::admin::ecb::state::ImportBatchView; - fn batch(id: i64, status: &str) -> ImportBatchView { + fn batch(id: i64, status: crate::ecb::ImportBatchStatus) -> ImportBatchView { ImportBatchView { batch_id: id, - status: status.to_string(), + status, requested_from: "2026-08-10".to_string(), requested_through: "2026-08-12".to_string(), endpoint: "https://data.ecb.europa.eu/...".to_string(), @@ -87,7 +87,7 @@ mod tests { days_behind: 0, import_running: false, next_import_at: "2026-08-13T15:00:00Z".to_string(), - batches: vec![batch(9, "succeeded")], + batches: vec![batch(9, crate::ecb::ImportBatchStatus::Succeeded)], covered_currencies: vec!["CZK".to_string(), "USD".to_string()], transactions_postable_through: Some("2026-08-13".to_string()), statements_postable_through: Some("2026-08-12".to_string()), @@ -143,8 +143,8 @@ mod tests { #[test] fn historical_failures_are_collapsed_as_technical_history() { let mut page = healthy_page(); - page.batches.push(batch(8, "failed")); - page.batches.push(batch(7, "failed")); + page.batches.push(batch(8, crate::ecb::ImportBatchStatus::Failed)); + page.batches.push(batch(7, crate::ecb::ImportBatchStatus::Failed)); let html = render_page(&page); @@ -196,7 +196,7 @@ mod tests { fn a_running_import_polls_until_it_finishes() { let mut page = healthy_page(); page.import_running = true; - page.batches.insert(0, batch(10, "running")); + page.batches.insert(0, batch(10, crate::ecb::ImportBatchStatus::Running)); let html = render_page(&page); assert!(!html.contains("Template error"), "{html}"); @@ -219,13 +219,13 @@ mod tests { let mut page = healthy_page(); page.healthy = false; page.batches = vec![ImportBatchView { - status: "failed".to_string(), + status: crate::ecb::ImportBatchStatus::Failed, completed_at: None, verified_through_date: None, observation_count: None, inserted_observation_count: None, error_message: Some("the ECB endpoint returned 503".to_string()), - ..batch(11, "failed") + ..batch(11, crate::ecb::ImportBatchStatus::Failed) }]; let html = render_page(&page);