commit 98f327eaebc17e9adf4ddb1a578c9cfd6ae60662
parent a5e917fc0b76e524ea6b7c77f0e6cca437a50a18
Author: triesap <tyson@radroots.org>
Date: Mon, 22 Dec 2025 17:09:41 +0000
trade: tighten listing parsing and pricing APIs
- gate typeshare/serde attributes for trade listing models and stages
- add explicit TradeListingStage parse error with tests
- harden chain tag validation and reduce tag allocations
- add try_* pricing helpers for explicit errors and unit-mismatch test
Diffstat:
13 files changed, 294 insertions(+), 75 deletions(-)
diff --git a/events/bindings/ts/src/types.ts b/events/bindings/ts/src/types.ts
@@ -1,5 +1,3 @@
-import type { RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice } from "@radroots/core-bindings";
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type JobFeedbackStatus = "payment_required" | "processing" | "error" | "success" | "partial";
diff --git a/trade/src/listing/kinds.rs b/trade/src/listing/kinds.rs
@@ -1,46 +1,46 @@
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_ORDER_REQ: u16 = 5301;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_ORDER_RES: u16 = 6301;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_ACCEPT_REQ: u16 = 5302;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_ACCEPT_RES: u16 = 6302;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_CONVEYANCE_REQ: u16 = 5303;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_CONVEYANCE_RES: u16 = 6303;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_INVOICE_REQ: u16 = 5304;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_INVOICE_RES: u16 = 6304;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_PAYMENT_REQ: u16 = 5305;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_PAYMENT_RES: u16 = 6305;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_FULFILL_REQ: u16 = 5306;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_FULFILL_RES: u16 = 6306;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_RECEIPT_REQ: u16 = 5307;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_RECEIPT_RES: u16 = 6307;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_CANCEL_REQ: u16 = 5309;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_CANCEL_RES: u16 = 6309;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_REFUND_REQ: u16 = 5310;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
pub const KIND_TRADE_LISTING_REFUND_RES: u16 = 6310;
#[inline]
diff --git a/trade/src/listing/meta.rs b/trade/src/listing/meta.rs
@@ -10,7 +10,7 @@ pub const MARKER_INVOICE_RESULT: &str = "invoice_result";
pub const MARKER_FULFILLMENT_RESULT: &str = "fulfillment_result";
pub const MARKER_PROOF: &str = "proof";
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(
@@ -29,9 +29,10 @@ pub enum TradeListingStage {
Refund,
}
-impl fmt::Display for TradeListingStage {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(match self {
+impl TradeListingStage {
+ #[inline]
+ pub const fn as_str(&self) -> &'static str {
+ match self {
TradeListingStage::Order => "order",
TradeListingStage::Accept => "accept",
TradeListingStage::Conveyance => "conveyance",
@@ -41,12 +42,37 @@ impl fmt::Display for TradeListingStage {
TradeListingStage::Receipt => "receipt",
TradeListingStage::Cancel => "cancel",
TradeListingStage::Refund => "refund",
- })
+ }
+ }
+}
+
+impl fmt::Display for TradeListingStage {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(self.as_str())
+ }
+}
+
+#[non_exhaustive]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum TradeListingStageParseError {
+ UnknownStage,
+}
+
+impl fmt::Display for TradeListingStageParseError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ TradeListingStageParseError::UnknownStage => {
+ write!(f, "unknown trade listing stage")
+ }
+ }
}
}
+#[cfg(feature = "std")]
+impl std::error::Error for TradeListingStageParseError {}
+
impl FromStr for TradeListingStage {
- type Err = ();
+ type Err = TradeListingStageParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"order" => Ok(Self::Order),
@@ -58,7 +84,39 @@ impl FromStr for TradeListingStage {
"receipt" => Ok(Self::Receipt),
"cancel" => Ok(Self::Cancel),
"refund" => Ok(Self::Refund),
- _ => Err(()),
+ _ => Err(TradeListingStageParseError::UnknownStage),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{TradeListingStage, TradeListingStageParseError};
+
+ #[test]
+ fn stage_roundtrip() {
+ let cases = [
+ (TradeListingStage::Order, "order"),
+ (TradeListingStage::Accept, "accept"),
+ (TradeListingStage::Conveyance, "conveyance"),
+ (TradeListingStage::Invoice, "invoice"),
+ (TradeListingStage::Payment, "payment"),
+ (TradeListingStage::Fulfillment, "fulfillment"),
+ (TradeListingStage::Receipt, "receipt"),
+ (TradeListingStage::Cancel, "cancel"),
+ (TradeListingStage::Refund, "refund"),
+ ];
+
+ for (stage, name) in cases {
+ assert_eq!(stage.as_str(), name);
+ assert_eq!(stage.to_string(), name);
+ assert_eq!(name.parse::<TradeListingStage>().unwrap(), stage);
}
}
+
+ #[test]
+ fn stage_parse_rejects_unknown() {
+ let err = "unknown".parse::<TradeListingStage>().unwrap_err();
+ assert_eq!(err, TradeListingStageParseError::UnknownStage);
+ }
}
diff --git a/trade/src/listing/model.rs b/trade/src/listing/model.rs
@@ -1,7 +1,4 @@
-#[cfg(not(feature = "std"))]
-use alloc::{string::String, vec::Vec};
-
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct RadrootsTradeListingSubtotal {
@@ -11,7 +8,7 @@ pub struct RadrootsTradeListingSubtotal {
pub quantity_unit: radroots_core::RadrootsCoreUnit,
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct RadrootsTradeListingTotal {
diff --git a/trade/src/listing/price_ext.rs b/trade/src/listing/price_ext.rs
@@ -1,7 +1,7 @@
use crate::listing::model::{RadrootsTradeListingSubtotal, RadrootsTradeListingTotal};
use radroots_core::{
RadrootsCoreDecimal, RadrootsCoreQuantity, RadrootsCoreQuantityPrice,
- RadrootsCoreQuantityPriceOps,
+ RadrootsCoreQuantityPriceError, RadrootsCoreQuantityPriceOps,
};
use radroots_events::listing::RadrootsListingQuantity;
@@ -14,19 +14,33 @@ pub trait ListingPricingExt {
fn total_for(&self, qty: &RadrootsListingQuantity) -> RadrootsTradeListingTotal;
}
+pub trait ListingPricingTryExt {
+ fn try_subtotal_for(
+ &self,
+ qty: &RadrootsListingQuantity,
+ ) -> Result<RadrootsTradeListingSubtotal, RadrootsCoreQuantityPriceError>;
+ fn try_total_for(
+ &self,
+ qty: &RadrootsListingQuantity,
+ ) -> Result<RadrootsTradeListingTotal, RadrootsCoreQuantityPriceError>;
+}
+
+#[inline]
+fn effective_quantity(qty: &RadrootsListingQuantity) -> RadrootsCoreQuantity {
+ let count = qty.count.unwrap_or(1);
+ let amount = qty.value.amount * RadrootsCoreDecimal::from(count);
+ RadrootsCoreQuantity::new(amount, qty.value.unit)
+}
+
impl ListingPricingExt for RadrootsCoreQuantityPrice {
fn subtotal_for(&self, qty: &RadrootsListingQuantity) -> RadrootsTradeListingSubtotal {
- let count = qty.count.unwrap_or(1);
- let effective_qty = RadrootsCoreQuantity::new(
- qty.value.amount * RadrootsCoreDecimal::from(count as u32),
- qty.value.unit,
- );
-
+ let effective_qty = effective_quantity(qty);
let money = self.cost_for_rounded(&effective_qty);
+ let currency = money.currency;
RadrootsTradeListingSubtotal {
- price_amount: money.clone(),
- price_currency: self.amount.currency,
+ price_amount: money,
+ price_currency: currency,
quantity_amount: effective_qty.amount,
quantity_unit: effective_qty.unit,
}
@@ -42,3 +56,70 @@ impl ListingPricingExt for RadrootsCoreQuantityPrice {
}
}
}
+
+impl ListingPricingTryExt for RadrootsCoreQuantityPrice {
+ fn try_subtotal_for(
+ &self,
+ qty: &RadrootsListingQuantity,
+ ) -> Result<RadrootsTradeListingSubtotal, RadrootsCoreQuantityPriceError> {
+ let effective_qty = effective_quantity(qty);
+ let money = self.try_cost_for_rounded(&effective_qty)?;
+ let currency = money.currency;
+
+ Ok(RadrootsTradeListingSubtotal {
+ price_amount: money,
+ price_currency: currency,
+ quantity_amount: effective_qty.amount,
+ quantity_unit: effective_qty.unit,
+ })
+ }
+
+ fn try_total_for(
+ &self,
+ qty: &RadrootsListingQuantity,
+ ) -> Result<RadrootsTradeListingTotal, RadrootsCoreQuantityPriceError> {
+ let sub = self.try_subtotal_for(qty)?;
+ Ok(RadrootsTradeListingTotal {
+ price_amount: sub.price_amount,
+ price_currency: sub.price_currency,
+ quantity_amount: sub.quantity_amount,
+ quantity_unit: sub.quantity_unit,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::ListingPricingTryExt;
+ use radroots_core::{
+ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
+ RadrootsCoreQuantityPrice, RadrootsCoreQuantityPriceError, RadrootsCoreUnit,
+ };
+ use radroots_events::listing::RadrootsListingQuantity;
+
+ #[test]
+ fn try_subtotal_for_rejects_unit_mismatch() {
+ let price = RadrootsCoreQuantityPrice::new(
+ RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD),
+ RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each),
+ );
+
+ let qty = RadrootsListingQuantity {
+ value: RadrootsCoreQuantity::new(
+ RadrootsCoreDecimal::from(1u32),
+ RadrootsCoreUnit::MassG,
+ ),
+ label: None,
+ count: None,
+ };
+
+ let err = price.try_subtotal_for(&qty).unwrap_err();
+ assert_eq!(
+ err,
+ RadrootsCoreQuantityPriceError::UnitMismatch {
+ have: RadrootsCoreUnit::MassG,
+ want: RadrootsCoreUnit::Each,
+ }
+ );
+ }
+}
diff --git a/trade/src/listing/stage/accept.rs b/trade/src/listing/stage/accept.rs
@@ -1,7 +1,7 @@
#[cfg(not(feature = "std"))]
use alloc::string::String;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingAcceptRequest {
@@ -9,7 +9,7 @@ pub struct TradeListingAcceptRequest {
pub listing_event_id: String,
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingAcceptResult {
diff --git a/trade/src/listing/stage/conveyance.rs b/trade/src/listing/stage/conveyance.rs
@@ -1,12 +1,13 @@
-#![cfg_attr(not(feature = "serde"), allow(unused_attributes))]
-
#[cfg(not(feature = "std"))]
use alloc::string::String;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
-#[serde(rename_all = "snake_case", tag = "kind", content = "amount")]
+#[cfg_attr(
+ feature = "serde",
+ serde(rename_all = "snake_case", tag = "kind", content = "amount")
+)]
pub enum TradeListingConveyanceMethod {
SellerDelivery {
window: Option<String>,
@@ -23,7 +24,7 @@ pub enum TradeListingConveyanceMethod {
},
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingConveyanceRequest {
@@ -31,7 +32,7 @@ pub struct TradeListingConveyanceRequest {
pub method: TradeListingConveyanceMethod,
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingConveyanceResult {
diff --git a/trade/src/listing/stage/fulfillment.rs b/trade/src/listing/stage/fulfillment.rs
@@ -1,14 +1,14 @@
#[cfg(not(feature = "std"))]
use alloc::string::String;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingFulfillmentRequest {
pub payment_result_event_id: String,
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
#[cfg_attr(
@@ -23,7 +23,7 @@ pub enum TradeListingFulfillmentState {
Canceled,
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingFulfillmentResult {
diff --git a/trade/src/listing/stage/invoice.rs b/trade/src/listing/stage/invoice.rs
@@ -1,14 +1,14 @@
#[cfg(not(feature = "std"))]
use alloc::string::String;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingInvoiceRequest {
pub accept_result_event_id: String,
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingInvoiceResult {
diff --git a/trade/src/listing/stage/order.rs b/trade/src/listing/stage/order.rs
@@ -1,11 +1,11 @@
#[cfg(not(feature = "std"))]
-use alloc::{string::String, vec::Vec};
+use alloc::vec::Vec;
use radroots_core::RadrootsCoreQuantityPrice;
use radroots_events::listing::{RadrootsListingDiscount, RadrootsListingQuantity};
use crate::listing::model::{RadrootsTradeListingSubtotal, RadrootsTradeListingTotal};
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingOrderRequestPayload {
@@ -13,7 +13,7 @@ pub struct TradeListingOrderRequestPayload {
pub quantity: RadrootsListingQuantity,
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingOrderRequest {
@@ -21,7 +21,7 @@ pub struct TradeListingOrderRequest {
pub payload: TradeListingOrderRequestPayload,
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingOrderResult {
diff --git a/trade/src/listing/stage/payment.rs b/trade/src/listing/stage/payment.rs
@@ -1,12 +1,13 @@
-#![cfg_attr(not(feature = "serde"), allow(unused_attributes))]
-
#[cfg(not(feature = "std"))]
use alloc::string::String;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
-#[serde(rename_all = "snake_case", tag = "kind", content = "amount")]
+#[cfg_attr(
+ feature = "serde",
+ serde(rename_all = "snake_case", tag = "kind", content = "amount")
+)]
pub enum TradeListingPaymentProof {
ZapEvent { id: String },
Preimage { hex: String },
@@ -14,7 +15,7 @@ pub enum TradeListingPaymentProof {
ExternalRef { provider: String, ref_id: String },
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingPaymentProofRequest {
@@ -22,7 +23,7 @@ pub struct TradeListingPaymentProofRequest {
pub proof: TradeListingPaymentProof,
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingPaymentResult {
diff --git a/trade/src/listing/stage/receipt.rs b/trade/src/listing/stage/receipt.rs
@@ -1,7 +1,7 @@
#[cfg(not(feature = "std"))]
use alloc::string::String;
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingReceiptRequest {
@@ -9,7 +9,7 @@ pub struct TradeListingReceiptRequest {
pub note: Option<String>,
}
-#[typeshare::typeshare]
+#[cfg_attr(feature = "typeshare", typeshare::typeshare)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct TradeListingReceiptResult {
diff --git a/trade/src/listing/tags.rs b/trade/src/listing/tags.rs
@@ -5,34 +5,117 @@ use radroots_events::tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT};
use radroots_events_codec::job::error::JobParseError;
#[inline]
+fn push_tag(tags: &mut Vec<Vec<String>>, name: &'static str, value: impl Into<String>) {
+ let mut tag = Vec::with_capacity(2);
+ tag.push(name.to_owned());
+ tag.push(value.into());
+ tags.push(tag);
+}
+
+#[inline]
pub fn push_trade_listing_chain_tags(
tags: &mut Vec<Vec<String>>,
e_root_id: impl Into<String>,
e_prev_id: Option<impl Into<String>>,
trade_id: Option<impl Into<String>>,
) {
- tags.push(vec![TAG_E_ROOT.into(), e_root_id.into()]);
+ let mut reserve = 1;
+ if e_prev_id.is_some() {
+ reserve += 1;
+ }
+ if trade_id.is_some() {
+ reserve += 1;
+ }
+ tags.reserve(reserve);
+
+ push_tag(tags, TAG_E_ROOT, e_root_id);
if let Some(prev) = e_prev_id {
- tags.push(vec![TAG_E_PREV.into(), prev.into()]);
+ push_tag(tags, TAG_E_PREV, prev);
}
if let Some(d) = trade_id {
- tags.push(vec![TAG_D.into(), d.into()]);
+ push_tag(tags, TAG_D, d);
}
}
#[inline]
pub fn validate_trade_listing_chain(tags: &[Vec<String>]) -> Result<(), JobParseError> {
- let has_root = tags
- .iter()
- .any(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_E_ROOT));
+ let mut has_root = false;
+ let mut has_d = false;
+
+ for tag in tags {
+ match tag.as_slice() {
+ [key, value, ..] if key == TAG_E_ROOT => {
+ if value.trim().is_empty() {
+ return Err(JobParseError::InvalidTag(TAG_E_ROOT));
+ }
+ has_root = true;
+ }
+ [key] if key == TAG_E_ROOT => {
+ return Err(JobParseError::InvalidTag(TAG_E_ROOT));
+ }
+ [key, value, ..] if key == TAG_D => {
+ if value.trim().is_empty() {
+ return Err(JobParseError::InvalidTag(TAG_D));
+ }
+ has_d = true;
+ }
+ [key] if key == TAG_D => {
+ return Err(JobParseError::InvalidTag(TAG_D));
+ }
+ _ => {}
+ }
+
+ if has_root && has_d {
+ return Ok(());
+ }
+ }
+
if !has_root {
return Err(JobParseError::MissingChainTag(TAG_E_ROOT));
}
- let has_d = tags
- .iter()
- .any(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_D));
if !has_d {
return Err(JobParseError::MissingChainTag(TAG_D));
}
Ok(())
}
+
+#[cfg(test)]
+mod tests {
+ use super::validate_trade_listing_chain;
+ use radroots_events::tags::{TAG_D, TAG_E_ROOT};
+ use radroots_events_codec::job::error::JobParseError;
+
+ #[test]
+ fn validate_trade_listing_chain_ok() {
+ let tags = vec![
+ vec![TAG_E_ROOT.into(), "root".into()],
+ vec![TAG_D.into(), "trade".into()],
+ ];
+ assert!(validate_trade_listing_chain(&tags).is_ok());
+ }
+
+ #[test]
+ fn validate_trade_listing_chain_rejects_missing_root() {
+ let tags = vec![vec![TAG_D.into(), "trade".into()]];
+ match validate_trade_listing_chain(&tags) {
+ Err(JobParseError::MissingChainTag(tag)) => {
+ assert_eq!(tag, TAG_E_ROOT);
+ }
+ other => panic!("expected missing root tag, got {other:?}"),
+ }
+ }
+
+ #[test]
+ fn validate_trade_listing_chain_rejects_empty_root_value() {
+ let tags = vec![
+ vec![TAG_E_ROOT.into(), " ".into()],
+ vec![TAG_D.into(), "trade".into()],
+ ];
+ match validate_trade_listing_chain(&tags) {
+ Err(JobParseError::InvalidTag(tag)) => {
+ assert_eq!(tag, TAG_E_ROOT);
+ }
+ other => panic!("expected invalid root tag, got {other:?}"),
+ }
+ }
+}