golem_cli/model/deploy_diff/
api_definition.rs

1// Copyright 2024-2025 Golem Cloud
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::log::LogColorize;
16use crate::model::api::to_method_pattern;
17use crate::model::app::HttpApiDefinitionName;
18use crate::model::app_raw::{
19    HttpApiDefinition, HttpApiDefinitionBindingType, HttpApiDefinitionRoute,
20};
21use crate::model::component::Component;
22use crate::model::deploy_diff::{DiffSerialize, ToYamlValueWithoutNulls};
23use crate::model::text::fmt::format_rib_source_for_error;
24use anyhow::anyhow;
25use golem_client::model::{
26    GatewayBindingComponent, GatewayBindingData, GatewayBindingType, HttpApiDefinitionRequest,
27    HttpApiDefinitionResponseData, RouteRequestData,
28};
29use serde::{Deserialize, Serialize};
30use std::collections::BTreeMap;
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct DiffableHttpApiDefinition(pub HttpApiDefinitionRequest);
34
35impl DiffableHttpApiDefinition {
36    pub fn from_server(api_definition: HttpApiDefinitionResponseData) -> anyhow::Result<Self> {
37        Ok(Self(HttpApiDefinitionRequest {
38            id: api_definition.id,
39            version: api_definition.version,
40            security: None, // TODO: check that this is not needed anymore
41            routes: api_definition
42                .routes
43                .into_iter()
44                .map(|route| RouteRequestData {
45                    method: route.method,
46                    path: route.path,
47                    binding: GatewayBindingData {
48                        binding_type: route.binding.binding_type,
49                        component: route.binding.component.map(|component| {
50                            GatewayBindingComponent {
51                                name: component.name,
52                                version: Some(component.version),
53                            }
54                        }),
55                        worker_name: route.binding.worker_name,
56                        idempotency_key: route.binding.idempotency_key,
57                        response: route.binding.response,
58                        invocation_context: route.binding.invocation_context,
59                    },
60                    security: route.security,
61                })
62                .collect(),
63            draft: api_definition.draft,
64        }))
65    }
66
67    pub fn from_manifest(
68        server_api_def: Option<&DiffableHttpApiDefinition>,
69        name: &HttpApiDefinitionName,
70        api_definition: &HttpApiDefinition,
71        latest_component_versions: &BTreeMap<String, Component>,
72    ) -> anyhow::Result<Self> {
73        let mut manifest_api_def = Self(HttpApiDefinitionRequest {
74            id: name.to_string(),
75            version: api_definition.version.clone(),
76            security: None, // TODO: check that this is not needed anymore
77            routes: api_definition
78                .routes
79                .iter()
80                .map(|route| normalize_http_api_route(latest_component_versions, route))
81                .collect::<Result<Vec<_>, _>>()?,
82            draft: true,
83        });
84
85        // NOTE: if the only diff is being non-draft on serverside, we hide that
86        if let Some(server_api_def) = server_api_def {
87            if manifest_api_def.0.version == server_api_def.0.version
88                && !server_api_def.0.draft
89                && manifest_api_def.0.draft
90            {
91                manifest_api_def.0.draft = false;
92            }
93        }
94
95        Ok(manifest_api_def)
96    }
97}
98
99impl DiffSerialize for DiffableHttpApiDefinition {
100    fn to_diffable_string(&self) -> anyhow::Result<String> {
101        let yaml_value = self.0.clone().to_yaml_value_without_nulls()?;
102        Ok(serde_yaml::to_string(&yaml_value)?)
103    }
104}
105
106fn normalize_http_api_route(
107    latest_component_versions: &BTreeMap<String, Component>,
108    route: &HttpApiDefinitionRoute,
109) -> anyhow::Result<RouteRequestData> {
110    Ok(RouteRequestData {
111        method: to_method_pattern(&route.method)?,
112        path: normalize_http_api_binding_path(&route.path),
113        binding: GatewayBindingData {
114            binding_type: Some(
115                route
116                    .binding
117                    .type_
118                    .as_ref()
119                    .map(|binding_type| match binding_type {
120                        HttpApiDefinitionBindingType::Default => GatewayBindingType::Default,
121                        HttpApiDefinitionBindingType::CorsPreflight => {
122                            GatewayBindingType::CorsPreflight
123                        }
124                        HttpApiDefinitionBindingType::FileServer => GatewayBindingType::FileServer,
125                        HttpApiDefinitionBindingType::HttpHandler => {
126                            GatewayBindingType::HttpHandler
127                        }
128                    })
129                    .unwrap_or_else(|| GatewayBindingType::Default),
130            ),
131            component: {
132                route
133                    .binding
134                    .component_name
135                    .as_ref()
136                    .map(|name| GatewayBindingComponent {
137                        name: name.clone(),
138                        version: route.binding.component_version.or_else(|| {
139                            latest_component_versions
140                                .get(name)
141                                .map(|component| component.versioned_component_id.version)
142                        }),
143                    })
144            },
145            worker_name: None,
146            idempotency_key: normalize_rib_property(&route.binding.idempotency_key)?,
147            invocation_context: normalize_rib_property(&route.binding.invocation_context)?,
148            response: normalize_rib_property(&route.binding.response)?,
149        },
150        security: route.security.clone(),
151    })
152}
153
154fn normalize_rib_property(rib: &Option<String>) -> anyhow::Result<Option<String>> {
155    rib.as_ref()
156        .map(|r| r.as_str())
157        .map(normalize_rib_source_code)
158        .transpose()
159}
160
161pub fn normalize_http_api_binding_path(path: &str) -> String {
162    path.to_string()
163        .strip_suffix("/")
164        .unwrap_or(path)
165        .to_string()
166}
167
168fn normalize_rib_source_code(rib: &str) -> anyhow::Result<String> {
169    Ok(rib::from_string(rib)
170        .map_err(|err| {
171            anyhow!(
172                "Failed to normalize Rib source code: {}\n{}\n{}",
173                err,
174                "Rib source:".log_color_highlight(),
175                format_rib_source_for_error(&err, rib)
176            )
177        })?
178        .to_string())
179}