Skip to main content

typewriter_plugin/
lib.rs

1//! # typewriter-plugin
2//!
3//! Plugin API for the typewriter type sync SDK.
4//!
5//! This crate defines the contract that external language emitter plugins must implement.
6//! Plugin authors depend on this crate, implement the [`EmitterPlugin`] trait, and use
7//! the [`declare_plugin!`] macro to expose C ABI entry points for dynamic loading.
8//!
9//! ## Writing a Plugin
10//!
11//! ```rust,ignore
12//! use typewriter_plugin::prelude::*;
13//!
14//! struct MyMapper;
15//!
16//! impl TypeMapper for MyMapper {
17//!     fn map_primitive(&self, ty: &PrimitiveType) -> String { todo!() }
18//!     fn map_option(&self, inner: &TypeKind) -> String { todo!() }
19//!     fn map_vec(&self, inner: &TypeKind) -> String { todo!() }
20//!     fn map_hashmap(&self, key: &TypeKind, value: &TypeKind) -> String { todo!() }
21//!     fn map_tuple(&self, elements: &[TypeKind]) -> String { todo!() }
22//!     fn map_named(&self, name: &str) -> String { todo!() }
23//!     fn emit_struct(&self, def: &StructDef) -> String { todo!() }
24//!     fn emit_enum(&self, def: &EnumDef) -> String { todo!() }
25//!     fn file_header(&self, type_name: &str) -> String { todo!() }
26//!     fn file_extension(&self) -> &str { todo!() }
27//!     fn file_naming(&self, type_name: &str) -> String { todo!() }
28//! }
29//!
30//! struct MyPlugin;
31//!
32//! impl EmitterPlugin for MyPlugin {
33//!     fn language_id(&self) -> &str { "mylang" }
34//!     fn language_name(&self) -> &str { "My Language" }
35//!     fn version(&self) -> &str { "0.1.0" }
36//!     fn default_output_dir(&self) -> &str { "./generated/mylang" }
37//!     fn mapper(&self, _config: &PluginConfig) -> Box<dyn TypeMapper> {
38//!         Box::new(MyMapper)
39//!     }
40//! }
41//!
42//! declare_plugin!(MyPlugin);
43//! ```
44
45use serde::Deserialize;
46
47// Re-export everything plugin authors need
48pub use typewriter_core::ir::*;
49pub use typewriter_core::mapper::TypeMapper;
50pub use typewriter_core::naming::{FileStyle, to_file_style};
51
52/// Current plugin API version.
53///
54/// Plugins built against a different API version will be rejected at load time.
55/// This is bumped whenever the `EmitterPlugin` or `TypeMapper` trait changes
56/// in a backward-incompatible way.
57pub const PLUGIN_API_VERSION: u32 = 1;
58
59/// Configuration data passed to a plugin from `typewriter.toml`.
60///
61/// This contains the plugin-specific TOML section (e.g. `[ruby]`) parsed into
62/// a generic structure. Each plugin can define its own config keys.
63#[derive(Debug, Clone, Default, Deserialize)]
64pub struct PluginConfig {
65    /// Output directory override from config
66    pub output_dir: Option<String>,
67    /// File naming style override from config
68    pub file_style: Option<String>,
69    /// All extra key-value pairs from the plugin's TOML section
70    #[serde(flatten)]
71    pub extra: toml::Table,
72}
73
74/// The core trait that every plugin must implement.
75///
76/// This defines the contract between typewriter and external language emitter plugins.
77/// Each plugin provides metadata about the language it targets and a factory method
78/// for creating the actual `TypeMapper` implementation.
79pub trait EmitterPlugin: Send + Sync {
80    /// Unique identifier for this language (e.g. `"ruby"`, `"php"`, `"dart"`).
81    ///
82    /// This is used in `#[sync_to(ruby)]` and as the TOML section key `[ruby]`.
83    fn language_id(&self) -> &str;
84
85    /// Human-readable display name (e.g. `"Ruby"`, `"PHP"`, `"Dart/Flutter"`).
86    fn language_name(&self) -> &str;
87
88    /// Plugin version as a semver string (e.g. `"0.1.0"`).
89    fn version(&self) -> &str;
90
91    /// Default output directory when not configured in `typewriter.toml`.
92    fn default_output_dir(&self) -> &str;
93
94    /// Create a `TypeMapper` instance for this language.
95    ///
96    /// The returned mapper is used to render type definitions.
97    /// The `config` parameter contains plugin-specific settings from `typewriter.toml`.
98    fn mapper(&self, config: &PluginConfig) -> Box<dyn TypeMapper>;
99
100    /// The TOML section key for this plugin's configuration.
101    ///
102    /// Defaults to `language_id()`. Override if you want a different key.
103    fn config_key(&self) -> &str {
104        self.language_id()
105    }
106
107    /// File extension for generated files (without leading dot).
108    ///
109    /// Used by drift detection and reporting. Defaults to asking the mapper,
110    /// but can be overridden for cases where the mapper isn't available yet.
111    fn file_extension(&self) -> &str;
112
113    /// Plugin API version this plugin was built against.
114    ///
115    /// Do not override — this is checked at load time for compatibility.
116    fn api_version(&self) -> u32 {
117        PLUGIN_API_VERSION
118    }
119}
120
121/// Declare a plugin's C ABI entry points for dynamic loading.
122///
123/// This macro generates the `extern "C"` functions that `typewriter-engine`
124/// calls when loading a plugin from a shared library (`.so`/`.dylib`/`.dll`).
125///
126/// # Usage
127///
128/// ```rust,ignore
129/// declare_plugin!(MyPlugin);
130/// ```
131///
132/// This expands to:
133/// - `_tw_plugin_create()` → returns a raw pointer to a boxed `EmitterPlugin`
134/// - `_tw_plugin_api_version()` → returns the API version constant
135#[macro_export]
136macro_rules! declare_plugin {
137    ($plugin_type:ty) => {
138        #[unsafe(no_mangle)]
139        pub extern "C" fn _tw_plugin_create() -> *mut dyn $crate::EmitterPlugin {
140            let plugin: Box<dyn $crate::EmitterPlugin> = Box::new(<$plugin_type>::new());
141            Box::into_raw(plugin)
142        }
143
144        #[unsafe(no_mangle)]
145        pub extern "C" fn _tw_plugin_api_version() -> u32 {
146            $crate::PLUGIN_API_VERSION
147        }
148    };
149}
150
151/// Convenience prelude for plugin authors.
152///
153/// ```rust,ignore
154/// use typewriter_plugin::prelude::*;
155/// ```
156pub mod prelude {
157    pub use super::{
158        EmitterPlugin, PLUGIN_API_VERSION, PluginConfig,
159    };
160    pub use typewriter_core::ir::*;
161    pub use typewriter_core::mapper::TypeMapper;
162    pub use typewriter_core::naming::{FileStyle, to_file_style};
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_plugin_api_version() {
171        assert_eq!(PLUGIN_API_VERSION, 1);
172    }
173
174    #[test]
175    fn test_plugin_config_default() {
176        let config = PluginConfig::default();
177        assert!(config.output_dir.is_none());
178        assert!(config.file_style.is_none());
179        assert!(config.extra.is_empty());
180    }
181
182    #[test]
183    fn test_plugin_config_deserialization() {
184        let toml_str = r#"
185output_dir = "./generated/ruby"
186file_style = "snake_case"
187gem_version = "3.2"
188"#;
189        let config: PluginConfig = toml::from_str(toml_str).unwrap();
190        assert_eq!(config.output_dir.as_deref(), Some("./generated/ruby"));
191        assert_eq!(config.file_style.as_deref(), Some("snake_case"));
192        assert_eq!(
193            config.extra.get("gem_version").and_then(|v| v.as_str()),
194            Some("3.2")
195        );
196    }
197}