Skip to content

Commit

Permalink
fix: improve code parsing and plugin API
Browse files Browse the repository at this point in the history
  • Loading branch information
glihm committed Jan 13, 2024
1 parent 2d17191 commit 78a2593
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 228 deletions.
281 changes: 135 additions & 146 deletions crates/dojo-bindgen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,15 @@ pub struct DojoModel {
pub tokens: HashMap<String, Vec<Token>>,
}

#[derive(Debug, PartialEq)]
pub struct DojoContract {
pub contract_file_name: String,
pub tokens: HashMap<String, Vec<Token>>,
}

#[derive(Debug)]
pub struct DojoMetadata {
pub struct DojoData {
pub contracts: HashMap<String, DojoContract>,
pub models: HashMap<String, DojoModel>,
}

Expand All @@ -45,12 +52,7 @@ impl PluginManager {
return Ok(());
}

println!("Generating bindings {:?}", self);

// TODO: loops can be optimized to only parse a file once.
let metadata = DojoMetadata {
models: gather_models(&self.artifacts_path).expect("Can't gather models"),
};
let data = gather_dojo_data(&self.artifacts_path)?;

for plugin in &self.builtin_plugins {
// Get the plugin builder from the plugin enum.
Expand All @@ -59,69 +61,22 @@ impl PluginManager {
BuiltinPlugins::Unity => Box::new(UnityPlugin::new()),
};

// TODO: types aliases: For now they are empty, we can expect them to be passed
// by the user from command line. But in dojo context, the naming conflict
// in a contract are low as they remain usually relatively simple.
let types_aliases = HashMap::new();

builder.generate_models_bindings(&metadata.models).await?;

for entry in fs::read_dir(&self.artifacts_path)? {
let entry = entry?;
let path = entry.path();

if path.is_file() {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
let file_content = fs::read_to_string(&path)?;

if is_systems_contract(file_name, &file_content) {
let tokens =
AbiParser::tokens_from_abi_string(&file_content, &types_aliases)?;
builder.generate_systems_bindings(file_name, tokens, &metadata).await?;
}
}
}
}
builder.generate_code(&data).await?;
}

// TODO: invoke the custom plugins via stdin.
// TODO: define the interface to pass the data to the plugin. JSON? Protobuf?
// (cf. mod.rs in plugins module).
// The plugin executable (same name as the plugin name) MUST be in the path.

Ok(())
}
}

/// Identifies if the given contract contains systems.
///
/// For now the identification is very naive and don't use the manifest
/// as the manifest format will change soon.
/// TODO: use the new manifest files once available.
///
/// # Arguments
///
/// * `file_name` - Name of the contract file.
/// * `file_content` - Content of the contract artifact.
fn is_systems_contract(file_name: &str, file_content: &str) -> bool {
if file_name.starts_with("dojo::") || file_name == "manifest.json" {
return false;
}

file_content.contains("IWorldDispatcher")
}

