passing all tests

This commit is contained in:
djkato 2025-02-14 15:14:36 +01:00
commit 17d20eef66
8 changed files with 50852 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1714
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

32
Cargo.toml Normal file
View file

@ -0,0 +1,32 @@
[package]
name = "heureka-xml-feed"
version = "0.1.0"
edition = "2024"
[dependencies]
fake = { version = "3.1.0", features = [
"http",
"random_color",
"chrono",
"derive",
"geo",
"rust_decimal",
"uuid",
"dummy",
] }
datetime = { version = "0.5.2", features = ["parse"] }
pyo3 = { version = "0.23.4", features = ["auto-initialize"] }
quick-xml = { version = "0.37.2", features = [
"serialize",
"escape-html",
"document-features",
"overlapped-lists",
# "serde-types",
] }
rand = "0.9.0"
rust_decimal = { version = "1.36.0", features = ["serde"] }
serde = { version = "1.0.217", features = ["derive"] }
serde_with = { version = "3.12.0", features = ["chrono"] }
thiserror = "2.0.11"
url = { version = "2.5.4", features = ["serde"] }
chrono = { version = "0.4.39", features = ["serde"] }

48396
a.xml Normal file

File diff suppressed because it is too large Load diff

44
heureka-example.xml Normal file
View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<SHOP>
<SHOPITEM>
<ITEM_ID>AB123</ITEM_ID>
<PRODUCTNAME>Nokia 5800 XpressMusic</PRODUCTNAME>
<PRODUCT>Nokia 5800 XpressMusic</PRODUCT>
<DESCRIPTION>Klasický s plným dotykovým uživatelským rozhraním</DESCRIPTION>
<URL>http://obchod.cz/mobily/nokia-5800-xpressmusic</URL>
<IMGURL>http://obchod.cz/mobily/nokia-5800-xpressmusic/obrazek.jpg</IMGURL>
<IMGURL_ALTERNATIVE>http://obchod.cz/mobily/nokia-5800-xpressmusic/obrazek2.jpg</IMGURL_ALTERNATIVE>
<PRICE_VAT>6000</PRICE_VAT>
<HEUREKA_CPC>5,8</HEUREKA_CPC>
<MANUFACTURER>NOKIA</MANUFACTURER>
<CATEGORYTEXT>Elektronika | Mobilní telefony</CATEGORYTEXT>
<EAN>6417182041488</EAN>
<PRODUCTNO>RM-559394</PRODUCTNO>
<PARAM>
<PARAM_NAME>Barva</PARAM_NAME>
<VAL>černá</VAL>
</PARAM>
<DELIVERY_DATE>2</DELIVERY_DATE>
<DELIVERY>
<DELIVERY_ID>CESKA_POSTA</DELIVERY_ID>
<DELIVERY_PRICE>120</DELIVERY_PRICE>
<DELIVERY_PRICE_COD>120</DELIVERY_PRICE_COD>
</DELIVERY>
<DELIVERY>
<DELIVERY_ID>PPL</DELIVERY_ID>
<DELIVERY_PRICE>90</DELIVERY_PRICE>
<DELIVERY_PRICE_COD>120</DELIVERY_PRICE_COD>
</DELIVERY>
<ACCESSORY>CD456</ACCESSORY>
<GIFT>Pouzdro zdarma</GIFT>
<EXTENDED_WARRANTY>
<VAL>36</VAL>
<DESC>Záruka na 36 měsíců</DESC>
</EXTENDED_WARRANTY>
<SPECIAL_SERVICE>Aplikace ochranné fólie</SPECIAL_SERVICE>
<SALES_VOUCHER>
<CODE>SLEVA20</CODE>
<DESC>Sleva 20% po zadání kódu do 31.12.2021!</DESC>
</SALES_VOUCHER>
</SHOPITEM>
</SHOP>

202
heureka-feed-2.0.xsd Normal file
View file

