gruggers 0.3.2

rust implementation of the grug language
Documentation
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;

use crate::ntstring::NTStr;
use crate::ast::{Argument, GrugType};
use crate::arena::Arena;

use allocator_api2::boxed::Box;

pub struct ModApi {
	entities: HashMap<Arc<str>, ModApiEntity<'static>>,
	game_functions: HashMap<Arc<str>, ModApiGameFn<'static>>,
	_arena: Arena,
}

impl ModApi {
	pub fn entities<'a>(&'a self) -> &'a HashMap<Arc<str>, ModApiEntity<'a>> {
		&self.entities
	}
	pub fn game_functions<'a>(&'a self) -> &'a HashMap<Arc<str>, ModApiGameFn<'a>> {
		&self.game_functions
	}
}

#[derive(Debug)]
pub struct ModApiEntity<'a> {
	#[allow(dead_code)]
	pub(crate) description: Option<String>,
	pub(crate) on_fns: Vec<(Arc<str>, ModApiOnFn<'a>)>,
}

impl<'a> ModApiEntity<'a> {
	pub fn get_on_fn(&self, name: &str) -> Option<(usize, &ModApiOnFn<'_>)> {
		self.on_fns.iter().enumerate().find_map(|(i, (fn_name, func))| (name == &**fn_name).then_some((i, func)))
	}
}

#[derive(Debug)]
pub struct ModApiOnFn<'a> {
	#[allow(dead_code)]
	pub(super) description: Option<String>,
	pub(super) arguments: Vec<Argument<'a>>,
}

#[derive(Debug)]
pub struct ModApiGameFn<'a> {
	#[allow(dead_code)]
	pub(super) description: Option<String>,
	pub(super) return_ty: GrugType<'a>,
	pub(super) arguments: Vec<Argument<'a>>,
}

// TODO: Add Display impl for all variants
#[derive(Debug)]
pub enum ModApiError{
	JsonError(json::Error),
	IOError(std::io::Error),
	EntitiesNotObject,
	OnFunctionsNotObject{
		entity_name: String,
	},
	OnFnsArgumentsNotArray{
		entity_name: String,
		on_fn_name: String,
	},
	OnFnArgumentMissingName{
		entity_name: String,
		on_fn_name: String,
	},
	OnFnArgumentMissingType{
		entity_name: String,
		on_fn_name: String,
	},
	OnFnArgumentResource {
		entity_name: String,
		on_fn_name: String,
		argument_name: String,
	},
	OnFnArgumentEntity {
		entity_name: String,
		on_fn_name: String,
		argument_name: String,
	},
	GameFnsNotObject,
	OnFnArgumentVoid {
		entity_name: String,
		on_fn_name: String,
		argument_name: String,
	},
	GameFnArgumentsNotArray{
		game_fn_name: String,
	},
	GameFnArgumentMissingName{
		game_fn_name: String,
	},
	GameFnArgumentMissingType{
		game_fn_name: String,
		argument_name: String,
	},
	GameFnArgumentVoid{
		game_fn_name: String,
		argument_name: String,
	},
	GameFnResourceMissingExtension{
		game_fn_name: String,
		argument_name: String,
	},
	GameFnEntityMissingType{
		game_fn_name: String,
		argument_name: String,
	},
	GameFnReturnsEntity{
		game_fn_name: String,
	},
	GameFnReturnsResource{
		game_fn_name: String,
	},
	GameFnNotProvided {
		game_fn_name: String,
	}
}

impl From<json::Error> for ModApiError {
	fn from(other: json::Error) -> ModApiError {
		ModApiError::JsonError(other)
	}
}

impl From<std::io::Error> for ModApiError {
	fn from(other: std::io::Error) -> ModApiError {
		ModApiError::IOError(other)
	}
}

pub fn get_mod_api<P: AsRef<Path>>(mod_api_path: P) -> Result<ModApi, ModApiError> {
	let mod_api_text = std::fs::read_to_string(mod_api_path)?;
	get_mod_api_from_text(&mod_api_text)
}

