use crate::ast::{
ImportSpec, ImportStatement, Member, MonDocument, MonValue, MonValueKind,
SymbolTable as AstSymbolTable, TypeDef, TypeSpec,
};
use crate::error::{ResolverError, ValidationError};
use log::warn;
use miette::NamedSource;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct Resolver {
resolved_documents: HashMap<PathBuf, MonDocument>,
resolving_stack: Vec<(PathBuf, Option<ImportStatement>)>,
pub symbol_table: AstSymbolTable,
pub anchors: HashMap<String, MonValue>,
builtin_schemas_path: PathBuf,
}
impl Resolver {
#[must_use]
pub fn new() -> Self {
Self::with_builtin_path(Self::default_builtin_path())
}
pub fn with_builtin_path(path: PathBuf) -> Self {
Resolver {
resolved_documents: HashMap::new(),
resolving_stack: Vec::new(),
symbol_table: AstSymbolTable::new(),
anchors: HashMap::new(),
builtin_schemas_path: path,
}
}
fn default_builtin_path() -> PathBuf {
if let Ok(path) = std::env::var("MON_BUILTIN_PATH") {
return PathBuf::from(path);
}
if let Some(home) = std::env::var_os("HOME") {
let user_schemas = PathBuf::from(home).join(".mon/schemas");
if user_schemas.exists() {
return user_schemas;
}
}
#[cfg(unix)]
{
let system_path = PathBuf::from("/usr/share/mon/schemas");
if system_path.exists() {
return system_path;
}
}
warn!(
"Could not determine default schemas path, defaulting to current directory.
This is probably not intended"
);
PathBuf::from(".")
}
fn resolve_import_path(&self, import_path: &str, current_dir: &Path) -> PathBuf {
if let Some(builtin_path) = import_path.strip_prefix("mon:") {
return self
.builtin_schemas_path
.join(builtin_path)
.with_extension("mon");
}
current_dir.join(import_path)
}
pub fn resolve(
&mut self,
document: MonDocument,
source_text: &str,
file_path: PathBuf,
causing_import: Option<ImportStatement>,
) -> Result<MonDocument, ResolverError> {
if let Some((_, Some(existing_causing_import))) =
self.resolving_stack.iter().find(|(p, _)| p == &file_path)
{
let cycle_str = self
.resolving_stack
.iter()
.map(|(p, _)| p.to_string_lossy().to_string())
.collect::<Vec<String>>()
.join(" -> ");
return Err(ResolverError::CircularDependency {
cycle: format!("{} -> {}", cycle_str, file_path.to_string_lossy()),
src: NamedSource::new(file_path.to_string_lossy(), source_text.to_string()).into(),
span: (
existing_causing_import.pos_start,
existing_causing_import.pos_end - existing_causing_import.pos_start,
)
.into(),
});
}
self.resolving_stack
.push((file_path.clone(), causing_import));
let current_dir = file_path.parent().unwrap_or_else(|| Path::new("."));
let source_arc = Arc::new(source_text.to_string());
for import_statement in &document.imports {
let imported_path_str = import_statement.path.trim_matches('"');
let absolute_imported_path = self.resolve_import_path(imported_path_str, current_dir);
if self
.resolved_documents
.contains_key(&absolute_imported_path)
{
continue;
}
let imported_source_text =
std::fs::read_to_string(&absolute_imported_path).map_err(|_| {
ResolverError::ModuleNotFound {
path: imported_path_str.to_string(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_arc.to_string(),
)),
span: (
import_statement.pos_start,
import_statement.pos_end - import_statement.pos_start,
)
.into(),
}
})?;
let mut parser = crate::parser::Parser::new_with_name(
&imported_source_text,
absolute_imported_path.to_string_lossy().to_string(),
)?;
let imported_document = parser.parse_document()?;
let resolved_imported_document = self.resolve(
imported_document,
&imported_source_text,
absolute_imported_path.clone(),
Some(import_statement.clone()),
)?;
self.resolved_documents
.insert(absolute_imported_path, resolved_imported_document);
}
for import_statement in &document.imports {
if let ImportSpec::Named(specifiers) = &import_statement.spec {
let imported_path_str = import_statement.path.trim_matches('"');
let absolute_imported_path = current_dir.join(imported_path_str);
if let Some(imported_doc) = self.resolved_documents.get(&absolute_imported_path) {
if let MonValueKind::Object(members) = &imported_doc.root.kind {
for specifier in specifiers {
if !specifier.is_anchor {
for member in members {
if let Member::TypeDefinition(td) = member {
if td.name == specifier.name {
self.symbol_table
.types
.insert(specifier.name.clone(), td.clone());
}
}
}
}
}
}
}
}
}
if let MonValueKind::Object(members) = &document.root.kind {
for member in members {
match member {
Member::TypeDefinition(type_def) => {
self.symbol_table
.types
.insert(type_def.name.clone(), type_def.clone());
}
Member::Pair(pair) => {
if let Some(anchor_name) = &pair.value.anchor {
self.anchors.insert(anchor_name.clone(), pair.value.clone());
}
}
_ => {}
}
}
}
let resolved_root = self.resolve_value(document.root, &file_path, source_text)?;
let final_resolved_root =
self.validate_document_root(resolved_root, &document.imports, &file_path, source_text)?;
let resolved_doc = MonDocument {
root: final_resolved_root,
imports: document.imports, };
self.resolving_stack.pop();
Ok(resolved_doc)
}
fn resolve_value(
&mut self,
mut value: MonValue,
file_path: &PathBuf,
source_text: &str,
) -> Result<MonValue, ResolverError> {
let alias_span = value.get_source_span();
match &mut value.kind {
MonValueKind::Alias(alias_name) => {
let anchor_value = self.anchors.get(alias_name).ok_or_else(|| {
ResolverError::AnchorNotFound {
name: alias_name.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: alias_span,
}
})?;
Ok(anchor_value.clone()) }
MonValueKind::Object(members) => {
let mut resolved_members = Vec::new();
for member in members.drain(..) {
match member {
Member::Spread(spread_name) => {
let anchor_value = self.anchors.get(&spread_name).ok_or_else(|| {
ResolverError::AnchorNotFound {
name: spread_name.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: alias_span,
}
})?;
if let MonValueKind::Object(spread_members) = &anchor_value.kind {
let spread_members_clone = spread_members.clone();
for spread_member in spread_members_clone {
resolved_members.push(self.resolve_value_member(
spread_member,
file_path,
source_text,
)?);
}
} else {
return Err(ResolverError::SpreadOnNonObject {
name: spread_name.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: alias_span,
});
}
}
_ => {
resolved_members.push(self.resolve_value_member(
member,
file_path,
source_text,
)?);
}
}
}
let mut final_members_map: HashMap<String, Member> = HashMap::new();
for member in resolved_members {
if let Member::Pair(pair) = member {
final_members_map.insert(pair.key.clone(), Member::Pair(pair));
} else {
final_members_map.insert(format!("{member:?}"), member);
}
}
let final_members = final_members_map.into_values().collect();
Ok(MonValue {
kind: MonValueKind::Object(final_members),
anchor: value.anchor,
pos_start: value.pos_start,
pos_end: value.pos_end,
})
}
MonValueKind::Array(elements) => {
let mut resolved_elements = Vec::new();
for element in elements.drain(..) {
match element.kind {
MonValueKind::ArraySpread(spread_name) => {
let anchor_value = self.anchors.get(&spread_name).ok_or_else(|| {
ResolverError::AnchorNotFound {
name: spread_name.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: alias_span,
}
})?;
if let MonValueKind::Array(spread_elements) = &anchor_value.kind {
let spread_elements_clone = spread_elements.clone();
for spread_element in spread_elements_clone {
resolved_elements.push(self.resolve_value(
spread_element,
file_path,
source_text,
)?);
}
} else {
return Err(ResolverError::SpreadOnNonArray {
name: spread_name.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: alias_span,
});
}
}
_ => {
resolved_elements.push(self.resolve_value(
element,
file_path,
source_text,
)?);
}
}
}
Ok(MonValue {
kind: MonValueKind::Array(resolved_elements),
anchor: value.anchor,
pos_start: value.pos_start,
pos_end: value.pos_end,
})
}
_ => Ok(value), }
}
fn resolve_value_member(
&mut self,
mut member: Member,
file_path: &PathBuf,
source_text: &str,
) -> Result<Member, ResolverError> {
match &mut member {
Member::Pair(pair) => {
pair.value = self.resolve_value(pair.value.clone(), file_path, source_text)?;
Ok(member)
}
_ => Ok(member),
}
}
fn validate_document_root(
&mut self,
mut root_value: MonValue,
imports: &[ImportStatement], file_path: &PathBuf,
source_text: &str,
) -> Result<MonValue, ResolverError> {
if let MonValueKind::Object(members) = &mut root_value.kind {
for member in members.iter_mut() {
if let Member::Pair(pair) = member {
if let Some(type_spec) = &pair.validation {
self.validate_value(
&mut pair.value,
type_spec,
&pair.key,
imports, file_path,
source_text,
)?;
}
}
}
}
Ok(root_value)
}
fn validate_value(
&mut self,
value: &mut MonValue,
type_spec: &TypeSpec,
field_name: &str, imports: &[ImportStatement], file_path: &PathBuf,
source_text: &str,
) -> Result<(), ResolverError> {
match type_spec {
TypeSpec::Simple(type_name, _) => {
match type_name.as_str() {
"String" => {
if !matches!(value.kind, MonValueKind::String(_)) {
return Err(ResolverError::Validation(ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: "String".to_string(),
found_type: format!("{:?}", value.kind),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (value.pos_start, value.pos_end - value.pos_start).into(),
}));
}
}
"Number" => {
if !matches!(value.kind, MonValueKind::Number(_)) {
return Err(ResolverError::Validation(ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: "Number".to_string(),
found_type: format!("{:?}", value.kind),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (value.pos_start, value.pos_end - value.pos_start).into(),
}));
}
}
"Boolean" => {
if !matches!(value.kind, MonValueKind::Boolean(_)) {
return Err(ResolverError::Validation(ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: "Boolean".to_string(),
found_type: format!("{:?}", value.kind),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (value.pos_start, value.pos_end - value.pos_start).into(),
}));
}
}
"Null" => {
if !matches!(value.kind, MonValueKind::Null) {
return Err(ResolverError::Validation(ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: "Null".to_string(),
found_type: format!("{:?}", value.kind),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (value.pos_start, value.pos_end - value.pos_start).into(),
}));
}
}
"Object" => {
if !matches!(value.kind, MonValueKind::Object(_)) {
return Err(ResolverError::Validation(ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: "Object".to_string(),
found_type: format!("{:?}", value.kind),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (value.pos_start, value.pos_end - value.pos_start).into(),
}));
}
}
"Array" => {
if !matches!(value.kind, MonValueKind::Array(_)) {
return Err(ResolverError::Validation(ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: "Array".to_string(),
found_type: format!("{:?}", value.kind),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (value.pos_start, value.pos_end - value.pos_start).into(),
}));
}
}
"Any" => { }
_ => {
let (namespace, type_name_part) =
if let Some((ns, tn)) = type_name.split_once('.') {
(Some(ns), tn)
} else {
(None, type_name.as_str())
};
let type_def = if let Some(namespace) = namespace {
let import_statement = imports
.iter()
.find(|i| {
if let ImportSpec::Namespace(ns) = &i.spec {
ns == namespace
} else {
false
}
})
.ok_or_else(|| {
ResolverError::Validation(ValidationError::UndefinedType {
type_name: type_name.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (value.pos_start, value.pos_end - value.pos_start)
.into(),
})
})?;
let imported_path_str = import_statement.path.trim_matches('"');
let parent_dir = file_path.parent().ok_or_else(|| {
ResolverError::ModuleNotFound {
path: import_statement.path.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
import_statement.pos_start,
import_statement.pos_end - import_statement.pos_start,
)
.into(),
}
})?;
let absolute_imported_path = parent_dir.join(imported_path_str);
let imported_doc = self
.resolved_documents
.get(&absolute_imported_path)
.ok_or_else(|| {
ResolverError::ModuleNotFound {
path: absolute_imported_path.to_string_lossy().to_string(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (value.pos_start, value.pos_end - value.pos_start)
.into(),
}
})?;
if let MonValueKind::Object(members) = &imported_doc.root.kind {
members.iter().find_map(|m| {
if let Member::TypeDefinition(td) = m {
if td.name == type_name_part {
return Some(td.def_type.clone());
}
}
None
})
} else {
None
}
} else {
self.symbol_table
.types
.get(type_name_part)
.map(|td| td.def_type.clone())
};
if let Some(type_def) = type_def {
match type_def {
TypeDef::Struct(struct_def) => {
if let MonValueKind::Object(value_members) = &mut value.kind {
let mut value_map: HashMap<String, &mut MonValue> =
HashMap::new();
for member in value_members.iter_mut() {
if let Member::Pair(pair) = member {
value_map.insert(pair.key.clone(), &mut pair.value);
}
}
let mut new_members = Vec::new();
for field_def in &struct_def.fields {
if let Some(field_value) =
value_map.get_mut(&field_def.name)
{
self.validate_value(
field_value,
&field_def.type_spec,
&field_def.name,
imports, file_path,
source_text,
)?;
} else {
if field_def.default_value.is_none() {
return Err(ResolverError::Validation(
ValidationError::MissingField {
field_name: field_def.name.clone(),
struct_name: type_name.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
value.pos_start,
value.pos_end - value.pos_start,
)
.into(),
},
));
}
if let Some(default_value) =
&field_def.default_value
{
new_members.push(Member::Pair(
crate::ast::Pair {
key: field_def.name.clone(),
value: default_value.clone(),
validation: None,
},
));
}
}
}
value_members.extend(new_members);
for member in value_members.iter() {
if let Member::Pair(pair) = member {
if !struct_def
.fields
.iter()
.any(|f| f.name == pair.key)
{
return Err(ResolverError::Validation(
ValidationError::UnexpectedField {
field_name: pair.key.clone(),
struct_name: type_name.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
value.pos_start,
value.pos_end - value.pos_start,
)
.into(),
},
));
}
}
}
} else {
return Err(ResolverError::Validation(
ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: type_name.clone(),
found_type: format!("{:?}", value.kind),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
value.pos_start,
value.pos_end - value.pos_start,
)
.into(),
},
));
}
}
TypeDef::Enum(enum_def) => {
if let MonValueKind::EnumValue {
enum_name,
variant_name,
} = &value.kind
{
if enum_name != type_name {
return Err(ResolverError::Validation(
ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: format!("enum {}", type_name),
found_type: format!("enum {}", enum_name),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
value.pos_start,
value.pos_end - value.pos_start,
)
.into(),
},
));
}
if !enum_def.variants.contains(variant_name) {
return Err(ResolverError::Validation(
ValidationError::UndefinedEnumVariant {
variant_name: variant_name.clone(),
enum_name: type_name.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
value.pos_start,
value.pos_end - value.pos_start,
)
.into(),
},
));
}
} else {
return Err(ResolverError::Validation(
ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: format!("enum {}", type_name),
found_type: format!("{:?}", value.kind),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
value.pos_start,
value.pos_end - value.pos_start,
)
.into(),
},
));
}
}
}
} else {
return Err(ResolverError::Validation(
ValidationError::UndefinedType {
type_name: type_name.clone(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (value.pos_start, value.pos_end - value.pos_start).into(),
},
));
}
}
}
}
TypeSpec::Collection(collection_types, _) => {
if let MonValueKind::Array(elements) = &mut value.kind {
self.validate_collection(
elements,
collection_types,
field_name,
imports, file_path,
source_text,
)?;
} else {
return Err(ResolverError::Validation(ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: "Array".to_string(),
found_type: format!("{:?}", value.kind),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (value.pos_start, value.pos_end - value.pos_start).into(),
}));
}
}
TypeSpec::Spread(_, _) => {
return Ok(());
}
}
Ok(())
}
fn validate_collection(
&mut self,
elements: &mut [MonValue],
collection_types: &[TypeSpec],
field_name: &str,
imports: &[ImportStatement], file_path: &PathBuf,
source_text: &str,
) -> Result<(), ResolverError> {
if collection_types.len() == 1 && !matches!(collection_types[0], TypeSpec::Spread(_, _)) {
self.validate_value(
&mut elements[0],
&collection_types[0],
field_name,
imports, file_path,
source_text,
)?;
return Ok(());
}
if collection_types.len() == 1 && matches!(collection_types[0], TypeSpec::Spread(_, _)) {
if let TypeSpec::Spread(inner_type, _) = &collection_types[0] {
for element in elements {
self.validate_value(
element,
inner_type,
field_name,
imports,
file_path,
source_text,
)?;
}
return Ok(());
}
}
let has_spread = collection_types
.iter()
.any(|t| matches!(t, TypeSpec::Spread(_, _)));
if !has_spread {
if elements.len() != collection_types.len() {
return Err(ResolverError::Validation(ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: format!("tuple with {} elements", collection_types.len()),
found_type: format!("tuple with {} elements", elements.len()),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
elements.first().map_or(0, |e| e.pos_start),
elements.last().map_or(0, |e| e.pos_end)
- elements.first().map_or(0, |e| e.pos_start),
)
.into(),
}));
}
for (i, element) in elements.iter_mut().enumerate() {
self.validate_value(
element,
&collection_types[i],
field_name,
imports, file_path,
source_text,
)?;
}
return Ok(());
}
if collection_types.len() == 2
&& !matches!(collection_types[0], TypeSpec::Spread(_, _))
&& matches!(collection_types[1], TypeSpec::Spread(_, _))
{
if elements.is_empty() {
return Err(ResolverError::Validation(ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: "array with at least 1 element".to_string(),
found_type: "empty array".to_string(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
elements.first().map_or(0, |e| e.pos_start),
elements.last().map_or(0, |e| e.pos_end)
- elements.first().map_or(0, |e| e.pos_start),
)
.into(),
}));
}
self.validate_value(
&mut elements[0],
&collection_types[0],
field_name,
imports, file_path,
source_text,
)?;
if let TypeSpec::Spread(inner_type, _) = &collection_types[1] {
for element in &mut elements[1..] {
self.validate_value(
element,
inner_type,
field_name,
imports,
file_path,
source_text,
)?;
}
}
return Ok(());
}
if collection_types.len() == 2
&& matches!(collection_types[0], TypeSpec::Spread(_, _))
&& !matches!(collection_types[1], TypeSpec::Spread(_, _))
{
if elements.is_empty() {
return Err(ResolverError::Validation(ValidationError::TypeMismatch {
field_name: field_name.to_string(),
expected_type: "array with at least 1 element".to_string(),
found_type: "empty array".to_string(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
elements.first().map_or(0, |e| e.pos_start),
elements.last().map_or(0, |e| e.pos_end)
- elements.first().map_or(0, |e| e.pos_start),
)
.into(),
}));
}
let (head, last) = elements.split_at_mut(elements.len() - 1);
self.validate_value(
last.first_mut().unwrap(), &collection_types[1],
field_name,
imports, file_path,
source_text,
)?;
if let TypeSpec::Spread(inner_type, _) = &collection_types[0] {
for element in head {
self.validate_value(
element,
inner_type,
field_name,
imports,
file_path,
source_text,
)?;
}
}
return Ok(());
}
Err(ResolverError::Validation(
ValidationError::UnimplementedCollectionValidation {
field_name: field_name.to_string(),
src: Arc::from(NamedSource::new(
file_path.to_string_lossy(),
source_text.to_string(),
)),
span: (
elements.first().map_or(0, |e| e.pos_start),
elements.last().map_or(0, |e| e.pos_end)
- elements.first().map_or(0, |e| e.pos_start),
)
.into(),
},
))
}
}
impl Default for Resolver {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use crate::parser::Parser;
use crate::resolver::Resolver;
use miette::Report;
use std::fs;
use std::path::{Path, PathBuf};
fn resolve_ok(source: &str, file_name: &str) -> crate::ast::MonDocument {
let mut parser = Parser::new_with_name(source, file_name.to_string()).unwrap();
let document = parser.parse_document().unwrap();
let mut resolver = Resolver::new();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push(file_name);
match resolver.resolve(document, source, path, None) {
Ok(doc) => doc,
Err(err) => {
let report = Report::from(err);
panic!("{report:#}");
}
}
}
fn resolve_err(source: &str, file_name: &str) -> crate::error::ResolverError {
let mut parser = Parser::new_with_name(source, file_name.to_string()).unwrap();
let document = parser.parse_document().unwrap();
let mut resolver = Resolver::new();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push(file_name);
match resolver.resolve(document, source, path, None) {
Ok(_) => panic!("Expected a ResolverError, but got Ok"),
Err(err) => err,
}
}
#[test]
fn test_simple_alias_resolution() {
let source = r"{ &my_value: 123, alias_value: *my_value }";
let doc = resolve_ok(source, "test.mon");
let root_object = match doc.root.kind {
crate::ast::MonValueKind::Object(members) => members,
_ => panic!("Expected an object"),
};
let alias_member = root_object
.iter()
.find(|m| {
if let crate::ast::Member::Pair(p) = m {
p.key == "alias_value"
} else {
false
}
})
.unwrap();
if let crate::ast::Member::Pair(p) = alias_member {
assert_eq!(p.value.kind, crate::ast::MonValueKind::Number(123.0));
} else {
panic!("Expected a pair member");
}
}
#[test]
fn test_object_spread_resolution() {
let source = r#"{
&base_config: { host: "localhost", port: 8080 },
app_config: {
...*base_config,
port: 9000, // Override
debug: true,
}
}"#;
let doc = resolve_ok(source, "test.mon");
let root_object = match doc.root.kind {
crate::ast::MonValueKind::Object(members) => members,
_ => panic!("Expected an object"),
};
let app_config_member = root_object
.iter()
.find(|m| {
if let crate::ast::Member::Pair(p) = m {
p.key == "app_config"
} else {
false
}
})
.unwrap();
if let crate::ast::Member::Pair(p) = app_config_member {
let app_config_object = match &p.value.kind {
crate::ast::MonValueKind::Object(members) => members,
_ => panic!("Expected app_config to be an object"),
};
let host_member = app_config_object
.iter()
.find(|m| {
if let crate::ast::Member::Pair(p) = m {
p.key == "host"
} else {
false
}
})
.unwrap();
if let crate::ast::Member::Pair(p) = host_member {
assert_eq!(
p.value.kind,
crate::ast::MonValueKind::String("localhost".to_string())
);
} else {
panic!("Expected host to be a pair");
}
let port_member = app_config_object
.iter()
.find(|m| {
if let crate::ast::Member::Pair(p) = m {
p.key == "port"
} else {
false
}
})
.unwrap();
if let crate::ast::Member::Pair(p) = port_member {
assert_eq!(p.value.kind, crate::ast::MonValueKind::Number(9000.0));
} else {
panic!("Expected port to be a pair");
}
let debug_member = app_config_object
.iter()
.find(|m| {
if let crate::ast::Member::Pair(p) = m {
p.key == "debug"
} else {
false
}
})
.unwrap();
if let crate::ast::Member::Pair(p) = debug_member {
assert_eq!(p.value.kind, crate::ast::MonValueKind::Boolean(true));
} else {
panic!("Expected debug to be a pair");
}
} else {
panic!("Expected app_config to be a pair member");
}
}
#[test]
fn test_array_spread_resolution() {
let source = r#"{
&base_tags: ["tag1", "tag2"],
item_tags: [
"start",
...*base_tags,
"end",
]
}"#;
let doc = resolve_ok(source, "test.mon");
let root_object = match doc.root.kind {
crate::ast::MonValueKind::Object(members) => members,
_ => panic!("Expected an object"),
};
let item_tags_member = root_object
.iter()
.find(|m| {
if let crate::ast::Member::Pair(p) = m {
p.key == "item_tags"
} else {
false
}
})
.unwrap();
if let crate::ast::Member::Pair(p) = item_tags_member {
let item_tags_array = match &p.value.kind {
crate::ast::MonValueKind::Array(elements) => elements,
_ => panic!("Expected item_tags to be an array"),
};
assert_eq!(item_tags_array.len(), 4);
assert_eq!(
item_tags_array[0].kind,
crate::ast::MonValueKind::String("start".to_string())
);
assert_eq!(
item_tags_array[1].kind,
crate::ast::MonValueKind::String("tag1".to_string())
);
assert_eq!(
item_tags_array[2].kind,
crate::ast::MonValueKind::String("tag2".to_string())
);
assert_eq!(
item_tags_array[3].kind,
crate::ast::MonValueKind::String("end".to_string())
);
} else {
panic!("Expected item_tags to be a pair member");
}
}
#[test]
fn test_struct_validation_with_defaults_and_collections_ok() {
let source = r###"
{
User: #struct {
id(Number),
name(String),
email(String) = "default@example.com",
is_active(Boolean) = true,
roles([String...]),
permissions([String, Number]),
log_data([String, Any...]),
status_history([Boolean..., String]),
},
// Valid user with defaults
user1 :: User = {
id: 1,
name: "Alice",
roles: ["admin", "editor"],
permissions: ["read", 1],
log_data: ["login", { timestamp: "...", ip: "..." }],
status_history: [true, false, "active"],
},
// Valid user, omitting optional fields
user2 :: User = {
id: 2,
name: "Bob",
roles: [],
permissions: ["write", 2],
log_data: ["logout"],
status_history: ["inactive"],
},
}
"###;
let doc = resolve_ok(source, "test_validation.mon");
let root_object = match doc.root.kind {
crate::ast::MonValueKind::Object(members) => members,
_ => panic!("Expected an object"),
};
let user1_member = root_object
.iter()
.find(|m| {
if let crate::ast::Member::Pair(p) = m {
p.key == "user1"
} else {
false
}
})
.unwrap();
if let crate::ast::Member::Pair(p) = user1_member {
let user1_object = match &p.value.kind {
crate::ast::MonValueKind::Object(members) => members,
_ => panic!("Expected user1 to be an object"),
};
let email_member = user1_object
.iter()
.find(|m| {
if let crate::ast::Member::Pair(p) = m {
p.key == "email"
} else {
false
}
})
.unwrap();
if let crate::ast::Member::Pair(p) = email_member {
assert_eq!(
p.value.kind,
crate::ast::MonValueKind::String("default@example.com".to_string())
);
} else {
panic!("Expected email to be a pair");
}
let is_active_member = user1_object
.iter()
.find(|m| {
if let crate::ast::Member::Pair(p) = m {
p.key == "is_active"
} else {
false
}
})
.unwrap();
if let crate::ast::Member::Pair(p) = is_active_member {
assert_eq!(p.value.kind, crate::ast::MonValueKind::Boolean(true));
} else {
panic!("Expected is_active to be a pair");
}
} else {
panic!("Expected user1 to be a pair member");
}
}
#[test]
fn test_struct_validation_missing_required_field() {
let source = r###"
{
User: #struct { id(Number), name(String) },
invalid_user :: User = { id: 3 },
}
"###;
let err = resolve_err(source, "test_validation.mon");
match err {
crate::error::ResolverError::Validation(
crate::error::ValidationError::MissingField { field_name, .. },
) => {
assert_eq!(field_name, "name");
}
_ => panic!("Expected MissingField error, but got {err:?}"),
}
}
#[test]
fn test_struct_validation_wrong_id_type() {
let source = r###"
{
User: #struct { id(Number), name(String) },
invalid_user :: User = { id: "four", name: "Charlie" },
}
"###;
let err = resolve_err(source, "test_validation.mon");
match err {
crate::error::ResolverError::Validation(
crate::error::ValidationError::TypeMismatch {
field_name,
expected_type,
found_type,
..
},
) => {
assert_eq!(field_name, "id");
assert_eq!(expected_type, "Number");
assert!(found_type.contains("String"));
}
_ => panic!("Expected TypeMismatch error for id, but got {err:?}"),
}
}
#[test]
fn test_struct_validation_unexpected_field() {
let source = r###"
{
User: #struct { id(Number), name(String) },
invalid_user :: User = { id: 5, name: "David", age: 30 },
}
"###;
let err = resolve_err(source, "test_validation.mon");
match err {
crate::error::ResolverError::Validation(
crate::error::ValidationError::UnexpectedField { field_name, .. },
) => {
assert_eq!(field_name, "age");
}
_ => panic!("Expected UnexpectedField error, but got {err:?}"),
}
}
#[test]
fn test_struct_validation_roles_type_mismatch() {
let source = r###"
{
User: #struct { roles([String...]) },
invalid_user :: User = { roles: ["viewer", 123] },
}
"###;
let err = resolve_err(source, "test_validation.mon");
match err {
crate::error::ResolverError::Validation(
crate::error::ValidationError::TypeMismatch {
field_name,
expected_type,
found_type,
..
},
) => {
assert_eq!(field_name, "roles");
assert_eq!(expected_type, "String");
assert!(found_type.contains("Number"));
}
_ => panic!("Expected TypeMismatch error for roles, but got {err:?}"),
}
}
#[test]
fn test_struct_validation_permissions_length_mismatch() {
let source = r###"
{
User: #struct { permissions([String, Number]) },
invalid_user :: User = { permissions: ["read"] },
}
"###;
let err = resolve_err(source, "test_validation.mon");
match err {
crate::error::ResolverError::Validation(
crate::error::ValidationError::TypeMismatch {
field_name,
expected_type,
found_type,
..
},
) => {
assert_eq!(field_name, "permissions");
assert!(expected_type.contains("tuple with 2 elements"));
assert!(found_type.contains("tuple with 1 elements"));
}
_ => panic!("Expected TypeMismatch error for permissions length, but got {err:?}"),
}
}
#[test]
fn test_struct_validation_permissions_type_mismatch() {
let source = r###"
{
User: #struct { permissions([String, Number]) },
invalid_user :: User = { permissions: [8, "write"] },
}
"###;
let err = resolve_err(source, "test_validation.mon");
match err {
crate::error::ResolverError::Validation(
crate::error::ValidationError::TypeMismatch {
field_name,
expected_type,
found_type,
..
},
) => {
assert_eq!(field_name, "permissions");
assert_eq!(expected_type, "String");
assert!(found_type.contains("Number"));
}
_ => panic!("Expected TypeMismatch error for permissions types, but got {err:?}"),
}
}
#[test]
fn test_struct_validation_log_data_first_type_mismatch() {
let source = r###"
{
User: #struct { log_data([String, Any...]) },
invalid_user :: User = { log_data: [123, "event"] },
}
"###;
let err = resolve_err(source, "test_validation.mon");
match err {
crate::error::ResolverError::Validation(
crate::error::ValidationError::TypeMismatch {
field_name,
expected_type,
found_type,
..
},
) => {
assert_eq!(field_name, "log_data");
assert_eq!(expected_type, "String");
assert!(found_type.contains("Number"));
}
_ => panic!("Expected TypeMismatch error for log_data first type, but got {err:?}"),
}
}
#[test]
fn test_struct_validation_status_history_last_type_mismatch() {
let source = r###"
{
User: #struct { status_history([Boolean..., String]) },
invalid_user :: User = { status_history: [true, 123] },
}
"###;
let err = resolve_err(source, "test_validation.mon");
match err {
crate::error::ResolverError::Validation(
crate::error::ValidationError::TypeMismatch {
field_name,
expected_type,
found_type,
..
},
) => {
assert_eq!(field_name, "status_history");
assert_eq!(expected_type, "String");
assert!(found_type.contains("Number"));
}
_ => {
panic!("Expected TypeMismatch error for status_history last type, but got {err:?}")
}
}
}
#[test]
fn test_nested_struct_validation_ok() {
let source = r###"
{
Profile: #struct {
username(String),
email(String),
},
User: #struct {
id(Number),
profile(Profile),
},
// Valid nested struct
user1 :: User = {
id: 1,
profile: {
username: "alice",
email: "alice@example.com",
},
},
}
"###;
resolve_ok(source, "test_nested_ok.mon");
}
#[test]
fn test_nested_struct_validation_err() {
let source = r###"
{
Profile: #struct {
username(String),
email(String),
},
User: #struct {
id(Number),
profile(Profile),
},
// Invalid: Nested struct has wrong type for username
user2 :: User = {
id: 2,
profile: {
username: 123,
email: "bob@example.com",
},
},
}
"###;
let err = resolve_err(source, "test_nested_err.mon");
match err {
crate::error::ResolverError::Validation(
crate::error::ValidationError::TypeMismatch {
field_name,
expected_type,
found_type,
..
},
) => {
assert_eq!(field_name, "username");
assert_eq!(expected_type, "String");
assert!(found_type.contains("Number"));
}
_ => panic!("Expected TypeMismatch error for username, but got {err:?}"),
}
}
#[test]
fn test_cross_file_validation() {
let source = fs::read_to_string("tests/cross_file_main.mon").unwrap();
resolve_ok(&source, "tests/cross_file_main.mon");
}
#[test]
fn test_parser_for_schemas_file() {
let source = fs::read_to_string("tests/cross_file_schemas.mon").unwrap();
let mut parser = Parser::new_with_name(&source, "test.mon".to_string()).unwrap();
let _ = parser.parse_document().unwrap();
}
#[test]
fn test_named_import_validation() {
let source = fs::read_to_string("tests/named_import_main.mon").unwrap();
resolve_ok(&source, "tests/named_import_main.mon");
}
use super::*;
use tempfile::TempDir;
fn test_resolver_with_builtin(builtin_path: PathBuf) -> Resolver {
Resolver::with_builtin_path(builtin_path)
}
fn create_test_file(dir: &Path, name: &str, content: &str) -> PathBuf {
let path = dir.join(name);
fs::write(&path, content).unwrap();
path
}
#[test]
fn test_mon_uri_resolution() {
let temp_dir = TempDir::new().unwrap();
let builtin_path = temp_dir.path().join("schemas");
fs::create_dir_all(builtin_path.join("types")).unwrap();
create_test_file(
&builtin_path.join("types"),
"linter.mon",
r#"{
LintConfig: #struct {
max_depth(Number) = 5
}
}"#,
);
let resolver = test_resolver_with_builtin(builtin_path.clone());
let resolved = resolver.resolve_import_path("mon:types/linter", Path::new("."));
assert_eq!(resolved, builtin_path.join("types/linter.mon"));
}
#[test]
fn test_relative_path_unchanged() {
let resolver = Resolver::new();
let current_dir = Path::new("/project/src");
let resolved = resolver.resolve_import_path("./config.mon", current_dir);
assert_eq!(resolved, PathBuf::from("/project/src/./config.mon"));
let resolved = resolver.resolve_import_path("../shared/types.mon", current_dir);
assert_eq!(resolved, PathBuf::from("/project/src/../shared/types.mon"));
}
#[test]
fn test_absolute_path_preserved() {
let resolver = Resolver::new();
let current_dir = Path::new("/project");
let resolved = resolver.resolve_import_path("/usr/share/schemas/common.mon", current_dir);
assert_eq!(resolved, PathBuf::from("/usr/share/schemas/common.mon"));
}
#[test]
fn test_mon_uri_nested_paths() {
let temp_dir = TempDir::new().unwrap();
let builtin_path = temp_dir.path().join("schemas");
fs::create_dir_all(builtin_path.join("types/database")).unwrap();
let resolver = test_resolver_with_builtin(builtin_path.clone());
let resolved = resolver.resolve_import_path("mon:types/database/postgres", Path::new("."));
assert_eq!(resolved, builtin_path.join("types/database/postgres.mon"));
}
#[test]
fn test_mon_uri_with_full_resolution() {
let temp_dir = TempDir::new().unwrap();
let builtin_path = temp_dir.path().join("schemas");
fs::create_dir_all(builtin_path.join("types")).unwrap();
let schema_content = r#"{
TestType: #struct {
field(String)
}
}"#;
create_test_file(&builtin_path.join("types"), "test.mon", schema_content);
let main_content = r###"
import { TestType } from "mon:types/test"
{
value :: TestType = { field: "hello" }
}
"###;
let main_path = create_test_file(temp_dir.path(), "main.mon", main_content);
let mut parser = crate::parser::Parser::new_with_name(
main_content,
main_path.to_string_lossy().to_string(),
)
.unwrap();
let doc = parser.parse_document().unwrap();
let mut resolver = test_resolver_with_builtin(builtin_path);
let result = resolver.resolve(doc, main_content, main_path.clone(), None);
assert!(result.is_ok(), "Resolution should succeed");
assert!(resolver.symbol_table.types.contains_key("TestType"));
}
#[test]
fn test_mixed_imports() {
let temp_dir = TempDir::new().unwrap();
let builtin_path = temp_dir.path().join("schemas");
fs::create_dir_all(builtin_path.join("types")).unwrap();
create_test_file(
&builtin_path.join("types"),
"builtin.mon",
r#"{ BuiltinType: #struct { id(Number) } }"#,
);
create_test_file(
temp_dir.path(),
"local.mon",
r#"{ LocalType: #struct { name(String) } }"#,
);
let main_content = r###"
import { BuiltinType } from "mon:types/builtin"
import { LocalType } from "./local.mon"
{
builtin :: BuiltinType = { id: 1 },
local :: LocalType = { name: "test" }
}
"###;
let main_path = create_test_file(temp_dir.path(), "main.mon", main_content);
let mut parser = crate::parser::Parser::new_with_name(
main_content,
main_path.to_string_lossy().to_string(),
)
.unwrap();
let doc = parser.parse_document().unwrap();
let mut resolver = test_resolver_with_builtin(builtin_path);
let result = resolver.resolve(doc, main_content, main_path, None);
assert!(result.is_ok());
assert!(resolver.symbol_table.types.contains_key("BuiltinType"));
assert!(resolver.symbol_table.types.contains_key("LocalType"));
}
#[test]
fn test_mon_uri_not_found() {
let temp_dir = TempDir::new().unwrap();
let builtin_path = temp_dir.path().join("schemas");
fs::create_dir_all(&builtin_path).unwrap();
let main_content = r###"
import { Missing } from "mon:types/nonexistent"
{}
"###;
let main_path = create_test_file(temp_dir.path(), "main.mon", main_content);
let mut parser = crate::parser::Parser::new_with_name(
main_content,
main_path.to_string_lossy().to_string(),
)
.unwrap();
let doc = parser.parse_document().unwrap();
let mut resolver = test_resolver_with_builtin(builtin_path);
let result = resolver.resolve(doc, main_content, main_path, None);
assert!(matches!(result, Err(ResolverError::ModuleNotFound { .. })))
}
#[test]
fn test_default_builtin_path() {
let path = Resolver::default_builtin_path();
assert!(path.is_absolute() || path == Path::new("."));
}
#[test]
fn test_mon_uri_auto_adds_extension() {
let temp_dir = TempDir::new().unwrap();
let builtin_path = temp_dir.path().join("schemas");
let resolver = test_resolver_with_builtin(builtin_path.clone());
let resolved = resolver.resolve_import_path("mon:config/app", Path::new("."));
assert_eq!(resolved, builtin_path.join("config/app.mon"));
assert!(resolved.to_string_lossy().ends_with(".mon"));
}
#[test]
fn test_environment_variable_override() {
std::env::set_var("MON_BUILTIN_PATH", "/custom/builtin/path");
let path = Resolver::default_builtin_path();
std::env::remove_var("MON_BUILTIN_PATH");
assert_eq!(path, PathBuf::from("/custom/builtin/path"));
}
#[test]
fn test_circular_dependency_with_mon_imports() {
let temp_dir = TempDir::new().unwrap();
let builtin_path = temp_dir.path().join("schemas");
fs::create_dir_all(&builtin_path).unwrap();
create_test_file(
&builtin_path,
"a.mon",
r###"import { B } from "mon:b" { A: #struct { b(B) } }"###,
);
create_test_file(
&builtin_path,
"b.mon",
r###"import { A } from "mon:a" { B: #struct { a(A) } }"###,
);
let main_content = r###"import { A } from "mon:a" {}"###;
let main_path = create_test_file(temp_dir.path(), "main.mon", main_content);
let mut parser = crate::parser::Parser::new_with_name(
main_content,
main_path.to_string_lossy().to_string(),
)
.unwrap();
let doc = parser.parse_document().unwrap();
let mut resolver = test_resolver_with_builtin(builtin_path);
let result = resolver.resolve(doc, main_content, main_path, None);
assert!(matches!(
result,
Err(ResolverError::CircularDependency { .. })
));
}
}