Add code generation of React Native bindings

This commit is contained in:
Ross Savage
2024-04-25 13:11:10 +02:00
parent 567b077717
commit 4efb664dde
51 changed files with 2828 additions and 0 deletions

View File

@@ -8,6 +8,7 @@ rpath = true
[workspace]
members = [
"ls-sdk-bindings",
"ls-sdk-bindings/bindings-react-native",
"ls-sdk-core",
]
resolver = "2"

View File

@@ -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/

View File

@@ -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"] }

View File

@@ -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
```

View File

@@ -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"

View File

@@ -0,0 +1,5 @@
SOURCES=$(sort $(wildcard ./src/*.rs ./src/**/*.rs))
codegen: $(SOURCES)
cargo run

View File

@@ -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<HashSet<String>> = 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")]
#[allow(dead_code)]
pub struct MapperGenerator<'a> {
config: RNConfig,
ci: &'a ComponentInterface,
// Track types used in sequences with the `add_sequence_type()` macro
sequence_types: RefCell<BTreeSet<String>>,
}
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<String> {
let sequence_types = self.sequence_types.clone().into_inner();
sequence_types.into_iter().collect()
}
}
#[derive(Template)]
#[template(syntax = "rn", escape = "none", path = "module.kt")]
#[allow(dead_code)]
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<String, askama::Error> {
Ok(codetype.type_label(oracle()))
}
pub fn fn_name(nm: &str) -> Result<String, askama::Error> {
Ok(oracle().fn_name(nm))
}
pub fn render_to_array(
type_name: &str,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
let res: Result<String, askama::Error> = match type_name {
"Boolean" => Ok("array.pushBoolean(value)".to_string()),
"Double" => Ok("array.pushDouble(value)".to_string()),
"Int" => Ok("array.pushInt(value)".to_string()),
"ReadableArray" => Ok("array.pushArray(value)".to_string()),
"ReadableMap" => Ok("array.pushMap(value)".to_string()),
"String" => Ok("array.pushString(value)".to_string()),
"UByte" => Ok("array.pushInt(value.toInt())".to_string()),
"UInt" => Ok("array.pushInt(value.toInt())".to_string()),
"UShort" => Ok("array.pushInt(value.toInt())".to_string()),
"ULong" => Ok("array.pushDouble(value.toDouble())".to_string()),
_ => 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("array.pushString(value.name.lowercase())".to_string()),
false => Ok("array.pushMap(readableMapOf(value))".to_string()),
}
}
_ => Ok("array.pushMap(readableMapOf(value))".to_string()),
},
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<String, askama::Error> {
let res: Result<String, askama::Error> = match t {
Type::UInt8 => Ok(format!("{obj_name}.{field_name}")),
Type::Int8 => Ok(format!("{obj_name}.{field_name}")),
Type::UInt16 => Ok(format!("{obj_name}.{field_name}")),
Type::Int16 => Ok(format!("{obj_name}.{field_name}")),
Type::UInt32 => Ok(format!("{obj_name}.{field_name}")),
Type::Int32 => Ok(format!("{obj_name}.{field_name}")),
Type::UInt64 => Ok(format!("{obj_name}.{field_name}")),
Type::Int64 => Ok(format!("{obj_name}.{field_name}")),
Type::Float32 => Ok(format!("{obj_name}.{field_name}")),
Type::Float64 => Ok(format!("{obj_name}.{field_name}")),
Type::Boolean => Ok(format!("{obj_name}.{field_name}")),
Type::String => Ok(format!("{obj_name}.{field_name}")),
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) }}"
)),
false => Ok(format!("readableMapOf({obj_name}.{field_name})")),
},
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() }}"
)),
false => Ok(format!("{obj_name}.{field_name}.name.lowercase()")),
},
false => match optional {
true => Ok(format!(
"{obj_name}.{field_name}?.let {{ readableMapOf(it) }}"
)),
false => Ok(format!("readableMapOf({obj_name}.{field_name})")),
},
}
}
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<String, askama::Error> {
let mut mandatory_suffix = "";
if !optional {
mandatory_suffix = "!!"
}
let res: String = match t {
Type::UInt8 => format!("{name}.getInt(\"{field_name}\").toUByte()"),
Type::Int8 => format!("{name}.getInt(\"{field_name}\").toByte()"),
Type::UInt16 => format!("{name}.getInt(\"{field_name}\").toUShort()"),
Type::Int16 => format!("{name}.getInt(\"{field_name}\").toShort()"),
Type::UInt32 => format!("{name}.getInt(\"{field_name}\").toUInt()"),
Type::Int32 => format!("{name}.getInt(\"{field_name}\")"),
Type::UInt64 => format!("{name}.getDouble(\"{field_name}\").toULong()"),
Type::Int64 => format!("{name}.getDouble(\"{field_name}\").toLong()"),
Type::Float32 => format!("{name}.getDouble(\"{field_name}\")"),
Type::Float64 => format!("{name}.getDouble(\"{field_name}\")"),
Type::Boolean => format!("{name}.getBoolean(\"{field_name}\")"),
Type::String => format!("{name}.getString(\"{field_name}\"){mandatory_suffix}"),
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}"
)
}
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}")
}
true => format!(
"{name}.getString(\"{field_name}\")?.let {{ as{inner}(it)}}{mandatory_suffix}"
),
}
}
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<String, askama::Error> {
Ok(format!("`{}`", nm.to_string().to_lower_camel_case()))
}
pub fn unquote(nm: &str) -> Result<String, askama::Error> {
Ok(nm.trim_matches('`').to_string())
}
pub fn ignored_function(nm: &str) -> Result<bool, askama::Error> {
Ok(IGNORED_FUNCTIONS.contains(nm))
}
pub fn rn_convert_type(
t: &TypeIdentifier,
_ci: &ComponentInterface,
) -> Result<String, askama::Error> {
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<String, askama::Error> {
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<String, askama::Error> {
Ok(format!("{nm}Tmp"))
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,96 @@
fun readableMapOf(vararg values: Pair<String, *>): 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<String>): 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<UByte> {
val list = ArrayList<UByte>()
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<String> {
val list = ArrayList<String>()
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}"
}

View File

@@ -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 %}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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 -%}

