1pub use dpp_plugin_traits as traits;
36
37pub use dpp_rules as rules;
41
42pub mod validate;
43
44use dpp_plugin_traits::{AbiResult, DppSectorPlugin, PluginError, PluginInput};
45pub use dpp_plugin_traits::{
46 METRIC_CO2E_SCORE, METRIC_RECYCLED_CONTENT_PCT, METRIC_REPAIRABILITY_INDEX,
47 PluginComplianceStatus,
48};
49use serde::Serialize;
50
51pub mod abi {
54 use std::alloc::{Layout, alloc as mem_alloc, dealloc as mem_dealloc};
55
56 #[must_use]
59 pub fn host_alloc(len: u32) -> u32 {
60 if len == 0 {
61 return 0;
62 }
63 let layout = Layout::from_size_align(len as usize, 1).expect("valid layout");
64 unsafe { mem_alloc(layout) as u32 }
66 }
67
68 pub fn host_dealloc(ptr: u32, len: u32) {
71 if ptr == 0 || len == 0 {
72 return;
73 }
74 let layout = Layout::from_size_align(len as usize, 1).expect("valid layout");
75 unsafe { mem_dealloc(ptr as *mut u8, layout) }
77 }
78
79 #[must_use]
86 pub unsafe fn read_input<'a>(ptr: u32, len: u32) -> &'a [u8] {
87 unsafe {
88 if len == 0 {
89 return &[];
90 }
91 std::slice::from_raw_parts(ptr as *const u8, len as usize)
92 }
93 }
94
95 #[must_use]
103 pub fn write_output(bytes: Vec<u8>) -> u64 {
104 let mut boxed = bytes.into_boxed_slice();
105 let out_len = boxed.len() as u32;
106 let out_ptr = boxed.as_mut_ptr() as usize as u32;
107 std::mem::forget(boxed);
108 ((out_ptr as u64) << 32) | (out_len as u64)
109 }
110}
111
112fn to_bytes<T: Serialize>(value: &T) -> Vec<u8> {
115 serde_json::to_vec(value).unwrap_or_default()
116}
117
118fn parse_input(bytes: &[u8]) -> Result<PluginInput, PluginError> {
119 serde_json::from_slice(bytes).map_err(|e| PluginError::InvalidInput(e.to_string()))
120}
121
122pub fn metadata_bytes<P: DppSectorPlugin>(plugin: &P) -> Vec<u8> {
124 to_bytes(&plugin.meta())
125}
126
127pub fn describe_bytes<P: DppSectorPlugin>(plugin: &P) -> Vec<u8> {
129 to_bytes(&plugin.capabilities())
130}
131
132pub fn validate_bytes<P: DppSectorPlugin>(plugin: &P, input: &[u8]) -> Vec<u8> {
134 let outcome = match parse_input(input) {
135 Ok(value) => match plugin.validate_input(&value) {
136 Ok(()) => AbiResult::Ok(serde_json::Value::Null),
137 Err(e) => AbiResult::Error(e),
138 },
139 Err(e) => AbiResult::Error(e),
140 };
141 to_bytes(&outcome)
142}
143
144pub fn calculate_metrics_bytes<P: DppSectorPlugin>(plugin: &P, input: &[u8]) -> Vec<u8> {
146 let outcome = match parse_input(input) {
147 Ok(value) => match plugin.calculate_metrics(&value) {
148 Ok(result) => AbiResult::ok(&result),
149 Err(e) => AbiResult::Error(e),
150 },
151 Err(e) => AbiResult::Error(e),
152 };
153 to_bytes(&outcome)
154}
155
156pub fn generate_passport_bytes<P: DppSectorPlugin>(plugin: &P, input: &[u8]) -> Vec<u8> {
158 let outcome = match parse_input(input) {
159 Ok(value) => match plugin.generate_passport(&value) {
160 Ok(payload) => AbiResult::Ok(payload),
161 Err(e) => AbiResult::Error(e),
162 },
163 Err(e) => AbiResult::Error(e),
164 };
165 to_bytes(&outcome)
166}
167
168pub fn run_metadata<P: DppSectorPlugin>(plugin: &P) -> u64 {
171 abi::write_output(metadata_bytes(plugin))
172}
173
174pub fn run_describe<P: DppSectorPlugin>(plugin: &P) -> u64 {
175 abi::write_output(describe_bytes(plugin))
176}
177
178pub unsafe fn run_validate<P: DppSectorPlugin>(plugin: &P, ptr: u32, len: u32) -> u64 {
181 unsafe { abi::write_output(validate_bytes(plugin, abi::read_input(ptr, len))) }
182}
183
184pub unsafe fn run_calculate_metrics<P: DppSectorPlugin>(plugin: &P, ptr: u32, len: u32) -> u64 {
187 unsafe { abi::write_output(calculate_metrics_bytes(plugin, abi::read_input(ptr, len))) }
188}
189
190pub unsafe fn run_generate_passport<P: DppSectorPlugin>(plugin: &P, ptr: u32, len: u32) -> u64 {
193 unsafe { abi::write_output(generate_passport_bytes(plugin, abi::read_input(ptr, len))) }
194}
195
196#[macro_export]
214macro_rules! export_plugin {
215 ($plugin:ty) => {
216 #[unsafe(no_mangle)]
217 pub extern "C" fn alloc(len: u32) -> u32 {
218 $crate::abi::host_alloc(len)
219 }
220
221 #[unsafe(no_mangle)]
222 pub extern "C" fn dealloc(ptr: u32, len: u32) {
223 $crate::abi::host_dealloc(ptr, len)
224 }
225
226 #[unsafe(no_mangle)]
227 pub extern "C" fn metadata() -> u64 {
228 $crate::run_metadata(&<$plugin as ::core::default::Default>::default())
229 }
230
231 #[unsafe(no_mangle)]
232 pub extern "C" fn describe() -> u64 {
233 $crate::run_describe(&<$plugin as ::core::default::Default>::default())
234 }
235
236 #[unsafe(no_mangle)]
237 pub extern "C" fn validate(ptr: u32, len: u32) -> u64 {
238 unsafe {
240 $crate::run_validate(&<$plugin as ::core::default::Default>::default(), ptr, len)
241 }
242 }
243
244 #[unsafe(no_mangle)]
245 pub extern "C" fn calculate_metrics(ptr: u32, len: u32) -> u64 {
246 unsafe {
248 $crate::run_calculate_metrics(
249 &<$plugin as ::core::default::Default>::default(),
250 ptr,
251 len,
252 )
253 }
254 }
255
256 #[unsafe(no_mangle)]
257 pub extern "C" fn generate_passport(ptr: u32, len: u32) -> u64 {
258 unsafe {
260 $crate::run_generate_passport(
261 &<$plugin as ::core::default::Default>::default(),
262 ptr,
263 len,
264 )
265 }
266 }
267 };
268}
269
270#[cfg(test)]
273mod tests {
274 use super::*;
275 use dpp_plugin_traits::{
276 AbiVersion, METRIC_CO2E_SCORE, PluginCapabilities, PluginCapability,
277 PluginComplianceStatus, PluginFieldError, PluginMeta, PluginResult, SchemaVersionRange,
278 };
279 use serde_json::{Value, json};
280
281 #[derive(Default)]
283 struct DummyPlugin;
284
285 impl DppSectorPlugin for DummyPlugin {
286 fn meta(&self) -> PluginMeta {
287 PluginMeta {
288 sector: "dummy".into(),
289 name: "Dummy".into(),
290 version: "0.1.0".into(),
291 license: "Apache-2.0".into(),
292 description: None,
293 author: None,
294 homepage: None,
295 }
296 }
297
298 fn capabilities(&self) -> PluginCapabilities {
299 PluginCapabilities {
300 abi_version: AbiVersion::current(),
301 supported_schemas: vec![SchemaVersionRange {
302 min_version: "1.0.0".into(),
303 max_version: "1.0.0".into(),
304 }],
305 capabilities: vec![PluginCapability::ComputeMetrics],
306 min_host_version: None,
307 max_fuel: None,
308 max_memory_bytes: None,
309 }
310 }
311
312 fn validate_input(&self, input: &PluginInput) -> Result<(), PluginError> {
313 if input.get("ok").is_some() {
314 Ok(())
315 } else {
316 Err(PluginError::ValidationErrors(vec![PluginFieldError {
317 field: "/ok".into(),
318 code: "missing".into(),
319 message: "ok is required".into(),
320 }]))
321 }
322 }
323
324 fn calculate_metrics(&self, input: &PluginInput) -> Result<PluginResult, PluginError> {
325 self.validate_input(input)?;
326 Ok(PluginResult::new(PluginComplianceStatus::NotAssessed)
327 .maybe_metric(METRIC_CO2E_SCORE, input.get("co2e").and_then(Value::as_f64)))
328 }
329
330 fn generate_passport(&self, input: &PluginInput) -> Result<Value, PluginError> {
331 self.validate_input(input)?;
332 Ok(input.clone())
333 }
334 }
335
336 fn parse(bytes: &[u8]) -> Value {
337 serde_json::from_slice(bytes).expect("glue emits valid JSON")
338 }
339
340 #[test]
341 fn describe_emits_capabilities() {
342 let json = parse(&describe_bytes(&DummyPlugin));
343 assert_eq!(json["abiVersion"]["major"], 1);
344 assert!(json["supportedSchemas"].is_array());
345 let back: PluginCapabilities = serde_json::from_value(json).unwrap();
347 assert_eq!(back.abi_version, AbiVersion::current());
348 }
349
350 #[test]
351 fn metadata_emits_meta() {
352 let json = parse(&metadata_bytes(&DummyPlugin));
353 assert_eq!(json["sector"], "dummy");
354 }
355
356 #[test]
357 fn calculate_metrics_ok_envelope() {
358 let input = json!({ "ok": true, "co2e": 42.0 });
359 let json = parse(&calculate_metrics_bytes(&DummyPlugin, &to_bytes(&input)));
360 assert_eq!(json["ok"]["metrics"]["co2e_score"], 42.0);
361 assert_eq!(json["ok"]["complianceStatus"], "NOT_ASSESSED");
362 }
363
364 #[test]
365 fn calculate_metrics_validation_error_envelope() {
366 let input = json!({ "co2e": 42.0 }); let json = parse(&calculate_metrics_bytes(&DummyPlugin, &to_bytes(&input)));
368 assert!(json.get("error").is_some());
369 assert!(json.get("ok").is_none());
370 }
371
372 #[test]
373 fn validate_error_on_malformed_json() {
374 let json = parse(&validate_bytes(&DummyPlugin, b"not json {{{"));
375 let back: AbiResult = serde_json::from_value(json).unwrap();
376 assert!(!back.is_ok());
377 }
378
379 #[test]
380 fn validate_ok_envelope_is_null() {
381 let input = json!({ "ok": true });
382 let json = parse(&validate_bytes(&DummyPlugin, &to_bytes(&input)));
383 assert!(json["ok"].is_null());
384 }
385
386 #[test]
387 fn generate_passport_passthrough() {
388 let input = json!({ "ok": true, "gtin": "12345678901231" });
389 let json = parse(&generate_passport_bytes(&DummyPlugin, &to_bytes(&input)));
390 assert_eq!(json["ok"]["gtin"], "12345678901231");
391 }
392
393 #[test]
394 fn validate_error_when_input_parses_but_is_rejected() {
395 let input = json!({ "missing": "ok" });
398 let json = parse(&validate_bytes(&DummyPlugin, &to_bytes(&input)));
399 assert!(json.get("error").is_some());
400 assert!(json.get("ok").is_none());
401 }
402
403 #[test]
404 fn generate_passport_error_when_input_parses_but_is_rejected() {
405 let input = json!({ "missing": "ok" });
406 let json = parse(&generate_passport_bytes(&DummyPlugin, &to_bytes(&input)));
407 assert!(json.get("error").is_some());
408 assert!(json.get("ok").is_none());
409 }
410
411 export_plugin!(DummyPlugin);
426
427 fn out_len(packed: u64) -> usize {
429 (packed & 0xFFFF_FFFF) as usize
430 }
431
432 #[test]
433 fn macro_alloc_dealloc_are_callable() {
434 assert_eq!(alloc(0), 0);
436 let _ = alloc(8);
440 dealloc(0, 0);
442 }
443
444 #[test]
445 fn macro_metadata_and_describe_pack_glue_output() {
446 assert_eq!(out_len(metadata()), metadata_bytes(&DummyPlugin).len());
447 assert_eq!(out_len(describe()), describe_bytes(&DummyPlugin).len());
448 }
449
450 #[test]
451 fn macro_input_exports_pack_error_envelope_for_empty_input() {
452 assert_eq!(
455 out_len(validate(0, 0)),
456 validate_bytes(&DummyPlugin, &[]).len()
457 );
458 assert_eq!(
459 out_len(calculate_metrics(0, 0)),
460 calculate_metrics_bytes(&DummyPlugin, &[]).len()
461 );
462 assert_eq!(
463 out_len(generate_passport(0, 0)),
464 generate_passport_bytes(&DummyPlugin, &[]).len()
465 );
466 }
467}