Skip to main content

clickhouse_cloud_api/
convert.rs

1//! Explicit conversions from response models back into request models.
2//!
3//! Response models are tolerant by construction: every field is `Option<T>`, so
4//! a field the API drops or sends as `null` deserializes to `None` instead of
5//! failing. Request models are strict: a field the API requires is `T`.
6//!
7//! A caller that fetches a resource, edits it, and writes it back therefore has
8//! to resolve absence explicitly, and that is the point of this module — the old
9//! `#[serde(default)]` policy silently fabricated `""`/`0`/`false` for a dropped
10//! field and persisted it on the next write. A conversion that can lose that
11//! information is a [`TryFrom`] reporting the missing wire field names; a
12//! conversion that cannot is a [`From`].
13//!
14//! The ClickStack source tree converts as a group: [`TryFrom<ClickStackSourceResponse>`]
15//! for [`ClickStackSource`] is the entry point, and every nested object in that
16//! tree carries its own conversion so a missing field is named at the level it is
17//! missing from.
18
19use std::fmt;
20
21use crate::models::{
22    ClickStackAggregatedColumn, ClickStackAggregatedColumnResponse, ClickStackFilterSettingsColumn,
23    ClickStackFilterSettingsColumnResponse, ClickStackHighlightedAttributeExpression,
24    ClickStackHighlightedAttributeExpressionResponse, ClickStackLogSource,
25    ClickStackLogSourceMetadataMaterializedViews,
26    ClickStackLogSourceMetadataMaterializedViewsResponse, ClickStackLogSourceResponse,
27    ClickStackMaterializedView, ClickStackMaterializedViewResponse, ClickStackMetricSource,
28    ClickStackMetricSourceFrom, ClickStackMetricSourceFromResponse, ClickStackMetricSourceResponse,
29    ClickStackMetricTables, ClickStackMetricTablesResponse, ClickStackPromqlSource,
30    ClickStackPromqlSourceResponse, ClickStackQuerySetting, ClickStackQuerySettingResponse,
31    ClickStackSessionSource, ClickStackSessionSourceResponse, ClickStackSource,
32    ClickStackSourceFilterSettings, ClickStackSourceFilterSettingsResponse, ClickStackSourceFrom,
33    ClickStackSourceFromResponse, ClickStackSourceResponse, ClickStackTraceSource,
34    ClickStackTraceSourceMetadataMaterializedViews,
35    ClickStackTraceSourceMetadataMaterializedViewsResponse, ClickStackTraceSourceResponse,
36    PgBouncerConfig, PgBouncerConfigResponse, PgConfig, PgConfigResponse, PostgresInstanceConfig,
37    PostgresInstanceConfigResponse, ResourceTagsV1, ResourceTagsV1Response, ScalingScheduleEntry,
38    ScalingScheduleEntryRequest, UpgradeWindow, UpgradeWindowPutRequest,
39};
40
41/// The response omitted fields that the matching request model requires.
42///
43/// Field names are the wire (spec) names, so an error message points at the
44/// JSON the API returned rather than at Rust identifiers.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct MissingRequiredFields {
47    fields: Vec<&'static str>,
48}
49
50impl MissingRequiredFields {
51    pub(crate) fn new(fields: Vec<&'static str>) -> Self {
52        Self { fields }
53    }
54
55    /// The missing wire field names, in declaration order.
56    pub fn fields(&self) -> &[&'static str] {
57        &self.fields
58    }
59}
60
61impl fmt::Display for MissingRequiredFields {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(
64            f,
65            "the API response is missing required field(s): {}",
66            self.fields.join(", ")
67        )
68    }
69}
70
71impl std::error::Error for MissingRequiredFields {}
72
73impl TryFrom<ClickStackAggregatedColumnResponse> for ClickStackAggregatedColumn {
74    type Error = MissingRequiredFields;
75
76    fn try_from(value: ClickStackAggregatedColumnResponse) -> Result<Self, Self::Error> {
77        let mut missing = Vec::new();
78        if value.agg_fn.is_none() {
79            missing.push("aggFn");
80        }
81        if value.mv_column.is_none() {
82            missing.push("mvColumn");
83        }
84        match (value.agg_fn, value.mv_column) {
85            (Some(agg_fn), Some(mv_column)) => Ok(Self {
86                agg_fn,
87                mv_column,
88                source_column: value.source_column,
89            }),
90            _ => Err(MissingRequiredFields::new(missing)),
91        }
92    }
93}
94
95impl TryFrom<ClickStackFilterSettingsColumnResponse> for ClickStackFilterSettingsColumn {
96    type Error = MissingRequiredFields;
97
98    fn try_from(value: ClickStackFilterSettingsColumnResponse) -> Result<Self, Self::Error> {
99        let mut missing = Vec::new();
100        if value.label.is_none() {
101            missing.push("label");
102        }
103        if value.name.is_none() {
104            missing.push("name");
105        }
106        match (value.label, value.name) {
107            (Some(label), Some(name)) => Ok(Self { label, name }),
108            _ => Err(MissingRequiredFields::new(missing)),
109        }
110    }
111}
112
113impl TryFrom<ClickStackHighlightedAttributeExpressionResponse>
114    for ClickStackHighlightedAttributeExpression
115{
116    type Error = MissingRequiredFields;
117
118    fn try_from(
119        value: ClickStackHighlightedAttributeExpressionResponse,
120    ) -> Result<Self, Self::Error> {
121        let mut missing = Vec::new();
122        if value.sql_expression.is_none() {
123            missing.push("sqlExpression");
124        }
125        match value.sql_expression {
126            Some(sql_expression) => Ok(Self {
127                alias: value.alias,
128                lucene_expression: value.lucene_expression,
129                sql_expression,
130            }),
131            None => Err(MissingRequiredFields::new(missing)),
132        }
133    }
134}
135
136impl TryFrom<ClickStackLogSourceMetadataMaterializedViewsResponse>
137    for ClickStackLogSourceMetadataMaterializedViews
138{
139    type Error = MissingRequiredFields;
140
141    fn try_from(
142        value: ClickStackLogSourceMetadataMaterializedViewsResponse,
143    ) -> Result<Self, Self::Error> {
144        let mut missing = Vec::new();
145        if value.granularity.is_none() {
146            missing.push("granularity");
147        }
148        if value.key_rollup_table.is_none() {
149            missing.push("keyRollupTable");
150        }
151        if value.kv_rollup_table.is_none() {
152            missing.push("kvRollupTable");
153        }
154        match (
155            value.granularity,
156            value.key_rollup_table,
157            value.kv_rollup_table,
158        ) {
159            (Some(granularity), Some(key_rollup_table), Some(kv_rollup_table)) => Ok(Self {
160                granularity,
161                key_rollup_table,
162                kv_rollup_table,
163            }),
164            _ => Err(MissingRequiredFields::new(missing)),
165        }
166    }
167}
168
169impl TryFrom<ClickStackLogSourceResponse> for ClickStackLogSource {
170    type Error = MissingRequiredFields;
171
172    fn try_from(value: ClickStackLogSourceResponse) -> Result<Self, Self::Error> {
173        let mut missing = Vec::new();
174        if value.connection.is_none() {
175            missing.push("connection");
176        }
177        if value.default_table_select_expression.is_none() {
178            missing.push("defaultTableSelectExpression");
179        }
180        if value.from.is_none() {
181            missing.push("from");
182        }
183        if value.kind.is_none() {
184            missing.push("kind");
185        }
186        if value.name.is_none() {
187            missing.push("name");
188        }
189        if value.timestamp_value_expression.is_none() {
190            missing.push("timestampValueExpression");
191        }
192        match (
193            value.connection,
194            value.default_table_select_expression,
195            value.from,
196            value.kind,
197            value.name,
198            value.timestamp_value_expression,
199        ) {
200            (
201                Some(connection),
202                Some(default_table_select_expression),
203                Some(from),
204                Some(kind),
205                Some(name),
206                Some(timestamp_value_expression),
207            ) => Ok(Self {
208                body_expression: value.body_expression,
209                connection,
210                default_table_select_expression,
211                disabled: value.disabled,
212                displayed_timestamp_value_expression: value.displayed_timestamp_value_expression,
213                event_attributes_expression: value.event_attributes_expression,
214                filter_settings: value.filter_settings.map(TryInto::try_into).transpose()?,
215                from: from.try_into()?,
216                highlighted_row_attribute_expressions: value
217                    .highlighted_row_attribute_expressions
218                    .map(|items| {
219                        items
220                            .into_iter()
221                            .map(TryInto::try_into)
222                            .collect::<Result<Vec<_>, _>>()
223                    })
224                    .transpose()?,
225                highlighted_trace_attribute_expressions: value
226                    .highlighted_trace_attribute_expressions
227                    .map(|items| {
228                        items
229                            .into_iter()
230                            .map(TryInto::try_into)
231                            .collect::<Result<Vec<_>, _>>()
232                    })
233                    .transpose()?,
234                id: value.id,
235                implicit_column_expression: value.implicit_column_expression,
236                kind,
237                known_columns_list_expression: value.known_columns_list_expression,
238                materialized_views: value
239                    .materialized_views
240                    .map(|items| {
241                        items
242                            .into_iter()
243                            .map(TryInto::try_into)
244                            .collect::<Result<Vec<_>, _>>()
245                    })
246                    .transpose()?,
247                metadata_materialized_views: value
248                    .metadata_materialized_views
249                    .map(TryInto::try_into)
250                    .transpose()?,
251                metric_source_id: value.metric_source_id,
252                name,
253                query_settings: value
254                    .query_settings
255                    .map(|items| {
256                        items
257                            .into_iter()
258                            .map(TryInto::try_into)
259                            .collect::<Result<Vec<_>, _>>()
260                    })
261                    .transpose()?,
262                resource_attributes_expression: value.resource_attributes_expression,
263                section: value.section,
264                service_name_expression: value.service_name_expression,
265                severity_text_expression: value.severity_text_expression,
266                span_id_expression: value.span_id_expression,
267                timestamp_value_expression,
268                trace_id_expression: value.trace_id_expression,
269                trace_source_id: value.trace_source_id,
270                use_text_index_for_implicit_column: value.use_text_index_for_implicit_column,
271            }),
272            _ => Err(MissingRequiredFields::new(missing)),
273        }
274    }
275}
276
277impl TryFrom<ClickStackMaterializedViewResponse> for ClickStackMaterializedView {
278    type Error = MissingRequiredFields;
279
280    fn try_from(value: ClickStackMaterializedViewResponse) -> Result<Self, Self::Error> {
281        let mut missing = Vec::new();
282        if value.aggregated_columns.is_none() {
283            missing.push("aggregatedColumns");
284        }
285        if value.database_name.is_none() {
286            missing.push("databaseName");
287        }
288        if value.dimension_columns.is_none() {
289            missing.push("dimensionColumns");
290        }
291        if value.min_granularity.is_none() {
292            missing.push("minGranularity");
293        }
294        if value.table_name.is_none() {
295            missing.push("tableName");
296        }
297        if value.timestamp_column.is_none() {
298            missing.push("timestampColumn");
299        }
300        match (
301            value.aggregated_columns,
302            value.database_name,
303            value.dimension_columns,
304            value.min_granularity,
305            value.table_name,
306            value.timestamp_column,
307        ) {
308            (
309                Some(aggregated_columns),
310                Some(database_name),
311                Some(dimension_columns),
312                Some(min_granularity),
313                Some(table_name),
314                Some(timestamp_column),
315            ) => Ok(Self {
316                aggregated_columns: aggregated_columns
317                    .into_iter()
318                    .map(TryInto::try_into)
319                    .collect::<Result<Vec<_>, _>>()?,
320                database_name,
321                dimension_columns,
322                min_date: value.min_date,
323                min_granularity,
324                table_name,
325                timestamp_column,
326            }),
327            _ => Err(MissingRequiredFields::new(missing)),
328        }
329    }
330}
331
332impl TryFrom<ClickStackMetricSourceFromResponse> for ClickStackMetricSourceFrom {
333    type Error = MissingRequiredFields;
334
335    fn try_from(value: ClickStackMetricSourceFromResponse) -> Result<Self, Self::Error> {
336        let mut missing = Vec::new();
337        if value.database_name.is_none() {
338            missing.push("databaseName");
339        }
340        match value.database_name {
341            Some(database_name) => Ok(Self {
342                database_name,
343                table_name: value.table_name,
344            }),
345            None => Err(MissingRequiredFields::new(missing)),
346        }
347    }
348}
349
350impl TryFrom<ClickStackMetricSourceResponse> for ClickStackMetricSource {
351    type Error = MissingRequiredFields;
352
353    fn try_from(value: ClickStackMetricSourceResponse) -> Result<Self, Self::Error> {
354        let mut missing = Vec::new();
355        if value.connection.is_none() {
356            missing.push("connection");
357        }
358        if value.from.is_none() {
359            missing.push("from");
360        }
361        if value.kind.is_none() {
362            missing.push("kind");
363        }
364        if value.metric_tables.is_none() {
365            missing.push("metricTables");
366        }
367        if value.name.is_none() {
368            missing.push("name");
369        }
370        if value.resource_attributes_expression.is_none() {
371            missing.push("resourceAttributesExpression");
372        }
373        if value.timestamp_value_expression.is_none() {
374            missing.push("timestampValueExpression");
375        }
376        match (
377            value.connection,
378            value.from,
379            value.kind,
380            value.metric_tables,
381            value.name,
382            value.resource_attributes_expression,
383            value.timestamp_value_expression,
384        ) {
385            (
386                Some(connection),
387                Some(from),
388                Some(kind),
389                Some(metric_tables),
390                Some(name),
391                Some(resource_attributes_expression),
392                Some(timestamp_value_expression),
393            ) => Ok(Self {
394                connection,
395                disabled: value.disabled,
396                from: from.try_into()?,
397                id: value.id,
398                kind,
399                log_source_id: value.log_source_id,
400                metric_tables: metric_tables.try_into()?,
401                name,
402                query_settings: value
403                    .query_settings
404                    .map(|items| {
405                        items
406                            .into_iter()
407                            .map(TryInto::try_into)
408                            .collect::<Result<Vec<_>, _>>()
409                    })
410                    .transpose()?,
411                resource_attributes_expression,
412                section: value.section,
413                timestamp_value_expression,
414            }),
415            _ => Err(MissingRequiredFields::new(missing)),
416        }
417    }
418}
419
420impl TryFrom<ClickStackMetricTablesResponse> for ClickStackMetricTables {
421    type Error = MissingRequiredFields;
422
423    fn try_from(value: ClickStackMetricTablesResponse) -> Result<Self, Self::Error> {
424        let mut missing = Vec::new();
425        if value.exponential_histogram.is_none() {
426            missing.push("exponential histogram");
427        }
428        if value.gauge.is_none() {
429            missing.push("gauge");
430        }
431        if value.histogram.is_none() {
432            missing.push("histogram");
433        }
434        if value.sum.is_none() {
435            missing.push("sum");
436        }
437        if value.summary.is_none() {
438            missing.push("summary");
439        }
440        match (
441            value.exponential_histogram,
442            value.gauge,
443            value.histogram,
444            value.sum,
445            value.summary,
446        ) {
447            (
448                Some(exponential_histogram),
449                Some(gauge),
450                Some(histogram),
451                Some(sum),
452                Some(summary),
453            ) => Ok(Self {
454                exponential_histogram,
455                gauge,
456                histogram,
457                sum,
458                summary,
459            }),
460            _ => Err(MissingRequiredFields::new(missing)),
461        }
462    }
463}
464
465impl TryFrom<ClickStackPromqlSourceResponse> for ClickStackPromqlSource {
466    type Error = MissingRequiredFields;
467
468    fn try_from(value: ClickStackPromqlSourceResponse) -> Result<Self, Self::Error> {
469        let mut missing = Vec::new();
470        if value.connection.is_none() {
471            missing.push("connection");
472        }
473        if value.from.is_none() {
474            missing.push("from");
475        }
476        if value.kind.is_none() {
477            missing.push("kind");
478        }
479        if value.name.is_none() {
480            missing.push("name");
481        }
482        if value.timestamp_value_expression.is_none() {
483            missing.push("timestampValueExpression");
484        }
485        match (
486            value.connection,
487            value.from,
488            value.kind,
489            value.name,
490            value.timestamp_value_expression,
491        ) {
492            (
493                Some(connection),
494                Some(from),
495                Some(kind),
496                Some(name),
497                Some(timestamp_value_expression),
498            ) => Ok(Self {
499                connection,
500                disabled: value.disabled,
501                from: from.try_into()?,
502                id: value.id,
503                kind,
504                name,
505                query_settings: value
506                    .query_settings
507                    .map(|items| {
508                        items
509                            .into_iter()
510                            .map(TryInto::try_into)
511                            .collect::<Result<Vec<_>, _>>()
512                    })
513                    .transpose()?,
514                section: value.section,
515                timestamp_value_expression,
516            }),
517            _ => Err(MissingRequiredFields::new(missing)),
518        }
519    }
520}
521
522impl TryFrom<ClickStackQuerySettingResponse> for ClickStackQuerySetting {
523    type Error = MissingRequiredFields;
524
525    fn try_from(value: ClickStackQuerySettingResponse) -> Result<Self, Self::Error> {
526        let mut missing = Vec::new();
527        if value.setting.is_none() {
528            missing.push("setting");
529        }
530        if value.value.is_none() {
531            missing.push("value");
532        }
533        match (value.setting, value.value) {
534            (Some(setting), Some(value)) => Ok(Self { setting, value }),
535            _ => Err(MissingRequiredFields::new(missing)),
536        }
537    }
538}
539
540impl TryFrom<ClickStackSessionSourceResponse> for ClickStackSessionSource {
541    type Error = MissingRequiredFields;
542
543    fn try_from(value: ClickStackSessionSourceResponse) -> Result<Self, Self::Error> {
544        let mut missing = Vec::new();
545        if value.connection.is_none() {
546            missing.push("connection");
547        }
548        if value.from.is_none() {
549            missing.push("from");
550        }
551        if value.kind.is_none() {
552            missing.push("kind");
553        }
554        if value.name.is_none() {
555            missing.push("name");
556        }
557        if value.trace_source_id.is_none() {
558            missing.push("traceSourceId");
559        }
560        match (
561            value.connection,
562            value.from,
563            value.kind,
564            value.name,
565            value.trace_source_id,
566        ) {
567            (Some(connection), Some(from), Some(kind), Some(name), Some(trace_source_id)) => {
568                Ok(Self {
569                    connection,
570                    disabled: value.disabled,
571                    from: from.try_into()?,
572                    id: value.id,
573                    kind,
574                    name,
575                    query_settings: value
576                        .query_settings
577                        .map(|items| {
578                            items
579                                .into_iter()
580                                .map(TryInto::try_into)
581                                .collect::<Result<Vec<_>, _>>()
582                        })
583                        .transpose()?,
584                    section: value.section,
585                    timestamp_value_expression: value.timestamp_value_expression,
586                    trace_source_id,
587                })
588            }
589            _ => Err(MissingRequiredFields::new(missing)),
590        }
591    }
592}
593
594impl TryFrom<ClickStackSourceFilterSettingsResponse> for ClickStackSourceFilterSettings {
595    type Error = MissingRequiredFields;
596
597    fn try_from(value: ClickStackSourceFilterSettingsResponse) -> Result<Self, Self::Error> {
598        let mut missing = Vec::new();
599        if value.columns.is_none() {
600            missing.push("columns");
601        }
602        if value.database_name.is_none() {
603            missing.push("databaseName");
604        }
605        if value.table_name.is_none() {
606            missing.push("tableName");
607        }
608        match (value.columns, value.database_name, value.table_name) {
609            (Some(columns), Some(database_name), Some(table_name)) => Ok(Self {
610                columns: columns
611                    .into_iter()
612                    .map(TryInto::try_into)
613                    .collect::<Result<Vec<_>, _>>()?,
614                database_name,
615                table_name,
616            }),
617            _ => Err(MissingRequiredFields::new(missing)),
618        }
619    }
620}
621
622impl TryFrom<ClickStackSourceFromResponse> for ClickStackSourceFrom {
623    type Error = MissingRequiredFields;
624
625    fn try_from(value: ClickStackSourceFromResponse) -> Result<Self, Self::Error> {
626        let mut missing = Vec::new();
627        if value.database_name.is_none() {
628            missing.push("databaseName");
629        }
630        if value.table_name.is_none() {
631            missing.push("tableName");
632        }
633        match (value.database_name, value.table_name) {
634            (Some(database_name), Some(table_name)) => Ok(Self {
635                database_name,
636                table_name,
637            }),
638            _ => Err(MissingRequiredFields::new(missing)),
639        }
640    }
641}
642
643impl TryFrom<ClickStackSourceResponse> for ClickStackSource {
644    type Error = MissingRequiredFields;
645
646    /// Turns a fetched source into a create/update body.
647    ///
648    /// A write replaces the whole source, so every field the schema requires for
649    /// the source's `kind` has to be present in the response; a missing one is
650    /// named rather than invented. Nested objects report their own wire names,
651    /// unprefixed — `databaseName` for a source's `from`, say.
652    ///
653    /// An `Unknown` payload — a `kind` this crate does not model, or a body that
654    /// did not fit the variant its `kind` selected — converts losslessly: the
655    /// request union's own `Unknown` arm holds the raw JSON and serializes it
656    /// verbatim, so such a source can still be written back.
657    fn try_from(value: ClickStackSourceResponse) -> Result<Self, Self::Error> {
658        Ok(match value {
659            ClickStackSourceResponse::ClickStackLogSource(source) => {
660                Self::ClickStackLogSource(source.try_into()?)
661            }
662            ClickStackSourceResponse::ClickStackTraceSource(source) => {
663                Self::ClickStackTraceSource(source.try_into()?)
664            }
665            ClickStackSourceResponse::ClickStackMetricSource(source) => {
666                Self::ClickStackMetricSource(source.try_into()?)
667            }
668            ClickStackSourceResponse::ClickStackSessionSource(source) => {
669                Self::ClickStackSessionSource(source.try_into()?)
670            }
671            ClickStackSourceResponse::ClickStackPromqlSource(source) => {
672                Self::ClickStackPromqlSource(source.try_into()?)
673            }
674            ClickStackSourceResponse::Unknown(raw) => Self::Unknown(raw),
675        })
676    }
677}
678
679impl TryFrom<ClickStackTraceSourceMetadataMaterializedViewsResponse>
680    for ClickStackTraceSourceMetadataMaterializedViews
681{
682    type Error = MissingRequiredFields;
683
684    fn try_from(
685        value: ClickStackTraceSourceMetadataMaterializedViewsResponse,
686    ) -> Result<Self, Self::Error> {
687        let mut missing = Vec::new();
688        if value.granularity.is_none() {
689            missing.push("granularity");
690        }
691        if value.key_rollup_table.is_none() {
692            missing.push("keyRollupTable");
693        }
694        if value.kv_rollup_table.is_none() {
695            missing.push("kvRollupTable");
696        }
697        match (
698            value.granularity,
699            value.key_rollup_table,
700            value.kv_rollup_table,
701        ) {
702            (Some(granularity), Some(key_rollup_table), Some(kv_rollup_table)) => Ok(Self {
703                granularity,
704                key_rollup_table,
705                kv_rollup_table,
706            }),
707            _ => Err(MissingRequiredFields::new(missing)),
708        }
709    }
710}
711
712impl TryFrom<ClickStackTraceSourceResponse> for ClickStackTraceSource {
713    type Error = MissingRequiredFields;
714
715    fn try_from(value: ClickStackTraceSourceResponse) -> Result<Self, Self::Error> {
716        let mut missing = Vec::new();
717        if value.connection.is_none() {
718            missing.push("connection");
719        }
720        if value.default_table_select_expression.is_none() {
721            missing.push("defaultTableSelectExpression");
722        }
723        if value.duration_expression.is_none() {
724            missing.push("durationExpression");
725        }
726        if value.duration_precision.is_none() {
727            missing.push("durationPrecision");
728        }
729        if value.from.is_none() {
730            missing.push("from");
731        }
732        if value.kind.is_none() {
733            missing.push("kind");
734        }
735        if value.name.is_none() {
736            missing.push("name");
737        }
738        if value.parent_span_id_expression.is_none() {
739            missing.push("parentSpanIdExpression");
740        }
741        if value.span_id_expression.is_none() {
742            missing.push("spanIdExpression");
743        }
744        if value.span_kind_expression.is_none() {
745            missing.push("spanKindExpression");
746        }
747        if value.span_name_expression.is_none() {
748            missing.push("spanNameExpression");
749        }
750        if value.timestamp_value_expression.is_none() {
751            missing.push("timestampValueExpression");
752        }
753        if value.trace_id_expression.is_none() {
754            missing.push("traceIdExpression");
755        }
756        match (
757            value.connection,
758            value.default_table_select_expression,
759            value.duration_expression,
760            value.duration_precision,
761            value.from,
762            value.kind,
763            value.name,
764            value.parent_span_id_expression,
765            value.span_id_expression,
766            value.span_kind_expression,
767            value.span_name_expression,
768            value.timestamp_value_expression,
769            value.trace_id_expression,
770        ) {
771            (
772                Some(connection),
773                Some(default_table_select_expression),
774                Some(duration_expression),
775                Some(duration_precision),
776                Some(from),
777                Some(kind),
778                Some(name),
779                Some(parent_span_id_expression),
780                Some(span_id_expression),
781                Some(span_kind_expression),
782                Some(span_name_expression),
783                Some(timestamp_value_expression),
784                Some(trace_id_expression),
785            ) => Ok(Self {
786                connection,
787                default_table_select_expression,
788                disabled: value.disabled,
789                duration_expression,
790                duration_precision,
791                event_attributes_expression: value.event_attributes_expression,
792                filter_settings: value.filter_settings.map(TryInto::try_into).transpose()?,
793                from: from.try_into()?,
794                highlighted_row_attribute_expressions: value
795                    .highlighted_row_attribute_expressions
796                    .map(|items| {
797                        items
798                            .into_iter()
799                            .map(TryInto::try_into)
800                            .collect::<Result<Vec<_>, _>>()
801                    })
802                    .transpose()?,
803                highlighted_trace_attribute_expressions: value
804                    .highlighted_trace_attribute_expressions
805                    .map(|items| {
806                        items
807                            .into_iter()
808                            .map(TryInto::try_into)
809                            .collect::<Result<Vec<_>, _>>()
810                    })
811                    .transpose()?,
812                id: value.id,
813                implicit_column_expression: value.implicit_column_expression,
814                kind,
815                known_columns_list_expression: value.known_columns_list_expression,
816                log_source_id: value.log_source_id,
817                materialized_views: value
818                    .materialized_views
819                    .map(|items| {
820                        items
821                            .into_iter()
822                            .map(TryInto::try_into)
823                            .collect::<Result<Vec<_>, _>>()
824                    })
825                    .transpose()?,
826                metadata_materialized_views: value
827                    .metadata_materialized_views
828                    .map(TryInto::try_into)
829                    .transpose()?,
830                metric_source_id: value.metric_source_id,
831                name,
832                parent_span_id_expression,
833                query_settings: value
834                    .query_settings
835                    .map(|items| {
836                        items
837                            .into_iter()
838                            .map(TryInto::try_into)
839                            .collect::<Result<Vec<_>, _>>()
840                    })
841                    .transpose()?,
842                resource_attributes_expression: value.resource_attributes_expression,
843                section: value.section,
844                service_name_expression: value.service_name_expression,
845                session_source_id: value.session_source_id,
846                span_events_value_expression: value.span_events_value_expression,
847                span_id_expression,
848                span_kind_expression,
849                span_name_expression,
850                status_code_expression: value.status_code_expression,
851                status_message_expression: value.status_message_expression,
852                timestamp_value_expression,
853                trace_id_expression,
854                use_text_index_for_implicit_column: value.use_text_index_for_implicit_column,
855            }),
856            _ => Err(MissingRequiredFields::new(missing)),
857        }
858    }
859}
860
861impl From<PgBouncerConfigResponse> for PgBouncerConfig {
862    fn from(_value: PgBouncerConfigResponse) -> Self {
863        // The schema declares no properties, so the conversion is total.
864        Self {}
865    }
866}
867
868impl From<PgConfigResponse> for PgConfig {
869    fn from(value: PgConfigResponse) -> Self {
870        // Every `pgConfig` GUC is optional in both directions (omitting one
871        // selects the server default), so the conversion is total.
872        Self {
873            autovacuum_analyze_scale_factor: value.autovacuum_analyze_scale_factor,
874            autovacuum_max_workers: value.autovacuum_max_workers,
875            autovacuum_naptime: value.autovacuum_naptime,
876            autovacuum_vacuum_cost_delay: value.autovacuum_vacuum_cost_delay,
877            autovacuum_vacuum_cost_limit: value.autovacuum_vacuum_cost_limit,
878            autovacuum_vacuum_insert_scale_factor: value.autovacuum_vacuum_insert_scale_factor,
879            autovacuum_vacuum_scale_factor: value.autovacuum_vacuum_scale_factor,
880            autovacuum_work_mem: value.autovacuum_work_mem,
881            default_transaction_isolation: value.default_transaction_isolation,
882            effective_cache_size: value.effective_cache_size,
883            effective_io_concurrency: value.effective_io_concurrency,
884            idle_in_transaction_session_timeout: value.idle_in_transaction_session_timeout,
885            idle_session_timeout: value.idle_session_timeout,
886            lock_timeout: value.lock_timeout,
887            maintenance_work_mem: value.maintenance_work_mem,
888            max_connections: value.max_connections,
889            max_parallel_maintenance_workers: value.max_parallel_maintenance_workers,
890            max_parallel_workers: value.max_parallel_workers,
891            max_parallel_workers_per_gather: value.max_parallel_workers_per_gather,
892            max_slot_wal_keep_size: value.max_slot_wal_keep_size,
893            max_wal_size: value.max_wal_size,
894            max_worker_processes: value.max_worker_processes,
895            min_wal_size: value.min_wal_size,
896            random_page_cost: value.random_page_cost,
897            ssl_min_protocol_version: value.ssl_min_protocol_version,
898            statement_timeout: value.statement_timeout,
899            transaction_timeout: value.transaction_timeout,
900            wal_compression: value.wal_compression,
901            wal_keep_size: value.wal_keep_size,
902            wal_sender_timeout: value.wal_sender_timeout,
903            work_mem: value.work_mem,
904        }
905    }
906}
907
908impl TryFrom<PostgresInstanceConfigResponse> for PostgresInstanceConfig {
909    type Error = MissingRequiredFields;
910
911    /// Turns a fetched configuration into a POST/PATCH body.
912    ///
913    /// The API requires both `pgConfig` and `pgBouncerConfig` in a write body
914    /// (it rejects a body omitting either), so a response missing one cannot be
915    /// written back verbatim and the caller has to supply it.
916    fn try_from(value: PostgresInstanceConfigResponse) -> Result<Self, Self::Error> {
917        let mut missing = Vec::new();
918        if value.pg_bouncer_config.is_none() {
919            missing.push("pgBouncerConfig");
920        }
921        if value.pg_config.is_none() {
922            missing.push("pgConfig");
923        }
924        match (value.pg_bouncer_config, value.pg_config) {
925            (Some(pg_bouncer_config), Some(pg_config)) => Ok(Self {
926                pg_bouncer_config: pg_bouncer_config.into(),
927                pg_config: pg_config.into(),
928            }),
929            _ => Err(MissingRequiredFields::new(missing)),
930        }
931    }
932}
933
934impl TryFrom<ResourceTagsV1Response> for ResourceTagsV1 {
935    type Error = MissingRequiredFields;
936
937    /// Turns a fetched tag into one that can be sent back.
938    ///
939    /// A tag is identified by its key, so a response tag without one cannot be
940    /// written back — dropping it silently would delete the tag on the next
941    /// write, and inventing an empty key would create a bogus one.
942    fn try_from(value: ResourceTagsV1Response) -> Result<Self, Self::Error> {
943        match value.key {
944            Some(key) => Ok(Self {
945                key,
946                value: value.value,
947            }),
948            None => Err(MissingRequiredFields::new(vec!["key"])),
949        }
950    }
951}
952
953impl TryFrom<ScalingScheduleEntry> for ScalingScheduleEntryRequest {
954    type Error = MissingRequiredFields;
955
956    /// Turns a fetched schedule entry into one that can be re-sent.
957    ///
958    /// An upsert replaces the whole schedule, so a caller that reads a schedule
959    /// and writes it back has to send every entry in full: the window bounds,
960    /// weekdays and name the API requires cannot be defaulted away.
961    fn try_from(value: ScalingScheduleEntry) -> Result<Self, Self::Error> {
962        let mut missing = Vec::new();
963        if value.end_hour_utc.is_none() {
964            missing.push("endHourUtc");
965        }
966        if value.name.is_none() {
967            missing.push("name");
968        }
969        if value.start_hour_utc.is_none() {
970            missing.push("startHourUtc");
971        }
972        if value.weekdays.is_none() {
973            missing.push("weekdays");
974        }
975        match (
976            value.end_hour_utc,
977            value.name,
978            value.start_hour_utc,
979            value.weekdays,
980        ) {
981            (Some(end_hour_utc), Some(name), Some(start_hour_utc), Some(weekdays)) => Ok(Self {
982                autoscaling_mode: value.autoscaling_mode,
983                end_hour_utc,
984                idle_scaling: value.idle_scaling,
985                idle_timeout_minutes: value.idle_timeout_minutes,
986                max_replica_memory_gb: value.max_replica_memory_gb,
987                max_replicas: value.max_replicas,
988                min_replica_memory_gb: value.min_replica_memory_gb,
989                min_replicas: value.min_replicas,
990                name,
991                // Not part of the response shape; a horizontal entry carries a
992                // min/max band instead of a fixed replica count.
993                num_replicas: None,
994                start_hour_utc,
995                weekdays,
996            }),
997            _ => Err(MissingRequiredFields::new(missing)),
998        }
999    }
1000}
1001
1002impl TryFrom<UpgradeWindow> for UpgradeWindowPutRequest {
1003    type Error = MissingRequiredFields;
1004
1005    /// Turns a fetched upgrade window into one that can be re-sent.
1006    ///
1007    /// `duration` is response-only (the API derives it), so only the window's
1008    /// start hour and weekday cross over — and both are required.
1009    fn try_from(value: UpgradeWindow) -> Result<Self, Self::Error> {
1010        let mut missing = Vec::new();
1011        if value.start_hour_utc.is_none() {
1012            missing.push("startHourUtc");
1013        }
1014        if value.weekday.is_none() {
1015            missing.push("weekday");
1016        }
1017        match (value.start_hour_utc, value.weekday) {
1018            (Some(start_hour_utc), Some(weekday)) => Ok(Self {
1019                start_hour_utc,
1020                weekday,
1021            }),
1022            _ => Err(MissingRequiredFields::new(missing)),
1023        }
1024    }
1025}