feat: Add Store avro

This commit is contained in:
ldrogo27
2026-04-07 12:44:18 +02:00
parent ca4aa96565
commit 28a8f1c8c1
14 changed files with 1357 additions and 130 deletions

4
.gitignore vendored
View File

@@ -1 +1,5 @@
/target
.DS_Store
.idea
Cargo.lock

112
Cargo.lock generated
View File

@@ -93,9 +93,9 @@ checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "apache-avro"
version = "0.19.0"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce74162d8bc5bc22bd746e1849fb33f2c9416d2ce505d1d65ba49f170a207c90"
checksum = "36fa98bc79671c7981272d91a8753a928ff6a1cd8e4f20a44c45bd5d313840bf"
dependencies = [
"bigdecimal",
"bon",
@@ -111,7 +111,7 @@ dependencies = [
"serde_json",
"strum",
"strum_macros",
"thiserror 2.0.15",
"thiserror 2.0.18",
"uuid",
]
@@ -156,9 +156,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bigdecimal"
version = "0.4.8"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013"
checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695"
dependencies = [
"autocfg",
"libm",
@@ -166,7 +166,6 @@ dependencies = [
"num-integer",
"num-traits",
"serde",
"serde_json",
]
[[package]]
@@ -186,9 +185,9 @@ dependencies = [
[[package]]
name = "bon"
version = "3.7.1"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537c317ddf588aab15c695bf92cf55dec159b93221c074180ca3e0e5a94da415"
checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe"
dependencies = [
"bon-macros",
"rustversion",
@@ -196,9 +195,9 @@ dependencies = [
[[package]]
name = "bon-macros"
version = "3.7.1"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca5abbf2d4a4c6896197c9de13d6d7cb7eff438c63dacde1dde980569cb00248"
checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c"
dependencies = [
"darling",
"ident_case",
@@ -957,9 +956,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.27"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
@@ -1265,9 +1264,9 @@ dependencies = [
[[package]]
name = "rdkafka"
version = "0.38.0"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f1856d72dbbbea0d2a5b2eaf6af7fb3847ef2746e883b11781446a51dbc85c0"
checksum = "d7956f9ac12b5712e50372d9749a3102f4810a8d42481c5eae3748d36d585bcf"
dependencies = [
"futures-channel",
"futures-util",
@@ -1283,9 +1282,9 @@ dependencies = [
[[package]]
name = "rdkafka-sys"
version = "4.9.0+2.10.0"
version = "4.10.0+2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5230dca48bc354d718269f3e4353280e188b610f7af7e2fcf54b7a79d5802872"
checksum = "e234cf318915c1059d4921ef7f75616b5219b10b46e9f3a511a15eb4b56a3f77"
dependencies = [
"libc",
"libz-sys",
@@ -1304,15 +1303,15 @@ dependencies = [
[[package]]
name = "regex-lite"
version = "0.1.6"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
[[package]]
name = "reqwest"
version = "0.12.23"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
dependencies = [
"base64",
"bytes",
@@ -1331,7 +1330,6 @@ dependencies = [
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-native-tls",
@@ -1395,9 +1393,9 @@ dependencies = [
[[package]]
name = "schema_registry_converter"
version = "4.5.0"
version = "4.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a88502ff545fb0501e0418f06e58936667b1bf99c15b4c7cdd769a917de6c3a2"
checksum = "67d077e1c21a26fb61a65460945db7cedd14e0865ae999abf51ff68ee383e54e"
dependencies = [
"apache-avro",
"byteorder",
@@ -1439,27 +1437,38 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.219"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.17"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96"
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -1468,26 +1477,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.143"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@@ -1623,11 +1621,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.15"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl 2.0.15",
"thiserror-impl 2.0.18",
]
[[package]]
@@ -1643,9 +1641,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.15"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
@@ -1737,9 +1735,9 @@ dependencies = [
[[package]]
name = "tower-http"
version = "0.6.6"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags",
"bytes",
@@ -1827,12 +1825,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.0"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"js-sys",
"serde",
"serde_core",
"wasm-bindgen",
]
@@ -2244,3 +2242,9 @@ dependencies = [
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -12,8 +12,8 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml2 = "0.1"
rdkafka = { version = "0.38", features = ["tokio"] }
schema_registry_converter = { version = "4", features = ["avro"] }
apache-avro = "0.19"
rdkafka = { version = "0.39", features = ["tokio"] }
schema_registry_converter = { version = "4.8.0", features = ["avro"] }
apache-avro = "0.21"
anyhow = "1"
tokio = { version = "1", features = ["full"] }

View File

@@ -1,11 +1,16 @@
kafka:
brokers: "localhost:9092"
schema_registry: "http://localhost:9093/apis/ccompat/v6"
security:
protocol: "PLAINTEXT"
schemas:
users:
file: "schemas/stores.avsc"
orders:
file: "schemas/link.avsc"
third:
file: "schemas/supplyThirdReferential.avsc"
link:
file: "schemas/supplyThirdlink.avsc"
detail:
file: "schemas/supplyStoreDetail.avsc"
store:
file: "schemas/store.avsc"

2
detail.csv Normal file
View File

@@ -0,0 +1,2 @@
store_third_type,store_third_id,store_third_sub_id,sap_store_id,linear_size,opening_date,river_id,logistic_area
007,00839,00839,0839,3973,2014-09-05,839,00038
1 store_third_type store_third_id store_third_sub_id sap_store_id linear_size opening_date river_id logistic_area
2 007 00839 00839 0839 3973 2014-09-05 839 00038

2
link.csv Normal file
View File

@@ -0,0 +1,2 @@
parent_third_type,parent_third_id,parent_third_sub_id,third_type,third_id,third_sub_id,link_id,creation_date
007,10460,10460,007,43386,43386,500,2025-08-29
1 parent_third_type parent_third_id parent_third_sub_id third_type third_id third_sub_id link_id creation_date
2 007 10460 10460 007 43386 43386 500 2025-08-29

415
schemas/store.avsc Normal file
View File

@@ -0,0 +1,415 @@
{
"type": "record",
"name": "StoreAvro",
"namespace": "com.decathlon.onestore.avro",
"fields": [
{
"name": "thirdType",
"type": "int"
},
{
"name": "thirdNum",
"type": "int"
},
{
"name": "name",
"type": "string"
},
{
"name": "country",
"type": [
"null",
{
"type": "record",
"name": "CountryAvro",
"fields": [
{
"name": "countryCode",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "countryName",
"type": [
"null",
"string"
],
"default": null
}
]
}
],
"default": null
},
{
"name": "fiscalCompanyNumber",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "eanCode",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "address",
"type": {
"type": "record",
"name": "AddressAvro",
"fields": [
{
"name": "street",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "addressComplement",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "zipCode",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "city",
"type": "string"
},
{
"name": "administrativeAreaLevel1",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "administrativeAreaLevel2",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "administrativeAreaLevel3",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "administrativeAreaLevel4",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "administrativeAreaLevel5",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "latitude",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "longitude",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "timezone",
"type": [
"null",
"string"
],
"default": null
}
]
}
},
{
"name": "phones",
"type": {
"type": "record",
"name": "PhonesAvro",
"fields": [
{
"name": "phone1",
"type": {
"type": "record",
"name": "PhoneAvro",
"fields": [
{
"name": "label",
"type": "string"
},
{
"name": "index",
"type": [
"null",
"int"
],
"default": null
},
{
"name": "number",
"type": "string"
}
]
}
},
{
"name": "phone2",
"type": "PhoneAvro"
}
]
}
},
{
"name": "genericStoreType",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "category",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "language",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "status",
"type": "string"
},
{
"name": "franchised",
"type": "boolean",
"default": false
},
{
"name": "openingDate",
"type": [
"null",
{
"type": "int",
"logicalType": "date"
}
],
"default": null
},
{
"name": "workingHours",
"type": {
"type": "array",
"items": {
"type": "record",
"name": "WorkingHourAvro",
"fields": [
{
"name": "day",
"type": "string"
},
{
"name": "status",
"type": "string"
},
{
"name": "opens",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "closes",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "dividedCloses",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "dividedOpens",
"type": [
"null",
"string"
],
"default": null
}
]
}
},
"default": []
},
{
"name": "leader",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "socialMedias",
"type": {
"type": "array",
"items": {
"type": "record",
"name": "SocialMediaAvro",
"fields": [
{
"name": "name",
"type": "string"
},
{
"name": "link",
"type": "string"
}
]
}
},
"default": []
},
{
"name": "publishOnEcommerce",
"type": "boolean"
},
{
"name": "picture",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "welcomeMessages",
"type": {
"type": "array",
"items": {
"type": "record",
"name": "WelcomeMessageAvro",
"fields": [
{
"name": "language",
"type": "string"
},
{
"name": "message",
"type": "string"
}
]
}
},
"default": []
},
{
"name": "exceptionalHours",
"type": {
"type": "array",
"items": {
"type": "record",
"name": "ExceptionalHourAvro",
"fields": [
{
"name": "date",
"type": "string"
},
{
"name": "status",
"type": "string"
},
{
"name": "opens",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "closes",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "dividedCloses",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "dividedOpens",
"type": [
"null",
"string"
],
"default": null
}
]
}
},
"default": []
}
]
}

View File

@@ -0,0 +1,82 @@
{
"doc": "Legacy Store data",
"domain": "Supply",
"fields": [
{
"doc": "Third type",
"name": "store_third_type",
"type": "string"
},
{
"doc": "Third Id",
"name": "store_third_id",
"type": "string"
},
{
"doc": "Third sub id",
"name": "store_third_sub_id",
"type": "string"
},
{
"doc": "Store Sap Identifier",
"name": "sap_store_id",
"type": "string"
},
{
"default": null,
"doc": "Linear Size",
"name": "linear_size",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Opening Date",
"name": "opening_date",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "River Identifier",
"name": "river_id",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Logistic arera",
"name": "logistic_area",
"type": [
"null",
"string"
]
},
{
"default": null,
"name": "store_type",
"type": [
"null",
"string"
]
},
{
"default": null,
"name": "store_sign",
"type": [
"null",
"string"
]
}
],
"name": "SupplyStoreDetail",
"namespace": "com.decathlon.vct.vcstream.avro.supplystoredetail",
"type": "record",
"version": "1"
}

View File

@@ -0,0 +1,54 @@
{
"doc": "",
"domain": "Supply",
"fields": [
{
"doc": "parent third type",
"name": "parent_third_type",
"type": "string"
},
{
"doc": "parent_third_id",
"name": "parent_third_id",
"type": "string"
},
{
"doc": "Parent third sub Id",
"name": "parent_third_sub_id",
"type": "string"
},
{
"doc": "third type",
"name": "third_type",
"type": "string"
},
{
"doc": "third Id",
"name": "third_id",
"type": "string"
},
{
"doc": "third sub Id",
"name": "third_sub_id",
"type": "string"
},
{
"doc": "link between 2 thirds",
"name": "link_id",
"type": "string"
},
{
"default": null,
"doc": "Creation Date",
"name": "creation_date",
"type": [
"null",
"string"
]
}
],
"name": "SupplyThirdlink",
"namespace": "com.decathlon.vct.vcstream.avro.supplythirdlink",
"type": "record",
"version": "1"
}

View File

@@ -0,0 +1,300 @@
{
"doc": "Main Third data",
"domain": "Supply",
"fields": [
{
"doc": "Third type",
"name": "third_type",
"type": "string"
},
{
"doc": "Third Id",
"name": "third_id",
"type": "string"
},
{
"doc": "Third sub Id",
"name": "third_sub_id",
"type": "string"
},
{
"doc": "Supplier Name",
"name": "legal_name",
"type": "string"
},
{
"default": null,
"doc": "Country Code",
"name": "country_code",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Currency for Purchase Order",
"name": "order_currency",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Is this material internal ?",
"name": "is_internal",
"type": [
"null",
"boolean"
]
},
{
"default": null,
"doc": "SIREN Code",
"name": "tax_number",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Code identifier for VAT",
"name": "vat_number",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Vendor GLN Code",
"name": "third_gln",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Sort Name",
"name": "sort_name",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "CNUF Code",
"name": "cnuf_code",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Street description",
"name": "street",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Address Complement",
"name": "address_complement",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "ZIP Code",
"name": "zip_code",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "City",
"name": "city",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Phone number",
"name": "phone_number",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Fax number",
"name": "fax_number",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Rivers Id (obsolete)",
"name": "rivers_id",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Is this material autonomous ?",
"name": "is_autonomous",
"type": [
"null",
"boolean"
]
},
{
"default": null,
"doc": "define the vendor type (specific Brazil)",
"name": "is_linked_to_dp",
"type": [
"null",
"boolean"
]
},
{
"default": null,
"doc": "is it a subcontractor?",
"name": "is_subcontractor",
"type": [
"null",
"boolean"
]
},
{
"default": null,
"doc": "Fiscal company number",
"name": "fiscal_company_number",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Accounting Entity",
"name": "accounting_entity",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Cost center",
"name": "cost_center",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "jde Code",
"name": "jde_code",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "treeview organization",
"name": "treeview_organization",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "treeview level",
"name": "treeview_level",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "treeview number",
"name": "treeview_number",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Creation date",
"name": "creation_date",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Modification date",
"name": "modification_date",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Is this third closed ?",
"name": "is_closed",
"type": [
"null",
"boolean"
]
},
{
"default": null,
"doc": "language code",
"name": "language_code",
"type": [
"null",
"string"
]
},
{
"default": null,
"doc": "Logistic area",
"name": "logistic_area",
"type": [
"null",
"string"
]
}
],
"name": "SupplyThirdReferential",
"namespace": "com.decathlon.vct.vcstream.avro.supplythirdreferential",
"type": "record",
"version": "1"
}

View File

@@ -1,10 +1,9 @@
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Deserialize)]
pub struct AppConfig {
pub kafka: KafkaConfig,
pub schemas: HashMap<String, SchemaConfig>,
// pub schemas: HashMap<String, SchemaConfig>,
}
#[derive(Debug, Deserialize)]
@@ -13,7 +12,7 @@ pub struct KafkaConfig {
pub schema_registry: String,
}
#[derive(Debug, Deserialize)]
pub struct SchemaConfig {
pub file: String,
}
// #[derive(Debug, Deserialize)]
// pub struct SchemaConfig {
// pub file: String,
// }

View File

@@ -1,47 +1,50 @@
mod registry;
mod config;
mod registry;
use clap::Parser;
use csv::ReaderBuilder;
use schema_registry_converter::async_impl::avro::AvroEncoder;
use schema_registry_converter::async_impl::schema_registry::SrSettings;
use serde::Deserialize;
use std::fs;
use anyhow::{Context, Ok};
use clap::{Parser, ValueEnum};
use serde::de::DeserializeOwned;
use std::{env, fs};
use crate::config::AppConfig;
use crate::registry::{ChatMessage, KafkaProducer};
use crate::registry::{
AddressAvro, CountryAvro, Detail, KafkaProducer, Link, PhoneAvro, PhonesAvro, Store, StoreAvro,
WorkingHourAvro,
};
/// Énumération pour un choix de topic sécurisé via la ligne de commande.
#[derive(ValueEnum, Clone, Debug)]
#[clap(rename_all = "kebab_case")]
enum Topic {
Store,
Third,
Detail,
Link,
}
/// CLI tool to send CSV rows as Avro messages to Kafka
#[derive(Parser, Debug)]
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Path to the config file (YAML)
#[arg(short, long, default_value = "config.yaml")]
config: String,
/// Path to the CSV file
#[arg(short, long)]
csv: String,
#[arg(long)]
csv: Option<String>,
/// Kafka topic to produce to
#[arg(short, long)]
topic: String,
/// Schema Registry URL (Apicurio or Confluent compatible)
#[arg(short = 'r', long, default_value = "http://localhost:8080/apis/registry/v2/groups/default/subjects")]
registry_url: String,
}
#[derive(Debug, Deserialize)]
struct User {
id: i32,
name: String,
#[arg(value_enum)]
topic: Topic,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// this method needs to be inside main() method
unsafe {
env::set_var("RUST_BACKTRACE", "1");
}
let args = Args::parse();
// Charger la configuration
@@ -49,21 +52,195 @@ async fn main() -> anyhow::Result<()> {
let cfg: AppConfig = serde_yaml2::from_str(&cfg_str).unwrap();
// 1. Kafka Producer
let producer: KafkaProducer = KafkaProducer::new(cfg.kafka.brokers, cfg.kafka.schema_registry, args.topic);
let producer: KafkaProducer = KafkaProducer::new(cfg.kafka.brokers, cfg.kafka.schema_registry);
// 2. Lecture CSV
let mut rdr = ReaderBuilder::new()
.has_headers(true)
.from_path(&args.csv)?;
let payload = StoreAvro {
third_type: 7,
third_num: 77777,
name: "Decathlon Lille".to_string(),
country: Some(CountryAvro {
country_code: Some("FR".to_string()),
country_name: Some("France".to_string()),
}),
fiscal_company_number: Some("200".to_string()),
ean_code: Some("3020913027281".to_string()),
address: AddressAvro {
street: Some("1 Avenue Frederic et Irene Joliot-Curie PLACE DE LA BOULE".to_string()),
address_complement: None,
zip_code: Some("59000".to_string()),
city: "Lille".to_string(),
administrative_area_level1: None,
administrative_area_level2: None,
administrative_area_level3: None,
administrative_area_level4: None,
administrative_area_level5: None,
latitude: Some("48.888112987144".to_string()),
longitude: Some("2.2013429927292".to_string()),
timezone: Some("Europe/Paris".to_string()),
},
phones: PhonesAvro {
phone1: PhoneAvro {
label: "main".into(),
index: Some(1),
number: "0102030405".into(),
},
phone2: PhoneAvro {
label: "alt".into(),
index: Some(2),
number: "0607080910".into(),
},
},
generic_store_type: None,
category: None,
language: Some("fr".into()),
status: "OPEN".into(),
franchised: false,
opening_date: None,
working_hours: vec![
WorkingHourAvro {
day: "MONDAY".into(),
status: "Closed".into(),
opens: None,
closes: None,
divided_opens: None,
divided_closes: None,
},
WorkingHourAvro {
day: "TUESDAY".into(),
status: "Closed".into(),
opens: None,
closes: None,
divided_opens: None,
divided_closes: None,
},
WorkingHourAvro {
day: "WEDNESDAY".into(),
status: "Closed".into(),
opens: None,
closes: None,
divided_opens: None,
divided_closes: None,
},
WorkingHourAvro {
day: "THURSDAY".into(),
status: "Closed".into(),
opens: None,
closes: None,
divided_opens: None,
divided_closes: None,
},
WorkingHourAvro {
day: "FRIDAY".into(),
status: "Closed".into(),
opens: None,
closes: None,
divided_opens: None,
divided_closes: None,
},
WorkingHourAvro {
day: "SATURDAY".into(),
status: "Closed".into(),
opens: None,
closes: None,
divided_opens: None,
divided_closes: None,
},
WorkingHourAvro {
day: "SUNDAY".into(),
status: "Closed".into(),
opens: None,
closes: None,
divided_opens: None,
divided_closes: None,
},
],
leader: Some("Vincent, Leader Magasin".to_string()),
social_medias: vec![],
publish_on_ecommerce: true,
picture: Some("https://onestore-cdn.decathlon.net/shop/117".to_string()),
welcome_messages: vec![],
exceptional_hours: vec![],
};
for result in rdr.deserialize() {
let chat: ChatMessage = result.unwrap();
match args.topic {
Topic::Third => {
let ok = producer
.produce(
"store-key-77777".to_string(),
payload.clone(),
"onestore_v2_store",
)
.await;
if ok {
println!("✅ Sent third {:?}", payload);
}
}
Topic::Store => {
let csv_path = args
.csv
.as_deref()
.context("L'option --csv est obligatoire pour ce topic")?;
// 3. Envoi Kafka
if producer.produce(chat.user.to_string(), chat.clone()).await {
println!("✅ Sent chat {:?}", chat);
let records = read_csv_file::<Store>(csv_path)?;
for store in records {
if producer
.produce("Store".to_string(), store.clone(), "third_referential")
.await
{
println!("✅ Sent store {:?}", store);
}
}
}
Topic::Link => {
let csv_path = args
.csv
.as_deref()
.context("L'option --csv est obligatoire pour ce topic")?;
let records = read_csv_file::<Link>(csv_path)?;
for link in records {
if producer
.produce("link".to_string(), link.clone(), "third_link")
.await
{
println!("✅ Sent link {:?}", link);
}
}
}
Topic::Detail => {
let csv_path = args
.csv
.as_deref()
.context("L'option --csv est obligatoire pour ce topic")?;
let records = read_csv_file::<Detail>(csv_path)?;
for detail in records {
if producer
.produce("Detail".to_string(), detail.clone(), "store_detail")
.await
{
println!("✅ Sent detail {:?}", detail);
} else {
println!("X Sent detail {:?}", detail);
}
}
}
}
Ok(())
}
fn read_csv_file<T>(file: &str) -> Result<Vec<T>, anyhow::Error>
where
T: DeserializeOwned,
{
let mut rdr = csv::Reader::from_path(file)
.with_context(|| format!("Impossible d'ouvrir le fichier CSV : {:?}", file))
.unwrap();
let mut values = Vec::new();
for result in rdr.deserialize() {
let record: T = result.unwrap();
values.push(record);
}
Ok(values)
}

View File

@@ -6,39 +6,220 @@ use schema_registry_converter::async_impl::schema_registry::SrSettings;
use schema_registry_converter::avro_common::get_supplied_schema;
use schema_registry_converter::schema_registry_common::SubjectNameStrategy;
use serde::{Deserialize, Serialize};
use std::fs;
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ChatMessage {
pub user: String,
pub message: String,
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Store {
pub third_type: String,
pub third_id: String,
pub third_sub_id: String,
pub legal_name: String,
pub country_code: String,
pub order_currency: String,
pub is_internal: Option<bool>,
pub tax_number: Option<String>,
pub vat_number: Option<String>,
pub third_gln: Option<String>,
pub sort_name: Option<String>,
pub cnuf_code: Option<String>,
pub street: Option<String>,
pub address_complement: Option<String>,
pub zip_code: Option<String>,
pub city: Option<String>,
pub phone_number: Option<String>,
pub fax_number: Option<String>,
pub rivers_id: Option<String>,
pub is_autonomous: Option<bool>,
pub is_linked_to_dp: Option<bool>,
pub is_subcontractor: Option<bool>,
pub fiscal_company_number: Option<String>,
pub accounting_entity: Option<String>,
pub cost_center: Option<String>,
pub jde_code: Option<String>,
pub treeview_organization: Option<String>,
pub treeview_level: Option<String>,
pub treeview_number: Option<String>,
pub creation_date: Option<String>,
pub modification_date: Option<String>,
pub is_closed: Option<bool>,
pub language_code: Option<String>,
pub logistic_area: Option<String>,
}
impl AvroSchema for ChatMessage {
fn get_schema() -> Schema {
// Define the Avro schema for ChatMessage
Schema::parse_str(
r#"{
"type": "record",
"name": "ChatMessage",
"fields": [
{"name": "user", "type": "string"},
{"name": "message", "type": "string"}
]
}"#
).unwrap()
impl AvroSchema for Store {
fn get_schema() -> apache_avro::schema::Schema {
let schema_path = "schemas/supplyThirdReferential.avsc";
let schema_json = fs::read_to_string(schema_path).unwrap();
Schema::parse_str(&schema_json).unwrap()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Link {
pub parent_third_type: String,
pub parent_third_id: String,
pub parent_third_sub_id: String,
pub third_type: String,
pub third_id: String,
pub third_sub_id: String,
pub link_id: String,
pub creation_date: Option<String>,
}
impl AvroSchema for Link {
fn get_schema() -> apache_avro::schema::Schema {
let schema_path = "schemas/supplyThirdLink.avsc";
let schema_json = fs::read_to_string(schema_path).unwrap();
Schema::parse_str(&schema_json).unwrap()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Detail {
pub store_third_type: String,
pub store_third_id: String,
pub store_third_sub_id: String,
pub sap_store_id: String,
pub linear_size: Option<String>,
pub opening_date: Option<String>,
pub river_id: Option<String>,
pub logistic_area: Option<String>,
pub store_type: Option<String>,
pub store_sign: Option<String>,
}
impl AvroSchema for Detail {
fn get_schema() -> apache_avro::schema::Schema {
let schema_path = "schemas/supplyStoreDetail.avsc";
let schema_json = fs::read_to_string(schema_path).unwrap();
Schema::parse_str(&schema_json).unwrap()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StoreAvro {
pub third_type: i32,
pub third_num: i32,
pub name: String,
pub country: Option<CountryAvro>,
pub fiscal_company_number: Option<String>,
pub ean_code: Option<String>,
pub address: AddressAvro,
pub phones: PhonesAvro,
pub generic_store_type: Option<String>,
pub category: Option<String>,
pub language: Option<String>,
pub status: String,
pub franchised: bool,
pub opening_date: Option<i32>, // logicalType date = int (jours depuis epoch)
pub working_hours: Vec<WorkingHourAvro>,
pub leader: Option<String>,
pub social_medias: Vec<SocialMediaAvro>,
pub publish_on_ecommerce: bool,
pub picture: Option<String>,
pub welcome_messages: Vec<WelcomeMessageAvro>,
pub exceptional_hours: Vec<ExceptionalHourAvro>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CountryAvro {
pub country_code: Option<String>,
pub country_name: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AddressAvro {
pub street: Option<String>,
pub address_complement: Option<String>,
pub zip_code: Option<String>,
pub city: String,
pub administrative_area_level1: Option<String>,
pub administrative_area_level2: Option<String>,
pub administrative_area_level3: Option<String>,
pub administrative_area_level4: Option<String>,
pub administrative_area_level5: Option<String>,
pub latitude: Option<String>,
pub longitude: Option<String>,
pub timezone: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PhonesAvro {
pub phone1: PhoneAvro,
pub phone2: PhoneAvro,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PhoneAvro {
pub label: String,
pub index: Option<i32>,
pub number: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkingHourAvro {
pub day: String,
pub status: String,
pub opens: Option<String>,
pub closes: Option<String>,
pub divided_closes: Option<String>,
pub divided_opens: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SocialMediaAvro {
pub name: String,
pub link: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WelcomeMessageAvro {
pub language: String,
pub message: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExceptionalHourAvro {
pub date: String,
pub status: String,
pub opens: Option<String>,
pub closes: Option<String>,
pub divided_closes: Option<String>,
pub divided_opens: Option<String>,
}
impl AvroSchema for StoreAvro {
fn get_schema() -> Schema {
let schema_json =
fs::read_to_string("schemas/store.avsc").expect("Impossible de lire schemas/store.avsc");
Schema::parse_str(&schema_json).expect("Schema Avro invalide")
}
}
pub struct KafkaProducer<'a> {
producer: FutureProducer,
#[warn(dead_code)]
avro_encoder: Arc<AvroEncoder<'a>>,
topic: String,
}
impl KafkaProducer<'_> {
pub fn new(bootstrap_servers: String, schema_registry_url: String, topic: String) -> Self {
pub fn new(bootstrap_servers: String, schema_registry_url: String) -> Self {
let producer: FutureProducer = ClientConfig::new()
.set("bootstrap.servers", bootstrap_servers)
.set("produce.offset.report", "true")
@@ -52,14 +233,16 @@ impl KafkaProducer<'_> {
Self {
producer,
topic,
avro_encoder: Arc::new(avro_encoder),
}
}
pub async fn produce<T: Serialize + apache_avro::AvroSchema>(&self, key: String, payload: T) -> bool {
let value_strategy = SubjectNameStrategy::TopicNameStrategyWithSchema(
self.topic.clone(),
pub async fn produce<T>(&self, key: String, payload: T, topic: &str) -> bool
where
T: Serialize + apache_avro::AvroSchema,
{
let value_strategy = SubjectNameStrategy::TopicNameStrategyWithSchema(
topic.to_string(),
true,
get_supplied_schema(&T::get_schema()),
);
@@ -73,9 +256,7 @@ impl KafkaProducer<'_> {
Err(e) => panic!("Error encoding payload: {}", e),
};
let record = FutureRecord::to(&self.topic)
.key(&key)
.payload(&payload_bytes);
let record = FutureRecord::to(topic).key(&key).payload(&payload_bytes);
match self.producer.send(record, Duration::from_secs(10)).await {
Ok(_) => true,

2
store.csv Normal file
View File

@@ -0,0 +1,2 @@
third_type,third_id,third_sub_id,legal_name,country_code,order_currency,is_internal,third_gln,sort_name,cnuf_code,street,zip_code,city,phone_number,fax_number,is_autonomous,is_linked_to_dp,fiscal_company_number,accounting_entity,cost_center,jde_code,treeview_organization,treeview_level,treeview_number,creation_date,modification_date,is_closed,language_code
007,01692,01692,ROTHSCHILD,IL,ILS,false,3020916685709,ROTHS,0000000000,36 Rothschild,65122,TEL AVIV,0,0,false,false,0024,1691,1034501691,0668570,0002,0006,00001,2019-01-04,2025-08-19,false,HE
1 third_type third_id third_sub_id legal_name country_code order_currency is_internal third_gln sort_name cnuf_code street zip_code city phone_number fax_number is_autonomous is_linked_to_dp fiscal_company_number accounting_entity cost_center jde_code treeview_organization treeview_level treeview_number creation_date modification_date is_closed language_code
2 007 01692 01692 ROTHSCHILD IL ILS false 3020916685709 ROTHS 0000000000 36 Rothschild 65122 TEL AVIV 0 0 false false 0024 1691 1034501691 0668570 0002 0006 00001 2019-01-04 2025-08-19 false HE