diff --git a/.github/workflows/lint-and-test.yaml b/.github/workflows/lint-and-test.yaml index 91445f2..f0aa6be 100644 --- a/.github/workflows/lint-and-test.yaml +++ b/.github/workflows/lint-and-test.yaml @@ -18,9 +18,9 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4.2.0 + - uses: actions/checkout@v4.2.2 - name: Rust cache - uses: Swatinem/rust-cache@v2.7.3 + uses: Swatinem/rust-cache@v2.7.5 with: prefix-key: cargo-debug-${{ matrix.platform }} @@ -43,9 +43,9 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4.2.0 + - uses: actions/checkout@v4.2.2 - name: Rust cache - uses: Swatinem/rust-cache@v2.7.3 + uses: Swatinem/rust-cache@v2.7.5 with: prefix-key: cargo-debug-${{ matrix.platform }} - name: Install dependencies (ubuntu only) @@ -65,9 +65,9 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4.2.0 + - uses: actions/checkout@v4.2.2 - name: Rust cache - uses: Swatinem/rust-cache@v2.7.3 + uses: Swatinem/rust-cache@v2.7.5 with: prefix-key: cargo-release-${{ matrix.platform }} @@ -82,7 +82,7 @@ jobs: run: cargo build --release - name: Node.js cache - uses: actions/cache@v4.1.0 + uses: actions/cache@v4.1.2 with: path: ${{ github.workspace }}/gui/frontend/.next/cache # Generate a new cache whenever packages or source files change. @@ -93,7 +93,7 @@ jobs: run: npm ci - name: Sync node version - uses: actions/setup-node@v4.0.4 + uses: actions/setup-node@v4.1.0 with: node-version: 'lts/*' cache: 'npm' @@ -126,7 +126,7 @@ jobs: mv ./target/release/dar2oar ./build - name: Upload a Build Artifact - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.3 with: name: DAR_to_OAR_Converter-${{runner.os}}-Portable path: | diff --git a/.github/workflows/release-gui.yaml b/.github/workflows/release-gui.yaml index 976fc74..0b5377f 100644 --- a/.github/workflows/release-gui.yaml +++ b/.github/workflows/release-gui.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: Install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-latest' @@ -34,7 +34,7 @@ jobs: prefix-key: cargo-${{ matrix.platform }} - name: Sync node version and setup cache - uses: actions/setup-node@v4.0.4 + uses: actions/setup-node@v4.1.0 with: node-version: 'lts/*' cache: 'npm' diff --git a/Cargo.lock b/Cargo.lock index 9d76f18..c2a2218 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,7 +246,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -281,7 +281,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -657,7 +657,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -934,7 +934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -944,7 +944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -974,8 +974,8 @@ dependencies = [ "serde", "serde-untagged", "serde_json", + "snafu", "temp-dir", - "thiserror", "tokio", "tokio-stream", "tracing", @@ -1003,7 +1003,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1014,7 +1014,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1027,7 +1027,7 @@ dependencies = [ "deluxe-macros", "once_cell", "proc-macro2", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1040,7 +1040,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1055,7 +1055,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1089,7 +1089,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1164,7 +1164,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1268,7 +1268,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1386,7 +1386,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1483,7 +1483,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1751,7 +1751,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -1830,7 +1830,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -2520,7 +2520,7 @@ dependencies = [ "proc-macro-crate 2.0.2", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -2888,7 +2888,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -2935,7 +2935,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -3171,7 +3171,7 @@ dependencies = [ "deluxe", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -3429,9 +3429,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" @@ -3472,7 +3472,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -3518,9 +3518,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] @@ -3538,13 +3538,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -3555,7 +3555,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -3578,7 +3578,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -3629,7 +3629,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -3759,7 +3759,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -3894,9 +3894,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -4071,7 +4071,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.72", + "syn 2.0.87", "tauri-utils", "thiserror", "time", @@ -4089,7 +4089,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", "tauri-codegen", "tauri-utils", ] @@ -4330,7 +4330,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -4401,9 +4401,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -4425,7 +4425,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -4569,7 +4569,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -4867,7 +4867,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -4901,7 +4901,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5061,7 +5061,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -5160,7 +5160,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -5171,7 +5171,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] @@ -5609,7 +5609,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.87", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8b4391e..3461784 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,10 @@ default-members = ["cli"] resolver = "2" [workspace.dependencies] -serde = { version = "1.0.210", features = ["derive"] } # Implement (De)Serialize +serde = { version = "1.0.214", features = ["derive"] } # Implement (De)Serialize serde_json = "1.0.132" # core: To json/GUI: To avoid generate_context error. snafu = "0.8.5" -tokio = { version = "1.40.0" } # Async runtime +tokio = { version = "1.41.1" } # Async runtime tracing = { version = "0.1.40" } # logger tracing-subscriber = "0.3.18" tracing-appender = "0.2.3" diff --git a/core/Cargo.toml b/core/Cargo.toml index 30c06b8..66b2400 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -26,7 +26,7 @@ jwalk = "0.8.1" # To parallel traverse dir recursivly serde = { workspace = true } # Implement (De)Serializer serde-untagged = "0.1.6" serde_json = { workspace = true } # Json converter -thiserror = "1.0.63" # define errors type +snafu = { workspace = true } # define errors type tokio = { workspace = true, features = [ "fs", "io-util", diff --git a/core/src/condition_parser/actor.rs b/core/src/condition_parser/actor.rs index 8e8677c..079dcfe 100644 --- a/core/src/condition_parser/actor.rs +++ b/core/src/condition_parser/actor.rs @@ -1,30 +1,32 @@ //! Parses an actor-related condition based on the provided arguments and condition name. -use super::dar_interface::ParseError; -use super::macros::get_try_into; +use super::errors::{ParseError, Result}; use crate::{ conditions::{CompareValues, ConditionSet, IsActorBase}, - dar_syntax::syntax::FnArg, + dar_syntax::FnArgs, values::{ActorValue, ActorValueType, Cmp, NumericValue}, }; /// Parses an actor-related condition based on the provided arguments and condition name. /// # Errors /// If parsing fails. -pub(super) fn parse_actor( - condition_name: &str, - args: Vec>, +pub(super) fn parse_actor<'a>( + condition_name: &'a str, + mut args: FnArgs<'a>, negated: bool, -) -> Result { - let create_actor_cond = - |comparison: Cmp, actor_value_type: ActorValueType| -> Result { +) -> Result> { + let mut create_actor_cond = + |comparison: Cmp, actor_value_type: ActorValueType| -> Result> { + let actor_value = args.pop_front()?.try_into()?; + let value_b = args.pop_front()?.try_into()?; + Ok(ConditionSet::CompareValues(CompareValues { negated, value_a: NumericValue::ActorValue(ActorValue { - actor_value: get_try_into!(args[0], "Hex | Decimal | Float")?, + actor_value, actor_value_type, }), comparison, - value_b: NumericValue::StaticValue(get_try_into!(args[1], "Float")?), + value_b: NumericValue::StaticValue(value_b), ..Default::default() })) }; @@ -38,15 +40,15 @@ pub(super) fn parse_actor( "IsActorValuePercentageEqualTo" => create_actor_cond(Cmp::Eq, ActorValueType::Percentage)?, "IsActorValuePercentageLessThan" => create_actor_cond(Cmp::Lt, ActorValueType::Percentage)?, "IsActorBase" => ConditionSet::IsActorBase(IsActorBase { + actor_base: args.pop_front()?.try_into()?, negated, - actor_base: get_try_into!(args[0], "PluginValue")?, ..Default::default() }), unknown_condition => { - return Err(ParseError::UnexpectedValue( - "IsActor(Value|Base|Max|Percentage)(EqualTo|LessThan)".into(), - unknown_condition.into(), - )) + return Err(ParseError::UnexpectedValue { + expected: "IsActor(Value|Base|Max|Percentage)(EqualTo|LessThan)".into(), + actual: unknown_condition.into(), + }) } }) } @@ -55,7 +57,7 @@ pub(super) fn parse_actor( mod tests { use super::*; use crate::{ - dar_syntax::syntax::NumberLiteral, + dar_syntax::{ast::fn_args::fn_args, FnArg, NumberLiteral}, values::{ActorValue, Cmp, NumericLiteral, NumericValue, PluginValue, StaticValue}, }; use pretty_assertions::assert_eq; @@ -64,7 +66,7 @@ mod tests { fn test_parse_actor_is_actor_value_equal_to() { // test inputs let condition_name = "IsActorValueEqualTo"; - let args = vec![ + let args = fn_args![ FnArg::Number(NumberLiteral::Float(3.3)), // actor_value FnArg::Number(NumberLiteral::Float(3.5)), // compare value ]; @@ -94,7 +96,7 @@ mod tests { #[test] fn test_parse_actor_is_actor_base() { let condition_name = "IsActorBase"; - let args = vec![FnArg::PluginValue { + let args = fn_args![FnArg::PluginValue { plugin_name: "Skyrim.esm", form_id: NumberLiteral::Hex(0x0000_0007), }]; diff --git a/core/src/condition_parser/compare.rs b/core/src/condition_parser/compare.rs index 2bf4f3b..5e30f76 100644 --- a/core/src/condition_parser/compare.rs +++ b/core/src/condition_parser/compare.rs @@ -1,9 +1,8 @@ //! Parses a comparison-based condition for plugin values. -use super::dar_interface::ParseError; -use super::macros::get_try_into; +use super::errors::{ParseError, Result}; use crate::{ conditions::{CompareValues, ConditionSet}, - dar_syntax::syntax::FnArg, + dar_syntax::FnArgs, values::{Cmp, NumericValue, PluginValue}, }; @@ -12,21 +11,13 @@ use crate::{ /// /// # Errors /// Parsing failed. -pub(super) fn parse_compare( - condition_name: &str, - args: Vec>, +pub(super) fn parse_compare<'a>( + condition_name: &'a str, + mut args: FnArgs<'a>, negated: bool, -) -> Result { - let plugin_value: PluginValue = get_try_into!( - args[0], - "Plugin value: in ValueEqualTo | ValueLessThan 1st arg", - "None" - )?; - let static_value = get_try_into!( - args[1], - " float(e.g. 1.0): in ValueEqualTo | ValueLessThan 2nd arg", - "None" - )?; +) -> Result> { + let plugin_value: PluginValue = args.pop_front()?.try_into()?; + let static_value = args.pop_front()?.try_into()?; let create_compare = |comparison: Cmp| { ConditionSet::CompareValues(CompareValues { @@ -42,10 +33,10 @@ pub(super) fn parse_compare( "ValueEqualTo" => create_compare(Cmp::Eq), "ValueLessThan" => create_compare(Cmp::Lt), _ => { - return Err(ParseError::UnexpectedValue( - "ValueEqualTo or ValueLessThan".into(), - condition_name.into(), - )) + return Err(ParseError::UnexpectedValue { + expected: "ValueEqualTo or ValueLessThan".into(), + actual: condition_name.into(), + }) } }) } diff --git a/core/src/condition_parser/conditions.rs b/core/src/condition_parser/conditions.rs index fdaa062..f7f82cc 100644 --- a/core/src/condition_parser/conditions.rs +++ b/core/src/condition_parser/conditions.rs @@ -1,25 +1,25 @@ //! Parses a high-level condition set based on the provided syntax. use super::actor::parse_actor; use super::compare::parse_compare; -use super::dar_interface::ParseError; use super::equip::parse_equip; +use super::errors::{ParseError, Result}; use super::faction::parse_faction; use super::has::parse_has; -use super::macros::{gen_cond, get_try_into, GetArg as _}; +use super::macros::gen_cond; use crate::conditions::{ And, Condition, ConditionSet, CurrentGameTime, CurrentWeather, IsClass, IsCombatStyle, IsInLocation, IsMovementDirection, IsParentCell, IsRace, IsVoiceType, IsWorldSpace, IsWorn, IsWornHasKeyword, Level, Or, RandomCondition, }; -use crate::dar_syntax::syntax::{self, Expression}; +use crate::dar_syntax::{Condition as DarCondition, Expression}; use crate::values::{Cmp, DirectionValue}; /// Parses a high-level condition set based on the provided syntax. /// # Errors /// Parsing failed. -pub fn parse_conditions(input: syntax::Condition) -> Result { +pub fn parse_conditions(input: DarCondition) -> Result { Ok(match input { - syntax::Condition::And(conditions) => { + DarCondition::And(conditions) => { let mut inner_conditions = vec![]; for condition in conditions { inner_conditions.push(parse_conditions(condition)?); @@ -29,7 +29,7 @@ pub fn parse_conditions(input: syntax::Condition) -> Result { + DarCondition::Or(conditions) => { let mut inner_conditions = vec![]; for condition in conditions { inner_conditions.push(parse_conditions(condition)?); @@ -39,25 +39,25 @@ pub fn parse_conditions(input: syntax::Condition) -> Result parse_condition(expression)?, + DarCondition::Exp(expression) => parse_condition(expression)?, }) } /// Parses a conditional expression and translates it into a corresponding [`ConditionSet`]. /// # Errors /// Parsing failed. -fn parse_condition(condition: Expression<'_>) -> Result { +fn parse_condition(condition: Expression) -> Result { let Expression { negated, fn_name, - args, + mut args, } = condition; Ok(match fn_name { "CurrentGameTimeLessThan" => ConditionSet::CurrentGameTime(CurrentGameTime { negated, comparison: Cmp::Lt, - numeric_value: args.try_get(0, "NumericValue for CurrentGameTime")?.into(), + numeric_value: args.pop_front()?.into(), ..Default::default() }), "CurrentWeather" => gen_cond!( @@ -82,7 +82,7 @@ fn parse_condition(condition: Expression<'_>) -> Result ConditionSet::Level(Level { negated, comparison: Cmp::Lt, - numeric_value: args.try_get(0, "NumericValue for Level")?.into(), + numeric_value: args.pop_front()?.into(), ..Default::default() }), "IsParentCell" => gen_cond!( @@ -93,7 +93,7 @@ fn parse_condition(condition: Expression<'_>) -> Result ConditionSet::IsDirectionMovement(IsMovementDirection { negated, direction: DirectionValue { - value: get_try_into!(args[0], "Direction: 0..=4")?, + value: args.pop_front()?.try_into()?, }, ..Default::default() }), @@ -121,7 +121,7 @@ fn parse_condition(condition: Expression<'_>) -> Result ConditionSet::RandomCondition(RandomCondition { negated, comparison: Cmp::Le, - numeric_value: args.try_get(0, "NumericValue in Random")?.into(), + numeric_value: args.pop_front()?.into(), ..Default::default() }), "ValueEqualTo" | "ValueLessThan" => parse_compare(fn_name, args, negated)?, @@ -136,10 +136,10 @@ fn parse_condition(condition: Expression<'_>) -> Result { - return Err(ParseError::UnexpectedValue( - "Unknown condition: ".into(), - unknown_condition.into(), - )) + return Err(ParseError::UnexpectedValue { + expected: "Unknown condition: ".into(), + actual: unknown_condition.into(), + }) } }) } @@ -149,7 +149,7 @@ mod tests { use super::*; use crate::{ conditions::{And, IsActorBase, IsEquippedType}, - dar_syntax::syntax::{FnArg, NumberLiteral}, + dar_syntax::{ast::fn_args::fn_args, FnArg, NumberLiteral}, values::{PluginValue, TypeValue, WeaponType}, }; use pretty_assertions::assert_eq; @@ -159,7 +159,7 @@ mod tests { let actor = Expression { negated: false, fn_name: "IsActorBase", - args: vec![FnArg::PluginValue { + args: fn_args![FnArg::PluginValue { plugin_name: "Skyrim.esm", form_id: NumberLiteral::Hex(0x0000_0007), }], @@ -167,27 +167,24 @@ mod tests { let player = Expression { negated: false, fn_name: "IsPlayerTeammate", - args: vec![], + args: fn_args![], }; let equip_r3 = Expression { negated: false, fn_name: "IsEquippedLeftType", - args: vec![FnArg::Number(NumberLiteral::Decimal(3))], + args: fn_args![FnArg::Number(NumberLiteral::Decimal(3))], }; let equip_r4 = Expression { negated: true, fn_name: "IsEquippedRightType", - args: vec![FnArg::Number(NumberLiteral::Decimal(4))], + args: fn_args![FnArg::Number(NumberLiteral::Decimal(4))], }; - let input = syntax::Condition::And(vec![ - syntax::Condition::Or(vec![ - syntax::Condition::Exp(actor), - syntax::Condition::Exp(player), - ]), - syntax::Condition::Or(vec![ - syntax::Condition::Exp(equip_r3), - syntax::Condition::Exp(equip_r4), + let input = DarCondition::And(vec![ + DarCondition::Or(vec![DarCondition::Exp(actor), DarCondition::Exp(player)]), + DarCondition::Or(vec![ + DarCondition::Exp(equip_r3), + DarCondition::Exp(equip_r4), ]), ]); diff --git a/core/src/condition_parser/dar_interface.rs b/core/src/condition_parser/dar_interface.rs index f705434..212a28f 100644 --- a/core/src/condition_parser/dar_interface.rs +++ b/core/src/condition_parser/dar_interface.rs @@ -1,22 +1,14 @@ //! Type conversion definitions to change from DAR syntax to OAR json +use super::errors::{ParseError, Result}; use crate::{ - dar_syntax::syntax::{FnArg, NumberLiteral}, + dar_syntax::{FnArg, NumberLiteral}, values::{ - Direction, DirectionValue, FormValue, Keyword, NumericLiteral, NumericValue, PluginValue, + Direction, FormValue, Keyword, LiteralValue, NumericLiteral, NumericValue, PluginValue, StaticValue, }, }; -/// Couldn't parse in DAR to OAR processing Errors -#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] -pub enum ParseError { - /// - 1st arg: Expected value - /// - 2nd arg: Actual value - #[error("Expected {0}. but got {1}")] - UnexpectedValue(String, String), -} - impl From for NumericLiteral { fn from(value: NumberLiteral) -> Self { match value { @@ -52,16 +44,16 @@ impl TryFrom<&FnArg<'_>> for NumericLiteral { fn try_from(value: &FnArg) -> Result { match value { FnArg::Number(num) => Ok(num.into()), - other @ FnArg::PluginValue { .. } => Err(ParseError::UnexpectedValue( - "Number(e.g. 3.0)".into(), - format!("{other:?}",), - )), + other @ FnArg::PluginValue { .. } => Err(ParseError::UnexpectedValue { + expected: "Number(e.g. 3.0)".into(), + actual: other.to_string(), + }), } } } -impl From<&FnArg<'_>> for NumericValue { - fn from(value: &FnArg) -> Self { +impl<'a> From> for NumericValue<'a> { + fn from(value: FnArg<'a>) -> Self { match value { FnArg::PluginValue { plugin_name, @@ -73,7 +65,7 @@ impl From<&FnArg<'_>> for NumericValue { } .into(), ), - FnArg::Number(num) => match *num { + FnArg::Number(num) => match num { NumberLiteral::Float(value) => Self::StaticValue(value.into()), NumberLiteral::Decimal(value) => Self::StaticValue((value as f32).into()), NumberLiteral::Hex(value) => Self::StaticValue((value as f32).into()), @@ -110,18 +102,18 @@ impl TryFrom<&FnArg<'_>> for StaticValue { fn try_from(value: &FnArg) -> Result { match value { FnArg::Number(num) => Ok(num.into()), - other @ FnArg::PluginValue { .. } => Err(ParseError::UnexpectedValue( - "StaticValue(e.g. 3.0)".to_owned(), - format!("{other:?}",), - )), + other @ FnArg::PluginValue { .. } => Err(ParseError::UnexpectedValue { + expected: "StaticValue(e.g. 3.0)".to_string(), + actual: other.to_string(), + }), } } } -impl TryFrom> for PluginValue { +impl<'a> TryFrom> for PluginValue<'a> { type Error = ParseError; - fn try_from(value: FnArg) -> Result { + fn try_from(value: FnArg<'a>) -> Result { match value { FnArg::PluginValue { plugin_name, @@ -130,36 +122,16 @@ impl TryFrom> for PluginValue { plugin_name: plugin_name.into(), form_id: NumericLiteral::from(form_id).into(), }), - FnArg::Number(num) => Err(ParseError::UnexpectedValue( - "plugin_name, form_id (in cast FnArg to PluginValue)".into(), - num.to_string(), - )), - } - } -} - -impl TryFrom<&FnArg<'_>> for PluginValue { - type Error = ParseError; - - fn try_from(value: &FnArg) -> Result { - match value { - FnArg::PluginValue { - plugin_name, - form_id, - } => Ok(Self { - plugin_name: (*plugin_name).into(), - form_id: NumericLiteral::from(form_id).into(), + FnArg::Number(num) => Err(ParseError::UnexpectedValue { + expected: "plugin_name, form_id (in cast FnArg to PluginValue)".into(), + actual: num.to_string(), }), - FnArg::Number(num) => Err(ParseError::UnexpectedValue( - "plugin_name, form_id (in cast &FnArg to PluginValue)".into(), - num.to_string(), - )), } } } -impl From<&FnArg<'_>> for Keyword { - fn from(value: &FnArg<'_>) -> Self { +impl<'a> From> for Keyword<'a> { + fn from(value: FnArg<'a>) -> Self { match value { FnArg::PluginValue { plugin_name, @@ -170,43 +142,27 @@ impl From<&FnArg<'_>> for Keyword { form_id: NumericLiteral::from(form_id).into(), }, }), - FnArg::Number(num) => Self::Literal(crate::values::LiteralValue { - editor_id: NumericLiteral::from(num).to_string(), + FnArg::Number(num) => Self::Literal(LiteralValue { + editor_id: NumericLiteral::from(num).to_string().into(), }), } } } -impl TryFrom<&FnArg<'_>> for Direction { +impl TryFrom> for Direction { type Error = ParseError; - fn try_from(value: &FnArg<'_>) -> Result { + fn try_from(value: FnArg<'_>) -> Result { match value { - FnArg::Number(num) => Ok(match *num { - NumberLiteral::Hex(num) => (num as f64) - .try_into() - .map_err(|e: &str| ParseError::UnexpectedValue(e.into(), "0..=4".into()))?, - NumberLiteral::Decimal(num) => (num as f64) - .try_into() - .map_err(|e: &str| ParseError::UnexpectedValue(e.into(), "0..=4".into()))?, - NumberLiteral::Float(num) => (num as f64) - .try_into() - .map_err(|e: &str| ParseError::UnexpectedValue(e.into(), "0..=4".into()))?, + FnArg::Number(num) => Ok(match num { + NumberLiteral::Hex(num) => (num as f64).try_into()?, + NumberLiteral::Decimal(num) => (num as f64).try_into()?, + NumberLiteral::Float(num) => (num as f64).try_into()?, + }), + other @ FnArg::PluginValue { .. } => Err(ParseError::UnexpectedValue { + expected: "1..=4(in Cast &FnArg to Direction)".into(), + actual: other.to_string(), }), - other @ FnArg::PluginValue { .. } => Err(ParseError::UnexpectedValue( - "1..=4(in Cast &FnArg to Direction)".into(), - format!("{other:?}"), - )), } } } - -impl TryFrom<&FnArg<'_>> for DirectionValue { - type Error = ParseError; - - fn try_from(value: &FnArg<'_>) -> Result { - Ok(Self { - value: value.try_into()?, - }) - } -} diff --git a/core/src/condition_parser/equip.rs b/core/src/condition_parser/equip.rs index a1dbd51..b916971 100644 --- a/core/src/condition_parser/equip.rs +++ b/core/src/condition_parser/equip.rs @@ -1,9 +1,8 @@ //! Parses equipment-related conditions based on the provided arguments and condition name. -use super::macros::{gen_cond, get_try_into}; -use super::{dar_interface::ParseError, macros::GetArg as _}; +use super::errors::{ParseError, Result}; use crate::{ conditions::{ConditionSet, IsEquipped, IsEquippedHasKeyword, IsEquippedShout, IsEquippedType}, - dar_syntax::syntax::FnArg, + dar_syntax::FnArgs, values::{NumericLiteral, TypeValue}, }; @@ -11,24 +10,22 @@ use crate::{ /// /// # Errors /// If parsing fails. -pub(super) fn parse_equip( - condition_name: &str, - args: Vec>, +pub(super) fn parse_equip<'a>( + condition_name: &'a str, + mut args: FnArgs<'a>, negated: bool, -) -> Result { +) -> Result> { Ok(match condition_name { "IsEquippedRight" | "IsEquippedLeft" => ConditionSet::IsEquipped(IsEquipped { negated, - form: get_try_into!(args[0], "PluginValue")?, + form: args.pop_front()?.try_into()?, left_hand: condition_name == "IsEquippedLeft", ..Default::default() }), "IsEquippedRightType" | "IsEquippedLeftType" => { - let numeric_value: NumericLiteral = get_try_into!(args[0], "WeaponType -1..18")?; + let numeric_value: NumericLiteral = args.pop_front()?.try_into()?; let type_value = TypeValue { - value: numeric_value.try_into().map_err(|_err| { - ParseError::UnexpectedValue("-1..18".into(), "Unknown value".into()) - })?, + value: numeric_value.try_into()?, }; ConditionSet::IsEquippedType(IsEquippedType { negated, @@ -41,20 +38,20 @@ pub(super) fn parse_equip( ConditionSet::IsEquippedHasKeyword(IsEquippedHasKeyword { negated, left_hand: condition_name == "IsEquippedLeftHasKeyword", - keyword: args.try_get(0, "Keyword")?.into(), + keyword: args.pop_front()?.into(), ..Default::default() }) } - "IsEquippedShout" => gen_cond!( - IsEquippedShout(shout, negated), - args, - "shout(PluginValue) in IsEquippedShout" - ), + "IsEquippedShout" => ConditionSet::IsEquippedShout(IsEquippedShout { + shout: args.pop_front()?.try_into()?, + negated, + ..Default::default() + }), _ => { - return Err(ParseError::UnexpectedValue( - "IsEquipped prefix condition unexpected to come in: ".into(), - condition_name.into(), - )) + return Err(ParseError::UnexpectedValue { + expected: "`IsEquipped` prefix condition: ".into(), + actual: condition_name.into(), + }) } }) } diff --git a/core/src/condition_parser/errors.rs b/core/src/condition_parser/errors.rs new file mode 100644 index 0000000..79e42e0 --- /dev/null +++ b/core/src/condition_parser/errors.rs @@ -0,0 +1,38 @@ +//! Dar to OAR cast error definitions +use crate::values::ValueError; + +/// Couldn't parse in DAR to OAR processing Errors +#[derive(Debug, Clone, snafu::Snafu, PartialEq, Eq)] +pub enum ParseError { + /// Expected {expected}. but got {actual} + UnexpectedValue { + /// Expected value + expected: String, + /// Actual value + actual: String, + }, + + /// OAR condition error + #[snafu(transparent)] + ConditionError { + /// transparent + source: crate::conditions::ConditionError, + }, + + /// Value error. + #[snafu(transparent)] + ValueError { + /// transparent + source: ValueError, + }, + + /// DAR error. + #[snafu(transparent)] + DarError { + /// transparent + source: crate::dar_syntax::errors::DarError, + }, +} + +/// Condition parser Error. +pub(super) type Result = core::result::Result; diff --git a/core/src/condition_parser/faction.rs b/core/src/condition_parser/faction.rs index 7df2bb2..0f52d4e 100644 --- a/core/src/condition_parser/faction.rs +++ b/core/src/condition_parser/faction.rs @@ -1,9 +1,8 @@ //! Parses faction-related conditions based on the provided arguments and condition name. -use super::dar_interface::ParseError; -use super::macros::{get_try_into, GetArg as _}; +use super::errors::{ParseError, Result}; use crate::{ conditions::{ConditionSet, FactionRank, IsInFaction}, - dar_syntax::syntax::FnArg, + dar_syntax::FnArgs, values::Cmp, }; @@ -11,17 +10,20 @@ use crate::{ /// /// # Errors /// If parsing fails. -pub(super) fn parse_faction( - condition_name: &str, - args: Vec>, +pub(super) fn parse_faction<'a>( + condition_name: &'a str, + mut args: FnArgs<'a>, negated: bool, -) -> Result { - let create_cond = |comparison: Cmp| -> Result { +) -> Result> { + let mut create_cond = |comparison: Cmp| -> Result { + let faction = args.pop_front()?.try_into()?; + let numeric_value = args.pop_front()?.into(); + Ok(ConditionSet::FactionRank(FactionRank { negated, - faction: get_try_into!(args[0], "PluginValue")?, + faction, comparison, - numeric_value: args.try_get(1, "NumericValue")?.into(), + numeric_value, ..Default::default() })) }; @@ -29,16 +31,16 @@ pub(super) fn parse_faction( Ok(match condition_name { "IsInFaction" => ConditionSet::IsInFaction(IsInFaction { negated, - faction: get_try_into!(args[0], "PluginValue")?, + faction: args.pop_front()?.try_into()?, ..Default::default() }), "IsFactionRankEqualTo" => create_cond(Cmp::Eq)?, "IsFactionRankLessThan" => create_cond(Cmp::Lt)?, _ => { - return Err(ParseError::UnexpectedValue( - "IsInFaction, IsFactionRankEqualTo or IsFactionRankLessThan".to_string(), - condition_name.to_string(), - )) + return Err(ParseError::UnexpectedValue { + expected: "IsInFaction, IsFactionRankEqualTo or IsFactionRankLessThan".to_string(), + actual: condition_name.to_string(), + }) } }) } @@ -46,14 +48,15 @@ pub(super) fn parse_faction( #[cfg(test)] mod tests { use super::*; - use crate::dar_syntax::syntax::NumberLiteral; + use crate::dar_syntax::ast::fn_args::fn_args; + use crate::dar_syntax::{FnArg, NumberLiteral}; use crate::values::{NumericValue, PluginValue}; use pretty_assertions::assert_eq; #[test] fn should_parse_is_in_faction() { let condition_name = "IsInFaction"; - let args = vec![ + let args = fn_args![ FnArg::PluginValue { plugin_name: "Skyrim.esm", form_id: NumberLiteral::Decimal(7), @@ -79,7 +82,7 @@ mod tests { #[test] fn should_parse_is_faction_rank_equal_to() { let condition_name = "IsFactionRankEqualTo"; - let args = vec![ + let args = fn_args![ FnArg::PluginValue { plugin_name: "Skyrim.esm", form_id: NumberLiteral::Decimal(7), diff --git a/core/src/condition_parser/has.rs b/core/src/condition_parser/has.rs index 93a62bf..2f21f0e 100644 --- a/core/src/condition_parser/has.rs +++ b/core/src/condition_parser/has.rs @@ -1,52 +1,60 @@ //! Parses has-prefix conditions based on the provided arguments and condition name. -use super::dar_interface::ParseError; -use super::macros::{gen_cond, get_try_into, GetArg as _}; +use super::errors::{ParseError, Result}; use crate::conditions::{ ConditionSet, HasKeyword, HasMagicEffect, HasMagicEffectWithKeyword, HasPerk, HasRefType, HasSpell, }; -use crate::dar_syntax::syntax::FnArg; +use crate::dar_syntax::FnArgs; /// Parses has-prefix conditions based on the provided arguments and condition name. /// /// # Errors /// If parsing fails. -pub(super) fn parse_has( - condition_name: &str, - args: Vec>, +pub(super) fn parse_has<'a>( + condition_name: &'a str, + mut args: FnArgs<'a>, negated: bool, -) -> Result { +) -> Result> { + let arg = args.pop_front()?; + Ok(match condition_name { "HasKeyword" => ConditionSet::HasKeyword(HasKeyword { - keyword: args.try_get(0, "keyword in HasKeyword")?.into(), + keyword: arg.into(), + negated, + ..Default::default() + }), + "HasPerk" => ConditionSet::HasPerk(HasPerk { + perk: arg.try_into()?, + negated, + ..Default::default() + }), + "HasSpell" => ConditionSet::HasSpell(HasSpell { + spell: arg.try_into()?, negated, ..Default::default() }), - "HasPerk" => gen_cond!(HasPerk(perk, negated), args, "PluginValue in HasPerk"), - "HasSpell" => gen_cond!(HasSpell(spell, negated), args, "PluginValue in HasSpell"), "HasMagicEffect" => ConditionSet::HasMagicEffect(HasMagicEffect { - magic_effect: get_try_into!(args[0], "PluginValue in HasMagicEffect")?, + magic_effect: arg.try_into()?, negated, ..Default::default() }), - "HasMagicEffectWithKeyword" => gen_cond!( - HasMagicEffectWithKeyword(keyword, negated), - args, - "PluginValue in HasMagicEffectWithKeyword", - into - ), - "HasRefType" => gen_cond!( - HasRefType(location_ref_type, negated), - args, - "PluginValue in HasRefType", - into - ), - unknown_condition => { - return Err(ParseError::UnexpectedValue( + "HasMagicEffectWithKeyword" => { + ConditionSet::HasMagicEffectWithKeyword(HasMagicEffectWithKeyword { + keyword: arg.into(), + negated, + ..Default::default() + }) + } + "HasRefType" => ConditionSet::HasRefType(HasRefType { + location_ref_type: arg.into(), + negated, + ..Default::default() + }), + unknown_condition => return Err(ParseError::UnexpectedValue { + expected: "HasKeyword|HasPerk|HasSpell|HasMagicEffect|HasMagicEffectWithKeyword|HasRefType" .into(), - unknown_condition.into(), - )) - } + actual: unknown_condition.into(), + }), }) } diff --git a/core/src/condition_parser/macros.rs b/core/src/condition_parser/macros.rs index 5eaf397..c8a2586 100644 --- a/core/src/condition_parser/macros.rs +++ b/core/src/condition_parser/macros.rs @@ -1,72 +1,4 @@ //! Macros for type conversion of parsed DAR structures into easily serializable OAR structures -use super::ParseError; -use crate::dar_syntax::syntax::FnArg; - -/// A trait for safely accessing elements in a vector without causing runtime panics. -/// -/// This trait provides methods to access the index of a vector, returning a result -/// with either the desired element or an error if the index is out of bounds. -pub(super) trait GetArg { - /// The type of the output, which is a result containing a reference to the desired element - /// or a error if the index is out of bounds. - /// - /// Use [Generic Associated Types(GATs)](https://blog.rust-lang.org/2022/10/28/gats-stabilization.html#what-are-gats) - /// for the `&T` in [`Vec`] because it has the same lifeTime as [Vec]. - type Output<'this> - where - Self: 'this; - - /// Access the element at the specified index of the vector. - /// - /// # Returns - /// - /// A result containing a reference to the desired element or a `Error` if the index is out of bounds. - fn try_get(&self, index: usize, expected: impl ToString) -> Self::Output<'_>; - - /// Access the element at the specified index of the vector with additional information in case of an error. - /// - /// # Returns - /// - /// A result containing a reference to the desired element or a `Error` with detailed information if the index is out of bounds. - fn try_get_real(&self, index: usize, expected: T, actual: T) -> Self::Output<'_> - where - T: Into; -} - -impl GetArg for Vec> { - type Output<'this> = Result<&'this FnArg<'this>, ParseError> where Self: 'this; - - fn try_get(&self, index: usize, expected: impl ToString) -> Self::Output<'_> { - self.get(index).ok_or_else(|| { - ParseError::UnexpectedValue(expected.to_string(), format!("None in args[{index}]")) - }) - } - - fn try_get_real(&self, index: usize, expected: T, actual: T) -> Self::Output<'_> - where - T: Into, - { - self.get(index) - .ok_or_else(|| ParseError::UnexpectedValue(expected.into(), actual.into())) - } -} - -/// [`Vec::get(index)`](https://doc.rust-lang.org/stable/alloc/vec/struct.Vec.html#method.get) & [`TryInto`] -macro_rules! get_try_into { - ($args:ident[$index:literal], $expected:literal) => { - > as $crate::condition_parser::macros::GetArg>::try_get( - &$args, $index, $expected, - )? - .try_into() - }; - ($args:ident[$index:literal], $expected:literal, $actual:literal) => { - > as $crate::condition_parser::macros::GetArg>::try_get_real( - &$args, $index, $expected, $actual - )? - .try_into() - }; -} -pub(super) use get_try_into; /// Generate `ConditionSet` & /// [`Vec::get`](https://doc.rust-lang.org/stable/alloc/vec/struct.Vec.html#method.get)(index) & @@ -75,15 +7,14 @@ macro_rules! gen_cond { ($id:ident($field_name:ident, $negated:ident), $args:ident, $expected:literal) => { ConditionSet::$id($id { negated: $negated, - $field_name: $crate::condition_parser::macros::get_try_into!($args[0], $expected)?, + $field_name: $args.pop_front()?.try_into()?, ..Default::default() }) }; ($id:ident($field_name:ident, $negated:ident), $args:ident, $expected:literal, into) => { ConditionSet::$id($id { - negated: $negated, - $field_name: - > as $crate::condition_parser::macros::GetArg>::try_get(&$args, 0, $expected)?.into(), + negated: $negated, + $field_name: $args.pop_front()?.into(), ..Default::default() }) }; diff --git a/core/src/condition_parser/mod.rs b/core/src/condition_parser/mod.rs index 794e1f1..70b470f 100644 --- a/core/src/condition_parser/mod.rs +++ b/core/src/condition_parser/mod.rs @@ -4,12 +4,13 @@ mod compare; mod conditions; mod dar_interface; mod equip; +mod errors; mod faction; mod has; mod macros; use self::conditions::parse_conditions; -pub use self::dar_interface::ParseError; +pub use self::errors::ParseError; use crate::conditions::ConditionSet; use crate::dar_syntax::parse_dar_syntax; use crate::error::{ConvertError, Result}; @@ -27,19 +28,15 @@ pub fn parse_dar2oar

