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}