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).
+
+[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](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 | [![crates.io](https://img.shields.io/crates/v/ovsdb-schema.svg)](https://crates.io/crates/ovsdb-schema) |
+| [`ovsdb-derive`](./derive) | Procedural macros for OVSDB struct generation | [![crates.io](https://img.shields.io/crates/v/ovsdb-derive.svg)](https://crates.io/crates/ovsdb-derive) |
+| [`ovsdb-client`](./client) | Async client for the OVSDB protocol | [![crates.io](https://img.shields.io/crates/v/ovsdb-client.svg)](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");
+}