| #[cfg(test)] |
| mod tests { |
| use chrono::{Datelike, Offset, Timelike}; |
| use firm_lang::parser::dsl::{ParsedValue, parse_source}; |
|
|
| #[test] |
| fn test_basic_entity_parsing() { |
| let source = r#" |
| person john_doe { |
| name = "John Doe" |
| age = 42 |
| active = true |
| } |
| "#; |
|
|
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
|
|
| assert_eq!(entities.len(), 1); |
| assert_eq!(entities[0].entity_type(), Some("person")); |
| assert_eq!(entities[0].id(), Some("john_doe")); |
| } |
|
|
| #[test] |
| fn test_multiline_string_with_intentional_indentation() { |
| let source = r#"contact test { |
| code = """ |
| function example() { |
| if (true) { |
| return "nested"; |
| } |
| } |
| """ |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
| let field = &fields[0]; |
|
|
| assert_eq!(field.id(), Some("code")); |
| assert_eq!( |
| field.value(), |
| Ok(ParsedValue::String( |
| "function example() {\n if (true) {\n return \"nested\";\n }\n}" |
| .to_string() |
| )) |
| ); |
| } |
|
|
| #[test] |
| fn test_multiline_string_single_line() { |
| let source = r#"contact test { note = """Just one line""" }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
| let field = &fields[0]; |
|
|
| assert_eq!(field.id(), Some("note")); |
| assert_eq!( |
| field.value(), |
| Ok(ParsedValue::String("Just one line".to_string())) |
| ); |
| } |
|
|
| #[test] |
| fn test_multiline_string_empty_lines() { |
| let source = r#"contact test { |
| text = """ |
| First paragraph |
| |
| Second paragraph after empty line |
| """ |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
| let field = &fields[0]; |
|
|
| assert_eq!(field.id(), Some("text")); |
| assert_eq!( |
| field.value(), |
| Ok(ParsedValue::String( |
| "First paragraph\n\nSecond paragraph after empty line".to_string() |
| )) |
| ); |
| } |
|
|
| #[test] |
| fn test_mixed_string_types() { |
| let source = r#"contact test { |
| name = "John Doe" |
| bio = """ |
| Software engineer |
| Based in San Francisco |
| """ |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| assert_eq!( |
| fields[0].value(), |
| Ok(ParsedValue::String("John Doe".to_string())) |
| ); |
| assert_eq!( |
| fields[1].value(), |
| Ok(ParsedValue::String( |
| "Software engineer\nBased in San Francisco".to_string() |
| )) |
| ); |
| } |
|
|
| #[test] |
| fn test_number_fields() { |
| let source = r#"contact test { age = 42, height = 5.9 }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
|
|
| assert_eq!( |
| parsed.entities()[0].fields()[0].value(), |
| Ok(ParsedValue::Integer(42)) |
| ); |
| assert_eq!( |
| parsed.entities()[0].fields()[1].value(), |
| Ok(ParsedValue::Float(5.9)) |
| ); |
| } |
|
|
| #[test] |
| fn test_boolean_fields() { |
| let source = r#"contact test { active = true, verified = false }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
|
|
| assert_eq!( |
| parsed.entities()[0].fields()[0].value(), |
| Ok(ParsedValue::Boolean(true)) |
| ); |
| assert_eq!( |
| parsed.entities()[0].fields()[1].value(), |
| Ok(ParsedValue::Boolean(false)) |
| ); |
| } |
|
|
| #[test] |
| fn test_currency_decimal_amount() { |
| let source = r#"contact test { |
| salary = 75000.50 USD |
| bonus = 5000.00 EUR |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| if let Ok(ParsedValue::Currency { amount, currency }) = &fields[0].value() { |
| assert_eq!(amount.to_string(), "75000.50"); |
| assert_eq!(currency.code(), "USD"); |
| } else { |
| panic!("Expected currency value"); |
| } |
| if let Ok(ParsedValue::Currency { amount, currency }) = &fields[1].value() { |
| assert_eq!(amount.to_string(), "5000.00"); |
| assert_eq!(currency.code(), "EUR"); |
| } else { |
| panic!("Expected currency value"); |
| } |
| } |
|
|
| #[test] |
| fn test_currency_integer_amount() { |
| let source = r#"contact test { |
| salary = 50000 USD |
| budget = 1000 DKK |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| if let Ok(ParsedValue::Currency { amount, currency }) = &fields[0].value() { |
| assert_eq!(amount.to_string(), "50000"); |
| assert_eq!(currency.code(), "USD"); |
| } else { |
| panic!("Expected currency value"); |
| } |
| if let Ok(ParsedValue::Currency { amount, currency }) = &fields[1].value() { |
| assert_eq!(amount.to_string(), "1000"); |
| assert_eq!(currency.code(), "DKK"); |
| } else { |
| panic!("Expected currency value"); |
| } |
| } |
|
|
| #[test] |
| fn test_multiple_currency_fields() { |
| let source = r#"project budget_2023 { |
| total_budget = 100000 USD |
| marketing_spend = 25000.50 USD |
| r_and_d = 40000 USD |
| operations = 15000.25 USD |
| contingency = 19999.25 USD |
| }"#; |
|
|
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| assert_eq!(entities.len(), 1); |
|
|
| let fields = entities[0].fields(); |
| assert_eq!(fields.len(), 5); |
|
|
| for field in fields { |
| match field.value() { |
| Ok(ParsedValue::Currency { |
| amount: _, |
| currency, |
| }) => { |
| assert_eq!(currency.code(), "USD"); |
| } |
| _ => panic!("Expected currency value"), |
| } |
| } |
| } |
|
|
| #[test] |
| fn test_entity_references() { |
| let source = r#"contact test { |
| manager = contact.john_doe |
| company = organization.acme_corp |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| assert_eq!( |
| fields[0].value(), |
| Ok(ParsedValue::EntityReference { |
| entity_type: "contact".to_string(), |
| entity_id: "john_doe".to_string() |
| }) |
| ); |
| assert_eq!( |
| fields[1].value(), |
| Ok(ParsedValue::EntityReference { |
| entity_type: "organization".to_string(), |
| entity_id: "acme_corp".to_string() |
| }) |
| ); |
| } |
|
|
| #[test] |
| fn test_field_references() { |
| let source = r#"contact test { |
| title = contact.john_doe.title |
| department = organization.acme_corp.department |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| assert_eq!( |
| fields[0].value(), |
| Ok(ParsedValue::FieldReference { |
| entity_type: "contact".to_string(), |
| entity_id: "john_doe".to_string(), |
| field_id: "title".to_string() |
| }) |
| ); |
| assert_eq!( |
| fields[1].value(), |
| Ok(ParsedValue::FieldReference { |
| entity_type: "organization".to_string(), |
| entity_id: "acme_corp".to_string(), |
| field_id: "department".to_string() |
| }) |
| ); |
| } |
|
|
| #[test] |
| fn test_simple_lists() { |
| let source = r#"contact test { |
| tags = ["work", "important", "urgent"] |
| scores = [85, 92, 78] |
| flags = [true, false, true] |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| assert_eq!( |
| fields[0].value(), |
| Ok(ParsedValue::List(vec![ |
| ParsedValue::String("work".to_string()), |
| ParsedValue::String("important".to_string()), |
| ParsedValue::String("urgent".to_string()) |
| ])) |
| ); |
| assert_eq!( |
| fields[1].value(), |
| Ok(ParsedValue::List(vec![ |
| ParsedValue::Integer(85), |
| ParsedValue::Integer(92), |
| ParsedValue::Integer(78) |
| ])) |
| ); |
| assert_eq!( |
| fields[2].value(), |
| Ok(ParsedValue::List(vec![ |
| ParsedValue::Boolean(true), |
| ParsedValue::Boolean(false), |
| ParsedValue::Boolean(true) |
| ])) |
| ); |
| } |
|
|
| #[test] |
| fn test_empty_lists() { |
| let source = r#"contact test { tags = [] }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
|
|
| assert_eq!( |
| parsed.entities()[0].fields()[0].value(), |
| Ok(ParsedValue::List(vec![])) |
| ); |
| } |
|
|
| #[test] |
| fn test_nested_lists() { |
| let source = r#"contact test { |
| matrix = [[1, 2, 3], [4, 5, 6]] |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| assert_eq!( |
| fields[0].value(), |
| Ok(ParsedValue::List(vec![ |
| ParsedValue::List(vec![ |
| ParsedValue::Integer(1), |
| ParsedValue::Integer(2), |
| ParsedValue::Integer(3) |
| ]), |
| ParsedValue::List(vec![ |
| ParsedValue::Integer(4), |
| ParsedValue::Integer(5), |
| ParsedValue::Integer(6) |
| ]) |
| ])) |
| ); |
| } |
|
|
| #[test] |
| fn test_date_fields() { |
| let source = r#"contact test { |
| birthday = 1990-05-15 |
| start_date = 2023-01-01 |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| if let Ok(ParsedValue::DateTime(dt)) = &fields[0].value() { |
| assert_eq!(dt.naive_local().date().year(), 1990); |
| assert_eq!(dt.naive_local().date().month(), 5); |
| assert_eq!(dt.naive_local().date().day(), 15); |
| } else { |
| panic!("Expected date value"); |
| } |
|
|
| if let Ok(ParsedValue::DateTime(dt)) = &fields[1].value() { |
| assert_eq!(dt.naive_local().date().year(), 2023); |
| assert_eq!(dt.naive_local().date().month(), 1); |
| assert_eq!(dt.naive_local().date().day(), 1); |
| } else { |
| panic!("Expected date value"); |
| } |
| } |
|
|
| #[test] |
| fn test_multiple_date_fields() { |
| let source = r#"project timeline { |
| start_date = 2023-01-15 |
| phase_1_end = 2023-03-31 |
| phase_2_end = 2023-06-30 |
| final_deadline = 2023-12-31 |
| review_date = 2024-01-15 |
| }"#; |
|
|
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| assert_eq!(entities.len(), 1); |
|
|
| let fields = entities[0].fields(); |
| assert_eq!(fields.len(), 5); |
|
|
| |
| for field in fields { |
| assert!(matches!(field.value(), Ok(ParsedValue::DateTime(_)))); |
| } |
| } |
|
|
| #[test] |
| fn test_datetime_local_timezone() { |
| let source = r#"contact test { |
| meeting = 2025-01-15 at 14:30 UTC+2 |
| deadline = 2025-02-28 at 23:59 UTC-5 |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| if let Ok(ParsedValue::DateTime(dt)) = &fields[0].value() { |
| assert_eq!(dt.naive_local().date().year(), 2025); |
| assert_eq!(dt.naive_local().date().month(), 1); |
| assert_eq!(dt.naive_local().date().day(), 15); |
| assert_eq!(dt.naive_local().time().hour(), 14); |
| assert_eq!(dt.naive_local().time().minute(), 30); |
| assert_eq!(dt.offset().fix().local_minus_utc(), 2 * 3600); |
| } else { |
| panic!("Expected datetime value"); |
| } |
|
|
| if let Ok(ParsedValue::DateTime(dt)) = &fields[1].value() { |
| assert_eq!(dt.naive_local().date().year(), 2025); |
| assert_eq!(dt.naive_local().date().month(), 2); |
| assert_eq!(dt.naive_local().date().day(), 28); |
| assert_eq!(dt.naive_local().time().hour(), 23); |
| assert_eq!(dt.naive_local().time().minute(), 59); |
| assert_eq!(dt.offset().fix().local_minus_utc(), -5 * 3600); |
| } else { |
| panic!("Expected datetime value"); |
| } |
| } |
|
|
| #[test] |
| fn test_datetime_single_digit_hour() { |
| let source = r#"contact test { |
| meeting = 2025-01-15 at 9:30 |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| if let Ok(ParsedValue::DateTime(dt)) = &fields[0].value() { |
| assert_eq!(dt.naive_local().date().year(), 2025); |
| assert_eq!(dt.naive_local().date().month(), 1); |
| assert_eq!(dt.naive_local().date().day(), 15); |
| assert_eq!(dt.naive_local().time().hour(), 9); |
| assert_eq!(dt.naive_local().time().minute(), 30); |
| } else { |
| panic!("Expected datetime value"); |
| } |
| } |
|
|
| #[test] |
| fn test_datetime_utc_negative_offset() { |
| let source = r#"contact test { |
| meeting = 2025-01-15 at 14:30 UTC-7 |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| if let Ok(ParsedValue::DateTime(dt)) = &fields[0].value() { |
| assert_eq!(dt.offset().fix().local_minus_utc(), -7 * 3600); |
| } else { |
| panic!("Expected datetime value"); |
| } |
| } |
|
|
| #[test] |
| fn test_datetime_utc_positive_offset() { |
| let source = r#"contact test { |
| meeting = 2025-01-15 at 14:30 UTC+3 |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| if let Ok(ParsedValue::DateTime(dt)) = &fields[0].value() { |
| assert_eq!(dt.offset().fix().local_minus_utc(), 3 * 3600); |
| } else { |
| panic!("Expected datetime value"); |
| } |
| } |
|
|
| #[test] |
| fn test_datetime_utc() { |
| let source = r#"contact test { |
| meeting = 2025-01-15 at 14:30 UTC |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| if let Ok(ParsedValue::DateTime(dt)) = &fields[0].value() { |
| assert_eq!(dt.offset().fix().local_minus_utc(), 0); |
| } else { |
| panic!("Expected datetime value"); |
| } |
| } |
|
|
| #[test] |
| fn test_heterogeneous_list_error() { |
| let source = r#"contact test { |
| mixed = ["string", 42] |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| match fields[0].value() { |
| Err(_) => { |
| |
| } |
| Ok(_) => panic!("Expected error for heterogeneous list"), |
| } |
| } |
|
|
| #[test] |
| fn test_invalid_boolean_error() { |
| let source = r#"contact test { flag = maybe }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| assert!(parsed.has_error()); |
| } |
|
|
| #[test] |
| fn test_invalid_integer_error() { |
| let source = r#"contact test { age = 42abc }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| assert!(parsed.has_error()); |
| } |
|
|
| #[test] |
| fn test_invalid_float_error() { |
| let source = r#"contact test { height = 5.9.2 }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| assert!(parsed.has_error()); |
| } |
|
|
| #[test] |
| fn test_invalid_currency_format_error() { |
| let source = r#"contact test { salary = 50000USD }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| assert!(parsed.has_error()); |
| } |
|
|
| #[test] |
| fn test_invalid_currency_amount_error() { |
| let source = r#"contact test { salary = abc USD }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| assert!(parsed.has_error()); |
| } |
|
|
| #[test] |
| fn test_invalid_currency_code_error() { |
| let source = r#"contact test { |
| salary = 50000 DOLLARS |
| bonus = 5000 US |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| assert!(parsed.has_error()); |
| } |
|
|
| #[test] |
| fn test_invalid_reference_format_error() { |
| let source = r#"contact test { ref = contact.too.many.parts.here }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| assert!(parsed.has_error()); |
| } |
|
|
| #[test] |
| fn test_invalid_date_error() { |
| let source = r#"contact test { |
| bad_date1 = 2023-13-01 |
| bad_date2 = 2023-02-30 |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| assert!(fields[0].value().is_err()); |
| assert!(fields[1].value().is_err()); |
| } |
|
|
| #[test] |
| fn test_invalid_datetime_error() { |
| let source = r#"contact test { |
| bad_datetime1 = 2023-13-01 at 14:30 |
| bad_datetime2 = 2023-01-01 at 25:30 |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| assert!(fields[0].value().is_err()); |
| assert!(fields[1].value().is_err()); |
| } |
|
|
| #[test] |
| fn test_invalid_timezone_error() { |
| let source = r#"contact test { |
| bad_tz = 2023-01-01 at 14:30 UTC+25 |
| }"#; |
| let parsed = parse_source(String::from(source), None).unwrap(); |
| let entities = parsed.entities(); |
| let fields = entities[0].fields(); |
|
|
| assert!(fields[0].value().is_err()); |
| } |
| } |
|
|