nginx_lint_plugin/lib.rs
1//! Plugin SDK for building nginx-lint WASM plugins
2//!
3//! This crate provides everything needed to create custom lint rules as WASM plugins
4//! for [nginx-lint](https://github.com/walf443/nginx-lint).
5//!
6//! # Getting Started
7//!
8//! 1. Create a library crate with `crate-type = ["cdylib", "rlib"]`
9//! 2. Implement the [`Plugin`] trait
10//! 3. Register with [`export_plugin!`]
11//! 4. Build with `cargo build --target wasm32-unknown-unknown --release`
12//!
13//! # Modules
14//!
15//! - [`types`] - Core types: [`Plugin`], [`PluginSpec`], [`LintError`], [`Fix`],
16//! [`ConfigExt`], [`DirectiveExt`]
17//! - [`helpers`] - Utility functions for common checks (domain names, URLs, etc.)
18//! - [`testing`] - Test runner and builder: [`testing::PluginTestRunner`], [`testing::TestCase`]
19//! - [`native`] - [`native::NativePluginRule`] adapter for running plugins without WASM
20//! - [`prelude`] - Convenient re-exports for `use nginx_lint_plugin::prelude::*`
21//!
22//! # API Versioning
23//!
24//! Plugins declare the API version they use via [`PluginSpec::api_version`].
25//! This allows the host to support multiple output formats for backward compatibility.
26//! [`PluginSpec::new()`] automatically sets the current API version ([`API_VERSION`]).
27//!
28//! # Example
29//!
30//! ```
31//! use nginx_lint_plugin::prelude::*;
32//!
33//! #[derive(Default)]
34//! struct MyRule;
35//!
36//! impl Plugin for MyRule {
37//! fn spec(&self) -> PluginSpec {
38//! PluginSpec::new("my-custom-rule", "custom", "My custom lint rule")
39//! .with_severity("warning")
40//! .with_why("Explain why this rule matters.")
41//! .with_bad_example("server {\n dangerous_directive on;\n}")
42//! .with_good_example("server {\n # dangerous_directive removed\n}")
43//! }
44//!
45//! fn check(&self, config: &Config, _path: &str) -> Vec<LintError> {
46//! let mut errors = Vec::new();
47//! let err = self.spec().error_builder();
48//!
49//! for ctx in config.all_directives_with_context() {
50//! if ctx.directive.is("dangerous_directive") {
51//! errors.push(
52//! err.warning_at("Avoid using dangerous_directive", ctx.directive)
53//! );
54//! }
55//! }
56//! errors
57//! }
58//! }
59//!
60//! // export_plugin!(MyRule); // Required for WASM build
61//!
62//! // Verify it works
63//! let plugin = MyRule;
64//! let config = nginx_lint_plugin::parse_string("dangerous_directive on;").unwrap();
65//! let errors = plugin.check(&config, "test.conf");
66//! assert_eq!(errors.len(), 1);
67//! ```
68
69pub mod helpers;
70pub mod native;
71pub mod testing;
72mod types;
73
74#[cfg(feature = "container-testing")]
75pub mod container_testing;
76
77#[cfg(feature = "wit-export")]
78pub mod wasm_config;
79#[cfg(feature = "wit-export")]
80pub mod wit_guest;
81
82pub use types::*;
83
84// Re-export common types from nginx-lint-common
85pub use nginx_lint_common::parse_string;
86pub use nginx_lint_common::parser;
87
88/// Prelude module for convenient imports.
89///
90/// Importing everything from this module is the recommended way to use the SDK:
91///
92/// ```
93/// use nginx_lint_plugin::prelude::*;
94///
95/// // All core types are now available
96/// let spec = PluginSpec::new("example", "test", "Example rule");
97/// assert_eq!(spec.name, "example");
98/// ```
99///
100/// This re-exports all core types ([`Plugin`], [`PluginSpec`], [`LintError`], [`Fix`],
101/// [`Config`], [`Directive`], etc.), extension traits ([`ConfigExt`], [`DirectiveExt`]),
102/// the [`helpers`] module, and the [`export_plugin!`] macro.
103pub mod prelude {
104 pub use super::export_component_plugin;
105 pub use super::export_plugin;
106 pub use super::helpers;
107 pub use super::types::API_VERSION;
108 pub use super::types::*;
109}
110
111/// Macro to export a plugin implementation (legacy core module format)
112///
113/// **Deprecated**: Use [`export_component_plugin!`] instead, which generates
114/// WIT component model exports. This macro generates legacy core WASM module
115/// exports and will be removed in a future version.
116///
117/// # Example
118///
119/// ```
120/// use nginx_lint_plugin::prelude::*;
121///
122/// #[derive(Default)]
123/// struct MyPlugin;
124///
125/// impl Plugin for MyPlugin {
126/// fn spec(&self) -> PluginSpec {
127/// PluginSpec::new("my-plugin", "custom", "My plugin")
128/// }
129///
130/// fn check(&self, config: &Config, _path: &str) -> Vec<LintError> {
131/// Vec::new()
132/// }
133/// }
134///
135/// // Preferred:
136/// export_component_plugin!(MyPlugin);
137/// ```
138#[deprecated(
139 since = "0.7.0",
140 note = "Use export_component_plugin! instead for WIT component model support"
141)]
142#[doc(hidden)]
143pub fn _export_plugin_deprecated() {}
144
145#[macro_export]
146macro_rules! export_plugin {
147 ($plugin_type:ty) => {
148 const _: fn() = $crate::_export_plugin_deprecated;
149
150 #[cfg(all(target_arch = "wasm32", feature = "wasm-export"))]
151 const _: () = {
152 static PLUGIN: std::sync::OnceLock<$plugin_type> = std::sync::OnceLock::new();
153 static PLUGIN_SPEC_CACHE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
154 static CHECK_RESULT_CACHE: std::sync::Mutex<String> =
155 std::sync::Mutex::new(String::new());
156
157 fn get_plugin() -> &'static $plugin_type {
158 PLUGIN.get_or_init(|| <$plugin_type>::default())
159 }
160
161 /// Allocate memory for the host to write data
162 #[unsafe(no_mangle)]
163 pub extern "C" fn alloc(size: u32) -> *mut u8 {
164 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
165 unsafe { std::alloc::alloc(layout) }
166 }
167
168 /// Deallocate memory
169 #[unsafe(no_mangle)]
170 pub extern "C" fn dealloc(ptr: *mut u8, size: u32) {
171 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
172 unsafe { std::alloc::dealloc(ptr, layout) }
173 }
174
175 /// Get the length of the plugin spec JSON
176 #[unsafe(no_mangle)]
177 pub extern "C" fn plugin_spec_len() -> u32 {
178 let info = PLUGIN_SPEC_CACHE.get_or_init(|| {
179 let plugin = get_plugin();
180 let info = $crate::Plugin::spec(plugin);
181 serde_json::to_string(&info).unwrap_or_default()
182 });
183 info.len() as u32
184 }
185
186 /// Get the plugin spec JSON pointer
187 #[unsafe(no_mangle)]
188 pub extern "C" fn plugin_spec() -> *const u8 {
189 let info = PLUGIN_SPEC_CACHE.get_or_init(|| {
190 let plugin = get_plugin();
191 let info = $crate::Plugin::spec(plugin);
192 serde_json::to_string(&info).unwrap_or_default()
193 });
194 info.as_ptr()
195 }
196
197 /// Run the lint check
198 #[unsafe(no_mangle)]
199 pub extern "C" fn check(
200 config_ptr: *const u8,
201 config_len: u32,
202 path_ptr: *const u8,
203 path_len: u32,
204 ) -> *const u8 {
205 // Read config JSON from memory
206 let config_json = unsafe {
207 let slice = std::slice::from_raw_parts(config_ptr, config_len as usize);
208 std::str::from_utf8_unchecked(slice)
209 };
210
211 // Read path from memory
212 let path = unsafe {
213 let slice = std::slice::from_raw_parts(path_ptr, path_len as usize);
214 std::str::from_utf8_unchecked(slice)
215 };
216
217 // Parse config
218 let config: $crate::Config = match serde_json::from_str(config_json) {
219 Ok(c) => c,
220 Err(e) => {
221 let errors = vec![$crate::LintError::error(
222 "plugin-error",
223 "plugin",
224 &format!("Failed to parse config: {}", e),
225 0,
226 0,
227 )];
228 let result = serde_json::to_string(&errors).unwrap_or_default();
229 let mut cache = CHECK_RESULT_CACHE.lock().unwrap();
230 *cache = result;
231 return cache.as_ptr();
232 }
233 };
234
235 // Run the check
236 let plugin = get_plugin();
237 let errors = $crate::Plugin::check(plugin, &config, path);
238
239 // Serialize result
240 let result = serde_json::to_string(&errors).unwrap_or_else(|_| "[]".to_string());
241 let mut cache = CHECK_RESULT_CACHE.lock().unwrap();
242 *cache = result;
243 cache.as_ptr()
244 }
245
246 /// Get the length of the check result
247 #[unsafe(no_mangle)]
248 pub extern "C" fn check_result_len() -> u32 {
249 let cache = CHECK_RESULT_CACHE.lock().unwrap();
250 cache.len() as u32
251 }
252 };
253 };
254}
255
256/// Macro to export a plugin as a WIT component
257///
258/// This generates the WIT component model exports for your plugin.
259/// Use this instead of `export_plugin!` when building component model plugins.
260///
261/// # Example
262///
263/// ```ignore
264/// use nginx_lint_plugin::prelude::*;
265///
266/// #[derive(Default)]
267/// struct MyPlugin;
268///
269/// impl Plugin for MyPlugin {
270/// fn spec(&self) -> PluginSpec { /* ... */ }
271/// fn check(&self, config: &Config, _path: &str) -> Vec<LintError> { /* ... */ }
272/// }
273///
274/// export_component_plugin!(MyPlugin);
275/// ```
276#[macro_export]
277macro_rules! export_component_plugin {
278 ($plugin_type:ty) => {
279 #[cfg(all(target_arch = "wasm32", feature = "wit-export"))]
280 const _: () = {
281 use $crate::wit_guest::Guest;
282
283 static PLUGIN: std::sync::OnceLock<$plugin_type> = std::sync::OnceLock::new();
284
285 fn get_plugin() -> &'static $plugin_type {
286 PLUGIN.get_or_init(|| <$plugin_type>::default())
287 }
288
289 struct ComponentExport;
290
291 impl Guest for ComponentExport {
292 fn spec() -> $crate::wit_guest::nginx_lint::plugin::types::PluginSpec {
293 let plugin = get_plugin();
294 let sdk_spec = $crate::Plugin::spec(plugin);
295 $crate::wit_guest::convert_spec(sdk_spec)
296 }
297
298 fn check(
299 config: &$crate::wit_guest::nginx_lint::plugin::config_api::Config,
300 path: String,
301 ) -> Vec<$crate::wit_guest::nginx_lint::plugin::types::LintError> {
302 let plugin = get_plugin();
303 // Reconstruct parser Config from host resource handle
304 let config = $crate::wit_guest::reconstruct_config(config);
305 let errors = $crate::Plugin::check(plugin, &config, &path);
306 errors
307 .into_iter()
308 .map($crate::wit_guest::convert_lint_error)
309 .collect()
310 }
311 }
312
313 $crate::wit_guest::export!(ComponentExport with_types_in $crate::wit_guest);
314 };
315 };
316}