@ -0,0 +1,202 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
<xs:complexType name="DeliveryType">
<xs:all>
<xs:element name="DELIVERY_ID">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="CESKA_POSTA"/>
<xs:enumeration value="SLOVENSKA_POSTA"/>
<xs:enumeration value="CESKA_POSTA_DOPORUCENA_ZASILKA"/>
<xs:enumeration value="CSAD_LOGISTIK_OSTRAVA"/>
<xs:enumeration value="DPD"/>
<xs:enumeration value="DHL"/>
<xs:enumeration value="DSV"/>
<xs:enumeration value="FOFR"/>
<xs:enumeration value="EXPRES_KURIER"/>
<xs:enumeration value="GEBRUDER_WEISS"/>
<xs:enumeration value="GEIS"/>
<xs:enumeration value="GLS"/>
<xs:enumeration value="HDS"/>
<xs:enumeration value="PPL"/>
<xs:enumeration value="SEEGMULLER"/>
<xs:enumeration value="EXPRESS_ONE"/>
<xs:enumeration value="TNT"/>
<xs:enumeration value="TOPTRANS"/>
<xs:enumeration value="UPS"/>
<xs:enumeration value="DEPO"/>
<xs:enumeration value="FEDEX"/>
<xs:enumeration value="RABEN_LOGISTICS"/>
<xs:enumeration value="ZASILKOVNA_NA_ADRESU"/>
<xs:enumeration value="SDS"/>
<xs:enumeration value="SPS"/>
<xs:enumeration value="123KURIER"/>
<xs:enumeration value="PACKETA_DOMOV"/>
<xs:enumeration value="WEDO_HOME"/>
<xs:enumeration value="RHENUS_LOGISTICS"/>
<xs:enumeration value="MESSENGER"/>
<xs:enumeration value="PALET_EXPRESS"/>
<xs:enumeration value="SLOVENSKA_POSTA_NAPOSTU_DEPOTAPI"/>
<xs:enumeration value="ZASILKOVNA"/>
<xs:enumeration value="DPD_PICKUP"/>
<xs:enumeration value="BALIKOVNA_DEPOTAPI"/>
<xs:enumeration value="PACKETA"/>
<xs:enumeration value="WEDO_POINT"/>
<xs:enumeration value="BALIKOVO"/>
<xs:enumeration value="CESKA_POSTA_NAPOSTU"/>
<xs:enumeration value="PPL_PARCELSHOP"/>
<xs:enumeration value="GLS_PARCELSHOP"/>
<xs:enumeration value="ALZAPOINT"/>
<xs:enumeration value="DPD_BOX"/>
<xs:enumeration value="Z_BOX"/>
<xs:enumeration value="WEDO_BOX"/>
<xs:enumeration value="PPL_PARCELBOX"/>
<xs:enumeration value="BALIKOVNA_BOX"/>
<xs:enumeration value="BALIKO_BOX"/>
<xs:enumeration value="GLS_PARCELLOCKER"/>
<xs:enumeration value="ALZABOX"/>
<xs:enumeration value="ONLINE"/>
<xs:enumeration value="VLASTNI_PREPRAVA"/>
<xs:enumeration value="VLASTNA_PREPRAVA"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="DELIVERY_PRICE" type="xs:decimal"/>
<xs:element name="DELIVERY_PRICE_COD" type="xs:decimal"/>
</xs:all>
</xs:complexType>
<xs:complexType name="ParamType">
<xs:all>
<xs:element name="PARAM_NAME" type="xs:string"/>
<xs:element name="VAL" type="xs:string"/>
</xs:all>
</xs:complexType>
<xs:complexType name="ExtendedWarrantyType">
<xs:all>
<xs:element name="VAL" type="xs:int"/>
<xs:element name="DESC" type="xs:string"/>
</xs:all>
</xs:complexType>
<xs:simpleType name="DateOrDays">
<xs:union memberTypes="xs:int xs:date"/>
</xs:simpleType>
<xs:complexType name="SalesVoucherType">
<xs:all>
<xs:element name="CODE" type="xs:string"/>
<xs:element name="DESC" type="xs:string"/>
</xs:all>
</xs:complexType>
<xs:simpleType name="Percent">
<xs:restriction base="xs:string">
<xs:pattern value="\d+%"/>
</xs:restriction>
</xs:simpleType>
<xs:element name="SHOP">
<xs:complexType>
<xs:sequence>
<xs:element name="SHOPITEM" maxOccurs="unbounded">
<xs:complexType>
<xs:all>
<xs:element name="ITEM_ID" type="xs:string">
<xs:annotation>
<xs:documentation>ITEM_ID can only contain characters: [ _ - 0-9 a-z A-Z ] and be a maximum of 36 characters long.</xs:documentation>
<xs:appinfo>
<xs:pattern value="[a-zA-Z0-9_-]+"/>
<xs:maxLength value="36"/>
</xs:appinfo>
</xs:annotation>
</xs:element>
<xs:element name="ITEMGROUP_ID" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>ITEMGROUP_ID can only contain characters: [ _ - 0-9 a-z A-Z ] and be a maximum of 36 characters long.</xs:documentation>
<xs:appinfo>
<xs:pattern value="[a-zA-Z0-9_-]+"/>
<xs:maxLength value="36"/>
</xs:appinfo>
</xs:annotation>
</xs:element>
<xs:element name="PRODUCTNAME" type="xs:string">
<xs:annotation>
<xs:documentation>PRODUCTNAME can be a maximum of 200 characters long.</xs:documentation>
<xs:appinfo>
<xs:maxLength value="200"/>
</xs:appinfo>
</xs:annotation>
</xs:element>
<xs:element name="PRODUCT" type="xs:string" minOccurs="0"/>
<xs:element name="DESCRIPTION" type="xs:string" minOccurs="0"/>
<xs:element name="URL" type="xs:anyURI" minOccurs="0">
<xs:annotation>
<xs:documentation>URL can be a maximum of 255 characters long.</xs:documentation>
<xs:appinfo>
<xs:maxLength value="255"/>
</xs:appinfo>
</xs:annotation>
</xs:element>
<xs:element name="IMGURL" type="xs:anyURI" minOccurs="0">
<xs:annotation>
<xs:documentation>IMGURL can be a maximum of 255 characters long.</xs:documentation>
<xs:appinfo>
<xs:maxLength value="255"/>
</xs:appinfo>
</xs:annotation>
</xs:element>
<xs:element name="IMGURL_ALTERNATIVE" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>IMGURL_ALTERNATIVE can be a maximum of 255 characters long.</xs:documentation>
<xs:appinfo>
<xs:maxLength value="255"/>
</xs:appinfo>
</xs:annotation>
</xs:element>
<xs:element name="VIDEO_URL" type="xs:anyURI" minOccurs="0"/>
<xs:element name="VAT" type="Percent" minOccurs="0" />
<xs:element name="PRICE_VAT" type="xs:decimal"/>
<xs:element name="ITEM_TYPE" type="xs:string" minOccurs="0"/>
<xs:element name="HEUREKA_CPC" type="xs:decimal" minOccurs="0"/>
<xs:element name="MANUFACTURER" type="xs:string" minOccurs="0"/>
<xs:element name="CATEGORYTEXT" type="xs:string"/>
<xs:element name="EAN" type="xs:string" minOccurs="0"/>
<xs:element name="ISBN" type="xs:string" minOccurs="0"/>
<xs:element name="PRODUCTNO" type="xs:string" minOccurs="0"/>
<xs:element name="PARAM" maxOccurs="unbounded" type="ParamType" minOccurs="0"/>
<xs:element name="DELIVERY_DATE" type="DateOrDays" minOccurs="0"/>
<xs:element name="DELIVERY" maxOccurs="unbounded" type="DeliveryType" minOccurs="0"/>
<xs:element name="ACCESSORY" type="xs:string" minOccurs="0" maxOccurs="10"/>
<xs:element name="DUES" type="xs:string" minOccurs="0"/>
<xs:element name="GIFT" type="xs:string" minOccurs="0"/>
<xs:element name="GIFT_ID" type="xs:string" minOccurs="0"/>
<xs:element name="EXTENDED_WARRANTY" type="ExtendedWarrantyType" minOccurs="0"/>
<xs:element name="SPECIAL_SERVICE" type="xs:string" minOccurs="0" maxOccurs="5">
<xs:annotation>
<xs:documentation>SPECIAL_SERVICE can be a maximum of 128 characters long.</xs:documentation>
<xs:appinfo>
<xs:maxLength value="128"/>
</xs:appinfo>
</xs:annotation>
</xs:element>
<xs:element name="SALES_VOUCHER" type="SalesVoucherType" minOccurs="0"/>
</xs:all>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

