diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 9d832bf95..df98c45c1 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -8,6 +8,7 @@ rpath = true [workspace] members = [ "ls-sdk-bindings", + "ls-sdk-bindings/bindings-react-native", "ls-sdk-core", ] resolver = "2" diff --git a/lib/ls-sdk-bindings/bindings-react-native/.gitignore b/lib/ls-sdk-bindings/bindings-react-native/.gitignore new file mode 100644 index 000000000..0875dfd72 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/.gitignore @@ -0,0 +1,15 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated files +android/ +ios/ +ts/ diff --git a/lib/ls-sdk-bindings/bindings-react-native/Cargo.toml b/lib/ls-sdk-bindings/bindings-react-native/Cargo.toml new file mode 100644 index 000000000..5160c2432 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "bindings-react-native" +version = "0.0.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { version = "1.0.57", features = ["backtrace"] } +thiserror = "1.0" +tokio = { version = "1", features = ["full"] } +uniffi = { version = "0.23.0", features = ["bindgen-tests", "cli"] } +uniffi_bindgen = "0.23.0" +uniffi_macros = "0.23.0" +camino = "1.1.1" +log = "*" +serde = "*" +askama = { version = "0.11.1", default-features = false, features = ["config"] } +toml = "0.5" +clap = { version = "3.2.22", features = ["derive"] } +heck = "0.4" +paste = "1.0" +once_cell = "1.12" + +[build-dependencies] +uniffi_build = { version = "0.23.0" } +uniffi_bindgen = "0.23.0" +anyhow = { version = "1.0.57", features = ["backtrace"] } diff --git a/lib/ls-sdk-bindings/bindings-react-native/README.md b/lib/ls-sdk-bindings/bindings-react-native/README.md new file mode 100644 index 000000000..657ca9184 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/README.md @@ -0,0 +1,19 @@ +# breez-sdk-rn-generator + +This utility generates the liquid-swap-sdk React Native package code. + +## Prerequisites + +```bash +brew install ktlint kotlin swiftformat +``` + +```bash +yarn global add tslint typescript +``` + +## Run + +``` +cargo run +``` diff --git a/lib/ls-sdk-bindings/bindings-react-native/askama.toml b/lib/ls-sdk-bindings/bindings-react-native/askama.toml new file mode 100644 index 000000000..ab4312978 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/askama.toml @@ -0,0 +1,6 @@ +[general] +# Directories to search for templates, relative to the crate root. +dirs = [ "src/gen_kotlin/templates", "src/gen_swift/templates", "src/gen_typescript/templates" ] + +[[syntax]] +name = "rn" diff --git a/lib/ls-sdk-bindings/bindings-react-native/makefile b/lib/ls-sdk-bindings/bindings-react-native/makefile new file mode 100644 index 000000000..3dcdc4c52 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/makefile @@ -0,0 +1,5 @@ +SOURCES=$(sort $(wildcard ./src/*.rs ./src/**/*.rs)) + +codegen: $(SOURCES) + cargo run + \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/mod.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/mod.rs new file mode 100644 index 000000000..410cfa70e --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/mod.rs @@ -0,0 +1,334 @@ +use std::cell::RefCell; +use std::collections::{BTreeSet, HashSet}; + +use askama::Template; +use once_cell::sync::Lazy; +use uniffi_bindgen::interface::*; + +pub use uniffi_bindgen::bindings::kotlin::gen_kotlin::*; + +use crate::generator::RNConfig; + +static IGNORED_FUNCTIONS: Lazy> = Lazy::new(|| { + let list: Vec<&str> = vec!["init"]; + HashSet::from_iter(list.into_iter().map(|s| s.to_string())) +}); + +#[derive(Template)] +#[template(syntax = "rn", escape = "none", path = "mapper.kt")] +pub struct MapperGenerator<'a> { + config: RNConfig, + ci: &'a ComponentInterface, + // Track types used in sequences with the `add_sequence_type()` macro + sequence_types: RefCell>, +} + +impl<'a> MapperGenerator<'a> { + pub fn new(config: RNConfig, ci: &'a ComponentInterface) -> Self { + Self { + config, + ci, + sequence_types: RefCell::new(BTreeSet::new()), + } + } + + // Helper to add a sequence type + // + // Call this inside your template to add a type used in a sequence. + // This type is then added to the pushToArray helper. + // Imports will be sorted and de-deuped. + // + // Returns an empty string so that it can be used inside an askama `{{ }}` block. + fn add_sequence_type(&self, type_name: &str) -> &str { + self.sequence_types + .borrow_mut() + .insert(type_name.to_owned()); + "" + } + + pub fn sequence_types(&self) -> Vec { + let sequence_types = self.sequence_types.clone().into_inner(); + sequence_types.into_iter().collect() + } +} + +#[derive(Template)] +#[template(syntax = "rn", escape = "none", path = "module.kt")] +pub struct ModuleGenerator<'a> { + config: RNConfig, + ci: &'a ComponentInterface, +} + +impl<'a> ModuleGenerator<'a> { + pub fn new(config: RNConfig, ci: &'a ComponentInterface) -> Self { + Self { config, ci } + } +} + +pub mod filters { + use heck::*; + use uniffi_bindgen::backend::CodeOracle; + use uniffi_bindgen::backend::{CodeType, TypeIdentifier}; + + use super::*; + + fn oracle() -> &'static KotlinCodeOracle { + &KotlinCodeOracle + } + + pub fn type_name(codetype: &impl CodeType) -> Result { + Ok(codetype.type_label(oracle())) + } + + pub fn fn_name(nm: &str) -> Result { + Ok(oracle().fn_name(nm)) + } + + pub fn render_to_array( + type_name: &str, + ci: &ComponentInterface, + ) -> Result { + let res: Result = match type_name { + "Boolean" => Ok(format!("array.pushBoolean(value)").into()), + "Double" => Ok(format!("array.pushDouble(value)").into()), + "Int" => Ok(format!("array.pushInt(value)").into()), + "ReadableArray" => Ok(format!("array.pushArray(value)").into()), + "ReadableMap" => Ok(format!("array.pushMap(value)").into()), + "String" => Ok(format!("array.pushString(value)").into()), + "UByte" => Ok(format!("array.pushInt(value.toInt())").into()), + "UInt" => Ok(format!("array.pushInt(value.toInt())").into()), + "UShort" => Ok(format!("array.pushInt(value.toInt())").into()), + "ULong" => Ok(format!("array.pushDouble(value.toDouble())").into()), + _ => match ci.get_type(type_name) { + Some(t) => match t { + Type::Enum(inner) => { + let enum_def = ci.get_enum_definition(&inner).unwrap(); + match enum_def.is_flat() { + true => Ok(format!("array.pushString(value.name.lowercase())").into()), + false => Ok(format!("array.pushMap(readableMapOf(value))").into()), + } + } + _ => Ok(format!("array.pushMap(readableMapOf(value))").into()), + }, + None => unimplemented!("known type: {type_name}"), + }, + }; + res + } + + pub fn render_to_map( + t: &TypeIdentifier, + ci: &ComponentInterface, + obj_name: &str, + field_name: &str, + optional: bool, + ) -> Result { + let res: Result = match t { + Type::UInt8 => Ok(format!("{obj_name}.{field_name}").into()), + Type::Int8 => Ok(format!("{obj_name}.{field_name}").into()), + Type::UInt16 => Ok(format!("{obj_name}.{field_name}").into()), + Type::Int16 => Ok(format!("{obj_name}.{field_name}").into()), + Type::UInt32 => Ok(format!("{obj_name}.{field_name}").into()), + Type::Int32 => Ok(format!("{obj_name}.{field_name}").into()), + Type::UInt64 => Ok(format!("{obj_name}.{field_name}").into()), + Type::Int64 => Ok(format!("{obj_name}.{field_name}").into()), + Type::Float32 => Ok(format!("{obj_name}.{field_name}").into()), + Type::Float64 => Ok(format!("{obj_name}.{field_name}").into()), + Type::Boolean => Ok(format!("{obj_name}.{field_name}").into()), + Type::String => Ok(format!("{obj_name}.{field_name}").into()), + Type::Timestamp => unimplemented!("render_to_map: Timestamp is not implemented"), + Type::Duration => unimplemented!("render_to_map: Duration is not implemented"), + Type::Object(_) => unimplemented!("render_to_map: Object is not implemented"), + Type::Record(_) => match optional { + true => Ok(format!("{obj_name}.{field_name}?.let {{ readableMapOf(it) }}").into()), + false => Ok(format!("readableMapOf({obj_name}.{field_name})").into()), + }, + Type::Enum(inner) => { + let enum_def = ci.get_enum_definition(inner).unwrap(); + match enum_def.is_flat() { + true => match optional { + true => Ok(format!( + "{obj_name}.{field_name}?.let {{ it.name.lowercase() }}" + ) + .into()), + false => Ok(format!("{obj_name}.{field_name}.name.lowercase()").into()), + }, + false => match optional { + true => Ok( + format!("{obj_name}.{field_name}?.let {{ readableMapOf(it) }}").into(), + ), + false => Ok(format!("readableMapOf({obj_name}.{field_name})").into()), + }, + } + } + Type::Error(_) => unimplemented!("render_to_map: Error is not implemented"), + Type::CallbackInterface(_) => { + unimplemented!("render_to_map: CallbackInterface is not implemented") + } + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + render_to_map(unboxed, ci, obj_name, field_name, true) + } + Type::Sequence(_) => match optional { + true => Ok(format!( + "{obj_name}.{field_name}?.let {{ readableArrayOf(it) }}" + )), + false => Ok(format!("readableArrayOf({obj_name}.{field_name})")), + }, + Type::Map(_, _) => unimplemented!("render_to_map: Map is not implemented"), + Type::External { .. } => { + unimplemented!("render_to_map: External is not implemented") + } + Type::Custom { .. } => { + unimplemented!("render_to_map: Custom is not implemented") + } + Type::Unresolved { .. } => { + unimplemented!("render_to_map: Unresolved is not implemented") + } + }; + res + } + + pub fn render_from_map( + t: &TypeIdentifier, + ci: &ComponentInterface, + name: &str, + field_name: &str, + optional: bool, + ) -> Result { + let mut mandatory_suffix = ""; + if !optional { + mandatory_suffix = "!!" + } + let res: String = match t { + Type::UInt8 => format!("{name}.getInt(\"{field_name}\").toUByte()").into(), + Type::Int8 => format!("{name}.getInt(\"{field_name}\").toByte()").into(), + Type::UInt16 => format!("{name}.getInt(\"{field_name}\").toUShort()").into(), + Type::Int16 => format!("{name}.getInt(\"{field_name}\").toShort()").into(), + Type::UInt32 => format!("{name}.getInt(\"{field_name}\").toUInt()").into(), + Type::Int32 => format!("{name}.getInt(\"{field_name}\")").into(), + Type::UInt64 => format!("{name}.getDouble(\"{field_name}\").toULong()").into(), + Type::Int64 => format!("{name}.getDouble(\"{field_name}\").toLong()").into(), + Type::Float32 => format!("{name}.getDouble(\"{field_name}\")").into(), + Type::Float64 => format!("{name}.getDouble(\"{field_name}\")").into(), + Type::Boolean => format!("{name}.getBoolean(\"{field_name}\")").into(), + Type::String => format!("{name}.getString(\"{field_name}\"){mandatory_suffix}").into(), + Type::Timestamp => "".into(), + Type::Duration => "".into(), + Type::Object(_) => "".into(), + Type::Record(_) => { + let record_type_name = type_name(t)?; + format!( + "{name}.getMap(\"{field_name}\")?.let {{ as{record_type_name}(it)}}{mandatory_suffix}" + ) + .into() + } + Type::Enum(inner) => { + let enum_def = ci.get_enum_definition(inner).unwrap(); + match enum_def.is_flat() { + false => { + format!("{name}.getMap(\"{field_name}\")?.let {{ as{inner}(it)}}{mandatory_suffix}") + .into() + } + true => format!( + "{name}.getString(\"{field_name}\")?.let {{ as{inner}(it)}}{mandatory_suffix}" + ) + .into(), + } + } + Type::Error(_) => "".into(), + Type::CallbackInterface(_) => "".into(), + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + let inner_res = render_from_map(unboxed, ci, name, field_name, true)?; + format!("if (hasNonNullKey({name}, \"{field_name}\")) {inner_res} else null") + } + Type::Sequence(inner) => { + let unboxed = inner.as_ref(); + let element_type_name = type_name(unboxed)?; + format!("{name}.getArray(\"{field_name}\")?.let {{ as{element_type_name}List(it) }}{mandatory_suffix}") + } + Type::Map(_, _) => "".into(), + Type::External { .. } => "".into(), + Type::Custom { .. } => "".into(), + Type::Unresolved { .. } => "".into(), + }; + Ok(res.to_string()) + } + + /// Get the idiomatic Kotlin rendering of a variable name. + pub fn var_name(nm: &str) -> Result { + Ok(format!("`{}`", nm.to_string().to_lower_camel_case())) + } + + pub fn unquote(nm: &str) -> Result { + Ok(nm.trim_matches('`').to_string()) + } + + pub fn ignored_function(nm: &str) -> Result { + Ok(IGNORED_FUNCTIONS.contains(nm)) + } + + pub fn rn_convert_type( + t: &TypeIdentifier, + ci: &ComponentInterface, + ) -> Result { + match t { + Type::UInt8 | Type::UInt16 | Type::UInt32 => Ok(".toUInt()".to_string()), + Type::Int64 => Ok(".toLong()".to_string()), + Type::UInt64 => Ok(".toULong()".to_string()), + Type::Float32 | Type::Float64 => Ok(".toFloat()".to_string()), + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + let conversion = rn_convert_type(unboxed, ci).unwrap(); + let optional = match *unboxed { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 => ".takeUnless { it == 0 }".to_string(), + Type::Int64 => ".takeUnless { it == 0L }".to_string(), + Type::UInt64 => ".takeUnless { it == 0UL }".to_string(), + Type::Float32 | Type::Float64 => ".takeUnless { it == 0.0 }".to_string(), + Type::String => ".takeUnless { it.isEmpty() }".to_string(), + _ => "".to_string(), + }; + Ok(format!("{}{}", conversion, optional)) + } + _ => Ok("".to_string()), + } + } + + pub fn rn_type_name( + t: &TypeIdentifier, + ci: &ComponentInterface, + ) -> Result { + match t { + Type::Boolean => Ok("Boolean".to_string()), + Type::Int8 | Type::UInt8 | Type::Int16 | Type::UInt16 | Type::Int32 | Type::UInt32 => { + Ok("Int".to_string()) + } + Type::Int64 | Type::UInt64 | Type::Float32 | Type::Float64 => Ok("Double".to_string()), + Type::String => Ok("String".to_string()), + Type::Enum(inner) => { + let enum_def = ci.get_enum_definition(inner).unwrap(); + match enum_def.is_flat() { + false => Ok("ReadableMap".to_string()), + true => Ok("String".to_string()), + } + } + Type::Record(_) => Ok("ReadableMap".to_string()), + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + rn_type_name(unboxed, ci) + } + Type::Sequence(_) => Ok("ReadableArray".to_string()), + _ => Ok("".to_string()), + } + } + + pub fn temporary(nm: &str) -> Result { + Ok(format!("{nm}Tmp")) + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/EnumTemplate.kt b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/EnumTemplate.kt new file mode 100644 index 000000000..581bf4b12 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/EnumTemplate.kt @@ -0,0 +1,56 @@ +{%- let e = ci.get_enum_definition(name).unwrap() %} +{%- if e.is_flat() %} + +fun as{{ type_name }}(type: String): {{ type_name }} { + return {{ type_name }}.valueOf(type.uppercase()) +} + +{%- else %} + +fun as{{ type_name }}({{ type_name|var_name|unquote }}: ReadableMap): {{ type_name }}? { + val type = {{ type_name|var_name|unquote }}.getString("type") + + {% for variant in e.variants() -%} + if (type == "{{ variant.name()|var_name|unquote }}") { + {% if variant.has_fields() -%} + return {{ type_name }}.{{ variant.name() }}( {{ variant.fields()[0].type_()|render_from_map(ci, type_name|var_name|unquote, variant.fields()[0].name()|var_name|unquote, false) }}) + {%- else %} + return {{ type_name }}.{{ variant.name() }} + {%- endif %} + } + {% endfor -%} + + return null +} + +fun readableMapOf({{ type_name|var_name|unquote }}: {{ type_name }}): ReadableMap? { + val map = Arguments.createMap() + when ({{ type_name|var_name|unquote }}) { + {% for variant in e.variants() -%} + is {{ type_name }}.{{ variant.name() }} -> { + pushToMap(map, "type", "{{ variant.name()|var_name|unquote }}") + {% for f in variant.fields() -%} + pushToMap(map, "{{ f.name()|var_name|unquote }}", {{ f.type_()|render_to_map(ci,type_name|var_name|unquote,f.name()|var_name|unquote, false) }}) + {% endfor -%} + } + {% endfor %} + } + return map +} + +{%- endif %} + +fun as{{ type_name }}List(arr: ReadableArray): List<{{ type_name }}> { + val list = ArrayList<{{ type_name }}>() + for (value in arr.toArrayList()) { + when (value) { +{%- if e.is_flat() %} + is String -> list.add(as{{ type_name }}(value)!!) +{%- else %} + is ReadableMap -> list.add(as{{ type_name }}(value)!!) +{%- endif %} + else -> throw LsSdkException.Generic(errUnexpectedType("${value::class.java.name}")) + } + } + return list +} \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/Helpers.kt b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/Helpers.kt new file mode 100644 index 000000000..e94184152 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/Helpers.kt @@ -0,0 +1,96 @@ +fun readableMapOf(vararg values: Pair): ReadableMap { + val map = Arguments.createMap() + for ((key, value) in values) { + pushToMap(map, key, value) + } + return map +} + +fun hasNonNullKey(map: ReadableMap, key: String): Boolean { + return map.hasKey(key) && !map.isNull(key) +} + +fun validateMandatoryFields(map: ReadableMap, keys: Array): Boolean { + for (k in keys) { + if (!hasNonNullKey(map, k)) return false + } + + return true +} + +fun pushToArray(array: WritableArray, value: Any?) { + when (value) { + null -> array.pushNull() + {%- for sequence_type in self.sequence_types() %} + is {{ sequence_type }} -> {{sequence_type|render_to_array(ci)}} + {%- endfor %} + is Array<*> -> array.pushArray(readableArrayOf(value.asIterable())) + is List<*> -> array.pushArray(readableArrayOf(value)) + else -> throw LsSdkException.Generic(errUnexpectedType("${value::class.java.name}")) + } +} + +fun pushToMap(map: WritableMap, key: String, value: Any?) { + when (value) { + null -> map.putNull(key) + is Boolean -> map.putBoolean(key, value) + is Byte -> map.putInt(key, value.toInt()) + is Double -> map.putDouble(key, value) + is Int -> map.putInt(key, value) + is Long -> map.putDouble(key, value.toDouble()) + is ReadableArray -> map.putArray(key, value) + is ReadableMap -> map.putMap(key, value) + is String -> map.putString(key, value) + is UByte -> map.putInt(key, value.toInt()) + is UInt -> map.putInt(key, value.toInt()) + is UShort -> map.putInt(key, value.toInt()) + is ULong -> map.putDouble(key, value.toDouble()) + is Array<*> -> map.putArray(key, readableArrayOf(value.asIterable())) + is List<*> -> map.putArray(key, readableArrayOf(value)) + else -> throw LsSdkException.Generic("Unexpected type ${value::class.java.name} for key [$key]") + } +} + +fun readableArrayOf(values: Iterable<*>?): ReadableArray { + val array = Arguments.createArray() + if (values != null) { + for (value in values) { + pushToArray(array, value) + } + } + + return array +} + +fun asUByteList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toArrayList()) { + when (value) { + is Double -> list.add(value.toInt().toUByte()) + is Int -> list.add(value.toUByte()) + is UByte -> list.add(value) + else -> throw LsSdkException.Generic(errUnexpectedType("${value::class.java.name}")) + } + } + return list +} + +fun asStringList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toArrayList()) { + list.add(value.toString()) + } + return list +} + +fun errMissingMandatoryField(fieldName: String, typeName: String): String { + return "Missing mandatory field ${fieldName} for type ${typeName}" + } + +fun errUnexpectedType(typeName: String): String { + return "Unexpected type ${typeName}" + } + +fun errUnexpectedValue(fieldName: String): String { + return "Unexpected value for optional field ${fieldName}" +} \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/Objects.kt b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/Objects.kt new file mode 100644 index 000000000..f3fc7d51a --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/Objects.kt @@ -0,0 +1,13 @@ +{%- for type_ in ci.iter_types() %} +{%- let type_name = type_|type_name %} +{%- match type_ %} +{%- when Type::Object ( name ) %} +{% let obj = ci.get_object_definition(name).unwrap() %} +{% let obj_interface = "getBindingWallet()." %} +{%- for func in obj.methods() -%} +{%- include "TopLevelFunctionTemplate.kt" %} +{% endfor %} +{%- else -%} +{%- endmatch -%} +{%- endfor %} + diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/RecordTemplate.kt b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/RecordTemplate.kt new file mode 100644 index 000000000..a62078a4e --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/RecordTemplate.kt @@ -0,0 +1,50 @@ +{%- let rec = ci.get_record_definition(name).unwrap() %} +fun as{{ type_name }}({{ type_name|var_name|unquote }}: ReadableMap): {{ type_name }}? { + if (!validateMandatoryFields({{ type_name|var_name|unquote }}, arrayOf( + {%- for field in rec.fields() %} + {%- match field.type_() %} + {%- when Type::Optional(_) %} + {%- else %} + "{{ field.name()|var_name |unquote }}", + {%- endmatch %} + {%- endfor %} + ))) { + return null + } + + {%- for field in rec.fields() %} + val {{field.name()|var_name|unquote}} = {{ field.type_()|render_from_map(ci, type_name|var_name|unquote, field.name()|var_name|unquote, false) }} + {%- endfor %} + return {{ type_name }}({%- call kt::field_list(rec) -%}) +} + +fun readableMapOf({{ type_name|var_name|unquote }}: {{ type_name }}): ReadableMap { + return readableMapOf( + {%- for field in rec.fields() %} + {%- match field.type_() %} + {%- when Type::Optional(inner) %} + {%- let unboxed = inner.as_ref() %} + {%- match unboxed %} + {%- when Type::Sequence(inner_type) %} + {{- self.add_sequence_type(inner_type|type_name) }} + {%- else %} + {%- endmatch %} + {%- when Type::Sequence(inner_type) %} + {{- self.add_sequence_type(inner_type|type_name) }} + {%- else %} + {%- endmatch %} + "{{ field.name()|var_name|unquote }}" to {{ field.type_()|render_to_map(ci,type_name|var_name|unquote, field.name()|var_name|unquote, false) }}, + {%- endfor %} + ) +} + +fun as{{ type_name }}List(arr: ReadableArray): List<{{ type_name }}> { + val list = ArrayList<{{ type_name }}>() + for (value in arr.toArrayList()) { + when (value) { + is ReadableMap -> list.add(as{{ type_name }}(value)!!) + else -> throw LsSdkException.Generic(errUnexpectedType("${value::class.java.name}")) + } + } + return list +} \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/TopLevelFunctionTemplate.kt b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/TopLevelFunctionTemplate.kt new file mode 100644 index 000000000..b271c8316 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/TopLevelFunctionTemplate.kt @@ -0,0 +1,40 @@ + + @ReactMethod + fun {{ func.name()|fn_name|unquote }}({%- call kt::arg_list_decl(func) -%}promise: Promise) { + executor.execute { + try { +{%- for arg in func.arguments() -%} + {%- match arg.type_() %} + {%- when Type::Enum(inner) %} + {%- let e = ci.get_enum_definition(inner).unwrap() %} + {%- if e.is_flat() %} + val {{arg.name()|var_name|unquote|temporary}} = as{{arg.type_()|type_name}}({{ arg.name()|var_name|unquote }}) + {%- else %} + val {{arg.name()|var_name|unquote|temporary}} = as{{arg.type_()|type_name}}({{ arg.name()|var_name|unquote }}) ?: run { throw LsSdkException.Generic(errMissingMandatoryField("{{arg.name()|var_name|unquote}}", "{{ arg.type_()|type_name }}")) } + {%- endif %} + {%- when Type::Optional(_) %} + val {{arg.name()|var_name|unquote|temporary}} = {{arg.name()|var_name|unquote}}{{ arg.type_()|rn_convert_type(ci) -}} + {%- when Type::Record(_) %} + val {{arg.type_()|type_name|var_name|unquote}} = as{{arg.type_()|type_name}}({{ arg.name()|var_name|unquote }}) ?: run { throw LsSdkException.Generic(errMissingMandatoryField("{{arg.name()|var_name|unquote}}", "{{ arg.type_()|type_name }}")) } + {%- else %} + {%- endmatch %} +{%- endfor %} +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + val res = {{ obj_interface }}{{ func.name()|fn_name|unquote }}({%- call kt::arg_list(func) -%}) + {%- match return_type %} + {%- when Type::Optional(inner) %} + {%- let unboxed = inner.as_ref() %} + promise.resolve(res?.let { {% call kt::return_value(unboxed) %} }) + {%- else %} + promise.resolve({% call kt::return_value(return_type) %}) + {%- endmatch %} +{%- when None %} + {{ obj_interface }}{{ func.name()|fn_name|unquote }}({%- call kt::arg_list(func) -%}) + promise.resolve(readableMapOf("status" to "ok")) +{%- endmatch %} + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/Types.kt b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/Types.kt new file mode 100644 index 000000000..2985efec3 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/Types.kt @@ -0,0 +1,30 @@ +{%- for type_ in ci.iter_types() -%} +{%- let type_name = type_|type_name -%} +{%- match type_ -%} +{%- when Type::Record ( name ) %} + {%- include "RecordTemplate.kt" %} +{%- when Type::Enum ( name ) %} + {%- include "EnumTemplate.kt" %} +{%- when Type::Object ( name ) %} + {% let obj = ci.get_object_definition(name).unwrap() -%} + {%- for func in obj.methods() -%} + {%- match func.return_type() -%} + {%- when Some with (return_type) -%} + {%- match return_type -%} + {%- when Type::Optional(inner) -%} + {%- let unboxed = inner.as_ref() -%} + {%- match unboxed -%} + {%- when Type::Sequence(inner_type) -%} + {{- self.add_sequence_type(inner_type|type_name) -}} + {%- else -%} + {%- endmatch -%} + {%- when Type::Sequence(inner_type) -%} + {{- self.add_sequence_type(inner_type|type_name) -}} + {%- else -%} + {%- endmatch -%} + {%- else -%} + {%- endmatch -%} + {% endfor -%} +{%- else -%} +{%- endmatch -%} +{%- endfor -%} \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/macros.kt b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/macros.kt new file mode 100644 index 000000000..c3b780003 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/macros.kt @@ -0,0 +1,36 @@ + +{% macro arg_list(func) %} + {%- for arg in func.arguments() -%} + {%- match arg.type_() -%} + {%- when Type::Enum(_) -%} + {{ arg.name()|var_name|unquote|temporary }} + {%- when Type::Optional(_) -%} + {{ arg.name()|var_name|unquote|temporary }} + {%- when Type::Record(_) -%} + {{ arg.type_()|type_name|var_name|unquote -}} + {%- else -%} + {{ arg.name()|var_name|unquote }}{{ arg.type_()|rn_convert_type(ci) -}} + {%- endmatch -%} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {{- arg.name()|var_name|unquote }}: {{ arg.type_()|rn_type_name(ci) -}}, {% endfor %} +{%- endmacro %} + +{%- macro field_list(rec) %} + {%- for f in rec.fields() %} + {{ f.name()|var_name|unquote }}, + {%- endfor %} +{%- endmacro -%} + +{% macro return_value(ret_type) %} + {%- match ret_type %} + {%- when Type::Enum(_) %}readableMapOf(res) + {%- when Type::Record(_) %}readableMapOf(res) + {%- when Type::Sequence(_) %}readableArrayOf(res) + {%- else %}res + {%- endmatch %} +{%- endmacro %} \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/mapper.kt b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/mapper.kt new file mode 100644 index 000000000..e08a14b85 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/mapper.kt @@ -0,0 +1,13 @@ +{%- import "macros.kt" as kt -%} +package com.lssdk +import ls_sdk.* +import com.facebook.react.bridge.* +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter +import java.io.File +import java.util.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +{%- include "Types.kt" %} + +{%- include "Helpers.kt" %} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/module.kt b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/module.kt new file mode 100644 index 000000000..ce6aa541d --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_kotlin/templates/module.kt @@ -0,0 +1,72 @@ +package com.lssdk + +import ls_sdk.* +import com.facebook.react.bridge.* +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter +import java.io.File +import java.util.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +{% import "macros.kt" as kt %} + +class LiquidSwapSDKModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + private lateinit var executor: ExecutorService + private var bindingWallet: BindingWallet? = null + + companion object { + const val TAG = "RNLiquidSwapSDK" + } + + override fun initialize() { + super.initialize() + + executor = Executors.newFixedThreadPool(3) + } + + override fun getName(): String { + return TAG + } + + @Throws(LsSdkException::class) + fun getBindingWallet(): BindingWallet { + if (bindingWallet != null) { + return bindingWallet!! + } + + throw LsSdkException.Generic("Not initialized") + } + + @ReactMethod + fun addListener(eventName: String) {} + + @ReactMethod + fun removeListeners(count: Int) {} + + {% let obj_interface = "" -%} + {% for func in ci.function_definitions() %} + {%- if func.name()|ignored_function == false -%} + {% include "TopLevelFunctionTemplate.kt" %} + {% endif -%} + {%- endfor %} + + @ReactMethod + fun initBindingWallet(mnemonic: String, dataDir: String, network: String, promise: Promise) { + if (bindingWallet != null) { + promise.reject("Generic", "Already initialized") + return + } + + executor.execute { + try { + val dataDirTmp = dataDir.takeUnless { it.isEmpty() } ?: run { reactApplicationContext.filesDir.toString() + "/lsSdk" } + val networkTmp = asNetwork(network) + bindingWallet = init(mnemonic, dataDirTmp, networkTmp) + promise.resolve(readableMapOf("status" to "ok")) + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } + {%- include "Objects.kt" %} +} + diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/mod.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/mod.rs new file mode 100644 index 000000000..0699fab85 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/mod.rs @@ -0,0 +1,388 @@ +use std::collections::HashSet; + +use askama::Template; +use once_cell::sync::Lazy; +use uniffi_bindgen::interface::*; + +use crate::generator::RNConfig; + +pub use uniffi_bindgen::bindings::swift::gen_swift::*; + +static IGNORED_FUNCTIONS: Lazy> = Lazy::new(|| { + let list: Vec<&str> = vec!["init"]; + HashSet::from_iter(list.into_iter().map(|s| s.to_string())) +}); + +#[derive(Template)] +#[template(syntax = "rn", escape = "none", path = "mapper.swift")] +pub struct MapperGenerator<'a> { + config: RNConfig, + ci: &'a ComponentInterface, +} + +impl<'a> MapperGenerator<'a> { + pub fn new(config: RNConfig, ci: &'a ComponentInterface) -> Self { + Self { config, ci } + } +} + +#[derive(Template)] +#[template(syntax = "rn", escape = "none", path = "extern.m")] +pub struct ExternGenerator<'a> { + config: RNConfig, + ci: &'a ComponentInterface, +} + +impl<'a> ExternGenerator<'a> { + pub fn new(config: RNConfig, ci: &'a ComponentInterface) -> Self { + Self { config, ci } + } +} + +#[derive(Template)] +#[template(syntax = "rn", escape = "none", path = "module.swift")] +pub struct ModuleGenerator<'a> { + config: RNConfig, + ci: &'a ComponentInterface, +} + +impl<'a> ModuleGenerator<'a> { + pub fn new(config: RNConfig, ci: &'a ComponentInterface) -> Self { + Self { config, ci } + } +} + +pub mod filters { + + use heck::*; + use uniffi_bindgen::backend::CodeOracle; + use uniffi_bindgen::backend::{CodeType, TypeIdentifier}; + + use super::*; + + fn oracle() -> &'static SwiftCodeOracle { + &SwiftCodeOracle + } + + pub fn type_name(codetype: &impl CodeType) -> Result { + Ok(codetype.type_label(oracle())) + } + + pub fn fn_name(nm: &str) -> Result { + Ok(oracle().fn_name(nm)) + } + + pub fn render_to_map( + t: &TypeIdentifier, + ci: &ComponentInterface, + obj_name: &str, + field_name: &str, + optional: bool, + ) -> Result { + let type_name = filters::type_name(t)?; + let type_name_str = type_name.as_str(); + let var_name = filters::unquote(filters::var_name(type_name_str)?.as_str())?; + let mut obj_prefix = "".to_string(); + if !obj_name.is_empty() { + obj_prefix = format!("{obj_name}."); + } + let mut optional_suffix = ""; + if optional { + optional_suffix = "!"; + } + let res: Result = match t { + Type::UInt8 => Ok(format!("{obj_prefix}{field_name}").into()), + Type::Int8 => Ok(format!("{obj_prefix}{field_name}").into()), + Type::UInt16 => Ok(format!("{obj_prefix}{field_name}").into()), + Type::Int16 => Ok(format!("{obj_prefix}{field_name}").into()), + Type::UInt32 => Ok(format!("{obj_prefix}{field_name}").into()), + Type::Int32 => Ok(format!("{obj_prefix}{field_name}").into()), + Type::UInt64 => Ok(format!("{obj_prefix}{field_name}").into()), + Type::Int64 => Ok(format!("{obj_prefix}{field_name}").into()), + Type::Float32 => Ok(format!("{obj_prefix}{field_name}").into()), + Type::Float64 => Ok(format!("{obj_prefix}{field_name}").into()), + Type::Boolean => Ok(format!("{obj_prefix}{field_name}").into()), + Type::String => Ok(format!("{obj_prefix}{field_name}").into()), + Type::Timestamp => unimplemented!("render_to_map: Timestamp is not implemented"), + Type::Duration => unimplemented!("render_to_map: Duration is not implemented"), + Type::Object(_) => unimplemented!("render_to_map: Object is not implemented"), + Type::Record(_) => Ok(format!( + "dictionaryOf({var_name}: {obj_prefix}{field_name}{optional_suffix})" + ) + .into()), + Type::Enum(inner) => { + let enum_def = ci.get_enum_definition(inner).unwrap(); + match enum_def.is_flat() { + true => Ok(format!( + "valueOf( {var_name}: {obj_prefix}{field_name}{optional_suffix})" + ) + .into()), + false => Ok(format!( + "dictionaryOf({var_name}: {obj_prefix}{field_name}{optional_suffix})" + ) + .into()), + } + } + Type::Error(_) => unimplemented!("render_to_map: Error is not implemented"), + Type::CallbackInterface(_) => { + unimplemented!("render_to_map: CallbackInterface is not implemented") + } + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + let inner_render = render_to_map(unboxed, ci, obj_name, field_name, true)?; + Ok(format!( + "{obj_prefix}{field_name} == nil ? nil : {inner_render}" + )) + } + Type::Sequence(inner) => { + let unboxed = inner.as_ref(); + let type_name = filters::type_name(unboxed)?; + let var_name = filters::var_name(type_name.as_str())?; + let var_name = filters::unquote(var_name.as_str())?; + let as_array_statment = match unboxed { + Type::Record(_) => format!( + "arrayOf({var_name}List: {obj_prefix}{field_name}{optional_suffix})" + ), + Type::Enum(_) => format!( + "arrayOf({var_name}List: {obj_prefix}{field_name}{optional_suffix})" + ), + _ => format!("{obj_prefix}{field_name}"), + }; + Ok(as_array_statment) + } + Type::Map(_, _) => unimplemented!("render_to_map: Map is not implemented"), + Type::External { .. } => { + unimplemented!("render_to_map: External is not implemented") + } + Type::Custom { .. } => { + unimplemented!("render_to_map: Custom is not implemented") + } + Type::Unresolved { .. } => { + unimplemented!("render_to_map: Unresolved is not implemented") + } + }; + res + } + + pub fn rn_convert_type( + t: &TypeIdentifier, + converted_var_name: &str, + ) -> Result { + match t { + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + let optional = match *unboxed { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 => { + format!("{} == 0 ? nil : {}", converted_var_name, converted_var_name) + } + Type::Float32 | Type::Float64 => format!( + "{} == 0.0 ? nil : {}", + converted_var_name, converted_var_name + ), + Type::String => format!( + "{}.isEmpty ? nil : {}", + converted_var_name, converted_var_name + ), + _ => "".to_string(), + }; + Ok(optional.to_string()) + } + _ => Ok(converted_var_name.to_string()), + } + } + + pub fn rn_return_type( + t: &TypeIdentifier, + name: &str, + optional: bool, + ) -> Result { + let mut optional_suffix = ""; + if optional { + optional_suffix = "!"; + } + match t { + Type::Enum(_) | Type::Record(_) => Ok(format!( + "LiquidSwapSDKMapper.dictionaryOf({}: res{})", + name, optional_suffix + )), + Type::Sequence(inner) => { + let unboxed = inner.as_ref(); + match unboxed { + Type::Enum(_) | Type::Record(_) => Ok(format!( + "LiquidSwapSDKMapper.arrayOf({}List: res{})", + name, optional_suffix + )), + _ => Ok(format!("res{}", optional_suffix)), + } + } + _ => Ok(format!("res{}", optional_suffix)), + } + } + + pub fn rn_type_name( + t: &TypeIdentifier, + ci: &ComponentInterface, + optional: bool, + ) -> Result { + let mut optional_suffix = ""; + if optional { + optional_suffix = "?"; + } + match t { + Type::Record(_) => Ok(format!("[String: Any{}]", optional_suffix)), + Type::Enum(inner) => { + let enum_def = ci.get_enum_definition(inner).unwrap(); + match enum_def.is_flat() { + false => Ok(format!("[String: Any{}]", optional_suffix)), + true => Ok("String".into()), + } + } + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + rn_type_name(unboxed, ci, optional) + } + Type::Sequence(inner) => { + let unboxed = inner.as_ref(); + Ok(format!("[{}]", rn_type_name(unboxed, ci, optional)?)) + } + t => { + let name = filters::type_name(t)?; + Ok(format!("{name}")) + } + } + } + + pub fn extern_type_name( + t: &TypeIdentifier, + ci: &ComponentInterface, + ) -> Result { + match t { + Type::Boolean => Ok("BOOL".to_string()), + Type::Int8 | Type::Int16 | Type::Int32 | Type::Int64 => Ok("NSInteger*".to_string()), + Type::UInt8 | Type::UInt16 | Type::UInt32 | Type::UInt64 => { + Ok("NSUInteger*".to_string()) + } + Type::Float32 | Type::Float64 => Ok("NSNumber*".to_string()), + Type::String => Ok("NSString*".to_string()), + Type::Enum(inner) => { + let enum_def = ci.get_enum_definition(inner).unwrap(); + match enum_def.is_flat() { + false => Ok("NSDictionary*".to_string()), + true => Ok("NSString*".to_string()), + } + } + Type::Record(_) => Ok("NSDictionary*".to_string()), + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + extern_type_name(unboxed, ci) + } + Type::Sequence(_) => Ok("NSArray*".to_string()), + _ => Ok("".to_string()), + } + } + + pub fn inline_optional_field( + t: &TypeIdentifier, + ci: &ComponentInterface, + ) -> Result { + match t { + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + inline_optional_field(unboxed, ci) + } + _ => { + let mapped_name = filters::rn_type_name(t, ci, true)?; + let type_name = filters::type_name(t)?; + Ok(mapped_name == type_name) + } + } + } + + pub fn render_from_map( + t: &TypeIdentifier, + ci: &ComponentInterface, + map_var_name: &str, + ) -> Result { + let res: String = match t { + Type::UInt8 => format!("{map_var_name}").into(), + Type::Int8 => format!("{map_var_name}").into(), + Type::UInt16 => format!("{map_var_name}").into(), + Type::Int16 => format!("{map_var_name}").into(), + Type::UInt32 => format!("{map_var_name}").into(), + Type::Int32 => format!("{map_var_name}").into(), + Type::UInt64 => format!("{map_var_name}").into(), + Type::Int64 => format!("{map_var_name}").into(), + Type::Float32 => format!("{map_var_name}").into(), + Type::Float64 => format!("{map_var_name}").into(), + Type::Boolean => format!("{map_var_name}").into(), + Type::String => format!("{map_var_name}").into(), + Type::Timestamp => "".into(), + Type::Duration => "".into(), + Type::Object(_) => "".into(), + Type::Record(_) => { + let record_type_name = type_name(t)?; + let record_var_name = var_name(&record_type_name)?; + let record_unquoted_name = unquote(&record_var_name)?; + format!("try as{record_type_name}({record_unquoted_name}: {map_var_name})") + } + Type::Enum(inner) => { + let enum_def = ci.get_enum_definition(inner).unwrap(); + let enum_var_name = var_name(&inner)?; + let enum_unquoted_name = unquote(&enum_var_name)?; + match enum_def.is_flat() { + false => format!("try as{inner}({enum_unquoted_name}: {map_var_name})").into(), + true => format!("try as{inner}({enum_unquoted_name}: {map_var_name})").into(), + } + } + Type::Error(_) => "".into(), + Type::CallbackInterface(_) => "".into(), + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + let inner_res = render_from_map(unboxed, ci, map_var_name)?; + inner_res + } + Type::Sequence(inner) => { + let unboxed = inner.as_ref(); + let element_type_name = type_name(unboxed)?; + match unboxed { + Type::Enum(_) | Type::Record(_) => { + format!("try as{element_type_name}List(arr: {map_var_name})") + } + _ => format!("{map_var_name}").into(), + } + } + Type::Map(_, _) => "".into(), + Type::External { .. } => "".into(), + Type::Custom { .. } => "".into(), + Type::Unresolved { .. } => "".into(), + }; + Ok(res.to_string()) + } + + pub fn var_name(nm: &str) -> Result { + Ok(format!("`{}`", nm.to_string().to_lower_camel_case())) + } + + pub fn unquote(nm: &str) -> Result { + Ok(nm.trim_matches('`').to_string()) + } + + pub fn ignored_function(nm: &str) -> Result { + Ok(IGNORED_FUNCTIONS.contains(nm)) + } + + pub fn list_arg(nm: &str) -> Result { + Ok(format!("`{nm}List`")) + } + + pub fn temporary(nm: &str) -> Result { + Ok(format!("{nm}Tmp")) + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/EnumTemplate.swift b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/EnumTemplate.swift new file mode 100644 index 000000000..7963f8b5f --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/EnumTemplate.swift @@ -0,0 +1,115 @@ +{%- let e = ci.get_enum_definition(name).unwrap() %} +{%- if e.is_flat() %} + +static func as{{ type_name }}({{ type_name|var_name|unquote }}: String) throws -> {{ type_name }} { + switch({{ type_name|var_name|unquote }}) { + {%- for variant in e.variants() %} + + case "{{variant.name()|var_name|unquote}}": + return {{ type_name }}.{{variant.name()|var_name|unquote}} + + {%- endfor %} + + default: throw LsSdkError.Generic(message: "Invalid variant \({{ type_name|var_name|unquote }}) for enum {{ type_name }}") + } +} + +static func valueOf({{ type_name|var_name|unquote }}: {{ type_name }}) -> String { + switch({{ type_name|var_name|unquote }}) { + {%- for variant in e.variants() %} + + case .{{variant.name()|var_name|unquote}}: + return "{{variant.name()|var_name|unquote}}" + + {%- endfor %} + + } +} + +static func arrayOf({{ type_name|var_name|unquote|list_arg }}: [{{ type_name }}]) -> [String] { + return {{ type_name|var_name|unquote|list_arg }}.map { (v) -> String in return valueOf({{ type_name|var_name|unquote }}: v) } +} + +{%- else %} + +static func as{{ type_name }}({{ type_name|var_name|unquote }}: [String: Any?]) throws -> {{ type_name }} { + let type = {{ type_name|var_name|unquote }}["type"] as! String + + {%- for variant in e.variants() %} + if (type == "{{ variant.name()|var_name|unquote }}") { + {%- if variant.has_fields() %} + {% let field = variant.fields()[0] %} + {%- match field.type_() %} + {%- when Type::Optional(_) %} + {% if field.type_()|inline_optional_field(ci) -%} + let _{{field.name()|var_name|unquote}} = {{ type_name|var_name|unquote }}["{{field.name()|var_name|unquote}}"] as? {{field.type_()|rn_type_name(ci, true)}} + {% else -%} + var _{{field.name()|var_name|unquote}}: {{field.type_()|type_name}} + if let {{field.name()|var_name|unquote|temporary}} = {{ type_name|var_name|unquote }}["{{field.name()|var_name|unquote}}"] as? {{field.type_()|rn_type_name(ci, true)}} { + _{{field.name()|var_name|unquote}} = {{field.type_()|render_from_map(ci, field.name()|var_name|unquote|temporary)}} + } + {% endif -%} + {%- else %} + {% if field.type_()|inline_optional_field(ci) -%} + guard let _{{field.name()|var_name|unquote}} = {{ type_name|var_name|unquote }}["{{field.name()|var_name|unquote}}"] as? {{field.type_()|rn_type_name(ci, true)}} else { + throw LsSdkError.Generic(message: errMissingMandatoryField(fieldName: "{{field.name()|var_name|unquote}}", typeName: "{{ type_name }}")) + } + {%- else -%} + guard let {{field.name()|var_name|unquote|temporary}} = {{ type_name|var_name|unquote }}["{{field.name()|var_name|unquote}}"] as? {{field.type_()|rn_type_name(ci, true)}} else { + throw LsSdkError.Generic(message: errMissingMandatoryField(fieldName: "{{field.name()|var_name|unquote}}", typeName: "{{ type_name }}")) + } + let _{{field.name()|var_name|unquote}} = {{field.type_()|render_from_map(ci, field.name()|var_name|unquote|temporary)}} + {% endif -%} + {% endmatch %} + return {{ type_name }}.{{ variant.name()|var_name|unquote }}({{ variant.fields()[0].name()|var_name|unquote }}: _{{field.name()|var_name|unquote}}) + {%- else %} + return {{ type_name }}.{{ variant.name()|var_name|unquote }} + {%- endif %} + } + {%- endfor %} + + throw LsSdkError.Generic(message: "Unexpected type \(type) for enum {{ type_name }}") +} + +static func dictionaryOf({{ type_name|var_name|unquote }}: {{ type_name }}) -> [String: Any?] { + switch ({{ type_name|var_name|unquote }}) { + {%- for variant in e.variants() %} + {% if variant.has_fields() %} + case let .{{ variant.name()|var_name|unquote }}( + {% for f in variant.fields() %}{{f.name()|var_name|unquote}}{%- if !loop.last %}, {% endif -%}{%- endfor %} + ): + {% else %} + case .{{ variant.name()|var_name|unquote }}: + {% endif -%} + return [ + "type": "{{ variant.name()|var_name|unquote }}", + {%- for f in variant.fields() %} + "{{ f.name()|var_name|unquote }}": {{ f.type_()|render_to_map(ci,"",f.name()|var_name|unquote, false) }}, + {%- endfor %} + ] + {%- endfor %} + } +} + +static func arrayOf({{ type_name|var_name|unquote|list_arg }}: [{{ type_name }}]) -> [Any] { + return {{ type_name|var_name|unquote|list_arg }}.map { (v) -> [String: Any?] in return dictionaryOf({{ type_name|var_name|unquote }}: v) } +} + +{%- endif %} + +static func as{{ type_name }}List(arr: [Any]) throws -> [{{ type_name }}] { + var list = [{{ type_name }}]() + for value in arr { +{%- if e.is_flat() %} + if let val = value as? String { +{%- else %} + if let val = value as? [String: Any?] { + {%- endif %} + var {{ type_name|var_name|unquote }} = try as{{ type_name }}({{ type_name|var_name|unquote }}: val) + list.append({{ type_name|var_name|unquote }}) + } else { + throw LsSdkError.Generic(message: errUnexpectedType(typeName: "{{ type_name }}")) + } + } + return list +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/ExternFunctionTemplate.m b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/ExternFunctionTemplate.m new file mode 100644 index 000000000..30f00a2f7 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/ExternFunctionTemplate.m @@ -0,0 +1,16 @@ + +RCT_EXTERN_METHOD( +{%- if func.arguments().len() == 0 %} + {{ func.name()|fn_name|unquote }}: (RCTPromiseResolveBlock)resolve{# -#} +{% else -%} + {%- for arg in func.arguments() %} + {%- if loop.first %} + {{ func.name()|fn_name|unquote }}: ({{arg.type_()|extern_type_name(ci)}}){{ arg.name()|var_name|unquote }} + {%- else %} + {{ arg.name()|var_name|unquote }}: ({{arg.type_()|extern_type_name(ci)}}){{ arg.name()|var_name|unquote }} + {%- endif -%} + {% endfor %} + resolve: (RCTPromiseResolveBlock)resolve +{%- endif %} + reject: (RCTPromiseRejectBlock)reject +) \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/Helpers.swift b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/Helpers.swift new file mode 100644 index 000000000..83de8abfd --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/Helpers.swift @@ -0,0 +1,20 @@ + + static func hasNonNilKey(data: [String: Any?], key: String) -> Bool { + if let val = data[key] { + return !(val == nil || val is NSNull) + } + + return false + } + + static func errMissingMandatoryField(fieldName: String, typeName: String) -> String { + return "Missing mandatory field \(fieldName) for type \(typeName)" + } + + static func errUnexpectedType(typeName: String) -> String { + return "Unexpected type \(typeName)" + } + + static func errUnexpectedValue(fieldName: String) -> String { + return "Unexpected value for optional field \(fieldName)" + } diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/Objects.swift b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/Objects.swift new file mode 100644 index 000000000..95a57fba5 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/Objects.swift @@ -0,0 +1,13 @@ +{%- for type_ in ci.iter_types() %} +{%- let type_name = type_|type_name %} +{%- match type_ %} +{%- when Type::Object ( name ) %} +{% let obj = ci.get_object_definition(name).unwrap() %} +{% let obj_interface = "getBindingWallet()." %} +{%- for func in obj.methods() -%} +{%- include "TopLevelFunctionTemplate.swift" %} +{% endfor %} +{%- else -%} +{%- endmatch -%} +{%- endfor %} + diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/RecordTemplate.swift b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/RecordTemplate.swift new file mode 100644 index 000000000..ce38ced60 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/RecordTemplate.swift @@ -0,0 +1,59 @@ +{%- let rec = ci.get_record_definition(name).unwrap() %} +static func as{{ type_name }}({{ type_name|var_name|unquote }}: [String: Any?]) throws -> {{ type_name }} { + {%- for field in rec.fields() %} + {%- match field.type_() %} + {%- when Type::Optional(_) %} + var {{field.name()|var_name|unquote}}: {{field.type_()|type_name}} + {% if field.type_()|inline_optional_field(ci) -%} + if hasNonNilKey(data: {{ type_name|var_name|unquote }}, key: "{{field.name()|var_name|unquote}}") { + guard let {{field.name()|var_name|unquote|temporary}} = {{ type_name|var_name|unquote }}["{{field.name()|var_name|unquote}}"] as? {{field.type_()|rn_type_name(ci, true)}} else { + throw LsSdkError.Generic(message: errUnexpectedValue(fieldName: "{{field.name()|var_name|unquote}}")) + } + {{field.name()|var_name|unquote}} = {{field.name()|var_name|unquote|temporary}} + } + {%- else -%} + if let {{field.name()|var_name|unquote|temporary}} = {{ type_name|var_name|unquote }}["{{field.name()|var_name|unquote}}"] as? {{field.type_()|rn_type_name(ci, true)}} { + {{field.name()|var_name|unquote}} = {{field.type_()|render_from_map(ci, field.name()|var_name|unquote|temporary)}} + } + {% endif -%} + {%- else %} + {% if field.type_()|inline_optional_field(ci) -%} + guard let {{field.name()|var_name|unquote}} = {{ type_name|var_name|unquote }}["{{field.name()|var_name|unquote}}"] as? {{field.type_()|rn_type_name(ci, true)}} else { + throw LsSdkError.Generic(message: errMissingMandatoryField(fieldName: "{{field.name()|var_name|unquote}}", typeName: "{{ type_name }}")) + } + {%- else -%} + guard let {{field.name()|var_name|unquote|temporary}} = {{ type_name|var_name|unquote }}["{{field.name()|var_name|unquote}}"] as? {{field.type_()|rn_type_name(ci, true)}} else { + throw LsSdkError.Generic(message: errMissingMandatoryField(fieldName: "{{field.name()|var_name|unquote}}", typeName: "{{ type_name }}")) + } + let {{field.name()|var_name|unquote}} = {{field.type_()|render_from_map(ci, field.name()|var_name|unquote|temporary)}} + {% endif -%} + {% endmatch %} + {%- endfor %} + + return {{ type_name }}({%- call swift::field_list(rec) -%}) +} + +static func dictionaryOf({{ type_name|var_name|unquote }}: {{ type_name }}) -> [String: Any?] { + return [ + {%- for field in rec.fields() %} + "{{ field.name()|var_name|unquote }}": {{ field.type_()|render_to_map(ci,type_name|var_name|unquote,field.name()|var_name|unquote,false)}}, + {%- endfor %} + ] +} + +static func as{{ type_name }}List(arr: [Any]) throws -> [{{ type_name }}] { + var list = [{{ type_name }}]() + for value in arr { + if let val = value as? [String: Any?] { + var {{ type_name|var_name|unquote }} = try as{{ type_name }}({{ type_name|var_name|unquote }}: val) + list.append({{ type_name|var_name|unquote }}) + } else { + throw LsSdkError.Generic(message: errUnexpectedType(typeName: "{{ type_name }}")) + } + } + return list +} + +static func arrayOf({{ type_name|var_name|unquote|list_arg }}: [{{ type_name }}]) -> [Any] { + return {{ type_name|var_name|unquote|list_arg }}.map { (v) -> [String: Any?] in return dictionaryOf({{ type_name|var_name|unquote }}: v) } +} \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/TopLevelFunctionTemplate.swift b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/TopLevelFunctionTemplate.swift new file mode 100644 index 000000000..7bab4637b --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/TopLevelFunctionTemplate.swift @@ -0,0 +1,42 @@ + + @objc({%- call swift::extern_arg_list(func) -%}) + func {{ func.name()|fn_name|unquote }}(_ {% call swift::arg_list_decl(func) -%}resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + do { +{%- for arg in func.arguments() -%} + {%- match arg.type_() %} + {%- when Type::Enum(inner) %} + {%- let e = ci.get_enum_definition(inner).unwrap() %} + {%- if e.is_flat() %} + let {{arg.name()|var_name|unquote|temporary}} = try LiquidSwapSDKMapper.as{{arg.type_()|type_name}}({{ arg.type_()|type_name|var_name|unquote }}: {{ arg.name()|var_name|unquote }}) + {%- else %} + let {{arg.name()|var_name|unquote|temporary}} = try LiquidSwapSDKMapper.as{{arg.type_()|type_name}}({{ arg.type_()|type_name|var_name|unquote }}: {{ arg.name()|var_name|unquote }}) + {%- endif %} + {%- when Type::Optional(_) %} + let {{arg.name()|var_name|unquote|temporary}} = {{ arg.type_()|rn_convert_type(arg.name()|var_name|unquote) -}} + {%- when Type::Record(_) %} + let {{arg.type_()|type_name|var_name|unquote}} = try LiquidSwapSDKMapper.as{{arg.type_()|type_name}}({{ arg.type_()|type_name|var_name|unquote }}: {{ arg.name()|var_name|unquote }}) + {%- else %} + {%- endmatch %} +{%- endfor %} +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + var res = {%- call swift::throws_decl(func) -%}{{ obj_interface }}{{ func.name()|fn_name|unquote }}({%- call swift::arg_list(func) -%}) + {%- match return_type %} + {%- when Type::Optional(inner) %} + {%- let unboxed = inner.as_ref() %} + if res != nil { + resolve({{ unboxed|rn_return_type(unboxed|type_name|var_name|unquote, true) }}) + } else { + resolve(nil) + } + {%- else %} + resolve({{ return_type|rn_return_type(return_type|type_name|var_name|unquote, false) }}) + {%- endmatch %} +{%- when None %} + try {{ obj_interface }}{{ func.name()|fn_name|unquote }}({%- call swift::arg_list(func) -%}) + resolve(["status": "ok"]) +{%- endmatch %} + } catch let err { + rejectErr(err: err, reject: reject) + } + } \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/Types.swift b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/Types.swift new file mode 100644 index 000000000..d82cceb69 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/Types.swift @@ -0,0 +1,11 @@ +{%- for type_ in ci.iter_types() %} +{%- let type_name = type_|type_name %} +{%- match type_ %} +{%- when Type::Record ( name ) %} + {%- include "RecordTemplate.swift" %} +{%- when Type::Enum ( name ) %} + {%- include "EnumTemplate.swift" %} +{%- else %} +{%- endmatch -%} + +{%- endfor %} \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/extern.m b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/extern.m new file mode 100644 index 000000000..27d286369 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/extern.m @@ -0,0 +1,28 @@ +#import +#import + +@interface RCT_EXTERN_MODULE(RNLiquidSwapSDK, RCTEventEmitter) +{% for func in ci.function_definitions() %} +{%- if func.name()|ignored_function == false -%} +{% include "ExternFunctionTemplate.m" %} +{% endif %} +{%- endfor %} +RCT_EXTERN_METHOD( + initBindingWallet: (NSString*)mnemonic + dataDir: (NSString*)dataDir + network: (NSString*)network + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) +{%- for type_ in ci.iter_types() %} +{%- let type_name = type_|type_name %} +{%- match type_ %} +{%- when Type::Object ( name ) %} +{% let obj = ci.get_object_definition(name).unwrap() %} +{%- for func in obj.methods() -%} +{%- include "ExternFunctionTemplate.m" %} +{% endfor %} +{%- else -%} +{%- endmatch -%} +{%- endfor %} +@end diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/macros.swift b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/macros.swift new file mode 100644 index 000000000..67598a929 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/macros.swift @@ -0,0 +1,42 @@ +{% macro arg_list(func) %} + {%- for arg in func.arguments() -%} + {%- match arg.type_() -%} + {%- when Type::Enum(_) -%} + {{ arg.name()|var_name|unquote }}: {{ arg.name()|var_name|unquote|temporary -}} + {%- when Type::Optional(_) -%} + {{ arg.name()|var_name|unquote }}: {{ arg.name()|var_name|unquote|temporary -}} + {%- when Type::Record(_) -%} + {{ arg.name()|var_name|unquote }}: {{ arg.type_()|type_name|var_name|unquote -}} + {%- else -%} + {{ arg.name()|var_name|unquote }}: {{ arg.name()|var_name|unquote -}} + {%- endmatch -%} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {{- arg.name()|var_name|unquote }}: {{ arg.type_()|rn_type_name(ci, false) -}}, {% endfor %} +{%- endmacro %} + +{% macro extern_arg_list(func) %} + {{- func.name()|var_name|unquote -}}: + {%- for arg in func.arguments() -%} + {%- if !loop.first -%} + {{- arg.name()|var_name|unquote }}: + {%- endif -%} + {%- endfor %} + {%- if func.arguments().len() >= 1 -%}resolve:{%- endif -%}reject: +{%- endmacro %} + +{%- macro field_list(rec) %} + {%- for f in rec.fields() %} + {{ f.name()|var_name|unquote }}: {{ f.name()|var_name|unquote }}{%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro -%} + +{%- macro throws_decl(func) %} + {%- match func.throws_type() -%} + {%- when Some with (throws_type) -%}try {% else -%} + {%- endmatch -%} +{%- endmacro -%} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/mapper.swift b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/mapper.swift new file mode 100644 index 000000000..e768441e3 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/mapper.swift @@ -0,0 +1,11 @@ +{%- import "macros.swift" as swift -%} +import Foundation +import LiquidSwapSDK + +enum LiquidSwapSDKMapper { + +{%- include "Types.swift" %} + +{%- include "Helpers.swift" %} + +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/module.swift b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/module.swift new file mode 100644 index 000000000..e05c5c88c --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_swift/templates/module.swift @@ -0,0 +1,91 @@ +import Foundation +import LiquidSwapSDK + +@objc(RNLiquidSwapSDK) +class RNLiquidSwapSDK: RCTEventEmitter { + static let TAG: String = "LiquidSwapSDK" + + public static var emitter: RCTEventEmitter! + public static var hasListeners: Bool = false + + private var bindingWallet: BindingWallet! + + static var defaultDataDir: URL { + let applicationDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + + return applicationDirectory.appendingPathComponent("lsSdk", isDirectory: true) + } + + override init() { + super.init() + RNLiquidSwapSDK.emitter = self + } + + @objc + override static func moduleName() -> String! { + TAG + } + + override func supportedEvents() -> [String]! { + return [] + } + + override func startObserving() { + RNLiquidSwapSDK.hasListeners = true + } + + override func stopObserving() { + RNLiquidSwapSDK.hasListeners = false + } + + @objc + override static func requiresMainQueueSetup() -> Bool { + return false + } + + func getBindingWallet() throws -> BindingWallet { + if bindingWallet != nil { + return bindingWallet + } + + throw LsSdkError.Generic(message: "Not initialized") + } + + {% let obj_interface = "LiquidSwapSDK." -%} + {% for func in ci.function_definitions() %} + {%- if func.name()|ignored_function == false -%} + {% include "TopLevelFunctionTemplate.swift" %} + {% endif -%} + {%- endfor %} + @objc(initBindingWallet:dataDir:network:resolve:reject:) + func initBindingWallet(_ mnemonic: String, dataDir: String, network: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + if bindingWallet != nil { + reject("Generic", "Already initialized", nil) + return + } + + do { + let dataDirTmp = dataDir.isEmpty ? RNLiquidSwapSDK.defaultDataDir.path : dataDir + let networkTmp = try LiquidSwapSDKMapper.asNetwork(network: network) + bindingWallet = try LiquidSwapSDK.`init`(mnemonic: mnemonic, dataDir: dataDirTmp, network: networkTmp) + resolve(["status": "ok"]) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + {%- include "Objects.swift" %} + + func rejectErr(err: Error, reject: @escaping RCTPromiseRejectBlock) { + var errorName = "Generic" + var message = "\(err)" + if let errAssociated = Mirror(reflecting: err).children.first { + errorName = errAssociated.label ?? errorName + if let associatedMessage = Mirror(reflecting: errAssociated.value).children.first { + message = associatedMessage.value as! String + } + } + reject(errorName, message, err) + } +} + +{% import "macros.swift" as swift %} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/callback_interface.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/callback_interface.rs new file mode 100644 index 000000000..e3cb462de --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/callback_interface.rs @@ -0,0 +1,33 @@ +/* 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 uniffi_bindgen::backend::{CodeOracle, CodeType, Literal}; + +pub struct CallbackInterfaceCodeType { + id: String, +} + +impl CallbackInterfaceCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for CallbackInterfaceCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/compounds.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/compounds.rs new file mode 100644 index 000000000..8ce2037a8 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/compounds.rs @@ -0,0 +1,94 @@ +/* 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 paste::paste; +use uniffi_bindgen::backend::{CodeOracle, CodeType, Literal, TypeIdentifier}; + +fn render_literal(oracle: &dyn CodeOracle, literal: &Literal, inner: &TypeIdentifier) -> String { + match literal { + Literal::Null => "null".into(), + Literal::EmptySequence => "[]".into(), + Literal::EmptyMap => "{}".into(), + + // For optionals + _ => oracle.find(inner).literal(oracle, literal), + } +} + +macro_rules! impl_code_type_for_compound { + ($T:ty, $type_label_pattern:literal, $canonical_name_pattern: literal) => { + paste! { + pub struct $T { + inner: TypeIdentifier, + } + + impl $T { + pub fn new(inner: TypeIdentifier) -> Self { + Self { inner } + } + fn inner(&self) -> &TypeIdentifier { + &self.inner + } + } + + impl CodeType for $T { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + format!($type_label_pattern, oracle.find(self.inner()).type_label(oracle)) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!($canonical_name_pattern, oracle.find(self.inner()).canonical_name(oracle)) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, literal, self.inner()) + } + } + } + } + } + +impl_code_type_for_compound!(OptionalCodeType, "{}?", "Optional{}"); +impl_code_type_for_compound!(SequenceCodeType, "{}[]", "Sequence{}"); + +pub struct MapCodeType { + key: TypeIdentifier, + value: TypeIdentifier, +} + +impl MapCodeType { + pub fn new(key: TypeIdentifier, value: TypeIdentifier) -> Self { + Self { key, value } + } + + fn key(&self) -> &TypeIdentifier { + &self.key + } + + fn value(&self) -> &TypeIdentifier { + &self.value + } +} + +impl CodeType for MapCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Record<{}, {}>", + self.key().type_label(oracle), + self.value().type_label(oracle), + ) + } + + fn canonical_name(&self, oracle: &dyn CodeOracle) -> String { + format!( + "Record{}{}", + self.key().type_label(oracle), + self.value().type_label(oracle), + ) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, literal, &self.value) + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/custom.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/custom.rs new file mode 100644 index 000000000..b5209d0de --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/custom.rs @@ -0,0 +1,29 @@ +/* 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 uniffi_bindgen::backend::{CodeOracle, CodeType}; + +pub struct CustomCodeType { + name: String, +} + +impl CustomCodeType { + pub fn new(name: String) -> Self { + Self { name } + } +} + +impl CodeType for CustomCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + self.name.clone() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.name) + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/enum_.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/enum_.rs new file mode 100644 index 000000000..418284fc7 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/enum_.rs @@ -0,0 +1,37 @@ +/* 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 uniffi_bindgen::backend::{CodeOracle, CodeType, Literal}; + +pub struct EnumCodeType { + id: String, +} + +impl EnumCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for EnumCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + if let Literal::Enum(v, _) = literal { + format!( + "{}.{}", + self.type_label(oracle), + oracle.enum_variant_name(v) + ) + } else { + unreachable!(); + } + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/error.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/error.rs new file mode 100644 index 000000000..86168a732 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/error.rs @@ -0,0 +1,29 @@ +/* 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 uniffi_bindgen::backend::{CodeOracle, CodeType, Literal}; + +pub struct ErrorCodeType { + id: String, +} + +impl ErrorCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for ErrorCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.error_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/external.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/external.rs new file mode 100644 index 000000000..9b7d15604 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/external.rs @@ -0,0 +1,29 @@ +/* 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 uniffi_bindgen::backend::{CodeOracle, CodeType}; + +pub struct ExternalCodeType { + name: String, +} + +impl ExternalCodeType { + pub fn new(name: String) -> Self { + Self { name } + } +} + +impl CodeType for ExternalCodeType { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + self.name.clone() + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.name) + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.into() + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/miscellany.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/miscellany.rs new file mode 100644 index 000000000..ea1d07c97 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/miscellany.rs @@ -0,0 +1,30 @@ +use paste::paste; +use uniffi_bindgen::backend::{CodeOracle, CodeType, Literal}; + +macro_rules! impl_code_type_for_miscellany { + ($T:ty, $canonical_name:literal) => { + paste! { + pub struct $T; + + impl CodeType for $T { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + format!("{}", $canonical_name) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("{}", $canonical_name) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!() + } + + fn coerce(&self, _oracle: &dyn CodeOracle, nm: &str) -> String { + nm.to_string() + } + } + } + }; +} + +impl_code_type_for_miscellany!(TimestampCodeType, "Date"); diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/mod.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/mod.rs new file mode 100644 index 000000000..26168bc9c --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/mod.rs @@ -0,0 +1,235 @@ +use std::collections::HashSet; + +use askama::Template; +use heck::{ToLowerCamelCase, ToShoutySnakeCase, ToUpperCamelCase}; +use once_cell::sync::Lazy; +use uniffi_bindgen::backend::{CodeOracle, CodeType, TypeIdentifier}; +use uniffi_bindgen::interface::*; + +use crate::generator::RNConfig; + +mod callback_interface; +mod compounds; +mod custom; +mod enum_; +mod error; +mod external; +mod miscellany; +mod object; +mod primitives; +mod record; + +// Keywords to fix +static KEYWORDS: Lazy> = Lazy::new(|| { + let list = vec!["Function", "Number", "Object", "Record", "String", "Symbol"]; + HashSet::from_iter(list.into_iter().map(|s| s.to_string())) +}); + +static IGNORED_FUNCTIONS: Lazy> = Lazy::new(|| { + let list: Vec<&str> = vec!["init"]; + HashSet::from_iter(list.into_iter().map(|s| s.to_string())) +}); + +#[derive(Template)] +#[template(syntax = "rn", escape = "none", path = "module.ts")] +pub struct ModuleGenerator<'a> { + config: RNConfig, + ci: &'a ComponentInterface, +} + +impl<'a> ModuleGenerator<'a> { + pub fn new(config: RNConfig, ci: &'a ComponentInterface) -> Self { + Self { config, ci } + } +} + +fn fixup_keyword(name: String, append: String) -> String { + if KEYWORDS.contains(&name) { + format!("{name}{append}") + } else { + name + } +} + +#[derive(Clone)] +pub struct TypescriptCodeOracle; + +impl TypescriptCodeOracle { + // Map `Type` instances to a `Box` for that type. + // + // There is a companion match in `templates/Types.ts` which performs a similar function for the + // template code. + // + // - When adding additional types here, make sure to also add a match arm to the `Types.ts` template. + // - To keep things managable, let's try to limit ourselves to these 2 mega-matches + fn create_code_type(&self, type_: TypeIdentifier) -> Box { + match type_ { + Type::UInt8 => Box::new(primitives::UInt8CodeType), + Type::Int8 => Box::new(primitives::Int8CodeType), + Type::UInt16 => Box::new(primitives::UInt16CodeType), + Type::Int16 => Box::new(primitives::Int16CodeType), + Type::UInt32 => Box::new(primitives::UInt32CodeType), + Type::Int32 => Box::new(primitives::Int32CodeType), + Type::UInt64 => Box::new(primitives::UInt64CodeType), + Type::Int64 => Box::new(primitives::Int64CodeType), + Type::Float32 => Box::new(primitives::Float32CodeType), + Type::Float64 => Box::new(primitives::Float64CodeType), + Type::Boolean => Box::new(primitives::BooleanCodeType), + Type::String => Box::new(primitives::StringCodeType), + + Type::Timestamp => Box::new(miscellany::TimestampCodeType), + Type::Duration => { + unimplemented!("Duration is not implemented") + } + + Type::Enum(id) => Box::new(enum_::EnumCodeType::new(id)), + Type::Object(id) => Box::new(object::ObjectCodeType::new(id)), + Type::Record(id) => Box::new(record::RecordCodeType::new(id)), + Type::Error(id) => Box::new(error::ErrorCodeType::new(id)), + Type::CallbackInterface(id) => { + Box::new(callback_interface::CallbackInterfaceCodeType::new(id)) + } + Type::Optional(inner) => Box::new(compounds::OptionalCodeType::new(*inner)), + Type::Sequence(inner) => Box::new(compounds::SequenceCodeType::new(*inner)), + Type::Map(key, value) => Box::new(compounds::MapCodeType::new(*key, *value)), + Type::External { name, .. } => Box::new(external::ExternalCodeType::new(name)), + Type::Custom { name, .. } => Box::new(custom::CustomCodeType::new(name)), + + Type::Unresolved { name } => { + unreachable!("Type `{name}` must be resolved before calling create_code_type") + } + } + } +} + +impl CodeOracle for TypescriptCodeOracle { + fn find(&self, type_: &TypeIdentifier) -> Box { + self.create_code_type(type_.clone()) + } + + /// Get the idiomatic Typescript rendering of a class name (for enums, records, errors, etc). + fn class_name(&self, nm: &str) -> String { + fixup_keyword(nm.to_string().to_upper_camel_case(), "Type".to_string()) + } + + /// Get the idiomatic Typescript rendering of a function name. + fn fn_name(&self, nm: &str) -> String { + fixup_keyword(nm.to_string().to_lower_camel_case(), "Fn".to_string()) + } + + /// Get the idiomatic Typescript rendering of a variable name. + fn var_name(&self, nm: &str) -> String { + fixup_keyword(nm.to_string().to_lower_camel_case(), "Var".to_string()) + } + + /// Get the idiomatic Typescript rendering of an individual enum variant. + fn enum_variant_name(&self, nm: &str) -> String { + fixup_keyword(nm.to_string().to_shouty_snake_case(), "Enum".to_string()) + } + + /// Get the idiomatic Typescript rendering of an exception name + fn error_name(&self, nm: &str) -> String { + self.class_name(nm) + } + + fn ffi_type_label(&self, ffi_type: &FfiType) -> String { + match ffi_type { + FfiType::Int8 + | FfiType::UInt8 + | FfiType::Int16 + | FfiType::UInt16 + | FfiType::Int32 + | FfiType::UInt32 + | FfiType::Int64 + | FfiType::UInt64 + | FfiType::Float32 + | FfiType::Float64 => "number".to_string(), + FfiType::RustArcPtr(name) => format!("{}SafeHandle", name), + FfiType::RustBuffer(_) => "RustBuffer".to_string(), + FfiType::ForeignBytes => "ForeignBytes".to_string(), + FfiType::ForeignCallback => "ForeignCallback".to_string(), + } + } +} + +pub mod filters { + use uniffi_bindgen::backend::CodeType; + + use super::*; + + fn oracle() -> &'static TypescriptCodeOracle { + &TypescriptCodeOracle + } + + pub fn type_name(codetype: &impl CodeType) -> Result { + Ok(codetype.type_label(oracle())) + } + + /// Get the idiomatic Typescript rendering of a function name. + pub fn fn_name(nm: &str) -> Result { + Ok(oracle().fn_name(nm)) + } + + /// Get the idiomatic Typescript rendering of a variable name. + pub fn var_name(nm: &str) -> Result { + Ok(oracle().var_name(nm)) + } + + /// Get the idiomatic Typescript rendering of an individual enum variant. + pub fn enum_variant(nm: &str) -> Result { + Ok(oracle().enum_variant_name(nm)) + } + + pub fn absolute_type_name(t: &TypeIdentifier) -> Result { + let res: Result = match t { + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + type_name(unboxed) + } + _ => type_name(t), + }; + res + } + + pub fn return_type_name(t: &TypeIdentifier) -> Result { + let res: Result = match t { + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + let name = type_name(unboxed)?; + Ok(format!("{name} | null")) + } + _ => type_name(t), + }; + res + } + + pub fn default_value(t: &TypeIdentifier) -> Result { + let res: Result = match t { + Type::Optional(inner) => { + let unboxed = inner.as_ref(); + match unboxed { + Type::UInt8 + | Type::Int8 + | Type::UInt16 + | Type::Int16 + | Type::UInt32 + | Type::Int32 + | Type::UInt64 + | Type::Int64 + | Type::Float32 + | Type::Float64 => Ok(" = 0".into()), + Type::String => Ok(" = \"\"".into()), + Type::Record(_) => Ok(" = {}".into()), + Type::Sequence(_) => Ok(" = []".into()), + _ => Ok("".into()), + } + } + _ => Ok("".into()), + }; + res + } + + pub fn ignored_function(nm: &str) -> Result { + Ok(IGNORED_FUNCTIONS.contains(nm)) + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/object.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/object.rs new file mode 100644 index 000000000..9d54bab05 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/object.rs @@ -0,0 +1,29 @@ +/* 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 uniffi_bindgen::backend::{CodeOracle, CodeType, Literal}; + +pub struct ObjectCodeType { + id: String, +} + +impl ObjectCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for ObjectCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/primitives.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/primitives.rs new file mode 100644 index 000000000..399506ba7 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/primitives.rs @@ -0,0 +1,75 @@ +use paste::paste; +use uniffi_bindgen::backend::{CodeOracle, CodeType, Literal}; +use uniffi_bindgen::interface::{types::Type, Radix}; + +fn render_literal(_oracle: &dyn CodeOracle, literal: &Literal) -> String { + fn typed_number(type_: &Type, num_str: String) -> String { + match type_ { + // Bytes, Shorts and Ints can all be inferred from the type. + Type::Int8 | Type::Int16 | Type::Int32 => num_str, + Type::Int64 => format!("{num_str}L"), + + Type::UInt8 | Type::UInt16 | Type::UInt32 => format!("{num_str}u"), + Type::UInt64 => format!("{num_str}uL"), + + Type::Float32 => format!("{num_str}f"), + Type::Float64 => num_str, + _ => panic!("Unexpected literal: {num_str} is not a number"), + } + } + + match literal { + Literal::Boolean(v) => format!("{v}"), + Literal::String(s) => format!("\"{s}\""), + Literal::Int(i, radix, type_) => typed_number( + type_, + match radix { + Radix::Octal => format!("{i:#x}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + ), + Literal::UInt(i, radix, type_) => typed_number( + type_, + match radix { + Radix::Octal => format!("{i:#x}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + ), + Literal::Float(string, type_) => typed_number(type_, string.clone()), + + _ => unreachable!("Literal"), + } +} + +macro_rules! impl_code_type_for_primitive { + ($T:ty, $class_name:literal) => { + paste! { + pub struct $T; + + impl CodeType for $T { + fn type_label(&self, _oracle: &dyn CodeOracle) -> String { + $class_name.into() + } + + fn literal(&self, oracle: &dyn CodeOracle, literal: &Literal) -> String { + render_literal(oracle, &literal) + } + } + } + }; +} + +impl_code_type_for_primitive!(BooleanCodeType, "boolean"); +impl_code_type_for_primitive!(StringCodeType, "string"); +impl_code_type_for_primitive!(Int8CodeType, "number"); +impl_code_type_for_primitive!(Int16CodeType, "number"); +impl_code_type_for_primitive!(Int32CodeType, "number"); +impl_code_type_for_primitive!(Int64CodeType, "number"); +impl_code_type_for_primitive!(UInt8CodeType, "number"); +impl_code_type_for_primitive!(UInt16CodeType, "number"); +impl_code_type_for_primitive!(UInt32CodeType, "number"); +impl_code_type_for_primitive!(UInt64CodeType, "number"); +impl_code_type_for_primitive!(Float32CodeType, "number"); +impl_code_type_for_primitive!(Float64CodeType, "number"); diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/record.rs b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/record.rs new file mode 100644 index 000000000..820a4b068 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/record.rs @@ -0,0 +1,29 @@ +/* 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 uniffi_bindgen::backend::{CodeOracle, CodeType, Literal}; + +pub struct RecordCodeType { + id: String, +} + +impl RecordCodeType { + pub fn new(id: String) -> Self { + Self { id } + } +} + +impl CodeType for RecordCodeType { + fn type_label(&self, oracle: &dyn CodeOracle) -> String { + oracle.class_name(&self.id) + } + + fn canonical_name(&self, _oracle: &dyn CodeOracle) -> String { + format!("Type{}", self.id) + } + + fn literal(&self, _oracle: &dyn CodeOracle, _literal: &Literal) -> String { + unreachable!(); + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/EnumTemplate.ts b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/EnumTemplate.ts new file mode 100644 index 000000000..05b67828c --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/EnumTemplate.ts @@ -0,0 +1,26 @@ +{%- let e = ci.get_enum_definition(name).unwrap() %} +{%- if e.is_flat() %} + +export enum {{ type_name }} { + {% for variant in e.variants() -%} + {{ variant.name()|enum_variant }} = "{{ variant.name()|var_name }}"{% if !loop.last %}, + {% endif %} + {%- endfor %} +} + +{%- else %} + +export enum {{ type_name }}Variant { + {% for variant in e.variants() -%} + {{ variant.name()|enum_variant }} = "{{ variant.name()|var_name }}"{% if !loop.last %}, + {% endif %} + {%- endfor %} +} + +export type {{ type_name }} = {% for variant in e.variants() -%}{ + type: {{ type_name }}Variant.{{ variant.name()|enum_variant }}{% if variant.has_fields() %}, + {%- call ts::field_list_decl(variant) -%}{% endif %} +}{% if !loop.last %} | {% endif %} +{%- endfor %} + +{%- endif %} \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/Helpers.ts b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/Helpers.ts new file mode 100644 index 000000000..6944725c8 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/Helpers.ts @@ -0,0 +1,5 @@ + +export const init = async (mnemonic: string, dataDir: string = "", network: Network): Promise => { + const response = await LiquidSwapSDK.initBindingWallet(mnemonic, dataDir, network) + return response +} \ No newline at end of file diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/Objects.ts b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/Objects.ts new file mode 100644 index 000000000..d7d07f613 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/Objects.ts @@ -0,0 +1,12 @@ +{%- for type_ in ci.iter_types() %} +{%- let type_name = type_|type_name %} +{%- match type_ %} +{%- when Type::Object ( name ) %} +{% let obj = ci.get_object_definition(name).unwrap() %} +{%- for func in obj.methods() -%} +{%- include "TopLevelFunctionTemplate.ts" %} +{% endfor %} +{%- else -%} +{%- endmatch -%} +{%- endfor %} + diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/RecordTemplate.ts b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/RecordTemplate.ts new file mode 100644 index 000000000..24e297a29 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/RecordTemplate.ts @@ -0,0 +1,5 @@ +{%- let rec = ci.get_record_definition(name).unwrap() %} + +export type {{ type_name }} = { + {%- call ts::field_list_decl(rec) %} +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/TopLevelFunctionTemplate.ts b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/TopLevelFunctionTemplate.ts new file mode 100644 index 000000000..f8091b788 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/TopLevelFunctionTemplate.ts @@ -0,0 +1,11 @@ +{%- match func.return_type() -%} +{%- when Some with (return_type) %} +export const {{ func.name()|fn_name }} = async ({%- call ts::arg_list_decl(func) -%}): Promise<{{ return_type|return_type_name }}> => { + const response = await LiquidSwapSDK.{{func.name()|fn_name}}({%- call ts::arg_list(func) -%}) + return response +} +{%- when None %} +export const {{ func.name()|fn_name }} = async ({%- call ts::arg_list_decl(func) -%}): Promise => { + await LiquidSwapSDK.{{ func.name()|fn_name }}({%- call ts::arg_list(func) -%}) +} +{%- endmatch %} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/Types.ts b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/Types.ts new file mode 100644 index 000000000..c8fd018cb --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/Types.ts @@ -0,0 +1,10 @@ +{%- for type_ in ci.iter_types() %} +{%- let type_name = type_|type_name %} +{%- match type_ %} +{%- when Type::Record ( name ) %} +{%- include "RecordTemplate.ts" %} +{%- when Type::Enum ( name ) %} +{%- include "EnumTemplate.ts" %} +{%- else %} +{%- endmatch -%} +{%- endfor %} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/macros.ts b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/macros.ts new file mode 100644 index 000000000..4f8708ee9 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/macros.ts @@ -0,0 +1,32 @@ + +{% macro arg_list(func) %} + {%- for arg in func.arguments() -%} + {{ arg.name()|var_name -}} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + +{%- macro field_list(rec) %} + {%- for f in rec.fields() %} + {{ f.name()|var_name|unquote }}, + {%- endfor %} +{%- endmacro -%} + +{%- macro field_list_decl(rec) %} + {%- for f in rec.fields() %} + {%- match f.type_() %} + {%- when Type::Optional(inner) %} + {%- let unboxed = inner.as_ref() %} + {{ f.name()|var_name }}?: {{ unboxed|type_name }} + {%- else %} + {{ f.name()|var_name }}: {{ f.type_()|type_name }} + {%- endmatch %} + {%- endfor %} +{%- endmacro -%} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {{ arg.name()|var_name }}: {{ arg.type_()|absolute_type_name }}{{- arg.type_()|default_value -}} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/module.ts b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/module.ts new file mode 100644 index 000000000..7ec7751b9 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/gen_typescript/templates/module.ts @@ -0,0 +1,28 @@ +import { NativeModules, Platform } from "react-native" + +const LINKING_ERROR = + `The package 'react-native-liquid-swap-sdk' doesn't seem to be linked. Make sure: \n\n` + + Platform.select({ ios: "- You have run 'pod install'\n", default: "" }) + + "- You rebuilt the app after installing the package\n" + + "- You are not using Expo managed workflow\n" + +const LiquidSwapSDK = NativeModules.RNLiquidSwapSDK + ? NativeModules.RNLiquidSwapSDK + : new Proxy( + {}, + { + get() { + throw new Error(LINKING_ERROR) + } + } + ) + +{%- import "macros.ts" as ts %} +{%- include "Types.ts" %} +{% include "Helpers.ts" -%} +{% for func in ci.function_definitions() %} +{%- if func.name()|ignored_function == false -%} +{%- include "TopLevelFunctionTemplate.ts" %} +{% endif -%} +{% endfor -%} +{%- include "Objects.ts" %} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/generator.rs b/lib/ls-sdk-bindings/bindings-react-native/src/generator.rs new file mode 100644 index 000000000..aedb9c992 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/generator.rs @@ -0,0 +1,270 @@ +use anyhow::Result; +use askama::Template; +use camino::Utf8Path; +use camino::Utf8PathBuf; +use serde::*; +use std::fs; +use std::fs::File; +use std::io::Write; +use std::process::Command; +use uniffi_bindgen::{BindingGenerator, BindingGeneratorConfig, ComponentInterface}; + +use crate::gen_kotlin; +use crate::gen_swift; +use crate::gen_typescript; + +pub struct RNBindingGenerator {} + +impl RNBindingGenerator { + fn write_bindings( + &self, + bindings_output: &String, + output_path: &Utf8Path, + file_name: &Utf8Path, + ) -> Result { + fs::create_dir_all(output_path)?; + let bindings_path: camino::Utf8PathBuf = output_path.join(file_name); + let mut f: File = File::create(&bindings_path)?; + write!(f, "{}", bindings_output)?; + Ok(bindings_path) + } + + fn write_kotlin_mapper_bindings( + &self, + ci: &ComponentInterface, + config: RNConfig, + base_output_path: &Utf8Path, + ) -> Result<()> { + // Create the path + let output_path = + base_output_path.join(Utf8Path::new("android/src/main/java/com/lssdk")); + // Generate and write the binding to file + let bindings_output = self::gen_kotlin::MapperGenerator::new(config.clone(), ci) + .render() + .map_err(anyhow::Error::new)?; + let bindings_file = self + .write_bindings( + &bindings_output, + &output_path, + Utf8Path::new("LiquidSwapSDKMapper.kt"), + ) + .unwrap(); + // Lint binding + self.lint_kotlin_bindings(&bindings_file); + Ok(()) + } + + fn write_kotlin_module_bindings( + &self, + ci: &ComponentInterface, + config: RNConfig, + base_output_path: &Utf8Path, + ) -> Result<()> { + // Create the path + let output_path = + base_output_path.join(Utf8Path::new("android/src/main/java/com/lssdk")); + // Generate and write the binding to file + let bindings_output = self::gen_kotlin::ModuleGenerator::new(config.clone(), ci) + .render() + .map_err(anyhow::Error::new)?; + let bindings_file = self + .write_bindings( + &bindings_output, + &output_path, + Utf8Path::new("LiquidSwapSDKModule.kt"), + ) + .unwrap(); + // Lint binding + self.lint_kotlin_bindings(&bindings_file); + Ok(()) + } + + fn lint_kotlin_bindings(&self, bindings_file: &Utf8PathBuf) { + if let Err(e) = Command::new("ktlint") + .arg("-F") + .arg(&bindings_file) + .output() + { + println!( + "Warning: Unable to auto-format {} using ktlint: {:?}", + bindings_file.file_name().unwrap(), + e + ) + } + } + + fn write_swift_mapper_bindings( + &self, + ci: &ComponentInterface, + config: RNConfig, + base_output_path: &Utf8Path, + ) -> Result<()> { + // Create the path + let output_path = base_output_path.join(Utf8Path::new("ios")); + // Generate and write the binding to file + let bindings_output = self::gen_swift::MapperGenerator::new(config.clone(), ci) + .render() + .map_err(anyhow::Error::new)?; + let bindings_file = self + .write_bindings( + &bindings_output, + &output_path, + Utf8Path::new("LiquidSwapSDKMapper.swift"), + ) + .unwrap(); + // Lint binding + self.lint_swift_bindings(&bindings_file); + Ok(()) + } + + fn write_swift_extern_bindings( + &self, + ci: &ComponentInterface, + config: RNConfig, + base_output_path: &Utf8Path, + ) -> Result<()> { + // Create the path + let output_path = base_output_path.join(Utf8Path::new("ios")); + // Generate and write the binding to file + let bindings_output = self::gen_swift::ExternGenerator::new(config.clone(), ci) + .render() + .map_err(anyhow::Error::new)?; + let bindings_file = self + .write_bindings( + &bindings_output, + &output_path, + Utf8Path::new("RNLiquidSwapSDK.m"), + ) + .unwrap(); + // Lint binding + self.lint_swift_bindings(&bindings_file); + Ok(()) + } + + fn write_swift_module_bindings( + &self, + ci: &ComponentInterface, + config: RNConfig, + base_output_path: &Utf8Path, + ) -> Result<()> { + // Create the path + let output_path = base_output_path.join(Utf8Path::new("ios")); + // Generate and write the binding to file + let bindings_output = self::gen_swift::ModuleGenerator::new(config.clone(), ci) + .render() + .map_err(anyhow::Error::new)?; + let bindings_file = self + .write_bindings( + &bindings_output, + &output_path, + Utf8Path::new("RNLiquidSwapSDK.swift"), + ) + .unwrap(); + // Lint binding + self.lint_swift_bindings(&bindings_file); + Ok(()) + } + + fn lint_swift_bindings(&self, bindings_file: &Utf8PathBuf) { + if let Err(e) = Command::new("swiftformat") + .arg(bindings_file.as_str()) + .output() + { + println!( + "Warning: Unable to auto-format {} using swiftformat: {:?}", + bindings_file.file_name().unwrap(), + e + ) + } + } + + fn write_typescript_bindings( + &self, + ci: &ComponentInterface, + config: RNConfig, + base_output_path: &Utf8Path, + ) -> Result<()> { + // Create the path + let output_path = base_output_path.join(Utf8Path::new("ts/src")); + // Generate and write the binding to file + let bindings_output = self::gen_typescript::ModuleGenerator::new(config.clone(), ci) + .render() + .map_err(anyhow::Error::new)?; + let bindings_file = self + .write_bindings(&bindings_output, &output_path, Utf8Path::new("index.ts")) + .unwrap(); + // Lint binding + self.lint_typescript_bindings(&bindings_file); + Ok(()) + } + + fn lint_typescript_bindings(&self, bindings_file: &Utf8PathBuf) { + if let Err(e) = Command::new("tslint") + .arg("--fix") + .arg(&bindings_file) + .output() + { + println!( + "Warning: Unable to auto-format {} using tslint: {:?}", + bindings_file.file_name().unwrap(), + e + ) + } + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct RNConfig { + package_name: Option, +} + +impl RNConfig {} + +impl BindingGeneratorConfig for RNConfig { + fn get_entry_from_bindings_table(_bindings: &toml::value::Value) -> Option { + if let Some(table) = _bindings.as_table() { + table.get("rn").map(|v| v.clone()) + } else { + None + } + } + + fn get_config_defaults(ci: &ComponentInterface) -> Vec<(String, toml::value::Value)> { + vec![ + ( + "package_name".to_string(), + toml::value::Value::String(ci.namespace().to_string()), + ), + ( + "cdylib_name".to_string(), + toml::value::Value::String(ci.namespace().to_string()), + ), + ] + } +} + +impl BindingGenerator for RNBindingGenerator { + type Config = RNConfig; + + fn write_bindings( + &self, + ci: ComponentInterface, + config: Self::Config, + out_dir: &Utf8Path, + ) -> Result<()> { + fs::create_dir_all(out_dir)?; + + // generate kotlin + self.write_kotlin_mapper_bindings(&ci, config.clone(), out_dir)?; + self.write_kotlin_module_bindings(&ci, config.clone(), out_dir)?; + + // generate ios + self.write_swift_mapper_bindings(&ci, config.clone(), out_dir)?; + self.write_swift_extern_bindings(&ci, config.clone(), out_dir)?; + self.write_swift_module_bindings(&ci, config.clone(), out_dir)?; + + // generate typescript + self.write_typescript_bindings(&ci, config.clone(), out_dir)?; + Ok(()) + } +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/src/main.rs b/lib/ls-sdk-bindings/bindings-react-native/src/main.rs new file mode 100644 index 000000000..cb87db129 --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/src/main.rs @@ -0,0 +1,34 @@ +mod gen_kotlin; +mod gen_swift; +mod gen_typescript; +mod generator; +use camino::Utf8Path; +use clap::Parser; +use generator::RNBindingGenerator; + +#[derive(Parser, Debug)] +pub(crate) struct Cli { + #[clap(name = "binding_dir", short = 'b', long = "binding_dir")] + pub(crate) binding_dir: Option, + #[clap(name = "out_dir", short = 'o', long = "out_dir")] + pub(crate) out_dir: Option, +} + +fn main() { + let cli = Cli::parse(); + let cli_binding_dir = cli.binding_dir.unwrap_or("../".into()); + let cli_out_dir = cli.out_dir.unwrap_or("./".into()); + let binding_dir = Utf8Path::new(cli_binding_dir.as_str()); + let udl_file = binding_dir.join(Utf8Path::new("src/ls_sdk.udl")); + let config = binding_dir.join(Utf8Path::new("uniffi.toml")); + let out_dir = Utf8Path::new(cli_out_dir.as_str()); + + // React Native generator + uniffi_bindgen::generate_external_bindings( + RNBindingGenerator {}, + udl_file, + Some(config), + Some(out_dir), + ) + .unwrap(); +} diff --git a/lib/ls-sdk-bindings/bindings-react-native/tslint.json b/lib/ls-sdk-bindings/bindings-react-native/tslint.json new file mode 100644 index 000000000..f766cfc6c --- /dev/null +++ b/lib/ls-sdk-bindings/bindings-react-native/tslint.json @@ -0,0 +1,97 @@ +{ + "extends": "tslint:recommended", + "rules": { + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "import-blacklist": [ + true + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "member-access": false, + "member-ordering": [ + true, + { + "order": "fields-first" + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-shadowed-variable": false, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "double" + ], + "radix": true, + "semicolon": false, + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } + } \ No newline at end of file diff --git a/lib/ls-sdk-bindings/makefile b/lib/ls-sdk-bindings/makefile index f44a74d69..04d343ea5 100644 --- a/lib/ls-sdk-bindings/makefile +++ b/lib/ls-sdk-bindings/makefile @@ -111,3 +111,7 @@ bindings-swift: ios-universal darwin-universal python: $(SOURCES) cargo build --release --target $(TARGET) cargo run --features=uniffi/cli --bin uniffi-bindgen generate src/ls_sdk.udl --no-format --language python -o ffi/python + +## React Native +react-native: + make -C bindings-react-native codegen \ No newline at end of file