1/*
  2 * SPDX-FileCopyrightText: 2025 Copyright (c) Contributors to the Eclipse Foundation
  3 *
  4 * See the NOTICE file(s) distributed with this work for additional
  5 * information regarding copyright ownership.
  6 *
  7 * This program and the accompanying materials are made available under the
  8 * terms of the Apache License Version 2.0 which is available at
  9 * https://www.apache.org/licenses/LICENSE-2.0
 10 *
 11 * SPDX-License-Identifier: Apache-2.0
 12 */
 13
 14use aide::{axum::IntoApiResponse, transform::TransformOperation};
 15use axum::{
 16    Json,
 17    body::Bytes,
 18    extract::{Query, State},
 19    response::{IntoResponse, Response},
 20};
 21use axum_extra::extract::WithRejection;
 22use cda_interfaces::{
 23    DiagComm, DynamicPlugin, SchemaProvider, UdsEcu,
 24    diagservices::{DiagServiceJsonResponse, DiagServiceResponse, DiagServiceResponseType},
 25    file_manager::FileManager,
 26};
 27use cda_plugin_security::SecurityPlugin;
 28use http::{HeaderMap, StatusCode};
 29
 30use crate::{
 31    openapi,
 32    sovd::{
 33        IntoSovd, WebserverEcuState,
 34        components::get_content_type_and_accept,
 35        create_response_schema, create_schema,
 36        error::{ApiError, ErrorWrapper, api_error_from_diag_response},
 37        field_parse_errors_to_json, get_payload_data,
 38    },
 39};
 40
 41pub(crate) mod configurations;
 42pub(crate) mod data;
 43pub(crate) mod faults;
 44pub(crate) mod genericservice;
 45pub(crate) mod modes;
 46pub(crate) mod operations;
 47pub(crate) mod x_single_ecu_jobs;
 48pub(crate) mod x_sovd2uds_bulk_data;
 49pub(crate) mod x_sovd2uds_download;
 50
