summaryrefslogtreecommitdiffstats
path: root/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby')
-rw-r--r--third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs375
-rw-r--r--third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs47
2 files changed, 422 insertions, 0 deletions
diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs
new file mode 100644
index 0000000000..1f1bf8e299
--- /dev/null
+++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs
@@ -0,0 +1,375 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use anyhow::Result;
+use askama::Template;
+use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
+use serde::{Deserialize, Serialize};
+use std::borrow::Borrow;
+use std::collections::HashMap;
+
+use crate::interface::*;
+use crate::BindingsConfig;
+
+const RESERVED_WORDS: &[&str] = &[
+ "alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined?", "do", "else",
+ "elsif", "END", "end", "ensure", "false", "for", "if", "module", "next", "nil", "not", "or",
+ "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless",
+ "until", "when", "while", "yield", "__FILE__", "__LINE__",
+];
+
+fn is_reserved_word(word: &str) -> bool {
+ RESERVED_WORDS.contains(&word)
+}
+
+/// Get the canonical, unique-within-this-component name for a type.
+///
+/// When generating helper code for foreign language bindings, it's sometimes useful to be
+/// able to name a particular type in order to e.g. call a helper function that is specific
+/// to that type. We support this by defining a naming convention where each type gets a
+/// unique canonical name, constructed recursively from the names of its component types (if any).
+pub fn canonical_name(t: &Type) -> String {
+ match t {
+ // Builtin primitive types, with plain old names.
+ Type::Int8 => "i8".into(),
+ Type::UInt8 => "u8".into(),
+ Type::Int16 => "i16".into(),
+ Type::UInt16 => "u16".into(),
+ Type::Int32 => "i32".into(),
+ Type::UInt32 => "u32".into(),
+ Type::Int64 => "i64".into(),
+ Type::UInt64 => "u64".into(),
+ Type::Float32 => "f32".into(),
+ Type::Float64 => "f64".into(),
+ Type::String => "string".into(),
+ Type::Bytes => "bytes".into(),
+ Type::Boolean => "bool".into(),
+ // API defined types.
+ // Note that these all get unique names, and the parser ensures that the names do not
+ // conflict with a builtin type. We add a prefix to the name to guard against pathological
+ // cases like a record named `SequenceRecord` interfering with `sequence<Record>`.
+ // However, types that support importing all end up with the same prefix of "Type", so
+ // that the import handling code knows how to find the remote reference.
+ Type::Object { name, .. } => format!("Type{name}"),
+ Type::Enum { name, .. } => format!("Type{name}"),
+ Type::Record { name, .. } => format!("Type{name}"),
+ Type::CallbackInterface { name, .. } => format!("CallbackInterface{name}"),
+ Type::Timestamp => "Timestamp".into(),
+ Type::Duration => "Duration".into(),
+ Type::ForeignExecutor => "ForeignExecutor".into(),
+ // Recursive types.
+ // These add a prefix to the name of the underlying type.
+ // The component API definition cannot give names to recursive types, so as long as the
+ // prefixes we add here are all unique amongst themselves, then we have no chance of
+ // acccidentally generating name collisions.
+ Type::Optional { inner_type } => format!("Optional{}", canonical_name(inner_type)),
+ Type::Sequence { inner_type } => format!("Sequence{}", canonical_name(inner_type)),
+ Type::Map {
+ key_type,
+ value_type,
+ } => format!(
+ "Map{}{}",
+ canonical_name(key_type).to_upper_camel_case(),
+ canonical_name(value_type).to_upper_camel_case()
+ ),
+ // A type that exists externally.
+ Type::External { name, .. } | Type::Custom { name, .. } => format!("Type{name}"),
+ }
+}
+
+// Some config options for it the caller wants to customize the generated ruby.
+// Note that this can only be used to control details of the ruby *that do not affect the underlying component*,
+// since the details of the underlying component are entirely determined by the `ComponentInterface`.
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct Config {
+ cdylib_name: Option<String>,
+ cdylib_path: Option<String>,
+}
+
+impl Config {
+ pub fn cdylib_name(&self) -> String {
+ self.cdylib_name
+ .clone()
+ .unwrap_or_else(|| "uniffi".to_string())
+ }
+
+ pub fn custom_cdylib_path(&self) -> bool {
+ self.cdylib_path.is_some()
+ }
+
+ pub fn cdylib_path(&self) -> String {
+ self.cdylib_path.clone().unwrap_or_default()
+ }
+}
+
+impl BindingsConfig for Config {
+ fn update_from_ci(&mut self, ci: &ComponentInterface) {
+ self.cdylib_name
+ .get_or_insert_with(|| format!("uniffi_{}", ci.namespace()));
+ }
+
+ fn update_from_cdylib_name(&mut self, cdylib_name: &str) {
+ self.cdylib_name
+ .get_or_insert_with(|| cdylib_name.to_string());
+ }
+
+ fn update_from_dependency_configs(&mut self, _config_map: HashMap<&str, &Self>) {}
+}
+
+#[derive(Template)]
+#[template(syntax = "rb", escape = "none", path = "wrapper.rb")]
+pub struct RubyWrapper<'a> {
+ config: Config,
+ ci: &'a ComponentInterface,
+ canonical_name: &'a dyn Fn(&Type) -> String,
+}
+impl<'a> RubyWrapper<'a> {
+ pub fn new(config: Config, ci: &'a ComponentInterface) -> Self {
+ Self {
+ config,
+ ci,
+ canonical_name: &canonical_name,
+ }
+ }
+}
+
+mod filters {
+ use super::*;
+ pub use crate::backend::filters::*;
+
+ pub fn type_ffi(type_: &FfiType) -> Result<String, askama::Error> {
+ Ok(match type_ {
+ FfiType::Int8 => ":int8".to_string(),
+ FfiType::UInt8 => ":uint8".to_string(),
+ FfiType::Int16 => ":int16".to_string(),
+ FfiType::UInt16 => ":uint16".to_string(),
+ FfiType::Int32 => ":int32".to_string(),
+ FfiType::UInt32 => ":uint32".to_string(),
+ FfiType::Int64 => ":int64".to_string(),
+ FfiType::UInt64 => ":uint64".to_string(),
+ FfiType::Float32 => ":float".to_string(),
+ FfiType::Float64 => ":double".to_string(),
+ FfiType::RustArcPtr(_) => ":pointer".to_string(),
+ FfiType::RustBuffer(_) => "RustBuffer.by_value".to_string(),
+ FfiType::ForeignBytes => "ForeignBytes".to_string(),
+ FfiType::ForeignCallback => unimplemented!("Callback interfaces are not implemented"),
+ FfiType::ForeignExecutorCallback => {
+ unimplemented!("Foreign executors are not implemented")
+ }
+ FfiType::ForeignExecutorHandle => {
+ unimplemented!("Foreign executors are not implemented")
+ }
+ FfiType::RustFutureHandle
+ | FfiType::RustFutureContinuationCallback
+ | FfiType::RustFutureContinuationData => {
+ unimplemented!("Async functions are not implemented")
+ }
+ })
+ }
+
+ pub fn literal_rb(literal: &Literal) -> Result<String, askama::Error> {
+ Ok(match literal {
+ Literal::Boolean(v) => {
+ if *v {
+ "true".into()
+ } else {
+ "false".into()
+ }
+ }
+ // use the double-quote form to match with the other languages, and quote escapes.
+ Literal::String(s) => format!("\"{s}\""),
+ Literal::Null => "nil".into(),
+ Literal::EmptySequence => "[]".into(),
+ Literal::EmptyMap => "{}".into(),
+ Literal::Enum(v, type_) => match type_ {
+ Type::Enum { name, .. } => {
+ format!("{}::{}", class_name_rb(name)?, enum_name_rb(v)?)
+ }
+ _ => panic!("Unexpected type in enum literal: {type_:?}"),
+ },
+ // https://docs.ruby-lang.org/en/2.0.0/syntax/literals_rdoc.html
+ Literal::Int(i, radix, _) => match radix {
+ Radix::Octal => format!("0o{i:o}"),
+ Radix::Decimal => format!("{i}"),
+ Radix::Hexadecimal => format!("{i:#x}"),
+ },
+ Literal::UInt(i, radix, _) => match radix {
+ Radix::Octal => format!("0o{i:o}"),
+ Radix::Decimal => format!("{i}"),
+ Radix::Hexadecimal => format!("{i:#x}"),
+ },
+ Literal::Float(string, _type_) => string.clone(),
+ })
+ }
+
+ pub fn class_name_rb(nm: &str) -> Result<String, askama::Error> {
+ Ok(nm.to_string().to_upper_camel_case())
+ }
+
+ pub fn fn_name_rb(nm: &str) -> Result<String, askama::Error> {
+ Ok(nm.to_string().to_snake_case())
+ }
+
+ pub fn var_name_rb(nm: &str) -> Result<String, askama::Error> {
+ let nm = nm.to_string();
+ let prefix = if is_reserved_word(&nm) { "_" } else { "" };
+
+ Ok(format!("{prefix}{}", nm.to_snake_case()))
+ }
+
+ pub fn enum_name_rb(nm: &str) -> Result<String, askama::Error> {
+ Ok(nm.to_string().to_shouty_snake_case())
+ }
+
+ pub fn coerce_rb(nm: &str, ns: &str, type_: &Type) -> Result<String, askama::Error> {
+ Ok(match type_ {
+ Type::Int8 => format!("{ns}::uniffi_in_range({nm}, \"i8\", -2**7, 2**7)"),
+ Type::Int16 => format!("{ns}::uniffi_in_range({nm}, \"i16\", -2**15, 2**15)"),
+ Type::Int32 => format!("{ns}::uniffi_in_range({nm}, \"i32\", -2**31, 2**31)"),
+ Type::Int64 => format!("{ns}::uniffi_in_range({nm}, \"i64\", -2**63, 2**63)"),
+ Type::UInt8 => format!("{ns}::uniffi_in_range({nm}, \"u8\", 0, 2**8)"),
+ Type::UInt16 => format!("{ns}::uniffi_in_range({nm}, \"u16\", 0, 2**16)"),
+ Type::UInt32 => format!("{ns}::uniffi_in_range({nm}, \"u32\", 0, 2**32)"),
+ Type::UInt64 => format!("{ns}::uniffi_in_range({nm}, \"u64\", 0, 2**64)"),
+ Type::Float32 | Type::Float64 => nm.to_string(),
+ Type::Boolean => format!("{nm} ? true : false"),
+ Type::Object { .. } | Type::Enum { .. } | Type::Record { .. } => nm.to_string(),
+ Type::String => format!("{ns}::uniffi_utf8({nm})"),
+ Type::Bytes => format!("{ns}::uniffi_bytes({nm})"),
+ Type::Timestamp | Type::Duration => nm.to_string(),
+ Type::CallbackInterface { .. } => {
+ panic!("No support for coercing callback interfaces yet")
+ }
+ Type::Optional { inner_type: t } => format!("({nm} ? {} : nil)", coerce_rb(nm, ns, t)?),
+ Type::Sequence { inner_type: t } => {
+ let coerce_code = coerce_rb("v", ns, t)?;
+ if coerce_code == "v" {
+ nm.to_string()
+ } else {
+ format!("{nm}.map {{ |v| {coerce_code} }}")
+ }
+ }
+ Type::Map { value_type: t, .. } => {
+ let k_coerce_code = coerce_rb("k", ns, &Type::String)?;
+ let v_coerce_code = coerce_rb("v", ns, t)?;
+
+ if k_coerce_code == "k" && v_coerce_code == "v" {
+ nm.to_string()
+ } else {
+ format!(
+ "{nm}.each.with_object({{}}) {{ |(k, v), res| res[{k_coerce_code}] = {v_coerce_code} }}"
+ )
+ }
+ }
+ Type::External { .. } => panic!("No support for external types, yet"),
+ Type::Custom { .. } => panic!("No support for custom types, yet"),
+ Type::ForeignExecutor => unimplemented!("Foreign executors are not implemented"),
+ })
+ }
+
+ pub fn lower_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> {
+ Ok(match type_ {
+ Type::Int8
+ | Type::UInt8
+ | Type::Int16
+ | Type::UInt16
+ | Type::Int32
+ | Type::UInt32
+ | Type::Int64
+ | Type::UInt64
+ | Type::Float32
+ | Type::Float64 => nm.to_string(),
+ Type::Boolean => format!("({nm} ? 1 : 0)"),
+ Type::String => format!("RustBuffer.allocFromString({nm})"),
+ Type::Bytes => format!("RustBuffer.allocFromBytes({nm})"),
+ Type::Object { name, .. } => format!("({}._uniffi_lower {nm})", class_name_rb(name)?),
+ Type::CallbackInterface { .. } => {
+ panic!("No support for lowering callback interfaces yet")
+ }
+ Type::Enum { .. }
+ | Type::Record { .. }
+ | Type::Optional { .. }
+ | Type::Sequence { .. }
+ | Type::Timestamp
+ | Type::Duration
+ | Type::Map { .. } => format!(
+ "RustBuffer.alloc_from_{}({})",
+ class_name_rb(&canonical_name(type_))?,
+ nm
+ ),
+ Type::External { .. } => panic!("No support for lowering external types, yet"),
+ Type::Custom { .. } => panic!("No support for lowering custom types, yet"),
+ Type::ForeignExecutor => unimplemented!("Foreign executors are not implemented"),
+ })
+ }
+
+ pub fn lift_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> {
+ Ok(match type_ {
+ Type::Int8
+ | Type::UInt8
+ | Type::Int16
+ | Type::UInt16
+ | Type::Int32
+ | Type::UInt32
+ | Type::Int64
+ | Type::UInt64 => format!("{nm}.to_i"),
+ Type::Float32 | Type::Float64 => format!("{nm}.to_f"),
+ Type::Boolean => format!("1 == {nm}"),
+ Type::String => format!("{nm}.consumeIntoString"),
+ Type::Bytes => format!("{nm}.consumeIntoBytes"),
+ Type::Object { name, .. } => format!("{}._uniffi_allocate({nm})", class_name_rb(name)?),
+ Type::CallbackInterface { .. } => {
+ panic!("No support for lifting callback interfaces, yet")
+ }
+ Type::Enum { .. } => {
+ format!(
+ "{}.consumeInto{}",
+ nm,
+ class_name_rb(&canonical_name(type_))?
+ )
+ }
+ Type::Record { .. }
+ | Type::Optional { .. }
+ | Type::Sequence { .. }
+ | Type::Timestamp
+ | Type::Duration
+ | Type::Map { .. } => format!(
+ "{}.consumeInto{}",
+ nm,
+ class_name_rb(&canonical_name(type_))?
+ ),
+ Type::External { .. } => panic!("No support for lifting external types, yet"),
+ Type::Custom { .. } => panic!("No support for lifting custom types, yet"),
+ Type::ForeignExecutor => unimplemented!("Foreign executors are not implemented"),
+ })
+ }
+}
+
+#[cfg(test)]
+mod test_type {
+ use super::*;
+
+ #[test]
+ fn test_canonical_names() {
+ // Non-exhaustive, but gives a bit of a flavour of what we want.
+ assert_eq!(canonical_name(&Type::UInt8), "u8");
+ assert_eq!(canonical_name(&Type::String), "string");
+ assert_eq!(canonical_name(&Type::Bytes), "bytes");
+ assert_eq!(
+ canonical_name(&Type::Optional {
+ inner_type: Box::new(Type::Sequence {
+ inner_type: Box::new(Type::Object {
+ module_path: "anything".to_string(),
+ name: "Example".into(),
+ imp: ObjectImpl::Struct,
+ })
+ })
+ }),
+ "OptionalSequenceTypeExample"
+ );
+ }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs
new file mode 100644
index 0000000000..9ae5d1816f
--- /dev/null
+++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs
@@ -0,0 +1,47 @@
+use super::{is_reserved_word, Config};
+
+#[test]
+fn when_reserved_word() {
+ assert!(is_reserved_word("end"));
+}
+
+#[test]
+fn when_not_reserved_word() {
+ assert!(!is_reserved_word("ruby"));
+}
+
+#[test]
+fn cdylib_name() {
+ let config = Config {
+ cdylib_name: None,
+ cdylib_path: None,
+ };
+
+ assert_eq!("uniffi", config.cdylib_name());
+
+ let config = Config {
+ cdylib_name: Some("todolist".to_string()),
+ cdylib_path: None,
+ };
+
+ assert_eq!("todolist", config.cdylib_name());
+}
+
+#[test]
+fn cdylib_path() {
+ let config = Config {
+ cdylib_name: None,
+ cdylib_path: None,
+ };
+
+ assert_eq!("", config.cdylib_path());
+ assert!(!config.custom_cdylib_path());
+
+ let config = Config {
+ cdylib_name: None,
+ cdylib_path: Some("/foo/bar".to_string()),
+ };
+
+ assert_eq!("/foo/bar", config.cdylib_path());
+ assert!(config.custom_cdylib_path());
+}