reacty_yew 0.1.0

Generate Yew components from React component via Typescript type definitions
Documentation
use proc_macro_error::{diagnostic, emit_error, Level};
use quote::quote;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::process::Command;

const ANALYZER_SCRIPT_FILE: &'static str = include_str!("./js/index.js");
const TYPESCRIPT_SCRIPT_FILE: &'static str = include_str!("./js/typescript.js");

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AnalyzerOutput {
    pub types: Vec<Type>,
    pub components: Vec<Component>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Type {
    pub name: String,
    pub properties: Vec<TypeProperty>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TypeProperty {
    #[serde(rename = "intrinsicType")]
    pub intrinsic_type: Option<String>,
    pub name: String,
    pub optional: bool,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Component {
    pub name: String,
    #[serde(rename = "propsName")]
    pub props_name: String,
}

impl AnalyzerOutput {
    pub fn types(&self) -> Vec<Type> {
        let mut types = self.types.clone();
        types.dedup_by_key(|n| n.name.clone());
        types
    }
}

impl TypeProperty {
    pub fn name_ident(&self) -> syn::Ident {
        syn::Ident::new(&self.name, proc_macro2::Span::call_site())
    }

    pub fn rust_type_for_intrinsic_type(&self) -> Option<proc_macro2::TokenStream> {
        self.intrinsic_type
            .as_ref()
            .map(|intrinsic_type| match intrinsic_type.as_ref() {
                "string" => quote! { String },
                "number" => quote! { f64 },
                "boolean" => quote! { bool },
                "any" => quote! { JsValue },
                other => {
                    emit_error!(diagnostic!(
                        proc_macro::Span::call_site(),
                        Level::Error,
                        format!(
                            "reacty_yew: Unable to convert intrinsic type \"{}\" to Rust type",
                            other
                        ),
                    ));
                    quote! {}
                }
            })
    }

    pub fn conversion_to_js_type(&self, prop_name: syn::Ident) -> proc_macro2::TokenStream {
        self.intrinsic_type
            .as_ref()
            .map(|intrinsic_type| match intrinsic_type.as_ref() {
                "string" | "number" | "boolean" => {
                    quote! { JsValue::from_serde(#prop_name).unwrap() }
                }
                "any" => {
                    quote! { #prop_name.to_owned() }
                }
                _ => unimplemented!(),
            })
            .unwrap()
    }
}

pub fn init_js_scripts() {
    let analyzer_script_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./target/index.js");
    let typescript_script_path =
        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./target/typescript.js");

    fs::write(&analyzer_script_path, ANALYZER_SCRIPT_FILE).unwrap();
    fs::write(&typescript_script_path, TYPESCRIPT_SCRIPT_FILE).unwrap();
}

pub fn run_analyzer(path: &str) -> AnalyzerOutput {
    let analyzer_script_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./target/index.js");
    let input_path = PathBuf::from(path).canonicalize().unwrap();

    let output = Command::new("node")
        .args(&[
            analyzer_script_path.to_string_lossy().as_ref(),
            input_path.to_string_lossy().as_ref(),
        ])
        .output()
        .expect("failed to execute process");

    serde_json::from_slice(&output.stdout).unwrap()
}