pub fn get_mod_api_from_text(mod_api_text: &str) -> Result<ModApi, ModApiError> {
	let arena = Arena::new();
	let mod_api_json = json::parse(mod_api_text)?;
	// "entities" object
	let entities = &mod_api_json["entities"];
	if !entities.is_object() {
		return Err(ModApiError::EntitiesNotObject);
	}

	let entities = entities.entries().map(|(entity_name, entity_values)| {
		// optional "description" string
		let description = entity_values["description"].as_str().map(str::to_string);

		// optional "on_fns" object
		let on_fns = &entity_values["on_functions"];
		if !on_fns.is_object() && !on_fns.is_null() {
			return Err(ModApiError::OnFunctionsNotObject{
				entity_name: entity_name.to_string(),
			});
		}
		let on_fns = on_fns.entries().map(|(fn_name, fn_values)| {
			// optional "description" string
			let description = fn_values["description"].as_str().map(str::to_string);
			
			// optional "arguments" object
			let arguments = &fn_values["arguments"];
			if !arguments.is_array() && !arguments.is_null(){
				return Err(ModApiError::OnFnsArgumentsNotArray{
					entity_name: entity_name.to_string(),
					on_fn_name: fn_name.to_string(),
				});
			}
			let arguments = arguments.members().map(|argument_values| {
				// optional "name" string
				let argument_name = argument_values["name"].as_str().ok_or(ModApiError::OnFnArgumentMissingName{
					entity_name: entity_name.to_string(),
					on_fn_name: fn_name.to_string(),
				})?;
				let argument_name = Box::leak(NTStr::box_from_str_in(argument_name, &arena));
				// "type" string
				let ty = argument_values["type"].as_str().ok_or(ModApiError::OnFnArgumentMissingType{
					entity_name: entity_name.to_string(),
					on_fn_name: fn_name.to_string(),
				})?;
				let ty = match ty {
					// arguments can't be void
					"void"     => Err(ModApiError::OnFnArgumentVoid{
						entity_name: entity_name.to_string(),
						on_fn_name: fn_name.to_string(),
						argument_name: argument_name.to_string(),
					})?,
					"bool"     => GrugType::Bool,
					"number"   => GrugType::Number,
					"string"   => GrugType::String,
					"id"       => GrugType::Id{custom_name: None},
					"resource" => Err(ModApiError::OnFnArgumentResource{
						entity_name: entity_name.to_string(),
						on_fn_name: fn_name.to_string(),
						argument_name: argument_name.to_string(),
					})?,
					"entity" => Err(ModApiError::OnFnArgumentEntity{
						entity_name: entity_name.to_string(),
						on_fn_name: fn_name.to_string(),
						argument_name: argument_name.to_string(),
					})?,
					type_name => {
						let extra_value = Box::leak(NTStr::box_from_str_in(type_name, &arena));
						GrugType::Id {
							custom_name: Some(extra_value.as_ntstrptr()),
						}
					}
				};
				Ok(Argument{
					name: unsafe{argument_name.as_ntstrptr().detach_lifetime()},
					ty,
				})
			}).collect::<Result<Vec<_>, ModApiError>>()?;
			let fn_name = Arc::from(fn_name);
			Ok((Arc::clone(&fn_name), ModApiOnFn{
				description,
				arguments,
			}))
		}).collect::<Result<Vec<_>, _>>()?;
		Ok((Arc::from(entity_name), ModApiEntity{
			description,
			on_fns
		}))
	}).collect::<Result<HashMap<_, _>, ModApiError>>()?;
	
	// "game_functions" object
	let game_functions = &mod_api_json["game_functions"];
	if !game_functions.is_object() {
		return Err(ModApiError::GameFnsNotObject);
	}

	let game_functions = game_functions.entries().map(|(fn_name, game_fn_values)| {
		// optional "description" string
		let description = game_fn_values["description"].as_str().map(str::to_string);

		// optional "arguments" object
		let arguments = &game_fn_values["arguments"];
		if !arguments.is_array() && !arguments.is_null(){
			return Err(ModApiError::GameFnArgumentsNotArray{
				game_fn_name: fn_name.to_string(),
			});
		}
		let arguments = arguments.members().map(|argument_values| {
			// "name" string
			let argument_name = argument_values["name"].as_str().ok_or(ModApiError::GameFnArgumentMissingName{
				game_fn_name: fn_name.to_string(),
			})?;
			let argument_name = Box::leak(NTStr::box_from_str_in(argument_name, &arena));
			// "type" string
			let ty = argument_values["type"].as_str().ok_or(ModApiError::GameFnArgumentMissingType{
				game_fn_name: fn_name.to_string(),
				argument_name: argument_name.to_string(),
			})?;
			let ty = match ty {
				// arguments can't be void
				"void"     => Err(ModApiError::GameFnArgumentVoid{
					game_fn_name: fn_name.to_string(),
					argument_name: argument_name.to_string(),
				})?,
				"bool"     => GrugType::Bool,
				"number"      => GrugType::Number,
				"string"   => GrugType::String,
				"id"       => GrugType::Id{custom_name: None},
				"entity"   => {
					let entity_type = argument_values["entity_type"].as_str().ok_or(ModApiError::GameFnEntityMissingType{
						game_fn_name: fn_name.to_string(),
						argument_name: argument_name.to_string(),
					})?;
					GrugType::Entity {
						entity_type: (!entity_type.is_empty()).then(|| {
							let entity_type = Box::leak(NTStr::box_from_str_in(entity_type, &arena));
							entity_type.as_ntstrptr()
						})
					}
				},
				"resource" => {
					let extension = argument_values["resource_extension"].as_str().ok_or(ModApiError::GameFnResourceMissingExtension{
						game_fn_name: fn_name.to_string(),
						argument_name: argument_name.to_string(),
					})?;
					let extension = Box::leak(NTStr::box_from_str_in(extension, &arena)).as_ntstrptr();
					GrugType::Resource {
						extension
					}
				}
				type_name => {
					let extra_value = Box::leak(NTStr::box_from_str_in(type_name, &arena)).as_ntstrptr();
					GrugType::Id {
						custom_name: Some(extra_value),
					}
				}
			};
			Ok(Argument{
				name: unsafe{argument_name.as_ntstrptr().detach_lifetime()},
				ty,
			})
		}).collect::<Result<Vec<_>, ModApiError>>()?;

		// optional "return_type" string
		let return_ty = game_fn_values["return_type"].as_str().unwrap_or("void");
		let return_ty = match return_ty {
			"void"     => GrugType::Void,
			"bool"     => GrugType::Bool,
			"number"      => GrugType::Number,
			"string"   => GrugType::String,
			"id"       => GrugType::Id{custom_name: None},
			"entity" => Err(ModApiError::GameFnReturnsEntity{
				game_fn_name: fn_name.to_string(),
			})?,
			"resource" => Err(ModApiError::GameFnReturnsResource{
				game_fn_name: fn_name.to_string(),
			})?,
			type_name => {
				let extra_value = Box::leak(NTStr::box_from_str_in(type_name, &arena)).as_ntstrptr();
				GrugType::Id {
					custom_name: Some(extra_value),
				}
			}
		};
		let fn_name = Arc::from(fn_name);
		Ok((Arc::clone(&fn_name), ModApiGameFn{
			return_ty,
			description,
			arguments
		}))
	}).collect::<Result<HashMap<_, _>, ModApiError>>()?;

	Ok(ModApi{
		entities: unsafe{std::mem::transmute::<HashMap<Arc<str>, ModApiEntity<'_>>, _>(entities)},
		game_functions: unsafe{std::mem::transmute::<HashMap<Arc<str>, ModApiGameFn<'_>>, _>(game_functions)},
		_arena: arena,
	})
}