Skip to main content

oxigdal_wasm/
bindings.rs

1//! TypeScript bindings and documentation generation
2//!
3//! This module provides utilities for generating TypeScript type definitions,
4//! API documentation, and example code for the WASM bindings.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// TypeScript type representation
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub enum TsType {
12    /// String primitive type
13    String,
14    /// Number primitive type
15    Number,
16    /// Boolean primitive type
17    Boolean,
18    /// Void type (function returns nothing)
19    Void,
20    /// Null type
21    Null,
22    /// Undefined type
23    Undefined,
24    /// Any type (accepts anything)
25    Any,
26
27    /// Array type
28    Array(Box<TsType>),
29
30    /// Object type with fields
31    Object(Vec<(String, TsType)>),
32
33    /// Union type
34    Union(Vec<TsType>),
35
36    /// Optional type
37    Optional(Box<TsType>),
38
39    /// Promise type
40    Promise(Box<TsType>),
41
42    /// Tuple type
43    Tuple(Vec<TsType>),
44
45    /// Custom type reference
46    Reference(String),
47}
48
49impl TsType {
50    /// Converts to TypeScript string representation
51    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/// Function parameter
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct TsParameter {
92    /// Parameter name
93    pub name: String,
94    /// Parameter type
95    pub ty: TsType,
96    /// Is optional
97    pub optional: bool,
98    /// Default value (as string)
99    pub default: Option<String>,
100    /// Description
101    pub description: Option<String>,
102}
103
104impl TsParameter {
105    /// Creates a new parameter
106    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    /// Makes the parameter optional
117    pub fn optional(mut self) -> Self {
118        self.optional = true;
119        self
120    }
121
122    /// Sets a default value
123    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    /// Sets a description
130    pub fn with_description(mut self, description: impl Into<String>) -> Self {
131        self.description = Some(description.into());
132        self
133    }
134
135    /// Converts to TypeScript string
136    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/// Function signature
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct TsFunction {
157    /// Function name
158    pub name: String,
159    /// Parameters
160    pub parameters: Vec<TsParameter>,
161    /// Return type
162    pub return_type: TsType,
163    /// Is async
164    pub is_async: bool,
165    /// Description
166    pub description: Option<String>,
167    /// Example code
168    pub examples: Vec<String>,
169}
170
171impl TsFunction {
172    /// Creates a new function
173    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    /// Adds a parameter
185    pub fn parameter(mut self, param: TsParameter) -> Self {
186        self.parameters.push(param);
187        self
188    }
189
190    /// Marks as async
191    pub fn async_fn(mut self) -> Self {
192        self.is_async = true;
193        self
194    }
195
196    /// Sets a description
197    pub fn with_description(mut self, description: impl Into<String>) -> Self {
198        self.description = Some(description.into());
199        self
200    }
201
202    /// Adds an example
203    pub fn with_example(mut self, example: impl Into<String>) -> Self {
204        self.examples.push(example.into());
205        self
206    }
207
208    /// Generates TypeScript declaration
209    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    /// Generates JSDoc comment
232    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/// Class member
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub enum TsMember {
286    /// Constructor
287    Constructor(TsFunction),
288    /// Method
289    Method(TsFunction),
290    /// Property
291    Property {
292        name: String,
293        ty: TsType,
294        readonly: bool,
295        description: Option<String>,
296    },
297}
298
299/// Class definition
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct TsClass {
302    /// Class name
303    pub name: String,
304    /// Description
305    pub description: Option<String>,
306    /// Members
307    pub members: Vec<TsMember>,
308    /// Examples
309    pub examples: Vec<String>,
310}
311
312impl TsClass {
313    /// Creates a new class
314    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    /// Sets a description
324    pub fn with_description(mut self, description: impl Into<String>) -> Self {
325        self.description = Some(description.into());
326        self
327    }
328
329    /// Adds a member
330    pub fn member(mut self, member: TsMember) -> Self {
331        self.members.push(member);
332        self
333    }
334
335    /// Adds an example
336    pub fn with_example(mut self, example: impl Into<String>) -> Self {
337        self.examples.push(example.into());
338        self
339    }
340
341    /// Generates TypeScript declaration
342    pub fn to_ts_declaration(&self) -> String {
343        let mut lines = Vec::new();
344
345        // Class JSDoc
346        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/// Interface definition
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct TsInterface {
411    /// Interface name
412    pub name: String,
413    /// Description
414    pub description: Option<String>,
415    /// Fields
416    pub fields: Vec<(String, TsType, Option<String>)>, // (name, type, description)
417}
418
419impl TsInterface {
420    /// Creates a new interface
421    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    /// Sets a description
430    pub fn with_description(mut self, description: impl Into<String>) -> Self {
431        self.description = Some(description.into());
432        self
433    }
434
435    /// Adds a field
436    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    /// Adds a field with description
442    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    /// Generates TypeScript declaration
454    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/// Type alias definition
478#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct TsTypeAlias {
480    /// Alias name
481    pub name: String,
482    /// Target type
483    pub ty: TsType,
484    /// Description
485    pub description: Option<String>,
486}
487
488impl TsTypeAlias {
489    /// Creates a new type alias
490    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    /// Sets a description
499    pub fn with_description(mut self, description: impl Into<String>) -> Self {
500        self.description = Some(description.into());
501        self
502    }
503
504    /// Generates TypeScript declaration
505    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/// Module definition
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct TsModule {
526    /// Module name
527    pub name: String,
528    /// Description
529    pub description: Option<String>,
530    /// Classes
531    pub classes: Vec<TsClass>,
532    /// Interfaces
533    pub interfaces: Vec<TsInterface>,
534    /// Type aliases
535    pub type_aliases: Vec<TsTypeAlias>,
536    /// Functions
537    pub functions: Vec<TsFunction>,
538}
539
540impl TsModule {
541    /// Creates a new module
542    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    /// Sets a description
554    pub fn with_description(mut self, description: impl Into<String>) -> Self {
555        self.description = Some(description.into());
556        self
557    }
558
559    /// Adds a class
560    pub fn class(mut self, class: TsClass) -> Self {
561        self.classes.push(class);
562        self
563    }
564
565    /// Adds an interface
566    pub fn interface(mut self, interface: TsInterface) -> Self {
567        self.interfaces.push(interface);
568        self
569    }
570
571    /// Adds a type alias
572    pub fn type_alias(mut self, alias: TsTypeAlias) -> Self {
573        self.type_aliases.push(alias);
574        self
575    }
576
577    /// Adds a function
578    pub fn function(mut self, function: TsFunction) -> Self {
579        self.functions.push(function);
580        self
581    }
582
583    /// Generates TypeScript declarations
584    pub fn to_ts_declarations(&self) -> String {
585        let mut lines = Vec::new();
586
587        // Module header
588        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        // Type aliases
598        for alias in &self.type_aliases {
599            lines.push(alias.to_ts_declaration());
600            lines.push("".to_string());
601        }
602
603        // Interfaces
604        for interface in &self.interfaces {
605            lines.push(interface.to_ts_declaration());
606            lines.push("".to_string());
607        }
608
609        // Classes
610        for class in &self.classes {
611            lines.push(class.to_ts_declaration());
612            lines.push("".to_string());
613        }
614
615        // Functions
616        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
629/// Documentation generator
630pub struct DocGenerator {
631    /// Modules
632    modules: HashMap<String, TsModule>,
633}
634
635impl DocGenerator {
636    /// Creates a new documentation generator
637    pub fn new() -> Self {
638        Self {
639            modules: HashMap::new(),
640        }
641    }
642
643    /// Adds a module
644    pub fn add_module(&mut self, module: TsModule) {
645        self.modules.insert(module.name.clone(), module);
646    }
647
648    /// Generates all declarations
649    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    /// Generates a combined .d.ts file
657    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
679/// Creates the standard OxiGDAL WASM bindings documentation
680pub fn create_oxigdal_wasm_docs() -> DocGenerator {
681    let mut generator = DocGenerator::new();
682
683    // Create main module
684    let mut main_module = TsModule::new("oxigdal-wasm")
685        .with_description("WebAssembly bindings for OxiGDAL - Browser-based geospatial processing");
686
687    // Add WasmCogViewer class
688    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    // Add TileCoord interface
714    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}