(path: P, input: &str) -> Result> where P: AsRef, { - match parse_dar_syntax(input) { - Ok(dar_ast) => { - #[cfg(feature = "tracing")] - tracing::debug!("Input => Parsed DAR:\n{:#?}", dar_ast); + let dar_ast = parse_dar_syntax(input).map_err(|err| ConvertError::InvalidDarSyntax { + path: path.as_ref().to_path_buf(), + source: err, + })?; + #[cfg(feature = "tracing")] + tracing::debug!("Input => Parsed DAR:\n{:#?}", dar_ast); - let oar_ast = parse_conditions(dar_ast)?; - #[cfg(feature = "tracing")] - tracing::debug!("Parsed DAR => Serialized OAR:\n{:#?}", &oar_ast); - Ok(oar_ast.try_into()?) - } - Err(mut err) => Err(ConvertError::InvalidDarSyntax({ - err.title = path.as_ref().to_string_lossy().to_string(); - err.to_string() - })), - } + let oar_ast = parse_conditions(dar_ast)?; + #[cfg(feature = "tracing")] + tracing::debug!("Parsed DAR => Serialized OAR:\n{:#?}", &oar_ast); + Ok(oar_ast.try_into()?) } diff --git a/core/src/conditions/and.rs b/core/src/conditions/and.rs index a7da916..baae6a5 100644 --- a/core/src/conditions/and.rs +++ b/core/src/conditions/and.rs @@ -1,7 +1,7 @@ //! Represents a logical AND condition set. use super::{condition::default_required_version, is_false, ConditionSet}; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a logical AND condition set. /// @@ -13,13 +13,13 @@ use serde::{Deserialize, Serialize}; /// In DAR, AND is pushed up to the root conditions. /// The non-conditions definitions exist in anticipation of future OAR parsing. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct And { +pub struct And<'a> { /// The name of the condition, which is "AND". - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -27,10 +27,10 @@ pub struct And { /// The list of conditions forming the logical AND. #[serde(rename = "Conditions")] - pub conditions: Vec, + pub conditions: Vec>, } -impl Default for And { +impl Default for And<'_> { fn default() -> Self { Self { condition: "AND".into(), diff --git a/core/src/conditions/compare_values.rs b/core/src/conditions/compare_values.rs index b6f0310..1150953 100644 --- a/core/src/conditions/compare_values.rs +++ b/core/src/conditions/compare_values.rs @@ -1,18 +1,18 @@ //! Structure comparing two A and two B use super::{condition::default_required_version, is_false}; use crate::values::{Cmp, NumericValue}; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Structure comparing A and B #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CompareValues { +pub struct CompareValues<'a> { /// Condition name "`CompareValues`" - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for compatibility with this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -21,7 +21,7 @@ pub struct CompareValues { /// Comparison object A #[serde(default)] #[serde(rename = "Value A")] - pub value_a: NumericValue, + pub value_a: NumericValue<'a>, #[serde(rename = "Comparison")] #[serde(default)] /// == | != | > | >= | < | <= @@ -29,10 +29,10 @@ pub struct CompareValues { /// Comparison object B #[serde(default)] #[serde(rename = "Value B")] - pub value_b: NumericValue, + pub value_b: NumericValue<'a>, } -impl Default for CompareValues { +impl Default for CompareValues<'_> { fn default() -> Self { Self { condition: "CompareValues".into(), @@ -118,12 +118,12 @@ mod tests { fn should_stringify_compare_values_with_graph_variable() -> Result<()> { let compare_values = CompareValues { value_a: NumericValue::GraphVariable(GraphValue { - graph_variable: "true".to_string(), + graph_variable: "true".into(), graph_variable_type: GraphVariableType::Bool, }), value_b: NumericValue::GraphVariable(GraphValue { // This is invalid as an Int, but valid as a syntax (any string will do since text is expected). - graph_variable: "another_variable".to_string(), + graph_variable: "another_variable".into(), graph_variable_type: GraphVariableType::Int, }), comparison: Cmp::Ne, diff --git a/core/src/conditions/condition.rs b/core/src/conditions/condition.rs index a728bc1..f72ce70 100644 --- a/core/src/conditions/condition.rs +++ b/core/src/conditions/condition.rs @@ -1,26 +1,27 @@ //! Represents a generic condition. use super::is_false; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Representing the default required version. pub const REQUIRED_VERSION: &str = "1.0.0.0"; /// Create a default required version as a [`CompactString`]. -pub fn default_required_version() -> CompactString { - REQUIRED_VERSION.into() +#[inline] +pub const fn default_required_version() -> Cow<'static, str> { + Cow::Borrowed(REQUIRED_VERSION) } /// Represents a generic condition. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Condition { +pub struct Condition<'a> { /// The name of the condition (e.g., "`IsWornHasKeyword`"). #[serde(default)] - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not (default: `false`). /// /// # NOTE @@ -30,19 +31,19 @@ pub struct Condition { pub negated: bool, } -impl Default for Condition { +impl Default for Condition<'_> { fn default() -> Self { Self { - condition: CompactString::default(), + condition: Default::default(), required_version: default_required_version(), negated: false, } } } -impl Condition { +impl<'a> Condition<'a> { /// Creates a new `Condition` with the specified name. - pub fn new(condition: &str) -> Self { + pub fn new(condition: &'a str) -> Self { Self { condition: condition.into(), ..Default::default() diff --git a/core/src/conditions/condition_config.rs b/core/src/conditions/condition_config.rs index e35bf1a..8e18def 100644 --- a/core/src/conditions/condition_config.rs +++ b/core/src/conditions/condition_config.rs @@ -1,27 +1,27 @@ //! Represents the configuration for each animation root specified in a `config.json` file. use super::ConditionSet; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents the configuration for each animation root specified in a `config.json` file. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub struct ConditionsConfig { +pub struct ConditionsConfig<'a> { /// An arbitrary name given by the user (value in the mapping table). /// /// # Note /// The name will probably exceed 24 bytes, so it should not be a [`CompactString`]. #[serde(default)] - pub name: String, + pub name: Cow<'a, str>, /// The description associated with the animation root configuration. #[serde(default)] - pub description: CompactString, + pub description: Cow<'a, str>, /// The priority of the animation root. #[serde(default)] pub priority: i32, /// An optional override for the animations folder. #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "overrideAnimationsFolder")] - pub override_animations_folder: Option, + pub override_animations_folder: Option>, /// A vector containing the conditions associated with the animation root. - pub conditions: Vec, + pub conditions: Vec>, } diff --git a/core/src/conditions/current_weather.rs b/core/src/conditions/current_weather.rs index 94a5301..34670ab 100644 --- a/core/src/conditions/current_weather.rs +++ b/core/src/conditions/current_weather.rs @@ -1,18 +1,19 @@ //! Represents a condition to check if the current weather matches a specified weather. +use std::borrow::Cow; + use super::{condition::default_required_version, is_false}; use crate::values::PluginValue; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; /// Represents a condition to check if the current weather matches a specified weather. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CurrentWeather { +pub struct CurrentWeather<'a> { /// The name of the condition, which is "`CurrentWeather`". - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -21,10 +22,10 @@ pub struct CurrentWeather { /// The specific weather condition to check for. #[serde(default)] #[serde(rename = "Weather")] - pub weather: PluginValue, + pub weather: PluginValue<'a>, } -impl Default for CurrentWeather { +impl Default for CurrentWeather<'_> { fn default() -> Self { Self { condition: "CurrentWeather".into(), diff --git a/core/src/conditions/errors.rs b/core/src/conditions/errors.rs new file mode 100644 index 0000000..5c4d498 --- /dev/null +++ b/core/src/conditions/errors.rs @@ -0,0 +1,17 @@ +//! Represents an error that can occur while working with conditions. + +/// Represents an error that can occur while working with conditions. +#[derive(Debug, Clone, snafu::Snafu, PartialEq, Eq)] +#[snafu(visibility(pub))] +pub enum ConditionError { + /// Only `And` or `Or` can be converted to Vec. + CastError, + + /// Expected {expected}. but got {actual} + UnexpectedValue { + /// Expected value + expected: String, + /// Actual value + actual: String, + }, +} diff --git a/core/src/conditions/faction_rank.rs b/core/src/conditions/faction_rank.rs index 853249f..45d141a 100644 --- a/core/src/conditions/faction_rank.rs +++ b/core/src/conditions/faction_rank.rs @@ -1,18 +1,18 @@ //! Represents a condition to test the reference's faction rank against a specified rank. use super::{condition::default_required_version, is_false}; use crate::values::{Cmp, NumericValue, PluginValue}; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition to test the reference's faction rank against a specified rank. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct FactionRank { +pub struct FactionRank<'a> { /// The name of the condition, which is "`FactionRank`". - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -21,18 +21,17 @@ pub struct FactionRank { /// The faction to test against the reference's faction rank. #[serde(default)] #[serde(rename = "Faction")] - pub faction: PluginValue, + pub faction: PluginValue<'a>, /// The comparison operator to use in the faction rank comparison. #[serde(default)] #[serde(rename = "Comparison")] pub comparison: Cmp, /// The numeric value to compare the faction rank against. - #[serde(default)] #[serde(rename = "Numeric value")] - pub numeric_value: NumericValue, + pub numeric_value: NumericValue<'a>, } -impl Default for FactionRank { +impl Default for FactionRank<'_> { fn default() -> Self { Self { condition: "FactionRank".into(), @@ -102,7 +101,7 @@ mod tests { let expected = FactionRank { faction: PluginValue { - plugin_name: String::new(), + plugin_name: "".into(), form_id: "".into(), }, comparison: Cmp::Eq, diff --git a/core/src/conditions/has_keyword.rs b/core/src/conditions/has_keyword.rs index 56650ca..113f161 100644 --- a/core/src/conditions/has_keyword.rs +++ b/core/src/conditions/has_keyword.rs @@ -1,18 +1,18 @@ //! Represents a condition to check if an entity has a specific keyword. use super::{condition::default_required_version, is_false}; use crate::values::Keyword; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition to check if an entity has a specific keyword. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct HasKeyword { +pub struct HasKeyword<'a> { /// The name of the condition, which is "`HasKeyword`". - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -21,10 +21,10 @@ pub struct HasKeyword { /// The keyword to check for in the entity. #[serde(default)] #[serde(rename = "Keyword")] - pub keyword: Keyword, + pub keyword: Keyword<'a>, } -impl Default for HasKeyword { +impl Default for HasKeyword<'_> { fn default() -> Self { Self { condition: "HasKeyword".into(), @@ -72,7 +72,7 @@ mod tests { let expected = HasKeyword { keyword: Keyword::Literal(LiteralValue { - editor_id: "SomeKeyword".to_string(), + editor_id: "SomeKeyword".into(), }), ..Default::default() }; diff --git a/core/src/conditions/has_magic_effect.rs b/core/src/conditions/has_magic_effect.rs index f11c3d2..1d99067 100644 --- a/core/src/conditions/has_magic_effect.rs +++ b/core/src/conditions/has_magic_effect.rs @@ -1,18 +1,18 @@ //! Represents a condition to check if an entity has a specific magic effect. use super::{condition::default_required_version, is_false}; use crate::values::PluginValue; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition to check if an entity has a specific magic effect. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct HasMagicEffect { +pub struct HasMagicEffect<'a> { /// The name of the condition, which is "`HasMagicEffect`". - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -21,14 +21,14 @@ pub struct HasMagicEffect { /// The magic effect to check for in the entity. #[serde(default)] #[serde(rename = "Magic effect")] - pub magic_effect: PluginValue, + pub magic_effect: PluginValue<'a>, /// Indicates whether to consider only active effects. #[serde(default)] #[serde(rename = "Active effects only")] pub active_effects_only: bool, } -impl Default for HasMagicEffect { +impl Default for HasMagicEffect<'_> { fn default() -> Self { Self { condition: "HasMagicEffect".into(), diff --git a/core/src/conditions/has_magic_effect_with_keyword.rs b/core/src/conditions/has_magic_effect_with_keyword.rs index 8e70b32..8499ef4 100644 --- a/core/src/conditions/has_magic_effect_with_keyword.rs +++ b/core/src/conditions/has_magic_effect_with_keyword.rs @@ -1,23 +1,23 @@ //! Represents a condition to check if an entity has a magic effect with a specific keyword. use super::{condition::default_required_version, is_false}; use crate::values::{FormValue, Keyword}; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition to check if an entity has a magic effect with a specific keyword. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct HasMagicEffectWithKeyword { +pub struct HasMagicEffectWithKeyword<'a> { /// The name of the condition, which is "`HasMagicEffectWithKeyword`". /// /// # Note /// This condition name is 25 bytes. /// Optimization by [`CompactString`] is limited to 24 bytes, the size of a [`String`] structure. /// Therefore, this field cannot be SSO (Small String Optimization). - pub condition: String, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -26,14 +26,14 @@ pub struct HasMagicEffectWithKeyword { /// The keyword to check for in the magic effect. #[serde(default)] #[serde(rename = "Keyword")] - pub keyword: Keyword, + pub keyword: Keyword<'a>, /// Indicates whether to consider only active effects. #[serde(default)] #[serde(rename = "Active effects only")] pub active_effects_only: bool, } -impl Default for HasMagicEffectWithKeyword { +impl Default for HasMagicEffectWithKeyword<'_> { fn default() -> Self { Self { condition: "HasMagicEffectWithKeyword".into(), diff --git a/core/src/conditions/has_perk.rs b/core/src/conditions/has_perk.rs index a2e383d..150a10d 100644 --- a/core/src/conditions/has_perk.rs +++ b/core/src/conditions/has_perk.rs @@ -1,18 +1,18 @@ //! Represents a condition to check if an entity has a specific perk. use super::{condition::default_required_version, is_false}; use crate::values::PluginValue; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition to check if an entity has a specific perk. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct HasPerk { +pub struct HasPerk<'a> { /// The name of the condition, which is "`HasPerk`". - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -21,10 +21,10 @@ pub struct HasPerk { /// The perk to check for in the entity. #[serde(default)] #[serde(rename = "Perk")] - pub perk: PluginValue, + pub perk: PluginValue<'a>, } -impl Default for HasPerk { +impl Default for HasPerk<'_> { fn default() -> Self { Self { condition: "HasPerk".into(), diff --git a/core/src/conditions/has_ref_type.rs b/core/src/conditions/has_ref_type.rs index 7ef420c..5ed5768 100644 --- a/core/src/conditions/has_ref_type.rs +++ b/core/src/conditions/has_ref_type.rs @@ -1,18 +1,18 @@ //! Represents a condition to check if a reference has a specific type. use super::{condition::default_required_version, is_false}; use crate::values::Keyword; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition to check if a reference has a specific type. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct HasRefType { +pub struct HasRefType<'a> { /// The name of the condition, which is "`HasRefType`". - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -21,10 +21,10 @@ pub struct HasRefType { /// The reference type to check for. #[serde(default)] #[serde(rename = "Location ref type")] - pub location_ref_type: Keyword, + pub location_ref_type: Keyword<'a>, } -impl Default for HasRefType { +impl Default for HasRefType<'_> { fn default() -> Self { Self { condition: "HasRefType".into(), diff --git a/core/src/conditions/is_equipped.rs b/core/src/conditions/is_equipped.rs index 0a89730..74039d5 100644 --- a/core/src/conditions/is_equipped.rs +++ b/core/src/conditions/is_equipped.rs @@ -1,19 +1,19 @@ //! Represents a condition based on whether an entity is equipped with a specific form. use super::{condition::default_required_version, is_false}; use crate::values::PluginValue; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition based on whether an entity is equipped with a specific form. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct IsEquipped { +pub struct IsEquipped<'a> { /// The name of the condition, which is "`IsEquipped`". - pub condition: CompactString, - /// The required version for compatibility with this condition. + pub condition: Cow<'a, str>, + /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, - /// Indicates whether the condition is negated. + pub required_version: Cow<'a, str>, + /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub negated: bool, @@ -21,14 +21,14 @@ pub struct IsEquipped { /// The form associated with the condition. #[serde(default)] #[serde(rename = "Form")] - pub form: PluginValue, + pub form: PluginValue<'a>, /// Indicates whether the entity is equipped in the left hand. #[serde(default)] #[serde(rename = "Left hand")] pub left_hand: bool, } -impl Default for IsEquipped { +impl Default for IsEquipped<'_> { fn default() -> Self { Self { condition: "IsEquipped".into(), diff --git a/core/src/conditions/is_equipped_has_keyword.rs b/core/src/conditions/is_equipped_has_keyword.rs index 44f2a7c..a7ee367 100644 --- a/core/src/conditions/is_equipped_has_keyword.rs +++ b/core/src/conditions/is_equipped_has_keyword.rs @@ -1,18 +1,18 @@ //! Represents a condition to check if an equipped item has a specific keyword. use super::{condition::default_required_version, is_false}; use crate::values::Keyword; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition to check if an equipped item has a specific keyword. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct IsEquippedHasKeyword { +pub struct IsEquippedHasKeyword<'a> { /// The name of the condition, which is "`IsEquippedHasKeyword`". - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -21,14 +21,14 @@ pub struct IsEquippedHasKeyword { /// The keyword to check for in the equipped item. #[serde(default)] #[serde(rename = "Keyword")] - pub keyword: Keyword, + pub keyword: Keyword<'a>, /// Indicates whether the equipped item should be in the left hand. #[serde(default)] #[serde(rename = "Left hand")] pub left_hand: bool, } -impl Default for IsEquippedHasKeyword { +impl Default for IsEquippedHasKeyword<'_> { fn default() -> Self { Self { condition: "IsEquippedHasKeyword".into(), diff --git a/core/src/conditions/is_equipped_type.rs b/core/src/conditions/is_equipped_type.rs index 05f0fcb..625c2ef 100644 --- a/core/src/conditions/is_equipped_type.rs +++ b/core/src/conditions/is_equipped_type.rs @@ -1,18 +1,18 @@ //! Represents a condition to check if a specific type is equipped. use super::{condition::default_required_version, is_false}; use crate::values::TypeValue; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition to check if a specific type is equipped. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct IsEquippedType { +pub struct IsEquippedType<'a> { /// The name of the condition, which is "`IsEquippedType`". - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -28,7 +28,7 @@ pub struct IsEquippedType { pub left_hand: bool, } -impl Default for IsEquippedType { +impl Default for IsEquippedType<'_> { fn default() -> Self { Self { condition: "IsEquippedType".into(), diff --git a/core/src/conditions/is_movement_direction.rs b/core/src/conditions/is_movement_direction.rs index aca2e5d..fa315ee 100644 --- a/core/src/conditions/is_movement_direction.rs +++ b/core/src/conditions/is_movement_direction.rs @@ -1,19 +1,19 @@ //! Represents a condition based on the movement direction of an entity. use super::{condition::default_required_version, is_false}; use crate::values::DirectionValue; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition based on the movement direction of an entity. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct IsMovementDirection { +pub struct IsMovementDirection<'a> { /// The name of the condition, which is "`IsMovementDirection`". - pub condition: CompactString, - /// The required version for compatibility with this condition. + pub condition: Cow<'a, str>, + /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, - /// Indicates whether the condition is negated. + pub required_version: Cow<'a, str>, + /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub negated: bool, @@ -24,7 +24,7 @@ pub struct IsMovementDirection { pub direction: DirectionValue, } -impl Default for IsMovementDirection { +impl Default for IsMovementDirection<'_> { fn default() -> Self { Self { condition: "IsMovementDirection".into(), diff --git a/core/src/conditions/is_worn_has_keyword.rs b/core/src/conditions/is_worn_has_keyword.rs index 9b28535..6fed3fa 100644 --- a/core/src/conditions/is_worn_has_keyword.rs +++ b/core/src/conditions/is_worn_has_keyword.rs @@ -1,19 +1,19 @@ //! Represents a condition based on whether an entity is worn and has a specific keyword. use super::{condition::default_required_version, is_false}; use crate::values::Keyword; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition based on whether an entity is worn and has a specific keyword. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct IsWornHasKeyword { +pub struct IsWornHasKeyword<'a> { /// The name of the condition, which is "`IsWornHasKeyword`". - pub condition: CompactString, - /// The required version for compatibility with this condition. + pub condition: Cow<'a, str>, + /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, - /// Indicates whether the condition is negated. + pub required_version: Cow<'a, str>, + /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub negated: bool, @@ -21,10 +21,10 @@ pub struct IsWornHasKeyword { /// The keyword associated with the condition. #[serde(default)] #[serde(rename = "Keyword")] - pub keyword: Keyword, + pub keyword: Keyword<'a>, } -impl Default for IsWornHasKeyword { +impl Default for IsWornHasKeyword<'_> { fn default() -> Self { Self { condition: "IsWornHasKeyword".into(), diff --git a/core/src/conditions/mod.rs b/core/src/conditions/mod.rs index 424e6b3..9a94a9d 100644 --- a/core/src/conditions/mod.rs +++ b/core/src/conditions/mod.rs @@ -4,6 +4,7 @@ mod compare_values; mod condition; mod condition_config; mod current_weather; +mod errors; mod faction_rank; mod has_keyword; mod has_magic_effect; @@ -19,6 +20,7 @@ mod namespace_config; mod or; mod random; +pub use self::errors::ConditionError; pub use self::{ and::And, compare_values::CompareValues, condition::Condition, condition_config::ConditionsConfig, current_weather::CurrentWeather, faction_rank::FactionRank, @@ -32,8 +34,8 @@ pub use self::{ use self::condition::default_required_version; use crate::values::{Cmp, NumericValue, PluginValue}; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Returns `true` if the provided boolean value is `false`, otherwise `false`. /// @@ -50,11 +52,11 @@ macro_rules! gen_cmp_num_struct { $( $(#[$attr])* #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - pub struct $name { - pub condition: CompactString, + pub struct $name<'a> { + pub condition: Cow<'a, str>, #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub negated: bool, @@ -65,10 +67,10 @@ macro_rules! gen_cmp_num_struct { pub comparison: Cmp, #[serde(default)] #[serde(rename = "Numeric value")] - pub numeric_value: NumericValue, + pub numeric_value: NumericValue<'a>, } - impl Default for $name { + impl Default for $name<'_> { fn default() -> Self { Self { condition: stringify!($name).into(), @@ -98,21 +100,21 @@ macro_rules! gen_one_plugin_struct { $( $(#[$attr])* #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - pub struct $name { - pub condition: CompactString, + pub struct $name<'a> { + pub condition: Cow<'a, str>, #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub negated: bool, #[serde(rename = $rename_field)] #[serde(default)] - pub $field: PluginValue, + pub $field: PluginValue<'a>, } - impl Default for $name { + impl Default for $name<'_> { fn default() -> Self { Self { condition: stringify!($name).into(), @@ -127,122 +129,122 @@ macro_rules! gen_one_plugin_struct { } gen_one_plugin_struct!( - HasSpell, spell => "Spell", - IsActorBase, actor_base => "Actor base", - IsClass, class => "Class", - IsCombatStyle, combat_style => "Combat style", - IsEquippedShout, shout => "Shout", - IsInFaction, faction => "Faction", - IsInLocation, location => "Location", - IsParentCell, cell => "Cell", - IsRace, race => "Race", - IsVoiceType, voice_type => "Voice type", - IsWorldSpace, world_space => "WorldSpace", - IsWorn, form => "Form", + HasSpell, spell => "Spell", + IsActorBase, actor_base => "Actor base", + IsClass, class => "Class", + IsCombatStyle, combat_style => "Combat style", + IsEquippedShout, shout => "Shout", + IsInFaction, faction => "Faction", + IsInLocation, location => "Location", + IsParentCell, cell => "Cell", + IsRace, race => "Race", + IsVoiceType, voice_type => "Voice type", + IsWorldSpace, world_space => "WorldSpace", + IsWorn, form => "Form", ); /// Represents a set of conditions that can be serialized to the OAR of functions present in the DAR. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] -pub enum ConditionSet { +pub enum ConditionSet<'a> { /// Represents a logical AND operation between conditions. - And(And), + And(And<'a>), /// Represents a single condition. - Condition(Condition), + Condition(Condition<'a>), /// Represents a comparison between values. - CompareValues(CompareValues), + CompareValues(CompareValues<'a>), /// Represents a condition based on the current game time. - CurrentGameTime(CurrentGameTime), + CurrentGameTime(CurrentGameTime<'a>), /// Represents a condition based on the current weather in the game. - CurrentWeather(CurrentWeather), + CurrentWeather(CurrentWeather<'a>), /// Represents a condition based on the faction rank of an entity. - FactionRank(FactionRank), + FactionRank(FactionRank<'a>), /// Represents a condition based on whether an entity has a certain keyword. - HasKeyword(HasKeyword), + HasKeyword(HasKeyword<'a>), /// Represents a condition based on whether an entity has a specific magic effect. - HasMagicEffect(HasMagicEffect), + HasMagicEffect(HasMagicEffect<'a>), /// Represents a condition based on whether an entity has a magic effect with a certain keyword. - HasMagicEffectWithKeyword(HasMagicEffectWithKeyword), + HasMagicEffectWithKeyword(HasMagicEffectWithKeyword<'a>), /// Represents a condition based on whether an entity has a specific perk. - HasPerk(HasPerk), + HasPerk(HasPerk<'a>), /// Represents a condition based on the reference type of an entity. - HasRefType(HasRefType), + HasRefType(HasRefType<'a>), /// Represents a condition based on whether an entity has a specific spell. - HasSpell(HasSpell), + HasSpell(HasSpell<'a>), /// Represents a condition based on the actor base of an entity. - IsActorBase(IsActorBase), + IsActorBase(IsActorBase<'a>), /// Represents a condition based on the class of an entity. - IsClass(IsClass), + IsClass(IsClass<'a>), /// Represents a condition based on the combat style of an entity. - IsCombatStyle(IsCombatStyle), + IsCombatStyle(IsCombatStyle<'a>), /// Represents a condition based on whether an entity is equipped with something. - IsEquipped(IsEquipped), + IsEquipped(IsEquipped<'a>), /// Represents a condition based on whether an equipped item has a certain keyword. - IsEquippedHasKeyword(IsEquippedHasKeyword), + IsEquippedHasKeyword(IsEquippedHasKeyword<'a>), /// Represents a condition based on whether a shout is equipped. - IsEquippedShout(IsEquippedShout), + IsEquippedShout(IsEquippedShout<'a>), /// Represents a condition based on the equipped type of an entity. - IsEquippedType(IsEquippedType), + IsEquippedType(IsEquippedType<'a>), /// Represents a condition based on whether an entity is in a faction. - IsInFaction(IsInFaction), + IsInFaction(IsInFaction<'a>), /// Represents a condition based on whether an entity is in a specific location. - IsInLocation(IsInLocation), + IsInLocation(IsInLocation<'a>), /// Represents a condition based on the parent cell of an entity. - IsParentCell(IsParentCell), + IsParentCell(IsParentCell<'a>), /// Represents a condition based on the race of an entity. - IsRace(IsRace), + IsRace(IsRace<'a>), /// Represents a condition based on the voice type of an entity. - IsVoiceType(IsVoiceType), + IsVoiceType(IsVoiceType<'a>), /// Represents a condition based on the world space of an entity. - IsWorldSpace(IsWorldSpace), + IsWorldSpace(IsWorldSpace<'a>), /// Represents a condition based on whether an entity is worn. - IsWorn(IsWorn), + IsWorn(IsWorn<'a>), /// Represents a condition based on whether a worn item has a certain keyword. - IsWornHasKeyword(IsWornHasKeyword), + IsWornHasKeyword(IsWornHasKeyword<'a>), /// Represents a condition based on the movement direction of an entity. - IsDirectionMovement(IsMovementDirection), + IsDirectionMovement(IsMovementDirection<'a>), /// Represents a condition based on the level of an entity. - Level(Level), + Level(Level<'a>), /// Represents a logical OR operation between conditions. - Or(Or), + Or(Or<'a>), /// Represents a random condition. - RandomCondition(RandomCondition), + RandomCondition(RandomCondition<'a>), } -impl TryFrom for Vec { +impl<'a> TryFrom> for Vec> { type Error = ConditionError; - fn try_from(value: ConditionSet) -> Result { + fn try_from(value: ConditionSet<'a>) -> Result { Ok(match value { ConditionSet::And(and) => and.conditions, ConditionSet::Or(or) => or.conditions, @@ -250,11 +252,3 @@ impl TryFrom for Vec { }) } } - -/// Represents an error that can occur while working with conditions. -#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] -pub enum ConditionError { - /// Error indicating failure to cast to Vec. - #[error("Only And or Or can be converted to Vec.")] - CastError, -} diff --git a/core/src/conditions/or.rs b/core/src/conditions/or.rs index a378032..7bf543e 100644 --- a/core/src/conditions/or.rs +++ b/core/src/conditions/or.rs @@ -1,31 +1,31 @@ //! OR condition use super::{condition::default_required_version, is_false, ConditionSet}; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents the "OR" condition in the OAR of functions in the DAR. /// /// - OAR: OR /// - DAR: `fn_name() OR` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Or { +pub struct Or<'a> { /// The name of the condition, which is "OR". - pub condition: CompactString, - /// The required version for compatibility with this condition. + pub condition: Cow<'a, str>, + /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, - /// Indicates whether the condition is negated. + pub required_version: Cow<'a, str>, + /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub negated: bool, /// A vector containing the sub-conditions for the "OR" condition. #[serde(rename = "Conditions")] - pub conditions: Vec, + pub conditions: Vec>, } -impl Default for Or { +impl Default for Or<'_> { fn default() -> Self { Self { condition: "OR".into(), diff --git a/core/src/conditions/random.rs b/core/src/conditions/random.rs index a8b8df6..30e61ee 100644 --- a/core/src/conditions/random.rs +++ b/core/src/conditions/random.rs @@ -1,20 +1,20 @@ //! Represents a condition involving randomness. use super::{condition::default_required_version, is_false}; use crate::values::{Cmp, NumericValue, RandomValue}; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Represents a condition involving randomness. /// /// - OAR (Object Arithmetic Representation): Random #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RandomCondition { +pub struct RandomCondition<'a> { /// The name of the condition, which is "Random". - pub condition: CompactString, + pub condition: Cow<'a, str>, /// The required version for this condition. #[serde(default = "default_required_version")] #[serde(rename = "requiredVersion")] - pub required_version: CompactString, + pub required_version: Cow<'a, str>, /// Indicates whether the condition is negated or not. #[serde(default)] #[serde(skip_serializing_if = "is_false")] @@ -31,10 +31,10 @@ pub struct RandomCondition { /// The numeric value to compare against in the condition. #[serde(default)] #[serde(rename = "Numeric value")] - pub numeric_value: NumericValue, + pub numeric_value: NumericValue<'a>, } -impl Default for RandomCondition { +impl Default for RandomCondition<'_> { fn default() -> Self { Self { condition: "Random".into(), diff --git a/core/src/dar_syntax/ast/condition.rs b/core/src/dar_syntax/ast/condition.rs new file mode 100644 index 0000000..9625571 --- /dev/null +++ b/core/src/dar_syntax/ast/condition.rs @@ -0,0 +1,29 @@ +//! Represents a high-level condition, which can be an AND combination, OR combination, or a leaf expression. +use super::expression::Expression; + +/// Represents a high-level condition, which can be an AND combination, OR combination, or a leaf expression. +#[derive(Debug, Clone, PartialEq)] +pub enum Condition<'input> { + /// Represents an AND combination of multiple conditions. + And(Vec>), + /// Represents an OR combination of multiple conditions. + Or(Vec>), + /// Represents a leaf expression within the condition hierarchy. + Exp(Expression<'input>), +} + +impl<'input> Condition<'input> { + /// push to inner vec + /// + /// # panics + /// If push to [`Self::Exp`] + pub(crate) fn push(&mut self, expression: Self) { + match self { + Condition::And(inner) | Condition::Or(inner) => inner.push(expression), + Condition::Exp(_) => { + #[cfg(feature = "tracing")] + tracing::error!("Expression cannot push"); + } + } + } +} diff --git a/core/src/dar_syntax/ast/expression.rs b/core/src/dar_syntax/ast/expression.rs new file mode 100644 index 0000000..ae1e28f --- /dev/null +++ b/core/src/dar_syntax/ast/expression.rs @@ -0,0 +1,13 @@ +//! DAR One line representation +use super::fn_args::FnArgs; + +/// DAR One line representation +#[derive(Debug, Clone, PartialEq)] +pub struct Expression<'input> { + /// not condition + pub negated: bool, + /// function name == condition name + pub fn_name: &'input str, + /// arguments + pub args: FnArgs<'input>, +} diff --git a/core/src/dar_syntax/ast/fn_arg.rs b/core/src/dar_syntax/ast/fn_arg.rs new file mode 100644 index 0000000..e61fdb0 --- /dev/null +++ b/core/src/dar_syntax/ast/fn_arg.rs @@ -0,0 +1,31 @@ +//! DAR Function arguments +use super::number_literal::NumberLiteral; +use core::fmt; + +/// DAR Function arguments +/// - Plugin e.g. Skyrim.esm | 0x007 +/// - Literal e.g. 1.0 +#[derive(Debug, Clone, PartialEq)] +pub enum FnArg<'input> { + /// e.g. "Skyrim.esm" | 0x007 + PluginValue { + /// e.g. "Skyrim.esm" + plugin_name: &'input str, + /// e.g. 1 + form_id: NumberLiteral, + }, + /// Just number. (e.g. 1) + Number(NumberLiteral), +} + +impl fmt::Display for FnArg<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FnArg::PluginValue { + plugin_name, + form_id, + } => write!(f, r#""{plugin_name}" | {form_id}"#), + FnArg::Number(num) => write!(f, "{num}"), + } + } +} diff --git a/core/src/dar_syntax/ast/fn_args.rs b/core/src/dar_syntax/ast/fn_args.rs new file mode 100644 index 0000000..5cbcd01 --- /dev/null +++ b/core/src/dar_syntax/ast/fn_args.rs @@ -0,0 +1,60 @@ +//! DAR function arguments +use super::fn_arg::FnArg; +use crate::dar_syntax::errors::{EmptyCollectionSnafu, Result}; +use std::collections::VecDeque; +use winnow::stream::Accumulate; + +/// Function args +#[derive(Debug, Clone, Default, PartialEq)] +pub struct FnArgs<'input>(pub VecDeque>); + +impl<'input> FnArgs<'input> { + /// Create a new `FnArgs` + #[cfg(test)] + pub const fn new() -> Self { + Self(VecDeque::new()) + } + + /// Pop the first element and return a `Result` + pub fn pop_front(&mut self) -> Result> { + self.0.pop_front().ok_or(EmptyCollectionSnafu.build()) + } +} + +// Impl Trait to use `winnow::separated` +// +// NOTE: VecDeque is a std lib, so we can avoid orphan rule errors by impl the wrapped trait. +impl<'a> Accumulate> for FnArgs<'a> { + #[inline(always)] + fn initial(capacity: Option) -> Self { + capacity.map_or_else( + || FnArgs(VecDeque::new()), + |capacity| FnArgs(VecDeque::with_capacity(capacity)), + ) + } + #[inline(always)] + fn accumulate(&mut self, acc: FnArg<'a>) { + self.0.push_back(acc); + } +} + +/// It is difficult to create test data because `VecDeque` does not have macros like `vec![]`. +/// +/// Therefore, we will create a wrapper type macro to make it more convenient for testing. +#[cfg(test)] +macro_rules! fn_args { + () => ( + $crate::dar_syntax::ast::fn_args::FnArgs::new() + ); + ( $( $arg:expr ),* $(,)? ) => { + { + let mut args = $crate::dar_syntax::ast::fn_args::FnArgs::new(); + $( + args.0.push_back($arg); + )* + args + } + }; +} +#[cfg(test)] +pub(crate) use fn_args; diff --git a/core/src/dar_syntax/ast/mod.rs b/core/src/dar_syntax/ast/mod.rs new file mode 100644 index 0000000..c62c781 --- /dev/null +++ b/core/src/dar_syntax/ast/mod.rs @@ -0,0 +1,7 @@ +//! DAR abstract syntax tree +pub mod condition; +pub mod expression; +pub mod fn_arg; +pub mod fn_args; +pub mod number_literal; +pub mod op; diff --git a/core/src/dar_syntax/ast/number_literal.rs b/core/src/dar_syntax/ast/number_literal.rs new file mode 100644 index 0000000..041f2bb --- /dev/null +++ b/core/src/dar_syntax/ast/number_literal.rs @@ -0,0 +1,34 @@ +//! Hex | Decimal | Float +use core::fmt; + +/// Hex | Decimal | Float +#[derive(Clone, PartialEq)] +pub enum NumberLiteral { + /// e.g. 0x007 + Hex(usize), + /// e.g. 1 + Decimal(isize), + /// e.g. 1.0 + Float(f32), +} + +// Hex debugging display is displayed in hexadecimal notation because it is difficult to understand if it is in decimal. +impl fmt::Debug for NumberLiteral { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Hex(arg0) => f.debug_tuple("Hex").field(&format!("{arg0:#x}")).finish(), + Self::Decimal(arg0) => f.debug_tuple("Decimal").field(arg0).finish(), + Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(), + } + } +} + +impl fmt::Display for NumberLiteral { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Hex(hex) => write!(f, "0x{hex:x}"), + Self::Decimal(decimal) => write!(f, "{decimal}"), + Self::Float(float) => write!(f, "{float}"), + } + } +} diff --git a/core/src/dar_syntax/ast/op.rs b/core/src/dar_syntax/ast/op.rs new file mode 100644 index 0000000..2d1d711 --- /dev/null +++ b/core/src/dar_syntax/ast/op.rs @@ -0,0 +1,10 @@ +//! AND | OR + +/// AND | OR +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Operator { + /// AND + And, + /// OR + Or, +} diff --git a/core/src/dar_syntax/errors/mod.rs b/core/src/dar_syntax/errors/mod.rs new file mode 100644 index 0000000..650334e --- /dev/null +++ b/core/src/dar_syntax/errors/mod.rs @@ -0,0 +1,21 @@ +//! DAR parser errors +pub mod readable_error; + +/// DAR error +#[derive(Debug, Clone, snafu::Snafu, PartialEq, Eq)] +#[snafu(visibility(pub))] +pub enum DarError { + // New error variant to represent an empty collection. + /// Failed to pop from an empty collection + EmptyCollectionError, + + /// Readable parser position error + #[snafu(transparent)] + ReadableError { + /// transparent + source: readable_error::ReadableError, + }, +} + +/// DAR Result +pub type Result = core::result::Result; diff --git a/core/src/dar_syntax/error.rs b/core/src/dar_syntax/errors/readable_error.rs similarity index 97% rename from core/src/dar_syntax/error.rs rename to core/src/dar_syntax/errors/readable_error.rs index c2bb9c1..63ba04e 100644 --- a/core/src/dar_syntax/error.rs +++ b/core/src/dar_syntax/errors/readable_error.rs @@ -4,7 +4,7 @@ use core::ops::Range; use winnow::error::{ContextError, ParseError}; /// Error struct to represent parsing errors in a more user-friendly way. -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ReadableError { /// Title(e.g. Error tittle, Path) pub title: String, diff --git a/core/src/dar_syntax/mod.rs b/core/src/dar_syntax/mod.rs index dc886b8..61c7fea 100644 --- a/core/src/dar_syntax/mod.rs +++ b/core/src/dar_syntax/mod.rs @@ -1,6 +1,10 @@ //! DAR syntax parser & error handling -mod error; -mod float; -pub mod syntax; +pub mod ast; +pub mod errors; +mod parser; -pub use syntax::parse_dar_syntax; +pub use self::ast::{ + condition::Condition, expression::Expression, fn_arg::FnArg, fn_args::FnArgs, + number_literal::NumberLiteral, +}; +pub use self::parser::parse_dar_syntax; diff --git a/core/src/dar_syntax/parser/comment.rs b/core/src/dar_syntax/parser/comment.rs new file mode 100644 index 0000000..b31ccc1 --- /dev/null +++ b/core/src/dar_syntax/parser/comment.rs @@ -0,0 +1,45 @@ +//! DAR comment parser +use super::Stream; +use winnow::{ + ascii::{multispace0, till_line_ending}, + combinator::{delimited, preceded, repeat}, + error::{ + AddContext, PResult, ParserError, + StrContext::{self, Expected, Label}, + StrContextValue::Description, + }, + Parser, +}; + +/// Comments starting with ';' until newline. 0 or more. +pub fn line_comments0<'i, E>(input: &mut Stream<'i>) -> PResult>, E> +where + E: ParserError> + AddContext, StrContext>, +{ + repeat(0.., line_comment).parse_next(input) +} + +/// Comment starting with ';' until newline +fn line_comment<'i, E>(input: &mut Stream<'i>) -> PResult, E> +where + E: ParserError> + AddContext, StrContext>, +{ + delimited(multispace0, preceded(';', till_line_ending), multispace0) + .context(Label("Comment")) + .context(Expected(Description("Comment(e.g. `; Any String`)"))) + .parse_next(input) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dar_syntax::parser::parse_assert; + + #[test] + fn should_parse_comment() { + let input = r#" + ; comment +"#; + parse_assert!(line_comment(input), " comment"); + } +} diff --git a/core/src/dar_syntax/parser/condition.rs b/core/src/dar_syntax/parser/condition.rs new file mode 100644 index 0000000..3a8da61 --- /dev/null +++ b/core/src/dar_syntax/parser/condition.rs @@ -0,0 +1,93 @@ +//! DAR root parser +use super::Stream; +use crate::dar_syntax::{ + ast::{condition::Condition, op::Operator}, + parser::{comment::line_comments0, expression::parse_expression, op::parse_operator}, +}; +use std::{mem, num::ParseIntError}; +use winnow::{ + ascii::multispace0, + combinator::{eof, opt}, + error::{ + AddContext, FromExternalError, PResult, ParserError, + StrContext::{self, Expected, Label}, + StrContextValue::Description, + }, + seq, Parser, +}; + +/// Parse DAR syntax +pub fn parse_condition<'i, E>(input: &mut Stream<'i>) -> PResult, E> +where + E: ParserError> + + AddContext, StrContext> + + FromExternalError, ParseIntError>, +{ + let mut top_conditions = Condition::And(Vec::new()); + let mut or_vec = Vec::new(); + let mut is_in_or_stmt = false; + + loop { + #[cfg(feature = "tracing")] + tracing::trace!("top_conditions = {top_conditions:#?},\nor_vec = {or_vec:#?}"); + + let _ = multispace0(input)?; + // Dealing with cases where nothing is written in _condition.txt + if input.is_empty() { + break; + } + + let _ = line_comments0(input)?; + let (expr, operator) = seq!(parse_expression, opt(parse_operator)).parse_next(input)?; + let _ = line_comments0(input)?; + let _ = multispace0(input)?; + + #[cfg(feature = "tracing")] + tracing::trace!("expr: {expr:#?}"); + if let Some(operator) = operator { + match operator { + Operator::And => { + if is_in_or_stmt { + or_vec.push(Condition::Exp(expr)); + top_conditions.push(Condition::Or(mem::take(&mut or_vec))); + is_in_or_stmt = false; + } else { + top_conditions.push(Condition::Exp(expr)); + } + } + Operator::Or => { + or_vec.push(Condition::Exp(expr)); + is_in_or_stmt = true; + } + }; + + // To support tailing `OR` or `AND` statement. + if input.is_empty() { + if is_in_or_stmt { + top_conditions.push(Condition::Or(mem::take(&mut or_vec))); + } + break; + } + } else { + match is_in_or_stmt { + true => { + or_vec.push(Condition::Exp(expr)); + top_conditions.push(Condition::Or(or_vec)); + } + false => top_conditions.push(Condition::Exp(expr)), + } + + let _ = eof + .context(Label("End of file")) + .context(Expected(Description("end of file"))) + .context(Expected(Description("Tailing op: `OR` or `AND`"))) + .context(Expected(Description( + "Conditional statement(if it has op): e.g. `NOT IsInCombat() AND`", + ))) + .parse_next(input)?; + break; + } + } + + Ok(top_conditions) +} diff --git a/core/src/dar_syntax/parser/expression.rs b/core/src/dar_syntax/parser/expression.rs new file mode 100644 index 0000000..58436b6 --- /dev/null +++ b/core/src/dar_syntax/parser/expression.rs @@ -0,0 +1,41 @@ +//! DAR one line parser +use super::{fn_call::fn_call, Stream}; +use crate::dar_syntax::ast::expression::Expression; +use std::num::ParseIntError; +use winnow::{ + ascii::{multispace0, Caseless}, + combinator::opt, + error::{ + AddContext, FromExternalError, PResult, ParserError, + StrContext::{self, Expected}, + StrContextValue::StringLiteral, + }, + seq, Parser, +}; + +/// Parse one line DAR Syntax +/// # Expected Syntax examples +/// ```txt +/// NOT IsInCombat() +/// ``` +pub fn parse_expression<'i, E>(input: &mut Stream<'i>) -> PResult, E> +where + E: ParserError> + + AddContext, StrContext> + + FromExternalError, ParseIntError>, +{ + seq!( + _: multispace0, + opt(Caseless("NOT")).context(Expected(StringLiteral("NOT"))) + .map(|not| not.is_some()), + _: multispace0, + fn_call, + _: multispace0, + ) + .map(|(negated, (fn_name, args))| Expression { + negated, + fn_name, + args, + }) + .parse_next(input) +} diff --git a/core/src/dar_syntax/parser/fn_call/arg_types/mod.rs b/core/src/dar_syntax/parser/fn_call/arg_types/mod.rs new file mode 100644 index 0000000..8a38533 --- /dev/null +++ b/core/src/dar_syntax/parser/fn_call/arg_types/mod.rs @@ -0,0 +1,34 @@ +//! DAR argument types +pub mod number; +pub mod plugin; +pub mod string; + +use self::{number::number, plugin::parse_plugin}; +use crate::dar_syntax::{ast::fn_arg::FnArg, parser::Stream}; +use std::num::ParseIntError; +use winnow::{ + combinator::{alt, fail}, + error::{ + AddContext, FromExternalError, PResult, ParserError, + StrContext::{self, Expected, Label}, + StrContextValue::{Description, StringLiteral}, + }, + Parser, +}; + +/// Parse function arguments. +pub fn parse_fn_arg<'i, E>(input: &mut Stream<'i>) -> PResult, E> +where + E: ParserError> + + AddContext, StrContext> + + FromExternalError, ParseIntError>, +{ + alt(( + parse_plugin, + number.map(FnArg::Number), + fail.context(Label("function argument")) + .context(Expected(Description("plugin"))) + .context(Expected(StringLiteral("number"))), + )) + .parse_next(input) +} diff --git a/core/src/dar_syntax/parser/fn_call/arg_types/number.rs b/core/src/dar_syntax/parser/fn_call/arg_types/number.rs new file mode 100644 index 0000000..46a9940 --- /dev/null +++ b/core/src/dar_syntax/parser/fn_call/arg_types/number.rs @@ -0,0 +1,92 @@ +//! DAR Number type +use super::Stream; +use crate::dar_syntax::{ast::number_literal::NumberLiteral, parser::winnow_wrapper::float::float}; +use std::num::ParseIntError; +use winnow::{ + ascii::{dec_int, digit1, hex_digit1, oct_digit1}, + combinator::{alt, fail}, + dispatch, + error::{ + AddContext, FromExternalError, ParserError, + StrContext::{self, Expected, Label}, + StrContextValue::{Description, StringLiteral}, + }, + token::take, + PResult, Parser as _, +}; + +/// Replace a prefixed radix number such as `0x` with Replace with hexadecimal number without prefix. +fn radix_digits<'i, E>(input: &mut Stream<'i>) -> PResult +where + E: ParserError> + + AddContext, StrContext> + + FromExternalError, ParseIntError>, +{ + dispatch!(take(2_usize); + "0b" | "0B" => digit1.try_map(|s| usize::from_str_radix(s, 2)) + .context(Label("digit")).context(Expected(Description("binary"))), + "0o" | "0O" => oct_digit1.try_map(|s| usize::from_str_radix(s, 8)) + .context(Label("digit")).context(Expected(Description("octal"))), + "0d" | "0D" => digit1.try_map(|s: &str| s.parse::()) + .context(Label("digit")).context(Expected(Description("decimal"))), + "0x" | "0X" => hex_digit1.try_map(|s|usize::from_str_radix(s, 16)) + .context(Label("digit")).context(Expected(Description("hexadecimal"))), + _ => fail.context(Label("radix prefix")) + .context(Expected(StringLiteral("0b"))) + .context(Expected(StringLiteral("0o"))) + .context(Expected(StringLiteral("0d"))) + .context(Expected(StringLiteral("0x"))), + ) + .map(NumberLiteral::Hex) + .parse_next(input) +} + +/// Parse a number(e.g. "0x123", "123", "12.3") +pub fn number<'i, E>(input: &mut Stream<'i>) -> PResult +where + E: ParserError> + + AddContext, StrContext> + + FromExternalError, ParseIntError>, +{ + alt(( + radix_digits.context(Label("number")), + float.map(NumberLiteral::Float).context(Label("number")), + dec_int.map(NumberLiteral::Decimal).context(Label("number")), + // At this point, if the string `Hi`, etc. is received, the following error report is made. + fail.context(Label("number")) + .context(Expected(Description("radix: e.g. `0x007`"))) + .context(Expected(Description("float: e.g. `33.0`"))) + .context(Expected(Description("decimal: e.g. `10`"))), + )) + .parse_next(input) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dar_syntax::parser::parse_assert; + use winnow::error::ContextError; + + #[test] + fn should_parse_radix_number() { + parse_assert!(radix_digits("0b1010"), NumberLiteral::Hex(10)); + parse_assert!(radix_digits("0B1010"), NumberLiteral::Hex(10)); + parse_assert!(radix_digits("0o37"), NumberLiteral::Hex(31)); + parse_assert!(radix_digits("0O37"), NumberLiteral::Hex(31)); + parse_assert!(radix_digits("0x00000007"), NumberLiteral::Hex(7)); + parse_assert!(radix_digits("0X1A"), NumberLiteral::Hex(26)); + } + + #[test] + fn should_error_radix_number() { + assert!(radix_digits::.parse("0z123").is_err()); + assert!(radix_digits::.parse("0x").is_err()); + } + + #[test] + fn should_parse_number() { + parse_assert!(number("33"), NumberLiteral::Decimal(33)); + parse_assert!(number("33.0"), NumberLiteral::Float(33.0)); + parse_assert!(number("0x00000007"), NumberLiteral::Hex(0x00000007)); + } +} diff --git a/core/src/dar_syntax/parser/fn_call/arg_types/plugin.rs b/core/src/dar_syntax/parser/fn_call/arg_types/plugin.rs new file mode 100644 index 0000000..8e46aea --- /dev/null +++ b/core/src/dar_syntax/parser/fn_call/arg_types/plugin.rs @@ -0,0 +1,37 @@ +//! DAR Plugin value type +use super::{number::number, string::string, Stream}; +use crate::dar_syntax::ast::fn_arg::FnArg; +use std::num::ParseIntError; +use winnow::{ + ascii::multispace0, + combinator::{delimited, separated_pair}, + error::{ + AddContext, FromExternalError, PResult, ParserError, + StrContext::{self, Expected, Label}, + StrContextValue::Description, + }, + Parser, +}; + +/// Parse plugin value(e.g. `"Skyrim.esm" | 0x007`) +pub fn parse_plugin<'i, E>(input: &mut Stream<'i>) -> PResult, E> +where + E: ParserError> + + AddContext, StrContext> + + FromExternalError, ParseIntError>, +{ + separated_pair( + delimited(multispace0, string, multispace0), + "|", + delimited(multispace0, number, multispace0), + ) + .map(|(plugin_name, form_id)| FnArg::PluginValue { + plugin_name, + form_id, + }) + .context(Label("Plugin")) + .context(Expected(Description( + r#"Plugin: e.g. `"Skyrim.esm" | 0x007`"#, + ))) + .parse_next(input) +} diff --git a/core/src/dar_syntax/parser/fn_call/arg_types/string.rs b/core/src/dar_syntax/parser/fn_call/arg_types/string.rs new file mode 100644 index 0000000..653a2f7 --- /dev/null +++ b/core/src/dar_syntax/parser/fn_call/arg_types/string.rs @@ -0,0 +1,51 @@ +//! DAR string value type +use super::Stream; +use winnow::{ + ascii::take_escaped, + combinator::{alt, delimited}, + error::{ + AddContext, PResult, ParserError, + StrContext::{self, Expected, Label}, + StrContextValue::Description, + }, + token::{one_of, take_while}, + Parser, +}; + +/// single or double quote string +pub fn string<'i, E>(input: &mut Stream<'i>) -> PResult<&'i str, E> +where + E: ParserError> + AddContext, StrContext>, +{ + let single_quote = take_escaped( + take_while(1.., |c| c != '\'' && c != '\\'), + '\\', + one_of(['\\', '\'']), + ); + + let double_quote = take_escaped( + take_while(1.., |c| c != '\"' && c != '\\'), + '\\', + one_of(['\\', '"']), + ); + + alt(( + delimited('\"', single_quote, '\"'), + delimited('"', double_quote, '"'), + )) + .context(Label("String")) + .context(Expected(Description(r#"String: e.g. `"Skyrim.esm"`"#))) + .parse_next(input) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dar_syntax::parser::parse_assert; + + #[test] + fn should_parse_string() { + parse_assert!(string(r#""0""#), "0"); + parse_assert!(string(r#""with\"escaped""#), "with\\\"escaped"); + } +} diff --git a/core/src/dar_syntax/parser/fn_call/ident.rs b/core/src/dar_syntax/parser/fn_call/ident.rs new file mode 100644 index 0000000..3840ddc --- /dev/null +++ b/core/src/dar_syntax/parser/fn_call/ident.rs @@ -0,0 +1,33 @@ +//! DAR function identifier +use super::Stream; +use winnow::{ + error::{ + AddContext, PResult, ParserError, + StrContext::{self, Expected, Label}, + StrContextValue::Description, + }, + token::take_while, + Parser, +}; + +/// Parse identifier +pub fn ident<'i, E>(input: &mut Stream<'i>) -> PResult<&'i str, E> +where + E: ParserError> + AddContext, StrContext>, +{ + take_while(1.., |c: char| c.is_alphanumeric() || c == '_') + .context(Label("Identifier")) + .context(Expected(Description("Identifier(e.g. `IsActorBase`)"))) + .parse_next(input) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dar_syntax::parser::parse_assert; + + #[test] + fn should_parse_ident() { + parse_assert!(ident("IsActorBase"), "IsActorBase"); + } +} diff --git a/core/src/dar_syntax/parser/fn_call/mod.rs b/core/src/dar_syntax/parser/fn_call/mod.rs new file mode 100644 index 0000000..b857f70 --- /dev/null +++ b/core/src/dar_syntax/parser/fn_call/mod.rs @@ -0,0 +1,74 @@ +//! DAR function +pub mod arg_types; +pub mod ident; + +use self::arg_types::parse_fn_arg; +use self::ident::ident; +use super::{winnow_wrapper::delimited_with_multispace0::delimited_multispace0, Stream}; +use crate::dar_syntax::ast::fn_args::FnArgs; +use std::num::ParseIntError; +use winnow::{ + ascii::multispace0, + combinator::{delimited, opt, separated}, + error::{ + AddContext, FromExternalError, PResult, ParserError, + StrContext::{self, Label}, + }, + seq, Parser, +}; + +/// Parse function call(with arguments) +/// +/// # Expected Syntax Examples +/// ```txt +/// ; Pattern1 +/// IsActorBase("Skyrim.esm" | 0x00000007) +/// +/// ; Pattern2 +/// IsActorValueEqualTo(0x00000007, 30) +/// ``` +pub fn fn_call<'i, E>(input: &mut Stream<'i>) -> PResult<(&'i str, FnArgs<'i>), E> +where + E: ParserError> + + AddContext, StrContext> + + FromExternalError, ParseIntError>, +{ + seq!( + ident, + opt(delimited( + delimited_multispace0("("), + separated( + 0.., + delimited(multispace0, parse_fn_arg, multispace0).context(Label("FnArg")), + "," + ), + delimited_multispace0(")"), + )) + .map(|args| args.unwrap_or_default()) + ) + .context(Label("Function call")) + .parse_next(input) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dar_syntax::{ + ast::{fn_arg::FnArg, fn_args::fn_args, number_literal::NumberLiteral}, + parser::parse_assert, + }; + + #[test] + fn should_parse_fn_call() { + let input = r#"IsActorValueLessThan(30, 60)"#; + let expected = ( + "IsActorValueLessThan", + fn_args![ + FnArg::Number(NumberLiteral::Decimal(30)), + FnArg::Number(NumberLiteral::Decimal(60)), + ], + ); + + parse_assert!(fn_call(input), expected); + } +} diff --git a/core/src/dar_syntax/parser/mod.rs b/core/src/dar_syntax/parser/mod.rs new file mode 100644 index 0000000..e678925 --- /dev/null +++ b/core/src/dar_syntax/parser/mod.rs @@ -0,0 +1,229 @@ +//! DAR syntax parser +//! +//! # Example +//! ```txt +//! IsActorBase("Skyrim.esm" | 0x00000007) OR +//! ; comment +//! IsPlayerTeammate() AND +//! IsEquippedRightType(3) OR +//! IsEquippedRightType(4) +//! ``` +//! +//! # EBNF +//! - A | B: A or B +//! - \[ A \]: A is option +//! - { "," A }: 0 or more repetitions "," A +//! +//! ```ebnf +//! line = comment | expression +//! +//! comment = ";" [^"\n]* ; +//! expression = [ "NOT" ] function ( "AND" | "OR" ) ; +//! argument_list = argument { "," argument } ; +//! argument = plugin | number ; +//! +//! function = identifier | identifier "(" argument_list ")" ; +//! +//! identifier = (ASCII | "_") { ASCII | "_" } ; +//! +//! plugin = string "|" number ; +//! +//! string = "\"" [^"\n]* "\"" | "'" [^'\n]* "'" ; +//! number = decimal | hex | float ; +//! +//! decimal = ["-"] digit { digit } ; +//! hex = "0x" hex_digit { hex_digit } | "0X" hex_digit { hex_digit } ; +//! float = ["-"] digit { digit } "." digit { digit } ; +//! digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; +//! hex_digit = digit | "a" | "b" | "c" | "d" | "e" | "f" | "A" | "B" | "C" | "D" | "E" | "F" ; +//! ``` +pub mod comment; +pub mod condition; +pub mod expression; +pub mod fn_call; +pub mod op; +pub mod winnow_wrapper; + +use super::{ + ast::condition::Condition, + errors::{readable_error::ReadableError, Result}, +}; +use condition::parse_condition; +use winnow::{error::ContextError, Parser as _}; + +/// Parsing target. +pub type Stream<'i> = &'i str; + +/// Parse DAR syntax. +pub fn parse_dar_syntax(input: Stream<'_>) -> Result> { + let syntax = input; + Ok(parse_condition:: + .parse(syntax) + .map_err(|error| ReadableError::from_parse(error, input))?) +} + +#[cfg(test)] +macro_rules! parse_assert { + ($parser:ident($input:tt), $expected:expr) => { + match $parser::.parse($input) { + Ok(actual) => pretty_assertions::assert_eq!(actual, $expected), + Err(err) => panic!( + "{}", + crate::dar_syntax::errors::readable_error::ReadableError::from_parse(err, $input) + ), + } + }; +} +#[cfg(test)] +pub(crate) use parse_assert; + +#[cfg(test)] +mod tests { + use super::*; + use crate::dar_syntax::ast::{ + expression::Expression, fn_arg::FnArg, fn_args::fn_args, number_literal::NumberLiteral, + }; + use pretty_assertions::assert_eq; + + #[test] + #[cfg_attr(feature = "tracing", quick_tracing::init)] + fn should_parse_conditions() { + let input = r#" +IsActorBase("Skyrim.esm" | 0X000007) AND +NOT IsInCombat() AND +NOT IsActorValueLessThan(30, 60) + "#; + + let expected = Condition::And(vec![ + Condition::Exp(Expression { + fn_name: "IsActorBase", + args: fn_args![FnArg::PluginValue { + plugin_name: "Skyrim.esm", + form_id: NumberLiteral::Hex(0x7), + }], + negated: false, + }), + Condition::Exp(Expression { + fn_name: "IsInCombat", + args: fn_args![], + negated: true, + }), + Condition::Exp(Expression { + negated: true, + fn_name: "IsActorValueLessThan", + args: fn_args![ + FnArg::Number(NumberLiteral::Decimal(30)), + FnArg::Number(NumberLiteral::Decimal(60)), + ], + }), + ]); + + match parse_dar_syntax(input) { + Ok(actual) => assert_eq!(actual, expected), + Err(err) => panic!("{err}"), + } + } + + #[test] + #[cfg_attr(feature = "tracing", quick_tracing::init)] + fn should_parse_conditions_with_comments() { + let input = r#" + ; This is start of line comment. + + IsActorBase("Skyrim.esm" | 0x00BCDEF7) Or + ; Parse test only indent function call. + + noT IsPlayerTeammate aNd + ; This is a line comment. + ; This is a line comment. + + IsEquippedRightType(3) OR + + ; This is a line comment. + IsEquippedRightType(4) + + ; This is end of line comment. + ; This is end of line comment. + +"#; + + let actor = Expression { + negated: false, + fn_name: "IsActorBase", + args: fn_args![FnArg::PluginValue { + plugin_name: "Skyrim.esm", + form_id: NumberLiteral::Hex(0x00BC_DEF7), + }], + }; + let player = Expression { + negated: true, + fn_name: "IsPlayerTeammate", + args: fn_args![], + }; + let equip_r3 = Expression { + negated: false, + fn_name: "IsEquippedRightType", + args: fn_args![FnArg::Number(NumberLiteral::Decimal(3))], + }; + let equip_r4 = Expression { + negated: false, + fn_name: "IsEquippedRightType", + args: fn_args![FnArg::Number(NumberLiteral::Decimal(4))], + }; + + let expected = Condition::And(vec![ + Condition::Or(vec![Condition::Exp(actor), Condition::Exp(player)]), + Condition::Or(vec![Condition::Exp(equip_r3), Condition::Exp(equip_r4)]), + ]); + + match parse_dar_syntax(input) { + Ok(actual) => assert_eq!(actual, expected), + Err(err) => panic!("{err}"), + } + } + + #[test] + fn should_parse_with_space() { + let input = r#" IsActorBase ( "Skyrim.esm"|0x00000007 ) "#; + let expected = Condition::And(vec![Condition::Exp(Expression { + negated: false, + fn_name: "IsActorBase", + args: fn_args![FnArg::PluginValue { + plugin_name: "Skyrim.esm", + form_id: NumberLiteral::Hex(0x0000_0007), + }], + })]); + + assert_eq!(parse_condition::.parse(input), Ok(expected)); + } + + #[test] + fn should_parse_tailing_or() { + let input = "NOT IsActorBase ( \"Skyrim.esm\" | 0x00000007 )OR"; + let expected = Condition::And(vec![Condition::Or(vec![Condition::Exp(Expression { + negated: true, + fn_name: "IsActorBase", + args: fn_args![FnArg::PluginValue { + plugin_name: "Skyrim.esm", + form_id: NumberLiteral::Hex(0x0000_0007), + }], + })])]); + + assert_eq!(parse_condition::.parse(input), Ok(expected)); + } + + #[test] + fn should_parse_tailing_and() { + let input = "NOT IsActorBase ( \"Skyrim.esm\" | 0x00000007 )AND"; + let expected = Condition::And(vec![Condition::Exp(Expression { + negated: true, + fn_name: "IsActorBase", + args: fn_args![FnArg::PluginValue { + plugin_name: "Skyrim.esm", + form_id: NumberLiteral::Hex(0x0000_0007), + }], + })]); + + assert_eq!(parse_condition::.parse(input), Ok(expected)); + } +} diff --git a/core/src/dar_syntax/parser/op.rs b/core/src/dar_syntax/parser/op.rs new file mode 100644 index 0000000..bb26e3b --- /dev/null +++ b/core/src/dar_syntax/parser/op.rs @@ -0,0 +1,32 @@ +//! DAR tailing operator parser +use super::Stream; +use crate::dar_syntax::ast::op::Operator; +use winnow::{ + ascii::{multispace0, Caseless}, + combinator::{alt, preceded}, + error::{ + AddContext, PResult, ParserError, + StrContext::{self, Expected, Label}, + StrContextValue::StringLiteral, + }, + Parser, +}; + +/// - Expect an AND or OR string. +/// - After AND or OR comes Expression with a line break in between, so the line break is also checked. +pub fn parse_operator<'i, E>(input: &mut Stream<'i>) -> PResult +where + E: ParserError> + AddContext, StrContext>, +{ + preceded( + multispace0, + alt(( + Caseless("AND").value(Operator::And), + Caseless("OR").value(Operator::Or), + )), + ) + .context(Label("Operator")) + .context(Expected(StringLiteral("AND"))) + .context(Expected(StringLiteral("OR"))) + .parse_next(input) +} diff --git a/core/src/dar_syntax/parser/winnow_wrapper/delimited_with_multispace0.rs b/core/src/dar_syntax/parser/winnow_wrapper/delimited_with_multispace0.rs new file mode 100644 index 0000000..05bac0b --- /dev/null +++ b/core/src/dar_syntax/parser/winnow_wrapper/delimited_with_multispace0.rs @@ -0,0 +1,25 @@ +//! winnow utility +use winnow::{ + ascii::multispace0, + combinator::trace, + error::ParserError, + stream::{AsChar, Stream, StreamIsPartial}, + Parser, +}; + +/// Parser with surrounding whitespace(0 or more times) +pub fn delimited_multispace0( + mut parser: ParseNext, +) -> impl Parser +where + Input: StreamIsPartial + Stream, + Error: ParserError, + ParseNext: Parser, + ::Token: AsChar + Clone, +{ + trace("delimited_multispace0", move |input: &mut Input| { + let _ = multispace0.parse_next(input)?; + let o2 = parser.parse_next(input)?; + multispace0.parse_next(input).map(|_| o2) + }) +} diff --git a/core/src/dar_syntax/float.rs b/core/src/dar_syntax/parser/winnow_wrapper/float.rs similarity index 100% rename from core/src/dar_syntax/float.rs rename to core/src/dar_syntax/parser/winnow_wrapper/float.rs diff --git a/core/src/dar_syntax/parser/winnow_wrapper/mod.rs b/core/src/dar_syntax/parser/winnow_wrapper/mod.rs new file mode 100644 index 0000000..9b7d2d6 --- /dev/null +++ b/core/src/dar_syntax/parser/winnow_wrapper/mod.rs @@ -0,0 +1,3 @@ +//! winnow utility wrapper +pub mod delimited_with_multispace0; +pub mod float; diff --git a/core/src/dar_syntax/syntax.rs b/core/src/dar_syntax/syntax.rs deleted file mode 100644 index 67e5c91..0000000 --- a/core/src/dar_syntax/syntax.rs +++ /dev/null @@ -1,714 +0,0 @@ -//! DAR syntax parser -//! -//! # Example -//! ```txt -//! IsActorBase("Skyrim.esm" | 0x00000007) OR -//! ; comment -//! IsPlayerTeammate() AND -//! IsEquippedRightType(3) OR -//! IsEquippedRightType(4) -//! ``` -//! -//! # EBNF -//! - A | B: A or B -//! - \[ A \]: A is option -//! - { "," A }: 0 or more repetitions "," A -//! -//! ```ebnf -//! line = comment | expression -//! -//! comment = ";" [^"\n]* ; -//! expression = [ "NOT" ] function ( "AND" | "OR" ) ; -//! argument_list = argument { "," argument } ; -//! argument = plugin | number ; -//! -//! function = identifier | identifier "(" argument_list ")" ; -//! -//! identifier = (ASCII | "_") { ASCII | "_" } ; -//! -//! plugin = string "|" number ; -//! -//! string = "\"" [^"\n]* "\"" | "'" [^'\n]* "'" ; -//! number = decimal | hex | float ; -//! -//! decimal = ["-"] digit { digit } ; -//! hex = "0x" hex_digit { hex_digit } | "0X" hex_digit { hex_digit } ; -//! float = ["-"] digit { digit } "." digit { digit } ; -//! digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; -//! hex_digit = digit | "a" | "b" | "c" | "d" | "e" | "f" | "A" | "B" | "C" | "D" | "E" | "F" ; -//! ``` -use core::fmt; -use core::mem; -use core::num::ParseIntError; -use winnow::ascii::{ - dec_int, digit1, hex_digit1, multispace0, oct_digit1, take_escaped, till_line_ending, Caseless, -}; -use winnow::combinator::{ - alt, delimited, dispatch, eof, fail, opt, preceded, repeat, separated, separated_pair, seq, -}; -use winnow::error::{ - AddContext, FromExternalError, PResult, ParserError, - StrContext::{self, Expected, Label}, - StrContextValue, -}; -use winnow::token::{one_of, take, take_while}; -use winnow::Parser; - -use super::error::ReadableError; -use super::float::float; - -/// DAR Function arguments -/// - Plugin e.g. Skyrim.esm | 0x007 -/// - Literal e.g. 1.0 -#[derive(Debug, Clone, PartialEq)] -pub enum FnArg<'input> { - /// e.g. "Skyrim.esm" | 0x007 - PluginValue { - /// e.g. "Skyrim.esm" - plugin_name: &'input str, - /// e.g. 1 - form_id: NumberLiteral, - }, - /// Just number. (e.g. 1) - Number(NumberLiteral), -} - -impl fmt::Display for FnArg<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - FnArg::PluginValue { - plugin_name, - form_id, - } => write!(f, "\"{plugin_name}\" | {form_id}"), - FnArg::Number(num) => write!(f, "{num}"), - } - } -} - -/// Hex | Decimal | Float -#[derive(Clone, PartialEq)] -pub enum NumberLiteral { - /// e.g. 0x007 - Hex(usize), - /// e.g. 1 - Decimal(isize), - /// e.g. 1.0 - Float(f32), -} - -// Hex debugging display is displayed in hexadecimal notation because it is difficult to understand if it is in decimal. -impl fmt::Debug for NumberLiteral { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Hex(arg0) => f.debug_tuple("Hex").field(&format!("{arg0:#x}")).finish(), - Self::Decimal(arg0) => f.debug_tuple("Decimal").field(arg0).finish(), - Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(), - } - } -} - -impl fmt::Display for NumberLiteral { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Hex(hex) => write!(f, "0x{hex:x}"), - Self::Decimal(decimal) => write!(f, "{decimal}"), - Self::Float(float) => write!(f, "{float}"), - } - } -} - -/// DAR One line representation -#[derive(Debug, Clone, PartialEq)] -pub struct Expression<'input> { - /// not condition - pub negated: bool, - /// function name == condition name - pub fn_name: &'input str, - /// arguments - pub args: Vec>, -} - -/// AND | OR -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Operator { - /// AND - And, - /// OR - Or, -} - -/// Represents a high-level condition, which can be an AND combination, OR combination, or a leaf expression. -#[derive(Debug, Clone, PartialEq)] -pub enum Condition<'input> { - /// Represents an AND combination of multiple conditions. - And(Vec>), - /// Represents an OR combination of multiple conditions. - Or(Vec>), - /// Represents a leaf expression within the condition hierarchy. - Exp(Expression<'input>), -} - -impl<'input> Condition<'input> { - /// push to inner vec - /// - /// # panics - /// If push to [`Self::Exp`] - fn push(&mut self, expression: Self) { - match self { - Condition::And(inner) | Condition::Or(inner) => inner.push(expression), - Condition::Exp(_) => { - #[cfg(feature = "tracing")] - tracing::error!("Expression cannot push"); - } - } - } -} - -/// Parsing target. -type Stream<'i> = &'i str; - -/// A type alias to allow modification of error types. -type Error<'i> = winnow::error::ContextError; - -/// Parse DAR syntax. -pub fn parse_dar_syntax(input: Stream<'_>) -> Result, ReadableError> { - let syntax = input; - parse_condition:: - .parse(syntax) - .map_err(|error| ReadableError::from_parse(error, input)) -} - -/// Parses a string with surrounding whitespace(0 or more times) -fn delimited_with_multispace0<'i, E>(s: &'static str) -> impl Parser, &'i str, E> -where - E: ParserError> + AddContext, StrContext>, -{ - delimited(multispace0, s, multispace0) - .context(StrContext::Expected(StrContextValue::StringLiteral(s))) -} - -/// single or double quote string -fn string<'i, E>(input: &mut Stream<'i>) -> PResult<&'i str, E> -where - E: ParserError> + AddContext, StrContext>, -{ - let single_quote = take_escaped( - take_while(1.., |c| c != '\'' && c != '\\'), - '\\', - one_of(['\\', '\'']), - ); - - let double_quote = take_escaped( - take_while(1.., |c| c != '\"' && c != '\\'), - '\\', - one_of(['\\', '"']), - ); - - alt(( - delimited('\"', single_quote, '\"'), - delimited('"', double_quote, '"'), - )) - .context(Label("String")) - .context(Expected(StrContextValue::Description( - r#"String: e.g. `"Skyrim.esm"`"#, - ))) - .parse_next(input) -} - -/// Replace a prefixed radix number such as `0x` with Replace with hexadecimal number without prefix. -fn radix_digits<'i, E>(input: &mut Stream<'i>) -> PResult -where - E: ParserError> - + AddContext, StrContext> - + FromExternalError, ParseIntError>, -{ - dispatch!(take(2_usize); - "0b" | "0B" => digit1.try_map(|s| usize::from_str_radix(s, 2)) - .context(StrContext::Label("digit")).context(StrContext::Expected(StrContextValue::Description("binary"))), - "0o" | "0O" => oct_digit1.try_map(|s| usize::from_str_radix(s, 8)) - .context(StrContext::Label("digit")).context(StrContext::Expected(StrContextValue::Description("octal"))), - "0d" | "0D" => digit1.try_map(|s: &str| s.parse::()) - .context(StrContext::Label("digit")).context(StrContext::Expected(StrContextValue::Description("decimal"))), - "0x" | "0X" => hex_digit1.try_map(|s|usize::from_str_radix(s, 16)) - .context(StrContext::Label("digit")).context(StrContext::Expected(StrContextValue::Description("hexadecimal"))), - _ => fail.context(StrContext::Label("radix prefix")) - .context(StrContext::Expected(StrContextValue::StringLiteral("0b"))) - .context(StrContext::Expected(StrContextValue::StringLiteral("0o"))) - .context(StrContext::Expected(StrContextValue::StringLiteral("0d"))) - .context(StrContext::Expected(StrContextValue::StringLiteral("0x"))), - ) - .map(NumberLiteral::Hex) - .parse_next(input) -} - -/// Parse a number(e.g. "0x123", "123", "12.3") -fn number<'i, E>(input: &mut Stream<'i>) -> PResult -where - E: ParserError> - + AddContext, StrContext> - + FromExternalError, ParseIntError>, -{ - alt(( - radix_digits.context(Label("number")), - float.map(NumberLiteral::Float).context(Label("number")), - dec_int.map(NumberLiteral::Decimal).context(Label("number")), - // At this point, if the string `Hi`, etc. is received, the following error report is made. - fail.context(Label("number")) - .context(Expected(StrContextValue::Description( - "radix: e.g. `0x007`", - ))) - .context(Expected(StrContextValue::Description("float: e.g. `33.0`"))) - .context(Expected(StrContextValue::Description("decimal: e.g. `10`"))), - )) - .parse_next(input) -} - -/// Parse plugin value(e.g. `"Skyrim.esm" | 0x007`) -fn parse_plugin<'i, E>(input: &mut Stream<'i>) -> PResult, E> -where - E: ParserError> - + AddContext, StrContext> - + FromExternalError, ParseIntError>, -{ - separated_pair( - delimited(multispace0, string, multispace0), - "|", - delimited(multispace0, number, multispace0), - ) - .map(|(plugin_name, form_id)| FnArg::PluginValue { - plugin_name, - form_id, - }) - .context(Label("Plugin")) - .context(Expected(StrContextValue::Description( - r#"Plugin: e.g. `"Skyrim.esm" | 0x007`"#, - ))) - .parse_next(input) -} - -/// Parse function arguments. -fn parse_argument<'i, E>(input: &mut Stream<'i>) -> PResult, E> -where - E: ParserError> - + AddContext, StrContext> - + FromExternalError, ParseIntError>, -{ - alt(( - parse_plugin, - number.map(FnArg::Number), - fail.context(Label("function argument")) - .context(Expected(StrContextValue::Description("plugin"))) - .context(Expected(StrContextValue::StringLiteral("number"))), - )) - .parse_next(input) -} - -/// Parse identifier -fn ident<'i, E>(input: &mut Stream<'i>) -> PResult<&'i str, E> -where - E: ParserError> + AddContext, StrContext>, -{ - take_while(1.., |c: char| c.is_alphanumeric() || c == '_') - .context(Label("Identifier")) - .context(Expected(StrContextValue::Description( - "Identifier(e.g. `IsActorBase`)", - ))) - .parse_next(input) -} - -/// Parse function call(with arguments) -/// -/// # Expected Syntax Examples -/// ```txt -/// ; Pattern1 -/// IsActorBase("Skyrim.esm" | 0x00000007) -/// -/// ; Pattern2 -/// IsActorValueEqualTo(0x00000007, 30) -/// ``` -fn fn_call<'i, E>(input: &mut Stream<'i>) -> PResult<(&'i str, Vec>), E> -where - E: ParserError> - + AddContext, StrContext> - + FromExternalError, ParseIntError>, -{ - seq!( - ident, - opt(delimited( - delimited_with_multispace0("("), - separated( - 0.., - delimited(multispace0, parse_argument, multispace0).context(Label("FnArg")), - "," - ), - delimited_with_multispace0(")"), - )) - .map(|args| args.unwrap_or_default()) - ) - .context(Label("Function call")) - .parse_next(input) -} - -/// - Expect an AND or OR string. -/// - After AND or OR comes Expression with a line break in between, so the line break is also checked. -fn parse_operator<'i, E>(input: &mut Stream<'i>) -> PResult -where - E: ParserError> + AddContext, StrContext>, -{ - preceded( - multispace0, - alt(( - Caseless("AND").value(Operator::And), - Caseless("OR").value(Operator::Or), - )), - ) - .context(Label("Operator")) - .context(Expected(StrContextValue::StringLiteral("AND"))) - .context(Expected(StrContextValue::StringLiteral("OR"))) - .parse_next(input) -} - -/// Parse one line DAR Syntax -/// # Expected Syntax examples -/// ```txt -/// NOT IsInCombat() -/// ``` -fn parse_expression<'i, E>(input: &mut Stream<'i>) -> PResult, E> -where - E: ParserError> - + AddContext, StrContext> - + FromExternalError, ParseIntError>, -{ - seq!( - _: multispace0, - opt(Caseless("NOT")).context(Expected(StrContextValue::StringLiteral("NOT"))) - .map(|not| not.is_some()), - _: multispace0, - fn_call, - _: multispace0, - ) - .map(|(negated, (fn_name, args))| Expression { - negated, - fn_name, - args, - }) - .parse_next(input) -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/// Parse DAR syntax -fn parse_condition<'i, E>(input: &mut Stream<'i>) -> PResult, E> -where - E: ParserError> - + AddContext, StrContext> - + FromExternalError, ParseIntError>, -{ - let mut top_conditions = Condition::And(Vec::new()); - let mut or_vec = Vec::new(); - let mut is_in_or_stmt = false; - - loop { - #[cfg(feature = "tracing")] - tracing::trace!("top_conditions = {top_conditions:#?},\nor_vec = {or_vec:#?}"); - - let _ = multispace0(input)?; - // Dealing with cases where nothing is written in _condition.txt - if input.is_empty() { - break; - } - - let _ = line_comments0(input)?; - let (expr, operator) = seq!(parse_expression, opt(parse_operator)).parse_next(input)?; - let _ = line_comments0(input)?; - let _ = multispace0(input)?; - - #[cfg(feature = "tracing")] - tracing::trace!("expr: {expr:#?}"); - if let Some(operator) = operator { - match operator { - Operator::And => { - if is_in_or_stmt { - or_vec.push(Condition::Exp(expr)); - top_conditions.push(Condition::Or(mem::take(&mut or_vec))); - is_in_or_stmt = false; - } else { - top_conditions.push(Condition::Exp(expr)); - } - } - Operator::Or => { - or_vec.push(Condition::Exp(expr)); - is_in_or_stmt = true; - } - }; - - // To support tailing `OR` or `AND` statement. - if input.is_empty() { - if is_in_or_stmt { - top_conditions.push(Condition::Or(mem::take(&mut or_vec))); - } - break; - } - } else { - match is_in_or_stmt { - true => { - or_vec.push(Condition::Exp(expr)); - top_conditions.push(Condition::Or(or_vec)); - } - false => top_conditions.push(Condition::Exp(expr)), - } - - let _ = eof - .context(Label("End of file")) - .context(Expected(StrContextValue::Description("end of file"))) - .context(Expected(StrContextValue::Description( - "Tailing op: `OR` or `AND`", - ))) - .context(Expected(StrContextValue::Description( - "Conditional statement(if it has op): e.g. `NOT IsInCombat() AND`", - ))) - .parse_next(input)?; - break; - } - } - - Ok(top_conditions) -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/// Comment starting with ';' until newline -fn line_comment<'i, E>(input: &mut Stream<'i>) -> PResult, E> -where - E: ParserError> + AddContext, StrContext>, -{ - delimited(multispace0, preceded(';', till_line_ending), multispace0) - .context(Label("Comment")) - .context(Expected(StrContextValue::Description( - "Comment(e.g. `; Any String`)", - ))) - .parse_next(input) -} - -/// Comments starting with ';' until newline. 0 or more. -fn line_comments0<'i, E>(input: &mut Stream<'i>) -> PResult>, E> -where - E: ParserError> + AddContext, StrContext>, -{ - repeat(0.., line_comment).parse_next(input) -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - macro_rules! parse_assert { - ($parser:ident($input:tt), $expected:expr) => { - match $parser::>.parse($input) { - Ok(actual) => assert_eq!(actual, $expected), - Err(err) => panic!("{}", ReadableError::from_parse(err, $input)), - } - }; - } - - #[test] - fn should_parse_radix_number() { - parse_assert!(radix_digits("0b1010"), NumberLiteral::Hex(10)); - parse_assert!(radix_digits("0B1010"), NumberLiteral::Hex(10)); - parse_assert!(radix_digits("0o37"), NumberLiteral::Hex(31)); - parse_assert!(radix_digits("0O37"), NumberLiteral::Hex(31)); - parse_assert!(radix_digits("0x00000007"), NumberLiteral::Hex(7)); - parse_assert!(radix_digits("0X1A"), NumberLiteral::Hex(26)); - } - - #[test] - fn should_error_radix_number() { - assert!(radix_digits::>.parse("0z123").is_err()); - assert!(radix_digits::>.parse("0x").is_err()); - } - - #[test] - fn should_parse_number() { - parse_assert!(number("33"), NumberLiteral::Decimal(33)); - parse_assert!(number("33.0"), NumberLiteral::Float(33.0)); - parse_assert!(number("0x00000007"), NumberLiteral::Hex(0x00000007)); - } - - #[test] - fn should_parse_string() { - parse_assert!(string(r#""0""#), "0"); - parse_assert!(string(r#""with\"escaped""#), "with\\\"escaped"); - } - - #[test] - fn should_parse_ident() { - parse_assert!(ident("IsActorBase"), "IsActorBase"); - } - - #[test] - fn should_parse_fn_call() { - let input = r#"IsActorValueLessThan(30, 60)"#; - let expected = ( - "IsActorValueLessThan", - vec![ - FnArg::Number(NumberLiteral::Decimal(30)), - FnArg::Number(NumberLiteral::Decimal(60)), - ], - ); - - parse_assert!(fn_call(input), expected); - } - - #[test] - fn should_parse_comment() { - let input = r#" - ; comment -"#; - parse_assert!(line_comment(input), " comment"); - } - - #[test] - #[cfg_attr(feature = "tracing", quick_tracing::init)] - fn should_parse_conditions() { - let input = r#" -IsActorBase("Skyrim.esm" | 0X000007) AND -NOT IsInCombat() AND -NOT IsActorValueLessThan(30, 60) - "#; - - let expected = Condition::And(vec![ - Condition::Exp(Expression { - fn_name: "IsActorBase", - args: vec![FnArg::PluginValue { - plugin_name: "Skyrim.esm", - form_id: NumberLiteral::Hex(0x7), - }], - negated: false, - }), - Condition::Exp(Expression { - fn_name: "IsInCombat", - args: vec![], - negated: true, - }), - Condition::Exp(Expression { - negated: true, - fn_name: "IsActorValueLessThan", - args: vec![ - FnArg::Number(NumberLiteral::Decimal(30)), - FnArg::Number(NumberLiteral::Decimal(60)), - ], - }), - ]); - - match parse_dar_syntax(input) { - Ok(actual) => assert_eq!(actual, expected), - Err(err) => panic!("{err}"), - } - } - - #[test] - #[cfg_attr(feature = "tracing", quick_tracing::init)] - fn should_parse_conditions_with_comments() { - let input = r#" - ; This is start of line comment. - - IsActorBase("Skyrim.esm" | 0x00BCDEF7) Or - ; Parse test only indent function call. - - noT IsPlayerTeammate aNd - ; This is a line comment. - ; This is a line comment. - - IsEquippedRightType(3) OR - - ; This is a line comment. - IsEquippedRightType(4) - - ; This is end of line comment. - ; This is end of line comment. - -"#; - - let actor = Expression { - negated: false, - fn_name: "IsActorBase", - args: vec![FnArg::PluginValue { - plugin_name: "Skyrim.esm", - form_id: NumberLiteral::Hex(0x00BC_DEF7), - }], - }; - let player = Expression { - negated: true, - fn_name: "IsPlayerTeammate", - args: vec![], - }; - let equip_r3 = Expression { - negated: false, - fn_name: "IsEquippedRightType", - args: vec![FnArg::Number(NumberLiteral::Decimal(3))], - }; - let equip_r4 = Expression { - negated: false, - fn_name: "IsEquippedRightType", - args: vec![FnArg::Number(NumberLiteral::Decimal(4))], - }; - - let expected = Condition::And(vec![ - Condition::Or(vec![Condition::Exp(actor), Condition::Exp(player)]), - Condition::Or(vec![Condition::Exp(equip_r3), Condition::Exp(equip_r4)]), - ]); - - match parse_dar_syntax(input) { - Ok(actual) => assert_eq!(actual, expected), - Err(err) => panic!("{err}"), - } - } - - #[test] - fn should_parse_with_space() { - let input = r#" IsActorBase ( "Skyrim.esm"|0x00000007 ) "#; - let expected = Condition::And(vec![Condition::Exp(Expression { - negated: false, - fn_name: "IsActorBase", - args: vec![FnArg::PluginValue { - plugin_name: "Skyrim.esm", - form_id: NumberLiteral::Hex(0x0000_0007), - }], - })]); - - assert_eq!(parse_condition::.parse(input), Ok(expected)); - } - - #[test] - fn should_parse_tailing_or() { - let input = "NOT IsActorBase ( \"Skyrim.esm\" | 0x00000007 )OR"; - let expected = Condition::And(vec![Condition::Or(vec![Condition::Exp(Expression { - negated: true, - fn_name: "IsActorBase", - args: vec![FnArg::PluginValue { - plugin_name: "Skyrim.esm", - form_id: NumberLiteral::Hex(0x0000_0007), - }], - })])]); - - assert_eq!(parse_condition::.parse(input), Ok(expected)); - } - - #[test] - fn should_parse_tailing_and() { - let input = "NOT IsActorBase ( \"Skyrim.esm\" | 0x00000007 )AND"; - let expected = Condition::And(vec![Condition::Exp(Expression { - negated: true, - fn_name: "IsActorBase", - args: vec![FnArg::PluginValue { - plugin_name: "Skyrim.esm", - form_id: NumberLiteral::Hex(0x0000_0007), - }], - })]); - - assert_eq!(parse_condition::.parse(input), Ok(expected)); - } -} diff --git a/core/src/error.rs b/core/src/error.rs index 6101de8..7e35bc0 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -1,96 +1,118 @@ //! Error types for Converter -/// It is used to represent different types of errors that can occur during the conversion process. -/// Each variant of the enum represents a specific type of error, -/// and it can contain additional data associated with the error, -/// such as error messages or other relevant information. -#[derive(Debug, thiserror::Error)] -pub enum ConvertError { - /// Failed to write section config target. - #[error( - "Path was interpreted as the path to ActorBase, but the ID directory is missing. expected: [..]/DynamicAnimationReplacer/{{ESP name}}/{{ID Number}}, actual: {0}" - )] - MissingBaseId(String), +use snafu::Snafu; +use std::path::PathBuf; - /// Failed to write section config target. - #[error("Failed to write section config target: {0}")] - FailedWriteSectionConfig(String), +/// Represents different types of errors that can occur during the conversion process. +#[derive(Debug, Snafu)] +pub enum ConvertError { + /// Path interpreted as the path to `ActorBase`, but the ID directory is missing. + #[snafu(display("Path was interpreted as the path to ActorBase, but the ID directory is missing. expected: [..]/DynamicAnimationReplacer/{{ESP name}}/{{ID Number}}, actual: {}", path.display()))] + MissingBaseId { + /// path + path: PathBuf, + }, /// Never converted. - #[error("Never converted.")] NeverConverted, /// No such paths exist. - #[error("No such paths exist: \"{0}\"")] - NonExistPath(String), + #[snafu(display("No such paths exist: \"{}\"", path.display()))] + NonExistPath { + /// path + path: PathBuf, + }, /// Nothing in the specified path. - #[error("Nothing in the specified path")] NotFoundEntry, + /// Could not find files with ".mohidden" extension. - #[error("Could not find files with \".mohidden\" extension")] NotFoundUnhideTarget, + /// Not found `DynamicAnimationReplacer` directory. - #[error("Not found \"DynamicAnimationReplacer\" directory")] NotFoundDarDir, + /// Not found file name. - #[error("Not found file name")] NotFoundFileName, + /// Not found `OpenAnimationReplacer` directory. - #[error("Not found \"OpenAnimationReplacer\" directory")] NotFoundOarDir, - /// Not found DAR priority(Number) directory. - #[error("Not found DAR priority(Number) directory")] - NotFoundPriorityDir, - /// DAR syntax error. - #[error("[DAR Syntax Error]\n{0}")] - InvalidDarSyntax(String), + /// Not found DAR priority (Number) directory. + NotFoundPriorityDir, - /// This is not valid utf8. - #[error("This is not valid utf8")] + /// This is not valid UTF-8. InvalidUtf8, - /// Condition error. - #[error(transparent)] - ConditionError(#[from] crate::conditions::ConditionError), - - /// Parse error. - #[error(transparent)] - ParseError(#[from] crate::condition_parser::ParseError), - - /// Convert json error. - #[error(transparent)] - JsonError(#[from] serde_json::Error), + /// DAR syntax error with path. + #[snafu(display("[DAR Syntax Error] {}\n{}", path.display(), source))] + InvalidDarSyntax { + /// path + path: PathBuf, + /// transparent + source: crate::dar_syntax::errors::DarError, + }, + + /// OAR condition error + #[snafu(transparent)] + ConditionError { + /// transparent + source: crate::conditions::ConditionError, + }, + + /// Parse error + #[snafu(transparent)] + ParseError { + /// transparent + source: crate::condition_parser::ParseError, + }, + + /// JSON conversion error. + #[snafu(transparent)] + JsonError { + /// transparent + source: serde_json::Error, + }, /// Parse integer error. - #[error(transparent)] - ParseIntError(#[from] core::num::ParseIntError), - - /// Represents all other cases of `std::io::Error`. - #[error(transparent)] - IOError(#[from] std::io::Error), + #[snafu(transparent)] + ParseIntError { + /// transparent + source: core::num::ParseIntError, + }, + + /// I/O error. + #[snafu(transparent)] + IOError { + /// transparent + source: std::io::Error, + }, /// Async walkdir error. - #[error(transparent)] - AsyncWalkDirError(#[from] async_walkdir::Error), + #[snafu(transparent)] + AsyncWalkDirError { + /// transparent + source: async_walkdir::Error, + }, /// Thread join error. - #[error(transparent)] - JoinError(#[from] tokio::task::JoinError), + #[snafu(transparent)] + JoinError { + /// transparent + source: tokio::task::JoinError, + }, } -//? Implemented to facilitate testing with the `assert_eq!` macro. +// Implemented to facilitate testing with the `assert_eq!` macro. impl PartialEq for ConvertError { fn eq(&self, other: &Self) -> bool { match (self, other) { - (Self::FailedWriteSectionConfig(l0), Self::FailedWriteSectionConfig(r0)) - | (Self::InvalidDarSyntax(l0), Self::InvalidDarSyntax(r0)) => l0 == r0, - (Self::ConditionError(l0), Self::ConditionError(r0)) => l0 == r0, - (Self::ParseError(l0), Self::ParseError(r0)) => l0 == r0, - (Self::JsonError(l0), Self::JsonError(r0)) => l0.to_string() == r0.to_string(), - (Self::ParseIntError(l0), Self::ParseIntError(r0)) => l0 == r0, - (Self::IOError(l0), Self::IOError(r0)) => l0.kind() == r0.kind(), + (Self::ParseError { source: l0 }, Self::ParseError { source: r0 }) => l0 == r0, + (Self::JsonError { source: l0 }, Self::JsonError { source: r0 }) => { + l0.to_string() == r0.to_string() + } + (Self::ParseIntError { source: l0 }, Self::ParseIntError { source: r0 }) => l0 == r0, + (Self::IOError { source: l0 }, Self::IOError { source: r0 }) => l0.kind() == r0.kind(), _ => core::mem::discriminant(self) == core::mem::discriminant(other), } } diff --git a/core/src/fs/converter/common.rs b/core/src/fs/converter/common.rs index 0613cb0..906bc92 100644 --- a/core/src/fs/converter/common.rs +++ b/core/src/fs/converter/common.rs @@ -55,7 +55,7 @@ where }; // character, falmer, etc. - let actor_name = actor_name.as_deref().unwrap_or({ + let actor_name = actor_name.as_deref().unwrap_or_else(|| { #[cfg(feature = "tracing")] tracing::warn!( "actor_name could not be inferred from the dir name. Use the default value \"character\"." @@ -131,12 +131,12 @@ where #[cfg(feature = "tracing")] tracing::debug!("This path is ActorBase: {path:?}"); - let esp_dir = esp_dir - .as_ref() - .ok_or(ConvertError::MissingBaseId(path.display().to_string()))?; - let base_id = base_id - .as_ref() - .ok_or(ConvertError::MissingBaseId(path.display().to_string()))?; + let esp_dir = esp_dir.as_ref().ok_or(ConvertError::MissingBaseId { + path: path.to_path_buf(), + })?; + let base_id = base_id.as_ref().ok_or(ConvertError::MissingBaseId { + path: path.to_path_buf(), + })?; let content = format!("IsActorBase ( \"{esp_dir}\" | 0x{base_id} )"); #[cfg(feature = "tracing")] @@ -173,7 +173,7 @@ where if file_name == "_conditions.txt" { let content = fs::read_to_string(path).await?; #[cfg(feature = "tracing")] - tracing::debug!("{path:?} Content:\n{}", content); + tracing::debug!("{} Content:\n{content}", path.display()); let config_json = ConditionsConfig { name: section_name.into(), @@ -252,5 +252,5 @@ pub(super) const fn handle_conversion_results(is_converted_once: bool) -> Result pub(super) fn is_contain_dar(path: impl AsRef) -> Option { path.as_ref() .iter() - .position(|os_str| os_str == std::ffi::OsStr::new("DynamicAnimationReplacer")) + .position(|os_str| os_str.eq_ignore_ascii_case("DynamicAnimationReplacer")) } diff --git a/core/src/fs/converter/mod.rs b/core/src/fs/converter/mod.rs index cbc2a54..d188838 100644 --- a/core/src/fs/converter/mod.rs +++ b/core/src/fs/converter/mod.rs @@ -64,9 +64,9 @@ pub async fn convert_dar_to_oar( ) -> Result<()> { let dar_dir = std::path::Path::new(&options.dar_dir); if !dar_dir.exists() { - return Err(crate::error::ConvertError::NonExistPath(format!( - "{dar_dir:?}" - ))); + return Err(crate::error::ConvertError::NonExistPath { + path: dar_dir.to_path_buf(), + })?; }; match options.run_parallel { diff --git a/core/src/fs/mapping_table.rs b/core/src/fs/mapping_table.rs index b10d682..d9abccf 100644 --- a/core/src/fs/mapping_table.rs +++ b/core/src/fs/mapping_table.rs @@ -26,7 +26,9 @@ pub async fn read_mapping_table( ) -> Result> { let table_path = table_path.as_ref(); if !table_path.exists() { - return Err(ConvertError::NonExistPath(format!("{table_path:?}"))); + return Err(ConvertError::NonExistPath { + path: table_path.to_path_buf(), + }); }; let contents = read_to_string(table_path).await?; diff --git a/core/src/fs/section_writer.rs b/core/src/fs/section_writer.rs index 4c3a563..07b2b47 100644 --- a/core/src/fs/section_writer.rs +++ b/core/src/fs/section_writer.rs @@ -16,7 +16,10 @@ where } /// Write config.json for a dir with each motion file with priority. -pub(crate) async fn write_section_config

(oar_dir: P, config_json: ConditionsConfig) -> Result<()> +pub(crate) async fn write_section_config

( + oar_dir: P, + config_json: ConditionsConfig<'_>, +) -> Result<()> where P: AsRef, { diff --git a/core/src/values/direction_value.rs b/core/src/values/direction_value.rs index 149459a..880b538 100644 --- a/core/src/values/direction_value.rs +++ b/core/src/values/direction_value.rs @@ -3,6 +3,8 @@ use serde::de::Unexpected; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_untagged::UntaggedEnumVisitor; +use super::ValueError; + /// Actor's Direction #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct DirectionValue { @@ -27,7 +29,7 @@ pub enum Direction { } impl TryFrom for Direction { - type Error = &'static str; + type Error = ValueError; fn try_from(value: f64) -> Result { Ok(match value { @@ -36,7 +38,12 @@ impl TryFrom for Direction { x if (2.0..3.0).contains(&x) => Self::Right, x if (3.0..4.0).contains(&x) => Self::Back, x if (4.0..5.0).contains(&x) => Self::Left, - _ => return Err("Invalid value for Direction"), + invalid_value => { + return Err(ValueError::CastError { + expected: "1.0..=5.0".into(), + actual: invalid_value.to_string(), + }) + } }) } } diff --git a/core/src/values/errors.rs b/core/src/values/errors.rs new file mode 100644 index 0000000..cc4a1aa --- /dev/null +++ b/core/src/values/errors.rs @@ -0,0 +1,13 @@ +//! Represents an error that can occur while working with value parse. + +/// Represents an error that can occur while working with value parse. +#[derive(Debug, Clone, snafu::Snafu, PartialEq, Eq)] +pub enum ValueError { + /// Expected {expected}, but got {actual} + CastError { + /// Expected value + expected: String, + /// Actual value + actual: String, + }, +} diff --git a/core/src/values/form_value.rs b/core/src/values/form_value.rs index 56ff5d4..db0e47d 100644 --- a/core/src/values/form_value.rs +++ b/core/src/values/form_value.rs @@ -4,13 +4,13 @@ use serde::{Deserialize, Serialize}; /// Wrapper for wrapping pluginValue with a key called `form` #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct FormValue { +pub struct FormValue<'a> { /// A combination of the plugin name and the ID in it. - pub form: PluginValue, + pub form: PluginValue<'a>, } -impl From for FormValue { - fn from(value: PluginValue) -> Self { +impl<'a> From> for FormValue<'a> { + fn from(value: PluginValue<'a>) -> Self { Self { form: value } } } diff --git a/core/src/values/graph_value.rs b/core/src/values/graph_value.rs index 04246e9..a3ab1e0 100644 --- a/core/src/values/graph_value.rs +++ b/core/src/values/graph_value.rs @@ -1,14 +1,15 @@ //! Pair str & Int | Float | Bool use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Pair str & Int | Float | Bool #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct GraphValue { +pub struct GraphValue<'a> { /// string /// /// TODO: Unknown variable #[serde(rename = "graphVariable")] - pub graph_variable: String, + pub graph_variable: Cow<'a, str>, /// Float | Int | Bool #[serde(rename = "graphVariableType")] pub graph_variable_type: GraphVariableType, diff --git a/core/src/values/keyword_value.rs b/core/src/values/keyword_value.rs index b6ac09d..ed6e1e0 100644 --- a/core/src/values/keyword_value.rs +++ b/core/src/values/keyword_value.rs @@ -1,60 +1,24 @@ //! Trigger keywords use super::{FormValue, LiteralValue}; use serde::{Deserialize, Serialize}; -use serde_json::Value; +// NOTE: Changing the order of enums will cause Deserialize to error. /// Trigger keywords -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(untagged)] -pub enum Keyword { - /// Single numeric type - Literal(LiteralValue), +pub enum Keyword<'a> { /// plugin value - Form(FormValue), + Form(FormValue<'a>), + /// Single numeric type + Literal(LiteralValue<'a>), } -impl Default for Keyword { +impl Default for Keyword<'_> { fn default() -> Self { Self::Literal(LiteralValue::default()) } } -impl<'de> Deserialize<'de> for Keyword { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value: Value = Deserialize::deserialize(deserializer)?; - - if let Value::Object(map) = &value { - if map.contains_key("editorID") { - // If the "editorID" field is present, assume it's a Literal - let keyword_value: LiteralValue = match serde_json::from_value(value) { - Ok(keyword) => keyword, - Err(err) => return Err(serde::de::Error::custom(err)), - }; - Ok(Self::Literal(keyword_value)) - } else if map.contains_key("form") { - // If both "pluginName" and "formID" fields are present, assume it's a Form - let form_value: FormValue = match serde_json::from_value(value) { - Ok(form) => form, - Err(err) => return Err(serde::de::Error::custom(err)), - }; - Ok(Self::Form(form_value)) - } else { - Err(serde::de::Error::custom( - "Unable to determine Keyword variant", - )) - } - } else { - Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Other("Expected an object"), - &"a map", - )) - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -80,7 +44,7 @@ mod tests { }"#; let deserialized: Keyword = serde_json::from_str(input)?; let expected = Keyword::Literal(LiteralValue { - editor_id: "SomeKeyword".to_string(), + editor_id: "SomeKeyword".into(), }); assert_eq!(deserialized, expected); diff --git a/core/src/values/literal_value.rs b/core/src/values/literal_value.rs index 91506ca..a542694 100644 --- a/core/src/values/literal_value.rs +++ b/core/src/values/literal_value.rs @@ -1,11 +1,12 @@ //! String Literal use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// Wrapper `editor_id` #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct LiteralValue { +pub struct LiteralValue<'a> { /// Editor ID #[serde(rename = "editorID")] #[serde(default)] - pub editor_id: String, + pub editor_id: Cow<'a, str>, } diff --git a/core/src/values/mod.rs b/core/src/values/mod.rs index 3664751..0a5cb3d 100644 --- a/core/src/values/mod.rs +++ b/core/src/values/mod.rs @@ -2,6 +2,7 @@ mod actor_value; mod comparison; mod direction_value; +mod errors; mod form_value; mod graph_value; mod keyword_value; @@ -16,6 +17,7 @@ mod type_value; pub use self::actor_value::{ActorValue, ActorValueType}; pub use self::comparison::Cmp; pub use self::direction_value::{Direction, DirectionValue}; +pub use self::errors::ValueError; pub use self::form_value::FormValue; #[allow(unused)] pub use self::graph_value::{GraphValue, GraphVariableType}; @@ -31,17 +33,18 @@ pub use self::type_value::{TypeValue, WeaponType}; use serde::{Deserialize, Serialize}; /// DAR variable set +#[allow(clippy::enum_variant_names)] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(untagged)] -pub(crate) enum ValueSet { +pub enum Value<'a> { /// Person and its internal value ActorValue(ActorValue), /// Keyword ID - KeywordValue(LiteralValue), + KeywordValue(LiteralValue<'a>), /// Just f32 value NumericValue(StaticValue), /// Pair plugin name & ID - PluginValue(PluginValue), + PluginValue(PluginValue<'a>), /// A value with a range, used for randomization. RandomValue(RandomValue), /// Weapon type diff --git a/core/src/values/numeric_value.rs b/core/src/values/numeric_value.rs index 9553408..a08387f 100644 --- a/core/src/values/numeric_value.rs +++ b/core/src/values/numeric_value.rs @@ -1,72 +1,30 @@ //! A set of f32 | `PluginValue` | Form | Pair str, number use super::{actor_value::ActorValue, static_value::StaticValue, FormValue, GraphValue}; use serde::{Deserialize, Serialize}; -use serde_json::Value; /// f32 | `PluginValue` | Form | Pair str, number /// /// In fact, it can be variously accepted rather than Numeric, /// but the GUI description of OAR says Numeric Value, so we follow it. -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] -pub enum NumericValue { +pub enum NumericValue<'a> { /// Just f32 value StaticValue(StaticValue), /// Pair plugin name & ID - GlobalVariable(FormValue), + GlobalVariable(FormValue<'a>), /// Person and its internal value ActorValue(ActorValue), /// Pair str & Int | Float | Bool - GraphVariable(GraphValue), + GraphVariable(GraphValue<'a>), } -impl Default for NumericValue { +impl Default for NumericValue<'_> { fn default() -> Self { Self::StaticValue(StaticValue::default()) } } -impl<'de> Deserialize<'de> for NumericValue { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - /// Macro to change from [`serde_json`] error to [`serde`] custom error - macro_rules! deserialize_json { - ($value:expr) => { - serde_json::from_value($value).map_err(|e| serde::de::Error::custom(e)) - }; - } - - let value: Value = Deserialize::deserialize(deserializer)?; - - if let Value::Object(map) = &value { - if map.contains_key("value") { - // If the "value" field is present, assume it's a StaticValue - let static_value: StaticValue = deserialize_json!(value)?; - Ok(Self::StaticValue(static_value)) - } else if map.contains_key("form") { - let global_variable = deserialize_json!(value)?; - Ok(Self::GlobalVariable(global_variable)) - } else if map.contains_key("actorValue") { - let actor_value: ActorValue = deserialize_json!(value)?; - Ok(Self::ActorValue(actor_value)) - } else if map.contains_key("graphValue") { - Ok(Self::GraphVariable(deserialize_json!(value)?)) - } else { - Err(serde::de::Error::custom( - "Unable to determine NumericValue variant", - )) - } - } else { - Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Other("Expected an object"), - &"a map", - )) - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/core/src/values/plugin_value.rs b/core/src/values/plugin_value.rs index a558ecf..dc359e6 100644 --- a/core/src/values/plugin_value.rs +++ b/core/src/values/plugin_value.rs @@ -1,15 +1,15 @@ //! A combination of the plugin name and the ID in it. use super::NumericLiteral; -use compact_str::CompactString; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; /// A combination of the plugin name and the ID in it. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct PluginValue { +pub struct PluginValue<'a> { /// e.g. `Skyrim.esm` #[serde(rename = "pluginName")] #[serde(default)] - pub plugin_name: String, + pub plugin_name: Cow<'a, str>, /// - OAR: Non prefix(0x) hexadecimal /// - DAR: Decimal or Hex /// @@ -26,17 +26,17 @@ pub struct PluginValue { /// - OAR can read hexes that are not filled with zeros. #[serde(rename = "formID")] #[serde(default)] - pub form_id: FormID, + pub form_id: FormID<'a>, } /// Non prefix(0x) hexadecimal ID #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct FormID(CompactString); +pub struct FormID<'a>(Cow<'a, str>); -impl From<&str> for FormID { +impl<'a> From<&'a str> for FormID<'a> { /// Clone into /// - NOTE: non cast to hex - fn from(value: &str) -> Self { + fn from(value: &'a str) -> Self { Self(value.into()) } } @@ -45,7 +45,7 @@ impl From<&str> for FormID { macro_rules! from { ($($_type:ident),+ $(,)?) => { $( - impl From<$_type> for FormID { + impl From<$_type> for FormID<'_> { fn from(value: $_type) -> Self { NumericLiteral::from(value).into() } @@ -56,12 +56,12 @@ macro_rules! from { from!(usize, isize, f32); -impl From for FormID { +impl From for FormID<'_> { fn from(value: NumericLiteral) -> Self { Self(match value { NumericLiteral::Hex(hex_value) => format!("{hex_value:x}").into(), NumericLiteral::Decimal(decimal_value) => match decimal_value == 0 { - true => CompactString::default(), + true => Default::default(), false => format!("{decimal_value:x}").into(), }, NumericLiteral::Float(float_value) => format!("{:x}", float_value as usize).into(), @@ -108,16 +108,4 @@ mod tests { assert_eq!(deserialized, expected); Ok(()) } - - #[test] - fn should_default_plugin_value() { - let default_plugin_value = PluginValue::default(); - - let expected = PluginValue { - plugin_name: String::new(), - form_id: "".into(), - }; - - assert_eq!(default_plugin_value, expected); - } } diff --git a/core/src/values/type_value.rs b/core/src/values/type_value.rs index c89490a..5ce4977 100644 --- a/core/src/values/type_value.rs +++ b/core/src/values/type_value.rs @@ -1,9 +1,9 @@ //! Wrapper for [`WeaponType`] +use super::NumericLiteral; +use crate::conditions::ConditionError; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_untagged::UntaggedEnumVisitor; -use super::NumericLiteral; - /// Wrapper for [`WeaponType`] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct TypeValue { @@ -58,7 +58,7 @@ pub enum WeaponType { } impl TryFrom for WeaponType { - type Error = &'static str; + type Error = ConditionError; fn try_from(value: i64) -> Result { Ok(match value { @@ -82,31 +82,18 @@ impl TryFrom for WeaponType { 16 => Self::RestorationSpell, 17 => Self::Scroll, 18 => Self::Torch, - _ => return Err("Invalid value for WeaponType"), + invalid => { + return Err(ConditionError::UnexpectedValue { + expected: "-1..18".into(), + actual: invalid.to_string(), + }) + } }) } } -impl TryFrom for WeaponType { - type Error = &'static str; - - fn try_from(value: NumericLiteral) -> Result { - match value { - NumericLiteral::Hex(num) => match num { - 1..=18 => Ok((num as i64).try_into()?), - _ => Err("Got hex, Out of range 1..=18"), - }, - NumericLiteral::Decimal(num) => match num { - -1..=18 => Ok((num as i64).try_into()?), - _ => Err("Got Decimal, Out of range -1..=18"), - }, - NumericLiteral::Float(num) => Ok(num.try_into()?), - } - } -} - impl TryFrom for WeaponType { - type Error = &'static str; + type Error = ConditionError; fn try_from(float: f32) -> Result { Ok(match float { @@ -130,7 +117,12 @@ impl TryFrom for WeaponType { x if (16.0..17.0).contains(&x) => Self::RestorationSpell, x if (17.0..18.0).contains(&x) => Self::Scroll, x if (18.0..19.0).contains(&x) => Self::Torch, - _ => return Err("Expected -1.0..=18.0"), + invalid => { + return Err(ConditionError::UnexpectedValue { + expected: "-1..18".into(), + actual: invalid.to_string(), + }) + } }) } } @@ -162,6 +154,18 @@ impl From for f64 { } } +impl TryFrom for WeaponType { + type Error = ConditionError; + + fn try_from(value: NumericLiteral) -> Result { + match value { + NumericLiteral::Hex(num) => (num as i64).try_into(), + NumericLiteral::Decimal(num) => (num as i64).try_into(), + NumericLiteral::Float(num) => num.try_into(), + } + } +} + impl Serialize for WeaponType { fn serialize(&self, serializer: S) -> Result where diff --git a/gui/frontend/src/components/organisms/ConvertForm/ConvertForm.tsx b/gui/frontend/src/components/organisms/ConvertForm/ConvertForm.tsx index 6fb2c1f..9aa3d9a 100644 --- a/gui/frontend/src/components/organisms/ConvertForm/ConvertForm.tsx +++ b/gui/frontend/src/components/organisms/ConvertForm/ConvertForm.tsx @@ -5,14 +5,13 @@ import { FormProvider, type SubmitHandler, useForm } from 'react-hook-form'; import { useTranslation } from '@/components/hooks/useTranslation'; import { ConvertNav, ConvertNavPadding } from '@/components/organisms/ConvertNav'; +import { parseDarPath } from '@/lib/path/parseDarPath'; import { STORAGE } from '@/lib/storage'; import { PRIVATE_CACHE_OBJ, PUB_CACHE_OBJ } from '@/lib/storage/cacheKeys'; import { convertDar2oar } from '@/services/api/convert'; import { progressListener } from '@/services/api/event'; import { LOG, type LogLevel } from '@/services/api/log'; -import { parseDarPath } from '../../../lib/path/parseDarPath'; - import { CheckboxField } from './CheckboxField'; import { InputModInfoField } from './InputModInfoField'; import { InputPathField } from './InputPathField'; @@ -120,6 +119,7 @@ export function ConvertForm() { {pathFields.map((props) => { let onChange: ComponentPropsWithRef['onChange'] | undefined; + let setPathHook: ((path: string) => void) | undefined; if (props.name === 'src') { onChange = (e) => { @@ -129,8 +129,15 @@ export function ConvertForm() { setValue('modName', parsedPath.modName ?? ''); } }; + setPathHook = (path: string) => { + if (getValues('inferPath')) { + const parsedPath = parseDarPath(path); + setValue('dst', parsedPath.oarRoot); + setValue('modName', parsedPath.modName ?? ''); + } + }; } - return ; + return ; })} diff --git a/gui/frontend/src/components/organisms/ConvertForm/InputPathField.tsx b/gui/frontend/src/components/organisms/ConvertForm/InputPathField.tsx index 7e4e555..d2b6201 100644 --- a/gui/frontend/src/components/organisms/ConvertForm/InputPathField.tsx +++ b/gui/frontend/src/components/organisms/ConvertForm/InputPathField.tsx @@ -15,9 +15,17 @@ type Props = { placeholder: string; helperText: string | ReactNode; onChange?: TextFieldProps['onChange']; + setPathHook?: (path: string) => void; }; -export const InputPathField = ({ name, label, placeholder, helperText, onChange: onChangeOuter }: Props) => { +export const InputPathField = ({ + name, + label, + placeholder, + helperText, + onChange: onChangeOuter, + setPathHook, +}: Props) => { const { control, getValues, setValue } = useFormContext(); const path = (() => { @@ -33,6 +41,7 @@ export const InputPathField = ({ name, label, placeholder, helperText, onChange: const handleSetPath = (path: string) => { setValue(name, path); setPathToStorage(name, path); + setPathHook?.(path); }; return ( diff --git a/gui/frontend/src/services/api/event.ts b/gui/frontend/src/services/api/event.ts index 23f743a..33da812 100644 --- a/gui/frontend/src/services/api/event.ts +++ b/gui/frontend/src/services/api/event.ts @@ -61,12 +61,14 @@ export async function progressListener( let maxNum = 0; let unlisten: (() => void) | null = null; const eventHandler: EventCallback = (event) => { + /** file count to % */ const toPercentage = (num: number) => (num * 100) / maxNum; if (maxNum === 0) { maxNum = event.payload.index; } else { - setProgress(toPercentage(event.payload.index)); + const percent = toPercentage(event.payload.index); + setProgress(percent); } };