1
output.xml Normal file
View file

@ -0,0 +1 @@
<SHOP><SHOPITEM><ITEM_ID>325236407</ITEM_ID><PRODUCTNAME>Adidas Superstar 2 W EUR 36</PRODUCTNAME><PRODUCT>Adidas Superstar 2 W EUR 36</PRODUCT><DESCRIPTION>V rámci kolekcie Originals uvádza adidas športovú obuv The Superstar, ktorá je už od svojho vzniku jedničkou medzi obuvou. Jej poznávacím znamením je mimo iných detailov designové zakončenie špičky. Vďaka kvalitnému materiálu a trendy vzhľadu, podčiarknutého logami Adidas vo vnútri topánky aj na nej, bude hviezdou vášho botníku.</DESCRIPTION><URL>http://www.obchod-s-obuvou.sk/topanky/adidas-superstar-2-w7/eur-36/</URL><IMGURL>http://www.obchod-s-obuvou.sk/pictures/403078.jpg</IMGURL><IMGURL_ALTERNATIVE>http://www.obchod-s-obuvi.sk/pictures/403080.jpg</IMGURL_ALTERNATIVE><PRICE_VAT>240</PRICE_VAT><PARAM><PARAM_NAME>Farba</PARAM_NAME><VAL>čierna</VAL></PARAM><MANUFACTURER>Adidas</MANUFACTURER><CATEGORYTEXT>Obuv | Dámska obuv</CATEGORYTEXT><EAN>5051571703857</EAN><HEUREKA_CPC>0.24</HEUREKA_CPC><DELIVERY_DATE>2</DELIVERY_DATE><PRODUCTNO>G43755</PRODUCTNO><DELIVERY><DELIVERY_ID>SLOVENSKA_POSTA</DELIVERY_ID><DELIVERY_PRICE>3</DELIVERY_PRICE><DELIVERY_PRICE_COD>5</DELIVERY_PRICE_COD></DELIVERY><DELIVERY><DELIVERY_ID>PPL</DELIVERY_ID><DELIVERY_PRICE>3</DELIVERY_PRICE><DELIVERY_PRICE_COD>5</DELIVERY_PRICE_COD></DELIVERY><ITEMGROUP_ID>EF789</ITEMGROUP_ID><ACCESSORY>CD456</ACCESSORY><GIFT>Púzdro zadarmo</GIFT><EXTENDED_WARRANTY><VAL>36</VAL><DESC>Záruka na 36 mesiacov</DESC></EXTENDED_WARRANTY><SPECIAL_SERVICE>Aplikácia ochrannej fólie</SPECIAL_SERVICE></SHOPITEM></SHOP>

