Skip to main content

serde_nixos/
lib.rs

1//! # serde-nixos
2//!
3//! Generate NixOS type definitions from your Rust structures.
4//!
5//! This crate provides a derive macro that automatically generates NixOS module
6//! definitions from your Rust configuration structures, ensuring type safety
7//! across both Rust and Nix.
8//!
9//! ## Quick Start
10//!
11//! ```rust
12//! use serde::{Serialize, Deserialize};
13//! use serde_nixos::NixosType;
14//!
15//! #[derive(Serialize, Deserialize, NixosType)]
16//! struct ServerConfig {
17//!     #[nixos(description = "The server port to listen on")]
18//!     port: u16,
19//!
20//!     #[nixos(default = "\"localhost\"", description = "The hostname to bind to")]
21//!     host: String,
22//!
23//!     #[nixos(description = "Maximum number of concurrent connections")]
24//!     max_connections: Option<u32>,
25//!
26//!     #[nixos(description = "Database configuration")]
27//!     database: DatabaseConfig,
28//! }
29//!
30//! #[derive(Serialize, Deserialize, NixosType)]
31//! struct DatabaseConfig {
32//!     #[nixos(description = "Database connection string")]
33//!     url: String,
34//!
35//!     #[nixos(default = "5", description = "Connection pool size")]
36//!     pool_size: u32,
37//! }
38//! ```
39//!
40//! Then generate the NixOS module:
41//!
42//! ```rust
43//! # use serde::{Serialize, Deserialize};
44//! # use serde_nixos::NixosType;
45//! #
46//! # #[derive(Serialize, Deserialize, NixosType)]
47//! # struct ServerConfig {
48//! #     enable: bool,
49//! # }
50//! #
51//! let nixos_module = ServerConfig::nixos_type_definition();
52//! println!("{}", nixos_module);
53//! ```
54//!
55//! This will output a NixOS module definition that can be used in your NixOS configuration.
56//!
57//! ## Multi-Type Module Generation
58//!
59//! For generating `.nix` files that compose multiple types with proper
60//! `let` bindings and cross-references, use [`NixosModuleGenerator`]:
61//!
62//! ```rust
63//! use serde::{Serialize, Deserialize};
64//! use serde_nixos::{NixosType, type_registration};
65//! use serde_nixos::generator::NixosModuleGenerator;
66//!
67//! #[derive(Serialize, Deserialize, NixosType)]
68//! struct Inner { value: String }
69//!
70//! #[derive(Serialize, Deserialize, NixosType)]
71//! struct Outer { child: Inner }
72//!
73//! let nix = NixosModuleGenerator::new()
74//!     .register(type_registration!(Inner))
75//!     .register(type_registration!(Outer))
76//!     .export_all_types()
77//!     .generate();
78//! ```
79
80pub use serde_nixos_macros::{nixos_module, NixosType};
81
82/// Re-export commonly used serde traits for convenience
83pub use serde::{Deserialize, Serialize};
84
85/// Generator utilities for building NixOS modules
86pub mod generator;
87
88// Re-export key generator types for convenience
89pub use generator::{NixosModuleGenerator, TypeRegistration};
90
91/// Create a [`TypeRegistration`] from a type that derives [`NixosType`].
92///
93/// This macro captures the output of the derive-generated inherent methods
94/// (`nixos_type_name()`, `nixos_options()`, `nixos_options_named()`,
95/// `nixos_type()`) into a [`TypeRegistration`] struct suitable for
96/// [`NixosModuleGenerator::register`].
97///
98/// # Example
99///
100/// ```rust
101/// use serde::{Serialize, Deserialize};
102/// use serde_nixos::{NixosType, type_registration};
103///
104/// #[derive(Serialize, Deserialize, NixosType)]
105/// struct MyConfig {
106///     port: u16,
107///     host: String,
108/// }
109///
110/// let reg = type_registration!(MyConfig);
111/// assert_eq!(reg.type_name, "myConfigType");
112/// ```
113#[macro_export]
114macro_rules! type_registration {
115    ($ty:ty) => {
116        $crate::generator::TypeRegistration {
117            type_name: <$ty>::nixos_type_name(),
118            options: <$ty>::nixos_options(),
119            options_named: <$ty>::nixos_options_named(),
120            type_expr: <$ty>::nixos_type(),
121        }
122    };
123}
124
125/// Helper trait for types that can generate NixOS definitions
126pub trait NixosTypeGenerator {
127    /// Generate a complete NixOS module definition
128    fn nixos_type_definition() -> String;
129
130    /// Generate just the options portion of the module
131    fn nixos_options() -> String;
132
133    /// Get the NixOS type expression for this type
134    fn nixos_type() -> String;
135}
136
137/// Utility functions for working with NixOS types
138pub mod utils {
139
140    /// Format a Rust value as a Nix expression
141    pub fn format_nix_value(value: &serde_json::Value) -> String {
142        match value {
143            serde_json::Value::Null => "null".to_string(),
144            serde_json::Value::Bool(b) => b.to_string(),
145            serde_json::Value::Number(n) => n.to_string(),
146            serde_json::Value::String(s) => format!("\"{}\"", escape_nix_string(s)),
147            serde_json::Value::Array(arr) => {
148                let items: Vec<String> = arr.iter().map(format_nix_value).collect();
149                format!("[ {} ]", items.join(" "))
150            }
151            serde_json::Value::Object(obj) => {
152                let attrs: Vec<String> = obj
153                    .iter()
154                    .map(|(k, v)| format!("{} = {};", k, format_nix_value(v)))
155                    .collect();
156                format!("{{ {} }}", attrs.join(" "))
157            }
158        }
159    }
160
161    /// Escape a string for use in Nix expressions
162    pub fn escape_nix_string(s: &str) -> String {
163        s.replace('\\', "\\\\")
164            .replace('"', "\\\"")
165            .replace('\n', "\\n")
166            .replace('\r', "\\r")
167            .replace('\t', "\\t")
168    }
169
170    /// Generate a NixOS module file with proper formatting
171    pub fn generate_module_file(module_name: &str, options: &str, config: Option<&str>) -> String {
172        let mut result = String::new();
173
174        result.push_str("{ config, lib, pkgs, ... }:\n\n");
175        result.push_str("with lib;\n\n");
176        result.push_str("{\n");
177
178        // Add options
179        result.push_str("  options.");
180        result.push_str(module_name);
181        result.push_str(" = {\n");
182        result.push_str(options);
183        result.push_str("  };\n\n");
184
185        // Add config if provided
186        if let Some(cfg) = config {
187            result.push_str("  config = mkIf config.");
188            result.push_str(module_name);
189            result.push_str(".enable {\n");
190            result.push_str(cfg);
191            result.push_str("  };\n");
192        }
193
194        result.push_str("}\n");
195        result
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_nix_value_formatting() {
205        assert_eq!(utils::format_nix_value(&serde_json::json!(null)), "null");
206        assert_eq!(utils::format_nix_value(&serde_json::json!(true)), "true");
207        assert_eq!(utils::format_nix_value(&serde_json::json!(42)), "42");
208        assert_eq!(
209            utils::format_nix_value(&serde_json::json!("hello")),
210            "\"hello\""
211        );
212        assert_eq!(
213            utils::format_nix_value(&serde_json::json!([1, 2, 3])),
214            "[ 1 2 3 ]"
215        );
216    }
217
218    #[test]
219    fn test_escape_nix_string() {
220        assert_eq!(utils::escape_nix_string("hello"), "hello");
221        assert_eq!(utils::escape_nix_string("hello\"world"), "hello\\\"world");
222        assert_eq!(utils::escape_nix_string("line1\nline2"), "line1\\nline2");
223    }
224}