uniffi_bindgen/bindings/ruby/gen_ruby/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use anyhow::Result;
6use askama::Template;
7
8use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
9use serde::{Deserialize, Serialize};
10use std::borrow::Borrow;
11
12use crate::interface::*;
13
14const RESERVED_WORDS: &[&str] = &[
15    "alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined?", "do", "else",
16    "elsif", "END", "end", "ensure", "false", "for", "if", "module", "next", "nil", "not", "or",
17    "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless",
18    "until", "when", "while", "yield", "__FILE__", "__LINE__",
19];
20
21fn is_reserved_word(word: &str) -> bool {
22    RESERVED_WORDS.contains(&word)
23}
24
25/// Get the canonical, unique-within-this-component name for a type.
26///
27/// When generating helper code for foreign language bindings, it's sometimes useful to be
28/// able to name a particular type in order to e.g. call a helper function that is specific
29/// to that type. We support this by defining a naming convention where each type gets a
30/// unique canonical name, constructed recursively from the names of its component types (if any).
31pub fn canonical_name(t: &Type) -> String {
32    match t {
33        // Builtin primitive types, with plain old names.
34        Type::Int8 => "i8".into(),
35        Type::UInt8 => "u8".into(),
36        Type::Int16 => "i16".into(),
37        Type::UInt16 => "u16".into(),
38        Type::Int32 => "i32".into(),
39        Type::UInt32 => "u32".into(),
40        Type::Int64 => "i64".into(),
41        Type::UInt64 => "u64".into(),
42        Type::Float32 => "f32".into(),
43        Type::Float64 => "f64".into(),
44        Type::String => "string".into(),
45        Type::Bytes => "bytes".into(),
46        Type::Boolean => "bool".into(),
47        // API defined types.
48        // Note that these all get unique names, and the parser ensures that the names do not
49        // conflict with a builtin type. We add a prefix to the name to guard against pathological
50        // cases like a record named `SequenceRecord` interfering with `sequence<Record>`.
51        // However, types that support importing all end up with the same prefix of "Type", so
52        // that the import handling code knows how to find the remote reference.
53        Type::Object { name, .. } => format!("Type{name}"),
54        Type::Enum { name, .. } => format!("Type{name}"),
55        Type::Record { name, .. } => format!("Type{name}"),
56        Type::CallbackInterface { name, .. } => format!("CallbackInterface{name}"),
57        Type::Timestamp => "Timestamp".into(),
58        Type::Duration => "Duration".into(),
59        // Recursive types.
60        // These add a prefix to the name of the underlying type.
61        // The component API definition cannot give names to recursive types, so as long as the
62        // prefixes we add here are all unique amongst themselves, then we have no chance of
63        // acccidentally generating name collisions.
64        Type::Optional { inner_type } => format!("Optional{}", canonical_name(inner_type)),
65        Type::Sequence { inner_type } => format!("Sequence{}", canonical_name(inner_type)),
66        Type::Map {
67            key_type,
68            value_type,
69        } => format!(
70            "Map{}{}",
71            canonical_name(key_type).to_upper_camel_case(),
72            canonical_name(value_type).to_upper_camel_case()
73        ),
74        Type::Custom { name, .. } => format!("Type{name}"),
75    }
76}
77
78// Some config options for it the caller wants to customize the generated ruby.
79// Note that this can only be used to control details of the ruby *that do not affect the underlying component*,
80// since the details of the underlying component are entirely determined by the `ComponentInterface`.
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82pub struct Config {
83    pub(super) cdylib_name: Option<String>,
84    cdylib_path: Option<String>,
85}
86
87impl Config {
88    pub fn cdylib_name(&self) -> String {
89        self.cdylib_name
90            .clone()
91            .unwrap_or_else(|| "uniffi".to_string())
92    }
93
94    pub fn custom_cdylib_path(&self) -> bool {
95        self.cdylib_path.is_some()
96    }
97
98    pub fn cdylib_path(&self) -> String {
99        self.cdylib_path.clone().unwrap_or_default()
100    }
101}
102
103#[derive(Template)]
104#[template(syntax = "rb", escape = "none", path = "wrapper.rb")]
105pub struct RubyWrapper<'a> {
106    config: Config,
107    ci: &'a ComponentInterface,
108}
109impl<'a> RubyWrapper<'a> {
110    pub fn new(config: Config, ci: &'a ComponentInterface) -> Self {
111        Self { config, ci }
112    }
113}
114
115mod filters {
116    use super::*;
117
118    pub fn type_ffi(type_: &FfiType, _: &dyn askama::Values) -> Result<String, askama::Error> {
119        Ok(match type_ {
120            FfiType::Int8 => ":int8".to_string(),
121            FfiType::UInt8 => ":uint8".to_string(),
122            FfiType::Int16 => ":int16".to_string(),
123            FfiType::UInt16 => ":uint16".to_string(),
124            FfiType::Int32 => ":int32".to_string(),
125            FfiType::UInt32 => ":uint32".to_string(),
126            FfiType::Int64 => ":int64".to_string(),
127            FfiType::UInt64 => ":uint64".to_string(),
128            FfiType::Float32 => ":float".to_string(),
129            FfiType::Float64 => ":double".to_string(),
130            FfiType::Handle => ":uint64".to_string(),
131            FfiType::RustBuffer(_) => "RustBuffer.by_value".to_string(),
132            FfiType::RustCallStatus => "RustCallStatus".to_string(),
133            FfiType::ForeignBytes => "ForeignBytes".to_string(),
134            FfiType::Callback(_) => unimplemented!("FFI Callbacks not implemented"),
135            // Note: this can't just be `unimplemented!()` because some of the FFI function
136            // definitions use references.  Those FFI functions aren't actually used, so we just
137            // pick something that runs and makes some sense.  Revisit this once the references
138            // are actually implemented.
139            FfiType::Reference(_) | FfiType::MutReference(_) => ":pointer".to_string(),
140            FfiType::VoidPointer => ":pointer".to_string(),
141            FfiType::Struct(_) => {
142                unimplemented!("Structs are not implemented")
143            }
144        })
145    }
146
147    pub fn default_rb(
148        default: &DefaultValue,
149        _: &dyn askama::Values,
150    ) -> Result<String, askama::Error> {
151        let DefaultValue::Literal(literal) = default else {
152            unimplemented!("not supported.");
153        };
154        Ok(match literal {
155            Literal::Boolean(v) => {
156                if *v {
157                    "true".into()
158                } else {
159                    "false".into()
160                }
161            }
162            // use the double-quote form to match with the other languages, and quote escapes.
163            Literal::String(s) => format!("\"{s}\""),
164            Literal::None => "nil".into(),
165            Literal::Some { inner } => default_rb(inner, &())?,
166            Literal::EmptySequence => "[]".into(),
167            Literal::EmptyMap => "{}".into(),
168            Literal::Enum(v, type_) => match type_ {
169                Type::Enum { name, .. } => {
170                    format!("{}::{}", class_name_rb(name, &())?, enum_name_rb(v, &())?)
171                }
172                _ => panic!("Unexpected type in enum literal: {type_:?}"),
173            },
174            // https://docs.ruby-lang.org/en/2.0.0/syntax/literals_rdoc.html
175            Literal::Int(i, radix, _) => match radix {
176                Radix::Octal => format!("0o{i:o}"),
177                Radix::Decimal => format!("{i}"),
178                Radix::Hexadecimal => format!("{i:#x}"),
179            },
180            Literal::UInt(i, radix, _) => match radix {
181                Radix::Octal => format!("0o{i:o}"),
182                Radix::Decimal => format!("{i}"),
183                Radix::Hexadecimal => format!("{i:#x}"),
184            },
185            Literal::Float(string, _type_) => string.clone(),
186        })
187    }
188
189    pub fn class_name_rb(nm: &str, _: &dyn askama::Values) -> Result<String, askama::Error> {
190        Ok(nm.to_string().to_upper_camel_case())
191    }
192
193    pub fn fn_name_rb(nm: &str, _: &dyn askama::Values) -> Result<String, askama::Error> {
194        Ok(nm.to_string().to_snake_case())
195    }
196
197    pub fn var_name_rb(nm: &str, _: &dyn askama::Values) -> Result<String, askama::Error> {
198        let nm = nm.to_string();
199        let prefix = if is_reserved_word(&nm) { "_" } else { "" };
200
201        Ok(format!("{prefix}{}", nm.to_snake_case()))
202    }
203
204    pub fn enum_name_rb(nm: &str, _: &dyn askama::Values) -> Result<String, askama::Error> {
205        Ok(nm.to_string().to_shouty_snake_case())
206    }
207
208    pub fn coerce_rb<S1: AsRef<str>, S2: AsRef<str>>(
209        nm: S1,
210        _: &dyn askama::Values,
211        ns: S2,
212        type_: &Type,
213    ) -> Result<String, askama::Error> {
214        let nm = nm.as_ref();
215        let ns = ns.as_ref();
216        Ok(match type_ {
217            Type::Int8 => format!("{ns}::uniffi_in_range({nm}, \"i8\", -2**7, 2**7)"),
218            Type::Int16 => format!("{ns}::uniffi_in_range({nm}, \"i16\", -2**15, 2**15)"),
219            Type::Int32 => format!("{ns}::uniffi_in_range({nm}, \"i32\", -2**31, 2**31)"),
220            Type::Int64 => format!("{ns}::uniffi_in_range({nm}, \"i64\", -2**63, 2**63)"),
221            Type::UInt8 => format!("{ns}::uniffi_in_range({nm}, \"u8\", 0, 2**8)"),
222            Type::UInt16 => format!("{ns}::uniffi_in_range({nm}, \"u16\", 0, 2**16)"),
223            Type::UInt32 => format!("{ns}::uniffi_in_range({nm}, \"u32\", 0, 2**32)"),
224            Type::UInt64 => format!("{ns}::uniffi_in_range({nm}, \"u64\", 0, 2**64)"),
225            Type::Float32 | Type::Float64 => nm.to_string(),
226            Type::Boolean => format!("{nm} ? true : false"),
227            Type::Object { .. } | Type::Enum { .. } | Type::Record { .. } => nm.to_string(),
228            Type::String => format!("{ns}::uniffi_utf8({nm})"),
229            Type::Bytes => format!("{ns}::uniffi_bytes({nm})"),
230            Type::Timestamp | Type::Duration => nm.to_string(),
231            Type::CallbackInterface { .. } => {
232                panic!("No support for coercing callback interfaces yet")
233            }
234            Type::Optional { inner_type: t } => {
235                format!("({nm} ? {} : nil)", coerce_rb(nm, &(), ns, t)?)
236            }
237            Type::Sequence { inner_type: t } => {
238                let coerce_code = coerce_rb("v", &(), ns, t)?;
239                if coerce_code == "v" {
240                    nm.to_string()
241                } else {
242                    format!("{nm}.map {{ |v| {coerce_code} }}")
243                }
244            }
245            Type::Map { value_type: t, .. } => {
246                let k_coerce_code = coerce_rb("k", &(), ns, &Type::String)?;
247                let v_coerce_code = coerce_rb("v", &(), ns, t)?;
248
249                if k_coerce_code == "k" && v_coerce_code == "v" {
250                    nm.to_string()
251                } else {
252                    format!(
253                        "{nm}.each.with_object({{}}) {{ |(k, v), res| res[{k_coerce_code}] = {v_coerce_code} }}"
254                    )
255                }
256            }
257            Type::Custom { .. } => panic!("No support for custom types, yet"),
258        })
259    }
260
261    pub fn check_lower_rb<S: AsRef<str>>(
262        nm: S,
263        _: &dyn askama::Values,
264        type_: &Type,
265    ) -> Result<String, askama::Error> {
266        let nm = nm.as_ref();
267        Ok(match type_ {
268            Type::Object { name, .. } => {
269                format!("({}.uniffi_check_lower {nm})", class_name_rb(name, &())?)
270            }
271            Type::Enum { .. }
272            | Type::Record { .. }
273            | Type::Optional { .. }
274            | Type::Sequence { .. }
275            | Type::Map { .. } => format!(
276                "RustBuffer.check_lower_{}({})",
277                class_name_rb(&canonical_name(type_), &())?,
278                nm
279            ),
280            _ => "".to_owned(),
281        })
282    }
283
284    pub fn lower_rb(
285        nm: &str,
286        _: &dyn askama::Values,
287        type_: &Type,
288    ) -> Result<String, askama::Error> {
289        Ok(match type_ {
290            Type::Int8
291            | Type::UInt8
292            | Type::Int16
293            | Type::UInt16
294            | Type::Int32
295            | Type::UInt32
296            | Type::Int64
297            | Type::UInt64
298            | Type::Float32
299            | Type::Float64 => nm.to_string(),
300            Type::Boolean => format!("({nm} ? 1 : 0)"),
301            Type::String => format!("RustBuffer.allocFromString({nm})"),
302            Type::Bytes => format!("RustBuffer.allocFromBytes({nm})"),
303            Type::Object { name, .. } => {
304                format!("({}.uniffi_lower {nm})", class_name_rb(name, &())?)
305            }
306            Type::CallbackInterface { .. } => {
307                panic!("No support for lowering callback interfaces yet")
308            }
309            Type::Enum { .. }
310            | Type::Record { .. }
311            | Type::Optional { .. }
312            | Type::Sequence { .. }
313            | Type::Timestamp
314            | Type::Duration
315            | Type::Map { .. } => format!(
316                "RustBuffer.alloc_from_{}({})",
317                class_name_rb(&canonical_name(type_), &())?,
318                nm
319            ),
320            Type::Custom { .. } => panic!("No support for lowering custom types, yet"),
321        })
322    }
323
324    pub fn lift_rb(
325        nm: &str,
326        _: &dyn askama::Values,
327        type_: &Type,
328    ) -> Result<String, askama::Error> {
329        Ok(match type_ {
330            Type::Int8
331            | Type::UInt8
332            | Type::Int16
333            | Type::UInt16
334            | Type::Int32
335            | Type::UInt32
336            | Type::Int64
337            | Type::UInt64 => format!("{nm}.to_i"),
338            Type::Float32 | Type::Float64 => format!("{nm}.to_f"),
339            Type::Boolean => format!("1 == {nm}"),
340            Type::String => format!("{nm}.consumeIntoString"),
341            Type::Bytes => format!("{nm}.consumeIntoBytes"),
342            Type::Object { name, .. } => {
343                format!("{}.uniffi_allocate({nm})", class_name_rb(name, &())?)
344            }
345            Type::CallbackInterface { .. } => {
346                panic!("No support for lifting callback interfaces, yet")
347            }
348            Type::Enum { .. } => {
349                format!(
350                    "{}.consumeInto{}",
351                    nm,
352                    class_name_rb(&canonical_name(type_), &())?
353                )
354            }
355            Type::Record { .. }
356            | Type::Optional { .. }
357            | Type::Sequence { .. }
358            | Type::Timestamp
359            | Type::Duration
360            | Type::Map { .. } => format!(
361                "{}.consumeInto{}",
362                nm,
363                class_name_rb(&canonical_name(type_), &())?
364            ),
365            Type::Custom { .. } => panic!("No support for lifting custom types, yet"),
366        })
367    }
368}
369
370#[cfg(test)]
371mod test_type {
372    use super::*;
373
374    #[test]
375    fn test_canonical_names() {
376        // Non-exhaustive, but gives a bit of a flavour of what we want.
377        assert_eq!(canonical_name(&Type::UInt8), "u8");
378        assert_eq!(canonical_name(&Type::String), "string");
379        assert_eq!(canonical_name(&Type::Bytes), "bytes");
380        assert_eq!(
381            canonical_name(&Type::Optional {
382                inner_type: Box::new(Type::Sequence {
383                    inner_type: Box::new(Type::Object {
384                        module_path: "anything".to_string(),
385                        name: "Example".into(),
386                        imp: ObjectImpl::Struct,
387                    })
388                })
389            }),
390            "OptionalSequenceTypeExample"
391        );
392    }
393}
394
395#[cfg(test)]
396mod tests;