Initial commit
Change-Id: I2b916ff0acd2a88aeef709cf4f900503e823d44d
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/.zuul.yaml b/.zuul.yaml
new file mode 100644
index 0000000..d0b7b41
--- /dev/null
+++ b/.zuul.yaml
@@ -0,0 +1,23 @@
+- job:
+ name: ovsdb-cargo
+ pre-run: playbooks/pre.yml
+
+- job:
+ name: ovsdb-cargo-test
+ parent: ovsdb-cargo
+ run: playbooks/test.yml
+
+- job:
+ name: ovsdb-cargo-clippy
+ parent: ovsdb-cargo
+ run: playbooks/clippy.yml
+
+- project:
+ check:
+ jobs:
+ - ovsdb-cargo-test
+ - ovsdb-cargo-clippy
+ gate:
+ jobs:
+ - ovsdb-cargo-test
+ - ovsdb-cargo-clippy
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..070c95b
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,836 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "async-trait"
+version = "0.1.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "backtrace"
+version = "0.3.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-timer"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.13.3+wasi-0.2.2",
+ "windows-targets",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "http"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jsonrpsee"
+version = "0.24.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "834af00800e962dee8f7bfc0f60601de215e73e78e5497d733a2919da837d3c8"
+dependencies = [
+ "jsonrpsee-core",
+ "jsonrpsee-proc-macros",
+ "jsonrpsee-types",
+ "tracing",
+]
+
+[[package]]
+name = "jsonrpsee-core"
+version = "0.24.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76637f6294b04e747d68e69336ef839a3493ca62b35bf488ead525f7da75c5bb"
+dependencies = [
+ "async-trait",
+ "futures-timer",
+ "futures-util",
+ "jsonrpsee-types",
+ "pin-project",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+]
+
+[[package]]
+name = "jsonrpsee-proc-macros"
+version = "0.24.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fcae0c6c159e11541080f1f829873d8f374f81eda0abc67695a13fc8dc1a580"
+dependencies = [
+ "heck",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "jsonrpsee-types"
+version = "0.24.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddb81adb1a5ae9182df379e374a79e24e992334e7346af4d065ae5b2acb8d4c6"
+dependencies = [
+ "http",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.170"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
+
+[[package]]
+name = "log"
+version = "0.4.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+dependencies = [
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "ovsdb-client"
+version = "0.0.1"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "jsonrpsee",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.12",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "ovsdb-derive"
+version = "0.0.1"
+dependencies = [
+ "ovsdb-schema",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn",
+ "uuid",
+]
+
+[[package]]
+name = "ovsdb-schema"
+version = "0.0.1"
+dependencies = [
+ "ovsdb-derive",
+ "serde",
+ "serde_json",
+ "uuid",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "serde"
+version = "1.0.218"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.218"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+
+[[package]]
+name = "socket2"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl 2.0.12",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tokio"
+version = "1.43.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
+dependencies = [
+ "backtrace",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+
+[[package]]
+name = "toml_edit"
+version = "0.22.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+dependencies = [
+ "nu-ansi-term",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "uuid"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
+dependencies = [
+ "getrandom",
+ "serde",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasi"
+version = "0.13.3+wasi-0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
+dependencies = [
+ "bitflags",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..bed2b3d
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,3 @@
+[workspace]
+resolver = "3"
+members = ["client", "derive", "schema"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6ebf639
--- /dev/null
+++ b/README.md
@@ -0,0 +1,109 @@
+# OVSDB Rust
+
+A collection of Rust crates for working with the Open vSwitch Database Management Protocol (OVSDB).
+
+[](https://opensource.org/licenses/Apache-2.0)
+
+## Overview
+
+This repository provides a complete Rust implementation of the OVSDB protocol as defined in [RFC7047](https://datatracker.ietf.org/doc/html/rfc7047). It's structured as a monorepo containing the following crates:
+
+| Crate | Description | Status |
+|-------|-------------|--------|
+| [`ovsdb-schema`](./schema) | Rust types and serialization for OVSDB | [](https://crates.io/crates/ovsdb-schema) |
+| [`ovsdb-derive`](./derive) | Procedural macros for OVSDB struct generation | [](https://crates.io/crates/ovsdb-derive) |
+| [`ovsdb-client`](./client) | Async client for the OVSDB protocol | [](https://crates.io/crates/ovsdb-client) |
+
+## Features
+
+- **Complete Type System**: Full implementation of OVSDB's type system (atoms, sets, maps)
+- **Auto-generated Structs**: Derive macros for creating OVSDB-compatible structs
+- **Async Client**: Modern async client using Tokio and jsonrpsee
+- **Multiple Transports**: Support for TCP and Unix socket connections
+- **Table Monitoring**: Real-time monitoring of table changes
+
+## Quick Example
+
+```rust
+use ovsdb_derive::ovsdb_object;
+use ovsdb_client::{rpc, schema::MonitorRequest};
+use std::collections::HashMap;
+
+#[ovsdb_object]
+struct NbGlobal {
+ name: Option<String>,
+ nb_cfg: Option<i64>,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+ // Connect to an OVSDB server
+ let client = rpc::connect_tcp("127.0.0.1:6641").await?;
+
+ // Set up monitoring for the NB_Global table
+ let mut requests = HashMap::new();
+ requests.insert(
+ "NB_Global".to_owned(),
+ MonitorRequest {
+ columns: Some(vec!["name".to_owned(), "nb_cfg".to_owned()]),
+ ..Default::default()
+ },
+ );
+
+ // Start monitoring
+ let initial = client.monitor("OVN_Northbound", None, requests).await?;
+ println!("Initial state: {:?}", initial);
+
+ // Subscribe to updates
+ let mut stream = client.subscribe_to_method("update").await?;
+ while let Some(update) = stream.next().await {
+ if let Ok(update) = update {
+ println!("Received update: {:?}", update);
+ }
+ }
+
+ Ok(())
+}
+```
+
+## Getting Started
+
+To use these crates in your project, add the following to your `Cargo.toml`:
+
+```toml
+[dependencies]
+ovsdb-schema = "0.1.0"
+ovsdb-derive = "0.1.0"
+ovsdb-client = "0.1.0"
+```
+
+See the individual crate directories for more detailed documentation:
+- [ovsdb-schema](./schema/README.md)
+- [ovsdb-derive](./derive/README.md)
+- [ovsdb-client](./client/README.md)
+
+## Development
+
+### Prerequisites
+
+- Rust 1.75 or newer
+- OVSDB server for testing (see below)
+
+### Setting up a test environment
+
+For development and testing, you can run an OVSDB server using Docker:
+
+```bash
+docker run -it --rm -p 6641:6641 registry.atmosphere.dev/library/ovn-central:main \
+ /bin/bash -c "mkdir /etc/ovn; /root/ovnkube.sh nb-ovsdb"
+```
+
+### Running tests
+
+```bash
+cargo test --all
+```
+
+## License
+
+This project is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
diff --git a/client/Cargo.toml b/client/Cargo.toml
new file mode 100644
index 0000000..6773ee0
--- /dev/null
+++ b/client/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "ovsdb-client"
+version = "0.0.1"
+edition = "2021"
+description = "Async Rust client for the Open vSwitch Database Protocol with monitoring support"
+license = "Apache-2.0"
+keywords = ["ovsdb", "ovs", "openvswitch", "database", "networking"]
+categories = ["database", "network-programming", "api-bindings", "asynchronous"]
+repository = "https://review.vexxhost.dev/plugins/gitiles/ovsdb"
+
+[dependencies]
+bytes = "1.10.1"
+futures-util = { version = "0.3.31" }
+jsonrpsee = { version = "0.24.8", features = ["async-client", "client-core", "macros"] }
+serde = "1.0.218"
+serde_json = "1.0.140"
+thiserror = "2.0.12"
+tokio = { version = "1.43.0", features = ["net", "rt-multi-thread"] }
+tokio-util = { version = "0.7.13", features = ["codec"] }
+
+[dev-dependencies]
+tracing = "0.1.41"
+tracing-subscriber = "0.3.19"
diff --git a/client/README.md b/client/README.md
new file mode 100644
index 0000000..a8fed3a
--- /dev/null
+++ b/client/README.md
@@ -0,0 +1,143 @@
+# ovsdb-client
+
+A Rust implementation of the OVSDB protocol client based on [RFC7047](https://datatracker.ietf.org/doc/html/rfc7047).
+
+## Overview
+
+This crate provides a client implementation for the Open vSwitch Database Management Protocol (OVSDB), allowing Rust applications to:
+
+- Connect to OVSDB servers over TCP or Unix sockets
+- Query database schemas
+- Monitor tables for changes in real-time
+- Execute transactions against OVSDB databases
+
+## Features
+
+- **Multiple Transport Options**: Connect via TCP or Unix socket
+- **Schema Handling**: Retrieve and parse database schemas
+- **Monitoring**: Subscribe to changes in database tables
+- **JSON-RPC**: Built on top of `jsonrpsee` for reliable RPC communication
+- **Async API**: Fully async API designed for use with Tokio
+
+## Quick Start
+
+```rust
+use ovsdb_client::{
+ rpc::{self, RpcClient},
+ schema::{MonitorRequest, UpdateNotification},
+};
+use std::collections::HashMap;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+ // Connect to an OVSDB server on localhost
+ let client = rpc::connect_tcp("127.0.0.1:6641").await?;
+
+ // List available databases
+ let databases = client.list_databases().await?;
+ println!("Available databases: {:?}", databases);
+
+ // Get schema for a specific database
+ let schema = client.get_schema("OVN_Northbound").await?;
+
+ // Set up monitoring for a table
+ let mut requests = HashMap::new();
+ requests.insert(
+ "NB_Global".to_owned(),
+ MonitorRequest {
+ columns: Some(vec!["name".to_owned(), "nb_cfg".to_owned()]),
+ ..Default::default()
+ },
+ );
+
+ // Start monitoring and get initial state
+ let initial = client.monitor("OVN_Northbound", None, requests).await?;
+ println!("Initial state: {:?}", initial);
+
+ // Subscribe to updates
+ let mut stream = client.subscribe_to_method("update").await?;
+ while let Some(update) = stream.next().await {
+ match update {
+ Ok(update) => println!("Received update: {:?}", update),
+ Err(e) => eprintln!("Error: {:?}", e),
+ }
+ }
+
+ Ok(())
+}
+```
+
+## API Overview
+
+### Connections
+
+```rust
+// Connect via TCP
+let client = rpc::connect_tcp("127.0.0.1:6641").await?;
+
+// Connect via Unix socket
+let client = rpc::connect_unix("/var/run/openvswitch/db.sock").await?;
+```
+
+### Basic Operations
+
+```rust
+// List databases
+let databases = client.list_databases().await?;
+
+// Get schema
+let schema = client.get_schema("OVN_Northbound").await?;
+```
+
+### Monitoring
+
+```rust
+// Create monitor request
+let mut requests = HashMap::new();
+requests.insert(
+ "Table_Name".to_owned(),
+ MonitorRequest {
+ columns: Some(vec!["column1".to_owned(), "column2".to_owned()]),
+ ..Default::default()
+ },
+);
+
+// Start monitoring
+let initial_state = client.monitor("Database_Name", None, requests).await?;
+
+// Subscribe to updates
+let mut stream = client.subscribe_to_method("update").await?;
+while let Some(update) = stream.next().await {
+ // Process updates
+}
+```
+
+## Development Setup
+
+To develop or test with this crate, you'll need an OVSDB server. You can use Docker to run one:
+
+```bash
+docker run -it --rm -p 6641:6641 registry.atmosphere.dev/library/ovn-central:main /bin/bash -c "mkdir /etc/ovn; /root/ovnkube.sh nb-ovsdb"
+```
+
+This starts an OVN Northbound OVSDB server that listens on port 6641.
+
+## OVSDB Protocol Support
+
+This implementation supports the following OVSDB operations as defined in RFC7047:
+
+- List Databases (Section 4.1.1)
+- Get Schema (Section 4.1.2)
+- Monitor (Section 4.1.5)
+- Update Notifications (Section 4.1.6)
+
+Future versions will add support for additional operations such as Transact (Section 4.1.3) and Monitor Cancellation (Section 4.1.7).
+
+## Related Crates
+
+- [ovsdb-schema](https://crates.io/crates/ovsdb-schema): Core OVSDB data types and serialization
+- [ovsdb-derive](https://crates.io/crates/ovsdb-derive): Derive macros for OVSDB struct generation
+
+## License
+
+This project is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
diff --git a/client/examples/ovsdb-monitor.rs b/client/examples/ovsdb-monitor.rs
new file mode 100644
index 0000000..bd5362a
--- /dev/null
+++ b/client/examples/ovsdb-monitor.rs
@@ -0,0 +1,59 @@
+use jsonrpsee::core::client::{Subscription, SubscriptionClientT};
+use ovsdb_client::{
+ rpc::{self, RpcClient},
+ schema::{MonitorRequest, UpdateNotification},
+};
+use std::collections::HashMap;
+use tracing::Level;
+use tracing_subscriber::FmtSubscriber;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let subscriber = FmtSubscriber::builder()
+ .with_max_level(Level::TRACE)
+ .finish();
+ tracing::subscriber::set_global_default(subscriber)?;
+
+ let socket_addr = "127.0.0.1:6641";
+ let database = "OVN_Northbound";
+ let table = "NB_Global";
+
+ let client = rpc::connect_tcp(socket_addr).await?;
+
+ // // 4.1.1. List Databases
+ let _databases = client.list_databases().await?;
+
+ // // 4.1.2. Get Schema
+ let schema = client.get_schema(database).await?;
+ let columns = schema
+ .tables
+ .get(table)
+ .expect("table not found")
+ .columns
+ .keys()
+ .cloned()
+ .collect::<Vec<_>>();
+
+ let mut requests = HashMap::new();
+ requests.insert(
+ table.to_owned(),
+ MonitorRequest {
+ columns: Some(columns),
+ ..Default::default()
+ },
+ );
+
+ let initial = client.monitor("OVN_Northbound", None, requests).await?;
+ println!("Initial state: {:?}", initial);
+
+ let mut stream: Subscription<UpdateNotification<serde_json::Value>> = client.subscribe_to_method("update").await?;
+
+ while let Some(update) = stream.next().await {
+ match update {
+ Ok(update) => println!("Received update: {:?}", update),
+ Err(e) => eprintln!("Error receiving update: {:?}", e),
+ }
+ }
+
+ Ok(())
+}
diff --git a/client/src/lib.rs b/client/src/lib.rs
new file mode 100644
index 0000000..2d5fc87
--- /dev/null
+++ b/client/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod rpc;
+pub mod schema;
+mod transports;
diff --git a/client/src/rpc.rs b/client/src/rpc.rs
new file mode 100644
index 0000000..4b6b372
--- /dev/null
+++ b/client/src/rpc.rs
@@ -0,0 +1,55 @@
+use crate::{
+ schema::{DatabaseSchema, MonitorRequest, TableUpdate},
+ transports::{ipc, tcp},
+};
+use jsonrpsee::{async_client::ClientBuilder, core::client::SubscriptionClientT, proc_macros::rpc};
+use std::{collections::HashMap, path::Path};
+use tokio::net::ToSocketAddrs;
+
+#[rpc(client)]
+pub trait Rpc {
+ /// 4.1.1. List Databases
+ ///
+ /// This operation retrieves an array whose elements are the names of the
+ /// databases that can be accessed over this management protocol
+ /// connection.
+ #[method(name = "list_dbs")]
+ async fn list_databases(&self) -> Result<Vec<String>, ErrorObjectOwned>;
+
+ /// 4.1.2. Get Schema
+ ///
+ /// This operation retrieves a <database-schema> that describes hosted
+ /// database <db-name>.
+ #[method(name = "get_schema")]
+ async fn get_schema(&self, db_name: &str) -> Result<DatabaseSchema, ErrorObjectOwned>;
+
+ /// 4.1.5. Monitor
+ ///
+ /// The "monitor" request enables a client to replicate tables or subsets
+ /// of tables within an OVSDB database by requesting notifications of
+ /// changes to those tables and by receiving the complete initial state
+ /// of a table or a subset of a table.
+ #[method(name = "monitor")]
+ async fn monitor(
+ &self,
+ db_name: &str,
+ matcher: Option<&str>,
+ requests: HashMap<String, MonitorRequest>,
+ ) -> Result<TableUpdate<serde_json::Value>, ErrorObjectOwned>;
+}
+
+pub async fn connect_tcp(
+ tcp: impl ToSocketAddrs,
+) -> Result<impl SubscriptionClientT, std::io::Error> {
+ let (sender, receiver) = tcp::connect(tcp).await?;
+
+ Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
+}
+
+pub async fn connect_unix(
+ socket_path: impl AsRef<Path>,
+) -> Result<impl SubscriptionClientT, std::io::Error> {
+ let (sender, receiver) = ipc::connect(socket_path).await?;
+
+ Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
+}
diff --git a/client/src/schema.rs b/client/src/schema.rs
new file mode 100644
index 0000000..f17e757
--- /dev/null
+++ b/client/src/schema.rs
@@ -0,0 +1,120 @@
+use serde::de::{self, SeqAccess, Visitor};
+use serde::{Deserialize, Deserializer, Serialize};
+use std::collections::HashMap;
+use std::fmt;
+use std::marker::PhantomData;
+
+#[derive(Debug, Deserialize)]
+pub struct DatabaseSchema {
+ pub name: String,
+
+ pub version: String,
+
+ #[serde(rename = "cksum")]
+ pub checksum: Option<String>,
+
+ pub tables: HashMap<String, TableSchema>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct TableSchema {
+ pub columns: HashMap<String, ColumnSchema>,
+
+ #[serde(rename = "maxRows")]
+ pub max_rows: Option<u64>,
+
+ #[serde(rename = "isRoot")]
+ pub is_root: Option<bool>,
+
+ #[serde(rename = "indexes")]
+ pub indexes: Option<Vec<Vec<String>>>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct ColumnSchema {
+ pub r#type: serde_json::Value,
+
+ #[serde(rename = "ephemeral")]
+ pub ephemeral: Option<bool>,
+
+ #[serde(rename = "mutable")]
+ pub mutable: Option<bool>,
+}
+
+#[derive(Debug, Default, Deserialize, Serialize)]
+pub struct MonitorRequest {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub columns: Option<Vec<String>>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub select: Option<MonitorRequestSelect>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct MonitorRequestSelect {
+ initial: Option<bool>,
+ insert: Option<bool>,
+ delete: Option<bool>,
+ modify: Option<bool>,
+}
+
+pub type TableUpdate<T> = HashMap<String, TableUpdateRows<T>>;
+pub type TableUpdateRows<T> = HashMap<String, T>;
+
+#[derive(Debug, Deserialize)]
+pub struct RowUpdate<T> {
+ pub old: Option<T>,
+ pub new: Option<T>,
+}
+
+#[derive(Debug)]
+pub struct UpdateNotification<T> {
+ pub id: Option<String>,
+ pub message: TableUpdate<T>,
+}
+
+impl<'de, T> Deserialize<'de> for UpdateNotification<T>
+where
+ T: Deserialize<'de>,
+{
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ // Define a visitor that carries a PhantomData for T.
+ struct UpdateNotificationVisitor<T> {
+ marker: PhantomData<T>,
+ }
+
+ impl<'de, T> Visitor<'de> for UpdateNotificationVisitor<T>
+ where
+ T: Deserialize<'de>,
+ {
+ type Value = UpdateNotification<T>;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter
+ .write_str("an array with two elements: Option<String> and a TableUpdate<T>")
+ }
+
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'de>,
+ {
+ let id: Option<String> = seq
+ .next_element()?
+ .ok_or_else(|| de::Error::invalid_length(0, &self))?;
+ let message: TableUpdate<T> = seq
+ .next_element()?
+ .ok_or_else(|| de::Error::invalid_length(1, &self))?;
+
+ Ok(UpdateNotification { id, message })
+ }
+ }
+
+ // Start deserializing using the visitor.
+ deserializer.deserialize_seq(UpdateNotificationVisitor {
+ marker: PhantomData,
+ })
+ }
+}
diff --git a/client/src/transports/codec.rs b/client/src/transports/codec.rs
new file mode 100644
index 0000000..6e69571
--- /dev/null
+++ b/client/src/transports/codec.rs
@@ -0,0 +1,37 @@
+use bytes::{BufMut, BytesMut};
+use serde_json::Value;
+use std::io;
+use tokio_util::codec::{Decoder, Encoder};
+
+pub struct JsonCodec;
+
+impl Encoder<BytesMut> for JsonCodec {
+ type Error = io::Error;
+
+ fn encode(&mut self, data: BytesMut, buf: &mut BytesMut) -> Result<(), io::Error> {
+ buf.reserve(data.len());
+ buf.put(data);
+ Ok(())
+ }
+}
+
+impl Decoder for JsonCodec {
+ type Item = Value;
+ type Error = io::Error;
+
+ fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Value>, io::Error> {
+ if src.is_empty() {
+ return Ok(None);
+ }
+
+ match serde_json::from_slice::<Value>(src) {
+ Ok(val) => {
+ src.clear();
+
+ Ok(Some(val))
+ }
+ Err(ref e) if e.is_eof() => Ok(None),
+ Err(e) => Err(e.into()),
+ }
+ }
+}
diff --git a/client/src/transports/ipc.rs b/client/src/transports/ipc.rs
new file mode 100644
index 0000000..09798a7
--- /dev/null
+++ b/client/src/transports/ipc.rs
@@ -0,0 +1,18 @@
+use crate::transports::{Receiver, Sender, codec::JsonCodec};
+use futures_util::stream::StreamExt;
+use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
+use std::{io::Error, path::Path};
+use tokio::net::UnixStream;
+use tokio_util::codec::Framed;
+
+pub async fn connect(
+ socket: impl AsRef<Path>,
+) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> {
+ let connection = UnixStream::connect(socket).await?;
+ let (sink, stream) = Framed::new(connection, JsonCodec).split();
+
+ let sender = Sender { inner: sink };
+ let receiver = Receiver { inner: stream };
+
+ Ok((sender, receiver))
+}
diff --git a/client/src/transports/mod.rs b/client/src/transports/mod.rs
new file mode 100644
index 0000000..7456643
--- /dev/null
+++ b/client/src/transports/mod.rs
@@ -0,0 +1,115 @@
+mod codec;
+pub mod ipc;
+pub mod tcp;
+
+use bytes::BytesMut;
+use futures_util::{Sink, SinkExt, Stream, stream::StreamExt};
+use jsonrpsee::core::{
+ async_trait,
+ client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
+};
+use serde_json::{Value, json};
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+enum TransportError {
+ #[error("Connection closed.")]
+ ConnectionClosed,
+
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+
+ #[error("Unkown error: {0}")]
+ Unknown(String),
+}
+
+struct Sender<T: Send + Sink<BytesMut>> {
+ inner: T,
+}
+
+#[async_trait]
+impl<T: Send + Sink<BytesMut, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
+ for Sender<T>
+{
+ type Error = TransportError;
+
+ async fn send(&mut self, body: String) -> Result<(), Self::Error> {
+ let mut message: Value =
+ serde_json::from_str(&body).map_err(|e| TransportError::Unknown(e.to_string()))?;
+
+ // NOTE(mnaser): In order to be able to use the subscription client, we need to
+ // drop the subscription message for the "update" method, as the
+ // remote doesn't support JSON-RPC 2.0.
+ if message["method"] == json!("update") {
+ return Ok(());
+ }
+
+ // NOTE(mnaser): jsonrpsee runs using JSON-RPC 2.0 only which the remote doesn't
+ // support, so we intercept the message, remove "jsonrpc" and then
+ // send the message.
+ message.as_object_mut().map(|obj| obj.remove("jsonrpc"));
+
+ // NOTE(mnaser): OVSDB expects all requests to have a "params" key, so we add an
+ // empty array if it doesn't exist.
+ if !message.as_object().unwrap().contains_key("params") {
+ message["params"] = json!([]);
+ }
+
+ self.inner
+ .send(BytesMut::from(message.to_string().as_str()))
+ .await
+ .map_err(|e| TransportError::Unknown(e.to_string()))?;
+
+ Ok(())
+ }
+
+ async fn close(&mut self) -> Result<(), Self::Error> {
+ self.inner
+ .close()
+ .await
+ .map_err(|e| TransportError::Unknown(e.to_string()))?;
+
+ Ok(())
+ }
+}
+
+struct Receiver<T: Send + Stream> {
+ inner: T,
+}
+
+#[async_trait]
+impl<T: Send + Stream<Item = Result<Value, std::io::Error>> + Unpin + 'static> TransportReceiverT
+ for Receiver<T>
+{
+ type Error = TransportError;
+
+ async fn receive(&mut self) -> Result<ReceivedMessage, Self::Error> {
+ match self.inner.next().await {
+ None => Err(TransportError::ConnectionClosed),
+ Some(Ok(mut message)) => {
+ // NOTE(mnaser): jsonrpsee runs using JSON-RPC 2.0 only which the remote doesn't
+ // support, so we intercept the message, add "jsonrpc" and then
+ // send the message.
+ message
+ .as_object_mut()
+ .map(|obj| obj.insert("jsonrpc".to_string(), json!("2.0")));
+
+ // NOTE(mnaser): jsonrpsee expects no error field if there is a result, due to the
+ // remote not supporting JSON-RPC 2.0, we need to remove the "error"
+ // field if there is a "result" field.
+ if message.as_object().unwrap().contains_key("result") {
+ message.as_object_mut().map(|obj| obj.remove("error"));
+ }
+
+ // NOTE(mnaser): If a message comes in with it's "id" field set to null, then
+ // we remove it.
+ if message.as_object().unwrap().contains_key("id") && message["id"] == json!(null) {
+ message.as_object_mut().map(|obj| obj.remove("id"));
+ }
+
+ Ok(ReceivedMessage::Bytes(message.to_string().into_bytes()))
+ }
+ Some(Err(e)) => Err(TransportError::Io(e)),
+ }
+ }
+}
diff --git a/client/src/transports/tcp.rs b/client/src/transports/tcp.rs
new file mode 100644
index 0000000..2599b64
--- /dev/null
+++ b/client/src/transports/tcp.rs
@@ -0,0 +1,18 @@
+use crate::transports::{Receiver, Sender, codec::JsonCodec};
+use futures_util::stream::StreamExt;
+use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
+use std::io::Error;
+use tokio::net::{TcpStream, ToSocketAddrs};
+use tokio_util::codec::Framed;
+
+pub async fn connect(
+ socket: impl ToSocketAddrs,
+) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> {
+ let connection = TcpStream::connect(socket).await?;
+ let (sink, stream) = Framed::new(connection, JsonCodec).split();
+
+ let sender = Sender { inner: sink };
+ let receiver = Receiver { inner: stream };
+
+ Ok((sender, receiver))
+}
diff --git a/derive/Cargo.toml b/derive/Cargo.toml
new file mode 100644
index 0000000..99082fd
--- /dev/null
+++ b/derive/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "ovsdb-derive"
+version = "0.0.1"
+edition = "2021"
+description = "Derive macro for OVSDB table structs"
+license = "Apache-2.0"
+keywords = ["ovsdb", "derive", "macro"]
+categories = ["database"]
+repository = "https://review.vexxhost.dev/plugins/gitiles/ovsdb"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "2.0", features = ["full", "extra-traits"] }
+quote = "1.0"
+proc-macro2 = "1.0"
+ovsdb-schema = { version = "0.0.1", path = "../schema" }
+
+[dev-dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+uuid = { version = "1.0", features = ["v4"] }
diff --git a/derive/README.md b/derive/README.md
new file mode 100644
index 0000000..9684136
--- /dev/null
+++ b/derive/README.md
@@ -0,0 +1,64 @@
+# OVSDB Derive
+
+A procedural macro crate for Rust to generate code for OVSDB table structs.
+
+## Overview
+
+This crate provides two approaches for working with OVSDB tables:
+
+- `#[ovsdb_object]` attribute macro: Automatically adds `_uuid` and `_version` fields to your struct
+- `#[derive(OVSDB)]` derive macro: requires manual fields but offers more control
+
+## Usage
+
+You can either use the attribute macro or the derive macro to generate code for your OVSDB table structs. For more details
+on how to use the library, check out the examples in the `examples` directory.
+
+### Attribute Macro (Recommended)
+
+```rust
+use ovsdb_derive::ovsdb_object;
+use std::collections::HashMap;
+
+#[ovsdb_object]
+pub struct NbGlobal {
+ pub name: Option<String>,
+ pub nb_cfg: Option<i64>,
+ pub external_ids: Option<HashMap<String, String>>,
+ // No need to add _uuid and _version fields
+}
+```
+
+### Derive Macro (Alternative)
+
+```rust
+use ovsdb_derive::OVSDB;
+use std::collections::HashMap;
+use uuid::Uuid;
+
+#[derive(Debug, Clone, PartialEq, OVSDB)]
+pub struct NbGlobal {
+ pub name: Option<String>,
+ pub nb_cfg: Option<i64>,
+ pub external_ids: Option<HashMap<String, String>>,
+
+ // Required fields with the derive approach
+ pub _uuid: Option<Uuid>,
+ pub _version: Option<Uuid>,
+}
+```
+
+## Generated Code
+
+Both macros generate the following implementations:
+
+- `new()` method that creates a new instance with default values
+- `to_map()` method that converts the struct to a HashMap for OVSDB serialization
+- `from_map()` method that creates a struct from a HashMap received from OVSDB
+- `Default` trait implementation
+- `serde::Serialize` trait implementation
+- `serde::Deserialize` trait implementation
+
+## License
+
+This project is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
diff --git a/derive/examples/attribute.rs b/derive/examples/attribute.rs
new file mode 100644
index 0000000..9fb29df
--- /dev/null
+++ b/derive/examples/attribute.rs
@@ -0,0 +1,47 @@
+use ovsdb_derive::ovsdb_object;
+use std::collections::HashMap;
+use uuid::Uuid;
+
+#[ovsdb_object]
+#[derive(Debug, Clone, PartialEq)]
+pub struct NbGlobal {
+ pub name: String,
+ pub nb_cfg: i64,
+ pub nb_cfg_timestamp: i64,
+ pub sb_cfg: i64,
+ pub sb_cfg_timestamp: i64,
+ pub hv_cfg: i64,
+ pub hv_cfg_timestamp: i64,
+ pub external_ids: HashMap<String, String>,
+ pub connections: Vec<Uuid>,
+ pub ssl: Vec<Uuid>,
+ pub options: HashMap<String, String>,
+ pub ipsec: bool,
+}
+
+fn main() {
+ // Create a new NbGlobal instance
+ let mut nb_global = NbGlobal::new();
+
+ // Set some values
+ nb_global.name = "global".to_string();
+ nb_global.nb_cfg = 0;
+ nb_global
+ .external_ids
+ .insert("test".to_string(), "value".to_string());
+
+ // Convert to a HashMap for OVSDB serialization
+ let map = nb_global.to_map();
+ println!("{:?}", map);
+
+ // Convert to JSON for sending to OVSDB
+ let json = serde_json::to_string(&map).unwrap();
+ println!("{}", json);
+
+ // Simulate receiving JSON from OVSDB
+ let received_map: HashMap<String, serde_json::Value> = serde_json::from_str(&json).unwrap();
+
+ // Convert back to NbGlobal
+ let parsed_nb_global = NbGlobal::from_map(&received_map).unwrap();
+ println!("{:?}", parsed_nb_global);
+}
diff --git a/derive/examples/derive.rs b/derive/examples/derive.rs
new file mode 100644
index 0000000..08233d0
--- /dev/null
+++ b/derive/examples/derive.rs
@@ -0,0 +1,50 @@
+use ovsdb_derive::OVSDB;
+use std::collections::HashMap;
+use uuid::Uuid;
+
+#[derive(Debug, Clone, PartialEq, OVSDB)]
+pub struct NbGlobal {
+ pub name: String,
+ pub nb_cfg: i64,
+ pub nb_cfg_timestamp: i64,
+ pub sb_cfg: i64,
+ pub sb_cfg_timestamp: i64,
+ pub hv_cfg: i64,
+ pub hv_cfg_timestamp: i64,
+ pub external_ids: HashMap<String, String>,
+ pub connections: Vec<Uuid>,
+ pub ssl: Vec<Uuid>,
+ pub options: HashMap<String, String>,
+ pub ipsec: bool,
+
+ // Required fields
+ pub _uuid: Option<Uuid>,
+ pub _version: Option<Uuid>,
+}
+
+fn main() {
+ // Create a new NbGlobal instance
+ let mut nb_global = NbGlobal::new();
+
+ // Set some values
+ nb_global.name = "global".to_string();
+ nb_global.nb_cfg = 0;
+ nb_global
+ .external_ids
+ .insert("test".to_string(), "value".to_string());
+
+ // Convert to a HashMap for OVSDB serialization
+ let map = nb_global.to_map();
+ println!("{:?}", map);
+
+ // Convert to JSON for sending to OVSDB
+ let json = serde_json::to_string(&map).unwrap();
+ println!("{}", json);
+
+ // Simulate receiving JSON from OVSDB
+ let received_map: HashMap<String, serde_json::Value> = serde_json::from_str(&json).unwrap();
+
+ // Convert back to NbGlobal
+ let parsed_nb_global = NbGlobal::from_map(&received_map).unwrap();
+ println!("{:?}", parsed_nb_global);
+}
diff --git a/derive/src/lib.rs b/derive/src/lib.rs
new file mode 100644
index 0000000..149b437
--- /dev/null
+++ b/derive/src/lib.rs
@@ -0,0 +1,319 @@
+extern crate proc_macro;
+
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, parse_quote, Data, DeriveInput, Fields};
+
+/// Attribute macro for OVSDB table structs
+///
+/// This macro automatically adds `_uuid` and `_version` fields to your struct
+/// and generates the necessary implementations for it to work with OVSDB.
+///
+/// # Example
+///
+/// ```rust
+/// use ovsdb_derive::ovsdb_object;
+/// use std::collections::HashMap;
+///
+/// #[ovsdb_object]
+/// pub struct NbGlobal {
+/// pub name: Option<String>,
+/// pub nb_cfg: Option<i64>,
+/// pub external_ids: Option<HashMap<String, String>>,
+/// }
+/// ```
+#[proc_macro_attribute]
+pub fn ovsdb_object(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ // Parse the struct definition
+ let mut input = parse_macro_input!(item as DeriveInput);
+
+ // Add _uuid and _version fields if they don't exist
+ if let Data::Struct(ref mut data_struct) = input.data {
+ if let Fields::Named(ref mut fields) = data_struct.fields {
+ // Check if _uuid and _version already exist
+ let has_uuid = fields
+ .named
+ .iter()
+ .any(|f| f.ident.as_ref().is_some_and(|i| i == "_uuid"));
+ let has_version = fields
+ .named
+ .iter()
+ .any(|f| f.ident.as_ref().is_some_and(|i| i == "_version"));
+
+ // Add fields if they don't exist
+ if !has_uuid {
+ // Add _uuid field
+ fields.named.push(parse_quote! {
+ pub _uuid: Option<uuid::Uuid>
+ });
+ }
+ if !has_version {
+ // Add _version field
+ fields.named.push(parse_quote! {
+ pub _version: Option<uuid::Uuid>
+ });
+ }
+ }
+ }
+
+ // Get the name of the struct
+ let struct_name = &input.ident;
+
+ // Extract field names and types, excluding _uuid and _version
+ let mut field_names = Vec::new();
+ let mut field_types = Vec::new();
+
+ if let Data::Struct(ref data_struct) = input.data {
+ if let Fields::Named(ref fields) = data_struct.fields {
+ for field in &fields.named {
+ if let Some(ident) = &field.ident {
+ if ident == "_uuid" || ident == "_version" {
+ continue;
+ }
+ field_names.push(ident);
+ field_types.push(&field.ty);
+ }
+ }
+ }
+ }
+
+ // Generate implementations
+ let implementation = quote! {
+ // Re-export the input struct with the added fields
+ #input
+
+ // Automatically import necessary items from ovsdb-schema
+ use ::ovsdb_schema::{extract_uuid, OvsdbSerializableExt};
+
+ impl #struct_name {
+ /// Create a new instance with default values
+ pub fn new() -> Self {
+ Self {
+ #(
+ #field_names: Default::default(),
+ )*
+ _uuid: None,
+ _version: None,
+ }
+ }
+
+ /// Convert to a HashMap for OVSDB serialization
+ pub fn to_map(&self) -> std::collections::HashMap<String, serde_json::Value> {
+ let mut map = std::collections::HashMap::new();
+
+ #(
+ // Skip None values
+ let field_value = &self.#field_names;
+ if let Some(value) = field_value.to_ovsdb_json() {
+ map.insert(stringify!(#field_names).to_string(), value);
+ }
+ )*
+
+ map
+ }
+
+ /// Create from a HashMap received from OVSDB
+ pub fn from_map(map: &std::collections::HashMap<String, serde_json::Value>) -> Result<Self, String> {
+ let mut result = Self::new();
+
+ // Extract UUID if present
+ if let Some(uuid_val) = map.get("_uuid") {
+ if let Some(uuid) = extract_uuid(uuid_val) {
+ result._uuid = Some(uuid);
+ }
+ }
+
+ // Extract version if present
+ if let Some(version_val) = map.get("_version") {
+ if let Some(version) = extract_uuid(version_val) {
+ result._version = Some(version);
+ }
+ }
+
+ // Extract other fields
+ #(
+ if let Some(value) = map.get(stringify!(#field_names)) {
+ result.#field_names = <#field_types>::from_ovsdb_json(value)
+ .ok_or_else(|| format!("Failed to parse field {}", stringify!(#field_names)))?;
+ }
+ )*
+
+ Ok(result)
+ }
+ }
+
+ impl Default for #struct_name {
+ fn default() -> Self {
+ Self::new()
+ }
+ }
+
+ impl serde::Serialize for #struct_name {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer
+ {
+ self.to_map().serialize(serializer)
+ }
+ }
+
+ impl<'de> serde::Deserialize<'de> for #struct_name {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>
+ {
+ let map = std::collections::HashMap::<String, serde_json::Value>::deserialize(deserializer)?;
+ Self::from_map(&map).map_err(serde::de::Error::custom)
+ }
+ }
+ };
+
+ // Return the modified struct and implementations
+ TokenStream::from(implementation)
+}
+
+/// Derive macro for OVSDB table structs (requires manual _uuid and _version fields)
+///
+/// This macro generates the necessary implementations for a struct to work with OVSDB.
+/// The struct must have `_uuid` and `_version` fields of type `Option<uuid::Uuid>`.
+///
+/// # Example
+///
+/// ```rust
+/// use ovsdb_derive::OVSDB;
+/// use std::collections::HashMap;
+/// use uuid::Uuid;
+///
+/// #[derive(Debug, Clone, PartialEq, OVSDB)]
+/// pub struct NbGlobal {
+/// pub name: Option<String>,
+/// pub nb_cfg: Option<i64>,
+/// pub external_ids: Option<HashMap<String, String>>,
+///
+/// // Required fields
+/// pub _uuid: Option<Uuid>,
+/// pub _version: Option<Uuid>,
+/// }
+/// ```
+#[proc_macro_derive(OVSDB)]
+pub fn ovsdb_derive(input: TokenStream) -> TokenStream {
+ // Parse the input tokens into a syntax tree
+ let input = parse_macro_input!(input as DeriveInput);
+
+ // Get the name of the struct
+ let struct_name = &input.ident;
+
+ // Check if the input is a struct
+ let fields = match &input.data {
+ Data::Struct(data_struct) => match &data_struct.fields {
+ Fields::Named(fields_named) => &fields_named.named,
+ _ => panic!("OVSDB can only be derived for structs with named fields"),
+ },
+ _ => panic!("OVSDB can only be derived for structs"),
+ };
+
+ // Extract field names and types, excluding _uuid and _version
+ let mut field_names = Vec::new();
+ let mut field_types = Vec::new();
+
+ for field in fields {
+ if let Some(ident) = &field.ident {
+ if ident == "_uuid" || ident == "_version" {
+ continue;
+ }
+ field_names.push(ident);
+ field_types.push(&field.ty);
+ }
+ }
+
+ // Generate code for the implementation
+ let expanded = quote! {
+ // Automatically import necessary items from ovsdb-schema
+ use ::ovsdb_schema::{extract_uuid, OvsdbSerializableExt};
+
+ impl #struct_name {
+ /// Create a new instance with default values
+ pub fn new() -> Self {
+ Self {
+ #(
+ #field_names: Default::default(),
+ )*
+ _uuid: None,
+ _version: None,
+ }
+ }
+
+ /// Convert to a HashMap for OVSDB serialization
+ pub fn to_map(&self) -> std::collections::HashMap<String, serde_json::Value> {
+ let mut map = std::collections::HashMap::new();
+
+ #(
+ // Skip None values
+ let field_value = &self.#field_names;
+ if let Some(value) = field_value.to_ovsdb_json() {
+ map.insert(stringify!(#field_names).to_string(), value);
+ }
+ )*
+
+ map
+ }
+
+ /// Create from a HashMap received from OVSDB
+ pub fn from_map(map: &std::collections::HashMap<String, serde_json::Value>) -> Result<Self, String> {
+ let mut result = Self::new();
+
+ // Extract UUID if present
+ if let Some(uuid_val) = map.get("_uuid") {
+ if let Some(uuid) = extract_uuid(uuid_val) {
+ result._uuid = Some(uuid);
+ }
+ }
+
+ // Extract version if present
+ if let Some(version_val) = map.get("_version") {
+ if let Some(version) = extract_uuid(version_val) {
+ result._version = Some(version);
+ }
+ }
+
+ // Extract other fields
+ #(
+ if let Some(value) = map.get(stringify!(#field_names)) {
+ result.#field_names = <#field_types>::from_ovsdb_json(value)
+ .ok_or_else(|| format!("Failed to parse field {}", stringify!(#field_names)))?;
+ }
+ )*
+
+ Ok(result)
+ }
+ }
+
+ impl Default for #struct_name {
+ fn default() -> Self {
+ Self::new()
+ }
+ }
+
+ impl serde::Serialize for #struct_name {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer
+ {
+ self.to_map().serialize(serializer)
+ }
+ }
+
+ impl<'de> serde::Deserialize<'de> for #struct_name {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>
+ {
+ let map = std::collections::HashMap::<String, serde_json::Value>::deserialize(deserializer)?;
+ Self::from_map(&map).map_err(serde::de::Error::custom)
+ }
+ }
+ };
+
+ // Return the generated code
+ TokenStream::from(expanded)
+}
diff --git a/playbooks/clippy.yml b/playbooks/clippy.yml
new file mode 100644
index 0000000..47f7ed4
--- /dev/null
+++ b/playbooks/clippy.yml
@@ -0,0 +1,5 @@
+- hosts: all
+ tasks:
+ - shell: cargo clippy
+ args:
+ chdir: "{{ zuul.project.src_dir }}"
diff --git a/playbooks/pre.yml b/playbooks/pre.yml
new file mode 100644
index 0000000..c8244d1
--- /dev/null
+++ b/playbooks/pre.yml
@@ -0,0 +1,10 @@
+- hosts: all
+ roles:
+ - ensure-rust
+
+ tasks:
+ - name: Install dependencies
+ become: true
+ package:
+ name: build-essential
+ state: present
diff --git a/playbooks/test.yml b/playbooks/test.yml
new file mode 100644
index 0000000..ffe4dd9
--- /dev/null
+++ b/playbooks/test.yml
@@ -0,0 +1,5 @@
+- hosts: all
+ tasks:
+ - shell: cargo test
+ args:
+ chdir: "{{ zuul.project.src_dir }}"
diff --git a/schema/Cargo.toml b/schema/Cargo.toml
new file mode 100644
index 0000000..a7f44a6
--- /dev/null
+++ b/schema/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "ovsdb-schema"
+version = "0.0.1"
+edition = "2021"
+description = "Rust types and serialization for the Open vSwitch Database Management Protocol (OVSDB)"
+license = "Apache-2.0"
+keywords = ["ovsdb", "ovs", "openvswitch", "database", "serialization"]
+categories = ["database", "network-programming", "api-bindings"]
+repository = "https://review.vexxhost.dev/plugins/gitiles/ovsdb"
+
+[dependencies]
+serde = { version = "1.0.218", features = ["derive"] }
+serde_json = "1.0.140"
+uuid = { version = "1.15.1", features = ["serde"] }
+
+[dev-dependencies]
+ovsdb-derive = { path = "../derive" }
diff --git a/schema/README.md b/schema/README.md
new file mode 100644
index 0000000..59a218c
--- /dev/null
+++ b/schema/README.md
@@ -0,0 +1,103 @@
+# ovsdb-schema
+
+A Rust implementation of the OVSDB protocol serialization and deserialization types.
+
+## Overview
+
+This crate provides the core primitives and traits needed to work with the Open vSwitch Database Management Protocol (OVSDB) as defined in [RFC7047](https://datatracker.ietf.org/doc/html/rfc7047). It includes:
+
+- Type definitions for OVSDB data structures
+- Serialization/deserialization between Rust types and OVSDB JSON format
+- Traits to make your own types compatible with OVSDB
+
+This crate is designed to be used alongside `ovsdb-derive` for a complete OVSDB client implementation.
+
+## Features
+
+- `OvsdbAtom` and `OvsdbValue` types representing OVSDB's basic data types
+- `OvsdbSerializable` trait for converting between Rust types and OVSDB values
+- Implementations for common Rust types like `String`, `i64`, `bool`, etc.
+- Support for collections like `Vec<T>` and `HashMap<K, V>`
+- Helper functions for UUID handling
+- Full support for OVSDB's type system: atoms, sets, and maps
+
+## Usage
+
+### Basic Usage
+
+```rust
+use ovsdb_schema::{OvsdbSerializable, OvsdbSerializableExt};
+use std::collections::HashMap;
+use uuid::Uuid;
+
+// Use the trait directly
+let my_string = "hello".to_string();
+let ovsdb_value = my_string.to_ovsdb();
+let json_value = my_string.to_ovsdb_json().unwrap();
+
+// Extract UUIDs from JSON values
+let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
+let uuid = Uuid::parse_str(uuid_str).unwrap();
+let json_value = serde_json::json!(["uuid", uuid_str]);
+let extracted_uuid = ovsdb_schema::extract_uuid(&json_value).unwrap();
+assert_eq!(uuid, extracted_uuid);
+```
+
+### With `ovsdb-derive`
+
+This crate is designed to work with the companion `ovsdb-derive` crate:
+
+```rust
+use ovsdb_derive::ovsdb_object;
+use std::collections::HashMap;
+
+#[ovsdb_object]
+pub struct NbGlobal {
+ pub name: Option<String>,
+ pub nb_cfg: Option<i64>,
+ pub external_ids: Option<HashMap<String, String>>,
+}
+
+// The macro adds _uuid and _version fields and implements
+// OvsdbSerializable automatically
+```
+
+## Type Conversion
+
+| Rust Type | OVSDB Type |
+|-----------|------------|
+| `String` | string |
+| `i64` | integer |
+| `f64` | real |
+| `bool` | boolean |
+| `Uuid` | uuid |
+| `Vec<T>` | set |
+| `HashMap<K, V>` | map |
+| `Option<T>` | value or empty set |
+
+## Custom Types
+
+Implement `OvsdbSerializable` for your custom types:
+
+```rust
+use ovsdb_schema::{OvsdbSerializable, OvsdbValue, OvsdbAtom};
+
+struct MyType(String);
+
+impl OvsdbSerializable for MyType {
+ fn to_ovsdb(&self) -> OvsdbValue {
+ OvsdbValue::Atom(OvsdbAtom::String(self.0.clone()))
+ }
+
+ fn from_ovsdb(value: &OvsdbValue) -> Option<Self> {
+ match value {
+ OvsdbValue::Atom(OvsdbAtom::String(s)) => Some(MyType(s.clone())),
+ _ => None,
+ }
+ }
+}
+```
+
+## License
+
+This project is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
diff --git a/schema/src/lib.rs b/schema/src/lib.rs
new file mode 100644
index 0000000..e246b8a
--- /dev/null
+++ b/schema/src/lib.rs
@@ -0,0 +1,353 @@
+use serde::{Serialize, Serializer};
+use std::collections::HashMap;
+use uuid::Uuid;
+
+/// Primitive OVSDB Atom types
+#[derive(Debug, Clone, PartialEq)]
+pub enum OvsdbAtom {
+ String(String),
+ Integer(i64),
+ Real(f64),
+ Boolean(bool),
+ Uuid(Uuid),
+ NamedUuid(String),
+}
+
+/// OVSDB Value types (atom, set, or map)
+#[derive(Debug, Clone, PartialEq)]
+pub enum OvsdbValue {
+ Atom(OvsdbAtom),
+ Set(Vec<OvsdbAtom>),
+ Map(Vec<(OvsdbAtom, OvsdbAtom)>),
+}
+
+/// Trait for converting between Rust types and OVSDB Values
+pub trait OvsdbSerializable: Sized {
+ fn to_ovsdb(&self) -> OvsdbValue;
+ fn from_ovsdb(value: &OvsdbValue) -> Option<Self>;
+}
+
+impl<T: OvsdbSerializable> OvsdbSerializable for Option<T> {
+ fn to_ovsdb(&self) -> OvsdbValue {
+ match self {
+ Some(val) => val.to_ovsdb(),
+ None => OvsdbValue::Set(vec![]), // Empty set for None
+ }
+ }
+
+ fn from_ovsdb(value: &OvsdbValue) -> Option<Self> {
+ T::from_ovsdb(value).map(Some)
+ }
+}
+
+impl OvsdbSerializable for String {
+ fn to_ovsdb(&self) -> OvsdbValue {
+ OvsdbValue::Atom(OvsdbAtom::String(self.clone()))
+ }
+
+ fn from_ovsdb(value: &OvsdbValue) -> Option<Self> {
+ match value {
+ OvsdbValue::Atom(OvsdbAtom::String(s)) => Some(s.clone()),
+ _ => None,
+ }
+ }
+}
+
+impl OvsdbSerializable for i64 {
+ fn to_ovsdb(&self) -> OvsdbValue {
+ OvsdbValue::Atom(OvsdbAtom::Integer(*self))
+ }
+
+ fn from_ovsdb(value: &OvsdbValue) -> Option<Self> {
+ match value {
+ OvsdbValue::Atom(OvsdbAtom::Integer(i)) => Some(*i),
+ _ => None,
+ }
+ }
+}
+
+impl OvsdbSerializable for f64 {
+ fn to_ovsdb(&self) -> OvsdbValue {
+ OvsdbValue::Atom(OvsdbAtom::Real(*self))
+ }
+
+ fn from_ovsdb(value: &OvsdbValue) -> Option<Self> {
+ match value {
+ OvsdbValue::Atom(OvsdbAtom::Real(r)) => Some(*r),
+ _ => None,
+ }
+ }
+}
+
+impl OvsdbSerializable for bool {
+ fn to_ovsdb(&self) -> OvsdbValue {
+ OvsdbValue::Atom(OvsdbAtom::Boolean(*self))
+ }
+
+ fn from_ovsdb(value: &OvsdbValue) -> Option<Self> {
+ match value {
+ OvsdbValue::Atom(OvsdbAtom::Boolean(b)) => Some(*b),
+ _ => None,
+ }
+ }
+}
+
+impl OvsdbSerializable for Uuid {
+ fn to_ovsdb(&self) -> OvsdbValue {
+ OvsdbValue::Atom(OvsdbAtom::Uuid(*self))
+ }
+
+ fn from_ovsdb(value: &OvsdbValue) -> Option<Self> {
+ match value {
+ OvsdbValue::Atom(OvsdbAtom::Uuid(uuid)) => Some(*uuid),
+ _ => None,
+ }
+ }
+}
+
+impl<T: OvsdbSerializable> OvsdbSerializable for Vec<T> {
+ fn to_ovsdb(&self) -> OvsdbValue {
+ if self.is_empty() {
+ return OvsdbValue::Set(vec![]);
+ }
+
+ // Try to convert each item to an OvsdbAtom
+ let mut atoms = Vec::with_capacity(self.len());
+ for item in self {
+ match item.to_ovsdb() {
+ OvsdbValue::Atom(atom) => atoms.push(atom),
+ _ => return OvsdbValue::Set(vec![]), // Invalid conversion, return empty set
+ }
+ }
+
+ OvsdbValue::Set(atoms)
+ }
+
+ fn from_ovsdb(value: &OvsdbValue) -> Option<Self> {
+ match value {
+ OvsdbValue::Set(atoms) => {
+ let mut result = Vec::with_capacity(atoms.len());
+ for atom in atoms {
+ if let Some(item) = T::from_ovsdb(&OvsdbValue::Atom(atom.clone())) {
+ result.push(item);
+ } else {
+ return None;
+ }
+ }
+ Some(result)
+ }
+ // Handle single atom as a one-element set
+ OvsdbValue::Atom(atom) => {
+ T::from_ovsdb(&OvsdbValue::Atom(atom.clone())).map(|item| vec![item])
+ }
+ _ => None,
+ }
+ }
+}
+
+impl<K: OvsdbSerializable + ToString + Eq + std::hash::Hash, V: OvsdbSerializable> OvsdbSerializable
+ for HashMap<K, V>
+{
+ fn to_ovsdb(&self) -> OvsdbValue {
+ let mut pairs = Vec::with_capacity(self.len());
+
+ for (key, value) in self {
+ if let OvsdbValue::Atom(key_atom) = key.to_ovsdb() {
+ if let OvsdbValue::Atom(value_atom) = value.to_ovsdb() {
+ pairs.push((key_atom, value_atom));
+ continue;
+ }
+ }
+ return OvsdbValue::Map(vec![]);
+ }
+
+ OvsdbValue::Map(pairs)
+ }
+
+ fn from_ovsdb(value: &OvsdbValue) -> Option<Self> {
+ match value {
+ OvsdbValue::Map(map) => {
+ let mut result = HashMap::with_capacity(map.len());
+
+ for (key, val) in map {
+ if let Some(key_converted) = K::from_ovsdb(&OvsdbValue::Atom(key.clone())) {
+ if let Some(val_converted) = V::from_ovsdb(&OvsdbValue::Atom(val.clone())) {
+ result.insert(key_converted, val_converted);
+ } else {
+ return None;
+ }
+ } else {
+ return None;
+ }
+ }
+
+ Some(result)
+ }
+ _ => None,
+ }
+ }
+}
+
+/// Custom serde serialization format for OvsdbValue
+/// Implements the specific JSON format required by OVSDB
+impl Serialize for OvsdbValue {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ match self {
+ OvsdbValue::Atom(atom) => atom.serialize(serializer),
+ OvsdbValue::Set(set) => {
+ if set.is_empty() {
+ let empty: Vec<String> = vec![];
+ empty.serialize(serializer)
+ } else if set.len() == 1 {
+ set[0].serialize(serializer)
+ } else {
+ let wrapper = ("set", set);
+ wrapper.serialize(serializer)
+ }
+ }
+ OvsdbValue::Map(map) => {
+ let pairs: Vec<[&OvsdbAtom; 2]> = map.iter().map(|(k, v)| [k, v]).collect();
+ let wrapper = ("map", pairs);
+ wrapper.serialize(serializer)
+ }
+ }
+ }
+}
+
+impl Serialize for OvsdbAtom {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ match self {
+ OvsdbAtom::String(s) => s.serialize(serializer),
+ OvsdbAtom::Integer(i) => i.serialize(serializer),
+ OvsdbAtom::Real(r) => r.serialize(serializer),
+ OvsdbAtom::Boolean(b) => b.serialize(serializer),
+ OvsdbAtom::Uuid(uuid) => {
+ let wrapper = ("uuid", uuid.to_string());
+ wrapper.serialize(serializer)
+ }
+ OvsdbAtom::NamedUuid(name) => {
+ let wrapper = ("named-uuid", name);
+ wrapper.serialize(serializer)
+ }
+ }
+ }
+}
+
+/// Extension trait for OvsdbSerializable to handle JSON conversion
+pub trait OvsdbSerializableExt: OvsdbSerializable {
+ fn to_ovsdb_json(&self) -> Option<serde_json::Value> {
+ serde_json::to_value(self.to_ovsdb()).ok()
+ }
+
+ fn from_ovsdb_json(json: &serde_json::Value) -> Option<Self> {
+ // Convert JSON to OvsdbValue
+ let value = json_to_ovsdb_value(json)?;
+ Self::from_ovsdb(&value)
+ }
+}
+
+// Implement the extension trait for all types that implement OvsdbSerializable
+impl<T: OvsdbSerializable> OvsdbSerializableExt for T {}
+
+/// Helper function to extract a UUID from a JSON value
+pub fn extract_uuid(value: &serde_json::Value) -> Option<Uuid> {
+ if let serde_json::Value::Array(arr) = value {
+ if arr.len() == 2 && arr[0] == "uuid" {
+ if let serde_json::Value::String(uuid_str) = &arr[1] {
+ return Uuid::parse_str(uuid_str).ok();
+ }
+ }
+ }
+ None
+}
+
+/// Convert a JSON value to an OvsdbValue
+fn json_to_ovsdb_value(json: &serde_json::Value) -> Option<OvsdbValue> {
+ match json {
+ serde_json::Value::String(s) => Some(OvsdbValue::Atom(OvsdbAtom::String(s.clone()))),
+ serde_json::Value::Number(n) => {
+ if let Some(i) = n.as_i64() {
+ Some(OvsdbValue::Atom(OvsdbAtom::Integer(i)))
+ } else {
+ n.as_f64().map(|f| OvsdbValue::Atom(OvsdbAtom::Real(f)))
+ }
+ }
+ serde_json::Value::Bool(b) => Some(OvsdbValue::Atom(OvsdbAtom::Boolean(*b))),
+ serde_json::Value::Array(arr) => {
+ if arr.len() == 2 {
+ if let serde_json::Value::String(tag) = &arr[0] {
+ match tag.as_str() {
+ "uuid" => {
+ if let serde_json::Value::String(uuid_str) = &arr[1] {
+ if let Ok(uuid) = Uuid::parse_str(uuid_str) {
+ return Some(OvsdbValue::Atom(OvsdbAtom::Uuid(uuid)));
+ }
+ }
+ }
+ "named-uuid" => {
+ if let serde_json::Value::String(name) = &arr[1] {
+ return Some(OvsdbValue::Atom(OvsdbAtom::NamedUuid(name.clone())));
+ }
+ }
+ "set" => {
+ if let serde_json::Value::Array(elements) = &arr[1] {
+ let mut atoms = Vec::with_capacity(elements.len());
+ for elem in elements {
+ if let Some(OvsdbValue::Atom(atom)) = json_to_ovsdb_value(elem)
+ {
+ atoms.push(atom);
+ } else {
+ return None;
+ }
+ }
+ return Some(OvsdbValue::Set(atoms));
+ }
+ }
+ "map" => {
+ if let serde_json::Value::Array(pairs) = &arr[1] {
+ let mut map_pairs = Vec::with_capacity(pairs.len());
+ for pair in pairs {
+ if let serde_json::Value::Array(kv) = pair {
+ if kv.len() == 2 {
+ if let (
+ Some(OvsdbValue::Atom(key)),
+ Some(OvsdbValue::Atom(value)),
+ ) = (
+ json_to_ovsdb_value(&kv[0]),
+ json_to_ovsdb_value(&kv[1]),
+ ) {
+ map_pairs.push((key, value));
+ continue;
+ }
+ }
+ }
+ return None;
+ }
+ return Some(OvsdbValue::Map(map_pairs));
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ // Empty array means empty set
+ if arr.is_empty() {
+ return Some(OvsdbValue::Set(vec![]));
+ }
+
+ None
+ }
+ serde_json::Value::Null => {
+ // Null is represented as an empty set
+ Some(OvsdbValue::Set(vec![]))
+ }
+ _ => None,
+ }
+}
diff --git a/schema/tests/integration.rs b/schema/tests/integration.rs
new file mode 100644
index 0000000..aa80e18
--- /dev/null
+++ b/schema/tests/integration.rs
@@ -0,0 +1,274 @@
+use ovsdb_derive::ovsdb_object;
+use serde_json::Value;
+use std::collections::HashMap;
+use uuid::Uuid;
+
+#[ovsdb_object]
+#[derive(Debug, PartialEq)]
+pub struct NbGlobal {
+ pub name: Option<String>,
+ pub nb_cfg: Option<i64>,
+ pub nb_cfg_timestamp: Option<i64>,
+ pub sb_cfg: Option<i64>,
+ pub sb_cfg_timestamp: Option<i64>,
+ pub hv_cfg: Option<i64>,
+ pub hv_cfg_timestamp: Option<i64>,
+ pub external_ids: Option<HashMap<String, String>>,
+ pub connections: Option<Vec<Uuid>>,
+ pub ssl: Option<Vec<Uuid>>,
+ pub options: Option<HashMap<String, String>>,
+ pub ipsec: Option<bool>,
+
+ // Required fields
+ pub _uuid: Option<Uuid>,
+ pub _version: Option<Uuid>,
+}
+
+#[test]
+fn test_nb_global_deserialization() {
+ // The provided JSON sample
+ let json_str = r#"{
+ "connections": ["uuid", "601c7161-97df-42ae-b377-3baf21830d8f"],
+ "external_ids": ["map", [["test", "bara"]]],
+ "hv_cfg": 0,
+ "hv_cfg_timestamp": 0,
+ "ipsec": false,
+ "name": "global",
+ "nb_cfg": 0,
+ "nb_cfg_timestamp": 0,
+ "options": ["map", [["name", "global"], ["northd-backoff-interval-ms", "300"], ["northd_probe_interval", "5000"]]],
+ "sb_cfg": 0,
+ "sb_cfg_timestamp": 0,
+ "ssl": ["set", []]
+ }"#;
+
+ let json_value: Value = serde_json::from_str(json_str).unwrap();
+
+ // Parse the JSON to our NbGlobal struct
+ let nb_global =
+ NbGlobal::from_map(&serde_json::from_value(json_value.clone()).unwrap()).unwrap();
+
+ // Test individual fields
+ assert_eq!(nb_global.name, Some("global".to_string()));
+ assert_eq!(nb_global.ipsec, Some(false));
+ assert_eq!(nb_global.hv_cfg, Some(0));
+ assert_eq!(nb_global.hv_cfg_timestamp, Some(0));
+ assert_eq!(nb_global.nb_cfg, Some(0));
+ assert_eq!(nb_global.nb_cfg_timestamp, Some(0));
+ assert_eq!(nb_global.sb_cfg, Some(0));
+ assert_eq!(nb_global.sb_cfg_timestamp, Some(0));
+
+ // Test UUID field
+ let connection_uuid = Uuid::parse_str("601c7161-97df-42ae-b377-3baf21830d8f").unwrap();
+ assert_eq!(nb_global.connections, Some(vec![connection_uuid]));
+
+ // Test empty set
+ assert_eq!(nb_global.ssl, Some(vec![]));
+
+ // Test maps
+ let expected_external_ids = {
+ let mut map = HashMap::new();
+ map.insert("test".to_string(), "bara".to_string());
+ map
+ };
+ assert_eq!(nb_global.external_ids, Some(expected_external_ids));
+
+ let expected_options = {
+ let mut map = HashMap::new();
+ map.insert("name".to_string(), "global".to_string());
+ map.insert("northd-backoff-interval-ms".to_string(), "300".to_string());
+ map.insert("northd_probe_interval".to_string(), "5000".to_string());
+ map
+ };
+ assert_eq!(nb_global.options, Some(expected_options));
+}
+
+#[test]
+fn test_nb_global_serialization() {
+ // Create an NbGlobal object with the same values as the JSON sample
+ let mut nb_global = NbGlobal::new();
+
+ // Set scalar values
+ nb_global.name = Some("global".to_string());
+ nb_global.ipsec = Some(false);
+ nb_global.hv_cfg = Some(0);
+ nb_global.hv_cfg_timestamp = Some(0);
+ nb_global.nb_cfg = Some(0);
+ nb_global.nb_cfg_timestamp = Some(0);
+ nb_global.sb_cfg = Some(0);
+ nb_global.sb_cfg_timestamp = Some(0);
+
+ // Set UUID connection
+ let connection_uuid = Uuid::parse_str("601c7161-97df-42ae-b377-3baf21830d8f").unwrap();
+ nb_global.connections = Some(vec![connection_uuid]);
+
+ // Set empty SSL set
+ nb_global.ssl = Some(vec![]);
+
+ // Set maps
+ let mut external_ids = HashMap::new();
+ external_ids.insert("test".to_string(), "bara".to_string());
+ nb_global.external_ids = Some(external_ids);
+
+ let mut options = HashMap::new();
+ options.insert("name".to_string(), "global".to_string());
+ options.insert("northd-backoff-interval-ms".to_string(), "300".to_string());
+ options.insert("northd_probe_interval".to_string(), "5000".to_string());
+ nb_global.options = Some(options);
+
+ // Serialize to JSON
+ let serialized = nb_global.to_map();
+
+ // Verify each field
+ assert_eq!(serialized.get("name").unwrap().as_str().unwrap(), "global");
+ assert!(!serialized.get("ipsec").unwrap().as_bool().unwrap());
+ assert_eq!(serialized.get("hv_cfg").unwrap().as_i64().unwrap(), 0);
+
+ // Test UUID serialization
+ let connections_json = serialized.get("connections").unwrap();
+ assert!(connections_json.is_array());
+ let connections_array = connections_json.as_array().unwrap();
+ assert_eq!(connections_array[0].as_str().unwrap(), "uuid");
+ assert_eq!(
+ connections_array[1].as_str().unwrap(),
+ "601c7161-97df-42ae-b377-3baf21830d8f"
+ );
+
+ // Test empty set serialization
+ let ssl_json = serialized.get("ssl").unwrap();
+ assert!(ssl_json.is_array());
+ assert_eq!(ssl_json.as_array().unwrap().len(), 0);
+
+ // Test map serialization
+ let external_ids_json = serialized.get("external_ids").unwrap();
+ assert!(external_ids_json.is_array());
+ assert_eq!(
+ external_ids_json.as_array().unwrap()[0].as_str().unwrap(),
+ "map"
+ );
+
+ let options_json = serialized.get("options").unwrap();
+ assert!(options_json.is_array());
+ assert_eq!(options_json.as_array().unwrap()[0].as_str().unwrap(), "map");
+}
+
+#[test]
+fn test_round_trip() {
+ // JSON string representing an NB_Global object
+ let json_str = r#"{
+ "connections": ["uuid", "601c7161-97df-42ae-b377-3baf21830d8f"],
+ "external_ids": ["map", [["test", "bara"]]],
+ "hv_cfg": 0,
+ "hv_cfg_timestamp": 0,
+ "ipsec": false,
+ "name": "global",
+ "nb_cfg": 0,
+ "nb_cfg_timestamp": 0,
+ "options": ["map", [["name", "global"], ["northd-backoff-interval-ms", "300"], ["northd_probe_interval", "5000"]]],
+ "sb_cfg": 0,
+ "sb_cfg_timestamp": 0,
+ "ssl": ["set", []]
+ }"#;
+
+ // Deserialize from JSON string to NbGlobal object
+ let json_value: Value = serde_json::from_str(json_str).unwrap();
+ let nb_global = NbGlobal::from_map(&serde_json::from_value(json_value).unwrap()).unwrap();
+
+ // Serialize back to JSON
+ let serialized = serde_json::to_value(nb_global.to_map()).unwrap();
+
+ // Deserialize again
+ let nb_global2 = NbGlobal::from_map(&serde_json::from_value(serialized).unwrap()).unwrap();
+
+ // The two objects should be equal
+ assert_eq!(nb_global, nb_global2);
+}
+
+#[test]
+fn test_handle_single_element_set() {
+ // JSON with a single UUID in connections (no ["set", ...] wrapper)
+ let json_str = r#"{
+ "connections": ["uuid", "601c7161-97df-42ae-b377-3baf21830d8f"],
+ "name": "global"
+ }"#;
+
+ let json_value: Value = serde_json::from_str(json_str).unwrap();
+ let nb_global = NbGlobal::from_map(&serde_json::from_value(json_value).unwrap()).unwrap();
+
+ // Should be parsed as a Vec with one element
+ let connection_uuid = Uuid::parse_str("601c7161-97df-42ae-b377-3baf21830d8f").unwrap();
+ assert_eq!(nb_global.connections, Some(vec![connection_uuid]));
+}
+
+#[test]
+fn test_handle_multiple_element_set() {
+ // JSON with multiple UUIDs in connections using ["set", [...]] wrapper
+ let json_str = r#"{
+ "connections": ["set", [
+ ["uuid", "601c7161-97df-42ae-b377-3baf21830d8f"],
+ ["uuid", "701c7161-97df-42ae-b377-3baf21830d8f"]
+ ]],
+ "name": "global"
+ }"#;
+
+ let json_value: Value = serde_json::from_str(json_str).unwrap();
+ let nb_global = NbGlobal::from_map(&serde_json::from_value(json_value).unwrap()).unwrap();
+
+ // Should be parsed as a Vec with two elements
+ let uuid1 = Uuid::parse_str("601c7161-97df-42ae-b377-3baf21830d8f").unwrap();
+ let uuid2 = Uuid::parse_str("701c7161-97df-42ae-b377-3baf21830d8f").unwrap();
+ assert_eq!(nb_global.connections, Some(vec![uuid1, uuid2]));
+}
+
+#[test]
+fn test_empty_set() {
+ // JSON with empty set
+ let json_str = r#"{
+ "ssl": ["set", []],
+ "name": "global"
+ }"#;
+
+ let json_value: Value = serde_json::from_str(json_str).unwrap();
+ let nb_global = NbGlobal::from_map(&serde_json::from_value(json_value).unwrap()).unwrap();
+
+ // Should be parsed as an empty Vec
+ assert_eq!(nb_global.ssl, Some(vec![]));
+}
+
+#[test]
+fn test_serialization_single_element_set() {
+ let mut nb_global = NbGlobal::new();
+
+ // Set single UUID connection
+ let connection_uuid = Uuid::parse_str("601c7161-97df-42ae-b377-3baf21830d8f").unwrap();
+ nb_global.connections = Some(vec![connection_uuid]);
+
+ // Serialize to JSON
+ let serialized = nb_global.to_map();
+ let connections_json = serialized.get("connections").unwrap();
+
+ // Should be serialized as ["uuid", "..."] (not wrapped in ["set", [...]])
+ assert!(connections_json.is_array());
+ let connections_array = connections_json.as_array().unwrap();
+ assert_eq!(connections_array.len(), 2);
+ assert_eq!(connections_array[0].as_str().unwrap(), "uuid");
+}
+
+#[test]
+fn test_serialization_multiple_element_set() {
+ let mut nb_global = NbGlobal::new();
+
+ // Set multiple UUID connections
+ let uuid1 = Uuid::parse_str("601c7161-97df-42ae-b377-3baf21830d8f").unwrap();
+ let uuid2 = Uuid::parse_str("701c7161-97df-42ae-b377-3baf21830d8f").unwrap();
+ nb_global.connections = Some(vec![uuid1, uuid2]);
+
+ // Serialize to JSON
+ let serialized = nb_global.to_map();
+ let connections_json = serialized.get("connections").unwrap();
+
+ // Should be serialized as ["set", [...]]
+ assert!(connections_json.is_array());
+ let connections_array = connections_json.as_array().unwrap();
+ assert_eq!(connections_array[0].as_str().unwrap(), "set");
+}