Skip to content

Commit

Permalink
Merge pull request #148 from quartiq/rj/serde
Browse files Browse the repository at this point in the history
be generic over serde mechanism and path hierarchy separator
  • Loading branch information
ryan-summers authored Jul 25, 2023
2 parents f69e916 + 42ef817 commit be4c5e2
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 217 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed

* [breaking] The `Miniconf` trait is now generic over the `Deserializer`/`Serializer`. It
doesn't enforce `serde-json-core` or `u8` buffers anymore.
* [breaking] `MiniconfIter` takes the path hierarchy separator and passes it on to
`Miniconf::next_path`.
* [breaking] The `Miniconf` trait has been stripped of the provided functions that depended
on the `serde`-backend and path hierarchy separator. Those have been
moved into a super trait `SerDe<S>` that is generic over a specification marker
struct `S`. `SerDe<JsonCoreSlash>` has been implemented for all `Miniconf`
to provide the previously existing functionality.
* The only required change for most downstream crates to adapt to the above is to
make sure the `SerDe` trait is in scope (`use miniconf::SerDe`).

## [0.7.1] (https://github.com/quartiq/miniconf/compare/v0.7.0...v0.7.1)

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ rust-version = "1.65.0"

[dependencies]
miniconf_derive = { path = "miniconf_derive" , version = "0.6" }
serde-json-core = "0.5.0"
serde-json-core = { git = "https://github.com/rust-embedded-community/serde-json-core.git" }
serde = { version = "1.0.120", features = ["derive"], default-features = false }
log = "0.4"
heapless = { version = "0.7", features = ["serde"] }
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ Miniconf can be used as a very simple and flexible backend for run-time settings
over any transport. It was originally designed to work with JSON ([serde_json_core](https://docs.rs/serde-json-core))
payloads over MQTT ([minimq](https://docs.rs/minimq)) and provides a comlete [MQTT settings management
client](MqttClient) and a Python reference implementation to ineract with it.
`Miniconf` is generic over the `serde::Serializer`/`serde::Deserializer` backend and the path hierarchy separator.

## Example
```rust
use miniconf::{Error, Miniconf};
use miniconf::{Error, Miniconf, SerDe};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Copy, Clone, Default)]
Expand Down Expand Up @@ -121,7 +122,7 @@ python -m miniconf -d quartiq/application/+ foo=true
For structs with named fields, Miniconf offers a [derive macro](derive.Miniconf.html) to automatically
assign a unique path to each item in the namespace of the struct.
The macro implements the [Miniconf] trait that exposes access to serialized field values through their path.
All types supported by [serde_json_core] can be used as fields.
All types supported by [serde] (and the `serde::Serializer`/`serde::Deserializer` backend) can be used as fields.

Elements of homogeneous [core::array]s are similarly accessed through their numeric indices.
Structs, arrays, and Options can then be cascaded to construct a multi-level namespace.
Expand All @@ -136,9 +137,12 @@ atomic access to their respective inner element(s), [Array] and
into the inner element(s) through their respective inner [Miniconf] implementations.

## Formats
The path hierarchy separator is the slash `/`.

Values are serialized into and deserialized from JSON.
Miniconf is generic over the `serde` backend/payload format and the path hierarchy separator
(as long as the path can be split by it unambiguously).

Currently support for `/` as the path hierarchy separator and JSON (`serde_json_core`) is implemented
through [SerDe] for the [JsonCoreSlash] style.

## Transport
Miniconf is designed to be protocol-agnostic. Any means that can receive key-value input from
Expand Down
2 changes: 1 addition & 1 deletion examples/readback.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use miniconf::Miniconf;
use miniconf::{Miniconf, SerDe};

#[derive(Debug, Default, Miniconf)]
struct AdditionalSettings {
Expand Down
36 changes: 18 additions & 18 deletions miniconf_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ fn get_path_arm(struct_field: &StructField) -> proc_macro2::TokenStream {
if struct_field.deferred {
quote! {
stringify!(#match_name) => {
self.#match_name.get_path(path_parts, value)
self.#match_name.get_path(path_parts, ser)
}
}
} else {
Expand All @@ -58,7 +58,7 @@ fn get_path_arm(struct_field: &StructField) -> proc_macro2::TokenStream {
if peek {
Err(miniconf::Error::PathTooLong)
} else {
Ok(miniconf::serde_json_core::to_slice(&self.#match_name, value)?)
miniconf::serde::ser::Serialize::serialize(&self.#match_name, ser).map_err(|_| miniconf::Error::Serialization)
}
}
}
Expand All @@ -71,7 +71,7 @@ fn set_path_arm(struct_field: &StructField) -> proc_macro2::TokenStream {
if struct_field.deferred {
quote! {
stringify!(#match_name) => {
self.#match_name.set_path(path_parts, value)
self.#match_name.set_path(path_parts, de)
}
}
} else {
Expand All @@ -80,9 +80,8 @@ fn set_path_arm(struct_field: &StructField) -> proc_macro2::TokenStream {
if peek {
Err(miniconf::Error::PathTooLong)
} else {
let (value, len) = miniconf::serde_json_core::from_slice(value)?;
self.#match_name = value;
Ok(len)
self.#match_name = miniconf::serde::de::Deserialize::deserialize(de).map_err(|_| miniconf::Error::Deserialization)?;
Ok(())
}
}
}
Expand All @@ -99,7 +98,7 @@ fn next_path_arm((i, struct_field): (usize, &StructField)) -> proc_macro2::Token
path.push_str(concat!(stringify!(#field_name), "/"))
.map_err(|_| miniconf::IterError::PathLength)?;

if <#field_type>::next_path(&mut state[1..], path)? {
if <#field_type>::next_path(&mut state[1..], path, separator)? {
return Ok(true);
}
}
Expand Down Expand Up @@ -181,11 +180,11 @@ fn derive_struct(

quote! {
impl #impl_generics miniconf::Miniconf for #ident #ty_generics #where_clause {
fn set_path<'a, P: miniconf::Peekable<Item = &'a str>>(
&mut self,
path_parts: &'a mut P,
value: &[u8]
) -> Result<usize, miniconf::Error> {
fn set_path<'a, 'b: 'a, P, D>(&mut self, path_parts: &mut P, de: D) -> Result<(), miniconf::Error>
where
P: miniconf::Peekable<Item = &'a str>,
D: miniconf::serde::Deserializer<'b>,
{
let field = path_parts.next().ok_or(miniconf::Error::PathTooShort)?;
let peek = path_parts.peek().is_some();

Expand All @@ -195,11 +194,11 @@ fn derive_struct(
}
}

fn get_path<'a, P: miniconf::Peekable<Item = &'a str>>(
&self,
path_parts: &'a mut P,
value: &mut [u8]
) -> Result<usize, miniconf::Error> {
fn get_path<'a, P, S>(&self, path_parts: &mut P, ser: S) -> Result<S::Ok, miniconf::Error>
where
P: miniconf::Peekable<Item = &'a str>,
S: miniconf::serde::Serializer,
{
let field = path_parts.next().ok_or(miniconf::Error::PathTooShort)?;
let peek = path_parts.peek().is_some();

Expand All @@ -211,7 +210,8 @@ fn derive_struct(

fn next_path<const TS: usize>(
state: &mut [usize],
path: &mut miniconf::heapless::String<TS>
path: &mut miniconf::heapless::String<TS>,
separator: char,
) -> Result<bool, miniconf::IterError> {
let original_length = path.len();
loop {
Expand Down
57 changes: 29 additions & 28 deletions src/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,30 +107,30 @@ const fn digits(x: usize) -> usize {
}

impl<T: Miniconf, const N: usize> Miniconf for Array<T, N> {
fn set_path<'a, P: Peekable<Item = &'a str>>(
&mut self,
path_parts: &'a mut P,
value: &[u8],
) -> Result<usize, Error> {
fn set_path<'a, 'b: 'a, P, D>(&mut self, path_parts: &mut P, de: D) -> Result<(), Error>
where
P: Peekable<Item = &'a str>,
D: serde::Deserializer<'b>,
{
let i = self.0.index(path_parts.next())?;

self.0
.get_mut(i)
.ok_or(Error::BadIndex)?
.set_path(path_parts, value)
.set_path(path_parts, de)
}

fn get_path<'a, P: Peekable<Item = &'a str>>(
&self,
path_parts: &'a mut P,
value: &mut [u8],
) -> Result<usize, Error> {
fn get_path<'a, P, S>(&self, path_parts: &mut P, ser: S) -> Result<S::Ok, Error>
where
P: Peekable<Item = &'a str>,
S: serde::Serializer,
{
let i = self.0.index(path_parts.next())?;

self.0
.get(i)
.ok_or(Error::BadIndex)?
.get_path(path_parts, value)
.get_path(path_parts, ser)
}

fn metadata() -> Metadata {
Expand All @@ -149,17 +149,18 @@ impl<T: Miniconf, const N: usize> Miniconf for Array<T, N> {
fn next_path<const TS: usize>(
state: &mut [usize],
topic: &mut heapless::String<TS>,
separator: char,
) -> Result<bool, IterError> {
let original_length = topic.len();

while *state.first().ok_or(IterError::PathDepth)? < N {
// Add the array index and separator to the topic name.
topic
.push_str(itoa::Buffer::new().format(state[0]))
.and_then(|_| topic.push('/'))
.and_then(|_| topic.push(separator))
.map_err(|_| IterError::PathLength)?;

if T::next_path(&mut state[1..], topic)? {
if T::next_path(&mut state[1..], topic, separator)? {
return Ok(true);
}

Expand Down Expand Up @@ -189,36 +190,35 @@ impl<T, const N: usize> IndexLookup for [T; N] {
}

impl<T: crate::Serialize + crate::DeserializeOwned, const N: usize> Miniconf for [T; N] {
fn set_path<'a, P: Peekable<Item = &'a str>>(
&mut self,
path_parts: &mut P,
value: &[u8],
) -> Result<usize, Error> {
fn set_path<'a, 'b: 'a, P, D>(&mut self, path_parts: &mut P, de: D) -> Result<(), Error>
where
P: Peekable<Item = &'a str>,
D: serde::Deserializer<'b>,
{
let i = self.index(path_parts.next())?;

if path_parts.peek().is_some() {
return Err(Error::PathTooLong);
}

let item = <[T]>::get_mut(self, i).ok_or(Error::BadIndex)?;
let (value, len) = serde_json_core::from_slice(value)?;
*item = value;
Ok(len)
*item = serde::Deserialize::deserialize(de).map_err(|_| Error::Deserialization)?;
Ok(())
}

fn get_path<'a, P: Peekable<Item = &'a str>>(
&self,
path_parts: &mut P,
value: &mut [u8],
) -> Result<usize, Error> {
fn get_path<'a, P, S>(&self, path_parts: &mut P, ser: S) -> Result<S::Ok, Error>
where
P: Peekable<Item = &'a str>,
S: serde::Serializer,
{
let i = self.index(path_parts.next())?;

if path_parts.peek().is_some() {
return Err(Error::PathTooLong);
}

let item = <[T]>::get(self, i).ok_or(Error::BadIndex)?;
Ok(serde_json_core::to_slice(item, value)?)
serde::Serialize::serialize(item, ser).map_err(|_| Error::Serialization)
}

fn metadata() -> Metadata {
Expand All @@ -232,6 +232,7 @@ impl<T: crate::Serialize + crate::DeserializeOwned, const N: usize> Miniconf for
fn next_path<const TS: usize>(
state: &mut [usize],
path: &mut heapless::String<TS>,
_separator: char,
) -> Result<bool, IterError> {
if *state.first().ok_or(IterError::PathDepth)? < N {
// Add the array index to the topic name.
Expand Down
56 changes: 42 additions & 14 deletions src/iter.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
use super::Miniconf;
use super::{Metadata, Miniconf, SerDe};
use core::marker::PhantomData;
use heapless::String;

/// Errors that occur during iteration over topic paths.
#[non_exhaustive]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum IterError {
/// The provided state vector is not long enough.
PathDepth,

/// The provided topic length is not long enough.
PathLength,
}

/// An iterator over the paths in a Miniconf namespace.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MiniconfIter<M: ?Sized, const L: usize, const TS: usize> {
pub struct MiniconfIter<M: ?Sized, const L: usize, const TS: usize, S> {
/// Zero-size marker field to allow being generic over M and gaining access to M.
marker: PhantomData<M>,
miniconf: PhantomData<M>,
spec: PhantomData<S>,

/// The iteration state.
///
Expand All @@ -23,32 +35,48 @@ pub struct MiniconfIter<M: ?Sized, const L: usize, const TS: usize> {
count: Option<usize>,
}

impl<M: ?Sized, const L: usize, const TS: usize> Default for MiniconfIter<M, L, TS> {
impl<M: ?Sized, const L: usize, const TS: usize, S> Default for MiniconfIter<M, L, TS, S> {
fn default() -> Self {
MiniconfIter {
marker: PhantomData,
state: [0; L],
Self {
count: None,
miniconf: PhantomData,
spec: PhantomData,
state: [0; L],
}
}
}

impl<M: ?Sized, const L: usize, const TS: usize> MiniconfIter<M, L, TS> {
pub fn new(count: Option<usize>) -> Self {
Self {
count,
..Default::default()
impl<M: ?Sized + Miniconf, const L: usize, const TS: usize, S> MiniconfIter<M, L, TS, S> {
pub fn metadata() -> Result<Metadata, IterError> {
let meta = M::metadata();
if TS < meta.max_length {
return Err(IterError::PathLength);
}

if L < meta.max_depth {
return Err(IterError::PathDepth);
}
Ok(meta)
}

pub fn new() -> Result<Self, IterError> {
let meta = Self::metadata()?;
Ok(Self {
count: Some(meta.count),
..Default::default()
})
}
}

impl<M: Miniconf + ?Sized, const L: usize, const TS: usize> Iterator for MiniconfIter<M, L, TS> {
impl<M: Miniconf + SerDe<S> + ?Sized, const L: usize, const TS: usize, S> Iterator
for MiniconfIter<M, L, TS, S>
{
type Item = String<TS>;

fn next(&mut self) -> Option<Self::Item> {
let mut path = Self::Item::new();

if M::next_path(&mut self.state, &mut path).unwrap() {
if M::next_path(&mut self.state, &mut path, M::SEPARATOR).unwrap() {
self.count = self.count.map(|c| c - 1);
Some(path)
} else {
Expand Down
Loading

0 comments on commit be4c5e2

Please sign in to comment.