diff options
-rw-r--r-- | .github/workflows/ci.yaml | 42 | ||||
-rw-r--r-- | .sqlx/query-32cb5438dd7289fd29e38dc545bf8ca74ef9cbcdc6f51642ef21db4444d1db15.json | 25 | ||||
-rw-r--r-- | Cargo.lock | 18 | ||||
-rw-r--r-- | contrib/docker-compose/compose-monitoring.yaml | 2 | ||||
-rw-r--r-- | crates/configuration/src/server.rs | 12 | ||||
-rw-r--r-- | crates/rule-executor/.env.example | 1 | ||||
-rw-r--r-- | crates/rule-executor/Cargo.toml | 8 | ||||
-rw-r--r-- | crates/rule-executor/src/main.rs | 14 | ||||
-rw-r--r-- | crates/rule-executor/src/processor/publish.rs | 44 | ||||
-rw-r--r-- | crates/rule-executor/src/processor/rule.rs | 23 | ||||
-rw-r--r-- | crates/rule-executor/src/processor/rule/configuration.rs | 2 | ||||
-rw-r--r-- | crates/rule-executor/src/processor/rule/determine_outcome.rs | 119 | ||||
-rw-r--r-- | crates/rule-executor/src/processor/rule/rule_901.rs | 126 | ||||
-rw-r--r-- | crates/rule-executor/src/state.rs | 3 | ||||
-rw-r--r-- | crates/typologies/src/main.rs | 3 | ||||
-rw-r--r-- | crates/typologies/src/processor/typology.rs | 2 | ||||
-rw-r--r-- | lib/warden-core/src/configuration/conv.rs | 2 | ||||
-rw-r--r-- | lib/warden-core/src/google/parser/money.rs | 2 |
18 files changed, 434 insertions, 14 deletions
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 56cfdda..64e9601 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,6 +12,34 @@ concurrency: name: ci jobs: + clippy: + runs-on: ubuntu-latest + name: beta / clippy + steps: + - uses: actions/checkout@v5 + with: + submodules: true + - uses: dtolnay/rust-toolchain@master + with: + toolchain: beta + components: clippy,rustfmt + - uses: Swatinem/rust-cache@v2 + - run: cargo install clippy-sarif sarif-fmt + - name: install protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Check + run: > + cargo clippy --workspace --all-features --all-targets --message-format=json + | clippy-sarif + | tee clippy-results.sarif + | sarif-fmt + - name: Upload + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: clippy-results.sarif + os-check: runs-on: ${{ matrix.os }} name: ${{ matrix.os }} / stable @@ -175,14 +203,26 @@ jobs: # # cargo llvm-cov --no-report nextest # # cargo llvm-cov --no-report --doc # # cargo llvm-cov report --doctests --lcov --output-path lcov.info + - name: Create junit.xml (nextest) + run: | + mkdir -p .config + echo '[profile.ci.junit]' > .config/nextest.toml + echo 'path = "junit.xml"' >> .config/nextest.toml - name: cargo llvm-cov - run: cargo llvm-cov nextest --workspace --locked --all-features --lcov --output-path lcov.info + run: cargo llvm-cov nextest --profile ci --workspace --locked --all-features --lcov --output-path lcov.info - name: Upload to codecov.io uses: codecov/codecov-action@v5 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} slug: ${{ github.repository }} + - uses: codecov/test-results-action@v1 + if: ${{ !cancelled() }} + with: + fail_ci_if_error: true + files: ./target/nextest/ci/junit.xml + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true - name: Stop stack if: always() run: | diff --git a/.sqlx/query-32cb5438dd7289fd29e38dc545bf8ca74ef9cbcdc6f51642ef21db4444d1db15.json b/.sqlx/query-32cb5438dd7289fd29e38dc545bf8ca74ef9cbcdc6f51642ef21db4444d1db15.json new file mode 100644 index 0000000..3115b0d --- /dev/null +++ b/.sqlx/query-32cb5438dd7289fd29e38dc545bf8ca74ef9cbcdc6f51642ef21db4444d1db15.json @@ -0,0 +1,25 @@ +{ + "db_name": "PostgreSQL", + "query": "select count(*) from transaction_relationship tr\n where tr.destination = $1\n and tr.tx_tp = $2\n and extract(epoch from ($3::timestamptz - tr.cre_dt_tm)) * 1000 <= $4\n and tr.cre_dt_tm <= $3::timestamptz", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Timestamptz", + "Numeric" + ] + }, + "nullable": [ + null + ] + }, + "hash": "32cb5438dd7289fd29e38dc545bf8ca74ef9cbcdc6f51642ef21db4444d1db15" +} @@ -318,6 +318,19 @@ dependencies = [ ] [[package]] +name = "bigdecimal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2463,6 +2476,7 @@ dependencies = [ "prost 0.14.1", "serde", "serde_json", + "sqlx", "time", "tokio", "tonic 0.14.1", @@ -2933,6 +2947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64", + "bigdecimal", "bytes", "crc", "crossbeam-queue", @@ -3010,6 +3025,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", + "bigdecimal", "bitflags", "byteorder", "bytes", @@ -3054,6 +3070,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", + "bigdecimal", "bitflags", "byteorder", "crc", @@ -3070,6 +3087,7 @@ dependencies = [ "log", "md-5", "memchr", + "num-bigint", "once_cell", "rand 0.8.5", "serde", diff --git a/contrib/docker-compose/compose-monitoring.yaml b/contrib/docker-compose/compose-monitoring.yaml index fb68164..ef1d1d7 100644 --- a/contrib/docker-compose/compose-monitoring.yaml +++ b/contrib/docker-compose/compose-monitoring.yaml @@ -59,7 +59,7 @@ services: # And put them in an OTEL collector pipeline... otel-collector: - image: otel/opentelemetry-collector:0.132.2 + image: otel/opentelemetry-collector:0.132.4 command: [ "--config=/etc/otel-collector.yaml" ] volumes: - ./config/otel-collector.yaml:/etc/otel-collector.yaml diff --git a/crates/configuration/src/server.rs b/crates/configuration/src/server.rs index e31fc60..e31a57b 100644 --- a/crates/configuration/src/server.rs +++ b/crates/configuration/src/server.rs @@ -16,6 +16,10 @@ use warden_core::{ mutate_rule_configuration_server::MutateRuleConfigurationServer, query_rule_configuration_server::QueryRuleConfigurationServer, }, + typology::{ + mutate_typologies_server::MutateTypologiesServer, + query_typologies_server::QueryTypologiesServer, + }, }, }; use warden_middleware::grpc::interceptor::MyInterceptor; @@ -44,6 +48,14 @@ pub fn serve(state: AppHandle) -> Result<(axum::Router, axum::Router), AppError> state.clone(), MyInterceptor, )) + .add_service(QueryTypologiesServer::with_interceptor( + state.clone(), + MyInterceptor, + )) + .add_service(MutateTypologiesServer::with_interceptor( + state.clone(), + MyInterceptor, + )) .add_service(routing_reflector) .into_axum_router() .layer( diff --git a/crates/rule-executor/.env.example b/crates/rule-executor/.env.example new file mode 100644 index 0000000..ae98d94 --- /dev/null +++ b/crates/rule-executor/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgres://postgres:password@localhost:5432/database" diff --git a/crates/rule-executor/Cargo.toml b/crates/rule-executor/Cargo.toml index 3bb9561..614faf0 100644 --- a/crates/rule-executor/Cargo.toml +++ b/crates/rule-executor/Cargo.toml @@ -19,6 +19,14 @@ opentelemetry-semantic-conventions.workspace = true prost.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +sqlx = { workspace = true, features = [ + "bigdecimal", + "macros", + "postgres", + "runtime-tokio", + "time", + "tls-rustls", +] } time = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = [ "macros", diff --git a/crates/rule-executor/src/main.rs b/crates/rule-executor/src/main.rs index ed284c6..abae26d 100644 --- a/crates/rule-executor/src/main.rs +++ b/crates/rule-executor/src/main.rs @@ -1,4 +1,3 @@ -#[allow(dead_code)] mod cnfg; mod processor; @@ -47,6 +46,9 @@ async fn main() -> Result<()> { .nats_jetstream(&config.nats) .await .inspect_err(|e| error!("nats: {e}"))? + .postgres(&config.database) + .await + .inspect_err(|e| error!("postgres: {e}"))? .build(); let jetstream = services @@ -54,7 +56,15 @@ async fn main() -> Result<()> { .take() .ok_or_else(|| anyhow::anyhow!("jetstream is not ready"))?; - let services = state::Services { jetstream }; + let postgres = services + .postgres + .take() + .ok_or_else(|| anyhow::anyhow!("database is not ready"))?; + + let services = state::Services { + jetstream, + postgres, + }; processor::serve(services, config, provider) .await diff --git a/crates/rule-executor/src/processor/publish.rs b/crates/rule-executor/src/processor/publish.rs index 8b13789..0d35977 100644 --- a/crates/rule-executor/src/processor/publish.rs +++ b/crates/rule-executor/src/processor/publish.rs @@ -1 +1,45 @@ +use warden_stack::tracing::telemetry::nats::injector; +use opentelemetry::global; +use opentelemetry_semantic_conventions::attribute; +use tracing::{Instrument, Span, debug, info_span, warn}; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use warden_core::message::Payload; + +use crate::state::AppHandle; + +pub(super) async fn to_typologies( + subject: &str, + state: AppHandle, + payload: Payload, +) -> anyhow::Result<()> { + // send transaction to next with nats + let subject = format!("{}.{}", state.config.nats.destination_prefix, subject); + debug!(subject = ?subject, "publishing"); + + let payload = prost::Message::encode_to_vec(&payload); + + let mut headers = async_nats::HeaderMap::new(); + + let cx = Span::current().context(); + + global::get_text_map_propagator(|propagator| { + propagator.inject_context(&cx, &mut injector::HeaderMap(&mut headers)) + }); + + let span = info_span!("nats.publish"); + span.set_attribute( + attribute::MESSAGING_DESTINATION_SUBSCRIPTION_NAME, + subject.to_string(), + ); + span.set_attribute("otel.kind", "producer"); + state + .services + .jetstream + .publish_with_headers(subject.clone(), headers, payload.into()) + .instrument(span) + .await + .inspect_err(|e| warn!(subject = ?subject, "failed to publish: {e}"))?; + + Ok(()) +} diff --git a/crates/rule-executor/src/processor/rule.rs b/crates/rule-executor/src/processor/rule.rs index 3a54424..6eaf25c 100644 --- a/crates/rule-executor/src/processor/rule.rs +++ b/crates/rule-executor/src/processor/rule.rs @@ -2,15 +2,17 @@ use std::sync::Arc; use anyhow::Result; mod configuration; +mod determine_outcome; +mod rule_901; use async_nats::jetstream; use opentelemetry::global; -use tracing::{Span, error, instrument, warn}; +use tracing::{Span, debug, error, instrument, warn}; use tracing_opentelemetry::OpenTelemetrySpanExt; use warden_core::{configuration::rule::RuleConfigurationRequest, message::Payload}; use warden_stack::tracing::telemetry::nats; -use crate::state::AppHandle; +use crate::{processor::publish, state::AppHandle}; #[instrument( skip(message, state), @@ -27,7 +29,7 @@ pub async fn process_rule(message: jetstream::Message, state: AppHandle) -> Resu span.set_parent(context); }; - let payload: Payload = prost::Message::decode(message.payload.as_ref())?; + let mut payload: Payload = prost::Message::decode(message.payload.as_ref())?; if payload.transaction.is_none() { warn!("transaction is empty - proceeding with ack"); @@ -58,10 +60,23 @@ pub async fn process_rule(message: jetstream::Message, state: AppHandle) -> Resu span.record("rule_id", &req.id); span.record("rule_version", &req.version); - let _rule_configuration = configuration::get_configuration(req, Arc::clone(&state)) + let config = configuration::get_configuration(req, Arc::clone(&state)) .await .unwrap(); + match rule_901::process_901(&config, &payload, state.clone()).await { + Ok(res) => { + debug!(outcome = ?res.reason, "rule executed"); + payload.rule_result = Some(res); + publish::to_typologies(&config.id, state, payload) + .await + .inspect_err(|e| error!("{e}"))?; + } + Err(e) => { + error!("{e}"); + } + }; + if let Err(e) = message.ack().await { error!("ack error {e:?}"); }; diff --git a/crates/rule-executor/src/processor/rule/configuration.rs b/crates/rule-executor/src/processor/rule/configuration.rs index 5f384aa..d8579e6 100644 --- a/crates/rule-executor/src/processor/rule/configuration.rs +++ b/crates/rule-executor/src/processor/rule/configuration.rs @@ -37,10 +37,8 @@ pub(super) async fn get_configuration( .configuration .ok_or_else(|| anyhow!("missing configuration"))?; - println!("inserting"); let cache = state.local_cache.write().await; cache.insert(request, config.clone()).await; - println!("inserted"); Ok(config) } diff --git a/crates/rule-executor/src/processor/rule/determine_outcome.rs b/crates/rule-executor/src/processor/rule/determine_outcome.rs new file mode 100644 index 0000000..727846d --- /dev/null +++ b/crates/rule-executor/src/processor/rule/determine_outcome.rs @@ -0,0 +1,119 @@ +use tracing::trace; +use warden_core::{configuration::rule::Band, message::RuleResult}; + +pub(super) fn determine_outcome(value: i64, bands: &[Band], rule_result: &mut RuleResult) { + trace!("calculating outcome"); + for band in bands { + let value_f64 = value as f64; + + if band.lower_limit.is_none_or(|lower| value_f64 >= lower) + && band.upper_limit.is_none_or(|upper| value_f64 < upper) + { + rule_result.sub_rule_ref = band.sub_rule_ref.to_owned(); + rule_result.reason = band.reason.to_owned(); + break; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_band(lower: Option<f64>, upper: Option<f64>, sub_ref: &str, reason: &str) -> Band { + Band { + lower_limit: lower, + upper_limit: upper, + sub_rule_ref: sub_ref.to_string(), + reason: reason.to_string(), + } + } + + #[test] + fn matches_band_within_limits() { + let bands = vec![ + make_band(Some(0.0), Some(10.0), "A", "Between 0 and 10"), + make_band(Some(10.0), Some(20.0), "B", "Between 10 and 20"), + ]; + let mut rule_result = RuleResult::default(); + + determine_outcome(5, &bands, &mut rule_result); + + assert_eq!(rule_result.sub_rule_ref, "A"); + assert_eq!(rule_result.reason, "Between 0 and 10"); + } + + #[test] + fn matches_band_lower_inclusive_upper_exclusive() { + let bands = vec![ + make_band(Some(0.0), Some(10.0), "A", "Between 0 and 10"), + make_band(Some(10.0), Some(20.0), "B", "Between 10 and 20"), + ]; + let mut rule_result = RuleResult::default(); + + determine_outcome(10, &bands, &mut rule_result); + + assert_eq!(rule_result.sub_rule_ref, "B"); + assert_eq!(rule_result.reason, "Between 10 and 20"); + } + + #[test] + fn no_match_when_above_all_bands() { + let bands = vec![ + make_band(Some(0.0), Some(10.0), "A", "Between 0 and 10"), + make_band(Some(10.0), Some(20.0), "B", "Between 10 and 20"), + ]; + let mut rule_result = RuleResult::default(); + + determine_outcome(30, &bands, &mut rule_result); + + assert_eq!(rule_result, RuleResult::default()); + } + + #[test] + fn match_when_no_upper_limit() { + let bands = vec![make_band(Some(0.0), None, "A", "Above 0")]; + let mut rule_result = RuleResult::default(); + + determine_outcome(100, &bands, &mut rule_result); + + assert_eq!(rule_result.sub_rule_ref, "A"); + assert_eq!(rule_result.reason, "Above 0"); + } + + #[test] + fn match_when_no_lower_limit() { + let bands = vec![make_band(None, Some(50.0), "A", "Below 50")]; + let mut rule_result = RuleResult::default(); + + determine_outcome(-10, &bands, &mut rule_result); + + assert_eq!(rule_result.sub_rule_ref, "A"); + assert_eq!(rule_result.reason, "Below 50"); + } + + #[test] + fn match_when_no_limits() { + let bands = vec![make_band(None, None, "A", "Any value")]; + let mut rule_result = RuleResult::default(); + + determine_outcome(9999, &bands, &mut rule_result); + + assert_eq!(rule_result.sub_rule_ref, "A"); + assert_eq!(rule_result.reason, "Any value"); + } + + #[test] + fn stops_after_first_match() { + let bands = vec![ + make_band(None, None, "A", "Any value"), + make_band(None, None, "B", "Second band"), + ]; + let mut rule_result = RuleResult::default(); + + determine_outcome(5, &bands, &mut rule_result); + + assert_eq!(rule_result.sub_rule_ref, "A"); + assert_eq!(rule_result.reason, "Any value"); + } +} diff --git a/crates/rule-executor/src/processor/rule/rule_901.rs b/crates/rule-executor/src/processor/rule/rule_901.rs new file mode 100644 index 0000000..8dfe036 --- /dev/null +++ b/crates/rule-executor/src/processor/rule/rule_901.rs @@ -0,0 +1,126 @@ +use anyhow::{Result, anyhow}; +use determine_outcome::determine_outcome; +use opentelemetry_semantic_conventions::attribute; +use serde::Deserialize; +use sqlx::types::BigDecimal; +use time::OffsetDateTime; +use tracing::{Instrument, error, info_span, trace}; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use warden_core::{ + configuration::rule::RuleConfiguration, + iso20022::TransactionType, + message::{Payload, RuleResult}, +}; + +use crate::{processor::rule::determine_outcome, state::AppHandle}; + +#[derive(Deserialize)] +pub struct Parameters { + max_query_range: f64, +} + +pub(super) async fn process_901( + configuration: &RuleConfiguration, + payload: &Payload, + state: AppHandle, +) -> Result<RuleResult> { + let mut rule_result = RuleResult { + id: configuration.id.to_string(), + version: configuration.version.to_string(), + ..Default::default() + }; + let c = configuration.configuration.as_ref(); + + let bands = c + .and_then(|value| { + if value.bands.is_empty() { + None + } else { + Some(&value.bands) + } + }) + .ok_or_else(|| anyhow!("no bands available"))?; + + let exit_conditions = c + .and_then(|value| { + if value.exit_conditions.is_empty() { + None + } else { + Some(&value.exit_conditions) + } + }) + .ok_or_else(|| anyhow!("no exit conditions available"))?; + + let parameters = c + .and_then(|value| value.parameters.as_ref()) + .ok_or_else(|| anyhow!("no parameters available"))?; + + let params: Parameters = serde_json::from_value(parameters.clone().into()) + .inspect_err(|e| error!("failed to deserailise params: {e:?}"))?; + + let unsuccessful_transaction = exit_conditions + .iter() + .find(|value| value.sub_rule_ref.eq(".x00")); + + if let Some(warden_core::message::payload::Transaction::Pacs002(pacs002_document)) = + payload.transaction.as_ref() + { + let tx_sts = pacs002_document + .f_i_to_f_i_pmt_sts_rpt + .tx_inf_and_sts + .first() + .ok_or_else(|| anyhow::anyhow!("tx sts to be there"))?; + + if tx_sts.tx_sts().ne("ACCC") { + let unsuccessful_transaction = unsuccessful_transaction + .ok_or_else(|| anyhow::anyhow!("no unsuccessful transaction ref"))?; + rule_result.reason = unsuccessful_transaction.reason.to_owned(); + rule_result.sub_rule_ref = unsuccessful_transaction.sub_rule_ref.to_owned(); + + return Ok(rule_result); + } + + let current_pacs002_timeframe: OffsetDateTime = pacs002_document + .f_i_to_f_i_pmt_sts_rpt + .grp_hdr + .cre_dt_tm + .try_into()?; + + let data_cache = payload + .data_cache + .as_ref() + .ok_or_else(|| anyhow::anyhow!("data cache is missing"))?; + + let range = BigDecimal::try_from(params.max_query_range)?; + + let tx_tp = TransactionType::PACS002.to_string(); + + let span = info_span!("rule.logic"); + span.set_attribute(attribute::DB_SYSTEM_NAME, "postgres"); + span.set_attribute(attribute::DB_OPERATION_NAME, "901"); + span.set_attribute("otel.kind", "client"); + + trace!("executing rule query"); + let recent_transactions = sqlx::query_scalar!( + "select count(*) from transaction_relationship tr + where tr.destination = $1 + and tr.tx_tp = $2 + and extract(epoch from ($3::timestamptz - tr.cre_dt_tm)) * 1000 <= $4 + and tr.cre_dt_tm <= $3::timestamptz", + data_cache.dbtr_acct_id, + tx_tp, + current_pacs002_timeframe, + range, + ) + .fetch_one(&state.services.postgres) + .instrument(span) + .await? + .ok_or_else(|| anyhow::anyhow!("no data"))?; + + determine_outcome(recent_transactions, bands.as_ref(), &mut rule_result); + + Ok(rule_result) + } else { + Err(anyhow::anyhow!("no valid transaction")) + } +} diff --git a/crates/rule-executor/src/state.rs b/crates/rule-executor/src/state.rs index efad4ea..432068e 100644 --- a/crates/rule-executor/src/state.rs +++ b/crates/rule-executor/src/state.rs @@ -9,7 +9,7 @@ use warden_core::configuration::rule::{ RuleConfiguration, RuleConfigurationRequest, query_rule_configuration_client::QueryRuleConfigurationClient, }; -use warden_stack::Configuration; +use warden_stack::{Configuration, sqlx::PgPool}; use crate::cnfg::LocalConfig; use warden_middleware::grpc::interceptor::{Intercepted, MyInterceptor}; @@ -17,6 +17,7 @@ use warden_middleware::grpc::interceptor::{Intercepted, MyInterceptor}; #[derive(Clone)] pub struct Services { pub jetstream: Context, + pub postgres: PgPool, } pub type AppHandle = Arc<AppState>; diff --git a/crates/typologies/src/main.rs b/crates/typologies/src/main.rs index ea7843a..e96f6bb 100644 --- a/crates/typologies/src/main.rs +++ b/crates/typologies/src/main.rs @@ -45,6 +45,9 @@ async fn main() -> Result<()> { .nats_jetstream(&config.nats) .await .inspect_err(|e| error!("nats: {e}"))? + .cache(&config.cache) + .await + .inspect_err(|e| error!("cache: {e}"))? .build(); let jetstream = services diff --git a/crates/typologies/src/processor/typology.rs b/crates/typologies/src/processor/typology.rs index b1b2592..62e7089 100644 --- a/crates/typologies/src/processor/typology.rs +++ b/crates/typologies/src/processor/typology.rs @@ -83,7 +83,7 @@ pub async fn process_typology( Ok(()) } -#[instrument(skip(typology_result, routing, payload, state), err(Debug))] +#[instrument(skip(routing, payload, state), err(Debug))] async fn evaluate_typology( typology_result: &mut [TypologyResult], routing: &RoutingConfiguration, diff --git a/lib/warden-core/src/configuration/conv.rs b/lib/warden-core/src/configuration/conv.rs index 7f982b4..3b0fef9 100644 --- a/lib/warden-core/src/configuration/conv.rs +++ b/lib/warden-core/src/configuration/conv.rs @@ -266,7 +266,7 @@ mod tests { "x": 1, "y": [true, null, "str"], "z": { - "nested": 3.14 + "nested": 3.90 } }); diff --git a/lib/warden-core/src/google/parser/money.rs b/lib/warden-core/src/google/parser/money.rs index a703a4a..c12bfb1 100644 --- a/lib/warden-core/src/google/parser/money.rs +++ b/lib/warden-core/src/google/parser/money.rs @@ -172,7 +172,7 @@ mod tests { #[test] fn test_round_trip_conversion() { - let original = 1234.567_890_123; + let original = 1_234.567_890_123; let money = Money::try_from((original, "USD")).unwrap(); let back: f64 = money.into(); assert!( |