feat: add serde
Adds serialization/deserialization support via serde and serde_with. Also: - remove `license-file` in Cargo.toml (superfluous) - update README with Getting Started section - add `serde` and `serde_with` dependencies to README - add tests to `HeightVector` - tweak assertion in `NetworkCoordinate` - refactor `Vector` to be a struct witn an `inner` instead of a tuple struct so that we can use `serde_as` (from `serde_with`) to help us derive SerDe implementations for the const generic sized array - moved trait requirements (which are getting long) into where clauses Signed-off-by: Stephen Waits <steve@waits.net>
This commit is contained in:
1 parent
9265b5fa28
commit
b3147dfad7
5 files changed
+213
-34
No files matched your search
+3
-1
@@ -11,11 +11,13 @@ documentation = "https://docs.rs/vivaldi-nc/"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://sr.ht/~swaits/vivaldi-nc/"
|
homepage = "https://sr.ht/~swaits/vivaldi-nc/"
|
||||||
repository = "https://git.sr.ht/~swaits/vivaldi-nc"
|
repository = "https://git.sr.ht/~swaits/vivaldi-nc"
|
||||||
license-file = "LICENSE.txt"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_approx_eq = "1.1.0"
|
assert_approx_eq = "1.1.0"
|
||||||
|
serde_json = "1.0.91"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nanorand = {version = "0.7.0", features = ["wyrand", "getrandom"]}
|
nanorand = {version = "0.7.0", features = ["wyrand", "getrandom"]}
|
||||||
num-traits = "0.2.15"
|
num-traits = "0.2.15"
|
||||||
|
serde = {version = "1.0.152", features = ["serde_derive"]}
|
||||||
|
serde_with = "2.2.0"
|
||||||
@@ -10,7 +10,7 @@ other will be close to each other. Nodes with high latency between them will be
|
|||||||
far from each other.
|
far from each other.
|
||||||
|
|
||||||
This is an implementation of Vivaldi Network Coordinates, a specific NC
|
This is an implementation of Vivaldi Network Coordinates, a specific NC
|
||||||
algorithm, with a public simple interface and few dependencies. Vivaldi
|
algorithm, with a simple simple interface and few dependencies. Vivaldi
|
||||||
coordinates are typically used in distributed networking applications, like p2p
|
coordinates are typically used in distributed networking applications, like p2p
|
||||||
networks. They allow each node in the network to estimate its position in a
|
networks. They allow each node in the network to estimate its position in a
|
||||||
latency space in a distributed way, with no central authority. This enables
|
latency space in a distributed way, with no central authority. This enables
|
||||||
@@ -62,6 +62,51 @@ Regarding the two drawbacks mentioned by the Wikiepedia article, quoted above:
|
|||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
|
Add the crate to your project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo add vivaldi-nc
|
||||||
|
```
|
||||||
|
|
||||||
|
Each node in the network should create a `NetworkCoordinate` (NC). The node
|
||||||
|
uses this structure to track its latency position in the entire network.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use vivaldi_nc::network_coordinate::NetworkCoordinate;
|
||||||
|
|
||||||
|
// create a 2-dimensional NetworkCoordinate to track my position
|
||||||
|
let mut my_position = NetworkCoordinate::<2>::new();
|
||||||
|
```
|
||||||
|
Normally 2 or 3-dimensions is plenty. Higher dimensions may add some accuracy,
|
||||||
|
but not enough to be worth the extra costs.
|
||||||
|
|
||||||
|
Each node occasionally sends its NC to other nodes. Upon receiving a NC from a
|
||||||
|
remote node, update your local node's position with the actual measured ping
|
||||||
|
time to that node.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
my_position.update(&remote_position, Duration::from_millis(measured_rtt));
|
||||||
|
```
|
||||||
|
|
||||||
|
Over time your NC will get more and more accurate as it's updated against more
|
||||||
|
nodes in the network.
|
||||||
|
|
||||||
|
You can estimate your ping time with any other NC you receive, even if it was
|
||||||
|
forwarded to you indirectly.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let rtt_estimate = my_position.estimate_rtt(&remote_position);
|
||||||
|
```
|
||||||
|
|
||||||
|
That's the entire interface for creating and iteratively updating NCs.
|
||||||
|
|
||||||
|
If you want to save/restore NCs, or send/receive them over a network, you'll
|
||||||
|
want to serialize/deserialize them. `NetworkCoordinate` supports
|
||||||
|
[Serde](https://crates.io/crates/serde) by default. Many formats are supported
|
||||||
|
by Serde, including text formats like [JSON](https://github.com/serde-rs/json)
|
||||||
|
and compact binary formats like [bincode](https://crates.io/crates/bincode) and
|
||||||
|
[MessagePack](https://github.com/3Hren/msgpack-rust).
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
@@ -72,13 +117,19 @@ One design goal of this crate is to minimize dependencies. When dependencies
|
|||||||
are required, I try to be very selective about them (re: bloat and licensing).
|
are required, I try to be very selective about them (re: bloat and licensing).
|
||||||
This crate depends on:
|
This crate depends on:
|
||||||
|
|
||||||
- `nanorand`: A fast PRNG based on WyRand. It pulls in `getrandom` as a
|
- [`nanorand`](https://crates.io/crates/nanorand): A fast PRNG based on WyRand.
|
||||||
portable source of entropy. I chose this over the more commonly used, but
|
It pulls in `getrandom` as a portable source of entropy. I chose this over
|
||||||
heavyweight `rand` because it's significantly smaller and does just what I
|
the more commonly used, but heavyweight `rand` because it's significantly
|
||||||
need, and not much more.
|
smaller and does just what I need, and not much more.
|
||||||
- `num-traits`: A brilliant crate which makes it easier to operate on numbers
|
- [`num-traits`](https://crates.io/crates/num-traits): A brilliant crate which
|
||||||
in generics; like using `Float` as a constraint on a generic type. Its
|
makes it easier to operate on numbers in generics; like using `Float` as a
|
||||||
convenience outweighs its cost.
|
constraint on a generic type. Its convenience outweighs its cost.
|
||||||
|
- [`serde`](https://crates.io/crates/serde): I think NCs are usually meant to
|
||||||
|
be shared across a network. That requires serialization/deserialization and
|
||||||
|
serde is *the* choice for that. It might be big, but it's efficient.
|
||||||
|
- [`serde_with`](https://crates.io/crates/serde_with): Because the inner
|
||||||
|
`Vector<T,N>` uses a const generic length, we use this to help derive
|
||||||
|
`Deserialize`.
|
||||||
|
|
||||||
## Alternative implementations
|
## Alternative implementations
|
||||||
|
|
||||||
@@ -98,12 +149,12 @@ I had several design goals which the existing crates didn't satisfy:
|
|||||||
rigamarole. Support `serde` traits by default.
|
rigamarole. Support `serde` traits by default.
|
||||||
4. Well documented. Well tested. This varies by the crates.
|
4. Well documented. Well tested. This varies by the crates.
|
||||||
|
|
||||||
All of that said, those other implementations might work best for you. Here are
|
All of that said, those other rust implementations might work best for you.
|
||||||
the ones I know of today:
|
Here are the ones I know of today:
|
||||||
|
|
||||||
- TODO: list crates
|
- [netloc](https://github.com/dgtony/netloc)
|
||||||
- TODO: list crates
|
- [violin](https://crates.io/crates/violin)
|
||||||
- TODO: list crates
|
- [vivaldi](https://crates.io/crates/vivaldi)
|
||||||
|
|
||||||
## Other Algorithms
|
## Other Algorithms
|
||||||
|
|
||||||
|
|||||||
+66
-1
@@ -45,6 +45,7 @@
|
|||||||
use std::ops::{Add, Mul, Sub};
|
use std::ops::{Add, Mul, Sub};
|
||||||
|
|
||||||
use nanorand::{Rng, WyRand};
|
use nanorand::{Rng, WyRand};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::vector::Vector;
|
use crate::vector::Vector;
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ use crate::vector::Vector;
|
|||||||
/// ## Generic Parameters
|
/// ## Generic Parameters
|
||||||
///
|
///
|
||||||
/// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector
|
/// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct HeightVector<const N: usize>(Vector<f32, N>, f32);
|
pub(crate) struct HeightVector<const N: usize>(Vector<f32, N>, f32);
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -160,4 +161,68 @@ mod tests {
|
|||||||
let a = HeightVector::<3>::from(([1.0, 2.0, 3.0], 4.0));
|
let a = HeightVector::<3>::from(([1.0, 2.0, 3.0], 4.0));
|
||||||
assert_approx_eq!(a.len(), 7.741_657, 0.00001);
|
assert_approx_eq!(a.len(), 7.741_657, 0.00001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new() {
|
||||||
|
// new gives us a random unit length vec
|
||||||
|
let a = HeightVector::<3>::new();
|
||||||
|
assert_approx_eq!(a.len(), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default() {
|
||||||
|
// default() should give us a zero vec
|
||||||
|
let a = HeightVector::<3>::default();
|
||||||
|
assert_approx_eq!(a.len(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from() {
|
||||||
|
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
|
||||||
|
assert_eq!(a.0[0], 1.0);
|
||||||
|
assert_eq!(a.0[1], 2.0);
|
||||||
|
assert_eq!(a.1, 3.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add() {
|
||||||
|
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
|
||||||
|
let b = HeightVector::<2>::from(([3.0, 2.0], 1.0));
|
||||||
|
let c = a + b;
|
||||||
|
assert_eq!(c.0[0], 4.0);
|
||||||
|
assert_eq!(c.0[1], 4.0);
|
||||||
|
assert_eq!(c.1, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sub() {
|
||||||
|
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
|
||||||
|
let b = HeightVector::<2>::from(([3.0, 2.0], 1.0));
|
||||||
|
let c = a - b;
|
||||||
|
assert_eq!(c.0[0], -2.0);
|
||||||
|
assert_eq!(c.0[1], 0.0);
|
||||||
|
assert_eq!(c.1, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mul_scalar() {
|
||||||
|
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)) * 10.0;
|
||||||
|
assert_eq!(a.0[0], 10.0);
|
||||||
|
assert_eq!(a.0[1], 20.0);
|
||||||
|
assert_eq!(a.1, 30.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serde() {
|
||||||
|
// start with JSON, deserialize it
|
||||||
|
let s = "[{\"inner\":[1.0,2.0,3.0]},4.0]";
|
||||||
|
let a: HeightVector<3> = serde_json::from_str(s).unwrap();
|
||||||
|
|
||||||
|
// make sure it's the right length and works like we expect a normal NC
|
||||||
|
assert_approx_eq!(a.len(), 7.741_657, 0.001);
|
||||||
|
|
||||||
|
// serialize it into a new JSON string and make sure it matches the original
|
||||||
|
let t = serde_json::to_string(&a);
|
||||||
|
assert_eq!(t.as_ref().unwrap(), s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,8 @@
|
|||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::height_vector::HeightVector;
|
use crate::height_vector::HeightVector;
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -47,7 +49,7 @@ const MIN_ERROR: f32 = f32::EPSILON;
|
|||||||
// **** Structs ****
|
// **** Structs ****
|
||||||
//
|
//
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct NetworkCoordinate<const N: usize> {
|
pub struct NetworkCoordinate<const N: usize> {
|
||||||
position: HeightVector<N>,
|
position: HeightVector<N>,
|
||||||
error: f32,
|
error: f32,
|
||||||
@@ -274,6 +276,22 @@ mod tests {
|
|||||||
(slc.error.powf(2.0) + nyc.error.powf(2.0) + lax.error.powf(2.0) + mad.error.powf(2.0))
|
(slc.error.powf(2.0) + nyc.error.powf(2.0) + lax.error.powf(2.0) + mad.error.powf(2.0))
|
||||||
.sqrt();
|
.sqrt();
|
||||||
println!("error = {error}");
|
println!("error = {error}");
|
||||||
assert!(error < 0.07);
|
assert!(error < 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serde() {
|
||||||
|
// start with JSON, deserialize it
|
||||||
|
let s = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},0.1],\"error\":1.0}";
|
||||||
|
let a: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
|
||||||
|
|
||||||
|
// make sure it's the right length and works like we expect a normal NC
|
||||||
|
assert_approx_eq!(a.position.len(), 2.649_509, 0.001);
|
||||||
|
assert_eq!(a.error, 1.0);
|
||||||
|
assert_eq!(a.estimated_rtt(&a).as_millis(), 0);
|
||||||
|
|
||||||
|
// serialize it into a new JSON string and make sure it matches the original
|
||||||
|
let t = serde_json::to_string(&a);
|
||||||
|
assert_eq!(t.as_ref().unwrap(), s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+60
-17
@@ -4,6 +4,8 @@
|
|||||||
//! Vivaldi `HeightVector`.
|
//! Vivaldi `HeightVector`.
|
||||||
|
|
||||||
use num_traits::Float;
|
use num_traits::Float;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::serde_as;
|
||||||
|
|
||||||
use std::ops::{Add, Div, Index, Mul, Sub};
|
use std::ops::{Add, Div, Index, Mul, Sub};
|
||||||
|
|
||||||
@@ -16,14 +18,24 @@ use std::ops::{Add, Div, Index, Mul, Sub};
|
|||||||
/// ## Generic parameters:
|
/// ## Generic parameters:
|
||||||
/// - `T`: the type to use, must satisfy the `Float` trait (i.e. `f32` or `f64`)
|
/// - `T`: the type to use, must satisfy the `Float` trait (i.e. `f32` or `f64`)
|
||||||
/// - `N`: the number of dimensions (i.e. 2 for 2D vectors, 3 for 3D, etc)
|
/// - `N`: the number of dimensions (i.e. 2 for 2D vectors, 3 for 3D, etc)
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[serde_as]
|
||||||
pub(crate) struct Vector<T: Float, const N: usize>([T; N]);
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct Vector<T, const N: usize>
|
||||||
|
where
|
||||||
|
T: Float + Serialize + for<'d> Deserialize<'d>,
|
||||||
|
{
|
||||||
|
#[serde_as(as = "[_; N]")]
|
||||||
|
inner: [T; N],
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// **** Implementations ****
|
// **** Implementations ****
|
||||||
//
|
//
|
||||||
|
|
||||||
impl<T: Float, const N: usize> Vector<T, N> {
|
impl<T, const N: usize> Vector<T, N>
|
||||||
|
where
|
||||||
|
T: Float + Serialize + for<'d> Deserialize<'d>,
|
||||||
|
{
|
||||||
/// Create a new Zero vector.
|
/// Create a new Zero vector.
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
@@ -32,8 +44,8 @@ impl<T: Float, const N: usize> Vector<T, N> {
|
|||||||
/// Compute magnitude of vector.
|
/// Compute magnitude of vector.
|
||||||
pub(crate) fn len(&self) -> T {
|
pub(crate) fn len(&self) -> T {
|
||||||
(0..N)
|
(0..N)
|
||||||
.map(|i| self.0[i] * self.0[i])
|
.map(|i| self.inner[i] * self.inner[i])
|
||||||
.fold(T::zero(), |acc, x| acc + x)
|
.fold(T::zero(), |sum_sq, x| sum_sq + x)
|
||||||
.sqrt()
|
.sqrt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,21 +54,32 @@ impl<T: Float, const N: usize> Vector<T, N> {
|
|||||||
// **** Trait Implementations ****
|
// **** Trait Implementations ****
|
||||||
//
|
//
|
||||||
|
|
||||||
impl<T: Float, const N: usize> Default for Vector<T, N> {
|
impl<T, const N: usize> Default for Vector<T, N>
|
||||||
|
where
|
||||||
|
T: Float + Serialize + for<'d> Deserialize<'d>,
|
||||||
|
{
|
||||||
/// A default vector is all zeroes
|
/// A default vector is all zeroes
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self([T::zero(); N])
|
Self {
|
||||||
|
inner: [T::zero(); N],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Float, const N: usize> From<[T; N]> for Vector<T, N> {
|
impl<T, const N: usize> From<[T; N]> for Vector<T, N>
|
||||||
|
where
|
||||||
|
T: Float + Serialize + for<'d> Deserialize<'d>,
|
||||||
|
{
|
||||||
/// Create a new Vector from an array of type T, N dimensions.
|
/// Create a new Vector from an array of type T, N dimensions.
|
||||||
fn from(value: [T; N]) -> Self {
|
fn from(value: [T; N]) -> Self {
|
||||||
Self(value)
|
Self { inner: value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Float, const N: usize> Add for Vector<T, N> {
|
impl<T, const N: usize> Add for Vector<T, N>
|
||||||
|
where
|
||||||
|
T: Float + Serialize + for<'d> Deserialize<'d>,
|
||||||
|
{
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
/// Vector+Vector addition.
|
/// Vector+Vector addition.
|
||||||
@@ -69,7 +92,10 @@ impl<T: Float, const N: usize> Add for Vector<T, N> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Float, const N: usize> Sub for Vector<T, N> {
|
impl<T, const N: usize> Sub for Vector<T, N>
|
||||||
|
where
|
||||||
|
T: Float + Serialize + for<'d> Deserialize<'d>,
|
||||||
|
{
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
/// Vector-Vector subtraction.
|
/// Vector-Vector subtraction.
|
||||||
@@ -82,38 +108,47 @@ impl<T: Float, const N: usize> Sub for Vector<T, N> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Float, const N: usize> Mul<T> for Vector<T, N> {
|
impl<T, const N: usize> Mul<T> for Vector<T, N>
|
||||||
|
where
|
||||||
|
T: Float + Serialize + for<'d> Deserialize<'d>,
|
||||||
|
{
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
/// Vector*scalar multiplication.
|
/// Vector*scalar multiplication.
|
||||||
fn mul(self, rhs: T) -> Self::Output {
|
fn mul(self, rhs: T) -> Self::Output {
|
||||||
let mut ret = Self::Output::new();
|
let mut ret = Self::Output::new();
|
||||||
for i in 0..N {
|
for i in 0..N {
|
||||||
ret.0[i] = self.0[i] * rhs
|
ret.inner[i] = self.inner[i] * rhs
|
||||||
}
|
}
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Float, const N: usize> Div<T> for Vector<T, N> {
|
impl<T, const N: usize> Div<T> for Vector<T, N>
|
||||||
|
where
|
||||||
|
T: Float + Serialize + for<'d> Deserialize<'d>,
|
||||||
|
{
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
/// Vector/scalar division.
|
/// Vector/scalar division.
|
||||||
fn div(self, rhs: T) -> Self::Output {
|
fn div(self, rhs: T) -> Self::Output {
|
||||||
let mut ret = Self::Output::new();
|
let mut ret = Self::Output::new();
|
||||||
for i in 0..N {
|
for i in 0..N {
|
||||||
ret.0[i] = self.0[i] / rhs
|
ret.inner[i] = self.inner[i] / rhs
|
||||||
}
|
}
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Float, const N: usize> Index<usize> for Vector<T, N> {
|
impl<T, const N: usize> Index<usize> for Vector<T, N>
|
||||||
|
where
|
||||||
|
T: Float + Serialize + for<'d> Deserialize<'d>,
|
||||||
|
{
|
||||||
type Output = T;
|
type Output = T;
|
||||||
|
|
||||||
/// Nicer indexing [] for read only references.
|
/// Nicer indexing [] for read only references.
|
||||||
fn index(&self, index: usize) -> &Self::Output {
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
&self.0[index]
|
&self.inner[index]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,4 +218,12 @@ mod tests {
|
|||||||
let b = a * 10.0;
|
let b = a * 10.0;
|
||||||
assert_approx_eq!(b.len(), 37.416_57, 0.0001);
|
assert_approx_eq!(b.len(), 37.416_57, 0.0001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serde() {
|
||||||
|
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
|
||||||
|
let s = serde_json::to_string(&a);
|
||||||
|
assert_eq!(s.as_ref().unwrap(), "{\"inner\":[1.0,2.0,3.0]}");
|
||||||
|
assert!(s.is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user