-
-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optional nested objects and lists/maps of nested objects? #12
Comments
Maps of nested objects seem like they would be particularly useful! In my use case I have a config structure with this kind of thing (TOML): [oidc.clients.key1]
name = "abc"
secret = "123"
[oidc.clients.key2]
name = "abc"
secret = "123" I'd like to let users load the secret from a separate file if they want, so that they can keep their secret configuration separate from the less-sensitive configuration (e.g. because the secret configuration might be stored encrypted in their git repository that they use to set up the service). Because maps of nested objects aren't supported, I believe the whole map has to appear in one particular file since it only uses serde's |
I tried to tackle this issue a couple of times already but always kind of got stuck. What follows is a bunch of thoughts, mostly as a note to self, but of course I'm always happy for more input and ideas. Why even?Why not just have a struct and derive
@reivilibre provided an example above (thanks!) with the goal of merging objects from different sources. But one can also imagine each client having a bunch of fields and the documentation aspect being important. Some examples how the template for optional objects or lists or maps could look: Template examples/brainstormingOptional nested object #[derive(Config)]
struct PopupConfig {
/// The title of the popup.
title: String,
/// The body text of the popup, basic markdown is supported.
body: String,
/// Label for the button that makes the annoying popup go away.
#[config(default = "Yeah whatever")]
label: String,
}
#[derive(Config)]
struct Conf {
/// Allows you to configure an annoying popup on this website.
#[config(nested)]
popup: Option<PopupConfig>,
} # Allows you to configure an annoying popup on this website.
#
# This object is optional.
#[popup]
# The title of the popup.
#
# Required!
#title =
# The body text of the popup, basic markdown is supported.
#
# Required!
#body =
# Label for the button that makes the annoying popup go away.
#
# Default value: "Yeah whatever"
#label = "Yeah whatever" # Allows you to configure an annoying popup on this website.
#
# This object is optional.
#popup:
# The title of the popup.
#
# Required!
#title:
# The body text of the popup, basic markdown is supported.
#
# Required!
#body:
# Label for the button that makes the annoying popup go away.
#
# Default value: Yeah whatever
#label: Yeah whatever Or do we want to add double List of objects #[derive(Config)]
struct QuicklinkConfig {
/// Label of the link.
label: String,
/// Target of the link.
href: String,
}
#[derive(Config)]
struct Conf {
/// Links shown in the footer.
#[config(nested)]
quicklinks: Vec<QuicklinkConfig>,
} # Links shown in the footer.
#
# Required!
#[[quicklinks]]
# Label of the link.
#
# Required!
#label =
# Target of the link.
#
# Required!
#href = The other array syntax in TOML might be more preferable, but it's unclear how to best emit the field docs then. # Links shown in the footer.
#
# Each object in the list has the following fields:
# - label: Label of the link (required)
# - href: Target of the link (required)
#
# Required!
#quicklinks = [] Both TOML examples are not great IMO. The first one is confusing because of TOMLs YAML is a bit better here I think: # Links shown in the footer.
#
# Required.
quicklinks:
# Label of the link.
#
# Required.
# - label:
# Target of the link.
#
# Required.
# href: But I can't come up with a really nice way to incooperate the field docs here and how to indent them. Something similar can be observed with a map of nested objects. But there, the example key probably needs to be specified by the user. So I think my takeaway is that there isn't really a perfect way. Oftentimes the user wants/needs more control over how the template is formatted, which increases complexity. Just letting the user manually write a doc comment with examples, as is the current workaround for half of this issue, might not be too bad after all. Of course, so far I have only looked at small examples. If someone has a good use case where the nested object (in option, list, map) is quite large, please let me know! Because at some point manually writing the example and field descriptions will be a problem. Random thoughtsRandom thought: letting the user write simple Mustache-template-like things inside the doc comment, giving it access to docs of subfields? Random thought 2: to solve the "you can always write manually, but it gets problematic with multiple formats", confique could filter out fenced code blocks of the wrong format? And convert fenced code blocks into indented ones? #[derive(Config)]
struct Conf {
/// Cool example:
///
/// ```toml
/// foo = "heyyy"
/// ```
/// ```yaml
/// foo: heyyy
/// ```
foo: String,
} Would result in in these, depending on the format. # Cool example:
#
# foo = "heyyy"
#
# Required!
#foo = # Cool example:
#
# foo: heyyy
#
# Required!
#foo: Merge modesIt might also be a neat idea to add a attribute That would solve the "merge" part of this issue. Sorry for the extremly unstructured comment, but I needed to dump these thoughts as otherwise I start at 0 again next time I try tackling this. |
Oh also: |
Hello! mesh_bbox: &mesh_bbox
xmin: -98.00556
ymin: 8.534422
xmax: -60.040005
ymax: 45.831431
crs: 'epsg:4326'
rasters:
- &GEBCO_2021_sub_ice_topo_1
path: 'GEBCO_2021_sub_ice_topo/gebco_2021_sub_ice_topo_n90.0_s0.0_w-180.0_e-90.0.tif'
bbox: *mesh_bbox
chunk_size: 1500
- &GEBCO_2021_sub_ice_topo_2
path: 'GEBCO_2021_sub_ice_topo/gebco_2021_sub_ice_topo_n90.0_s0.0_w-90.0_e0.0.tif'
bbox: *mesh_bbox
chunk_size: 1500
geom: &geom
zmax: &zmax 10.
rasters:
- <<: *GEBCO_2021_sub_ice_topo_1
zmax: 0.
overlap: 5
- <<: *GEBCO_2021_sub_ice_topo_2
zmax: 0.
overlap: 5
sieve: true The objective is that the same yaml file that works for Python continues to work in Rust. To give you more context, my application will process separate keys, for example, the use confique::Config;
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Config, Deserialize)]
pub struct RasterConfig { ■ similarly named struct `RasterConfig` defined here
path: PathBuf,
}
#[derive(Config, Deserialize)]
pub enum RestersConfig { ■ `confique::Config` can only be derive for structs with named fields
Single(RasterConfig),
Multiple(Vec<RasterConfig>),
}
#[derive(Debug, Config, Deserialize)] ■ you might be missing a type parameter: `<RastersConfig>`
pub struct GeomConfigOpts { ■ you might be missing a type parameter: `<RastersConfig>`
zmax: Option<f64>,
zmin: Option<f64>,
sieve: bool,
rasters: Option<RastersConfig>, ■■ a struct with a similar name exists: `RasterConfig`
}
#[derive(Debug, Config, Deserialize)]
pub struct GeomConfig {
geom: GeomConfigOpts,
} I have included the error indications as well for reference. I think the confique style has a lot of potential, but I still haven't found one crate that is full featured like pydantic is. Any thoughts on this? |
@jreniel Is there any reason you have to derive |
Thanks for looking into this.
Because the compiler will complain that
But it won't validate and populate the nested struct, so reading the file this way is pointless. And it is pointless for two reasons. The In the end, I was able to get what I needed by just using plain I really like where Thanks for looking into this anyway! #[derive(Debug, Validate, Serialize, Deserialize)]
pub struct GeomRasterConfig {
#[validate(nested)]
#[serde(flatten)]
raster: RasterConfig,
zmax: Option<f64>,
zmin: Option<f64>,
}
#[derive(Debug, Validate, Serialize, Deserialize)]
pub struct GeomConfigOpts {
zmax: Option<f64>,
zmin: Option<f64>,
#[serde(default)]
sieve: bool,
#[validate(nested)]
rasters: Vec<GeomRasterConfig>,
}
#[derive(Debug, Validate, Serialize, Deserialize)]
pub struct GeomConfig {
#[validate(nested)]
geom: GeomConfigOpts,
}
impl GeomConfig {
pub fn iter_raster_windows(&self) -> RastersWindowIter {
RastersWindowIter {
geom: &self.geom,
current_raster_index: 0,
current_window_index: 0,
}
}
} |
I think this is the relevant issue, I needed support for having a map of a string id to a nested-config object in my config, and managed to add it with a newtype with a manual impl of #[derive(Debug)]
pub struct ConfigMap<K: DeserializeOwned + Eq + Hash, V: confique::Config> {
pub inner: HashMap<K, V>,
}
#[derive(Debug)]
pub struct ConfigMapPartial<K: DeserializeOwned + Eq + Hash, V: confique::Partial> {
pub inner: HashMap<K, V>,
}
impl<K: DeserializeOwned + Eq + Hash, V: confique::Config> confique::Config for ConfigMap<K, V> {
type Partial = ConfigMapPartial<K, V::Partial>;
// TODO
const META: confique::meta::Meta = confique::meta::Meta {
name: "",
doc: &[],
fields: &[],
};
fn from_partial(partial: Self::Partial) -> Result<Self, confique::Error> {
// TODO: this needs to use `confique::internal::map_err_prefix_path` to give the correct path in errors
let inner: Result<_, confique::Error> = partial.inner.into_iter().map(|(k, v)| Ok((k, V::from_partial(v)?))).collect();
Ok(Self { inner: inner? })
}
}
impl<'de, K: DeserializeOwned + Eq + Hash, V: confique::Partial> serde::Deserialize<'de> for ConfigMapPartial<K, V> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::de::Deserializer<'de> {
Ok(Self { inner: HashMap::deserialize(deserializer)? })
}
}
impl<K: DeserializeOwned + Eq + Hash, V: confique::Partial> confique::Partial for ConfigMapPartial<K, V> {
fn empty() -> Self { Self { inner: HashMap::new() } }
fn default_values() -> Self { Self::empty() }
fn from_env() -> Result<Self, confique::Error> {
// TODO: dunno if this makes sense to support somehow
Ok(Self::empty())
}
fn with_fallback(mut self, fallback: Self) -> Self {
for (k, v) in fallback.inner {
let v = match self.inner.remove(&k) {
Some(value) => value.with_fallback(v),
None => v,
};
self.inner.insert(k, v);
}
self
}
fn is_empty(&self) -> bool { self.inner.is_empty() }
fn is_complete(&self) -> bool { self.inner.values().all(|v| v.is_complete()) }
}
impl<K: DeserializeOwned + Eq + Hash + Clone, V: confique::Partial + Clone> Clone for ConfigMapPartial<K, V> {
fn clone(&self) -> Self {
Self { inner: self.inner.clone() }
}
} |
Are there any plans for Confique to support this, i.e. nested config templates. Would really love to have documentation of the nested config show up as well. |
It might be useful to treat nested configuration more like normal values? Making them optional or putting them in lists/maps. But there are lots of open questions and I have to reevaluate whether this requirement still makes sense now that we can have maps and arrays as default values.
The text was updated successfully, but these errors were encountered: