Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/txtx-addon-kit/src/types/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pub fn arg_checker_with_ctx(
}
// we don't have an "any" type, so if the array is of type null, we won't check types
if let Type::Array(inner) = typing {
if let Type::Null(_) = **inner {
if matches!(**inner, Type::Null | Type::TypedNull(_)) {
has_type_match = true;
break;
}
Expand Down
1 change: 1 addition & 0 deletions crates/txtx-addon-kit/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub mod functions;
pub mod package;
pub mod signers;
pub mod stores;
pub mod type_compatibility;
pub mod types;

pub const CACHED_NONCE: &str = "cached_nonce";
Expand Down
163 changes: 162 additions & 1 deletion crates/txtx-addon-kit/src/types/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::path::PathBuf;
use crate::helpers::fs::FileLocation;
use crate::types::AuthorizationContext;

use super::types::Value;
use super::types::{Type, Value};
use serde_json::json;
use serde_json::Value as JsonValue;
use test_case::test_case;
Expand Down Expand Up @@ -72,3 +72,164 @@ fn test_auth_context_get_path_from_str(path_str: &str, expected: &str) {
let result = auth_context.get_file_location_from_path_buf(&PathBuf::from(path_str)).unwrap();
assert_eq!(result.to_string(), expected);
}

// =============================================================================
// Type::Null and Type::TypedNull serialization/deserialization tests
// =============================================================================

/// Test that Type serialization produces expected string format
#[test_case(Type::null(), "null"; "untyped null")]
#[test_case(Type::typed_null(Type::String), "null<string>"; "typed null string")]
#[test_case(Type::typed_null(Type::Integer), "null<integer>"; "typed null integer")]
#[test_case(Type::typed_null(Type::Bool), "null<bool>"; "typed null bool")]
#[test_case(Type::typed_null(Type::Array(Box::new(Type::Integer))), "null<array[integer]>"; "typed null array")]
#[test_case(Type::typed_null(Type::typed_null(Type::String)), "null<null<string>>"; "nested typed null")]
#[test_case(Type::Array(Box::new(Type::typed_null(Type::String))), "array[null<string>]"; "array of typed null")]
fn test_type_null_to_string_format(typ: Type, expected_str: &str) {
assert_eq!(typ.to_string(), expected_str);
}

/// Test that Type string parsing produces correct types
#[test_case("null", Type::null(); "parse untyped null")]
#[test_case("null<string>", Type::typed_null(Type::String); "parse typed null string")]
#[test_case("null<integer>", Type::typed_null(Type::Integer); "parse typed null integer")]
#[test_case("null<array[integer]>", Type::typed_null(Type::Array(Box::new(Type::Integer))); "parse typed null array")]
#[test_case("array[null<string>]", Type::Array(Box::new(Type::typed_null(Type::String))); "parse array of typed null")]
#[test_case("array[string]", Type::Array(Box::new(Type::String)); "parse array string")]
fn test_type_null_parsing(input: &str, expected: Type) {
let parsed = Type::try_from(input.to_string()).unwrap();
assert_eq!(parsed, expected);
}

/// Test full serde roundtrip for Type (serialize to JSON, deserialize back)
#[test_case(Type::null(); "roundtrip untyped null")]
#[test_case(Type::typed_null(Type::String); "roundtrip typed null string")]
#[test_case(Type::typed_null(Type::Array(Box::new(Type::String))); "roundtrip typed null array")]
#[test_case(Type::typed_null(Type::typed_null(Type::Integer)); "roundtrip nested typed null")]
#[test_case(Type::Array(Box::new(Type::typed_null(Type::String))); "roundtrip array of typed null")]
fn test_type_null_serde_roundtrip(typ: Type) {
let serialized = serde_json::to_string(&typ).unwrap();
let deserialized: Type = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, typ);
}

/// Test that Type::Null acts as wildcard in type compatibility matching
#[test]
fn test_type_null_wildcard_pattern() {
// Type::Array(Box::new(Type::Null)) is used as "any array" pattern
let any_array_pattern = Type::Array(Box::new(Type::Null));
let string_array = Type::Array(Box::new(Type::String));
let int_array = Type::Array(Box::new(Type::Integer));

// The pattern match used in type_compatibility.rs
fn is_any_array_pattern(t: &Type) -> bool {
matches!(t, Type::Array(inner) if matches!(**inner, Type::Null | Type::TypedNull(_)))
}

assert!(is_any_array_pattern(&any_array_pattern));
assert!(!is_any_array_pattern(&string_array));
assert!(!is_any_array_pattern(&int_array));
}

// =============================================================================
// Deep nesting tests
// =============================================================================

/// Test deeply nested array types
#[test_case("array[array[string]]",
Type::Array(Box::new(Type::Array(Box::new(Type::String))));
"two level array nesting")]
#[test_case("array[array[array[integer]]]",
Type::Array(Box::new(Type::Array(Box::new(Type::Array(Box::new(Type::Integer))))));
"three level array nesting")]
fn test_deep_array_nesting(input: &str, expected: Type) {
let parsed = Type::try_from(input.to_string()).unwrap();
assert_eq!(parsed, expected);
// Verify roundtrip
assert_eq!(parsed.to_string(), input);
}

/// Test deeply nested null types
#[test_case("null<null<null<string>>>",
Type::typed_null(Type::typed_null(Type::typed_null(Type::String)));
"three level null nesting")]
fn test_deep_null_nesting(input: &str, expected: Type) {
let parsed = Type::try_from(input.to_string()).unwrap();
assert_eq!(parsed, expected);
// Verify roundtrip
assert_eq!(parsed.to_string(), input);
}

/// Test cross-nested types (array containing null, null containing array)
#[test_case("array[array[null<integer>]]",
Type::Array(Box::new(Type::Array(Box::new(Type::typed_null(Type::Integer)))));
"array of array of typed null")]
#[test_case("null<null<array[string]>>",
Type::typed_null(Type::typed_null(Type::Array(Box::new(Type::String))));
"null of null of array")]
#[test_case("array[null<array[null<string>]>]",
Type::Array(Box::new(Type::typed_null(Type::Array(Box::new(Type::typed_null(Type::String))))));
"alternating array and null nesting")]
fn test_cross_nesting(input: &str, expected: Type) {
let parsed = Type::try_from(input.to_string()).unwrap();
assert_eq!(parsed, expected);
// Verify roundtrip
assert_eq!(parsed.to_string(), input);
}

// =============================================================================
// Error handling tests
// =============================================================================

/// Test that empty inner types produce clear errors
#[test_case("null<>", "empty inner type"; "empty null inner")]
#[test_case("array[]", "empty inner type"; "empty array inner")]
#[test_case("addon()", "empty addon id"; "empty addon id")]
fn test_empty_inner_type_errors(input: &str, expected_error_contains: &str) {
let result = Type::try_from(input.to_string());
assert!(result.is_err(), "Expected error for input: {}", input);
let err = result.unwrap_err();
assert!(
err.contains(expected_error_contains),
"Error '{}' should contain '{}'",
err,
expected_error_contains
);
}

/// Test that invalid inner types produce contextual errors
#[test_case("null<invalid_type>", "invalid type in null<invalid_type>"; "invalid null inner")]
#[test_case("array[bad_type]", "invalid type in array[bad_type]"; "invalid array inner")]
fn test_invalid_inner_type_errors(input: &str, expected_error_contains: &str) {
let result = Type::try_from(input.to_string());
assert!(result.is_err(), "Expected error for input: {}", input);
let err = result.unwrap_err();
assert!(
err.contains(expected_error_contains),
"Error '{}' should contain '{}'",
err,
expected_error_contains
);
}

/// Test that malformed type strings produce errors
#[test_case("null<string"; "unclosed null angle bracket")]
#[test_case("array[string"; "unclosed array bracket")]
#[test_case("null<>string>"; "malformed null")]
#[test_case("unknown_type"; "completely unknown type")]
fn test_malformed_type_errors(input: &str) {
let result = Type::try_from(input.to_string());
assert!(result.is_err(), "Expected error for malformed input: {}", input);
}

/// Test error propagation through nested parsing
#[test]
fn test_nested_error_propagation() {
// Error in deeply nested type should propagate with context
let result = Type::try_from("array[null<invalid>]".to_string());
assert!(result.is_err());
let err = result.unwrap_err();
// Should contain context about where the error occurred
assert!(err.contains("array["), "Error should mention outer array context");
assert!(err.contains("invalid"), "Error should mention the invalid type");
}
83 changes: 83 additions & 0 deletions crates/txtx-addon-kit/src/types/type_compatibility.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use super::types::{Type, Value};

/// Type compatibility checking for txtx types
pub struct TypeChecker;

impl TypeChecker {
/// Check if a value matches any of the expected types
pub fn matches_any(value: &Value, expected_types: &[Type]) -> bool {
expected_types.iter().any(|expected| Self::matches(value, expected))
}

/// Check if a value matches a specific type
pub fn matches(value: &Value, expected_type: &Type) -> bool {
match (value.get_type(), expected_type) {
// Both are addons - any addon matches any addon type
// We don't check the specific addon ID for flexibility
(Type::Addon(_), Type::Addon(_)) => true,

// Empty arrays match any array type
(Type::Array(_), _) if value.expect_array().is_empty() => true,

// Array with null inner type accepts any array
// This is our "any array" pattern
(_, Type::Array(inner)) if matches!(**inner, Type::Null | Type::TypedNull(_)) => true,

// Otherwise require exact type match
(actual_type, expected) => actual_type.eq(expected),
}
}

/// Check if two types are compatible (for type checking without values)
pub fn types_compatible(actual: &Type, expected: &Type) -> bool {
match (actual, expected) {
// Any addon type matches any other addon type
(Type::Addon(_), Type::Addon(_)) => true,

// Array with null inner type accepts any array
(Type::Array(_), Type::Array(inner)) if matches!(**inner, Type::Null | Type::TypedNull(_)) => true,

// Otherwise require exact match
_ => actual == expected,
}
}
}

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

#[test]
fn test_addon_compatibility() {
let value = Value::addon(vec![1, 2, 3], "addon1");
let addon_type = Type::Addon("addon2".to_string());

assert!(TypeChecker::matches(&value, &addon_type));
}

#[test]
fn test_empty_array_compatibility() {
let value = Value::array(vec![]);
let array_type = Type::Array(Box::new(Type::String));

assert!(TypeChecker::matches(&value, &array_type));
}

#[test]
fn test_any_array_compatibility() {
let value = Value::array(vec![Value::string("test".to_string())]);
let any_array_type = Type::Array(Box::new(Type::Null));

assert!(TypeChecker::matches(&value, &any_array_type));
}

#[test]
fn test_exact_type_match() {
let value = Value::string("test".to_string());
let string_type = Type::String;
let int_type = Type::Integer;

assert!(TypeChecker::matches(&value, &string_type));
assert!(!TypeChecker::matches(&value, &int_type));
}
}
Loading
Loading