Skip to main content

moonlit_pdk/
lib.rs

1//! Write a Moonlit plugin: one [`Middleware`] impl per step, and one
2//! [`moonlit_plugin!`] block naming them.
3//!
4//! Targets plugin ABI `moonlit:plugin@0.3.0`. Each middleware publishes a JSON
5//! Schema for its `Input` and `Output`, which is what `moonlit plugin inspect`
6//! reads. See [`Middleware`] for a worked example and [`testing`] for driving
7//! one in a unit test.
8
9/// Generated WIT bindings. Public because the `moonlit_plugin!` macro output
10/// (expanded in the author crate) references this exact path via
11/// `export!(Component with_types_in moonlit_pdk::bindings)`.
12#[allow(
13  clippy::too_many_arguments,
14  clippy::same_length_and_capacity,
15  reason = "generated by wit_bindgen"
16)]
17pub mod bindings {
18  wit_bindgen::generate!({
19      path: "wit",
20      world: "plugin",
21      generate_all,
22      pub_export_macro: true,
23      default_bindings_module: "moonlit_pdk::bindings",
24  });
25}
26
27pub use bindings::export;
28
29pub use moonlit_pdk_macros::moonlit_plugin;
30
31mod result;
32pub use result::MiddlewareResult;
33
34pub mod config;
35
36mod context;
37pub use context::{Context, Host, LogLevel};
38
39#[cfg(target_arch = "wasm32")]
40mod real_host;
41#[cfg(target_arch = "wasm32")]
42pub use real_host::RealHost;
43
44pub mod env;
45
46pub mod process;
47
48pub mod state;
49
50pub mod http;
51
52pub mod clock;
53
54pub mod random;
55
56pub mod testing;
57
58mod middleware;
59pub use middleware::Middleware;
60
61/// Serialize a middleware `Input`/`Output` JSON Schema (draft 2020-12) to text.
62///
63/// The `moonlit_plugin!` macro calls this to fill `middleware-info.input-schema`
64/// and `output-schema`, keeping the `schemars`/`serde_json` calls inside the SDK
65/// rather than each author crate. Hidden from docs — it is macro-internal API.
66#[doc(hidden)]
67#[must_use]
68pub fn __schema_json<T: schemars::JsonSchema>() -> String {
69  serde_json::to_string(&schemars::schema_for!(T)).unwrap_or_default()
70}
71
72/// Marker for a middleware that reads no configuration. Deserializes from `{}`
73/// (or an absent config block) and yields an empty-object input schema
74/// ("no declared inputs"). Input-only: it has no `Serialize` impl.
75#[derive(serde::Deserialize, Default, schemars::JsonSchema)]
76pub struct NoInput {}
77
78/// Marker for a middleware that publishes no outputs. Serializes to `{}` and
79/// yields an empty-object output schema ("no declared outputs").
80#[derive(serde::Serialize, schemars::JsonSchema)]
81pub struct NoOutput {}
82
83mod plugin_config;
84pub use plugin_config::PluginConfig;
85
86pub mod prelude {
87  pub use crate::PluginConfig;
88  pub use crate::config::from_json_value;
89  pub use crate::moonlit_plugin;
90  pub use crate::process::LineHandler;
91  pub use crate::state::Shared;
92  pub use crate::{Context, LogLevel, Middleware, MiddlewareResult, NoInput, NoOutput};
93  pub use serde::{Deserialize, Serialize};
94}
95
96#[cfg(test)]
97mod markers {
98  use super::NoInput;
99
100  /// Pins the direction claimed by `NoInput`'s doc: it binds *from* an empty object. The
101  /// `NoOutput` counterpart is covered by `result::tests::no_output_serializes_to_empty`.
102  #[test]
103  fn no_input_deserializes_from_an_empty_object() {
104    serde_json::from_str::<NoInput>("{}").expect("NoInput binds from an empty object");
105  }
106}
107
108#[cfg(test)]
109mod wit_abi {
110  /// This crate's doc header names the ABI it targets, and that line is the docs.rs front page.
111  /// `moonlit-pdk-macros` carried a stale version across two bumps for want of this check, so
112  /// pin the string to the WIT that actually ships.
113  #[test]
114  fn crate_doc_states_the_shipped_abi_version() {
115    let wit = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("wit/moonlit-plugin.wit");
116    let text = std::fs::read_to_string(wit).unwrap();
117    let abi = text
118      .lines()
119      .find_map(|l| l.trim().strip_prefix("package "))
120      .expect("WIT declares a package")
121      .trim()
122      .trim_end_matches(';');
123
124    let doc: String = include_str!("lib.rs")
125      .lines()
126      .take_while(|l| l.starts_with("//!"))
127      .collect::<Vec<_>>()
128      .join("\n");
129
130    assert!(
131      doc.contains(abi),
132      "moonlit-pdk's crate doc must name the shipped ABI `{abi}`. It currently reads:\n{doc}"
133    );
134  }
135}