462
src/main.rs Normal file
View file

@ -0,0 +1,462 @@
use std::str::FromStr;
use fake::{Dummy, Fake, faker::lorem::en::Word, faker::lorem::en::Words, faker::name::en::Name};
use fake::{Faker, Rng};
use serde::Serialize;
use url::Url;
#[derive(Serialize, Dummy, Clone, Debug)]
#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))]
#[serde(rename = "SHOP")]
struct Shop {
#[dummy(expr = "fake::vec![ShopItem;0..1000]")]
#[serde(rename = "SHOPITEM")]
shop_item: Vec<ShopItem>,
}
struct UrlFaker;
impl Dummy<UrlFaker> for url::Url {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &UrlFaker, rng: &mut R) -> Self {
let domain: Vec<String> = Words(4..20).fake_with_rng(rng);
let mut url = "https://".into();
let path_length: usize = rand::random_range(1..15);
let subdomain_length: usize = rand::random_range(2..4);
domain.into_iter().enumerate().for_each(|(i, w)| match i {
0 => url = format!("{url}{w}"),
i if (0..subdomain_length).contains(&i) => url = format!("{url}.{w}"),
i if (subdomain_length..path_length).contains(&i) => url = format!("{url}/{w}"),
i if (path_length == i) => url = format!("{url}?{i}={w}"),
_ => url = format!("{url}&{i}={w}"),
});
Url::from_str(&url).unwrap()
}
fn dummy(_config: &UrlFaker) -> Self {
let domain: Vec<String> = Words(4..20).fake();
let mut url = "https://".into();
let path_length: usize = rand::random_range(1..15);
let subdomain_length: usize = rand::random_range(2..4);
domain.into_iter().enumerate().for_each(|(i, w)| match i {
0 => url = format!("{url}{w}"),
i if (0..subdomain_length).contains(&i) => url = format!("{url}.{w}"),
i if (subdomain_length..path_length).contains(&i) => url = format!("{url}/{w}"),
i if (path_length == i) => url = format!("{url}?{i}={w}"),
_ => url = format!("{url}&{i}={w}"),
});
Url::from_str(&url).unwrap()
}
}
impl Dummy<UrlFaker> for Vec<url::Url> {
fn dummy(config: &UrlFaker) -> Self {
vec![config.fake(); rand::random_range(1..20)]
}
fn dummy_with_rng<R: Rng + ?Sized>(config: &UrlFaker, rng: &mut R) -> Self {
vec![config.fake_with_rng(rng); rand::random_range(1..5)]
}
}
#[derive(Serialize, Dummy, Clone, Debug)]
#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))]
#[serde(rename = "SHOPITEM")]
///Guidelines to filling these out: https://sluzby.heureka.sk/napoveda/xml-feed/
/// All text should serialize like:
/// ```
/// <MANUFACTURER><![CDATA[Black & Decker]]></MANUFACTURER>
/// ```
struct ShopItem {
/// only [ _ - 0-9 a-z A-Z ]
#[dummy(faker = "fake::uuid::UUIDv7")]
item_id: String,
/// max 200 char
#[dummy(faker = "Name()")]
productname: String,
#[dummy(faker = "Name()")]
#[serde(skip_serializing_if = "Option::is_none")]
product: Option<String>,
#[dummy(faker = "Name()")]
#[serde(skip_serializing_if = "Option::is_none")]
/// limits to 200 char displayed at once
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[dummy(faker = "UrlFaker")]
/// max 300 char
url: Option<url::Url>,
#[dummy(faker = "UrlFaker")]
/// max 255 char
imgurl: url::Url,
/// max 255 char
#[dummy(faker = "UrlFaker")]
imgurl_alternative: Vec<url::Url>,
#[dummy(faker = "UrlFaker")]
#[serde(skip_serializing_if = "Option::is_none")]
/// only youtube.com
video_url: Option<url::Url>,
/// max 2 decimal places, if in czk has to be rounded to full number for marketplace
price_vat: rust_decimal::Decimal,
#[dummy(expr = "rand::random_bool(0.5).then(|| (0..100).fake::<usize>().to_string() + \"%\")")]
#[serde(skip_serializing_if = "Option::is_none")]
/// eg. 21%
vat: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// For items that aren't new, maybe there's an enum for this?
item_type: Option<String>,
param: Vec<Param>,
#[serde(skip_serializing_if = "Option::is_none")]
manufacturer: Option<String>,
/// download xml from here and typecheck? https://www.heureka.sk/direct/xml-export/shops/heureka-sekce.xml
categorytext: String,
#[serde(skip_serializing_if = "Option::is_none")]
/// typecheck?
ean: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// typecheck?
isbn: Option<String>,
// #[dummy(faker = "fake::faker::decimal")]
#[serde(skip_serializing_if = "Option::is_none")]
/// max 2 decimal places, max 50.00€ if in czk has to be rounded to full number for marketplace
heureka_cpc: Option<rust_decimal::Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
/// either number in days or a datetime
/**
skladom - 0
do 3 dní - 1-3
do týždňa - 4-7
do 2 týždňov - 8-14
do mesiaca - 15-30
viac ako mesiac - 31 a viac
info v obchode - pokiaľ dodaciu dobu neuvádzate
**/
delivery_date: Option<DateOrDays>,
///
productno: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
delivery: Vec<Delivery>,
//TODO: WHERE IS IT IN THE DOCS? WHY IS IT GONE? IT'S IN THE SUPPORT MAIL WHAT
//Was probably a vec<String>?
#[dummy(faker = "fake::uuid::UUIDv7")]
/// max 36 char, [ _ - 0-9 a-z A-Z ]
itemgroup_id: String,
/// ITEM_ID, ref to other shopItem.item_id
#[serde(skip_serializing_if = "Vec::is_empty")]
accessory: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Total additional costs, with TAX/DPH
dues: Option<rust_decimal::Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
///Pair with gift_id
gift: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
gift_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
extended_warranty: Option<ExtendedWarranty>,
#[dummy(faker = "Words(0..5)")]
///Max 5 of these
#[serde(skip_serializing_if = "Vec::is_empty")]
special_service: Vec<String>,
}
#[derive(Serialize, Dummy, Clone, Debug)]
#[serde(untagged)]
enum DateOrDays {
Date(chrono::NaiveDate),
Days(u16),
}
#[derive(Serialize, Dummy, Clone, Debug)]
#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))]
struct Param {
/// I refuse to make this an enum, options here: https://docs.google.com/spreadsheets/d/e/2PACX-1vROYv0vyQXMg7c7Xu5fRTCr1fXlhWaGqRsCtST7-2jy0zQBDcSkvkqO1qawTywbQe8Xd2rPtFiMSjQR/pubhtml?gid=1459300428&single=true
#[dummy(expr = "\"farba\".into()")]
param_name: String,
#[dummy(faker = "Word()")]
val: String,
}
/// impl Dummy<Name> for &'static str {
/// fn dummy_with_rng<R: Rng + ?Sized>(_: &Name, rng: &mut R) -> &'static str {
/// const NAMES: &[&str] = &["John Doe", "Jane Doe"];
/// NAMES.choose(rng).unwrap()
/// }
/// }
#[derive(Serialize, Dummy, Clone, Debug)]
#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))]
struct Delivery {
delivery_id: DeliveryCourierId,
/// Incl. TAX/DPH
delivery_price: rust_decimal::Decimal,
/// Incl. TAX/DPH of delivery price and COD tax
delivery_price_cod: rust_decimal::Decimal,
}
#[derive(Serialize, Dummy, Clone, Debug)]
#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))]
pub enum DeliveryCourierId {
SlovenskaPosta,
CeskaPosta,
CeskaPostaDoporucenaZasilka,
CsadLogistikOstrava,
#[serde(rename = "DPD")]
DPD,
#[serde(rename = "DHL")]
DHL,
#[serde(rename = "DSV")]
DSV,
#[serde(rename = "FOFR")]
FOFR,
ExpresKurier,
GebruderWeiss,
Geis,
#[serde(rename = "GLS")]
GLS,
#[serde(rename = "HDS")]
HDS,
ExpressOne,
#[serde(rename = "PPL")]
PPL,
Seegmuller,
#[serde(rename = "TNT")]
TNT,
Toptrans,
#[serde(rename = "UPS")]
UPS,
Fedex,
RabenLogistics,
ZasilkovnaNaAdresu,
#[serde(rename = "SDS")]
SDS,
#[serde(rename = "SPS")]
SPS,
///123KURIER
#[serde(rename = "123KURIER")]
JednaDvaTriKurier,
PacketaDomov,
PaletExpress,
WedoHome,
RhenusLogistics,
Messenger,
#[serde(rename = "SLOVENSKA_POSTA_NAPOSTU_DEPOTAPI")]
SlovenskaPostaNapostuDEPOTAPI,
Zasilkovna,
#[serde(rename = "BALIKOVNA_DEPOTAPI")]
BalikovnaDEPOTAPI,
Packeta,
DpdPickup,
WedoPoint,
Balikovo,
CeskaPostaNapostu,
PplParcelshop,
GlsParcelshop,
Depo,
Alzapoint,
DpdBox,
ZBox,
WedoBox,
BalikovnaBox,
BalikoBox,
GlsParcellocker,
Alzabox,
Online,
VlastnaPreprava,
VlastniPreprava,
}
#[derive(Serialize, Dummy, Clone, Debug)]
#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))]
struct ExtendedWarranty {
///in months, above 999 months is lifetime warranty
val: u16,
desc: String,
}
fn main() {
let shop = Faker.fake::<Shop>();
let mut xml = String::new();
let mut s = quick_xml::se::Serializer::new(&mut xml);
s.indent(' ', 4);
shop.serialize(s).unwrap();
std::fs::write("a.xml", &xml).unwrap();
}
#[cfg(test)]
mod test {
use super::*;
use pyo3::ffi::c_str;
use pyo3::prelude::*;
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive;
pub fn validate_xml(xml: &str) {
Python::with_gil(|py| {
pyo3::prepare_freethreaded_python();
let activators = PyModule::from_code(
py,
c_str!(
r#"
import xmlschema
import sys
def validate(schema, xml):
try:
schema = xmlschema.XMLSchema11(schema)
schema.validate(xml)
return
except Exception as e:
typ = type(e).__name__
msg = str(e)
return f"{typ}: {msg}"
"#
),
c_str!("activators.py"),
c_str!("activators"),
)
.unwrap();
let validation_result: Option<String> = activators
.getattr("validate")
.unwrap()
.call(("./heureka-feed-2.0.xsd", xml), None)
.unwrap()
.extract()
.unwrap();
if let Some(e) = validation_result {
panic!("{e}")
}
})
}
#[test]
fn xsd_schema_is_ok_and_heureka_example_passes() {
const HEUREKA_XML: &str = r###"<?xml version="1.0" encoding="utf-8"?>
<SHOP>
<SHOPITEM>
<ITEM_ID>AB123</ITEM_ID>
<PRODUCTNAME>Nokia 5800 XpressMusic</PRODUCTNAME>
<PRODUCT>Nokia 5800 XpressMusic</PRODUCT>
<DESCRIPTION>Klasický s plným dotykovým uživatelským rozhraním</DESCRIPTION>
<URL>http://obchod.cz/mobily/nokia-5800-xpressmusic</URL>
<IMGURL>http://obchod.cz/mobily/nokia-5800-xpressmusic/obrazek.jpg</IMGURL>
<IMGURL_ALTERNATIVE>http://obchod.cz/mobily/nokia-5800-xpressmusic/obrazek2.jpg</IMGURL_ALTERNATIVE>
<PRICE_VAT>6000</PRICE_VAT>
<HEUREKA_CPC>5.8</HEUREKA_CPC>
<MANUFACTURER>NOKIA</MANUFACTURER>
<CATEGORYTEXT>Elektronika | Mobilní telefony</CATEGORYTEXT>
<EAN>6417182041488</EAN>
<PRODUCTNO>RM-559394</PRODUCTNO>
<PARAM>
<PARAM_NAME>Barva</PARAM_NAME>
<VAL>černá</VAL>
</PARAM>
<DELIVERY_DATE>2</DELIVERY_DATE>
<DELIVERY>
<DELIVERY_ID>CESKA_POSTA</DELIVERY_ID>
<DELIVERY_PRICE>120</DELIVERY_PRICE>
<DELIVERY_PRICE_COD>120</DELIVERY_PRICE_COD>
</DELIVERY>
<DELIVERY>
<DELIVERY_ID>PPL</DELIVERY_ID>
<DELIVERY_PRICE>90</DELIVERY_PRICE>
<DELIVERY_PRICE_COD>120</DELIVERY_PRICE_COD>
</DELIVERY>
<ACCESSORY>CD456</ACCESSORY>
<GIFT>Pouzdro zdarma</GIFT>
<EXTENDED_WARRANTY>
<VAL>36</VAL>
<DESC>Záruka na 36 měsíců</DESC>
</EXTENDED_WARRANTY>
<SPECIAL_SERVICE>Aplikace ochranné fólie</SPECIAL_SERVICE>
<SALES_VOUCHER>
<CODE>SLEVA20</CODE>
<DESC>Sleva 20% po zadání kódu do 31.12.2021!</DESC>
</SALES_VOUCHER>
</SHOPITEM>
</SHOP>"###;
validate_xml(HEUREKA_XML);
}
#[test]
fn heureka_example_serializes_and_validates() {
use super::*;
let stuff = Shop {
shop_item: vec![ShopItem {
item_id: "325236407".into(),
productname: "Adidas Superstar 2 W EUR 36".into(),
product: Some("Adidas Superstar 2 W EUR 36".into(),),
description: Some(
"V rámci kolekcie Originals uvádza adidas športovú obuv The Superstar, ktorá je už od svojho vzniku jedničkou medzi obuvou. Jej poznávacím znamením je mimo iných detailov designové zakončenie špičky. Vďaka kvalitnému materiálu a trendy vzhľadu, podčiarknutého logami Adidas vo vnútri topánky aj na nej, bude hviezdou vášho botníku.".into(),
),
url: Some(
Url::parse("http://www.obchod-s-obuvou.sk/topanky/adidas-superstar-2-w7/eur-36/")
.unwrap(),
),
imgurl: Url::parse("http://www.obchod-s-obuvou.sk/pictures/403078.jpg").unwrap(),
imgurl_alternative: vec![
Url::parse("http://www.obchod-s-obuvi.sk/pictures/403080.jpg").unwrap(),
],
price_vat: Decimal::from_u8(240).unwrap(),
heureka_cpc: Some(Decimal::from_f32(0.24)).unwrap(),
manufacturer: Some("Adidas".into()),
categorytext: "Obuv | Dámska obuv".into(),
ean: Some("5051571703857".into()),
productno: "G43755".into(),
param: vec![Param {
param_name: "Farba".into(),
val: "čierna".into(),
}],
delivery_date: Some(DateOrDays::Days(0)),
delivery: vec![
Delivery {
delivery_id: DeliveryCourierId::SlovenskaPosta,
delivery_price_cod: Decimal::from_u8(5).unwrap(),
delivery_price: Decimal::from_u8(3).unwrap(),
},
Delivery {
delivery_id: DeliveryCourierId::PPL,
delivery_price_cod: Decimal::from_u8(5).unwrap(),
delivery_price: Decimal::from_u8(3).unwrap(),
},
],
itemgroup_id: "EF789".into(),
accessory: vec!["CD456".into()],
gift: Some("Púzdro zadarmo".into()),
extended_warranty: Some(ExtendedWarranty {
val: 36,
desc: "Záruka na 36 mesiacov".into(),
}),
special_service: vec!["Aplikácia ochrannej fólie".into()],
vat: None,
isbn: None,
dues: None,
gift_id: None,
video_url: None,
item_type: None,
}],
};
let mut xml = String::new();
let mut s = quick_xml::se::Serializer::new(&mut xml);
s.indent(' ', 4);
stuff.serialize(s).unwrap();
// std::fs::write("output.xml", &xml).unwrap();
validate_xml(&xml);
}
#[test]
fn manymany_shops_validate() {
let mut i = 0;
loop {
i += 1;
if i < 1_000_000 {
break;
}
let mut buf = String::new();
let mut s = quick_xml::se::Serializer::new(&mut buf);
s.indent(' ', 4);
fake::Faker.fake::<Shop>().serialize(s).unwrap();
validate_xml(&buf);
}
}
}