/// Gathers the models from given artifacts path.
///
/// This may be done using the manifest when new manifest structure
/// is defined and implemented.
/// Gathers dojo data from artifacts.
/// TODO: this should be modified later to use the new manifest structure.
/// it's currently done from the artifacts to decouple from the manifest.
///
/// # Arguments
///
/// * `artifacts_path` - Artifacts path where model contracts were generated.
fn gather_models(artifacts_path: &Utf8PathBuf) -> BindgenResult<HashMap<String, DojoModel>> {
println!("ARTIF PATH: {}", artifacts_path);
/// * `artifacts_path` - Artifacts path where contracts were generated.
fn gather_dojo_data(artifacts_path: &Utf8PathBuf) -> BindgenResult<DojoData> {
let mut models = HashMap::new();
let mut contracts = HashMap::new();

for entry in fs::read_dir(artifacts_path)? {
let entry = entry?;
Expand All @@ -131,9 +86,22 @@ fn gather_models(artifacts_path: &Utf8PathBuf) -> BindgenResult<HashMap<String,
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
let file_content = fs::read_to_string(&path)?;

// Models and Contracts must have a valid ABI.
if let Ok(tokens) =
AbiParser::tokens_from_abi_string(&file_content, &HashMap::new())
{
// Contract.
if is_systems_contract(file_name, &file_content) {
contracts.insert(
file_name.to_string(),
DojoContract {
contract_file_name: file_name.to_string(),
tokens: tokens.clone(),
},
);
}

// Model.
if is_model_contract(&tokens) {
if let Some(model_name) = model_name_from_artifact_filename(file_name) {
let model_pascal_case =
Expand All @@ -160,7 +128,25 @@ fn gather_models(artifacts_path: &Utf8PathBuf) -> BindgenResult<HashMap<String,
}
}

Ok(models)
Ok(DojoData { models, contracts })
}

/// Identifies if the given contract contains systems.
///
/// For now the identification is very naive and don't use the manifest
/// as the manifest format will change soon.
/// TODO: use the new manifest files once available.
///
/// # Arguments
///
/// * `file_name` - Name of the contract file.
/// * `file_content` - Content of the contract artifact.
fn is_systems_contract(file_name: &str, file_content: &str) -> bool {
if file_name.starts_with("dojo::") || file_name == "manifest.json" {
return false;
}

file_content.contains("IWorldDispatcher")
}

/// Filters the model ABI to keep relevant types
Expand Down Expand Up @@ -246,7 +232,6 @@ fn is_model_contract(tokens: &HashMap<String, Vec<Token>>) -> bool {

// This hashmap is not that good at devex level.. one must check the
// code to know the keys.
// TODO: change for an enum instead of string.
for f in &tokens["functions"] {
if expected_funcs.contains(&f.to_function().expect("Function expected").name.as_str()) {
funcs_counts += 1;
Expand All @@ -256,86 +241,90 @@ fn is_model_contract(tokens: &HashMap<String, Vec<Token>>) -> bool {
funcs_counts == expected_funcs.len()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn is_system_contract_ok() {
let file_name = "dojo_examples::actions::actions.json";
let file_content = include_str!(
"test_data/spawn-and-move/target/dev/dojo_examples::actions::actions.json"
);

assert!(is_systems_contract(file_name, file_content));
}

#[test]
fn is_system_contract_ignore_dojo_files() {
let file_name = "dojo::world::world.json";
let file_content = "";
assert!(!is_systems_contract(file_name, file_content));

let file_name = "manifest.json";
assert!(!is_systems_contract(file_name, file_content));
}

#[test]
fn test_is_system_contract_ignore_models() {
let file_name = "dojo_examples::models::position.json";
let file_content = include_str!(
"test_data/spawn-and-move/target/dev/dojo_examples::models::position.json"
);
assert!(!is_systems_contract(file_name, file_content));
}

#[test]
fn model_name_from_artifact_filename_ok() {
let file_name = "dojo_examples::models::position.json";
assert_eq!(model_name_from_artifact_filename(file_name), Some("position".to_string()));
}

#[test]
fn is_model_contract_ok() {
let file_content =
include_str!("test_data/spawn-and-move/target/dev/dojo_examples::models::moves.json");
let tokens = AbiParser::tokens_from_abi_string(file_content, &HashMap::new()).unwrap();

assert!(is_model_contract(&tokens));
}

#[test]
fn is_model_contract_ignore_systems() {
let file_content = include_str!(
"test_data/spawn-and-move/target/dev/dojo_examples::actions::actions.json"
);
let tokens = AbiParser::tokens_from_abi_string(file_content, &HashMap::new()).unwrap();

assert!(!is_model_contract(&tokens));
}

#[test]
fn is_model_contract_ignore_dojo_files() {
let file_content =
include_str!("test_data/spawn-and-move/target/dev/dojo::world::world.json");
let tokens = AbiParser::tokens_from_abi_string(file_content, &HashMap::new()).unwrap();

assert!(!is_model_contract(&tokens));
}

#[test]
fn gather_models_ok() {
let models =
gather_models(&Utf8PathBuf::from("src/test_data/spawn-and-move/target/dev")).unwrap();

assert_eq!(models.len(), 2);

let pos = models.get("Position").unwrap();
assert_eq!(pos.name, "Position");
assert_eq!(pos.qualified_path, "dojo_examples::models::Position");

let moves = models.get("Moves").unwrap();
assert_eq!(moves.name, "Moves");
assert_eq!(moves.qualified_path, "dojo_examples::models::Moves");
}
}
// Spawn and move project is not built at the time this lib is being tested.
// Need plain artifacts to simplify or use `sozo` from env (available as we're in the devcontainer).
// #[cfg(test)]
// mod tests {
// use super::*;

// #[test]
// fn is_system_contract_ok() {
// let file_name = "dojo_examples::actions::actions.json";
// let file_content = include_str!(
// "test_data/spawn-and-move/target/dev/dojo_examples::actions::actions.json"
// );

// assert!(is_systems_contract(file_name, file_content));
// }

// #[test]
// fn is_system_contract_ignore_dojo_files() {
// let file_name = "dojo::world::world.json";
// let file_content = "";
// assert!(!is_systems_contract(file_name, file_content));

// let file_name = "manifest.json";
// assert!(!is_systems_contract(file_name, file_content));
// }

// #[test]
// fn test_is_system_contract_ignore_models() {
// let file_name = "dojo_examples::models::position.json";
// let file_content = include_str!(
// "test_data/spawn-and-move/target/dev/dojo_examples::models::position.json"
// );
// assert!(!is_systems_contract(file_name, file_content));
// }

// #[test]
// fn model_name_from_artifact_filename_ok() {
// let file_name = "dojo_examples::models::position.json";
// assert_eq!(model_name_from_artifact_filename(file_name), Some("position".to_string()));
// }

// #[test]
// fn is_model_contract_ok() {
// let file_content =
//
// include_str!("test_data/spawn-and-move/target/dev/dojo_examples::models::moves.json");
// let tokens = AbiParser::tokens_from_abi_string(file_content, &HashMap::new()).unwrap();

// assert!(is_model_contract(&tokens));
// }

// #[test]
// fn is_model_contract_ignore_systems() {
// let file_content = include_str!(
// "test_data/spawn-and-move/target/dev/dojo_examples::actions::actions.json"
// );
// let tokens = AbiParser::tokens_from_abi_string(file_content, &HashMap::new()).unwrap();

// assert!(!is_model_contract(&tokens));
// }

// #[test]
// fn is_model_contract_ignore_dojo_files() {
// let file_content =
// include_str!("test_data/spawn-and-move/target/dev/dojo::world::world.json");
// let tokens = AbiParser::tokens_from_abi_string(file_content, &HashMap::new()).unwrap();

// assert!(!is_model_contract(&tokens));
// }

// #[test]
// fn gather_models_ok() {
// let models =
//
// gather_models(&Utf8PathBuf::from("src/test_data/spawn-and-move/target/dev")).unwrap();

// assert_eq!(models.len(), 2);

// let pos = models.get("Position").unwrap();
// assert_eq!(pos.name, "Position");
// assert_eq!(pos.qualified_path, "dojo_examples::models::Position");

// let moves = models.get("Moves").unwrap();
// assert_eq!(moves.name, "Moves");
// assert_eq!(moves.qualified_path, "dojo_examples::models::Moves");
// }
// }
44 changes: 4 additions & 40 deletions crates/dojo-bindgen/src/plugins/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
use std::collections::HashMap;

use async_trait::async_trait;
use cainome::parser::tokens::Token;

use crate::error::BindgenResult;
use crate::{DojoMetadata, DojoModel};
use crate::DojoData;

pub mod typescript;
pub mod unity;
Expand All @@ -17,43 +14,10 @@ pub enum BuiltinPlugins {

#[async_trait]
pub trait BuiltinPlugin {
/// Generates the models bindings.
/// Each [`DojoModel`] contains all the types that are required to
/// generate a model bindings.
///
/// Warning, some types may be repeated in different models, due to the fact
/// that the Cairo ABI contains all types used in a contract.
///
/// It's at the plugin discretion to separate models bindings in modules/namespace
/// to avoid collision, or ensuring unicity of a type among all the models.
/// Generates code by executing the plugin.
///
/// # Arguments
///
/// * `models` - All the models found in the project.
async fn generate_models_bindings(
&self,
models: &HashMap<String, DojoModel>,
) -> BindgenResult<()>;

/// Generates the bindings for all the systems found in the given contract.
/// The `tokens` fields is the self contained ABI, which means all the tokens
/// to call any function in the contract is present in `tokens` ABI.
///
/// A plugin may use the `generate_models_bindings` to centralized the models,
/// bindings and ignore models type generation in this call.
///
/// # Arguments
///
/// * `contract_name` - Fully qualified name (with modules) of the contract.
/// * `tokens` - Tokens extracted from the ABI of the contract.
async fn generate_systems_bindings(
&self,
contract_name: &str,
tokens: HashMap<String, Vec<Token>>,
metadata: &DojoMetadata,
) -> BindgenResult<()>;
/// * `data` - Dojo data gathered from the compiled project.
async fn generate_code(&self, data: &DojoData) -> BindgenResult<()>;
}

// TODO: define the Plugin interface communicating data via stdin.
// Data must be easily serializable to be deserialized on the plugin side.
// We need to define one PluginInput struct and one PluginOutput struct.
Loading

0 comments on commit 78a2593

Please sign in to comment.