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::UseApi;
 15use cda_plugin_security::Secured;
 16use sovd_interfaces::error::ApiErrorResponse;
 17
 18use super::{
 19    ApiError, DynamicPlugin, ErrorWrapper, FileManager, IntoResponse, Json, Query, Response, State,
 20    StatusCode, TransformOperation, UdsEcu, WebserverEcuState, WithRejection,
 21};
 22use crate::sovd::{self, create_schema};
 23
 24pub(crate) async fn get<T: UdsEcu + Clone, U: FileManager>(
 25    UseApi(Secured(security_plugin), _): UseApi<Secured, ()>,
 26    WithRejection(Query(query), _): WithRejection<
 27        Query<sovd_interfaces::components::ecu::data::get::Query>,
 28        ApiError,
 29    >,
 30    State(WebserverEcuState { ecu_name, uds, .. }): State<WebserverEcuState<T, U>>,
 31) -> Response {
 32    let schema = if query.include_schema {
 33        Some(create_schema!(
 34            sovd_interfaces::components::ecu::data::get::Response
 35        ))
 36    } else {
 37        None
 38    };
 39    match uds
 40        .get_components_data_info(&ecu_name, &(security_plugin as DynamicPlugin))
 41        .await
 42    {
 43        Ok(mut items) => {
 44            let sovd_component_data = sovd_interfaces::components::ecu::data::get::Response {
 45                items: items
 46                    .drain(0..)
 47                    .map(crate::sovd::IntoSovd::into_sovd)
 48                    .collect(),
 49                schema,
 50            };
 51            (StatusCode::OK, Json(sovd_component_data)).into_response()
 52        }
 53        Err(e) => ErrorWrapper {
 54            error: e.into(),
 55            include_schema: query.include_schema,
 56        }
 57        .into_response(),
 58    }
 59}
 60
 61pub(crate) fn docs_get(op: TransformOperation) -> TransformOperation {
 62    op.description("Get all ECU data.")
 63        .response_with::<200, Json<sovd_interfaces::components::ecu::data::get::Response>, _>(
 64            |res| {
 65                res.description("Response with all data.").example(
 66                    sovd_interfaces::components::ecu::data::get::Response {
 67                        items: vec![sovd_interfaces::components::ecu::ComponentDataInfo {
 68                            category: "example_category".to_string(),
 69                            id: "example_id".to_string(),
 70                            name: "example_name".to_string(),
 71                        }],
 72                        schema: None,
 73                    },
 74                )
 75            },
 76        )
 77        .response_with::<400, Json<ApiErrorResponse<sovd::error::VendorErrorCode>>, _>(|res| {
 78            res.description("Error while fetching data from ECU.")
 79                .example(sovd_interfaces::error::ApiErrorResponse {
 80                    message: "Failed to fetch ECU data".to_string(),
 81                    error_code: sovd_interfaces::error::ErrorCode::VendorSpecific,
 82                    vendor_code: Some(sovd::error::VendorErrorCode::BadRequest),
 83                    parameters: None,
 84                    error_source: Some("ECU".to_string()),
 85                    schema: None,
 86                })
 87        })
 88}
 89
 90pub(crate) mod diag_service {
 91    use aide::{
 92        UseApi,
 93        transform::{TransformOperation, TransformParameter},
 94    };
 95    use axum::{
 96        Json,
 97        body::Bytes,
 98        extract::{Path, Query, State},
 99        response::{IntoResponse, Response},
100    };
101    use axum_extra::extract::WithRejection;
102    use cda_interfaces::{
103        DiagComm, DiagCommType, HashMap, HashMapExtensions, SchemaProvider, UdsEcu,
104        file_manager::FileManager,
105    };
106    use cda_plugin_security::Secured;
107    use http::{HeaderMap, StatusCode};
108
109    use crate::{
110        openapi,
111        sovd::{
112            IntoSovd, WebserverEcuState,
113            components::ecu::{DiagServicePathParam, data_request},
114            create_schema,
115            error::{ApiError, ErrorWrapper},
116        },
117    };
118
[docs]119    // [[ dimpl~sovd-api-component-data-sdgsd, GET /data/{service} SDG handler ]]
120    async fn get_sdgs_handler<T: UdsEcu + Clone>(
121        service: String,
122        ecu_name: &str,
123        gateway: &T,
124        include_schema: bool,
125    ) -> Response {
126        let service_ops = vec![
127            DiagComm {
128                name: service.clone(),
129                type_: DiagCommType::Data,
130                lookup_name: None,
131                subfunction_id: None,
132            },
133            DiagComm {
134                name: service.clone(),
135                type_: DiagCommType::Data,
136                lookup_name: None,
137                subfunction_id: None,
138            },
139            DiagComm {
140                name: service,
141                type_: DiagCommType::Data,
142                lookup_name: None,
143                subfunction_id: None,
144            },
145        ];
146        let schema = if include_schema {
147            Some(create_schema!(
148                sovd_interfaces::components::ecu::ServicesSdgs
149            ))
150        } else {
151            None
152        };
153        let mut resp = sovd_interfaces::components::ecu::ServicesSdgs {
154            items: HashMap::new(),
155            schema,
156        };
157        for service in service_ops {
158            match gateway.get_sdgs(ecu_name, Some(&service)).await {
159                Ok(sdgs) => {
160                    if sdgs.is_empty() {
161                        continue;
162                    }
163                    resp.items.insert(
164                        format!("{}_{:?}", service.name, service.action()).to_lowercase(),
165                        sovd_interfaces::components::ecu::ServiceSdgs {
166                            sdgs: sdgs.into_sovd(),
167                        },
168                    );
169                }
170                Err(e) => {
171                    return ErrorWrapper {
172                        error: e.into(),
173                        include_schema,
174                    }
175                    .into_response();
176                }
177            }
178        }
179        (StatusCode::OK, Json(resp)).into_response()
180    }
181
182    pub(crate) async fn get<T: UdsEcu + SchemaProvider + Send + Sync + Clone, U: FileManager>(
183        headers: HeaderMap,
184        UseApi(Secured(security_plugin), _): UseApi<Secured, ()>,
185        Path(DiagServicePathParam {
186            service: diag_service,
187        }): Path<DiagServicePathParam>,
188        WithRejection(Query(query), _): WithRejection<
189            Query<sovd_interfaces::components::ComponentQuery>,
190            ApiError,
191        >,
192        State(WebserverEcuState { ecu_name, uds, .. }): State<WebserverEcuState<T, U>>,
193    ) -> Response {
194        let include_schema = query.include_schema;
195        if query.include_sdgs {
196            get_sdgs_handler::<T>(diag_service, &ecu_name, &uds, include_schema).await
197        } else {
198            if diag_service.contains('/') {
199                return ErrorWrapper {
200                    error: ApiError::BadRequest("Invalid path".to_owned()),
201                    include_schema,
202                }
203                .into_response();
204            }
205            data_request::<T>(
206                DiagComm {
207                    name: diag_service,
208                    type_: DiagCommType::Data,
209                    lookup_name: None,
210                    subfunction_id: None,
211                },
212                &ecu_name,
213                &uds,
214                headers,
215                None,
216                security_plugin,
217                include_schema,
218            )
219            .await
220        }
221    }
222
223    pub(crate) fn docs_get(op: TransformOperation) -> TransformOperation {
224        op.description("Get a specific diagnostic service.")
225            .parameter("x-sovd2uds-includesdgs", |op: TransformParameter<bool>| {
226                op.description("Set to true to include sdgs.")
227            })
228            .with(openapi::ecu_service_response)
229            .with(openapi::error_forbidden)
230            .with(openapi::error_not_found)
231            .with(openapi::error_internal_server)
232            .with(openapi::error_conflict)
233            .with(openapi::error_bad_request)
234            .with(openapi::error_bad_gateway)
235    }
236
237    pub(crate) async fn put<T: UdsEcu + SchemaProvider + Clone, U: FileManager>(
238        headers: HeaderMap,
239        UseApi(Secured(security_plugin), _): UseApi<Secured, ()>,
240        Path(DiagServicePathParam { service }): Path<DiagServicePathParam>,
241        WithRejection(Query(query), _): WithRejection<
242            Query<sovd_interfaces::components::ecu::data::service::put::Query>,
243            ApiError,
244        >,
245        State(WebserverEcuState { ecu_name, uds, .. }): State<WebserverEcuState<T, U>>,
246        body: Bytes,
247    ) -> Response {
248        let include_schema = query.include_schema;
249        if service.contains('/') {
250            return ErrorWrapper {
251                error: ApiError::BadRequest("Invalid path".to_owned()),
252                include_schema,
253            }
254            .into_response();
255        }
256        data_request::<T>(
257            DiagComm {
258                name: service.clone(),
259                type_: DiagCommType::Configurations,
260                lookup_name: None,
261                subfunction_id: None,
262            },
263            &ecu_name,
264            &uds,
265            headers,
266            Some(body),
267            security_plugin,
268            include_schema,
269        )
270        .await
271    }
272
273    pub(crate) fn docs_put(op: TransformOperation) -> TransformOperation {
274        openapi::request_json_and_octet::<
275            sovd_interfaces::components::ecu::data::DataRequestPayload
276        >(op)
277            .description("Update data for a specific data service")
278            .with(openapi::ecu_service_response)
279            .with(openapi::error_forbidden)
280            .with(openapi::error_not_found)
281            .with(openapi::error_internal_server)
282            .with(openapi::error_conflict)
283            .with(openapi::error_bad_request)
284            .with(openapi::error_bad_gateway)
285    }
286
287    /// `GET /data/{service}/docs` - online capability description for a data service.
288    pub(crate) mod docs_endpoint {
289        use aide::{UseApi, openapi::OpenApi, transform::TransformOperation};
290        use axum::{
291            Json,
292            extract::{Path, State},
293            response::{IntoResponse as _, Response},
294        };
295        use cda_interfaces::{DynamicPlugin, SchemaProvider, UdsEcu, file_manager::FileManager};
296        use cda_plugin_security::Secured;
297
298        use crate::{
299            openapi,
300            sovd::{WebserverEcuState, docs, error::ApiError},
301        };
302
303        openapi::aide_helper::gen_path_param!(DataDocsPathParam service String);
304
305        pub(crate) async fn get<T: UdsEcu + SchemaProvider + Clone, U: FileManager>(
306            UseApi(Secured(security_plugin), _): UseApi<Secured, ()>,
307            Path(DataDocsPathParam { service }): Path<DataDocsPathParam>,
308            State(WebserverEcuState { ecu_name, uds, .. }): State<WebserverEcuState<T, U>>,
309        ) -> Response {
310            let security_plugin: DynamicPlugin = security_plugin;
311
312            // Verify the data service exists
313            let data_info = match uds
314                .get_components_data_info(&ecu_name, &security_plugin)
315                .await
316            {
317                Ok(info) => info,
318                Err(e) => return ApiError::from(e).into_response(),
319            };
320
321            if !data_info
322                .iter()
323                .any(|d| d.id.eq_ignore_ascii_case(&service))
324            {
325                return ApiError::NotFound(Some(format!("Data service '{service}' not found")))
326                    .into_response();
327            }
328
329            docs::data::build_docs_response(&uds, &ecu_name, &service, "data").await
330        }
331
332        pub(crate) fn docs_transform(op: TransformOperation) -> TransformOperation {
333            op.description(
334                "Online capability description for a specific data service on this ECU component \
335                 (ISO 17978-3 Section 7.5). Returns a self-contained OpenAPI specification \
336                 describing the available methods (GET and optionally PUT) with their data types.",
337            )
338            .response_with::<200, Json<OpenApi>, _>(|res| {
339                res.description("Self-contained OpenAPI 3.1 specification for this data service.")
340            })
341            .with(openapi::error_not_found)
342        }
343
344        #[cfg(test)]
345        mod tests {
346            use aide::UseApi;
347            use axum::{extract::State, http::StatusCode};
348            use cda_interfaces::{
349                DiagServiceError,
350                datatypes::ComponentDataInfo,
351                file_manager::mock::MockFileManager,
352                mock::{MockUdsEcu, mock_ecu_state_online_variant_detected},
353            };
354            use cda_plugin_security::{Secured, mock::TestSecurityPlugin};
355
356            use super::*;
357            use crate::sovd::tests::create_test_webserver_state;
358
359            #[tokio::test]
360            async fn returns_200_with_openapi_doc_when_service_exists() {
361                let mut mock_uds = MockUdsEcu::new();
362                mock_uds
363                    .expect_get_ecu_state()
364                    .returning(|_| Ok(mock_ecu_state_online_variant_detected()));
365                mock_uds
366                    .expect_get_components_data_info()
367                    .withf(|ecu, _| ecu == "TestECU")
368                    .times(1)
369                    .returning(|_, _| {
370                        Ok(vec![ComponentDataInfo {
371                            category: "sensor".to_owned(),
372                            id: "EngineTemp".to_owned(),
373                            name: "Engine Temperature".to_owned(),
374                        }])
375                    });
376
377                let state = create_test_webserver_state::<MockUdsEcu, MockFileManager>(
378                    "TestECU".to_owned(),
379                    mock_uds,
380                    MockFileManager::new(),
381                );
382
383                let response = get::<MockUdsEcu, MockFileManager>(
384                    UseApi(
385                        Secured(Box::new(TestSecurityPlugin)),
386                        std::marker::PhantomData,
387                    ),
388                    Path(DataDocsPathParam {
389                        service: "EngineTemp".to_owned(),
390                    }),
391                    State(state),
392                )
393                .await;
394
395                assert_eq!(response.status(), StatusCode::OK);
396                let body = axum::body::to_bytes(response.into_body(), usize::MAX)
397                    .await
398                    .unwrap();
399                let doc: serde_json::Value = serde_json::from_slice(&body).unwrap();
400                assert!(
401                    doc.get("info").is_some(),
402                    "Response should be a valid OpenAPI doc"
403                );
404                assert!(doc.get("paths").is_some(), "Response should contain paths");
405            }
406
407            #[tokio::test]
408            async fn returns_404_when_service_not_found() {
409                let mut mock_uds = MockUdsEcu::new();
410                mock_uds
411                    .expect_get_ecu_state()
412                    .returning(|_| Ok(mock_ecu_state_online_variant_detected()));
413                mock_uds
414                    .expect_get_components_data_info()
415                    .returning(|_, _| {
416                        Ok(vec![ComponentDataInfo {
417                            category: "sensor".to_owned(),
418                            id: "EngineTemp".to_owned(),
419                            name: "Engine Temperature".to_owned(),
420                        }])
421                    });
422
423                let state = create_test_webserver_state::<MockUdsEcu, MockFileManager>(
424                    "TestECU".to_owned(),
425                    mock_uds,
426                    MockFileManager::new(),
427                );
428
429                let response = get::<MockUdsEcu, MockFileManager>(
430                    UseApi(
431                        Secured(Box::new(TestSecurityPlugin)),
432                        std::marker::PhantomData,
433                    ),
434                    Path(DataDocsPathParam {
435                        service: "NonExistent".to_owned(),
436                    }),
437                    State(state),
438                )
439                .await;
440
441                assert_eq!(response.status(), StatusCode::NOT_FOUND);
442            }
443
444            #[tokio::test]
445            async fn returns_error_when_data_info_lookup_fails() {
446                let mut mock_uds = MockUdsEcu::new();
447                mock_uds
448                    .expect_get_ecu_state()
449                    .returning(|_| Ok(mock_ecu_state_online_variant_detected()));
450                mock_uds
451                    .expect_get_components_data_info()
452                    .returning(|_, _| Err(DiagServiceError::NotFound("ECU not found".to_owned())));
453
454                let state = create_test_webserver_state::<MockUdsEcu, MockFileManager>(
455                    "TestECU".to_owned(),
456                    mock_uds,
457                    MockFileManager::new(),
458                );
459
460                let response = get::<MockUdsEcu, MockFileManager>(
461                    UseApi(
462                        Secured(Box::new(TestSecurityPlugin)),
463                        std::marker::PhantomData,
464                    ),
465                    Path(DataDocsPathParam {
466                        service: "Anything".to_owned(),
467                    }),
468                    State(state),
469                )
470                .await;
471
472                assert_eq!(response.status(), StatusCode::NOT_FOUND);
473            }
474        }
475    }
476}