View File

@@ -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 %}

View File

@@ -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" %}

View File

@@ -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" %}
}

View File

@@ -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<HashSet<String>> = 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")]
#[allow(dead_code)]
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")]
#[allow(dead_code)]
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")]
#[allow(dead_code)]
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<String, askama::Error> {
Ok(codetype.type_label(oracle()))
}
pub fn fn_name(nm: &str) -> Result<String, askama::Error> {
Ok(oracle().fn_name(nm))
}
pub fn render_to_map(
t: &TypeIdentifier,
ci: &ComponentInterface,
obj_name: &str,
field_name: &str,
optional: bool,
) -> Result<String, askama::Error> {
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<String, askama::Error> = match t {
Type::UInt8 => Ok(format!("{obj_prefix}{field_name}")),
Type::Int8 => Ok(format!("{obj_prefix}{field_name}")),
Type::UInt16 => Ok(format!("{obj_prefix}{field_name}")),
Type::Int16 => Ok(format!("{obj_prefix}{field_name}")),
Type::UInt32 => Ok(format!("{obj_prefix}{field_name}")),
Type::Int32 => Ok(format!("{obj_prefix}{field_name}")),
Type::UInt64 => Ok(format!("{obj_prefix}{field_name}")),
Type::Int64 => Ok(format!("{obj_prefix}{field_name}")),
Type::Float32 => Ok(format!("{obj_prefix}{field_name}")),
Type::Float64 => Ok(format!("{obj_prefix}{field_name}")),
Type::Boolean => Ok(format!("{obj_prefix}{field_name}")),
Type::String => Ok(format!("{obj_prefix}{field_name}")),
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})"
)),
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})"
)),
false => Ok(format!(
"dictionaryOf({var_name}: {obj_prefix}{field_name}{optional_suffix})"
)),
}
}
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<String, askama::Error> {
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<String, askama::Error> {
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<String, askama::Error> {
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(name.to_string())
}
}
}
pub fn extern_type_name(
t: &TypeIdentifier,
ci: &ComponentInterface,
) -> Result<String, askama::Error> {
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<bool, askama::Error> {
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<String, askama::Error> {
let res: String = match t {
Type::UInt8 => map_var_name.to_string(),
Type::Int8 => map_var_name.to_string(),
Type::UInt16 => map_var_name.to_string(),
Type::Int16 => map_var_name.to_string(),
Type::UInt32 => map_var_name.to_string(),
Type::Int32 => map_var_name.to_string(),
Type::UInt64 => map_var_name.to_string(),
Type::Int64 => map_var_name.to_string(),
Type::Float32 => map_var_name.to_string(),
Type::Float64 => map_var_name.to_string(),
Type::Boolean => map_var_name.to_string(),
Type::String => map_var_name.to_string(),
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})"),
true => format!("try as{inner}({enum_unquoted_name}: {map_var_name})"),
}
}
Type::Error(_) => "".into(),
Type::CallbackInterface(_) => "".into(),
Type::Optional(inner) => {
let unboxed = inner.as_ref();
render_from_map(unboxed, ci, map_var_name)?
}
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})")
}
_ => map_var_name.to_string(),
}
}
Type::Map(_, _) => "".into(),
Type::External { .. } => "".into(),
Type::Custom { .. } => "".into(),
Type::Unresolved { .. } => "".into(),
};
Ok(res.to_string())
}
pub fn var_name(nm: &str) -> Result<String, askama::Error> {
Ok(format!("`{}`", nm.to_string().to_lower_camel_case()))
}
pub fn unquote(nm: &str) -> Result<String, askama::Error> {
Ok(nm.trim_matches('`').to_string())
}
pub fn ignored_function(nm: &str) -> Result<bool, askama::Error> {
Ok(IGNORED_FUNCTIONS.contains(nm))
}
pub fn list_arg(nm: &str) -> Result<String, askama::Error> {
Ok(format!("`{nm}List`"))
}
pub fn temporary(nm: &str) -> Result<String, askama::Error> {
Ok(format!("{nm}Tmp"))
}
}

