passing all tests
This commit is contained in:
commit
17d20eef66
8 changed files with 50852 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1714
Cargo.lock
generated
Normal file
1714
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
32
Cargo.toml
Normal file
32
Cargo.toml
Normal 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"] }
|
44
heureka-example.xml
Normal file
44
heureka-example.xml
Normal 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
202
heureka-feed-2.0.xsd
Normal 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
1
output.xml
Normal 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
462
src/main.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue