typeshare_java/
language.rs

1use convert_case::{Case, Casing as _};
2use itertools::Itertools as _;
3use typeshare_model::Language;
4use typeshare_model::prelude::*;
5
6use crate::config::HeaderComment;
7use crate::config::JavaConfig;
8use crate::error::FormatSpecialTypeError;
9
10#[derive(Debug)]
11pub struct Java {
12    config: JavaConfig,
13}
14
15impl<'config> Language<'config> for Java {
16    type Config = JavaConfig;
17
18    const NAME: &'static str = "java";
19
20    fn new_from_config(config: Self::Config) -> anyhow::Result<Self> {
21        Ok(Self { config })
22    }
23
24    fn output_filename_for_crate(&self, crate_name: &CrateName) -> String {
25        crate_name.as_str().to_case(Case::Pascal) + ".java"
26    }
27
28    fn format_special_type(
29        &self,
30        special_ty: &SpecialRustType,
31        generic_context: &[TypeName],
32    ) -> anyhow::Result<String> {
33        Ok(match special_ty {
34            SpecialRustType::Vec(rtype) => {
35                format!(
36                    "java.util.ArrayList<{}>",
37                    self.format_type(rtype, generic_context)?
38                )
39            }
40            SpecialRustType::Array(rtype, _) => {
41                format!("{}[]", self.format_type(rtype, generic_context)?)
42            }
43            SpecialRustType::Slice(rtype) => {
44                format!("{}[]", self.format_type(rtype, generic_context)?)
45            }
46            SpecialRustType::HashMap(rtype1, rtype2) => {
47                format!(
48                    "java.util.HashMap<{}, {}>",
49                    self.format_type(rtype1, generic_context)?,
50                    self.format_type(rtype2, generic_context)?
51                )
52            }
53            SpecialRustType::Option(rtype) => self.format_type(rtype, generic_context)?,
54            SpecialRustType::Unit => "Void".into(),
55            // https://docs.oracle.com/javase/specs/jls/se23/html/jls-4.html#jls-IntegralType
56            // Char in Java is 16 bits long, so we need to use String
57            SpecialRustType::String | SpecialRustType::Char => "String".into(),
58            SpecialRustType::I8 => "byte".into(),
59            SpecialRustType::I16 => "short".into(),
60            SpecialRustType::ISize | SpecialRustType::I32 => "int".into(),
61            SpecialRustType::I54 | SpecialRustType::I64 => "long".into(),
62            // byte in Java is signed, so we need to use short to represent all possible values
63            SpecialRustType::U8 => "short".into(),
64            // short in Java is signed, so we need to use int to represent all possible values
65            SpecialRustType::U16 => "int".into(),
66            // ing in Java is signed, so we need to use long to represent all possible values
67            SpecialRustType::USize | SpecialRustType::U32 => "long".into(),
68            // long in Java is signed, so we need to use BigInteger to represent all possible values
69            SpecialRustType::U53 | SpecialRustType::U64 => "java.math.BigInteger".into(),
70            // https://docs.oracle.com/javase/specs/jls/se23/html/jls-4.html#jls-PrimitiveType
71            SpecialRustType::Bool => "boolean".into(),
72            // https://docs.oracle.com/javase/specs/jls/se23/html/jls-4.html#jls-FloatingPointType
73            SpecialRustType::F32 => "float".into(),
74            SpecialRustType::F64 => "double".into(),
75            _ => {
76                return Err(
77                    FormatSpecialTypeError::UnsupportedSpecialType(special_ty.clone()).into(),
78                );
79            }
80        })
81    }
82
83    fn begin_file(
84        &self,
85        w: &mut impl std::io::Write,
86        mode: FilesMode<&CrateName>,
87    ) -> anyhow::Result<()> {
88        match &self.config.header_comment {
89            HeaderComment::None => {}
90            HeaderComment::Default => {
91                writeln!(w, "/**")?;
92                writeln!(
93                    w,
94                    " * Generated by typeshare-java {}",
95                    env!("CARGO_PKG_VERSION")
96                )?;
97                writeln!(w, " */")?;
98                writeln!(w)?;
99            }
100            HeaderComment::Custom { comment } => {
101                writeln!(w, "/**")?;
102                for comment in comment.split("\n") {
103                    writeln!(w, " * {}", comment)?;
104                }
105                writeln!(w, " */")?;
106                writeln!(w)?;
107            }
108        }
109
110        if let Some(package) = &self.config.package {
111            if let FilesMode::Multi(crate_name) = mode {
112                writeln!(w, "package {}.{};", package, crate_name.as_str())?;
113            } else {
114                writeln!(w, "package {};", package)?;
115            }
116            writeln!(w)?;
117        }
118
119        Ok(())
120    }
121
122    fn write_imports<'a, Crates, Types>(
123        &self,
124        writer: &mut impl std::io::Write,
125        _crate_name: &CrateName,
126        imports: Crates,
127    ) -> anyhow::Result<()>
128    where
129        Crates: IntoIterator<Item = (&'a CrateName, Types)>,
130        Types: IntoIterator<Item = &'a TypeName>,
131    {
132        for (path, ty) in imports {
133            for t in ty {
134                writeln!(
135                    writer,
136                    "import {}.{path}.{t};",
137                    self.config
138                        .package
139                        .as_ref()
140                        .map(|package| format!("{package}."))
141                        .unwrap_or_default()
142                )?;
143            }
144        }
145        writeln!(writer).map_err(|err| err.into())
146    }
147
148    fn write_type_alias(
149        &self,
150        _w: &mut impl std::io::Write,
151        _t: &RustTypeAlias,
152    ) -> anyhow::Result<()> {
153        todo!("type aliases are not supported yet")
154    }
155
156    fn write_struct(&self, w: &mut impl std::io::Write, rs: &RustStruct) -> anyhow::Result<()> {
157        self.write_comments(w, 0, &rs.comments)?;
158
159        write!(
160            w,
161            "public record {}{}{}(",
162            self.config.prefix.as_ref().unwrap_or(&String::default()),
163            rs.id.renamed,
164            (!rs.generic_types.is_empty())
165                .then(|| format!("<{}>", rs.generic_types.join(", ")))
166                .unwrap_or_default()
167        )?;
168
169        if let Some((last, elements)) = rs.fields.split_last() {
170            writeln!(w)?;
171            for f in elements.iter() {
172                self.write_element(w, f, rs.generic_types.as_slice())?;
173                writeln!(w, ",")?;
174            }
175            self.write_element(w, last, rs.generic_types.as_slice())?;
176            writeln!(w)?;
177        }
178
179        writeln!(w, r") {{}}")?;
180
181        writeln!(w)?;
182
183        Ok(())
184    }
185
186    fn write_enum(&self, w: &mut impl std::io::Write, e: &RustEnum) -> anyhow::Result<()> {
187        // TODO: Generate named types for any anonymous struct variants of this enum
188
189        self.write_comments(w, 0, &e.shared().comments)?;
190
191        match e {
192            RustEnum::Unit {
193                shared,
194                unit_variants,
195            } => self.write_unit_enum(w, shared, unit_variants),
196            RustEnum::Algebraic { .. } => todo!("algebraic enums are not supported yet"),
197        }
198    }
199
200    fn write_const(&self, _w: &mut impl std::io::Write, _c: &RustConst) -> anyhow::Result<()> {
201        todo!("constants are not supported yet")
202    }
203}
204
205impl Java {
206    #[inline]
207    fn is_java_letter(&self, c: char) -> bool {
208        // https://docs.oracle.com/javase/specs/jls/se23/html/jls-3.html#jls-JavaLetter
209        c.is_ascii_alphabetic() || c == '_' || c == '$'
210    }
211
212    #[inline]
213    fn is_java_letter_or_number(&self, c: char) -> bool {
214        // https://docs.oracle.com/javase/specs/jls/se23/html/jls-3.html#jls-JavaLetterOrDigit
215        self.is_java_letter(c) || c.is_ascii_digit()
216    }
217
218    #[inline]
219    fn is_java_reserved_keyword(&self, name: &str) -> bool {
220        // https://docs.oracle.com/javase/specs/jls/se23/html/jls-3.html#jls-ReservedKeyword
221        matches!(
222            name,
223            "abstract"
224                | "continue"
225                | "for"
226                | "new"
227                | "switch"
228                | "assert"
229                | "default"
230                | "if"
231                | "package"
232                | "synchronized"
233                | "boolean"
234                | "do"
235                | "goto"
236                | "private"
237                | "this"
238                | "break"
239                | "double"
240                | "implements"
241                | "protected"
242                | "throw"
243                | "byte"
244                | "else"
245                | "import"
246                | "public"
247                | "throws"
248                | "case"
249                | "enum"
250                | "instanceof"
251                | "return"
252                | "transient"
253                | "catch"
254                | "extends"
255                | "int"
256                | "short"
257                | "try"
258                | "char"
259                | "final"
260                | "interface"
261                | "static"
262                | "void"
263                | "class"
264                | "finally"
265                | "long"
266                | "strictfp"
267                | "volatile"
268                | "const"
269                | "float"
270                | "native"
271                | "super"
272                | "while"
273                | "_"
274        )
275    }
276
277    #[inline]
278    fn is_java_boolean_literal(&self, name: &str) -> bool {
279        // https://docs.oracle.com/javase/specs/jls/se23/html/jls-3.html#jls-BooleanLiteral
280        matches!(name, "true" | "false")
281    }
282
283    #[inline]
284    fn is_java_null_literal(&self, name: &str) -> bool {
285        // https://docs.oracle.com/javase/specs/jls/se23/html/jls-3.html#jls-NullLiteral
286        matches!(name, "null")
287    }
288
289    fn santitize_itentifier(&self, name: &str) -> String {
290        // https://docs.oracle.com/javase/specs/jls/se23/html/jls-3.html#jls-Identifier
291        let mut chars = name.chars();
292
293        // Ensure the first character is valid "JavaLetter"
294        let first_char = chars
295            .next()
296            .map(|c| if self.is_java_letter(c) { c } else { '_' });
297
298        // Ensure each remaining characters is a valid "JavaLetterOrDigit"
299        let rest: String = chars
300            .filter_map(|c| match c {
301                '-' => Some('_'),
302                c if self.is_java_letter_or_number(c) => Some(c),
303                _ => None,
304            })
305            .collect();
306
307        // Combine and return the sanitized identifier
308        let name: String = first_char.into_iter().chain(rest.chars()).collect();
309
310        if self.is_java_reserved_keyword(&name)
311            || self.is_java_boolean_literal(&name)
312            || self.is_java_null_literal(&name)
313        {
314            format!("_{name}")
315        } else {
316            name
317        }
318    }
319
320    fn write_element(
321        &self,
322        w: &mut impl std::io::Write,
323        f: &RustField,
324        generic_types: &[TypeName],
325    ) -> anyhow::Result<()> {
326        self.write_comments(w, 1, &f.comments)?;
327        let ty = self.format_type(&f.ty, generic_types)?;
328        write!(
329            w,
330            "\t{} {}",
331            ty,
332            self.santitize_itentifier(&f.id.renamed.as_str()),
333        )
334        .map_err(|err| err.into())
335    }
336
337    fn write_unit_enum(
338        &self,
339        w: &mut impl std::io::Write,
340        shared: &RustEnumShared,
341        unit_variants: &[RustEnumVariantShared],
342    ) -> anyhow::Result<()> {
343        writeln!(
344            w,
345            "public enum {}{} {{",
346            self.config.prefix.as_ref().unwrap_or(&String::default()),
347            &shared.id.renamed
348        )?;
349
350        if let Some((last_variant, variants)) = unit_variants.split_last() {
351            for variant in variants {
352                self.write_comments(w, 1, &variant.comments)?;
353                writeln!(
354                    w,
355                    "\t{},",
356                    self.santitize_itentifier(&variant.id.renamed.as_str()),
357                )?;
358            }
359            self.write_comments(w, 1, &last_variant.comments)?;
360            writeln!(
361                w,
362                "\t{}",
363                self.santitize_itentifier(&last_variant.id.renamed.as_str()),
364            )?;
365        }
366
367        writeln!(w, "}}")?;
368
369        Ok(())
370    }
371
372    fn write_comment(
373        &self,
374        w: &mut impl std::io::Write,
375        indent: usize,
376        comment: &str,
377    ) -> std::io::Result<()> {
378        writeln!(w, "{}/// {}", "\t".repeat(indent), comment)?;
379        Ok(())
380    }
381
382    fn write_comments(
383        &self,
384        w: &mut impl std::io::Write,
385        indent: usize,
386        comments: &[String],
387    ) -> std::io::Result<()> {
388        comments
389            .iter()
390            .try_for_each(|comment| self.write_comment(w, indent, comment))
391    }
392
393    fn indent(&self, str: impl AsRef<str>, count: usize) -> String {
394        let indentation = "    ".repeat(count);
395        str.as_ref()
396            .split('\n')
397            .map(|line| {
398                if line.is_empty() {
399                    line.to_string()
400                } else {
401                    format!("{indentation}{line}")
402                }
403            })
404            .join("\n")
405    }
406}