[docs] 51// [[ dimpl~sovd-api-component-sdgsd, GET /components/{ecu} SDG handler ]]
 52pub(crate) async fn get<T: UdsEcu + Clone, U: FileManager>(
 53    State(WebserverEcuState { ecu_name, uds, .. }): State<WebserverEcuState<T, U>>,
 54    WithRejection(Query(query), _): WithRejection<
 55        Query<sovd_interfaces::components::ComponentQuery>,
 56        ApiError,
 57    >,
 58) -> impl IntoApiResponse {
 59    let include_schema = query.include_schema;
 60    let base_path = format!("http://localhost:20002/vehicle/v15/components/{ecu_name}");
 61    let variant = match uds.get_variant(&ecu_name).await {
 62        Ok(v) => v,
 63        Err(e) => {
 64            return ErrorWrapper {
 65                error: e.into(),
 66                include_schema,
 67            }
 68            .into_response();
 69        }
 70    };
 71
 72    let sdgs = if query.include_sdgs {
 73        match uds.get_sdgs(&ecu_name, None).await {
 74            Ok(v) => Some(
 75                v.into_iter()
 76                    .map(super::super::IntoSovd::into_sovd)
 77                    .collect(),
 78            ),
 79            Err(e) => {
 80                return ErrorWrapper {
 81                    error: e.into(),
 82                    include_schema,
 83                }
 84                .into_response();
 85            }
 86        }
 87    } else {
 88        None
 89    };
 90
 91    let schema = if include_schema {
 92        Some(create_schema!(
 93            sovd_interfaces::components::ecu::get::Response
 94        ))
 95    } else {
 96        None
 97    };
 98
 99    (
100        StatusCode::OK,
101        Json(sovd_interfaces::components::ecu::get::Response {
102            id: ecu_name.to_lowercase(),
103            name: ecu_name.clone(),
104            variant: variant.into_sovd(),
105            locks: format!("{base_path}/locks"),
106            operations: format!("{base_path}/operations"),
107            configurations: format!("{base_path}/configurations"),
108            data: format!("{base_path}/data"),
109            sdgs,
110            single_ecu_jobs: format!("{base_path}/x-single-ecu-jobs"),
111            faults: format!("{base_path}/faults"),
112            modes: format!("{base_path}/modes"),
113            schema,
114        }),
115    )
116        .into_response()
117}
118
119pub(crate) fn docs_get(op: TransformOperation) -> TransformOperation {
120    op.description("Get ECU details")
121        .response_with::<200, Json<sovd_interfaces::components::ecu::Ecu>, _>(|res| {
122            res.example(sovd_interfaces::components::ecu::Ecu {
123                id: "my_ecu".to_string(),
124                name: "My ECU".to_string(),
125                variant: sovd_interfaces::components::ecu::Variant {
126                    name: "Variant Name".to_owned(),
127                    is_base_variant: false,
128                    state: sovd_interfaces::components::ecu::State::Online,
129                    logical_address: "0x42".to_owned(),
130                },
131                locks: "http://localhost:20002/vehicle/v15/components/my_ecu/locks".to_string(),
132                operations: "http://localhost:20002/vehicle/v15/components/my_ecu/operations"
133                    .to_string(),
134                data: "http://localhost:20002/vehicle/v15/components/my_ecu/data".to_string(),
135                configurations:
136                    "http://localhost:20002/vehicle/v15/components/my_ecu/configurations"
137                        .to_string(),
138                sdgs: None,
139                single_ecu_jobs:
140                    "http://localhost:20002/vehicle/v15/components/my_ecu/x-single-ecu-jobs"
141                        .to_string(),
142                faults: "http://localhost:20002/vehicle/v15/components/my_ecu/faults".to_string(),
143                modes: "http://localhost:20002/vehicle/v15/components/my_ecu/modes".to_string(),
144                schema: None,
145            })
146            .description("Response with ECU information (i.e. detected variant) and service URLs")
147        })
148}
149
150pub(crate) async fn post<T: UdsEcu + Clone, U: FileManager>(
151    State(WebserverEcuState { ecu_name, uds, .. }): State<WebserverEcuState<T, U>>,
152) -> Response {
153    update(&ecu_name, uds).await
154}
155
[docs]156// [[ dimpl~sovd-api-ecu-variant-detection, PUT endpoint for ECU variant detection ]]
157//
158// Handles PUT requests on /components/{ecuName} to trigger variant detection.
159// Delegates to the UDS layer which sends diagnostic requests to the ECU and
160// evaluates the responses against known variant patterns. Returns 201 on
161// success or an error response if detection fails.
162pub(crate) async fn put<T: UdsEcu + Clone, U: FileManager>(
163    State(WebserverEcuState { ecu_name, uds, .. }): State<WebserverEcuState<T, U>>,
164) -> Response {
165    update(&ecu_name, uds).await
166}
167
168pub(crate) fn docs_put(op: TransformOperation) -> TransformOperation {
169    op.description("Trigger ECU variant detection")
170        .response_with::<201, (), _>(|res| res.description("ECU variant detection triggered."))
171}
172
173async fn update<T: UdsEcu + Clone>(ecu_name: &str, uds: T) -> Response {
174    match uds.detect_variant(ecu_name).await {
175        Ok(()) => (StatusCode::CREATED, ()).into_response(),
176        Err(e) => ErrorWrapper {
177            error: e.into(),
178            include_schema: false,
179        }
180        .into_response(),
181    }
182}
183
184impl IntoSovd for cda_interfaces::datatypes::ComplexComParamValue {
185    type SovdType = sovd_interfaces::components::ecu::operations::comparams::ComplexComParamValue;
186
187    fn into_sovd(self) -> Self::SovdType {
188        self.into_iter()
189            .map(|(key, value)| (key, value.into_sovd()))
190            .collect()
191    }
192}
193
194impl IntoSovd for cda_interfaces::datatypes::ComParamValue {
195    type SovdType = sovd_interfaces::components::ecu::operations::comparams::ComParamValue;
196
197    fn into_sovd(self) -> Self::SovdType {
198        match self {
199            Self::Simple(simple) => Self::SovdType::Simple(simple.into_sovd()),
200            Self::Complex(complex) => Self::SovdType::Complex(complex.into_sovd()),
201        }
202    }
203}
204
205impl IntoSovd for cda_interfaces::datatypes::ComParamSimpleValue {
206    type SovdType = sovd_interfaces::components::ecu::operations::comparams::ComParamSimpleValue;
207
208    fn into_sovd(self) -> Self::SovdType {
209        Self::SovdType {
210            value: self.value.clone(),
211            unit: self.unit.map(|u| {
212                sovd_interfaces::components::ecu::operations::comparams::Unit {
213                    factor_to_si_unit: u.factor_to_si_unit,
214                    offset_to_si_unit: u.offset_to_si_unit,
215                }
216            }),
217        }
218    }
219}
220
221openapi::aide_helper::gen_path_param!(DiagServicePathParam service String);
222#[allow(
223    clippy::too_many_lines,
224    reason = "Splitting this function is not worth it here"
225)]
226async fn data_request<T: UdsEcu + SchemaProvider + Clone>(
227    service: DiagComm,
228    ecu_name: &str,
229    gateway: &T,
230    headers: HeaderMap,
231    body: Option<Bytes>,
232    security_plugin: Box<dyn SecurityPlugin>,
233    include_schema: bool,
234) -> Response {
235    let (content_type, accept) = match get_content_type_and_accept(&headers) {
236        Ok(v) => v,
237        Err(e) => {
238            return ErrorWrapper {
239                error: e,
240                include_schema,
241            }
242            .into_response();
243        }
244    };
245
246    let data = if let Some(body) = body {
247        match get_payload_data::<sovd_interfaces::components::ecu::data::DataRequestPayload>(
248            content_type.as_ref(),
249            &headers,
250            &body,
251        ) {
252            Ok(value) => value,
253            Err(e) => {
254                return ErrorWrapper {
255                    error: e,
256                    include_schema,
257                }
258                .into_response();
259            }
260        }
261    } else {
262        None
263    };
264
265    let map_to_json = match (accept.type_(), accept.subtype()) {
266        (mime::APPLICATION, mime::JSON) => true,
267        (mime::APPLICATION, mime::OCTET_STREAM) => false,
268        unsupported => {
269            return ErrorWrapper {
270                error: ApiError::BadRequest(format!("Unsupported Accept: {unsupported:?}")),
271                include_schema,
272            }
273            .into_response();
274        }
275    };
276
277    if !map_to_json && include_schema {
278        return ErrorWrapper {
279            error: ApiError::BadRequest(
280                "Cannot use include-schema with non-JSON response".to_string(),
281            ),
282            include_schema,
283        }
284        .into_response();
285    }
286
287    let schema = if include_schema {
288        match gateway
289            .schema_for_responses(ecu_name, &service)
290            .await
291            .map(cda_interfaces::SchemaDescription::into_schema)
292        {
293            Ok(data_schema) => Some(create_response_schema!(
294                sovd_interfaces::ObjectDataItem<VendorErrorCode>,
295                "data",
296                data_schema
297            )),
298            Err(e) => {
299                return ErrorWrapper {
300                    error: e.into(),
301                    include_schema,
302                }
303                .into_response();
304            }
305        }
306    } else {
307        None
308    };
309
310    let response = match gateway
311        .send(
312            ecu_name,
313            service.clone(),
314            &(security_plugin as DynamicPlugin),
315            data,
316            map_to_json,
317        )
318        .await
319        .map_err(Into::into)
320    {
321        Err(e) => {
322            return ErrorWrapper {
323                error: e,
324                include_schema,
325            }
326            .into_response();
327        }
328        Ok(v) => v,
329    };
330
331    if let DiagServiceResponseType::Negative = response.response_type() {
332        return api_error_from_diag_response(&response, include_schema).into_response();
333    }
334
335    if response.is_empty() {
336        return StatusCode::NO_CONTENT.into_response();
337    }
338
339    if map_to_json {
340        let (mapped_data, errors) = match response.into_json() {
341            Ok(DiagServiceJsonResponse {
342                data: serde_json::Value::Object(mapped_data),
343                errors,
344            }) => (mapped_data, errors),
345            Ok(DiagServiceJsonResponse {
346                data: serde_json::Value::Null,
347                errors,
348            }) => {
349                if errors.is_empty() {
350                    return StatusCode::NO_CONTENT.into_response();
351                }
352                (serde_json::Map::new(), errors)
353            }
354            Ok(v) => {
355                return ErrorWrapper {
356                    error: ApiError::InternalServerError(Some(format!(
357                        "Expected JSON object but got: {}",
358                        v.data
359                    ))),
360                    include_schema,
361                }
362                .into_response();
363            }
364            Err(e) => {
365                return ErrorWrapper {
366                    error: ApiError::InternalServerError(Some(format!("{e:?}"))),
367                    include_schema,
368                }
369                .into_response();
370            }
371        };
372        (
373            StatusCode::OK,
374            Json(sovd_interfaces::ObjectDataItem {
375                id: service.name.to_lowercase(),
376                data: mapped_data,
377                errors: field_parse_errors_to_json(errors, "data"),
378                schema,
379            }),
380        )
381            .into_response()
382    } else {
383        let data = response.get_raw().to_vec();
384        (StatusCode::OK, Bytes::from_owner(data)).into_response()
385    }
386}