Repository: near/borsh-rs
Pull request #309 (2024-09-10 19:35:29)

feat: ser/de for `IpAddr` type; schema for `Ipv4Addr`/`Ipv6Addr`/`IpAddr` types 
this is a WIP attempt to add the base IpAddr type to the schema.


willco-1: @dj8yfo yes! Sorry .. I got busy and forgot to come back to it. Thanks for the tips, this is very helpful.
dj8yfo: @race-of-sloths include
race-of-sloths-bot: @dj8yfo Thank you for calling!
    
@willco-1 Thank you for the contribution! Join [Race of Sloths](https://race-of-sloths.com) by simply mentioning me in your comment/PRs description and start collecting Sloth Points through contributions to open source projects.

<details><summary>What is the Race of Sloths</summary>
<p>
Race of Sloths is a friendly competition where you can participate in challenges and compete with other open-source contributors within your normal workflow

For contributors:

- Tag @race-of-sloths inside your pull requests
- Wait for the maintainer to review and score your pull request
- Check out your position in the [Leaderboard](https://race-of-sloths.com/leaderboard)
- Keep weekly and monthly streaks to reach higher positions
- Boast your contributions with a dynamic picture of your [Profile](https://race-of-sloths.com/profile/willco-1)

For maintainers:
- Score pull requests that participate in the Race of Sloths
- Engage contributors with fair scoring and fast responses so they keep their streaks
- Promote the Race to the point where the Race starts promoting you
- Grow the community of your contributors

Feel free to check [our website](https://race-of-sloths.com) for additional details!
</p>
</details> 

<details>
<summary>Bot commands</summary>

- For contributors
  - **Include a PR:** `@race-of-sloths include` to enter the Race with your PR
- For maintainers:
  - **Invite contributor** `@race-of-sloths invite` to invite the contributor to participate in a race or include it, if it's already a runner.
  - **Assign points:** `@race-of-sloths score [1/2/3/5/8/13]` to award points based on your assessment.
  - **Reject this PR:** `@race-of-sloths exclude` to send this PR back to the drawing board.
  - **Exclude repo:** `@race-of-sloths pause` to stop bot activity in this repo until `@race-of-sloths unpause` command is called

</details>


race-of-sloths-bot: @dj8yfo Thank you for calling!
    
@willco-1 Thank you for the contribution! Join [Race of Sloths](https://race-of-sloths.com) by simply mentioning me in your comment/PRs description and start collecting Sloth Points through contributions to open source projects.

<details><summary>What is the Race of Sloths</summary>
<p>
Race of Sloths is a friendly competition where you can participate in challenges and compete with other open-source contributors within your normal workflow

For contributors:

- Tag @race-of-sloths inside your pull requests
- Wait for the maintainer to review and score your pull request
- Check out your position in the [Leaderboard](https://race-of-sloths.com/leaderboard)
- Keep weekly and monthly streaks to reach higher positions
- Boast your contributions with a dynamic picture of your [Profile](https://race-of-sloths.com/profile/willco-1)

For maintainers:
- Score pull requests that participate in the Race of Sloths
- Engage contributors with fair scoring and fast responses so they keep their streaks
- Promote the Race to the point where the Race starts promoting you
- Grow the community of your contributors

Feel free to check [our website](https://race-of-sloths.com) for additional details!
</p>
</details> 

<details>
<summary>Bot commands</summary>

- For contributors
  - **Include a PR:** `@race-of-sloths include` to enter the Race with your PR
- For maintainers:
  - **Invite contributor** `@race-of-sloths invite` to invite the contributor to participate in a race or include it, if it's already a runner.
  - **Assign points:** `@race-of-sloths score [1/2/3/5/8/13]` to award points based on your assessment.
  - **Reject this PR:** `@race-of-sloths exclude` to send this PR back to the drawing board.
  - **Exclude repo:** `@race-of-sloths pause` to stop bot activity in this repo until `@race-of-sloths unpause` command is called

</details>



Patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index cacfb8da..53d65d66 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -36,10 +36,10 @@ jobs:
       run: rustup default ${{ matrix.rust_version }}
     - name: print rustc version
       run: rustc --version
-    - name: downgrade `toml_edit`, time`, `toml_datetime` crate to support older Rust toolchain
-      if: matrix.rust_version == '1.67.0' 
-      run: |
-        cargo update -p toml_edit --precise 0.21.0
+    # - name: downgrade `toml_edit`, time`, `toml_datetime` crate to support older Rust toolchain
+    #   if: matrix.rust_version == '1.67.0' 
+    #   run: |
+    #     cargo update -p toml_edit --precise 0.21.0
     - name: Run tests
       run: ./.github/test.sh
 
diff --git a/borsh-derive/src/lib.rs b/borsh-derive/src/lib.rs
index ae541187..4ecf8dbf 100644
--- a/borsh-derive/src/lib.rs
+++ b/borsh-derive/src/lib.rs
@@ -74,7 +74,7 @@ Attribute takes literal string value, which is the syn's [Path] to `borsh` crate
 Attribute is optional.
 
 1. If the attribute is not provided, [crate_name](proc_macro_crate::crate_name) is used to find a version of `borsh`
-in `[dependencies]` of the relevant `Cargo.toml`. If there is no match, a compilation error, similar to the following, is raised:
+   in `[dependencies]` of the relevant `Cargo.toml`. If there is no match, a compilation error, similar to the following, is raised:
 
 ```bash
  1  error: proc-macro derive panicked
@@ -361,7 +361,7 @@ Attribute takes literal string value, which is the syn's [Path] to `borsh` crate
 Attribute is optional.
 
 1. If the attribute is not provided, [crate_name](proc_macro_crate::crate_name) is used to find a version of `borsh`
-in `[dependencies]` of the relevant `Cargo.toml`. If there is no match, a compilation error, similar to the following, is raised:
+   in `[dependencies]` of the relevant `Cargo.toml`. If there is no match, a compilation error, similar to the following, is raised:
 
 ```bash
  1  error: proc-macro derive panicked
@@ -697,7 +697,7 @@ Attribute takes literal string value, which is the syn's [Path] to `borsh` crate
 Attribute is optional.
 
 1. If the attribute is not provided, [crate_name](proc_macro_crate::crate_name) is used to find a version of `borsh`
-in `[dependencies]` of the relevant `Cargo.toml`. If there is no match, a compilation error, similar to the following, is raised:
+   in `[dependencies]` of the relevant `Cargo.toml`. If there is no match, a compilation error, similar to the following, is raised:
 
 ```bash
  1  error: proc-macro derive panicked
@@ -824,7 +824,7 @@ Attribute takes literal string value, which is a comma-separated list of `Parame
 It may be used in order to:
 
 1. fix complex cases, when derive hasn't figured out the right bounds on type parameters and
-declaration parameters automatically.
+   declaration parameters automatically.
 2. remove parameters, which do not take part in serialization/deserialization, from bounded ones and from declaration parameters.
 
 `ParameterOverride` describes an entry like `order_param => override_type`,
diff --git a/borsh/src/de/mod.rs b/borsh/src/de/mod.rs
index bce4f126..0abb36e3 100644
--- a/borsh/src/de/mod.rs
+++ b/borsh/src/de/mod.rs
@@ -659,6 +659,30 @@ impl BorshDeserialize for std::net::SocketAddr {
     }
 }
 
+#[cfg(feature = "std")]
+impl BorshDeserialize for std::net::IpAddr {
+    #[inline]
+    fn deserialize_reader<R: Read>(reader: &mut R) -> Result<Self> {
+        let kind = u8::deserialize_reader(reader)?;
+        match kind {
+            0u8 => {
+                // Deserialize an Ipv4Addr and convert it to IpAddr::V4
+                let ipv4_addr = std::net::Ipv4Addr::deserialize_reader(reader)?;
+                Ok(std::net::IpAddr::V4(ipv4_addr))
+            }
+            1u8 => {
+                // Deserialize an Ipv6Addr and convert it to IpAddr::V6
+                let ipv6_addr = std::net::Ipv6Addr::deserialize_reader(reader)?;
+                Ok(std::net::IpAddr::V6(ipv6_addr))
+            }
+            value => Err(Error::new(
+                ErrorKind::InvalidData,
+                format!("Invalid IpAddr variant: {}", value),
+            )),
+        }
+    }
+}
+
 #[cfg(feature = "std")]
 impl BorshDeserialize for std::net::SocketAddrV4 {
     #[inline]
diff --git a/borsh/src/schema.rs b/borsh/src/schema.rs
index 1fd0059e..65b5bb1c 100644
--- a/borsh/src/schema.rs
+++ b/borsh/src/schema.rs
@@ -844,3 +844,62 @@ impl_tuple!(
 impl_tuple!(
     T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20
 );
+
+#[cfg(feature = "std")]
+mod id_addr_std_derive_impl {
+    use crate::BorshSchema as BorshSchemaMacro;
+
+    #[derive(BorshSchemaMacro)]
+    #[borsh(crate = "crate")]
+    pub struct Ipv4Addr {
+        octets: [u8; 4],
+    }
+
+    #[derive(BorshSchemaMacro)]
+    #[borsh(crate = "crate")]
+    pub struct Ipv6Addr {
+        octets: [u8; 16],
+    }
+
+    #[derive(BorshSchemaMacro)]
+    #[borsh(crate = "crate")]
+    pub enum IpAddr {
+        /// An IPv4 address.
+        V4(std::net::Ipv4Addr),
+        /// An IPv6 address.
+        V6(std::net::Ipv6Addr),
+    }
+}
+
+#[cfg(feature = "std")]
+impl BorshSchema for std::net::Ipv4Addr {
+    fn add_definitions_recursively(definitions: &mut BTreeMap<Declaration, Definition>) {
+        <id_addr_std_derive_impl::Ipv4Addr>::add_definitions_recursively(definitions);
+    }
+
+    fn declaration() -> Declaration {
+        id_addr_std_derive_impl::Ipv4Addr::declaration()
+    }
+}
+
+#[cfg(feature = "std")]
+impl BorshSchema for std::net::Ipv6Addr {
+    fn add_definitions_recursively(definitions: &mut BTreeMap<Declaration, Definition>) {
+        <id_addr_std_derive_impl::Ipv6Addr>::add_definitions_recursively(definitions);
+    }
+
+    fn declaration() -> Declaration {
+        id_addr_std_derive_impl::Ipv6Addr::declaration()
+    }
+}
+
+#[cfg(feature = "std")]
+impl BorshSchema for std::net::IpAddr {
+    fn add_definitions_recursively(definitions: &mut BTreeMap<Declaration, Definition>) {
+        <id_addr_std_derive_impl::IpAddr>::add_definitions_recursively(definitions);
+    }
+
+    fn declaration() -> Declaration {
+        id_addr_std_derive_impl::IpAddr::declaration()
+    }
+}
diff --git a/borsh/src/ser/mod.rs b/borsh/src/ser/mod.rs
index bb198080..b7da5ebd 100644
--- a/borsh/src/ser/mod.rs
+++ b/borsh/src/ser/mod.rs
@@ -505,6 +505,22 @@ impl BorshSerialize for std::net::Ipv6Addr {
     }
 }
 
+#[cfg(feature = "std")]
+impl BorshSerialize for std::net::IpAddr {
+    #[inline]
+    fn serialize<W: Write>(&self, writer: &mut W) -> Result<()> {
+        match self {
+            std::net::IpAddr::V4(ipv4) => {
+                writer.write_all(&0u8.to_le_bytes())?;
+                ipv4.serialize(writer)
+            }
+            std::net::IpAddr::V6(ipv6) => {
+                writer.write_all(&1u8.to_le_bytes())?;
+                ipv6.serialize(writer)
+            }
+        }
+    }
+}
 impl<T: BorshSerialize + ?Sized> BorshSerialize for Box<T> {
     fn serialize<W: Write>(&self, writer: &mut W) -> Result<()> {
         self.as_ref().serialize(writer)


Test patch:
diff --git a/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipaddr_vec_roundtrip.snap b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipaddr_vec_roundtrip.snap
new file mode 100644
index 00000000..457d914c
--- /dev/null
+++ b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipaddr_vec_roundtrip.snap
@@ -0,0 +1,49 @@
+---
+source: borsh/tests/roundtrip/test_ip_addr.rs
+expression: encoded
+---
+[
+    3,
+    0,
+    0,
+    0,
+    0,
+    192,
+    168,
+    0,
+    1,
+    1,
+    32,
+    1,
+    13,
+    184,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    1,
+    254,
+    128,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+]
diff --git a/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv4_addr_roundtrip.snap b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv4_addr_roundtrip.snap
new file mode 100644
index 00000000..3272fbb1
--- /dev/null
+++ b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv4_addr_roundtrip.snap
@@ -0,0 +1,10 @@
+---
+source: borsh/tests/roundtrip/test_ip_addr.rs
+expression: encoded
+---
+[
+    192,
+    168,
+    0,
+    1,
+]
diff --git a/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv4_addr_roundtrip_enum.snap b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv4_addr_roundtrip_enum.snap
new file mode 100644
index 00000000..0353f9cb
--- /dev/null
+++ b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv4_addr_roundtrip_enum.snap
@@ -0,0 +1,11 @@
+---
+source: borsh/tests/roundtrip/test_ip_addr.rs
+expression: encoded
+---
+[
+    0,
+    192,
+    168,
+    0,
+    1,
+]
diff --git a/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv6_addr_roundtrip.snap b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv6_addr_roundtrip.snap
new file mode 100644
index 00000000..2ff0ed94
--- /dev/null
+++ b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv6_addr_roundtrip.snap
@@ -0,0 +1,22 @@
+---
+source: borsh/tests/roundtrip/test_ip_addr.rs
+expression: encoded
+---
+[
+    32,
+    1,
+    13,
+    184,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+]
diff --git a/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv6_addr_roundtrip_enum.snap b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv6_addr_roundtrip_enum.snap
new file mode 100644
index 00000000..66e2984b
--- /dev/null
+++ b/borsh/tests/roundtrip/snapshots/tests__roundtrip__test_ip_addr__ipv6_addr_roundtrip_enum.snap
@@ -0,0 +1,23 @@
+---
+source: borsh/tests/roundtrip/test_ip_addr.rs
+expression: encoded
+---
+[
+    1,
+    32,
+    1,
+    13,
+    184,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+]
diff --git a/borsh/tests/roundtrip/test_ip_addr.rs b/borsh/tests/roundtrip/test_ip_addr.rs
new file mode 100644
index 00000000..3a45dd8e
--- /dev/null
+++ b/borsh/tests/roundtrip/test_ip_addr.rs
@@ -0,0 +1,55 @@
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+
+#[test]
+fn test_ipv4_addr_roundtrip_enum() {
+    let original = IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1));
+    let encoded = borsh::to_vec(&original).expect("Serialization failed");
+    #[cfg(feature = "std")]
+    insta::assert_debug_snapshot!(encoded);
+    let decoded = borsh::from_slice::<IpAddr>(&encoded).expect("Deserialization failed");
+    assert_eq!(original, decoded);
+}
+
+#[test]
+fn test_ipv4_addr_roundtrip() {
+    let original = Ipv4Addr::new(192, 168, 0, 1);
+    let encoded = borsh::to_vec(&original).expect("Serialization failed");
+    #[cfg(feature = "std")]
+    insta::assert_debug_snapshot!(encoded);
+    let decoded = borsh::from_slice::<Ipv4Addr>(&encoded).expect("Deserialization failed");
+    assert_eq!(original, decoded);
+}
+
+#[test]
+fn test_ipv6_addr_roundtrip_enum() {
+    let original = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
+    let encoded = borsh::to_vec(&original).expect("Serialization failed");
+    #[cfg(feature = "std")]
+    insta::assert_debug_snapshot!(encoded);
+    let decoded = borsh::from_slice::<IpAddr>(&encoded).expect("Deserialization failed");
+    assert_eq!(original, decoded);
+}
+
+#[test]
+fn test_ipv6_addr_roundtrip() {
+    let original = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
+    let encoded = borsh::to_vec(&original).expect("Serialization failed");
+    #[cfg(feature = "std")]
+    insta::assert_debug_snapshot!(encoded);
+    let decoded = borsh::from_slice::<Ipv6Addr>(&encoded).expect("Deserialization failed");
+    assert_eq!(original, decoded);
+}
+
+#[test]
+fn test_ipaddr_vec_roundtrip() {
+    let original = vec![
+        IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)),
+        IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
+        IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)),
+    ];
+    let encoded = borsh::to_vec(&original).expect("Serialization failed");
+    #[cfg(feature = "std")]
+    insta::assert_debug_snapshot!(encoded);
+    let decoded = borsh::from_slice::<Vec<IpAddr>>(&encoded).expect("Deserialization failed");
+    assert_eq!(original, decoded);
+}
diff --git a/borsh/tests/schema/snapshots/tests__schema__test_ip_addr__ip_addr_schema.snap b/borsh/tests/schema/snapshots/tests__schema__test_ip_addr__ip_addr_schema.snap
new file mode 100644
index 00000000..68f0c710
--- /dev/null
+++ b/borsh/tests/schema/snapshots/tests__schema__test_ip_addr__ip_addr_schema.snap
@@ -0,0 +1,68 @@
+---
+source: borsh/tests/schema/test_ip_addr.rs
+expression: "format!(\"{:#?}\", defs)"
+---
+{
+    "IpAddr": Enum {
+        tag_width: 1,
+        variants: [
+            (
+                0,
+                "V4",
+                "IpAddrV4",
+            ),
+            (
+                1,
+                "V6",
+                "IpAddrV6",
+            ),
+        ],
+    },
+    "IpAddrV4": Struct {
+        fields: UnnamedFields(
+            [
+                "Ipv4Addr",
+            ],
+        ),
+    },
+    "IpAddrV6": Struct {
+        fields: UnnamedFields(
+            [
+                "Ipv6Addr",
+            ],
+        ),
+    },
+    "Ipv4Addr": Struct {
+        fields: NamedFields(
+            [
+                (
+                    "octets",
+                    "[u8; 4]",
+                ),
+            ],
+        ),
+    },
+    "Ipv6Addr": Struct {
+        fields: NamedFields(
+            [
+                (
+                    "octets",
+                    "[u8; 16]",
+                ),
+            ],
+        ),
+    },
+    "[u8; 16]": Sequence {
+        length_width: 0,
+        length_range: 16..=16,
+        elements: "u8",
+    },
+    "[u8; 4]": Sequence {
+        length_width: 0,
+        length_range: 4..=4,
+        elements: "u8",
+    },
+    "u8": Primitive(
+        1,
+    ),
+}
diff --git a/borsh/tests/schema/test_ip_addr.rs b/borsh/tests/schema/test_ip_addr.rs
new file mode 100644
index 00000000..297542a9
--- /dev/null
+++ b/borsh/tests/schema/test_ip_addr.rs
@@ -0,0 +1,11 @@
+use crate::common_macro::schema_imports::*;
+use std::net::IpAddr;
+
+#[test]
+fn ip_addr_schema() {
+    let actual_name = IpAddr::declaration();
+    assert_eq!("IpAddr", actual_name);
+    let mut defs = Default::default();
+    IpAddr::add_definitions_recursively(&mut defs);
+    insta::assert_snapshot!(format!("{:#?}", defs));
+}
diff --git a/borsh/tests/tests.rs b/borsh/tests/tests.rs
index daaac5ec..ea8cad5d 100644
--- a/borsh/tests/tests.rs
+++ b/borsh/tests/tests.rs
@@ -12,7 +12,7 @@ mod custom_reader {
 
 /// this module doesn't contain runnable tests;
 /// it's included into module tree to ensure derived code doesn't raise compilation
-/// errors 
+/// errors
 #[rustfmt::skip]
 #[cfg(feature = "derive")]
 mod compile_derives {
@@ -31,13 +31,15 @@ mod compile_derives {
 /// These are full roundtrip `BorshSerialize`/`BorshDeserialize` tests
 #[rustfmt::skip]
 mod roundtrip {
-    mod test_strings; 
+    mod test_strings;
     #[cfg(feature = "ascii")]
-    mod test_ascii_strings; 
+    mod test_ascii_strings;
     mod test_arrays;
     mod test_vecs;
     mod test_tuple;
     mod test_primitives;
+    #[cfg(feature = "std")]
+    mod test_ip_addr;
     mod test_nonzero_integers;
     mod test_range;
     // mod test_phantom_data; // NOTE: there's nothing corresponding to `schema::test_phantom_data`
@@ -59,7 +61,7 @@ mod roundtrip {
         mod test_generic_enums;
         mod test_recursive_structs;
         mod test_recursive_enums;
-        mod test_serde_with_third_party; 
+        mod test_serde_with_third_party;
         mod test_enum_discriminants;
         #[cfg(feature = "bytes")]
         mod test_ultimate_many_features_combined;
@@ -73,12 +75,14 @@ mod roundtrip {
 #[rustfmt::skip]
 mod schema {
     #[cfg(feature = "ascii")]
-    mod test_ascii_strings; 
-    mod test_strings; 
+    mod test_ascii_strings;
+    mod test_strings;
     mod test_arrays;
     mod test_vecs;
     mod test_tuple;
     mod test_primitives;
+    #[cfg(feature = "std")]
+    mod test_ip_addr;
     // mod test_nonzero_integers; // NOTE: there's nothing corresponding to `roundtrip::test_nonzero_integers`
     mod test_range;
     mod test_phantom_data;