View File

@@ -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
}

View File

@@ -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
)

View File

@@ -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)"
}

View File

@@ -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 %}

View File

@@ -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) }
}

View File

@@ -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)
}
}

View File

@@ -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 %}

View File

@@ -0,0 +1,28 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@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

View File

@@ -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 -%}

View File

@@ -0,0 +1,11 @@
{%- import "macros.swift" as swift -%}
import Foundation
import LiquidSwapSDK
enum LiquidSwapSDKMapper {
{%- include "Types.swift" %}
{%- include "Helpers.swift" %}
}

View File

@@ -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 %}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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!();
}
}
}

View File

@@ -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!();
}
}

View File

@@ -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()
}
}

View File

@@ -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");

View File

@@ -0,0 +1,236 @@
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<HashSet<String>> = 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<HashSet<String>> = 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")]
#[allow(dead_code)]
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<dyn CodeType>` 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<dyn CodeType> {
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<dyn CodeType> {
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<String, askama::Error> {
Ok(codetype.type_label(oracle()))
}
/// Get the idiomatic Typescript rendering of a function name.
pub fn fn_name(nm: &str) -> Result<String, askama::Error> {
Ok(oracle().fn_name(nm))
}
/// Get the idiomatic Typescript rendering of a variable name.
pub fn var_name(nm: &str) -> Result<String, askama::Error> {
Ok(oracle().var_name(nm))
}
/// Get the idiomatic Typescript rendering of an individual enum variant.
pub fn enum_variant(nm: &str) -> Result<String, askama::Error> {
Ok(oracle().enum_variant_name(nm))
}
pub fn absolute_type_name(t: &TypeIdentifier) -> Result<String, askama::Error> {
let res: Result<String, askama::Error> = 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<String, askama::Error> {
let res: Result<String, askama::Error> = 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<String, askama::Error> {
let res: Result<String, askama::Error> = 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<bool, askama::Error> {
Ok(IGNORED_FUNCTIONS.contains(nm))
}
}

View File

@@ -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!();
}
}

View File

@@ -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");

View File

@@ -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!();
}
}

View File

@@ -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 %}

View File

@@ -0,0 +1,5 @@
export const init = async (mnemonic: string, dataDir: string = "", network: Network): Promise<void> => {
const response = await LiquidSwapSDK.initBindingWallet(mnemonic, dataDir, network)
return response
}

View File

@@ -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 %}

View File

@@ -0,0 +1,5 @@
{%- let rec = ci.get_record_definition(name).unwrap() %}
export type {{ type_name }} = {
{%- call ts::field_list_decl(rec) %}
}

View File

@@ -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<void> => {
await LiquidSwapSDK.{{ func.name()|fn_name }}({%- call ts::arg_list(func) -%})
}
{%- endmatch %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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" %}

View File

@@ -0,0 +1,264 @@
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<Utf8PathBuf> {
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<String>,
}
impl RNConfig {}
impl BindingGeneratorConfig for RNConfig {
fn get_entry_from_bindings_table(_bindings: &toml::value::Value) -> Option<toml::value::Value> {
if let Some(table) = _bindings.as_table() {
table.get("rn").cloned()
} 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(())
}
}

View File

@@ -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<String>,
#[clap(name = "out_dir", short = 'o', long = "out_dir")]
pub(crate) out_dir: Option<String>,
}
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();
}

View File

@@ -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"
]
}
}

View File

@@ -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