From 47ffc24c0677f025a205965fb916cd403d4af876 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 6 Aug 2026 10:52:42 +0200 Subject: [PATCH] refactor(tui): Replace StructTable with Value rendering Collapse all 25 TUI resource Item types to serde_json::Value, rendered purely from config.yaml at runtime. Deletes the StructTable trait, ResourceKey trait, impl_dynamic_item!, ColumnSpec, and the wide mode config surface (no UI toggle existed). Adds view_render free functions (headers/row/status/id_field) operating on &ViewConfig + &Value. Backfills config.yaml from #[structable] attributes scraped from each resource's openstack_types struct. Downstream changes: filter_carry_action, TryFrom delete builders, and test helpers now take Option<&Value> and read fields via view_render::get_str instead of typed access. Signed-off-by: Artem Goncharov --- Cargo.lock | 1 - openstack_tui/.config/config.yaml | 14 +- openstack_tui/Cargo.toml | 1 - openstack_tui/src/cloud_worker/common.rs | 24 -- openstack_tui/src/components.rs | 3 +- openstack_tui/src/components/block_storage.rs | 1 + .../src/components/block_storage/backups.rs | 2 - .../src/components/block_storage/generated.rs | 19 ++ .../src/components/block_storage/snapshots.rs | 2 - .../src/components/block_storage/volumes.rs | 33 +-- openstack_tui/src/components/compute.rs | 1 + .../src/components/compute/aggregates.rs | 2 - .../src/components/compute/flavors.rs | 59 +--- .../src/components/compute/generated.rs | 19 ++ .../src/components/compute/hypervisors.rs | 52 ---- .../compute/server_instance_action_events.rs | 44 --- .../compute/server_instance_actions.rs | 28 +- .../src/components/compute/servers.rs | 80 ++---- openstack_tui/src/components/dns.rs | 1 + openstack_tui/src/components/dns/generated.rs | 19 ++ .../src/components/dns/recordsets.rs | 8 - openstack_tui/src/components/dns/zones.rs | 47 ++-- openstack_tui/src/components/dynamic_item.rs | 255 ------------------ .../src/components/generic_resource_view.rs | 21 +- openstack_tui/src/components/identity.rs | 1 + .../identity/application_credentials.rs | 8 - .../src/components/identity/generated.rs | 19 ++ .../src/components/identity/group_users.rs | 8 - .../src/components/identity/groups.rs | 47 ++-- .../src/components/identity/projects.rs | 25 +- .../src/components/identity/users.rs | 58 ++-- openstack_tui/src/components/image.rs | 1 + .../src/components/image/generated.rs | 19 ++ openstack_tui/src/components/image/images.rs | 66 +---- openstack_tui/src/components/load_balancer.rs | 1 + .../src/components/load_balancer/generated.rs | 19 ++ .../load_balancer/health_monitors.rs | 8 - .../src/components/load_balancer/listeners.rs | 8 - .../components/load_balancer/loadbalancers.rs | 42 ++- .../components/load_balancer/pool_members.rs | 8 - .../src/components/load_balancer/pools.rs | 42 ++- .../components/network/generated/network.rs | 2 - .../components/network/generated/router.rs | 2 - .../network/generated/security_group.rs | 41 --- .../network/generated/security_group_rule.rs | 53 ---- .../components/network/generated/subnet.rs | 37 --- .../src/components/network/networks.rs | 30 +-- .../src/components/network/routers.rs | 8 - .../network/security_group_rules.rs | 24 +- .../src/components/network/security_groups.rs | 30 +-- .../src/components/network/subnets.rs | 8 - .../src/components/resource_behaviour.rs | 131 ++------- .../src/components/resource_key_impls.rs | 67 ----- openstack_tui/src/components/table_view.rs | 149 ++++------ openstack_tui/src/components/view_render.rs | 191 +++++++++++++ openstack_tui/src/config.rs | 99 ++----- openstack_tui/src/utils.rs | 5 - 57 files changed, 624 insertions(+), 1369 deletions(-) create mode 100644 openstack_tui/src/components/block_storage/generated.rs create mode 100644 openstack_tui/src/components/compute/generated.rs create mode 100644 openstack_tui/src/components/dns/generated.rs delete mode 100644 openstack_tui/src/components/dynamic_item.rs create mode 100644 openstack_tui/src/components/identity/generated.rs create mode 100644 openstack_tui/src/components/image/generated.rs create mode 100644 openstack_tui/src/components/load_balancer/generated.rs delete mode 100644 openstack_tui/src/components/resource_key_impls.rs create mode 100644 openstack_tui/src/components/view_render.rs diff --git a/Cargo.lock b/Cargo.lock index b51ebc17e..ed831ffb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3856,7 +3856,6 @@ dependencies = [ "lazy_static", "open", "openstack_sdk", - "openstack_types", "pretty_assertions", "ratatui", "secrecy", diff --git a/openstack_tui/.config/config.yaml b/openstack_tui/.config/config.yaml index 4d090c15e..4349d0e04 100644 --- a/openstack_tui/.config/config.yaml +++ b/openstack_tui/.config/config.yaml @@ -357,12 +357,11 @@ mode_aliases: # View output # : # fields: -# wide: true views: # Block Storage block_storage.backup: default_fields: [id, name, az, size, status, created_at] - block_storage.snapshots: + block_storage.snapshot: default_fields: [id, name, status, created_at] block_storage.volume: default_fields: [id, name, az, size, status, updated_at] @@ -371,10 +370,8 @@ views: default_fields: [name, uuid, az, updated_at] compute.flavor: default_fields: [id, name, vcpus, ram, disk, swap] - wide_fields: [swap] compute.hypervisor: default_fields: [ip, hostname, status, state] - wide_fields: [vcpus, "memory mb"] status_field: status compute.server/instance_action/event: default_fields: [event, result, start_time, finish_time, host] @@ -382,7 +379,6 @@ views: default_fields: [id, action, message, start_time, user_id] compute.server: default_fields: [id, name, flavor, status, created, updated] - wide_fields: ["task state", "power state", "availability zone"] status_field: status fields: - name: flavor @@ -404,19 +400,23 @@ views: # image image.image: default_fields: [id, name, distro, version, visibility, min_disk, min_ram] - wide_fields: ["disk format", "container format"] status_field: status # load balancer load-balancer.healthmonitor: default_fields: [id, name, status, type] + status_field: operating_status load-balancer.listener: default_fields: [id, name, status, protocol, port] + status_field: operating_status load-balancer.loadbalancer: default_fields: [id, name, status, address] + status_field: operating_status load-balancer.pool/member: default_fields: [id, name, status, port] + status_field: operating_status load-balancer.pool: default_fields: [id, name, status, protocol] + status_field: operating_status # network network.network: default_fields: [id, name, status, description, created_at, updated_at] @@ -427,4 +427,4 @@ views: network.security_group_rule: default_fields: [id, ethertype, direction, protocol, port_range_min, port_range_max, description] network.security_group: - default_fields: [id, name, description, created_at, updated_at, description] + default_fields: [id, name, description, created_at, updated_at] diff --git a/openstack_tui/Cargo.toml b/openstack_tui/Cargo.toml index f1d0e0792..10bc8a675 100644 --- a/openstack_tui/Cargo.toml +++ b/openstack_tui/Cargo.toml @@ -35,7 +35,6 @@ itertools = { workspace = true } lazy_static = "^1.5" open.workspace = true openstack_sdk = { path = "../openstack_sdk", version = "^0.22", default-features = false, features = ["async", "block_storage", "compute", "dns", "identity", "image", "load_balancer", "network"] } -openstack_types = { path = "../openstack_types", version = "^0.22" } pretty_assertions = "^1.4" ratatui = { version = "^0.30", features = ["serde", "macros", "crossterm"] } secrecy = "0.10.3" diff --git a/openstack_tui/src/cloud_worker/common.rs b/openstack_tui/src/cloud_worker/common.rs index 619bab52c..2629d76ed 100644 --- a/openstack_tui/src/cloud_worker/common.rs +++ b/openstack_tui/src/cloud_worker/common.rs @@ -13,10 +13,6 @@ // SPDX-License-Identifier: Apache-2.0 use thiserror::Error; -use openstack_sdk::AsyncOpenStack; -use openstack_sdk::api::{AsyncClient, RestEndpoint, rest_endpoint::negotiate_microversion}; -use openstack_sdk::types::ApiVersion; - use crate::action; pub trait ConfirmableRequest { @@ -25,26 +21,6 @@ pub trait ConfirmableRequest { } } -/// Learn the microversion that will actually be negotiated for `endpoint` against the cloud -/// behind `session`, without sending any request. -/// -/// Reuses `openstack_sdk`'s own `negotiate_microversion` (the same bounds-check logic that picks -/// the version sent in the `OpenStack-API-Version` header) so callers that need to choose a -/// version-appropriate response schema can learn the answer up front, rather than guessing which -/// microversion-specific struct will match the JSON that comes back. -pub(crate) async fn negotiated_version( - session: &AsyncOpenStack, - endpoint: &E, -) -> Result, CloudWorkerError> { - let service_endpoint = session - .get_service_endpoint(&endpoint.service_type(), endpoint.api_version().as_ref()) - .await?; - Ok(negotiate_microversion::( - &service_endpoint, - endpoint, - )?) -} - #[derive(Error, Debug)] pub enum CloudWorkerError { #[error(transparent)] diff --git a/openstack_tui/src/components.rs b/openstack_tui/src/components.rs index 892870875..70f42bf74 100644 --- a/openstack_tui/src/components.rs +++ b/openstack_tui/src/components.rs @@ -30,7 +30,6 @@ pub mod compute; pub mod confirm_popup; pub mod describe; pub mod dns; -pub mod dynamic_item; pub mod error_popup; pub mod generic_resource_view; pub mod header; @@ -42,10 +41,10 @@ pub mod network; pub mod project_select_popup; pub mod region_select_popup; pub mod resource_behaviour; -mod resource_key_impls; // bring ResourceKey impls into scope pub mod resource_select_popup; pub mod resource_table; pub mod table_view; +pub mod view_render; // pub mod modal; // removed – replaced by generic Popup widget pub use crate::widgets::fuzzy_select::{FuzzySelect, FuzzySelectState}; pub use crate::widgets::popup::Popup; diff --git a/openstack_tui/src/components/block_storage.rs b/openstack_tui/src/components/block_storage.rs index a4c472c6d..f0f78cd62 100644 --- a/openstack_tui/src/components/block_storage.rs +++ b/openstack_tui/src/components/block_storage.rs @@ -13,5 +13,6 @@ // SPDX-License-Identifier: Apache-2.0 pub mod backups; +pub(crate) mod generated; pub mod snapshots; pub mod volumes; diff --git a/openstack_tui/src/components/block_storage/backups.rs b/openstack_tui/src/components/block_storage/backups.rs index 0eab363e9..0f54c4644 100644 --- a/openstack_tui/src/components/block_storage/backups.rs +++ b/openstack_tui/src/components/block_storage/backups.rs @@ -19,13 +19,11 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::block_storage::v3::backup::response::list_detailed::BackupResponse; /// Behaviour implementation for BlockStorageBackups. pub struct BlockStorageBackupsBehaviour; impl ResourceBehaviour for BlockStorageBackupsBehaviour { - type Item = BackupResponse; type Filter = BlockStorageBackupList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/block_storage/generated.rs b/openstack_tui/src/components/block_storage/generated.rs new file mode 100644 index 000000000..7f120e09e --- /dev/null +++ b/openstack_tui/src/components/block_storage/generated.rs @@ -0,0 +1,19 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +// WARNING: Individual `pub(crate) mod ;` lines below are automatically generated +// from OpenAPI schema using `openstack-codegenerator`. This scaffold line itself is hand-added, +// once per service, as the anchor new resources' lines get inserted after. + +// GENERATED-ANCHOR: resource mods diff --git a/openstack_tui/src/components/block_storage/snapshots.rs b/openstack_tui/src/components/block_storage/snapshots.rs index 87c1d7f51..7b5d7a4bd 100644 --- a/openstack_tui/src/components/block_storage/snapshots.rs +++ b/openstack_tui/src/components/block_storage/snapshots.rs @@ -19,13 +19,11 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::block_storage::v3::snapshot::response::list_detailed::SnapshotResponse; /// Behaviour implementation for BlockStorageSnapshots. pub struct BlockStorageSnapshotsBehaviour; impl ResourceBehaviour for BlockStorageSnapshotsBehaviour { - type Item = SnapshotResponse; type Filter = BlockStorageSnapshotList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/block_storage/volumes.rs b/openstack_tui/src/components/block_storage/volumes.rs index 9745840e1..c5301935b 100644 --- a/openstack_tui/src/components/block_storage/volumes.rs +++ b/openstack_tui/src/components/block_storage/volumes.rs @@ -21,23 +21,18 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::block_storage::v3::volume::response::list_detailed::VolumeResponse; const VIEW_CONFIG_KEY: &str = "block_storage.volume"; -impl crate::utils::ResourceKey for VolumeResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - -impl TryFrom<&VolumeResponse> for BlockStorageVolumeDelete { +impl TryFrom<&serde_json::Value> for BlockStorageVolumeDelete { type Error = crate::cloud_worker::block_storage::v3::BlockStorageVolumeDeleteBuilderError; - fn try_from(value: &VolumeResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = BlockStorageVolumeDeleteBuilder::default(); - builder.id(value.id.clone()); - if let Some(val) = &value.name { - builder.name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.id(val.to_string()); + } + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.name(val.to_string()); } builder.build() } @@ -46,7 +41,6 @@ impl TryFrom<&VolumeResponse> for BlockStorageVolumeDelete { pub struct BlockStorageVolumesBehaviour; impl ResourceBehaviour for BlockStorageVolumesBehaviour { - type Item = VolumeResponse; type Filter = BlockStorageVolumeList; fn view_key() -> &'static str { @@ -70,7 +64,10 @@ impl ResourceBehaviour for BlockStorageVolumesBehaviour { if matches!(**boxreq, BlockStorageVolumeApiRequest::ListDetailed(_)) ) } - fn confirm_request(action: &Action, selected: Option<&Self::Item>) -> Option { + fn confirm_request( + action: &Action, + selected: Option<&serde_json::Value>, + ) -> Option { if let Action::ResourceOp { key, op: crate::action::ResourceOp::Delete, @@ -93,10 +90,9 @@ pub type BlockStorageVolumes = GenericResourceView<'static, BlockStorageVolumesB mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::block_storage::v3::volume::response::list_detailed::VolumeResponse; - fn make_volume(id: &str, name: &str) -> VolumeResponse { - let json = serde_json::json!({ + fn make_volume(id: &str, name: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "status": "available", @@ -119,8 +115,7 @@ mod tests { "os-vol-mig-status.migration_status": null, "os-vol-host-attr:host": null, "os-vol-tenant-attr:tenant_id": "tenant-1" - }); - serde_json::from_value(json).unwrap() + }) } #[test] diff --git a/openstack_tui/src/components/compute.rs b/openstack_tui/src/components/compute.rs index 905bef111..fb61149c6 100644 --- a/openstack_tui/src/components/compute.rs +++ b/openstack_tui/src/components/compute.rs @@ -14,6 +14,7 @@ pub mod aggregates; pub mod flavors; +pub(crate) mod generated; pub mod hypervisors; pub mod server_instance_action_events; pub mod server_instance_actions; diff --git a/openstack_tui/src/components/compute/aggregates.rs b/openstack_tui/src/components/compute/aggregates.rs index 7c88655a4..e4e8f64a6 100644 --- a/openstack_tui/src/components/compute/aggregates.rs +++ b/openstack_tui/src/components/compute/aggregates.rs @@ -19,13 +19,11 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::compute::v2::aggregate::response::list_241::AggregateResponse; /// Behaviour implementation for ComputeAggregates. pub struct ComputeAggregatesBehaviour; impl ResourceBehaviour for ComputeAggregatesBehaviour { - type Item = AggregateResponse; type Filter = ComputeAggregateList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/compute/flavors.rs b/openstack_tui/src/components/compute/flavors.rs index 1adce083f..805e34439 100644 --- a/openstack_tui/src/components/compute/flavors.rs +++ b/openstack_tui/src/components/compute/flavors.rs @@ -18,7 +18,6 @@ use crate::{ ComputeApiRequest, ComputeFlavorApiRequest, ComputeFlavorList, ComputeServerListBuilder, }, cloud_worker::types::ApiRequest, - components::dynamic_item::{ColumnSpec, impl_dynamic_item}, components::generic_resource_view::GenericResourceView, components::resource_behaviour::{Mutation, ResourceBehaviour}, mode::Mode, @@ -27,54 +26,9 @@ use crate::{ const TITLE: &str = "Compute Flavors"; const VIEW_CONFIG_KEY: &str = "compute.flavor"; -// Flavor's `swap` field changed type (i64 -> i32) at microversion 2.102 -- no single -// `openstack_types` struct correctly represents every microversion, so `Item` reads columns out -// of the raw response by JSON pointer instead of deserializing into a versioned struct. -static FLAVOR_COLUMNS: &[ColumnSpec] = &[ - ColumnSpec { - title: "ID", - pointer: "/id", - wide: false, - status: false, - }, - ColumnSpec { - title: "Name", - pointer: "/name", - wide: false, - status: false, - }, - ColumnSpec { - title: "RAM", - pointer: "/ram", - wide: false, - status: false, - }, - ColumnSpec { - title: "VCPUs", - pointer: "/vcpus", - wide: false, - status: false, - }, - ColumnSpec { - title: "Disk", - pointer: "/disk", - wide: false, - status: false, - }, - ColumnSpec { - title: "Swap", - pointer: "/swap", - wide: true, - status: false, - }, -]; - -impl_dynamic_item!(FlavorItem, VIEW_CONFIG_KEY, FLAVOR_COLUMNS); - pub struct ComputeFlavorsBehaviour; impl ResourceBehaviour for ComputeFlavorsBehaviour { - type Item = FlavorItem; type Filter = ComputeFlavorList; fn view_key() -> &'static str { @@ -114,15 +68,15 @@ impl ResourceBehaviour for ComputeFlavorsBehaviour { fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, _filter: &Self::Filter, ) -> Vec { if let Action::ShowResource(key) = action && *key == crate::mode::COMPUTE_SERVER && let Some(sel) = selected - && let Some(flavor_id) = sel.get_str("/id") + && let Some(flavor_id) = crate::components::view_render::get_str(sel, "/id") && let Ok(server_list) = ComputeServerListBuilder::default() - .flavor(flavor_id) + .flavor(flavor_id.to_string()) .build() { return vec![ @@ -153,8 +107,8 @@ mod tests { use crate::cloud_worker::compute::v2::ComputeServerApiRequest; use crate::components::resource_behaviour::ResourceBehaviour; - fn make_flavor(id: &str) -> FlavorItem { - let json = serde_json::json!({ + fn make_flavor(id: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": "test", "vcpus": 1, @@ -166,8 +120,7 @@ mod tests { "OS-FLV-EXT-DATA:ephemeral": 0, "metadata": {}, "os-flavor-access:is_public": true, - }); - FlavorItem(json) + }) } #[test] diff --git a/openstack_tui/src/components/compute/generated.rs b/openstack_tui/src/components/compute/generated.rs new file mode 100644 index 000000000..7f120e09e --- /dev/null +++ b/openstack_tui/src/components/compute/generated.rs @@ -0,0 +1,19 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +// WARNING: Individual `pub(crate) mod ;` lines below are automatically generated +// from OpenAPI schema using `openstack-codegenerator`. This scaffold line itself is hand-added, +// once per service, as the anchor new resources' lines get inserted after. + +// GENERATED-ANCHOR: resource mods diff --git a/openstack_tui/src/components/compute/hypervisors.rs b/openstack_tui/src/components/compute/hypervisors.rs index 92b382639..b7f7ed8fe 100644 --- a/openstack_tui/src/components/compute/hypervisors.rs +++ b/openstack_tui/src/components/compute/hypervisors.rs @@ -16,68 +16,16 @@ use crate::cloud_worker::compute::v2::{ ComputeApiRequest, ComputeHypervisorApiRequest, ComputeHypervisorList, }; use crate::cloud_worker::types::ApiRequest; -use crate::components::dynamic_item::{ColumnSpec, impl_dynamic_item}; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; const VIEW_CONFIG_KEY: &str = "compute.hypervisor"; -// Hypervisor's `id` field changed type (i32 -> String) at microversion 2.53 -- no single -// `openstack_types` struct correctly represents every microversion, so `Item` reads columns out -// of the raw response by JSON pointer instead of deserializing into a versioned struct. -static HYPERVISOR_COLUMNS: &[ColumnSpec] = &[ - ColumnSpec { - title: "ID", - pointer: "/id", - wide: false, - status: false, - }, - ColumnSpec { - title: "Hostname", - pointer: "/hypervisor_hostname", - wide: false, - status: false, - }, - ColumnSpec { - title: "Type", - pointer: "/hypervisor_type", - wide: false, - status: false, - }, - ColumnSpec { - title: "State", - pointer: "/state", - wide: false, - status: false, - }, - ColumnSpec { - title: "Status", - pointer: "/status", - wide: false, - status: true, - }, - ColumnSpec { - title: "VCPUs", - pointer: "/vcpus", - wide: true, - status: false, - }, - ColumnSpec { - title: "Memory MB", - pointer: "/memory_mb", - wide: true, - status: false, - }, -]; - -impl_dynamic_item!(HypervisorItem, VIEW_CONFIG_KEY, HYPERVISOR_COLUMNS); - /// Behaviour implementation for ComputeHypervisors. pub struct ComputeHypervisorsBehaviour; impl ResourceBehaviour for ComputeHypervisorsBehaviour { - type Item = HypervisorItem; type Filter = ComputeHypervisorList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/compute/server_instance_action_events.rs b/openstack_tui/src/components/compute/server_instance_action_events.rs index dedf62ffc..10d6a0ce4 100644 --- a/openstack_tui/src/components/compute/server_instance_action_events.rs +++ b/openstack_tui/src/components/compute/server_instance_action_events.rs @@ -21,57 +21,13 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use serde::Deserialize; use serde_json::Value; -use structable::{StructTable, StructTableOptions}; const VIEW_CONFIG_KEY: &str = "compute.server/instance_action/event"; -/// Event type -#[derive(Clone, Debug, Deserialize, StructTable)] -pub struct ServerInstanceActionEventData { - /// Even details - #[structable(optional)] - pub details: Option, - - /// Event summary - pub event: String, - - /// Finish time of the event - #[structable(optional)] - pub finish_time: Option, - - /// Hostname - #[structable(optional)] - pub host: Option, - - /// Host ID - #[structable(optional)] - pub host_id: Option, - - /// Result - #[structable(optional)] - pub result: Option, - - /// Event start time - #[structable(optional)] - pub start_time: Option, - - /// Traceback - #[structable(optional)] - pub traceback: Option, -} - -impl crate::utils::ResourceKey for ServerInstanceActionEventData { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - pub struct ComputeServerInstanceActionEventsBehaviour; impl ResourceBehaviour for ComputeServerInstanceActionEventsBehaviour { - type Item = ServerInstanceActionEventData; type Filter = ComputeServerInstanceActionShow; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/compute/server_instance_actions.rs b/openstack_tui/src/components/compute/server_instance_actions.rs index 955a61a45..dcbb59eb0 100644 --- a/openstack_tui/src/components/compute/server_instance_actions.rs +++ b/openstack_tui/src/components/compute/server_instance_actions.rs @@ -21,20 +21,12 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::compute::v2::server::instance_action::response::list_21::InstanceActionResponse; const VIEW_CONFIG_KEY: &str = "compute.server/instance_action"; -impl crate::utils::ResourceKey for InstanceActionResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - pub struct ComputeServerInstanceActionsBehaviour; impl ResourceBehaviour for ComputeServerInstanceActionsBehaviour { - type Item = InstanceActionResponse; type Filter = ComputeServerInstanceActionList; fn view_key() -> &'static str { @@ -69,7 +61,7 @@ impl ResourceBehaviour for ComputeServerInstanceActionsBehaviour { } fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, filter: &Self::Filter, ) -> Vec { if let Action::ShowResource(key) = action @@ -77,8 +69,14 @@ impl ResourceBehaviour for ComputeServerInstanceActionsBehaviour { && let Some(sel) = selected { let mut req = ComputeServerInstanceActionShowBuilder::default(); - req.id(sel.request_id.clone()); - req.server_id(sel.instance_uuid.clone()); + req.id(crate::components::view_render::get_str(sel, "/request_id") + .unwrap_or_default() + .to_string()); + req.server_id( + crate::components::view_render::get_str(sel, "/instance_uuid") + .unwrap_or_default() + .to_string(), + ); if let Some(name) = &filter.server_name { req.server_name(name.clone()); } @@ -103,10 +101,9 @@ pub type ComputeServerInstanceActions = mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::compute::v2::server::instance_action::response::list_21::InstanceActionResponse; - fn make_instance_action() -> InstanceActionResponse { - let json = serde_json::json!({ + fn make_instance_action() -> serde_json::Value { + serde_json::json!({ "request_id": "req-1", "instance_uuid": "server-1", "server_name": "test-server", @@ -120,8 +117,7 @@ mod tests { "action": "boot_server", "start_time": "2024-01-01T00:00:00", "end_time": "2024-01-01T00:00:01" - }); - serde_json::from_value(json).unwrap() + }) } fn make_filter() -> ComputeServerInstanceActionList { diff --git a/openstack_tui/src/components/compute/servers.rs b/openstack_tui/src/components/compute/servers.rs index a062ffd72..dd2496a37 100644 --- a/openstack_tui/src/components/compute/servers.rs +++ b/openstack_tui/src/components/compute/servers.rs @@ -18,63 +18,16 @@ use crate::cloud_worker::compute::v2::{ ComputeServerInstanceActionListBuilder, ComputeServerList, }; use crate::cloud_worker::types::{ApiRequest, ComputeApiRequest}; -use crate::components::dynamic_item::{ColumnSpec, impl_dynamic_item}; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; const VIEW_CONFIG_KEY: &str = "compute.server"; -// Server's `OS-EXT-SRV-ATTR:hostname` field changed from absent/`Option` to a required -// `String` at microversion 2.90 (bucket `_a`) -- no single `openstack_types` struct correctly -// represents every microversion, so `Item` reads columns out of the raw response by JSON pointer -// instead of deserializing into a versioned struct. -static SERVER_COLUMNS: &[ColumnSpec] = &[ - ColumnSpec { - title: "ID", - pointer: "/id", - wide: false, - status: false, - }, - ColumnSpec { - title: "Name", - pointer: "/name", - wide: false, - status: false, - }, - ColumnSpec { - title: "Status", - pointer: "/status", - wide: false, - status: true, - }, - ColumnSpec { - title: "Task State", - pointer: "/OS-EXT-STS:task_state", - wide: true, - status: false, - }, - ColumnSpec { - title: "Power State", - pointer: "/OS-EXT-STS:power_state", - wide: true, - status: false, - }, - ColumnSpec { - title: "Availability Zone", - pointer: "/OS-EXT-AZ:availability_zone", - wide: true, - status: false, - }, -]; - -impl_dynamic_item!(ServerItem, VIEW_CONFIG_KEY, SERVER_COLUMNS); - /// Behaviour implementation for ComputeServers. pub struct ComputeServersBehaviour; impl ResourceBehaviour for ComputeServersBehaviour { - type Item = ServerItem; type Filter = ComputeServerList; fn view_key() -> &'static str { @@ -112,7 +65,10 @@ impl ResourceBehaviour for ComputeServersBehaviour { None } } - fn confirm_request(action: &Action, selected: Option<&Self::Item>) -> Option { + fn confirm_request( + action: &Action, + selected: Option<&serde_json::Value>, + ) -> Option { if let Action::ResourceOp { key, op: crate::action::ResourceOp::Delete, @@ -121,9 +77,9 @@ impl ResourceBehaviour for ComputeServersBehaviour { { let sel = selected?; let mut del_builder = ComputeServerDeleteBuilder::default(); - del_builder.id(sel.get_str("/id")?); - if let Some(name) = sel.get_str("/name") { - del_builder.name(name); + del_builder.id(crate::components::view_render::get_str(sel, "/id")?.to_string()); + if let Some(name) = crate::components::view_render::get_str(sel, "/name") { + del_builder.name(name.to_string()); } let del = del_builder.build().ok()?; Some(ApiRequest::from(ComputeServerApiRequest::Delete(Box::new( @@ -135,13 +91,13 @@ impl ResourceBehaviour for ComputeServersBehaviour { } fn action_to_singular_request( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, ) -> Option<(Vec, ApiRequest)> { if let Action::ShowServerConsoleOutput = action { - let server_id = selected?.get_str("/id")?; + let server_id = crate::components::view_render::get_str(selected?, "/id")?; let req = ComputeServerApiRequest::GetConsoleOutput(Box::new( ComputeServerGetConsoleOutputBuilder::default() - .id(server_id) + .id(server_id.to_string()) .os_get_console_output( crate::cloud_worker::compute::v2::server::get_console_output::OsGetConsoleOutputBuilder::default() .build() @@ -182,18 +138,18 @@ impl ResourceBehaviour for ComputeServersBehaviour { } fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, _filter: &Self::Filter, ) -> Vec { if let Action::ShowResource(key) = action && *key == crate::mode::COMPUTE_SERVER_INSTANCE_ACTION && let Some(sel) = selected - && let Some(server_id) = sel.get_str("/id") + && let Some(server_id) = crate::components::view_render::get_str(sel, "/id") { let mut list_builder = ComputeServerInstanceActionListBuilder::default(); - list_builder.server_id(server_id); - if let Some(name) = sel.get_str("/name") { - list_builder.server_name(name); + list_builder.server_id(server_id.to_string()); + if let Some(name) = crate::components::view_render::get_str(sel, "/name") { + list_builder.server_name(name.to_string()); } if let Ok(list) = list_builder.build() { return vec![ @@ -218,8 +174,8 @@ mod tests { use crate::cloud_worker::compute::v2::ComputeServerDelete; use crate::components::resource_behaviour::ResourceBehaviour; - fn make_server(id: &str, name: &str) -> ServerItem { - ServerItem(serde_json::json!({ + fn make_server(id: &str, name: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "status": "ACTIVE", @@ -241,7 +197,7 @@ mod tests { "hostId": "host1", "key_name": null, "security_groups": [] - })) + }) } #[test] diff --git a/openstack_tui/src/components/dns.rs b/openstack_tui/src/components/dns.rs index e6b32235b..82acba918 100644 --- a/openstack_tui/src/components/dns.rs +++ b/openstack_tui/src/components/dns.rs @@ -12,5 +12,6 @@ // // SPDX-License-Identifier: Apache-2.0 +pub(crate) mod generated; pub mod recordsets; pub mod zones; diff --git a/openstack_tui/src/components/dns/generated.rs b/openstack_tui/src/components/dns/generated.rs new file mode 100644 index 000000000..7f120e09e --- /dev/null +++ b/openstack_tui/src/components/dns/generated.rs @@ -0,0 +1,19 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +// WARNING: Individual `pub(crate) mod ;` lines below are automatically generated +// from OpenAPI schema using `openstack-codegenerator`. This scaffold line itself is hand-added, +// once per service, as the anchor new resources' lines get inserted after. + +// GENERATED-ANCHOR: resource mods diff --git a/openstack_tui/src/components/dns/recordsets.rs b/openstack_tui/src/components/dns/recordsets.rs index e3e6a61ef..116429bc9 100644 --- a/openstack_tui/src/components/dns/recordsets.rs +++ b/openstack_tui/src/components/dns/recordsets.rs @@ -21,16 +21,9 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::dns::v2::recordset::response::list::RecordsetResponse; const VIEW_CONFIG_KEY: &str = "dns.recordset"; -impl crate::utils::ResourceKey for RecordsetResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - impl From for DnsZoneRecordsetList { fn from(value: DnsRecordsetList) -> Self { Self { @@ -53,7 +46,6 @@ impl From for DnsZoneRecordsetList { pub struct DnsRecordsetsBehaviour; impl ResourceBehaviour for DnsRecordsetsBehaviour { - type Item = RecordsetResponse; type Filter = DnsRecordsetList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/dns/zones.rs b/openstack_tui/src/components/dns/zones.rs index 9b4eb52b7..d2b444220 100644 --- a/openstack_tui/src/components/dns/zones.rs +++ b/openstack_tui/src/components/dns/zones.rs @@ -21,39 +21,32 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::dns::v2::zone::response::list::ZoneResponse; const VIEW_CONFIG_KEY: &str = "dns.zone"; -impl crate::utils::ResourceKey for ZoneResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - -impl TryFrom<&ZoneResponse> for DnsZoneDelete { +impl TryFrom<&serde_json::Value> for DnsZoneDelete { type Error = crate::cloud_worker::dns::v2::DnsZoneDeleteBuilderError; - fn try_from(value: &ZoneResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = DnsZoneDeleteBuilder::default(); - if let Some(val) = &value.id { - builder.id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.id(val.to_string()); } - if let Some(val) = &value.name { - builder.name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.name(val.to_string()); } builder.build() } } -impl TryFrom<&ZoneResponse> for DnsRecordsetList { +impl TryFrom<&serde_json::Value> for DnsRecordsetList { type Error = crate::cloud_worker::dns::v2::DnsRecordsetListBuilderError; - fn try_from(value: &ZoneResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = DnsRecordsetListBuilder::default(); - if let Some(val) = &value.id { - builder.zone_id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.zone_id(val.to_string()); } - if let Some(val) = &value.name { - builder.zone_name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.zone_name(val.to_string()); } builder.build() } @@ -62,7 +55,6 @@ impl TryFrom<&ZoneResponse> for DnsRecordsetList { pub struct DnsZonesBehaviour; impl ResourceBehaviour for DnsZonesBehaviour { - type Item = ZoneResponse; type Filter = DnsZoneList; fn view_key() -> &'static str { @@ -91,7 +83,10 @@ impl ResourceBehaviour for DnsZonesBehaviour { None } } - fn confirm_request(action: &Action, selected: Option<&Self::Item>) -> Option { + fn confirm_request( + action: &Action, + selected: Option<&serde_json::Value>, + ) -> Option { if let Action::ResourceOp { key, op: crate::action::ResourceOp::Delete, @@ -106,7 +101,7 @@ impl ResourceBehaviour for DnsZonesBehaviour { } fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, _filter: &Self::Filter, ) -> Vec { if let Action::ShowResource(key) = action @@ -132,10 +127,9 @@ pub type DnsZones = GenericResourceView<'static, DnsZonesBehaviour>; mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::dns::v2::zone::response::list::ZoneResponse; - fn make_zone(id: &str, name: &str) -> ZoneResponse { - let json = serde_json::json!({ + fn make_zone(id: &str, name: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "email": "admin@example.com", @@ -151,8 +145,7 @@ mod tests { "created_at": "2024-01-01T00:00:00", "updated_at": "2024-01-01T00:00:00", "attributes": {} - }); - serde_json::from_value(json).unwrap() + }) } #[test] diff --git a/openstack_tui/src/components/dynamic_item.rs b/openstack_tui/src/components/dynamic_item.rs deleted file mode 100644 index da4b67183..000000000 --- a/openstack_tui/src/components/dynamic_item.rs +++ /dev/null @@ -1,255 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -//! Support for resources whose response schema genuinely changes shape (not just adds fields) -//! across microversions (e.g. compute.flavor's `swap` i64->i32, compute.hypervisor's `id` -//! i32->String). For those, no single Rust struct can correctly represent every microversion, so -//! `ResourceBehaviour::Item` is a thin newtype around the raw `serde_json::Value` instead of a -//! generated `openstack_types` struct -- table columns are read out of the JSON by pointer at -//! render time, tolerating whatever shape actually came back, with no per-version deserialization -//! logic at all. See `impl_dynamic_item!` for the per-resource wiring. - -use serde_json::Value; -use structable::StructTableOptions; - -/// One table column: display title, JSON pointer (RFC 6901) into the raw response entry, whether -/// it's a `wide`-only column, and whether it backs `StructTable::status()` (row-coloring hook). -pub struct ColumnSpec { - pub title: &'static str, - pub pointer: &'static str, - pub wide: bool, - pub status: bool, -} - -/// Stringify whatever's at `pointer` in `value`, the same way `StructTable`'s derive macro would -/// for a typed field: `None` for missing/null, plain text for a JSON string, `Display`-style (via -/// `Value`'s own formatting) for a scalar, and pretty-vs-compact JSON (per -/// `options.pretty_mode()`) for an object/array -- matching how `structable_derive` renders -/// `serialize`/`pretty`-tagged (i.e. non-primitive) fields. -fn stringify(value: &Value, pointer: &str, pretty: bool) -> Option { - match value.pointer(pointer) { - None | Some(Value::Null) => None, - Some(Value::String(s)) => Some(s.clone()), - Some(complex @ (Value::Object(_) | Value::Array(_))) => Some(if pretty { - serde_json::to_string_pretty(complex).unwrap_or_else(|_| complex.to_string()) - } else { - complex.to_string() - }), - Some(other) => Some(other.to_string()), - } -} - -/// `StructTable::class_headers` body for a dynamic item: header list is just the configured -/// column titles, filtered by `should_return_field` like any generated struct's headers are. -pub fn dynamic_headers( - columns: &[ColumnSpec], - options: &O, -) -> Option> { - Some( - columns - .iter() - .filter(|c| options.should_return_field(c.title, c.wide)) - .map(|c| c.title.to_string()) - .collect(), - ) -} - -/// `StructTable::data` body for a dynamic item. Honors `StructTableOptions::field_data_json_pointer` -/// so user config can remap a column to a different pointer into the raw response entry, same as -/// the `structable_derive` macro does for `serialize`/`pretty` fields. -pub fn dynamic_data( - value: &Value, - columns: &[ColumnSpec], - options: &O, -) -> Vec> { - let pretty = options.pretty_mode(); - columns - .iter() - .filter(|c| options.should_return_field(c.title, c.wide)) - .map(|c| { - let pointer = options.field_data_json_pointer(c.title); - stringify(value, pointer.as_deref().unwrap_or(c.pointer), pretty) - }) - .collect() -} - -/// `StructTable::status` body for a dynamic item: the value at the column marked `status: true` -/// (if any). Unlike `dynamic_data`, `StructTable::status` has no `options` parameter to consult -/// for a pointer override or pretty-mode, so this always reads the column's baked pointer as -/// plain text. -pub fn dynamic_status(value: &Value, columns: &[ColumnSpec]) -> Option { - let column = columns.iter().find(|c| c.status)?; - stringify(value, column.pointer, false) -} - -/// Declare a newtype wrapper around `serde_json::Value` usable as `ResourceBehaviour::Item` for a -/// resource with a genuinely breaking microversion schema change. `$columns` is a -/// `&'static [ColumnSpec]`. -/// -/// A per-resource newtype (rather than one shared `Item = Value`) is required because -/// `ResourceKey::get_key` is a static method with no way to know which resource a bare `Value` is -/// for. -macro_rules! impl_dynamic_item { - ($name:ident, $key:expr, $columns:expr) => { - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] - #[serde(transparent)] - pub struct $name(pub serde_json::Value); - - impl $name { - pub fn get(&self, pointer: &str) -> Option<&serde_json::Value> { - self.0.pointer(pointer) - } - - pub fn get_str(&self, pointer: &str) -> Option { - self.get(pointer) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - } - } - - impl crate::utils::ResourceKey for $name { - fn get_key() -> &'static str { - $key - } - } - - impl structable::StructTable for $name { - fn class_headers( - options: &O, - ) -> Option> { - crate::components::dynamic_item::dynamic_headers($columns, options) - } - fn data(&self, options: &O) -> Vec> { - crate::components::dynamic_item::dynamic_data(&self.0, $columns, options) - } - fn status(&self) -> Option { - crate::components::dynamic_item::dynamic_status(&self.0, $columns) - } - } - - impl structable::StructTable for &$name { - fn class_headers( - options: &O, - ) -> Option> { - <$name as structable::StructTable>::class_headers(options) - } - fn data(&self, options: &O) -> Vec> { - structable::StructTable::data(*self, options) - } - fn status(&self) -> Option { - structable::StructTable::status(*self) - } - } - }; -} - -pub(crate) use impl_dynamic_item; - -#[cfg(test)] -mod tests { - use super::*; - - const COLUMNS: &[ColumnSpec] = &[ - ColumnSpec { - title: "ID", - pointer: "/id", - wide: false, - status: false, - }, - ColumnSpec { - title: "Status", - pointer: "/status", - wide: false, - status: true, - }, - ColumnSpec { - title: "Metadata", - pointer: "/metadata", - wide: false, - status: false, - }, - ]; - - struct Opts { - pretty: bool, - } - - impl StructTableOptions for Opts { - fn wide_mode(&self) -> bool { - false - } - fn pretty_mode(&self) -> bool { - self.pretty - } - fn should_return_field>(&self, _field: S, _is_wide_field: bool) -> bool { - true - } - } - - #[test] - fn stringify_scalar_ignores_pretty() { - let value = serde_json::json!({"id": "abc"}); - assert_eq!(stringify(&value, "/id", true), Some("abc".to_string())); - } - - #[test] - fn stringify_object_respects_pretty_mode() { - let value = serde_json::json!({"metadata": {"a": 1}}); - assert_eq!( - stringify(&value, "/metadata", false), - Some(serde_json::json!({"a": 1}).to_string()) - ); - assert_eq!( - stringify(&value, "/metadata", true), - Some(serde_json::to_string_pretty(&serde_json::json!({"a": 1})).unwrap()) - ); - } - - #[test] - fn stringify_missing_or_null_is_none() { - let value = serde_json::json!({"id": null}); - assert_eq!(stringify(&value, "/id", false), None); - assert_eq!(stringify(&value, "/missing", false), None); - } - - #[test] - fn dynamic_data_honors_pretty_mode_for_complex_fields() { - let value = serde_json::json!({"id": "abc", "status": "ACTIVE", "metadata": {"a": 1}}); - let compact = dynamic_data(&value, COLUMNS, &Opts { pretty: false }); - assert_eq!(compact[2], Some(serde_json::json!({"a": 1}).to_string())); - let pretty = dynamic_data(&value, COLUMNS, &Opts { pretty: true }); - assert_eq!( - pretty[2], - Some(serde_json::to_string_pretty(&serde_json::json!({"a": 1})).unwrap()) - ); - } - - #[test] - fn dynamic_status_returns_marked_column_value() { - let value = serde_json::json!({"id": "abc", "status": "ACTIVE"}); - assert_eq!(dynamic_status(&value, COLUMNS), Some("ACTIVE".to_string())); - } - - #[test] - fn dynamic_status_none_when_no_column_marked() { - let value = serde_json::json!({"id": "abc"}); - let columns: &[ColumnSpec] = &[ColumnSpec { - title: "ID", - pointer: "/id", - wide: false, - status: false, - }]; - assert_eq!(dynamic_status(&value, columns), None); - } -} diff --git a/openstack_tui/src/components/generic_resource_view.rs b/openstack_tui/src/components/generic_resource_view.rs index 8583d4b27..6bf683943 100644 --- a/openstack_tui/src/components/generic_resource_view.rs +++ b/openstack_tui/src/components/generic_resource_view.rs @@ -20,7 +20,6 @@ use crate::mode::Mode; use crossterm::event::KeyEvent; use eyre::Result; use ratatui::prelude::*; -use structable::StructTable; use tokio::sync::mpsc::UnboundedSender; use super::resource_behaviour::ResourceBehaviour; @@ -31,22 +30,18 @@ use super::resource_behaviour::ResourceBehaviour; pub struct GenericResourceView<'a, B> where B: ResourceBehaviour, - B::Item: StructTable + 'static, - for<'b> &'b B::Item: StructTable, { - base: super::table_view::TableViewComponentBase<'a, B::Item, B::Filter>, + base: super::table_view::TableViewComponentBase<'a, B::Filter>, behaviour: std::marker::PhantomData, } impl<'a, B> GenericResourceView<'a, B> where B: ResourceBehaviour, - B::Item: StructTable + 'static, - for<'b> &'b B::Item: StructTable, { pub fn new() -> Self { Self { - base: super::table_view::TableViewComponentBase::new(), + base: super::table_view::TableViewComponentBase::new(B::view_key()), behaviour: std::marker::PhantomData, } } @@ -55,8 +50,6 @@ where impl<'a, B> Default for GenericResourceView<'a, B> where B: ResourceBehaviour, - B::Item: StructTable + 'static, - for<'b> &'b B::Item: StructTable, { fn default() -> Self { Self::new() @@ -67,8 +60,6 @@ impl<'a, B> Component for GenericResourceView<'a, B> where 'a: 'static, B: ResourceBehaviour + 'static, - B::Item: StructTable, - for<'b> &'b B::Item: StructTable, { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self @@ -155,12 +146,11 @@ where if let Action::ApiResponsesData { request, data, - negotiated_version, + negotiated_version: _, } = &action { if B::matches_request(request) { - let items = B::deserialize_items(data, *negotiated_version)?; - self.base.set_data_items(items, data.clone())?; + self.base.set_data(data.clone())?; return Ok(None); } // --- Singular API response: delegate to behaviour --- @@ -481,11 +471,10 @@ mod tests { } fn setup_comp_with_matching_singular_request_and_data() -> (Value, ApiRequest) { - let server = crate::components::compute::servers::ServerItem(make_server_json()); let (_display_actions, request) = crate::components::compute::servers::ComputeServersBehaviour::action_to_singular_request( &Action::ShowServerConsoleOutput, - Some(&server), + Some(&make_server_json()), ) .unwrap(); (serde_json::json!({ "output": "test console" }), request) diff --git a/openstack_tui/src/components/identity.rs b/openstack_tui/src/components/identity.rs index 292e0f893..84d85bbe8 100644 --- a/openstack_tui/src/components/identity.rs +++ b/openstack_tui/src/components/identity.rs @@ -13,6 +13,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod application_credentials; +pub(crate) mod generated; pub mod group_users; pub mod groups; pub mod projects; diff --git a/openstack_tui/src/components/identity/application_credentials.rs b/openstack_tui/src/components/identity/application_credentials.rs index 150c93ff4..2ebaae88e 100644 --- a/openstack_tui/src/components/identity/application_credentials.rs +++ b/openstack_tui/src/components/identity/application_credentials.rs @@ -21,20 +21,12 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::identity::v3::user::application_credential::response::list::ApplicationCredentialResponse; const VIEW_CONFIG_KEY: &str = "identity.user/application_credential"; -impl crate::utils::ResourceKey for ApplicationCredentialResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - pub struct IdentityApplicationCredentialsBehaviour; impl ResourceBehaviour for IdentityApplicationCredentialsBehaviour { - type Item = ApplicationCredentialResponse; type Filter = IdentityUserApplicationCredentialList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/identity/generated.rs b/openstack_tui/src/components/identity/generated.rs new file mode 100644 index 000000000..7f120e09e --- /dev/null +++ b/openstack_tui/src/components/identity/generated.rs @@ -0,0 +1,19 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +// WARNING: Individual `pub(crate) mod ;` lines below are automatically generated +// from OpenAPI schema using `openstack-codegenerator`. This scaffold line itself is hand-added, +// once per service, as the anchor new resources' lines get inserted after. + +// GENERATED-ANCHOR: resource mods diff --git a/openstack_tui/src/components/identity/group_users.rs b/openstack_tui/src/components/identity/group_users.rs index e66a82c22..f6e2ebfee 100644 --- a/openstack_tui/src/components/identity/group_users.rs +++ b/openstack_tui/src/components/identity/group_users.rs @@ -20,20 +20,12 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::identity::v3::group::user::response::list::UserResponse; const VIEW_CONFIG_KEY: &str = "identity.user"; -impl crate::utils::ResourceKey for UserResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - pub struct IdentityGroupUsersBehaviour; impl ResourceBehaviour for IdentityGroupUsersBehaviour { - type Item = UserResponse; type Filter = IdentityGroupUserList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/identity/groups.rs b/openstack_tui/src/components/identity/groups.rs index d5971e492..23610ce80 100644 --- a/openstack_tui/src/components/identity/groups.rs +++ b/openstack_tui/src/components/identity/groups.rs @@ -21,39 +21,32 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::identity::v3::group::response::list::GroupResponse; const VIEW_CONFIG_KEY: &str = "identity.group"; -impl crate::utils::ResourceKey for GroupResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - -impl TryFrom<&GroupResponse> for IdentityGroupUserList { +impl TryFrom<&serde_json::Value> for IdentityGroupUserList { type Error = crate::cloud_worker::identity::v3::IdentityGroupUserListBuilderError; - fn try_from(value: &GroupResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = IdentityGroupUserListBuilder::default(); - if let Some(val) = &value.id { - builder.group_id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.group_id(val.to_string()); } - if let Some(val) = &value.name { - builder.group_name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.group_name(val.to_string()); } builder.build() } } -impl TryFrom<&GroupResponse> for IdentityGroupDelete { +impl TryFrom<&serde_json::Value> for IdentityGroupDelete { type Error = crate::cloud_worker::identity::v3::IdentityGroupDeleteBuilderError; - fn try_from(value: &GroupResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = IdentityGroupDeleteBuilder::default(); - if let Some(val) = &value.id { - builder.id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.id(val.to_string()); } - if let Some(val) = &value.name { - builder.name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.name(val.to_string()); } builder.build() } @@ -62,7 +55,6 @@ impl TryFrom<&GroupResponse> for IdentityGroupDelete { pub struct IdentityGroupsBehaviour; impl ResourceBehaviour for IdentityGroupsBehaviour { - type Item = GroupResponse; type Filter = IdentityGroupList; fn view_key() -> &'static str { @@ -84,7 +76,10 @@ impl ResourceBehaviour for IdentityGroupsBehaviour { if matches!(**boxreq, IdentityGroupApiRequest::List(_)) ) } - fn confirm_request(action: &Action, selected: Option<&Self::Item>) -> Option { + fn confirm_request( + action: &Action, + selected: Option<&serde_json::Value>, + ) -> Option { if let Action::ResourceOp { key, op: crate::action::ResourceOp::Delete, @@ -101,7 +96,7 @@ impl ResourceBehaviour for IdentityGroupsBehaviour { } fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, _filter: &Self::Filter, ) -> Vec { if let Action::ShowResource(key) = action @@ -127,16 +122,14 @@ pub type IdentityGroups = GenericResourceView<'static, IdentityGroupsBehaviour>; mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::identity::v3::group::response::list::GroupResponse; - fn make_group(id: &str, name: &str) -> GroupResponse { - let json = serde_json::json!({ + fn make_group(id: &str, name: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "domain_id": "default", "description": "test group" - }); - serde_json::from_value(json).unwrap() + }) } #[test] diff --git a/openstack_tui/src/components/identity/projects.rs b/openstack_tui/src/components/identity/projects.rs index 19e615418..036ef71a2 100644 --- a/openstack_tui/src/components/identity/projects.rs +++ b/openstack_tui/src/components/identity/projects.rs @@ -20,20 +20,12 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::identity::v3::project::response::list::ProjectResponse; const VIEW_CONFIG_KEY: &str = "identity.project"; -impl crate::utils::ResourceKey for ProjectResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - pub struct IdentityProjectsBehaviour; impl ResourceBehaviour for IdentityProjectsBehaviour { - type Item = ProjectResponse; type Filter = IdentityProjectList; fn view_key() -> &'static str { @@ -57,17 +49,18 @@ impl ResourceBehaviour for IdentityProjectsBehaviour { } fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, _filter: &Self::Filter, ) -> Vec { if let Action::SwitchToProject = action && let Some(sel) = selected { let scope = openstack_sdk::types::identity::v3::Project { - id: sel.id.clone(), - name: sel.name.clone(), + id: crate::components::view_render::get_str(sel, "/id").map(|s| s.to_string()), + name: crate::components::view_render::get_str(sel, "/name").map(|s| s.to_string()), domain: Some(openstack_sdk::types::identity::v3::Domain { - id: sel.domain_id.clone(), + id: crate::components::view_render::get_str(sel, "/domain_id") + .map(|s| s.to_string()), name: None, }), }; @@ -85,17 +78,15 @@ pub type IdentityProjects = GenericResourceView<'static, IdentityProjectsBehavio mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::identity::v3::project::response::list::ProjectResponse; - fn make_project(id: &str, name: &str, domain_id: &str) -> ProjectResponse { - let json = serde_json::json!({ + fn make_project(id: &str, name: &str, domain_id: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "domain_id": domain_id, "enabled": true, "description": "test project" - }); - serde_json::from_value(json).unwrap() + }) } #[test] diff --git a/openstack_tui/src/components/identity/users.rs b/openstack_tui/src/components/identity/users.rs index 398995cd0..cc47e0e94 100644 --- a/openstack_tui/src/components/identity/users.rs +++ b/openstack_tui/src/components/identity/users.rs @@ -22,34 +22,35 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::{Mutation, ResourceBehaviour}; use crate::mode::Mode; -use openstack_types::identity::v3::user::response::list::UserResponse; use serde_json::Value; const VIEW_CONFIG_KEY: &str = "identity.user"; -impl crate::utils::ResourceKey for UserResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - -impl TryFrom<&UserResponse> for IdentityUserDelete { +impl TryFrom<&serde_json::Value> for IdentityUserDelete { type Error = crate::cloud_worker::identity::v3::IdentityUserDeleteBuilderError; - fn try_from(value: &UserResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = IdentityUserDeleteBuilder::default(); - builder.id(value.id.clone()); - builder.name(value.name.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.id(val.to_string()); + } + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.name(val.to_string()); + } builder.build() } } -impl TryFrom<&UserResponse> for IdentityUserApplicationCredentialList { +impl TryFrom<&serde_json::Value> for IdentityUserApplicationCredentialList { type Error = crate::cloud_worker::identity::v3::IdentityUserApplicationCredentialListBuilderError; - fn try_from(value: &UserResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = IdentityUserApplicationCredentialListBuilder::default(); - builder.user_id(value.id.clone()); - builder.user_name(value.name.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.user_id(val.to_string()); + } + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.user_name(val.to_string()); + } builder.build() } } @@ -57,7 +58,6 @@ impl TryFrom<&UserResponse> for IdentityUserApplicationCredentialList { pub struct IdentityUsersBehaviour; impl ResourceBehaviour for IdentityUsersBehaviour { - type Item = UserResponse; type Filter = IdentityUserList; fn view_key() -> &'static str { @@ -79,7 +79,10 @@ impl ResourceBehaviour for IdentityUsersBehaviour { if matches!(**boxreq, IdentityUserApiRequest::List(_)) ) } - fn action_to_request(action: &Action, selected: Option<&Self::Item>) -> Option { + fn action_to_request( + action: &Action, + selected: Option<&serde_json::Value>, + ) -> Option { if let Action::IdentityUserOp { key, op: crate::action::IdentityUserOp::FlipEnable, @@ -87,13 +90,15 @@ impl ResourceBehaviour for IdentityUsersBehaviour { && *key == Self::view_key() { let sel = selected?; + let enabled = crate::components::view_render::get(sel, "/enabled")?.as_bool()?; + let id = crate::components::view_render::get_str(sel, "/id")?; let req: crate::cloud_worker::identity::v3::user::set::User = crate::cloud_worker::identity::v3::user::set::UserBuilder::default() - .enabled(!sel.enabled) + .enabled(!enabled) .build() .ok()?; let set_req = IdentityUserSetBuilder::default() - .id(sel.id.clone()) + .id(id.to_string()) .user(req) .build() .ok()?; @@ -104,7 +109,10 @@ impl ResourceBehaviour for IdentityUsersBehaviour { None } } - fn confirm_request(action: &Action, selected: Option<&Self::Item>) -> Option { + fn confirm_request( + action: &Action, + selected: Option<&serde_json::Value>, + ) -> Option { if let Action::IdentityUserOp { key, op: crate::action::IdentityUserOp::Delete, @@ -121,7 +129,7 @@ impl ResourceBehaviour for IdentityUsersBehaviour { } fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, _filter: &Self::Filter, ) -> Vec { if let Action::ShowResource(key) = action @@ -160,16 +168,14 @@ pub type IdentityUsers = GenericResourceView<'static, IdentityUsersBehaviour>; mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::identity::v3::user::response::list::UserResponse; - fn make_user(id: &str, name: &str, enabled: bool) -> UserResponse { - let json = serde_json::json!({ + fn make_user(id: &str, name: &str, enabled: bool) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "enabled": enabled, "domain_id": "default" - }); - serde_json::from_value(json).unwrap() + }) } #[test] diff --git a/openstack_tui/src/components/image.rs b/openstack_tui/src/components/image.rs index 293d2963c..8df709e4f 100644 --- a/openstack_tui/src/components/image.rs +++ b/openstack_tui/src/components/image.rs @@ -12,4 +12,5 @@ // // SPDX-License-Identifier: Apache-2.0 +pub(crate) mod generated; pub mod images; diff --git a/openstack_tui/src/components/image/generated.rs b/openstack_tui/src/components/image/generated.rs new file mode 100644 index 000000000..7f120e09e --- /dev/null +++ b/openstack_tui/src/components/image/generated.rs @@ -0,0 +1,19 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +// WARNING: Individual `pub(crate) mod ;` lines below are automatically generated +// from OpenAPI schema using `openstack-codegenerator`. This scaffold line itself is hand-added, +// once per service, as the anchor new resources' lines get inserted after. + +// GENERATED-ANCHOR: resource mods diff --git a/openstack_tui/src/components/image/images.rs b/openstack_tui/src/components/image/images.rs index 7b7ac2d30..d0b4f9f88 100644 --- a/openstack_tui/src/components/image/images.rs +++ b/openstack_tui/src/components/image/images.rs @@ -18,7 +18,6 @@ use crate::cloud_worker::image::v2::{ ImageImageList, }; use crate::cloud_worker::types::ApiRequest; -use crate::components::dynamic_item::{ColumnSpec, impl_dynamic_item}; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::{Mutation, ResourceBehaviour}; use crate::mode::Mode; @@ -26,59 +25,15 @@ use serde_json::Value; const VIEW_CONFIG_KEY: &str = "image.image"; -// image.image has no microversion schema drift today (single unversioned `response::list`) -- -// converted to the dynamic-item pattern here purely to verify the design generalizes to a -// resource that isn't a genuine breaking-change case. -static IMAGE_COLUMNS: &[ColumnSpec] = &[ - ColumnSpec { - title: "ID", - pointer: "/id", - wide: false, - status: false, - }, - ColumnSpec { - title: "Name", - pointer: "/name", - wide: false, - status: false, - }, - ColumnSpec { - title: "Status", - pointer: "/status", - wide: false, - status: true, - }, - ColumnSpec { - title: "Visibility", - pointer: "/visibility", - wide: true, - status: false, - }, - ColumnSpec { - title: "Disk Format", - pointer: "/disk_format", - wide: true, - status: false, - }, - ColumnSpec { - title: "Container Format", - pointer: "/container_format", - wide: true, - status: false, - }, -]; - -impl_dynamic_item!(ImageItem, VIEW_CONFIG_KEY, IMAGE_COLUMNS); - -impl TryFrom<&ImageItem> for ImageImageDelete { +impl TryFrom<&Value> for ImageImageDelete { type Error = crate::cloud_worker::image::v2::ImageImageDeleteBuilderError; - fn try_from(value: &ImageItem) -> Result { + fn try_from(value: &Value) -> Result { let mut builder = ImageImageDeleteBuilder::default(); - if let Some(val) = value.get_str("/id") { - builder.id(val); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.id(val.to_string()); } - if let Some(val) = value.get_str("/name") { - builder.name(val); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.name(val.to_string()); } builder.build() } @@ -87,7 +42,6 @@ impl TryFrom<&ImageItem> for ImageImageDelete { pub struct ImageImagesBehaviour; impl ResourceBehaviour for ImageImagesBehaviour { - type Item = ImageItem; type Filter = ImageImageList; fn view_key() -> &'static str { @@ -116,7 +70,7 @@ impl ResourceBehaviour for ImageImagesBehaviour { None } } - fn confirm_request(action: &Action, selected: Option<&Self::Item>) -> Option { + fn confirm_request(action: &Action, selected: Option<&Value>) -> Option { if let Action::ResourceOp { key, op: crate::action::ResourceOp::Delete, @@ -148,8 +102,8 @@ mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - fn make_image(id: &str, name: &str) -> ImageItem { - ImageItem(serde_json::json!({ + fn make_image(id: &str, name: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "status": "active", @@ -162,7 +116,7 @@ mod tests { "visibility": "public", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" - })) + }) } #[test] diff --git a/openstack_tui/src/components/load_balancer.rs b/openstack_tui/src/components/load_balancer.rs index 4bfd2b37f..6e01196ea 100644 --- a/openstack_tui/src/components/load_balancer.rs +++ b/openstack_tui/src/components/load_balancer.rs @@ -12,6 +12,7 @@ // // SPDX-License-Identifier: Apache-2.0 +pub(crate) mod generated; pub mod health_monitors; pub mod listeners; pub mod loadbalancers; diff --git a/openstack_tui/src/components/load_balancer/generated.rs b/openstack_tui/src/components/load_balancer/generated.rs new file mode 100644 index 000000000..7f120e09e --- /dev/null +++ b/openstack_tui/src/components/load_balancer/generated.rs @@ -0,0 +1,19 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +// WARNING: Individual `pub(crate) mod ;` lines below are automatically generated +// from OpenAPI schema using `openstack-codegenerator`. This scaffold line itself is hand-added, +// once per service, as the anchor new resources' lines get inserted after. + +// GENERATED-ANCHOR: resource mods diff --git a/openstack_tui/src/components/load_balancer/health_monitors.rs b/openstack_tui/src/components/load_balancer/health_monitors.rs index eb104599a..b298bad11 100644 --- a/openstack_tui/src/components/load_balancer/health_monitors.rs +++ b/openstack_tui/src/components/load_balancer/health_monitors.rs @@ -20,20 +20,12 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::load_balancer::v2::healthmonitor::response::list::HealthmonitorResponse; const VIEW_CONFIG_KEY: &str = "load-balancer.healthmonitor"; -impl crate::utils::ResourceKey for HealthmonitorResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - pub struct LoadBalancerHealthMonitorsBehaviour; impl ResourceBehaviour for LoadBalancerHealthMonitorsBehaviour { - type Item = HealthmonitorResponse; type Filter = LoadBalancerHealthmonitorList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/load_balancer/listeners.rs b/openstack_tui/src/components/load_balancer/listeners.rs index 40678aa31..9af2d448b 100644 --- a/openstack_tui/src/components/load_balancer/listeners.rs +++ b/openstack_tui/src/components/load_balancer/listeners.rs @@ -20,20 +20,12 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::load_balancer::v2::listener::response::list::ListenerResponse; const VIEW_CONFIG_KEY: &str = "load-balancer.listener"; -impl crate::utils::ResourceKey for ListenerResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - pub struct LoadBalancerListenersBehaviour; impl ResourceBehaviour for LoadBalancerListenersBehaviour { - type Item = ListenerResponse; type Filter = LoadBalancerListenerList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/load_balancer/loadbalancers.rs b/openstack_tui/src/components/load_balancer/loadbalancers.rs index 0a57eb717..80bf22ee6 100644 --- a/openstack_tui/src/components/load_balancer/loadbalancers.rs +++ b/openstack_tui/src/components/load_balancer/loadbalancers.rs @@ -22,39 +22,32 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::load_balancer::v2::loadbalancer::response::list::LoadbalancerResponse; const VIEW_CONFIG_KEY: &str = "load-balancer.loadbalancer"; -impl crate::utils::ResourceKey for LoadbalancerResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - -impl TryFrom<&LoadbalancerResponse> for LoadBalancerListenerList { +impl TryFrom<&serde_json::Value> for LoadBalancerListenerList { type Error = crate::cloud_worker::load_balancer::v2::LoadBalancerListenerListBuilderError; - fn try_from(value: &LoadbalancerResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = LoadBalancerListenerListBuilder::default(); - if let Some(val) = &value.id { - builder.load_balancer_id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.load_balancer_id(val.to_string()); } - if let Some(val) = &value.name { - builder.load_balancer_name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.load_balancer_name(val.to_string()); } builder.build() } } -impl TryFrom<&LoadbalancerResponse> for LoadBalancerPoolList { +impl TryFrom<&serde_json::Value> for LoadBalancerPoolList { type Error = crate::cloud_worker::load_balancer::v2::LoadBalancerPoolListBuilderError; - fn try_from(value: &LoadbalancerResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = LoadBalancerPoolListBuilder::default(); - if let Some(val) = &value.id { - builder.loadbalancer_id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.loadbalancer_id(val.to_string()); } - if let Some(val) = &value.name { - builder.loadbalancer_name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.loadbalancer_name(val.to_string()); } builder.build() } @@ -63,7 +56,6 @@ impl TryFrom<&LoadbalancerResponse> for LoadBalancerPoolList { pub struct LoadBalancersBehaviour; impl ResourceBehaviour for LoadBalancersBehaviour { - type Item = LoadbalancerResponse; type Filter = LoadBalancerLoadbalancerList; fn view_key() -> &'static str { @@ -96,7 +88,7 @@ impl ResourceBehaviour for LoadBalancersBehaviour { } fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, _filter: &Self::Filter, ) -> Vec { if let Action::ShowResource(key) = action @@ -135,10 +127,9 @@ pub type LoadBalancers = GenericResourceView<'static, LoadBalancersBehaviour>; mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::load_balancer::v2::loadbalancer::response::list::LoadbalancerResponse; - fn make_lb(id: &str, name: &str) -> LoadbalancerResponse { - let json = serde_json::json!({ + fn make_lb(id: &str, name: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "provisioning_status": "ACTIVE", @@ -155,8 +146,7 @@ mod tests { "project_id": "tenant-1", "flavor_id": null, "tags": [] - }); - serde_json::from_value(json).unwrap() + }) } #[test] diff --git a/openstack_tui/src/components/load_balancer/pool_members.rs b/openstack_tui/src/components/load_balancer/pool_members.rs index e2e681593..ddf074109 100644 --- a/openstack_tui/src/components/load_balancer/pool_members.rs +++ b/openstack_tui/src/components/load_balancer/pool_members.rs @@ -21,20 +21,12 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::load_balancer::v2::pool::member::response::list::MemberResponse; const VIEW_CONFIG_KEY: &str = "load-balancer.pool/member"; -impl crate::utils::ResourceKey for MemberResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - pub struct LoadBalancerPoolMembersBehaviour; impl ResourceBehaviour for LoadBalancerPoolMembersBehaviour { - type Item = MemberResponse; type Filter = LoadBalancerPoolMemberList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/load_balancer/pools.rs b/openstack_tui/src/components/load_balancer/pools.rs index 96ef73395..6589ac55f 100644 --- a/openstack_tui/src/components/load_balancer/pools.rs +++ b/openstack_tui/src/components/load_balancer/pools.rs @@ -22,39 +22,32 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::ResourceBehaviour; use crate::mode::Mode; -use openstack_types::load_balancer::v2::pool::response::list::PoolResponse; const VIEW_CONFIG_KEY: &str = "load-balancer.pool"; -impl crate::utils::ResourceKey for PoolResponse { - fn get_key() -> &'static str { - VIEW_CONFIG_KEY - } -} - -impl TryFrom<&PoolResponse> for LoadBalancerPoolMemberList { +impl TryFrom<&serde_json::Value> for LoadBalancerPoolMemberList { type Error = crate::cloud_worker::load_balancer::v2::LoadBalancerPoolMemberListBuilderError; - fn try_from(value: &PoolResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = LoadBalancerPoolMemberListBuilder::default(); - if let Some(val) = &value.id { - builder.pool_id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.pool_id(val.to_string()); } - if let Some(val) = &value.name { - builder.pool_name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.pool_name(val.to_string()); } builder.build() } } -impl TryFrom<&PoolResponse> for LoadBalancerHealthmonitorList { +impl TryFrom<&serde_json::Value> for LoadBalancerHealthmonitorList { type Error = crate::cloud_worker::load_balancer::v2::LoadBalancerHealthmonitorListBuilderError; - fn try_from(value: &PoolResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = LoadBalancerHealthmonitorListBuilder::default(); - if let Some(val) = &value.id { - builder.pool_id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.pool_id(val.to_string()); } - if let Some(val) = &value.name { - builder.pool_name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.pool_name(val.to_string()); } builder.build() } @@ -63,7 +56,6 @@ impl TryFrom<&PoolResponse> for LoadBalancerHealthmonitorList { pub struct LoadBalancerPoolsBehaviour; impl ResourceBehaviour for LoadBalancerPoolsBehaviour { - type Item = PoolResponse; type Filter = LoadBalancerPoolList; fn view_key() -> &'static str { @@ -94,7 +86,7 @@ impl ResourceBehaviour for LoadBalancerPoolsBehaviour { } fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, _filter: &Self::Filter, ) -> Vec { if let Action::ShowResource(key) = action @@ -133,10 +125,9 @@ pub type LoadBalancerPools = GenericResourceView<'static, LoadBalancerPoolsBehav mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::load_balancer::v2::pool::response::list::PoolResponse; - fn make_pool(id: &str, name: &str) -> PoolResponse { - let json = serde_json::json!({ + fn make_pool(id: &str, name: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "description": "test pool", @@ -154,8 +145,7 @@ mod tests { "session_persistence": null, "monitor_ports": [], "tags": [] - }); - serde_json::from_value(json).unwrap() + }) } #[test] diff --git a/openstack_tui/src/components/network/generated/network.rs b/openstack_tui/src/components/network/generated/network.rs index 922eaf5fe..a9adaaa91 100644 --- a/openstack_tui/src/components/network/generated/network.rs +++ b/openstack_tui/src/components/network/generated/network.rs @@ -19,12 +19,10 @@ use crate::cloud_worker::types as cloud_types; use crate::cloud_worker::types::ApiRequest; use crate::components::resource_behaviour::GeneratedResourceBehaviour; use crate::mode::Mode; -use openstack_types::network::v2::network::response::list::NetworkResponse; pub(crate) struct Generated; impl GeneratedResourceBehaviour for Generated { - type Item = NetworkResponse; type Filter = cloud_types::NetworkNetworkList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/network/generated/router.rs b/openstack_tui/src/components/network/generated/router.rs index 62aee5326..4711ad6be 100644 --- a/openstack_tui/src/components/network/generated/router.rs +++ b/openstack_tui/src/components/network/generated/router.rs @@ -19,12 +19,10 @@ use crate::cloud_worker::types as cloud_types; use crate::cloud_worker::types::ApiRequest; use crate::components::resource_behaviour::GeneratedResourceBehaviour; use crate::mode::Mode; -use openstack_types::network::v2::router::response::list::RouterResponse; pub(crate) struct Generated; impl GeneratedResourceBehaviour for Generated { - type Item = RouterResponse; type Filter = cloud_types::NetworkRouterList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/network/generated/security_group.rs b/openstack_tui/src/components/network/generated/security_group.rs index 9d19147a7..8c6405555 100644 --- a/openstack_tui/src/components/network/generated/security_group.rs +++ b/openstack_tui/src/components/network/generated/security_group.rs @@ -17,53 +17,12 @@ use crate::action::Action; use crate::cloud_worker::types as cloud_types; use crate::cloud_worker::types::ApiRequest; -use crate::components::dynamic_item::{ColumnSpec, impl_dynamic_item}; use crate::components::resource_behaviour::GeneratedResourceBehaviour; use crate::mode::Mode; -static COLUMNS: &[ColumnSpec] = &[ - ColumnSpec { - title: "id", - pointer: "/id", - wide: false, - status: false, - }, - ColumnSpec { - title: "name", - pointer: "/name", - wide: false, - status: false, - }, - ColumnSpec { - title: "description", - pointer: "/description", - wide: false, - status: false, - }, - ColumnSpec { - title: "created_at", - pointer: "/created_at", - wide: false, - status: false, - }, - ColumnSpec { - title: "updated_at", - pointer: "/updated_at", - wide: false, - status: false, - }, -]; - -impl_dynamic_item!( - SecurityGroupItem, - crate::mode::NETWORK_SECURITY_GROUP, - COLUMNS -); - pub(crate) struct Generated; impl GeneratedResourceBehaviour for Generated { - type Item = SecurityGroupItem; type Filter = cloud_types::NetworkSecurityGroupList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/network/generated/security_group_rule.rs b/openstack_tui/src/components/network/generated/security_group_rule.rs index 495b92a86..9c19ca07b 100644 --- a/openstack_tui/src/components/network/generated/security_group_rule.rs +++ b/openstack_tui/src/components/network/generated/security_group_rule.rs @@ -17,65 +17,12 @@ use crate::action::Action; use crate::cloud_worker::types as cloud_types; use crate::cloud_worker::types::ApiRequest; -use crate::components::dynamic_item::{ColumnSpec, impl_dynamic_item}; use crate::components::resource_behaviour::GeneratedResourceBehaviour; use crate::mode::Mode; -static COLUMNS: &[ColumnSpec] = &[ - ColumnSpec { - title: "id", - pointer: "/id", - wide: false, - status: false, - }, - ColumnSpec { - title: "ethertype", - pointer: "/ethertype", - wide: false, - status: false, - }, - ColumnSpec { - title: "direction", - pointer: "/direction", - wide: false, - status: false, - }, - ColumnSpec { - title: "protocol", - pointer: "/protocol", - wide: false, - status: false, - }, - ColumnSpec { - title: "port_range_min", - pointer: "/port_range_min", - wide: false, - status: false, - }, - ColumnSpec { - title: "port_range_max", - pointer: "/port_range_max", - wide: false, - status: false, - }, - ColumnSpec { - title: "description", - pointer: "/description", - wide: false, - status: false, - }, -]; - -impl_dynamic_item!( - SecurityGroupRuleItem, - crate::mode::NETWORK_SECURITY_GROUP_RULE, - COLUMNS -); - pub(crate) struct Generated; impl GeneratedResourceBehaviour for Generated { - type Item = SecurityGroupRuleItem; type Filter = cloud_types::NetworkSecurityGroupRuleList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/network/generated/subnet.rs b/openstack_tui/src/components/network/generated/subnet.rs index dfd7c216c..9ea6cd80e 100644 --- a/openstack_tui/src/components/network/generated/subnet.rs +++ b/openstack_tui/src/components/network/generated/subnet.rs @@ -17,49 +17,12 @@ use crate::action::Action; use crate::cloud_worker::types as cloud_types; use crate::cloud_worker::types::ApiRequest; -use crate::components::dynamic_item::{ColumnSpec, impl_dynamic_item}; use crate::components::resource_behaviour::GeneratedResourceBehaviour; use crate::mode::Mode; -static COLUMNS: &[ColumnSpec] = &[ - ColumnSpec { - title: "id", - pointer: "/id", - wide: false, - status: false, - }, - ColumnSpec { - title: "name", - pointer: "/name", - wide: false, - status: false, - }, - ColumnSpec { - title: "cidr", - pointer: "/cidr", - wide: false, - status: false, - }, - ColumnSpec { - title: "description", - pointer: "/description", - wide: false, - status: false, - }, - ColumnSpec { - title: "created_at", - pointer: "/created_at", - wide: false, - status: false, - }, -]; - -impl_dynamic_item!(SubnetItem, crate::mode::NETWORK_SUBNET, COLUMNS); - pub(crate) struct Generated; impl GeneratedResourceBehaviour for Generated { - type Item = SubnetItem; type Filter = cloud_types::NetworkSubnetList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/network/networks.rs b/openstack_tui/src/components/network/networks.rs index cab187da3..f5f2a462a 100644 --- a/openstack_tui/src/components/network/networks.rs +++ b/openstack_tui/src/components/network/networks.rs @@ -18,23 +18,16 @@ use crate::cloud_worker::types::{self as cloud_types, ApiRequest}; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::{GeneratedResourceBehaviour, ResourceBehaviour}; use crate::mode::Mode; -use openstack_types::network::v2::network::response::list::NetworkResponse; -impl crate::utils::ResourceKey for NetworkResponse { - fn get_key() -> &'static str { - crate::mode::NETWORK_NETWORK - } -} - -impl TryFrom<&NetworkResponse> for NetworkSubnetList { +impl TryFrom<&serde_json::Value> for NetworkSubnetList { type Error = crate::cloud_worker::network::v2::NetworkSubnetListBuilderError; - fn try_from(value: &NetworkResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = NetworkSubnetListBuilder::default(); - if let Some(val) = &value.id { - builder.network_id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.network_id(val.to_string()); } - if let Some(val) = &value.name { - builder.network_name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.network_name(val.to_string()); } builder.build() } @@ -43,7 +36,6 @@ impl TryFrom<&NetworkResponse> for NetworkSubnetList { pub struct NetworkNetworksBehaviour; impl ResourceBehaviour for NetworkNetworksBehaviour { - type Item = NetworkResponse; type Filter = cloud_types::NetworkNetworkList; fn view_key() -> &'static str { @@ -73,7 +65,7 @@ impl ResourceBehaviour for NetworkNetworksBehaviour { } fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, _filter: &Self::Filter, ) -> Vec { if let Action::ShowResource(key) = action @@ -99,10 +91,9 @@ pub type NetworkNetworks = GenericResourceView<'static, NetworkNetworksBehaviour mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::network::v2::network::response::list::NetworkResponse; - fn make_network(id: &str, name: &str) -> NetworkResponse { - let json = serde_json::json!({ + fn make_network(id: &str, name: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "tenant_id": "tenant1", @@ -112,8 +103,7 @@ mod tests { "status": "ACTIVE", "admin_state_up": true, "shared": false - }); - serde_json::from_value(json).unwrap() + }) } #[test] diff --git a/openstack_tui/src/components/network/routers.rs b/openstack_tui/src/components/network/routers.rs index f60990e69..414f3e0f2 100644 --- a/openstack_tui/src/components/network/routers.rs +++ b/openstack_tui/src/components/network/routers.rs @@ -17,18 +17,10 @@ use crate::cloud_worker::types::{self as cloud_types, ApiRequest}; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::{GeneratedResourceBehaviour, ResourceBehaviour}; use crate::mode::Mode; -use openstack_types::network::v2::router::response::list::RouterResponse; - -impl crate::utils::ResourceKey for RouterResponse { - fn get_key() -> &'static str { - crate::mode::NETWORK_ROUTER - } -} pub struct NetworkRoutersBehaviour; impl ResourceBehaviour for NetworkRoutersBehaviour { - type Item = RouterResponse; type Filter = cloud_types::NetworkRouterList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/network/security_group_rules.rs b/openstack_tui/src/components/network/security_group_rules.rs index 50f102946..80911d6a0 100644 --- a/openstack_tui/src/components/network/security_group_rules.rs +++ b/openstack_tui/src/components/network/security_group_rules.rs @@ -23,21 +23,14 @@ use crate::components::resource_behaviour::{ GeneratedResourceBehaviour, Mutation, ResourceBehaviour, }; use crate::mode::Mode; -use openstack_types::network::v2::security_group_rule::response::list::SecurityGroupRuleResponse; use serde_json::Value; -impl crate::utils::ResourceKey for SecurityGroupRuleResponse { - fn get_key() -> &'static str { - crate::mode::NETWORK_SECURITY_GROUP_RULE - } -} - -impl TryFrom<&SecurityGroupRuleResponse> for NetworkSecurityGroupRuleDelete { +impl TryFrom<&Value> for NetworkSecurityGroupRuleDelete { type Error = crate::cloud_worker::network::v2::NetworkSecurityGroupRuleDeleteBuilderError; - fn try_from(value: &SecurityGroupRuleResponse) -> Result { + fn try_from(value: &Value) -> Result { let mut builder = NetworkSecurityGroupRuleDeleteBuilder::default(); - if let Some(val) = &value.id { - builder.id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.id(val.to_string()); } builder.build() } @@ -46,7 +39,6 @@ impl TryFrom<&SecurityGroupRuleResponse> for NetworkSecurityGroupRuleDelete { pub struct NetworkSecurityGroupRulesBehaviour; impl ResourceBehaviour for NetworkSecurityGroupRulesBehaviour { - type Item = SecurityGroupRuleResponse; type Filter = NetworkSecurityGroupRuleList; fn view_key() -> &'static str { @@ -79,7 +71,7 @@ impl ResourceBehaviour for NetworkSecurityGroupRulesBehaviour { } filter } - fn confirm_request(action: &Action, selected: Option<&Self::Item>) -> Option { + fn confirm_request(action: &Action, selected: Option<&Value>) -> Option { if let Action::ResourceOp { key, op: crate::action::ResourceOp::Delete, @@ -120,11 +112,9 @@ pub type NetworkSecurityGroupRules = mod tests { use super::*; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::network::v2::security_group_rule::response::list::SecurityGroupRuleResponse; - fn make_rule(id: &str) -> SecurityGroupRuleResponse { - let json = serde_json::json!({ "id": id }); - serde_json::from_value(json).unwrap() + fn make_rule(id: &str) -> serde_json::Value { + serde_json::json!({ "id": id }) } #[test] diff --git a/openstack_tui/src/components/network/security_groups.rs b/openstack_tui/src/components/network/security_groups.rs index 482e6c47b..f8a312481 100644 --- a/openstack_tui/src/components/network/security_groups.rs +++ b/openstack_tui/src/components/network/security_groups.rs @@ -20,23 +20,16 @@ use crate::cloud_worker::types::ApiRequest; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::{GeneratedResourceBehaviour, ResourceBehaviour}; use crate::mode::Mode; -use openstack_types::network::v2::security_group::response::list::SecurityGroupResponse; -impl crate::utils::ResourceKey for SecurityGroupResponse { - fn get_key() -> &'static str { - crate::mode::NETWORK_SECURITY_GROUP - } -} - -impl TryFrom<&SecurityGroupResponse> for NetworkSecurityGroupRuleList { +impl TryFrom<&serde_json::Value> for NetworkSecurityGroupRuleList { type Error = crate::cloud_worker::network::v2::NetworkSecurityGroupRuleListBuilderError; - fn try_from(value: &SecurityGroupResponse) -> Result { + fn try_from(value: &serde_json::Value) -> Result { let mut builder = NetworkSecurityGroupRuleListBuilder::default(); - if let Some(val) = &value.id { - builder.security_group_id(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/id") { + builder.security_group_id(val.to_string()); } - if let Some(val) = &value.name { - builder.security_group_name(val.clone()); + if let Some(val) = crate::components::view_render::get_str(value, "/name") { + builder.security_group_name(val.to_string()); } builder.build() } @@ -45,7 +38,6 @@ impl TryFrom<&SecurityGroupResponse> for NetworkSecurityGroupRuleList { pub struct NetworkSecurityGroupsBehaviour; impl ResourceBehaviour for NetworkSecurityGroupsBehaviour { - type Item = SecurityGroupResponse; type Filter = NetworkSecurityGroupList; fn view_key() -> &'static str { @@ -72,7 +64,7 @@ impl ResourceBehaviour for NetworkSecurityGroupsBehaviour { } fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&serde_json::Value>, _filter: &Self::Filter, ) -> Vec { if let Action::ShowResource(key) = action @@ -99,10 +91,9 @@ mod tests { use super::*; use crate::cloud_worker::network::v2::{NetworkApiRequest, NetworkSecurityGroupApiRequest}; use crate::components::resource_behaviour::ResourceBehaviour; - use openstack_types::network::v2::security_group::response::list::SecurityGroupResponse; - fn make_sg(id: &str, name: &str) -> SecurityGroupResponse { - let json = serde_json::json!({ + fn make_sg(id: &str, name: &str) -> serde_json::Value { + serde_json::json!({ "id": id, "name": name, "description": "test sg", @@ -110,8 +101,7 @@ mod tests { "security_group_rules": [], "created_at": "2024-01-01T00:00:00", "updated_at": "2024-01-01T00:00:00" - }); - serde_json::from_value(json).unwrap() + }) } #[test] diff --git a/openstack_tui/src/components/network/subnets.rs b/openstack_tui/src/components/network/subnets.rs index ebab656c6..aab84d7c5 100644 --- a/openstack_tui/src/components/network/subnets.rs +++ b/openstack_tui/src/components/network/subnets.rs @@ -17,18 +17,10 @@ use crate::cloud_worker::types::{self as cloud_types, ApiRequest}; use crate::components::generic_resource_view::GenericResourceView; use crate::components::resource_behaviour::{GeneratedResourceBehaviour, ResourceBehaviour}; use crate::mode::Mode; -use openstack_types::network::v2::subnet::response::list::SubnetResponse; - -impl crate::utils::ResourceKey for SubnetResponse { - fn get_key() -> &'static str { - crate::mode::NETWORK_SUBNET - } -} pub struct NetworkSubnetsBehaviour; impl ResourceBehaviour for NetworkSubnetsBehaviour { - type Item = SubnetResponse; type Filter = cloud_types::NetworkSubnetList; fn view_key() -> &'static str { diff --git a/openstack_tui/src/components/resource_behaviour.rs b/openstack_tui/src/components/resource_behaviour.rs index c81c0f840..98a1009a5 100644 --- a/openstack_tui/src/components/resource_behaviour.rs +++ b/openstack_tui/src/components/resource_behaviour.rs @@ -15,9 +15,6 @@ use crate::action::Action; use crate::cloud_worker::types::ApiRequest; use crate::mode::Mode; -use crate::utils::ResourceKey; -use openstack_sdk::types::ApiVersion; -use serde::de::DeserializeOwned; use serde_json::Value; use std::fmt::Display; @@ -25,7 +22,6 @@ use std::fmt::Display; /// metadata (view key, request/filter types, mode). Intended to be implemented by a generated /// `Generated` companion type per resource; `ResourceBehaviour`'s defaults delegate to it. pub trait GeneratedResourceBehaviour { - type Item: ResourceKey + DeserializeOwned; type Filter: Default + Display + Clone; /// The view configuration key used for persisting column/field settings. @@ -56,7 +52,6 @@ pub trait GeneratedResourceBehaviour { /// They stay required, non-default methods on this trait itself so that resources without a /// generated companion module yet are unaffected. pub trait ResourceBehaviour { - type Item: ResourceKey + DeserializeOwned; type Filter: Default + Display + Clone; /// The view configuration key used for persisting column/field settings. @@ -85,7 +80,7 @@ pub trait ResourceBehaviour { /// Translate an incoming Action (that is not a generic UI action) into an optional ApiRequest. /// Return `None` if the action is not handled specially for this resource. - fn action_to_request(action: &Action, selected: Option<&Self::Item>) -> Option { + fn action_to_request(action: &Action, selected: Option<&Value>) -> Option { let _ = (action, selected); None } @@ -94,7 +89,7 @@ pub trait ResourceBehaviour { /// The `filter` parameter provides access to the current filter state for sub-view drill actions. fn filter_carry_action( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&Value>, filter: &Self::Filter, ) -> Vec { let _ = (action, selected, filter); @@ -102,7 +97,7 @@ pub trait ResourceBehaviour { } /// Return custom Actions (deprecated, use filter_carry_action instead for filter access). - fn custom_action(action: &Action, selected: Option<&Self::Item>) -> Vec { + fn custom_action(action: &Action, selected: Option<&Value>) -> Vec { let _ = (action, selected); Vec::new() } @@ -121,7 +116,7 @@ pub trait ResourceBehaviour { /// returning the (display actions, api request) tuple. Default returns None. fn action_to_singular_request( action: &Action, - selected: Option<&Self::Item>, + selected: Option<&Value>, ) -> Option<(Vec, ApiRequest)> { let _ = (action, selected); None @@ -142,7 +137,7 @@ pub trait ResourceBehaviour { /// Translate an Action into a confirmable ApiRequest (e.g., delete). Return Some(ApiRequest) /// to send via Action::Confirm instead of Action::PerformApiRequest. - fn confirm_request(action: &Action, selected: Option<&Self::Item>) -> Option { + fn confirm_request(action: &Action, selected: Option<&Value>) -> Option { let _ = (action, selected); None } @@ -159,20 +154,6 @@ pub trait ResourceBehaviour { fn clear_data_on_filter_change() -> bool { false } - - /// Deserialize a list response batch into `Self::Item`, given the microversion actually - /// negotiated for the request that produced it (`None` for unversioned resources, or when the - /// worker didn't resolve it). The default ignores `negotiated_version` and deserializes as-is, - /// which is correct for every resource whose response schema does not vary by microversion. - /// Resources with real microversion-variant response schemas override this to pick the - /// variant matching `negotiated_version` before upcasting into the canonical `Self::Item`. - fn deserialize_items( - data: &[Value], - negotiated_version: Option, - ) -> serde_json::Result> { - let _ = negotiated_version; - serde_json::from_value(Value::Array(data.to_vec())) - } } /// Result of handling a mutation API response. @@ -190,19 +171,6 @@ pub enum Mutation { #[cfg(test)] mod tests { use super::*; - use serde::Deserialize; - - #[derive(Debug, Deserialize, Default, Clone, PartialEq)] - struct Item { - id: String, - #[serde(default)] - extra: Option, - } - impl ResourceKey for Item { - fn get_key() -> &'static str { - "test.item" - } - } #[derive(Debug, Default, Clone)] struct Filter; @@ -214,7 +182,6 @@ mod tests { struct DefaultBehaviour; impl ResourceBehaviour for DefaultBehaviour { - type Item = Item; type Filter = Filter; fn view_key() -> &'static str { "test.item" @@ -233,86 +200,24 @@ mod tests { } } - struct VersionedBehaviour; - impl ResourceBehaviour for VersionedBehaviour { - type Item = Item; - type Filter = Filter; - fn view_key() -> &'static str { - "test.item" - } - fn title() -> &'static str { - "Items" - } - fn mode() -> Mode { - Mode::Resource(Self::view_key()) - } - fn request_from_filter(_filter: &Self::Filter) -> ApiRequest { - unimplemented!() - } - fn matches_request(_request: &ApiRequest) -> bool { - false - } - fn deserialize_items( - data: &[Value], - negotiated_version: Option, - ) -> serde_json::Result> { - let mut items: Vec = serde_json::from_value(Value::Array(data.to_vec()))?; - if negotiated_version - .is_some_and(|v| v >= ApiVersion::from_apiver_str("2.5", false).unwrap()) - { - for item in &mut items { - item.extra = Some("present-since-2.5".into()); - } - } - Ok(items) - } + #[test] + fn view_key_and_title() { + assert_eq!(DefaultBehaviour::view_key(), "test.item"); + assert_eq!(DefaultBehaviour::title(), "Items"); + assert_eq!(DefaultBehaviour::mode(), Mode::Resource("test.item")); } #[test] - fn default_deserialize_items_ignores_negotiated_version() { - let data = vec![serde_json::json!({"id": "a"})]; - let items = DefaultBehaviour::deserialize_items(&data, None).unwrap(); - assert_eq!( - items, - vec![Item { - id: "a".into(), - extra: None - }] - ); - - let items = DefaultBehaviour::deserialize_items( - &data, - Some(ApiVersion::from_apiver_str("2.99", false).unwrap()), - ) - .unwrap(); - assert_eq!( - items, - vec![Item { - id: "a".into(), - extra: None - }] - ); + fn action_to_request_default_is_none() { + let value = serde_json::json!({"id": "a"}); + assert!(DefaultBehaviour::action_to_request(&Action::Tick, Some(&value)).is_none()); } #[test] - fn overridden_deserialize_items_dispatches_on_negotiated_version() { - let data = vec![serde_json::json!({"id": "a"})]; - - let items = VersionedBehaviour::deserialize_items(&data, None).unwrap(); - assert_eq!(items[0].extra, None); - - let items = VersionedBehaviour::deserialize_items( - &data, - Some(ApiVersion::from_apiver_str("2.1", false).unwrap()), - ) - .unwrap(); - assert_eq!(items[0].extra, None); - - let items = VersionedBehaviour::deserialize_items( - &data, - Some(ApiVersion::from_apiver_str("2.5", false).unwrap()), - ) - .unwrap(); - assert_eq!(items[0].extra, Some("present-since-2.5".into())); + fn filter_carry_action_default_is_empty() { + let value = serde_json::json!({"id": "a"}); + assert!( + DefaultBehaviour::filter_carry_action(&Action::Tick, Some(&value), &Filter).is_empty() + ); } } diff --git a/openstack_tui/src/components/resource_key_impls.rs b/openstack_tui/src/components/resource_key_impls.rs deleted file mode 100644 index 3ccffb33a..000000000 --- a/openstack_tui/src/components/resource_key_impls.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::utils::ResourceKey; -use openstack_types::block_storage::v3::backup::response::list_detailed::BackupResponse; -use openstack_types::block_storage::v3::snapshot::response::list_detailed::SnapshotResponse; -use openstack_types::compute::v2::aggregate::response::list_241::AggregateResponse; -use openstack_types::compute::v2::hypervisor::response::list_detailed_253::HypervisorResponse; -use openstack_types::compute::v2::server::response::list_detailed_21::ServerResponse; - -impl ResourceKey for ServerResponse { - fn get_key() -> &'static str { - "compute.server" - } -} - -impl ResourceKey for AggregateResponse { - fn get_key() -> &'static str { - "compute.aggregate" - } -} - -impl ResourceKey for HypervisorResponse { - fn get_key() -> &'static str { - "compute.hypervisor" - } -} - -impl ResourceKey for BackupResponse { - fn get_key() -> &'static str { - "block_storage.backup" - } -} - -impl ResourceKey for SnapshotResponse { - fn get_key() -> &'static str { - "block_storage.snapshot" - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::utils::ResourceKey; - - #[test] - fn server_response_key() { - assert_eq!(ServerResponse::get_key(), "compute.server"); - } - - #[test] - fn aggregate_response_key() { - assert_eq!(AggregateResponse::get_key(), "compute.aggregate"); - } - - #[test] - fn hypervisor_response_key() { - assert_eq!(HypervisorResponse::get_key(), "compute.hypervisor"); - } - - #[test] - fn backup_response_key() { - assert_eq!(BackupResponse::get_key(), "block_storage.backup"); - } - - #[test] - fn snapshot_response_key() { - assert_eq!(SnapshotResponse::get_key(), "block_storage.snapshot"); - } -} diff --git a/openstack_tui/src/components/table_view.rs b/openstack_tui/src/components/table_view.rs index ba7265281..73bb91079 100644 --- a/openstack_tui/src/components/table_view.rs +++ b/openstack_tui/src/components/table_view.rs @@ -17,10 +17,8 @@ use eyre::Result; use itertools::Itertools; use openstack_sdk::types::EntryStatus; use ratatui::{prelude::*, style::palette::tailwind, widgets::*}; -use serde::de::DeserializeOwned; use serde_json::Value; use std::{cmp, fmt::Display}; -use structable::{StructTable, build_list_table}; use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, instrument}; @@ -30,7 +28,6 @@ use crate::{ config::{Config, ViewConfig}, error::TuiError, mode::Mode, - utils::ResourceKey, }; const ITEM_HEIGHT: usize = 1; @@ -43,20 +40,17 @@ enum Focus { Describe, } -pub struct TableViewComponentBase<'a, T, F> +pub struct TableViewComponentBase<'a, F> where - T: StructTable, - T: DeserializeOwned, - T: ResourceKey, F: Default + Display, { command_tx: Option>, pub config: Config, + view_key: &'static str, state: TableState, scroll_state: ScrollbarState, - items: Vec, raw_items: Vec, filter: F, @@ -72,33 +66,16 @@ where focus: Focus, } -impl Default for TableViewComponentBase<'_, T, F> +impl TableViewComponentBase<'_, F> where - T: StructTable, - for<'a> &'a T: StructTable, - T: DeserializeOwned, - T: ResourceKey, F: Default + Display, { - fn default() -> Self { - Self::new() - } -} - -impl TableViewComponentBase<'_, T, F> -where - T: StructTable, - for<'a> &'a T: StructTable, - T: DeserializeOwned, - T: ResourceKey, - F: Default + Display, -{ - pub fn new() -> Self { + pub fn new(view_key: &'static str) -> Self { Self { command_tx: None, config: Config::default(), + view_key, state: TableState::default().with_selected(0), - items: Vec::new(), raw_items: Vec::new(), filter: F::default(), scroll_state: ScrollbarState::new(0), @@ -128,7 +105,7 @@ where } pub fn get_output_config(&mut self) -> &mut ViewConfig { - self.config.views.entry(T::get_key().into()).or_default() + self.config.views.entry(self.view_key.into()).or_default() } pub fn set_command_tx(&mut self, tx: UnboundedSender) -> Result<(), TuiError> { @@ -165,7 +142,8 @@ where pub fn cursor_last(&mut self) -> Result<(), TuiError> { match self.focus { Focus::Table => { - self.state.select(Some(self.items.len().saturating_sub(1))); + self.state + .select(Some(self.raw_items.len().saturating_sub(1))); self.scroll_state.last(); self.set_describe_content()?; } @@ -181,7 +159,7 @@ where Focus::Table => { let i = match self.state.selected() { Some(i) => { - if i < self.items.len() - 1 { + if i < self.raw_items.len() - 1 { i + 1 } else { i @@ -224,7 +202,7 @@ where let i = match self.state.selected() { Some(i) => cmp::min( i.saturating_add(self.content_size.height as usize), - self.items.len() - 1, + self.raw_items.len() - 1, ), None => 0, }; @@ -321,23 +299,10 @@ where pub fn set_data(&mut self, data: Vec) -> Result<(), TuiError> { if data != self.raw_items { - let items = serde_json::from_value::>(serde_json::Value::Array(data.clone()))?; - return self.set_data_items(items, data); - } - self.set_loading(false); - Ok(()) - } - - /// Like `set_data`, but accepts already-deserialized items alongside the raw payload, for - /// callers that need to pick the deserialization function themselves (e.g. based on a - /// negotiated microversion) rather than deserializing into a single fixed type here. - pub fn set_data_items(&mut self, items: Vec, raw_data: Vec) -> Result<(), TuiError> { - if raw_data != self.raw_items { - self.items = items; - self.raw_items = raw_data; + self.raw_items = data; self.state.select_first(); self.scroll_state = - ScrollbarState::new(self.items.len().saturating_sub(1) * ITEM_HEIGHT); + ScrollbarState::new(self.raw_items.len().saturating_sub(1) * ITEM_HEIGHT); self.sync_table_data()?; } self.set_loading(false); @@ -411,16 +376,26 @@ where (headers, rows, column_constrains) } - /// Synchronize table data from internal vector of typed entries + /// Synchronize table data from internal vector of raw entries pub fn sync_table_data(&mut self) -> Result<(), TuiError> { let view_config = self.get_output_config().clone(); - let data = build_list_table(self.items.iter(), &view_config); - let (table_headers, table_rows, _table_constraints) = self.prepare_table(data.0, data.1); + let headers = crate::components::view_render::headers(&view_config); + let rows: Vec> = self + .raw_items + .iter() + .map(|item| { + crate::components::view_render::row(item, &view_config) + .into_iter() + .map(|cell| cell.unwrap_or_default()) + .collect() + }) + .collect(); let mut statuses: Vec> = self - .items + .raw_items .iter() - .map(structable::StructTable::status) + .map(|item| crate::components::view_render::status(item, &view_config)) .collect(); + let (table_headers, table_rows, _table_constraints) = self.prepare_table(headers, rows); // Ensure we have as many statuses as rows to zip them properly statuses.resize_with(table_rows.len(), Default::default); @@ -473,21 +448,17 @@ where /// Update single record with the new data pub fn update_row_data(&mut self, data: Value) -> Result<(), TuiError> { - let updated_item: T = serde_json::from_value(data.clone())?; let updated_entry_id = data .get("id") .or(data.get("uuid")) .ok_or_else(|| TuiError::EntryIdNotPresent(data.clone()))?; - for (idx, raw_item) in self.raw_items.iter_mut().enumerate() { + for raw_item in self.raw_items.iter_mut() { if let Some(row_id) = raw_item.get("id").or(raw_item.get("uuid")) && row_id == updated_entry_id { *raw_item = data.clone(); - if let Some(typed_row) = self.items.get_mut(idx) { - *typed_row = updated_item; - self.sync_table_data()?; - break; - } + self.sync_table_data()?; + break; } } self.set_loading(false); @@ -557,7 +528,7 @@ where f.render_stateful_widget(t, area, &mut self.state); - if usize::from(self.content_size.height) < self.items.len() { + if usize::from(self.content_size.height) < self.raw_items.len() { self.render_scrollbar(f, area)?; } Ok(()) @@ -611,7 +582,7 @@ where )); } else { title.push(Span::styled( - format!(" ({}) ", self.items.len()), + format!(" ({}) ", self.raw_items.len()), self.config.styles.title_details_fg, )); } @@ -648,50 +619,40 @@ where Ok(()) } - pub fn get_selected(&self) -> Option<&T> { - self.state.selected().and_then(|idx| self.items.get(idx)) + pub fn get_selected(&self) -> Option<&Value> { + self.state + .selected() + .and_then(|idx| self.raw_items.get(idx)) } - /// Get mutable reference to the row with the typed data matching resource id + /// Get mutable reference to the row matching resource id #[instrument(level = "debug", skip(self))] - pub fn get_item_row_by_res_id_mut(&mut self, search_id: &String) -> Option<&mut T> { - for (idx, raw_item) in self.raw_items.iter_mut().enumerate() { - if let Some(row_item_id) = raw_item.get("id").or(raw_item.get("uuid")) - && row_item_id == search_id - { - return self.items.get_mut(idx); - } - } - None + pub fn get_item_row_by_res_id_mut(&mut self, search_id: &String) -> Option<&mut Value> { + self.raw_items.iter_mut().find(|raw_item| { + raw_item + .get("id") + .or(raw_item.get("uuid")) + .is_some_and(|row_id| row_id == search_id) + }) } - /// delete the row with the typed data matching resource id + /// delete the row matching resource id #[instrument(level = "debug", skip(self))] pub fn delete_item_row_by_res_id_mut(&mut self, search_id: &String) -> Result> { - let mut item_idx: Option = None; - for (idx, raw_item) in self.raw_items.iter_mut().enumerate() { - if let Some(row_item_id) = raw_item.get("id").or(raw_item.get("uuid")) - && row_item_id == search_id - { - item_idx = Some(idx); - break; - } - } + let item_idx = self.raw_items.iter().position(|raw_item| { + raw_item + .get("id") + .or(raw_item.get("uuid")) + .is_some_and(|row_id| row_id == search_id) + }); if let Some(idx) = item_idx { self.raw_items.remove(idx); - self.items.remove(idx); } Ok(item_idx) } - pub fn get_selected_raw(&self) -> Option<&Value> { - self.state - .selected() - .and_then(|idx| self.raw_items.get(idx)) - } - pub fn get_selected_resource_id(&self) -> Result, TuiError> { - self.get_selected_raw() + self.get_selected() .map(|entry| { entry .get("id") @@ -704,7 +665,7 @@ where pub fn describe_selected_entry(&self) -> Result<(), TuiError> { if let Some(command_tx) = self.get_command_tx() { // and have a selected entry - if let Some(raw_value) = self.get_selected_raw() { + if let Some(raw_value) = self.get_selected() { command_tx.send(Action::Mode { mode: Mode::Describe, stack: true, @@ -722,9 +683,7 @@ where /// append new row. #[instrument(level = "debug", skip(self))] pub fn append_new_row(&mut self, data: Value) -> Result<(), TuiError> { - let item = serde_json::from_value::(data.clone())?; - self.items.push(item); - self.raw_items.push(data.clone()); + self.raw_items.push(data); self.sync_table_data()?; self.set_loading(false); Ok(()) diff --git a/openstack_tui/src/components/view_render.rs b/openstack_tui/src/components/view_render.rs new file mode 100644 index 000000000..2b719c959 --- /dev/null +++ b/openstack_tui/src/components/view_render.rs @@ -0,0 +1,191 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Every TUI resource's `Item` is `serde_json::Value`. Column set/titles/pointers/status/id are +//! computed fresh from `.config/config.yaml`'s `ViewConfig` on every call here -- nothing baked +//! into Rust source, nothing generated by codegen. Table columns tolerate whatever shape a +//! response actually has at any microversion, since they read the raw JSON by pointer instead of +//! deserializing into a typed struct. + +use crate::config::ViewConfig; +use serde_json::Value; + +/// Stringify whatever's at `pointer` in `value`: `None` for missing/null, plain text for a JSON +/// string or other scalar, pretty-vs-compact JSON (per `view.pretty_mode()`) for an object/array. +fn stringify(value: &Value, pointer: &str, pretty: bool) -> Option { + match value.pointer(pointer) { + None | Some(Value::Null) => None, + Some(Value::String(s)) => Some(s.clone()), + Some(complex @ (Value::Object(_) | Value::Array(_))) => Some(if pretty { + serde_json::to_string_pretty(complex).unwrap_or_else(|_| complex.to_string()) + } else { + complex.to_string() + }), + Some(other) => Some(other.to_string()), + } +} + +/// Column list = `view.default_fields`, verbatim order and casing. +pub fn headers(view: &ViewConfig) -> Vec { + view.default_fields.clone() +} + +/// One row's cell values, same column list/order as `headers(view)`. Pointer is +/// `view.field_data_json_pointer(title)` if configured, else `/{title}`. +pub fn row(value: &Value, view: &ViewConfig) -> Vec> { + let pretty = view.pretty_mode(); + view.default_fields + .iter() + .map(|title| { + let pointer = view + .field_data_json_pointer(title) + .unwrap_or_else(|| format!("/{title}")); + stringify(value, &pointer, pretty) + }) + .collect() +} + +/// Row-status coloring hook: the value at `view.status_field`, plain text, `None` if unset or +/// missing. +pub fn status(value: &Value, view: &ViewConfig) -> Option { + let field = view.status_field.as_ref()?; + stringify(value, &format!("/{field}"), false) +} + +/// Row identifier for select/delete/describe. `view.id_field` if configured, else the `id`->`uuid` +/// fallback convention `table_view.rs` already used. +pub fn id(value: &Value, view: &ViewConfig) -> Option { + if let Some(field) = &view.id_field { + return get_str(value, &format!("/{field}")).map(String::from); + } + value + .get("id") + .or_else(|| value.get("uuid")) + .and_then(Value::as_str) + .map(String::from) +} + +pub fn get<'a>(value: &'a Value, pointer: &str) -> Option<&'a Value> { + value.pointer(pointer) +} + +pub fn get_str<'a>(value: &'a Value, pointer: &str) -> Option<&'a str> { + get(value, pointer).and_then(Value::as_str) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn cfg( + default_fields: &[&str], + status_field: Option<&str>, + id_field: Option<&str>, + ) -> ViewConfig { + ViewConfig { + default_fields: default_fields.iter().map(|s| s.to_string()).collect(), + status_field: status_field.map(String::from), + id_field: id_field.map(String::from), + fields: vec![], + } + } + + #[test] + fn headers_returns_default_fields_verbatim_order() { + let view = cfg(&["name", "id", "status"], None, None); + assert_eq!(headers(&view), vec!["name", "id", "status"]); + } + + #[test] + fn row_reads_scalar_and_missing_columns() { + let view = cfg(&["id", "missing"], None, None); + let value = serde_json::json!({"id": "abc"}); + assert_eq!(row(&value, &view), vec![Some("abc".to_string()), None]); + } + + #[test] + fn row_respects_json_pointer_override() { + use crate::config::FieldConfig; + let view = ViewConfig { + default_fields: vec!["flavor".into()], + fields: vec![FieldConfig { + name: "flavor".into(), + json_pointer: Some("/original_name".into()), + ..Default::default() + }], + ..Default::default() + }; + let value = serde_json::json!({"flavor": {"id": "1"}, "original_name": "m1.small"}); + assert_eq!(row(&value, &view), vec![Some("m1.small".to_string())]); + } + + #[test] + fn row_pretty_prints_objects_and_arrays() { + let view = cfg(&["metadata"], None, None); + let value = serde_json::json!({"metadata": {"a": 1}}); + assert_eq!( + row(&value, &view), + vec![Some( + serde_json::to_string_pretty(&serde_json::json!({"a": 1})).unwrap() + )] + ); + } + + #[test] + fn status_reads_configured_field() { + let view = cfg(&["id"], Some("status"), None); + let value = serde_json::json!({"id": "abc", "status": "ACTIVE"}); + assert_eq!(status(&value, &view), Some("ACTIVE".to_string())); + } + + #[test] + fn status_none_when_unset_or_missing() { + let view = cfg(&["id"], None, None); + let value = serde_json::json!({"id": "abc"}); + assert_eq!(status(&value, &view), None); + + let view = cfg(&["id"], Some("status"), None); + let value = serde_json::json!({"id": "abc"}); + assert_eq!(status(&value, &view), None); + } + + #[test] + fn id_falls_back_to_id_then_uuid() { + let view = cfg(&[], None, None); + assert_eq!( + id(&serde_json::json!({"id": "a"}), &view), + Some("a".to_string()) + ); + assert_eq!( + id(&serde_json::json!({"uuid": "b"}), &view), + Some("b".to_string()) + ); + assert_eq!(id(&serde_json::json!({}), &view), None); + } + + #[test] + fn id_uses_configured_id_field_when_set() { + let view = cfg(&[], None, Some("hypervisor_hostname")); + let value = serde_json::json!({"id": "1", "hypervisor_hostname": "host-a"}); + assert_eq!(id(&value, &view), Some("host-a".to_string())); + } + + #[test] + fn get_and_get_str_read_by_pointer() { + let value = serde_json::json!({"id": "abc"}); + assert_eq!(get(&value, "/id"), Some(&Value::String("abc".into()))); + assert_eq!(get_str(&value, "/id"), Some("abc")); + assert_eq!(get_str(&value, "/missing"), None); + } +} diff --git a/openstack_tui/src/config.rs b/openstack_tui/src/config.rs index 788f7aaa9..5668a550a 100644 --- a/openstack_tui/src/config.rs +++ b/openstack_tui/src/config.rs @@ -25,7 +25,6 @@ use std::{ collections::{BTreeMap, HashMap}, path::{Path, PathBuf}, }; -use structable::StructTableOptions; use thiserror::Error; use tracing::error; @@ -194,18 +193,16 @@ pub struct ViewConfig { /// Limit fields (their titles) to be returned #[serde(default)] pub default_fields: Vec, - /// Extra fields (their titles) only shown in wide mode, on top of `default_fields` - #[serde(default)] - pub wide_fields: Vec, /// Field (its title) backing row-status coloring, if any #[serde(default)] pub status_field: Option, + /// JSON pointer (or "id"/"uuid" fallback if unset) identifying each row, used for + /// select/delete/describe by id. + #[serde(default)] + pub id_field: Option, /// Fields configurations #[serde(default)] pub fields: Vec, - /// Defaults to wide mode - #[serde(default)] - pub wide: Option, } /// Field output configuration @@ -278,30 +275,12 @@ impl Config { } } -impl StructTableOptions for ViewConfig { - fn wide_mode(&self) -> bool { - self.wide.unwrap_or_default() - } - - fn pretty_mode(&self) -> bool { +impl ViewConfig { + pub fn pretty_mode(&self) -> bool { true } - fn should_return_field>(&self, field: S, is_wide_field: bool) -> bool { - let field = field.as_ref().to_lowercase(); - if self - .default_fields - .iter() - .any(|x| x.to_lowercase() == field) - { - return true; - } - is_wide_field - && self.wide_mode() - && self.wide_fields.iter().any(|x| x.to_lowercase() == field) - } - - fn field_data_json_pointer>(&self, field: S) -> Option { + pub fn field_data_json_pointer>(&self, field: S) -> Option { self.fields .iter() .find(|x| x.name.to_lowercase() == field.as_ref().to_lowercase()) @@ -1054,57 +1033,37 @@ mod tests { } #[test] - fn should_return_field_default_field_always_shown() { - let view = ViewConfig { + fn view_config_has_no_wide_fields_or_wide() { + let cfg = ViewConfig { default_fields: vec!["id".into()], - ..Default::default() + status_field: None, + id_field: Some("uuid".into()), + fields: vec![], }; - assert!(view.should_return_field("id", false)); - assert!(view.should_return_field("ID", false)); + assert_eq!(cfg.default_fields, vec!["id".to_string()]); + assert_eq!(cfg.id_field, Some("uuid".to_string())); } #[test] - fn should_return_field_wide_field_hidden_without_wide_mode() { - let view = ViewConfig { - default_fields: vec!["id".into()], - wide_fields: vec!["swap".into()], - wide: Some(false), - ..Default::default() - }; - assert!(!view.should_return_field("swap", true)); + fn pretty_mode_is_always_true() { + let cfg = ViewConfig::default(); + assert!(cfg.pretty_mode()); } #[test] - fn should_return_field_wide_field_shown_in_wide_mode() { - let view = ViewConfig { - default_fields: vec!["id".into()], - wide_fields: vec!["swap".into()], - wide: Some(true), + fn field_data_json_pointer_returns_configured_override() { + let cfg = ViewConfig { + fields: vec![FieldConfig { + name: "flavor".into(), + json_pointer: Some("/original_name".into()), + ..Default::default() + }], ..Default::default() }; - assert!(view.should_return_field("swap", true)); - } - - #[test] - fn should_return_field_unlisted_field_never_shown() { - let view = ViewConfig { - default_fields: vec!["id".into()], - wide: Some(true), - ..Default::default() - }; - assert!(!view.should_return_field("unknown", false)); - assert!(!view.should_return_field("unknown", true)); - } - - #[test] - fn should_return_field_no_wide_fields_configured_no_regression() { - // Resources with no `wide_fields` entry keep today's behavior: a compile-time - // `#[structable(wide)]` field is never shown via this options impl, wide mode or not. - let view = ViewConfig { - default_fields: vec!["id".into()], - wide: Some(true), - ..Default::default() - }; - assert!(!view.should_return_field("some_wide_struct_field", true)); + assert_eq!( + cfg.field_data_json_pointer("Flavor"), + Some("/original_name".to_string()) + ); + assert_eq!(cfg.field_data_json_pointer("missing"), None); } } diff --git a/openstack_tui/src/utils.rs b/openstack_tui/src/utils.rs index 8c77c8094..a9c4eb31f 100644 --- a/openstack_tui/src/utils.rs +++ b/openstack_tui/src/utils.rs @@ -22,11 +22,6 @@ use tracing_subscriber::{ }; const VERSION_MESSAGE: &str = concat!(env!("CARGO_PKG_VERSION"),); -pub trait ResourceKey { - fn get_key() -> &'static str { - "" - } -} lazy_static! { pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().clone();