1use 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
25pub fn canonical_name(t: &Type) -> String {
32 match t {
33 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 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 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#[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 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 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 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 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;