|
1 | 1 | use crate::cloud::client::{CloudClient, CloudError}; |
2 | 2 | use crate::cloud::credentials; |
3 | 3 | use crate::cloud::output::{ABSENT, or_absent, print_human}; |
| 4 | +use crate::cloud::shared::{parse_serde_enum, parse_tags, resolve_org_id}; |
4 | 5 | use clickhouse_cloud_api::models::{ |
5 | 6 | ApiKeyPatchRequest, ApiKeyPatchRequestState, ApiKeyPostRequest, ApiKeyPostRequestState, |
6 | 7 | AutoscalingMode, BackupConfigurationPatchRequest, InstancePrivateEndpointsPatch, |
7 | 8 | InstanceServiceQueryApiEndpointsPostRequest, InstanceTagsPatch, IpAccessListEntry, |
8 | 9 | IpAccessListPatch, OrganizationPatchPrivateEndpoint, |
9 | 10 | OrganizationPatchPrivateEndpointCloudprovider, OrganizationPatchPrivateEndpointRegion, |
10 | | - OrganizationPatchRequest, OrganizationPrivateEndpointsPatch, ResourceTagsV1, |
11 | | - ServicPrivateEndpointePostRequest, Service, ServiceEndpoint, ServiceEndpointChange, |
12 | | - ServiceEndpointChangeProtocol, ServicePasswordPatchRequest, ServicePatchRequest, |
13 | | - ServicePatchRequestReleasechannel, ServicePostRequest, ServicePostRequestCompliancetype, |
14 | | - ServicePostRequestProfile, ServicePostRequestProvider, ServicePostRequestRegion, |
15 | | - ServicePostRequestReleasechannel, ServiceReplicaScalingPatchRequest, ServiceState, |
16 | | - ServiceStatePatchRequestCommand, |
| 11 | + OrganizationPatchRequest, OrganizationPrivateEndpointsPatch, ServicPrivateEndpointePostRequest, |
| 12 | + Service, ServiceEndpoint, ServiceEndpointChange, ServiceEndpointChangeProtocol, |
| 13 | + ServicePasswordPatchRequest, ServicePatchRequest, ServicePatchRequestReleasechannel, |
| 14 | + ServicePostRequest, ServicePostRequestCompliancetype, ServicePostRequestProfile, |
| 15 | + ServicePostRequestProvider, ServicePostRequestRegion, ServicePostRequestReleasechannel, |
| 16 | + ServiceReplicaScalingPatchRequest, ServiceState, ServiceStatePatchRequestCommand, |
17 | 17 | }; |
18 | 18 | use std::io::{IsTerminal, Write}; |
19 | 19 | use tabled::{Table, Tabled, settings::Style}; |
@@ -46,17 +46,6 @@ fn first_endpoint(endpoints: Option<&[ServiceEndpoint]>) -> String { |
46 | 46 | .unwrap_or_else(|| ABSENT.to_string()) |
47 | 47 | } |
48 | 48 |
|
49 | | -/// Resolve org ID from explicit arg or auto-detect |
50 | | -pub(super) async fn resolve_org_id( |
51 | | - client: &CloudClient, |
52 | | - org_id: Option<&str>, |
53 | | -) -> Result<String, Box<dyn std::error::Error>> { |
54 | | - match org_id { |
55 | | - Some(id) => Ok(id.to_string()), |
56 | | - None => Ok(client.get_default_org_id().await?), |
57 | | - } |
58 | | -} |
59 | | - |
60 | 49 | /// Resolve a service by name or ID within the given org. |
61 | 50 | /// Exactly one of `name` or `id` must be provided. |
62 | 51 | async fn resolve_service( |
@@ -88,69 +77,6 @@ async fn resolve_service( |
88 | 77 | } |
89 | 78 | } |
90 | 79 |
|
91 | | -/// Parse a string into a library enum via serde deserialization, with client-side |
92 | | -/// validation against a known-values list. Library enums have an `Unknown(String)` |
93 | | -/// catch-all that prevents serde from ever failing, so we validate first. |
94 | | -pub(super) fn parse_serde_enum<T: serde::de::DeserializeOwned>( |
95 | | - value: &str, |
96 | | - field: &str, |
97 | | - known_values: &[&str], |
98 | | -) -> Result<T, Box<dyn std::error::Error>> { |
99 | | - if !known_values.contains(&value) { |
100 | | - return Err(format!( |
101 | | - "invalid {}: unknown value '{}', expected one of: {}", |
102 | | - field, |
103 | | - value, |
104 | | - known_values.join(", ") |
105 | | - ) |
106 | | - .into()); |
107 | | - } |
108 | | - serde_json::from_value(serde_json::Value::String(value.to_string())) |
109 | | - .map_err(|e| format!("invalid {}: {}", field, e).into()) |
110 | | -} |
111 | | - |
112 | | -pub(super) fn parse_tag(value: &str) -> Result<ResourceTagsV1, Box<dyn std::error::Error>> { |
113 | | - match value.split_once('=') { |
114 | | - Some((key, tag_value)) => { |
115 | | - let key = key.trim(); |
116 | | - if key.is_empty() { |
117 | | - Err(format!("invalid tag '{}': tag key cannot be empty", value).into()) |
118 | | - } else { |
119 | | - Ok(ResourceTagsV1 { |
120 | | - key: key.to_string(), |
121 | | - value: Some(tag_value.to_string()), |
122 | | - }) |
123 | | - } |
124 | | - } |
125 | | - None => { |
126 | | - let key = value.trim(); |
127 | | - if key.is_empty() { |
128 | | - Err(format!("invalid tag '{}': tag key cannot be empty", value).into()) |
129 | | - } else { |
130 | | - Ok(ResourceTagsV1 { |
131 | | - key: key.to_string(), |
132 | | - value: None, |
133 | | - }) |
134 | | - } |
135 | | - } |
136 | | - } |
137 | | -} |
138 | | - |
139 | | -pub(super) fn parse_tags( |
140 | | - values: &[String], |
141 | | -) -> Result<Option<Vec<ResourceTagsV1>>, Box<dyn std::error::Error>> { |
142 | | - if values.is_empty() { |
143 | | - Ok(None) |
144 | | - } else { |
145 | | - Ok(Some( |
146 | | - values |
147 | | - .iter() |
148 | | - .map(|value| parse_tag(value)) |
149 | | - .collect::<Result<Vec<_>, _>>()?, |
150 | | - )) |
151 | | - } |
152 | | -} |
153 | | - |
154 | 80 | fn parse_ip_access_entries(values: &[String]) -> Option<Vec<IpAccessListEntry>> { |
155 | 81 | (!values.is_empty()).then(|| { |
156 | 82 | values |
@@ -3869,21 +3795,6 @@ mod tests { |
3869 | 3795 | ); |
3870 | 3796 | } |
3871 | 3797 |
|
3872 | | - #[test] |
3873 | | - fn parse_tag_rejects_empty_keys() { |
3874 | | - let err = parse_tag("=value").unwrap_err(); |
3875 | | - assert_eq!( |
3876 | | - err.to_string(), |
3877 | | - "invalid tag '=value': tag key cannot be empty" |
3878 | | - ); |
3879 | | - |
3880 | | - let err = parse_tag(" ").unwrap_err(); |
3881 | | - assert_eq!( |
3882 | | - err.to_string(), |
3883 | | - "invalid tag ' ': tag key cannot be empty" |
3884 | | - ); |
3885 | | - } |
3886 | | - |
3887 | 3798 | #[test] |
3888 | 3799 | fn build_create_service_request_supports_ga_optional_fields() { |
3889 | 3800 | let opts = CreateServiceOptions { |
|
0 commit comments