libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 39069b2097c0429e4c256736993a4fa38308b029
parent 3e11aa831c75819a7e26e5255c6141b860d50c72
Author: Antoine A <>
Date:   Fri, 24 Apr 2026 10:37:59 +0200

ebics: pain001 gen

Diffstat:
MCargo.lock | 19+++++++++++--------
Mlibeufin-nexus/sample/platform/gls_pain001.xml | 4++--
Mlibeufin-nexus/sample/platform/maerki_baumann_pain001.xml | 4++--
Mlibeufin-nexus/sample/platform/postfinance_pain001.xml | 4++--
Mlibeufin-nexus/sample/platform/raiffeisen_pain001.xml | 4++--
Mlibeufin-nexus/sample/platform/valiant_pain001.xml | 4++--
Asrc/dialect.rs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/iso20022/camt.rs | 27++++++++++++++++++++-------
Msrc/iso20022/mod.rs | 1+
Asrc/iso20022/pain001.rs | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/iso20022/pain002.rs | 4++--
Msrc/key_management.rs | 21+++++++++++++--------
Msrc/lib.rs | 1+
Msrc/xml.rs | 62+++++++++++++++++++++++++++++++++-----------------------------
14 files changed, 400 insertions(+), 64 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1587,7 +1587,7 @@ dependencies = [ "jiff", "pem", "pretty_assertions", - "rand 0.10.0", + "rand 0.10.1", "rcgen", "reedline", "regex", @@ -2053,7 +2053,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.3", "ring", "rustc-hash", "rustls", @@ -2113,9 +2113,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -2123,9 +2123,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -3069,6 +3069,9 @@ dependencies = [ [[package]] name = "taler-build" version = "1.5.0" +dependencies = [ + "const_format", +] [[package]] name = "taler-common" @@ -3081,7 +3084,7 @@ dependencies = [ "glob", "indexmap", "jiff", - "rand 0.10.0", + "rand 0.10.1", "regex", "serde", "serde_json", @@ -3525,7 +3528,7 @@ checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", "js-sys", - "rand 0.10.0", + "rand 0.10.1", "wasm-bindgen", ] diff --git a/libeufin-nexus/sample/platform/gls_pain001.xml b/libeufin-nexus/sample/platform/gls_pain001.xml @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><PmtTpInf><SvcLvl><Cd>SEPA</Cd></SvcLvl></PmtTpInf><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>BIC</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> -\ No newline at end of file +<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><PmtTpInf><SvcLvl><Cd>SEPA</Cd></SvcLvl></PmtTpInf><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>AAAABBCC123</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> +\ No newline at end of file diff --git a/libeufin-nexus/sample/platform/maerki_baumann_pain001.xml b/libeufin-nexus/sample/platform/maerki_baumann_pain001.xml @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.ch.03.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>BIC</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> -\ No newline at end of file +<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.ch.03.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>AAAABBCC123</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> +\ No newline at end of file diff --git a/libeufin-nexus/sample/platform/postfinance_pain001.xml b/libeufin-nexus/sample/platform/postfinance_pain001.xml @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.ch.03.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>BIC</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> -\ No newline at end of file +<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.ch.03.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>AAAABBCC123</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> +\ No newline at end of file diff --git a/libeufin-nexus/sample/platform/raiffeisen_pain001.xml b/libeufin-nexus/sample/platform/raiffeisen_pain001.xml @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.ch.03.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>BIC</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> -\ No newline at end of file +<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.ch.03.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>AAAABBCC123</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> +\ No newline at end of file diff --git a/libeufin-nexus/sample/platform/valiant_pain001.xml b/libeufin-nexus/sample/platform/valiant_pain001.xml @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.ch.03.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>BIC</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> -\ No newline at end of file +<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.ch.03.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>MESSAGE_ID</MsgId><CreDtTm>2024-09-09T00:00:00Z</CreDtTm><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><InitgPty><Nm>myname</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>NOTPROVIDED</PmtInfId><PmtMtd>TRF</PmtMtd><BtchBookg>false</BtchBookg><NbOfTxs>3</NbOfTxs><CtrlSum>47.32</CtrlSum><ReqdExctnDt><Dt>2024-09-09Z</Dt></ReqdExctnDt><Dbtr><Nm>myname</Nm></Dbtr><DbtrAcct><Id><IBAN>CH7789144474425692816</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BICFI>AAAABBCC123</BICFI></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><InstrId>TX_FIRST</InstrId><EndToEndId>TX_FIRST</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">42</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 42</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_SECOND</InstrId><EndToEndId>TX_SECOND</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">5.11</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 5.11</Ustrd></RmtInf></CdtTrfTxInf><CdtTrfTxInf><PmtId><InstrId>TX_THIRD</InstrId><EndToEndId>TX_THIRD</EndToEndId></PmtId><Amt><InstdAmt Ccy="CHF">0.21</InstdAmt></Amt><Cdtr><Nm>Test</Nm></Cdtr><CdtrAcct><Id><IBAN>CH4189144589712575493</IBAN></Id></CdtrAcct><RmtInf><Ustrd>Test 0.21</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document> +\ No newline at end of file diff --git a/src/dialect.rs b/src/dialect.rs @@ -0,0 +1,56 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 Taler Systems S.A. + +* LibEuFin is free software; you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation; either version 3, or +* (at your option) any later version. + +* LibEuFin is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General +* Public License for more details. + +* You should have received a copy of the GNU Affero General Public +* License along with LibEuFin; see the file COPYING. If not, see +* <http://www.gnu.org/licenses/> +*/ + +use taler_enum_meta::EnumMeta; + +/** Supported EBICS standard */ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Standard { + /// Swiss Payment Standards + SIX, + /// German Banking Industry Committee + GBIC, +} + +impl Standard { + // TODO +} + +/** Supported bank dialects */ +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] +#[enum_meta(Str)] +#[allow(non_camel_case_types)] +pub enum Dialect { + valiant, + raiffeisen, + postfinance, + gls, + maerki_baumann, +} + +impl Dialect { + pub fn standard(&self) -> Standard { + match self { + Self::valiant | Self::raiffeisen | Self::postfinance | Self::maerki_baumann => { + Standard::SIX + } + Self::gls => Standard::GBIC, + } + } +} diff --git a/src/iso20022/camt.rs b/src/iso20022/camt.rs @@ -42,6 +42,7 @@ use crate::{ #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)] #[enum_meta(Str)] +#[allow(clippy::upper_case_acronyms)] enum Kind { CRDT, DBIT, @@ -523,14 +524,14 @@ pub fn parse_camt(xml: &str) -> xml::Result<Vec<AccountTransactions>> { } #[cfg(test)] -mod test { +pub mod test { use std::str::FromStr; - use jiff::civil::Date; + use jiff::{Timestamp, civil::Date}; use taler_common::types::{ amount::{Amount, Currency}, iban::IBAN, - payto::iban_payto, + payto::{BankID, IbanPayto, PaytoImpl as _, PaytoURI}, utils::date_to_utc_ts, }; @@ -539,6 +540,18 @@ mod test { model::{InId, InTx, OutBatch, OutId, OutReversal, OutTx, Tx}, }; + pub fn date_to_timestamp(date: &str) -> Timestamp { + date_to_utc_ts(&Date::from_str(date).unwrap()) + } + + fn iban_payto(iban: impl AsRef<str>, name: impl AsRef<str>) -> PaytoURI { + IbanPayto::new(BankID { + iban: iban.as_ref().parse().expect("invalid IBAN"), + bic: None, + }) + .as_full_payto(name.as_ref()) + } + pub fn check_tx(path: &str, iban: &str, currency: Option<&str>, txs: &[Tx]) { let content = std::fs::read_to_string(path).unwrap(); let res = parse_camt(&content).unwrap(); @@ -570,7 +583,7 @@ mod test { amount: Amount::from_str(amount).unwrap(), debit_fee: Amount::from_str(debit_fee).unwrap(), subject: subject.map(Into::into), - execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), + execution_time: date_to_timestamp(execution_time), creditor: creditor.map(|(iban, name)| iban_payto(iban, name)), }) } @@ -592,7 +605,7 @@ mod test { amount: Amount::from_str(amount).unwrap(), credit_fee: Amount::from_str(credit_fee).unwrap(), subject: subject.map(Into::into), - execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), + execution_time: date_to_timestamp(execution_time), debtor: debtor.map(|(iban, name)| iban_payto(iban, name)), }) } @@ -607,14 +620,14 @@ mod test { e2e_id: e2e_id.parse().unwrap(), msg_id: msg_id.map(Into::into), reason: reason.into(), - execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), + execution_time: date_to_timestamp(execution_time), }) } pub fn tx_batch(msg_id: &str, execution_time: &str) -> Tx { Tx::Batch(OutBatch { msg_id: msg_id.into(), - execution_time: date_to_utc_ts(&Date::from_str(execution_time).unwrap()), + execution_time: date_to_timestamp(execution_time), }) } diff --git a/src/iso20022/mod.rs b/src/iso20022/mod.rs @@ -22,6 +22,7 @@ use taler_enum_meta::EnumMeta; pub mod bank_tx_code; pub mod camt; pub mod hac; +pub mod pain001; pub mod pain002; pub mod status_code; diff --git a/src/iso20022/pain001.rs b/src/iso20022/pain001.rs @@ -0,0 +1,253 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 Taler Systems S.A. + +* LibEuFin is free software; you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation; either version 3, or +* (at your option) any later version. + +* LibEuFin is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General +* Public License for more details. + +* You should have received a copy of the GNU Affero General Public +* License along with LibEuFin; see the file COPYING. If not, see +* <http://www.gnu.org/licenses/> +*/ + +use anyhow::bail; +use compact_str::CompactString; +use jiff::{Timestamp, Zoned, tz::TimeZone}; +use taler_common::types::{ + amount::{Amount, Decimal}, + payto::FullIbanPayto, +}; + +use crate::{ + dialect::{Dialect, Standard}, + xml::XmlWriter, + xml_build, xml_el, +}; + +/** pain.001 transaction metadata */ +pub struct Pain001Tx { + pub creditor: FullIbanPayto, + pub amount: Amount, + pub subject: Box<str>, + pub e2e_id: CompactString, +} + +/** pain.001 message metadata */ +pub struct Pain001Msg { + pub msg_id: CompactString, + pub timestamp: Timestamp, + pub debtor: FullIbanPayto, + pub sum: Amount, + pub txs: Box<[Pain001Tx]>, +} + +/** Check EBICS compability of an amount */ +fn ebics_amount(amount: Amount) -> anyhow::Result<Decimal> { + if amount.is_sub_cent() { + bail!("Sub-cent amounts not supported") + } + Ok(amount.decimal()) +} + +/** Create a pain.001 XML document [msg] valid for [dialect] */ +pub fn create_pain001( + msg: &Pain001Msg, + dialect: &Dialect, + instant: bool, +) -> anyhow::Result<String> { + let version = "09"; + let suffix = match dialect.standard() { + Standard::SIX => ".ch.03", + Standard::GBIC => "", + }; + let total = ebics_amount(msg.sum)?; + Ok(xml_build!( + "Document" + ("xmlns": (format_args!("urn:iso:std:iso:20022:tech:xsd:pain.001.001.{version}"))) + ("xmlns:xsi": (format_args!("http://www.w3.org/2001/XMLSchema-instance"))) + ("xsi:schemaLocation": (format_args!("urn:iso:std:iso:20022:tech:xsd:pain.001.001.{version} pain.001.001.{version}{suffix}.xsd"))) + { + "CstmrCdtTrfInitn" { + "GrpHdr" { + // Used for idempotency as banks will refuse to process EBICS request with the same MsgId for a pre-agreed period + // Used to uniquely identify batches of transactions in other files + "MsgId": msg.msg_id, + "CreDtTm": msg.timestamp, + "NbOfTxs": msg.txs.len(), + "CtrlSum": total, + "InitgPty" { + "Nm": msg.debtor.name + }/* + // TODO fail with GLS: ES_VERIFICATION IncorrectFileStructure - 'Signature verification' 'The file format is incomplete or invalid' + "InitnSrc" { + "Nm": "LibEuFin", + "Prvdr": "Taler Systems SA", + "Vrsn": taler_build::long_version() + }*/ + }, + "PmtInf" { + "PmtInfId": "NOTPROVIDED", + "PmtMtd": "TRF", + "BtchBookg": "false", + "NbOfTxs": msg.txs.len(), + "CtrlSum": total, + @ |w: &mut XmlWriter| { + if dialect.standard() == Standard::GBIC { + xml_el!(w, "PmtTpInf" { + "SvcLvl" { + "Cd": "SEPA" + }, + @ |w: &mut XmlWriter| { + if instant { + xml_el!(w, "LclInstrm" { + "Cd": "INST" + }); + } + } + }); + } + }, + "ReqdExctnDt" { + "Dt": Zoned::new(msg.timestamp, TimeZone::UTC).date().to_string() + "Z" + }, + "Dbtr" { + "Nm": msg.debtor.name + }, + "DbtrAcct" { + "Id" { + "IBAN": msg.debtor.iban + } + }, + "DbtrAgt" { + "FinInstnId" { + @ |w: &mut XmlWriter| { + if let Some(bic) = &msg.debtor.bic { + xml_el!(w, "BICFI": bic); + } else { + xml_el!(w, "Othr" {"Id": "NOTPROVIDED"}); + } + } + } + + }, + "ChrgBr": "SLEV", + @ |w: &mut XmlWriter| { + for tx in &msg.txs { + xml_el!(w, "CdtTrfTxInf" { + "PmtId" { + "InstrId": tx.e2e_id, + // Used to uniquely identify transactions in other files + "EndToEndId": tx.e2e_id + }, + "Amt" { + "InstdAmt" ("Ccy": (tx.amount.currency)): ebics_amount(tx.amount).unwrap() + }, + @ |w: &mut XmlWriter| { + if let Some(bic) = &tx.creditor.bic { + xml_el!(w, "CdtrAgt" { + "FinInstnId" { + "BICFI": bic + } + }); + } + }, + "Cdtr" { + "Nm": tx.creditor.name + // Addr might become a requirement in the future + /*"PstlAdr" { + "TwnNm": "Bochum", + "Ctry": "DE" + }*/ + }, + "CdtrAcct" { + "Id" { + "IBAN": tx.creditor.iban + } + }, + "RmtInf" { + "Ustrd": tx.subject + } + }); + } + } + } + } + } + )) +} + +#[cfg(test)] +mod test { + use taler_common::types::{ + amount::amount, + payto::{BankID, FullIbanPayto}, + }; + + use crate::{ + dialect::Dialect, + iso20022::{ + camt::test::date_to_timestamp, + pain001::{Pain001Msg, Pain001Tx, create_pain001}, + }, + }; + + #[test] + fn pain001() { + let creditor = FullIbanPayto::new( + BankID { + iban: "CH4189144589712575493".parse().expect("invalid IBAN"), + bic: None, + }, + "Test", + ); + + let msg = Pain001Msg { + msg_id: "MESSAGE_ID".into(), + timestamp: date_to_timestamp("2024-09-09"), + debtor: FullIbanPayto::new( + BankID { + iban: "CH7789144474425692816".parse().expect("invalid IBAN"), + bic: Some("AAAABBCC123".parse().expect("invalid BIC")), + }, + "myname", + ), + sum: amount("CHF:47.32"), + txs: Box::new([ + Pain001Tx { + creditor: creditor.clone(), + amount: amount("CHF:42"), + subject: "Test 42".into(), + e2e_id: "TX_FIRST".into(), + }, + Pain001Tx { + creditor: creditor.clone(), + amount: amount("CHF:5.11"), + subject: "Test 5.11".into(), + e2e_id: "TX_SECOND".into(), + }, + Pain001Tx { + creditor: creditor, + amount: amount("CHF:0.21"), + subject: "Test 0.21".into(), + e2e_id: "TX_THIRD".into(), + }, + ]), + }; + for dialect in Dialect::entries { + pretty_assertions::assert_eq!( + std::fs::read_to_string(format!( + "libeufin-nexus/sample/platform/{dialect}_pain001.xml" + )) + .unwrap(), + create_pain001(&msg, dialect, false).unwrap() + ); + } + } +} diff --git a/src/iso20022/pain002.rs b/src/iso20022/pain002.rs @@ -135,7 +135,7 @@ pub fn parse_pain002(xml: &str) -> xml::Result<MsgStatus> { .collect() } - return Xml::parse_str(xml, "Document", |root| { + Xml::parse_str(xml, "Document", |root| { let n = root.one("CstmrPmtStsRpt")?; let status = n.one("OrgnlGrpInfAndSts")?; Ok(MsgStatus { @@ -164,7 +164,7 @@ pub fn parse_pain002(xml: &str) -> xml::Result<MsgStatus> { }) .collect::<xml::Result<_>>()?, }) - }); + }) } #[cfg(test)] diff --git a/src/key_management.rs b/src/key_management.rs @@ -155,6 +155,12 @@ pub async fn key_management( _ebics_logger: &EbicsLogger, order: Order, ) -> anyhow::Result<EbicsResponse<Option<String>>> { + let EbicsHostCfg { + host_id, + user_id, + partner_id, + .. + } = cfg; info!("Doing key request {}", order.name()); //val txLog = ebicsLogger.tx(order.name) // TODO is this still necessary ? @@ -201,7 +207,7 @@ pub async fn key_management( let b64 = BASE64_STANDARD.encode(der.as_ref()); xml_el!(w, "ds:X509Data" { - "ds:X509Certificate": &b64 + "ds:X509Certificate": b64 }); } let data = match order { @@ -243,18 +249,18 @@ pub async fn key_management( { "header" ("authenticate": "true") { "static" { - "HostID": &cfg.host_id, + "HostID": host_id, @ |w: &mut XmlWriter| { if order == Order::HPB { let nonce: u128 = rand::random(); xml_el!(w, - "Nonce": &format_args!("{:032x}", nonce), - "Timestamp": &jiff::Timestamp::now() + "Nonce": format_args!("{:032x}", nonce), + "Timestamp": jiff::Timestamp::now() ); } }, - "PartnerID": &cfg.partner_id, - "UserID": &cfg.user_id, + "PartnerID": partner_id, + "UserID": user_id, "OrderDetails" { "AdminOrderType": order.name() }, @@ -270,14 +276,13 @@ pub async fn key_management( "body" { @ |w: &mut XmlWriter| { if let Some(data) = data { - xml_el!(w,"DataTransfer" { + xml_el!(w, "DataTransfer" { "OrderData": data }); } } } } - ); let signed = if sign { sign_ebics(msg, &client.authentication_private_key) diff --git a/src/lib.rs b/src/lib.rs @@ -45,6 +45,7 @@ pub mod common; pub mod config; pub mod crypto; pub mod db; +pub mod dialect; pub mod ebics_code; pub mod iso20022; pub mod key_management; diff --git a/src/xml.rs b/src/xml.rs @@ -17,49 +17,48 @@ * <http://www.gnu.org/licenses/> */ -use std::{fmt::Display, str::FromStr}; +use std::{ + fmt::{Display, Write}, + str::FromStr, +}; use base64::{Engine, prelude::BASE64_STANDARD}; -use jiff::Timestamp; use roxmltree::{Document, Node}; #[macro_export] macro_rules! xml_el { + // Logic escape + ($w:ident, @ $logic:expr$(, $($rest:tt)*)?) => { + ($logic)($w); + $($crate::xml_el!($w, $($rest)*);)* + }; // Text element ($w:ident, $name:tt $(($k:tt: $v:tt))* : $content:expr $(, $($rest:tt)*)?) => { - $w.text($name, &[$(($k, $v)),*], $content); + $w.text(&$name, &[$((&$k, &$v)),*], &$content); $($crate::xml_el!($w, $($rest)*);)* }; // Nested block ($w:ident, $name:tt $(($k:tt: $v:tt))* { $($body:tt)* }$(, $($rest:tt)*)?) => { - let name = $name; - $w.open(name, &[$(($k, $v)),*]); + let name = &$name; + $w.open(&name, &[$((&$k, &$v)),*]); $crate::xml_el!($w, $($body)*); - $w.close(name); + $w.close(&name); $($crate::xml_el!($w, $($rest)*);)* }; // Empty element ($w:ident, $name:tt $(($k:tt: $v:tt))* $(, $($rest:tt)*)?) => { - $w.empty($name, &[$(($k, $v)),*]); - $($crate::xml_el!($w, $($rest)*);)* - }; - // Logic escape - ($w:ident, @ $logic:expr$(, $($rest:tt)*)?) => { - ($logic)($w); + $w.empty(&$name, &[$((&$k, &$v)),*]); $($crate::xml_el!($w, $($rest)*);)* }; } #[macro_export] macro_rules! xml_build { - ($name:tt $(($k:tt: $v:tt))* { $($body:tt)* }) => { + ($($xml:tt)*) => { { let mut writer = $crate::xml::XmlWriter::init(); let w = &mut writer; - let name = $name; - w.open(name, &[$(($k, $v)),*]); - $crate::xml_el!(w, $($body)*); - w.close(name); + $crate::xml_el!(w, $($xml)*); writer.finish() } }; @@ -74,43 +73,48 @@ impl XmlWriter { Self { buff } } - pub fn open(&mut self, name: &str, attrs: &[(&str, &str)]) { + pub fn open(&mut self, name: &dyn Display, attrs: &[(&dyn Display, &dyn Display)]) { self.buff.push('<'); self.write_tag_attrs(name, attrs); self.buff.push('>'); } - pub fn close(&mut self, name: &str) { + pub fn close(&mut self, name: &dyn Display) { self.buff.push_str("</"); - self.buff.push_str(name); + self.buff.write_fmt(format_args!("{name}")).unwrap(); self.buff.push('>'); } - pub fn empty(&mut self, name: &str, attrs: &[(&str, &str)]) { + pub fn empty(&mut self, name: &dyn Display, attrs: &[(&dyn Display, &dyn Display)]) { self.buff.push('<'); self.write_tag_attrs(name, attrs); self.buff.push_str("/>"); } - pub fn text<D: Display>(&mut self, name: &str, attrs: &[(&str, &str)], content: D) { + pub fn text( + &mut self, + name: &dyn Display, + attrs: &[(&dyn Display, &dyn Display)], + content: &dyn Display, + ) { self.open(name, attrs); self.write_escaped(content); self.close(name); } - fn write_tag_attrs(&mut self, name: &str, attrs: &[(&str, &str)]) { - self.buff.push_str(name); + fn write_tag_attrs(&mut self, name: &dyn Display, attrs: &[(&dyn Display, &dyn Display)]) { + self.buff.write_fmt(format_args!("{name}")).unwrap(); for (key, value) in attrs { self.buff.push(' '); - self.buff.push_str(key); + self.buff.write_fmt(format_args!("{key}")).unwrap(); self.buff.push_str("=\""); - self.write_escaped(value); + self.write_escaped(*value); self.buff.push('"'); } } - fn write_escaped<D: Display>(&mut self, content: D) { + fn write_escaped(&mut self, content: &dyn Display) { std::fmt::write(self, format_args!("{content}")).unwrap(); } @@ -422,8 +426,8 @@ mod test { "endOfDocument" { @ |w: &mut XmlWriter| { for i in 1..=10 { - xml_el!(w, (&format!("e{i}")) { - (&format!("e{i}{i}")): &format_args!("{i}{i}{i}") + xml_el!(w, (format_args!("e{i}")) { + (format_args!("e{i}{i}")): (format_args!("{i}{i}{i}")) }); } }