dynamo_llm/
lib.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! # Dynamo LLM
5//!
6//! The `dynamo.llm` crate is a Rust library that provides a set of traits and types for building
7//! distributed LLM inference solutions.
8
9use std::{fs::File, io::BufReader, path::Path};
10
11use anyhow::Context as _;
12
13pub mod backend;
14pub mod common;
15pub mod disagg_router;
16pub mod discovery;
17pub mod engines;
18pub mod gguf;
19pub mod http;
20pub mod hub;
21// pub mod key_value_store;
22pub mod kv_router;
23pub mod local_model;
24pub mod mocker;
25pub mod model_card;
26pub mod model_type;
27pub mod preprocessor;
28pub mod protocols;
29pub mod recorder;
30pub mod request_template;
31pub mod tokenizers;
32pub mod tokens;
33pub mod types;
34
35#[cfg(feature = "block-manager")]
36pub mod block_manager;
37
38/// Reads a JSON file, extracts a specific field, and deserializes it into type T.
39///
40/// # Arguments
41///
42/// * `json_file_path`: Path to the JSON file.
43/// * `field_name`: The name of the field to extract from the JSON map.
44///
45/// # Returns
46///
47/// A `Result` containing the deserialized value of type `T` if successful,
48/// or an `anyhow::Error` if any step fails (file I/O, JSON parsing, field not found,
49/// or deserialization to `T` fails).
50///
51/// # Type Parameters
52///
53/// * `T`: The expected type of the field's value. `T` must implement `serde::de::DeserializeOwned`.
54pub fn file_json_field<T: serde::de::DeserializeOwned>(
55    json_file_path: &Path,
56    field_name: &str,
57) -> anyhow::Result<T> {
58    // 1. Open the file
59    let file = File::open(json_file_path)
60        .with_context(|| format!("Failed to open file: {:?}", json_file_path))?;
61    let reader = BufReader::new(file);
62
63    // 2. Parse the JSON file into a generic serde_json::Value
64    // We parse into `serde_json::Value` first because we need to look up a specific field.
65    // If we tried to deserialize directly into `T`, `T` would need to represent the whole JSON structure.
66    let json_data: serde_json::Value = serde_json::from_reader(reader)
67        .with_context(|| format!("Failed to parse JSON from file: {:?}", json_file_path))?;
68
69    // 3. Ensure the root of the JSON is an object (map)
70    let map = json_data.as_object().ok_or_else(|| {
71        anyhow::anyhow!("JSON root is not an object in file: {:?}", json_file_path)
72    })?;
73
74    // 4. Get the specific field's value
75    let field_value = map.get(field_name).ok_or_else(|| {
76        anyhow::anyhow!(
77            "Field '{}' not found in JSON file: {:?}",
78            field_name,
79            json_file_path
80        )
81    })?;
82
83    // 5. Deserialize the field's value into the target type T
84    // We need to clone `field_value` because `from_value` consumes its input.
85    serde_json::from_value(field_value.clone()).with_context(|| {
86        format!(
87            "Failed to deserialize field '{}' (value: {:?}) to the expected type from file: {:?}",
88            field_name, field_value, json_file_path
89        )
90    })
91}
92
93#[cfg(test)]
94mod file_json_field_tests {
95    use super::file_json_field;
96    use serde::Deserialize;
97    use std::fs::File;
98    use std::io::Write;
99    use std::path::{Path, PathBuf};
100    use tempfile::tempdir;
101
102    // Helper function to create a temporary JSON file
103    fn create_temp_json_file(dir: &Path, file_name: &str, content: &str) -> PathBuf {
104        let file_path = dir.join(file_name);
105        let mut file = File::create(&file_path)
106            .unwrap_or_else(|_| panic!("Failed to create test file: {:?}", file_path));
107        file.write_all(content.as_bytes())
108            .unwrap_or_else(|_| panic!("Failed to write to test file: {:?}", file_path));
109        file_path
110    }
111
112    // Define a custom struct for testing deserialization
113    #[derive(Debug, PartialEq, Deserialize)]
114    struct MyConfig {
115        version: String,
116        enabled: bool,
117        count: u32,
118    }
119
120    #[test]
121    fn test_success_basic() {
122        let tmp_dir = tempdir().unwrap();
123        let file_path = create_temp_json_file(
124            tmp_dir.path(),
125            "test_basic.json",
126            r#"{ "name": "Rust", "age": 30, "is_active": true }"#,
127        );
128
129        let name: String = file_json_field(&file_path, "name").unwrap();
130        assert_eq!(name, "Rust");
131
132        let age: i32 = file_json_field(&file_path, "age").unwrap();
133        assert_eq!(age, 30);
134
135        let is_active: bool = file_json_field(&file_path, "is_active").unwrap();
136        assert!(is_active);
137    }
138
139    #[test]
140    fn test_success_custom_struct_field() {
141        let tmp_dir = tempdir().unwrap();
142        let file_path = create_temp_json_file(
143            tmp_dir.path(),
144            "test_struct.json",
145            r#"{
146                "config": {
147                    "version": "1.0.0",
148                    "enabled": true,
149                    "count": 123
150                },
151                "other_field": "value"
152            }"#,
153        );
154
155        let config: MyConfig = file_json_field(&file_path, "config").unwrap();
156        assert_eq!(
157            config,
158            MyConfig {
159                version: "1.0.0".to_string(),
160                enabled: true,
161                count: 123,
162            }
163        );
164    }
165
166    #[test]
167    fn test_file_not_found() {
168        let tmp_dir = tempdir().unwrap();
169        let non_existent_path = tmp_dir.path().join("non_existent.json");
170
171        let result: anyhow::Result<String> = file_json_field(&non_existent_path, "field");
172        assert!(result.is_err());
173        let err = result.unwrap_err();
174        assert!(err.to_string().contains("Failed to open file"));
175    }
176
177    #[test]
178    fn test_invalid_json_syntax() {
179        let tmp_dir = tempdir().unwrap();
180        let file_path = create_temp_json_file(
181            tmp_dir.path(),
182            "invalid.json",
183            r#"{ "key": "value", "bad_syntax": }"#, // Malformed JSON
184        );
185
186        let result: anyhow::Result<String> = file_json_field(&file_path, "key");
187        assert!(result.is_err());
188        let err = result.unwrap_err();
189        assert!(err.to_string().contains("Failed to parse JSON from file"));
190    }
191
192    #[test]
193    fn test_json_root_not_object_array() {
194        let tmp_dir = tempdir().unwrap();
195        let file_path = create_temp_json_file(
196            tmp_dir.path(),
197            "root_array.json",
198            r#"[ { "item": 1 }, { "item": 2 } ]"#, // Root is an array
199        );
200
201        let result: anyhow::Result<String> = file_json_field(&file_path, "item");
202        assert!(result.is_err());
203        let err = result.unwrap_err();
204        assert!(err.to_string().contains("JSON root is not an object"));
205    }
206
207    #[test]
208    fn test_json_root_not_object_primitive() {
209        let tmp_dir = tempdir().unwrap();
210        let file_path = create_temp_json_file(
211            tmp_dir.path(),
212            "root_primitive.json",
213            r#""just_a_string""#, // Root is a string
214        );
215
216        let result: anyhow::Result<String> = file_json_field(&file_path, "field");
217        assert!(result.is_err());
218        let err = result.unwrap_err();
219        assert!(err.to_string().contains("JSON root is not an object"));
220    }
221
222    #[test]
223    fn test_field_not_found() {
224        let tmp_dir = tempdir().unwrap();
225        let file_path = create_temp_json_file(
226            tmp_dir.path(),
227            "missing_field.json",
228            r#"{ "existing_field": "hello" }"#,
229        );
230
231        let result: anyhow::Result<String> = file_json_field(&file_path, "non_existent_field");
232        assert!(result.is_err());
233        let err = result.unwrap_err();
234        assert!(err
235            .to_string()
236            .contains("Field 'non_existent_field' not found"));
237    }
238
239    #[test]
240    fn test_field_type_mismatch() {
241        let tmp_dir = tempdir().unwrap();
242        let file_path = create_temp_json_file(
243            tmp_dir.path(),
244            "type_mismatch.json",
245            r#"{ "count": "not_an_integer" }"#,
246        );
247
248        let result: anyhow::Result<u32> = file_json_field(&file_path, "count");
249        assert!(result.is_err());
250        let err = result.unwrap_err();
251        assert!(err
252            .to_string()
253            .contains("Failed to deserialize field 'count'"));
254    }
255
256    #[test]
257    fn test_empty_file() {
258        let tmp_dir = tempdir().unwrap();
259        let file_path = create_temp_json_file(tmp_dir.path(), "empty.json", "");
260
261        let result: anyhow::Result<String> = file_json_field(&file_path, "field");
262        assert!(result.is_err());
263        let err = result.unwrap_err();
264        assert!(err.to_string().contains("Failed to parse JSON from file"));
265    }
266}