1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub enum TsType {
12 String,
14 Number,
16 Boolean,
18 Void,
20 Null,
22 Undefined,
24 Any,
26
27 Array(Box<TsType>),
29
30 Object(Vec<(String, TsType)>),
32
33 Union(Vec<TsType>),
35
36 Optional(Box<TsType>),
38
39 Promise(Box<TsType>),
41
42 Tuple(Vec<TsType>),
44
45 Reference(String),
47}
48
49impl TsType {
50 pub fn to_ts_string(&self) -> String {
52 match self {
53 Self::String => "string".to_string(),
54 Self::Number => "number".to_string(),
55 Self::Boolean => "boolean".to_string(),
56 Self::Void => "void".to_string(),
57 Self::Null => "null".to_string(),
58 Self::Undefined => "undefined".to_string(),
59 Self::Any => "any".to_string(),
60 Self::Array(inner) => format!("{}[]", inner.to_ts_string()),
61 Self::Object(fields) => {
62 let fields_str = fields
63 .iter()
64 .map(|(name, ty)| format!("{}: {}", name, ty.to_ts_string()))
65 .collect::<Vec<_>>()
66 .join(", ");
67 format!("{{ {} }}", fields_str)
68 }
69 Self::Union(types) => types
70 .iter()
71 .map(|t| t.to_ts_string())
72 .collect::<Vec<_>>()
73 .join(" | "),
74 Self::Optional(inner) => format!("{} | null", inner.to_ts_string()),
75 Self::Promise(inner) => format!("Promise<{}>", inner.to_ts_string()),
76 Self::Tuple(types) => {
77 let types_str = types
78 .iter()
79 .map(|t| t.to_ts_string())
80 .collect::<Vec<_>>()
81 .join(", ");
82 format!("[{}]", types_str)
83 }
84 Self::Reference(name) => name.clone(),
85 }
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct TsParameter {
92 pub name: String,
94 pub ty: TsType,
96 pub optional: bool,
98 pub default: Option<String>,
100 pub description: Option<String>,
102}
103
104impl TsParameter {
105 pub fn new(name: impl Into<String>, ty: TsType) -> Self {
107 Self {
108 name: name.into(),
109 ty,
110 optional: false,
111 default: None,
112 description: None,
113 }
114 }
115
116 pub fn optional(mut self) -> Self {
118 self.optional = true;
119 self
120 }
121
122 pub fn with_default(mut self, default: impl Into<String>) -> Self {
124 self.default = Some(default.into());
125 self.optional = true;
126 self
127 }
128
129 pub fn with_description(mut self, description: impl Into<String>) -> Self {
131 self.description = Some(description.into());
132 self
133 }
134
135 pub fn to_ts_string(&self) -> String {
137 let optional_marker = if self.optional { "?" } else { "" };
138 let default_str = if let Some(ref default) = self.default {
139 format!(" = {}", default)
140 } else {
141 String::new()
142 };
143
144 format!(
145 "{}{}: {}{}",
146 self.name,
147 optional_marker,
148 self.ty.to_ts_string(),
149 default_str
150 )
151 }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct TsFunction {
157 pub name: String,
159 pub parameters: Vec<TsParameter>,
161 pub return_type: TsType,
163 pub is_async: bool,
165 pub description: Option<String>,
167 pub examples: Vec<String>,
169}
170
171impl TsFunction {
172 pub fn new(name: impl Into<String>, return_type: TsType) -> Self {
174 Self {
175 name: name.into(),
176 parameters: Vec::new(),
177 return_type,
178 is_async: false,
179 description: None,
180 examples: Vec::new(),
181 }
182 }
183
184 pub fn parameter(mut self, param: TsParameter) -> Self {
186 self.parameters.push(param);
187 self
188 }
189
190 pub fn async_fn(mut self) -> Self {
192 self.is_async = true;
193 self
194 }
195
196 pub fn with_description(mut self, description: impl Into<String>) -> Self {
198 self.description = Some(description.into());
199 self
200 }
201
202 pub fn with_example(mut self, example: impl Into<String>) -> Self {
204 self.examples.push(example.into());
205 self
206 }
207
208 pub fn to_ts_declaration(&self) -> String {
210 let params_str = self
211 .parameters
212 .iter()
213 .map(|p| p.to_ts_string())
214 .collect::<Vec<_>>()
215 .join(", ");
216
217 let return_type = if self.is_async {
218 TsType::Promise(Box::new(self.return_type.clone()))
219 } else {
220 self.return_type.clone()
221 };
222
223 format!(
224 "{}({}): {}",
225 self.name,
226 params_str,
227 return_type.to_ts_string()
228 )
229 }
230
231 pub fn to_jsdoc(&self) -> String {
233 let mut lines = vec!["/**".to_string()];
234
235 if let Some(ref desc) = self.description {
236 lines.push(format!(" * {}", desc));
237 if !self.parameters.is_empty() || !self.examples.is_empty() {
238 lines.push(" *".to_string());
239 }
240 }
241
242 for param in &self.parameters {
243 let param_desc = param
244 .description
245 .as_ref()
246 .map(|d| format!(" - {}", d))
247 .unwrap_or_default();
248 lines.push(format!(
249 " * @param {{{}}} {}{}",
250 param.ty.to_ts_string(),
251 param.name,
252 param_desc
253 ));
254 }
255
256 if self.is_async {
257 lines.push(format!(
258 " * @returns {{Promise<{}>}}",
259 self.return_type.to_ts_string()
260 ));
261 } else {
262 lines.push(format!(
263 " * @returns {{{}}}",
264 self.return_type.to_ts_string()
265 ));
266 }
267
268 if !self.examples.is_empty() {
269 lines.push(" *".to_string());
270 for example in &self.examples {
271 lines.push(" * @example".to_string());
272 for line in example.lines() {
273 lines.push(format!(" * {}", line));
274 }
275 }
276 }
277
278 lines.push(" */".to_string());
279 lines.join("\n")
280 }
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub enum TsMember {
286 Constructor(TsFunction),
288 Method(TsFunction),
290 Property {
292 name: String,
293 ty: TsType,
294 readonly: bool,
295 description: Option<String>,
296 },
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct TsClass {
302 pub name: String,
304 pub description: Option<String>,
306 pub members: Vec<TsMember>,
308 pub examples: Vec<String>,
310}
311
312impl TsClass {
313 pub fn new(name: impl Into<String>) -> Self {
315 Self {
316 name: name.into(),
317 description: None,
318 members: Vec::new(),
319 examples: Vec::new(),
320 }
321 }
322
323 pub fn with_description(mut self, description: impl Into<String>) -> Self {
325 self.description = Some(description.into());
326 self
327 }
328
329 pub fn member(mut self, member: TsMember) -> Self {
331 self.members.push(member);
332 self
333 }
334
335 pub fn with_example(mut self, example: impl Into<String>) -> Self {
337 self.examples.push(example.into());
338 self
339 }
340
341 pub fn to_ts_declaration(&self) -> String {
343 let mut lines = Vec::new();
344
345 lines.push("/**".to_string());
347 if let Some(ref desc) = self.description {
348 lines.push(format!(" * {}", desc));
349 }
350 if !self.examples.is_empty() {
351 lines.push(" *".to_string());
352 for example in &self.examples {
353 lines.push(" * @example".to_string());
354 for line in example.lines() {
355 lines.push(format!(" * {}", line));
356 }
357 }
358 }
359 lines.push(" */".to_string());
360
361 lines.push(format!("export class {} {{", self.name));
362
363 for member in &self.members {
364 match member {
365 TsMember::Constructor(func) => {
366 lines.push("".to_string());
367 lines.push(format!(" {}", func.to_jsdoc().replace('\n', "\n ")));
368 lines.push(format!(
369 " constructor({});",
370 func.parameters
371 .iter()
372 .map(|p| p.to_ts_string())
373 .collect::<Vec<_>>()
374 .join(", ")
375 ));
376 }
377 TsMember::Method(func) => {
378 lines.push("".to_string());
379 lines.push(format!(" {}", func.to_jsdoc().replace('\n', "\n ")));
380 lines.push(format!(" {};", func.to_ts_declaration()));
381 }
382 TsMember::Property {
383 name,
384 ty,
385 readonly,
386 description,
387 } => {
388 lines.push("".to_string());
389 if let Some(desc) = description {
390 lines.push(format!(" /** {} */", desc));
391 }
392 let readonly_str = if *readonly { "readonly " } else { "" };
393 lines.push(format!(
394 " {}{}: {};",
395 readonly_str,
396 name,
397 ty.to_ts_string()
398 ));
399 }
400 }
401 }
402
403 lines.push("}".to_string());
404 lines.join("\n")
405 }
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct TsInterface {
411 pub name: String,
413 pub description: Option<String>,
415 pub fields: Vec<(String, TsType, Option<String>)>, }
418
419impl TsInterface {
420 pub fn new(name: impl Into<String>) -> Self {
422 Self {
423 name: name.into(),
424 description: None,
425 fields: Vec::new(),
426 }
427 }
428
429 pub fn with_description(mut self, description: impl Into<String>) -> Self {
431 self.description = Some(description.into());
432 self
433 }
434
435 pub fn field(mut self, name: impl Into<String>, ty: TsType) -> Self {
437 self.fields.push((name.into(), ty, None));
438 self
439 }
440
441 pub fn field_with_description(
443 mut self,
444 name: impl Into<String>,
445 ty: TsType,
446 description: impl Into<String>,
447 ) -> Self {
448 self.fields
449 .push((name.into(), ty, Some(description.into())));
450 self
451 }
452
453 pub fn to_ts_declaration(&self) -> String {
455 let mut lines = Vec::new();
456
457 lines.push("/**".to_string());
458 if let Some(ref desc) = self.description {
459 lines.push(format!(" * {}", desc));
460 }
461 lines.push(" */".to_string());
462
463 lines.push(format!("export interface {} {{", self.name));
464
465 for (name, ty, desc) in &self.fields {
466 if let Some(description) = desc {
467 lines.push(format!(" /** {} */", description));
468 }
469 lines.push(format!(" {}: {};", name, ty.to_ts_string()));
470 }
471
472 lines.push("}".to_string());
473 lines.join("\n")
474 }
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct TsTypeAlias {
480 pub name: String,
482 pub ty: TsType,
484 pub description: Option<String>,
486}
487
488impl TsTypeAlias {
489 pub fn new(name: impl Into<String>, ty: TsType) -> Self {
491 Self {
492 name: name.into(),
493 ty,
494 description: None,
495 }
496 }
497
498 pub fn with_description(mut self, description: impl Into<String>) -> Self {
500 self.description = Some(description.into());
501 self
502 }
503
504 pub fn to_ts_declaration(&self) -> String {
506 let mut lines = Vec::new();
507
508 if let Some(ref desc) = self.description {
509 lines.push("/**".to_string());
510 lines.push(format!(" * {}", desc));
511 lines.push(" */".to_string());
512 }
513
514 lines.push(format!(
515 "export type {} = {};",
516 self.name,
517 self.ty.to_ts_string()
518 ));
519 lines.join("\n")
520 }
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct TsModule {
526 pub name: String,
528 pub description: Option<String>,
530 pub classes: Vec<TsClass>,
532 pub interfaces: Vec<TsInterface>,
534 pub type_aliases: Vec<TsTypeAlias>,
536 pub functions: Vec<TsFunction>,
538}
539
540impl TsModule {
541 pub fn new(name: impl Into<String>) -> Self {
543 Self {
544 name: name.into(),
545 description: None,
546 classes: Vec::new(),
547 interfaces: Vec::new(),
548 type_aliases: Vec::new(),
549 functions: Vec::new(),
550 }
551 }
552
553 pub fn with_description(mut self, description: impl Into<String>) -> Self {
555 self.description = Some(description.into());
556 self
557 }
558
559 pub fn class(mut self, class: TsClass) -> Self {
561 self.classes.push(class);
562 self
563 }
564
565 pub fn interface(mut self, interface: TsInterface) -> Self {
567 self.interfaces.push(interface);
568 self
569 }
570
571 pub fn type_alias(mut self, alias: TsTypeAlias) -> Self {
573 self.type_aliases.push(alias);
574 self
575 }
576
577 pub fn function(mut self, function: TsFunction) -> Self {
579 self.functions.push(function);
580 self
581 }
582
583 pub fn to_ts_declarations(&self) -> String {
585 let mut lines = Vec::new();
586
587 lines.push("/**".to_string());
589 lines.push(format!(" * @module {}", self.name));
590 if let Some(ref desc) = self.description {
591 lines.push(" *".to_string());
592 lines.push(format!(" * {}", desc));
593 }
594 lines.push(" */".to_string());
595 lines.push("".to_string());
596
597 for alias in &self.type_aliases {
599 lines.push(alias.to_ts_declaration());
600 lines.push("".to_string());
601 }
602
603 for interface in &self.interfaces {
605 lines.push(interface.to_ts_declaration());
606 lines.push("".to_string());
607 }
608
609 for class in &self.classes {
611 lines.push(class.to_ts_declaration());
612 lines.push("".to_string());
613 }
614
615 for function in &self.functions {
617 lines.push(function.to_jsdoc());
618 lines.push(format!(
619 "export function {}();",
620 function.to_ts_declaration()
621 ));
622 lines.push("".to_string());
623 }
624
625 lines.join("\n")
626 }
627}
628
629pub struct DocGenerator {
631 modules: HashMap<String, TsModule>,
633}
634
635impl DocGenerator {
636 pub fn new() -> Self {
638 Self {
639 modules: HashMap::new(),
640 }
641 }
642
643 pub fn add_module(&mut self, module: TsModule) {
645 self.modules.insert(module.name.clone(), module);
646 }
647
648 pub fn generate_all(&self) -> HashMap<String, String> {
650 self.modules
651 .iter()
652 .map(|(name, module)| (name.clone(), module.to_ts_declarations()))
653 .collect()
654 }
655
656 pub fn generate_combined(&self) -> String {
658 let mut output = Vec::new();
659
660 output.push("// Auto-generated TypeScript declarations for oxigdal-wasm".to_string());
661 output.push("// Do not edit manually".to_string());
662 output.push("".to_string());
663
664 for module in self.modules.values() {
665 output.push(module.to_ts_declarations());
666 output.push("".to_string());
667 }
668
669 output.join("\n")
670 }
671}
672
673impl Default for DocGenerator {
674 fn default() -> Self {
675 Self::new()
676 }
677}
678
679pub fn create_oxigdal_wasm_docs() -> DocGenerator {
681 let mut generator = DocGenerator::new();
682
683 let mut main_module = TsModule::new("oxigdal-wasm")
685 .with_description("WebAssembly bindings for OxiGDAL - Browser-based geospatial processing");
686
687 let cog_viewer = TsClass::new("WasmCogViewer")
689 .with_description("Cloud Optimized GeoTIFF viewer for the browser")
690 .member(TsMember::Constructor(
691 TsFunction::new("constructor", TsType::Void)
692 .with_description("Creates a new COG viewer instance"),
693 ))
694 .member(TsMember::Method(
695 TsFunction::new("open", TsType::Promise(Box::new(TsType::Void)))
696 .async_fn()
697 .parameter(TsParameter::new("url", TsType::String).with_description("URL to the COG file"))
698 .with_description("Opens a COG file from a URL")
699 .with_example("const viewer = new WasmCogViewer();\nawait viewer.open('https://example.com/image.tif');"),
700 ))
701 .member(TsMember::Method(
702 TsFunction::new("width", TsType::Number)
703 .with_description("Returns the image width in pixels"),
704 ))
705 .member(TsMember::Method(
706 TsFunction::new("height", TsType::Number)
707 .with_description("Returns the image height in pixels"),
708 ))
709 .with_example("const viewer = new WasmCogViewer();\nawait viewer.open('image.tif');\nconsole.log(`Size: ${viewer.width()}x${viewer.height()}`);");
710
711 main_module = main_module.class(cog_viewer);
712
713 let tile_coord = TsInterface::new("TileCoord")
715 .with_description("Tile coordinate in a pyramid")
716 .field_with_description("level", TsType::Number, "Zoom level")
717 .field_with_description("x", TsType::Number, "Column index")
718 .field_with_description("y", TsType::Number, "Row index");
719
720 main_module = main_module.interface(tile_coord);
721
722 generator.add_module(main_module);
723 generator
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729
730 #[test]
731 fn test_ts_type_string() {
732 assert_eq!(TsType::String.to_ts_string(), "string");
733 assert_eq!(TsType::Number.to_ts_string(), "number");
734 assert_eq!(
735 TsType::Array(Box::new(TsType::String)).to_ts_string(),
736 "string[]"
737 );
738 assert_eq!(
739 TsType::Promise(Box::new(TsType::Number)).to_ts_string(),
740 "Promise<number>"
741 );
742 }
743
744 #[test]
745 fn test_ts_parameter() {
746 let param = TsParameter::new("name", TsType::String);
747 assert_eq!(param.to_ts_string(), "name: string");
748
749 let optional_param = TsParameter::new("value", TsType::Number).optional();
750 assert_eq!(optional_param.to_ts_string(), "value?: number");
751 }
752
753 #[test]
754 fn test_ts_function() {
755 let func = TsFunction::new("greet", TsType::String)
756 .parameter(TsParameter::new("name", TsType::String))
757 .with_description("Greets a person");
758
759 let declaration = func.to_ts_declaration();
760 assert!(declaration.contains("greet"));
761 assert!(declaration.contains("name: string"));
762 assert!(declaration.contains("string"));
763 }
764
765 #[test]
766 fn test_ts_class() {
767 let class = TsClass::new("MyClass")
768 .with_description("A test class")
769 .member(TsMember::Method(
770 TsFunction::new("method", TsType::Void).with_description("A test method"),
771 ));
772
773 let declaration = class.to_ts_declaration();
774 assert!(declaration.contains("export class MyClass"));
775 assert!(declaration.contains("method"));
776 }
777
778 #[test]
779 fn test_ts_interface() {
780 let interface = TsInterface::new("MyInterface")
781 .with_description("A test interface")
782 .field("name", TsType::String)
783 .field("age", TsType::Number);
784
785 let declaration = interface.to_ts_declaration();
786 assert!(declaration.contains("export interface MyInterface"));
787 assert!(declaration.contains("name: string"));
788 assert!(declaration.contains("age: number"));
789 }
790
791 #[test]
792 fn test_doc_generator() {
793 let generator = create_oxigdal_wasm_docs();
794 let combined = generator.generate_combined();
795
796 assert!(combined.contains("WasmCogViewer"));
797 assert!(combined.contains("TileCoord"));
798 }
799}