diff --git a/Cargo.lock b/Cargo.lock index 133b91bb..b2c7c221 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1868,6 +1868,7 @@ dependencies = [ "pulldown-cmark", "pulldown-cmark-mdcat", "serde", + "serde_json", "syntect", "thiserror", "tokio", diff --git a/crates/nix_rs/CHANGELOG.md b/crates/nix_rs/CHANGELOG.md index 741f8e3f..7aebd0fc 100644 --- a/crates/nix_rs/CHANGELOG.md +++ b/crates/nix_rs/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- **`flake::url`**: + - Remove `qualified_attr` module - **`eval::nix_eval`** - Display evaluation progress - Decrease logging verbosity diff --git a/crates/nix_rs/src/flake/url/mod.rs b/crates/nix_rs/src/flake/url/mod.rs index fc26e68f..37c6d30b 100644 --- a/crates/nix_rs/src/flake/url/mod.rs +++ b/crates/nix_rs/src/flake/url/mod.rs @@ -1,6 +1,5 @@ //! Work with flake URLs pub mod attr; mod core; -pub mod qualified_attr; pub use core::*; diff --git a/crates/nix_rs/src/flake/url/qualified_attr.rs b/crates/nix_rs/src/flake/url/qualified_attr.rs deleted file mode 100644 index e6249617..00000000 --- a/crates/nix_rs/src/flake/url/qualified_attr.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Flake attributes that are "qualified" -use crate::{ - command::{NixCmd, NixCmdError}, - flake::eval::nix_eval_attr, -}; - -use super::FlakeUrl; - -/// Like [nix_eval_attr] but relocates attr under one of the given root attributes -/// -/// A qualified attribute is expected to be found at the root of a flake -/// -/// `&["om.ci", "nixci"]` will locate the given attribute -/// under the one of these parent attributes, searched in that order. -pub async fn nix_eval_qualified_attr( - cmd: &NixCmd, - url: &FlakeUrl, - root_attrs: &[S], -) -> Result<(T, Vec), QualifiedAttrError> -where - S: AsRef, - T: Default + serde::de::DeserializeOwned, -{ - // Try one of root_attrs to see if it exists in flake - for root_attr in root_attrs { - let qualified_url = url.with_attr(root_attr.as_ref()); - if let Some(v) = nix_eval_attr(cmd, &qualified_url).await? { - return Ok((v, url.get_attr().as_list())); - } - } - - // When none of root_attr matches, return default - match url.get_attr().0 { - None => Ok((Default::default(), vec![])), - Some(attr) => Err(QualifiedAttrError::UnexpectedAttribute(attr)), - } -} - -/// Error type for [nix_eval_qualified_attr] -#[derive(thiserror::Error, Debug)] -pub enum QualifiedAttrError { - /// When the attribute is not found in the flake - #[error("Unexpected attribute, when config not present in flake: {0}")] - UnexpectedAttribute(String), - - /// A [NixCmdError] - #[error("Nix command error: {0}")] - CommandError(#[from] NixCmdError), -} diff --git a/crates/omnix-ci/src/command/core.rs b/crates/omnix-ci/src/command/core.rs index f2ed0671..4aede87e 100644 --- a/crates/omnix-ci/src/command/core.rs +++ b/crates/omnix-ci/src/command/core.rs @@ -5,7 +5,7 @@ use nix_rs::command::NixCmd; use omnix_common::config::OmConfig; use tracing::instrument; -use crate::{config::subflakes::SubflakesConfig, flake_ref::FlakeRef}; +use crate::flake_ref::FlakeRef; use super::{gh_matrix::GHMatrixCommand, run::RunCommand}; @@ -38,26 +38,20 @@ impl Command { #[instrument(name = "run", skip(self))] pub async fn run(self, nixcmd: &NixCmd, verbose: bool) -> anyhow::Result<()> { tracing::info!("{}", "\n👟 Reading om.ci config from flake".bold()); - let cfg = self.get_config(nixcmd).await?; + let url = self.get_flake_ref().to_flake_url().await?; + let cfg = OmConfig::from_flake_url(nixcmd, &url).await?; + tracing::debug!("OmConfig: {cfg:?}"); match self { Command::Run(cmd) => cmd.run(nixcmd, verbose, cfg).await, Command::DumpGithubActionsMatrix(cmd) => cmd.run(cfg).await, } } - /// Get the omnix-ci [config::Config] associated with this subcommand - async fn get_config(&self, cmd: &NixCmd) -> anyhow::Result> { - let url = self.get_flake_ref().to_flake_url().await?; - let cfg = crate::config::core::ci_config_from_flake_url(cmd, &url).await?; - tracing::debug!("Config: {cfg:?}"); - Ok(cfg) - } - - /// Get the flake ref associated with this subcommand - fn get_flake_ref(&self) -> FlakeRef { + /// Get the [FlakeRef] associated with this subcommand + fn get_flake_ref(&self) -> &FlakeRef { match self { - Command::Run(cmd) => cmd.flake_ref.clone(), - Command::DumpGithubActionsMatrix(cmd) => cmd.flake_ref.clone(), + Command::Run(cmd) => &cmd.flake_ref, + Command::DumpGithubActionsMatrix(cmd) => &cmd.flake_ref, } } diff --git a/crates/omnix-ci/src/command/gh_matrix.rs b/crates/omnix-ci/src/command/gh_matrix.rs index 6473c304..214f45c8 100644 --- a/crates/omnix-ci/src/command/gh_matrix.rs +++ b/crates/omnix-ci/src/command/gh_matrix.rs @@ -22,8 +22,8 @@ pub struct GHMatrixCommand { impl GHMatrixCommand { /// Run the command - pub async fn run(&self, cfg: OmConfig) -> anyhow::Result<()> { - let (config, _rest) = cfg.get_referenced()?; + pub async fn run(&self, cfg: OmConfig) -> anyhow::Result<()> { + let (config, _rest) = cfg.get_sub_config_under::("ci")?; let matrix = github::matrix::GitHubMatrix::from(self.systems.clone(), &config); println!("{}", serde_json::to_string(&matrix)?); Ok(()) diff --git a/crates/omnix-ci/src/command/run.rs b/crates/omnix-ci/src/command/run.rs index 9e4164f8..52b3dd76 100644 --- a/crates/omnix-ci/src/command/run.rs +++ b/crates/omnix-ci/src/command/run.rs @@ -65,12 +65,7 @@ impl RunCommand { } /// Run the build command which decides whether to do ci run on current machine or a remote machine - pub async fn run( - &self, - nixcmd: &NixCmd, - verbose: bool, - cfg: OmConfig, - ) -> anyhow::Result<()> { + pub async fn run(&self, nixcmd: &NixCmd, verbose: bool, cfg: OmConfig) -> anyhow::Result<()> { match &self.on { Some(store_uri) => run_remote::run_on_remote_store(nixcmd, self, &cfg, store_uri).await, None => self.run_local(nixcmd, verbose, cfg).await, @@ -78,12 +73,7 @@ impl RunCommand { } /// Run [RunCommand] on local Nix store. - async fn run_local( - &self, - nixcmd: &NixCmd, - verbose: bool, - cfg: OmConfig, - ) -> anyhow::Result<()> { + async fn run_local(&self, nixcmd: &NixCmd, verbose: bool, cfg: OmConfig) -> anyhow::Result<()> { // TODO: We'll refactor this function to use steps // https://github.com/juspay/omnix/issues/216 @@ -95,7 +85,7 @@ impl RunCommand { // First, run the necessary health checks tracing::info!("{}", "\n🫀 Performing health check".bold()); - check_nix_version(&cfg.flake_url, nix_info).await?; + check_nix_version(&cfg, nix_info).await?; // Then, do the CI steps tracing::info!( @@ -153,9 +143,11 @@ impl RunCommand { } /// Check that Nix version is not too old. -pub async fn check_nix_version(flake_url: &FlakeUrl, nix_info: &NixInfo) -> anyhow::Result<()> { - let omnix_health = NixHealth::from_flake(flake_url).await?; - let checks = omnix_health.nix_version.check(nix_info, Some(flake_url)); +pub async fn check_nix_version(cfg: &OmConfig, nix_info: &NixInfo) -> anyhow::Result<()> { + let omnix_health = NixHealth::from_om_config(cfg)?; + let checks = omnix_health + .nix_version + .check(nix_info, Some(&cfg.flake_url)); let exit_code = NixHealth::print_report_returning_exit_code(&checks).await?; if exit_code != 0 { @@ -169,13 +161,14 @@ pub async fn ci_run( cmd: &NixCmd, verbose: bool, run_cmd: &RunCommand, - cfg: &OmConfig, + cfg: &OmConfig, nix_config: &NixConfig, ) -> anyhow::Result { let mut res = HashMap::new(); let systems = run_cmd.get_systems(cmd, nix_config).await?; - let (config, attrs) = cfg.get_referenced()?; + let (config, attrs) = cfg.get_sub_config_under::("ci")?; + // User's filter by subflake name let only_subflake = attrs.first(); diff --git a/crates/omnix-ci/src/command/run_remote.rs b/crates/omnix-ci/src/command/run_remote.rs index c4a9adbe..30673c06 100644 --- a/crates/omnix-ci/src/command/run_remote.rs +++ b/crates/omnix-ci/src/command/run_remote.rs @@ -10,8 +10,6 @@ use omnix_common::config::OmConfig; use std::path::PathBuf; use tokio::process::Command; -use crate::config::subflakes::SubflakesConfig; - use super::run::RunCommand; /// Path to Rust source corresponding to this (running) instance of Omnix @@ -21,7 +19,7 @@ const OMNIX_SOURCE: &str = env!("OMNIX_SOURCE"); pub async fn run_on_remote_store( nixcmd: &NixCmd, run_cmd: &RunCommand, - cfg: &OmConfig, + cfg: &OmConfig, store_uri: &StoreURI, ) -> anyhow::Result<()> { tracing::info!( @@ -48,10 +46,7 @@ pub async fn run_on_remote_store( } /// Return the locally cached [FlakeUrl] for the given flake url that points to same selected [ConfigRef]. -async fn cache_flake( - nixcmd: &NixCmd, - cfg: &OmConfig, -) -> anyhow::Result<(PathBuf, FlakeUrl)> { +async fn cache_flake(nixcmd: &NixCmd, cfg: &OmConfig) -> anyhow::Result<(PathBuf, FlakeUrl)> { let metadata = FlakeMetadata::from_nix(nixcmd, &cfg.flake_url).await?; let path = metadata.path.to_string_lossy().into_owned(); let attr = cfg.reference.join("."); diff --git a/crates/omnix-ci/src/config/core.rs b/crates/omnix-ci/src/config/core.rs index 713926d2..016e0ee0 100644 --- a/crates/omnix-ci/src/config/core.rs +++ b/crates/omnix-ci/src/config/core.rs @@ -1,34 +1,11 @@ //! The top-level configuration of omnix-ci, as defined in flake.nix -use anyhow::Result; -use nix_rs::{command::NixCmd, flake::url::FlakeUrl}; -use omnix_common::config::OmConfig; - -use super::subflakes::SubflakesConfig; - -/// Create a `Config` pointed to by this [FlakeUrl] -/// -/// Example: -/// ```text -/// let url = FlakeUrl("github:srid/haskell-flake#default.dev".to_string()); -/// let cfg = Config::from_flake_url(&url).await?; -/// ``` -/// along with the config. -pub async fn ci_config_from_flake_url( - cmd: &NixCmd, - url: &FlakeUrl, -) -> Result> { - let v = omnix_common::config::OmConfig::::from_flake_url( - cmd, - url, - &["om.ci", "nixci"], - ) - .await?; - Ok(v) -} #[cfg(test)] mod tests { - use super::*; + use nix_rs::{command::NixCmd, flake::url::FlakeUrl}; + use omnix_common::config::OmConfig; + + use crate::config::subflakes::SubflakesConfig; #[tokio::test] async fn test_config_loading() { @@ -38,10 +15,10 @@ mod tests { "github:srid/haskell-flake/76214cf8b0d77ed763d1f093ddce16febaf07365#default.dev" .to_string(), ); - let cfg = ci_config_from_flake_url(&NixCmd::default(), url) + let cfg = OmConfig::from_flake_url(&NixCmd::default(), url) .await .unwrap(); - let (config, attrs) = cfg.get_referenced().unwrap(); + let (config, attrs) = cfg.get_sub_config_under::("ci").unwrap(); assert_eq!(attrs, &["dev"]); // assert_eq!(cfg.selected_subconfig, Some("dev".to_string())); assert_eq!(config.0.len(), 7); diff --git a/crates/omnix-cli/src/command/develop.rs b/crates/omnix-cli/src/command/develop.rs index 27bc7985..c7e44983 100644 --- a/crates/omnix-cli/src/command/develop.rs +++ b/crates/omnix-cli/src/command/develop.rs @@ -1,5 +1,6 @@ use clap::Parser; -use nix_rs::flake::url::FlakeUrl; +use nix_rs::{command::NixCmd, flake::url::FlakeUrl}; +use omnix_common::config::OmConfig; /// Prepare to develop on a flake project #[derive(Parser, Debug)] @@ -26,8 +27,11 @@ enum Stage { impl DevelopCommand { pub async fn run(&self) -> anyhow::Result<()> { let flake = self.flake_shell.without_attr(); + + let om_config = OmConfig::from_flake_url(NixCmd::get().await, &self.flake_shell).await?; + tracing::info!("⌨️ Preparing to develop project: {:}", &flake); - let prj = omnix_develop::core::Project::new(flake).await?; + let prj = omnix_develop::core::Project::new(flake, om_config).await?; match self.stage { Some(Stage::PreShell) => omnix_develop::core::develop_on_pre_shell(&prj).await?, Some(Stage::PostShell) => omnix_develop::core::develop_on_post_shell(&prj).await?, diff --git a/crates/omnix-common/Cargo.toml b/crates/omnix-common/Cargo.toml index e5419d65..4190abec 100644 --- a/crates/omnix-common/Cargo.toml +++ b/crates/omnix-common/Cargo.toml @@ -26,3 +26,4 @@ tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } which = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/omnix-common/src/config.rs b/crates/omnix-common/src/config.rs index 5186c572..50b031c9 100644 --- a/crates/omnix-common/src/config.rs +++ b/crates/omnix-common/src/config.rs @@ -4,72 +4,88 @@ use std::collections::BTreeMap; use nix_rs::{ command::NixCmd, - flake::url::{ - qualified_attr::{nix_eval_qualified_attr, QualifiedAttrError}, - FlakeUrl, - }, + flake::{eval::nix_eval_attr, url::FlakeUrl}, }; -use serde::de::DeserializeOwned; +use serde::{de::DeserializeOwned, Deserialize}; -/// Reference to some Omnix configuration of type `BTeeMap` in a flake +/// [OmConfigTree] with additional metadata about the flake URL and reference. /// -/// For example, CI configuration at `om.ci.default` is captured by the `T` type. +/// `reference` here is the part of the flake URL after `#` #[derive(Debug)] -pub struct OmConfig { +pub struct OmConfig { /// The flake URL used to load this configuration pub flake_url: FlakeUrl, /// The (nested) key reference into the flake config. pub reference: Vec, - /// The whole `om.??` configuration parsed as `T` - pub config: BTreeMap, + /// The config tree + pub config: OmConfigTree, } -impl OmConfig { - /// Read the Om configuration from the flake URL - pub async fn from_flake_url( - cmd: &NixCmd, - url: &FlakeUrl, - k: &[S], - ) -> Result, OmConfigError> - where - S: AsRef, - T: DeserializeOwned, - { - let (config, reference) = - nix_eval_qualified_attr::, _>(cmd, url, k).await?; +impl OmConfig { + /// Read the om configuration from the flake url + pub async fn from_flake_url(cmd: &NixCmd, flake_url: &FlakeUrl) -> Result { Ok(OmConfig { - flake_url: url.without_attr(), - reference, - config, + flake_url: flake_url.without_attr(), + reference: flake_url.get_attr().as_list(), + config: nix_eval_attr(cmd, &flake_url.with_attr("om")) + .await? + .unwrap_or_default(), }) } - /// Get the user-referenced config value `T` - /// - /// If the user passes `.#foo.bar` this selects "foo" from the config tree, along with returning ["bar"]. - /// - /// If nothing is specifically passed, a default value is returned, either from config tree (key "default") or `T::default()`. + /// Get the user referenced (per `referenced`) sub-tree under the given root key. /// - /// TODO: This needs to be adjusted to support `om.templates` style configuration as well, where this default behaviour makes no sense. - pub fn get_referenced(&self) -> Result<(T, &[String]), OmConfigError> + /// get_sub_config_under("ci") will return `ci.default` (or Default instance if config is missing) without a reference. Otherwise, it will use the reference to find the correct sub-tree. + pub fn get_sub_config_under(&self, root_key: &str) -> Result<(T, &[String]), OmConfigError> where - T: Default + Clone, + T: Default + DeserializeOwned + Clone, { - if let Some((k, rest)) = self.reference.split_first() { - if let Some(v) = self.config.get(k) { - Ok((v.clone(), rest)) - } else { - Err(OmConfigError::MissingConfigAttribute(k.to_string())) + // Get the config map, returning default if it doesn't exist + let config = match self.config.get::(root_key)? { + Some(res) => res, + None => { + return if self.reference.is_empty() { + Ok((T::default(), &[])) + } else { + // Reference requires the config to exist. + Err(OmConfigError::UnexpectedAttribute(self.reference.join("."))) + }; } - } else { - // Use default - if let Some(v) = self.config.get("default") { - Ok((v.clone(), &[])) - } else { - Ok((T::default(), &[])) + }; + + let default = "default".to_string(); + let (k, rest) = self.reference.split_first().unwrap_or((&default, &[])); + + let v: &T = config + .get(k) + .ok_or(OmConfigError::MissingConfigAttribute(k.to_string()))?; + Ok((v.clone(), rest)) + } +} + +/// Represents the whole configuration for `omnix` parsed from JSON +#[derive(Debug, Default, Deserialize)] +pub struct OmConfigTree(BTreeMap>); + +impl OmConfigTree { + /// Get all the configs of type `T` for a given sub-config + /// + /// Return None if key doesn't exist + pub fn get(&self, key: &str) -> Result>, serde_json::Error> + where + T: DeserializeOwned, + { + match self.0.get(key) { + Some(config) => { + let result: Result, _> = config + .iter() + .map(|(k, v)| serde_json::from_value(v.clone()).map(|value| (k.clone(), value))) + .collect(); + result.map(Some) } + None => Ok(None), } } } @@ -77,11 +93,19 @@ impl OmConfig { /// Error type for OmConfig #[derive(thiserror::Error, Debug)] pub enum OmConfigError { - /// Qualified attribute error - #[error("Qualified attribute error: {0}")] - QualifiedAttrError(#[from] QualifiedAttrError), - /// Missing configuration attribute #[error("Missing configuration attribute: {0}")] MissingConfigAttribute(String), + + /// Unexpected attribute + #[error("Unexpected attribute: {0}")] + UnexpectedAttribute(String), + + /// A [nix_rs::command::NixCmdError] + #[error("Nix command error: {0}")] + NixCmdError(#[from] nix_rs::command::NixCmdError), + + /// Failed to parse JSON + #[error("Failed to decode (json error): {0}")] + DecodeErrorJson(#[from] serde_json::Error), } diff --git a/crates/omnix-develop/src/config.rs b/crates/omnix-develop/src/config.rs index 4633e8bb..1ef3a714 100644 --- a/crates/omnix-develop/src/config.rs +++ b/crates/omnix-develop/src/config.rs @@ -1,6 +1,5 @@ use serde::Deserialize; -use nix_rs::{command::NixCmd, flake::url::FlakeUrl}; use omnix_common::config::OmConfig; use crate::readme::Readme; @@ -17,11 +16,8 @@ pub struct CacheConfig { } impl DevelopConfig { - pub async fn from_flake(url: &FlakeUrl) -> anyhow::Result { - let v = OmConfig::::from_flake_url(NixCmd::get().await, url, &["om.develop"]) - .await? - .config; - let config = v.get("default").cloned().unwrap_or_default(); + pub fn from_om_config(om_config: &OmConfig) -> anyhow::Result { + let (config, _rest) = om_config.get_sub_config_under("develop")?; Ok(config) } } diff --git a/crates/omnix-develop/src/core.rs b/crates/omnix-develop/src/core.rs index 56f0f104..9ae5574d 100644 --- a/crates/omnix-develop/src/core.rs +++ b/crates/omnix-develop/src/core.rs @@ -2,7 +2,7 @@ use anyhow::Context; use std::{env::current_dir, path::PathBuf}; use nix_rs::{flake::url::FlakeUrl, info::NixInfo}; -use omnix_common::markdown::print_markdown; +use omnix_common::{config::OmConfig, markdown::print_markdown}; use omnix_health::{check::caches::CachixCache, traits::Checkable, NixHealth}; use crate::config::DevelopConfig; @@ -13,18 +13,21 @@ pub struct Project { pub dir: Option, /// [FlakeUrl] corresponding to the project. pub flake: FlakeUrl, - /// The develop configuration - pub cfg: DevelopConfig, + /// The `om` configuration + pub om_config: OmConfig, } impl Project { - pub async fn new(flake: FlakeUrl) -> anyhow::Result { + pub async fn new(flake: FlakeUrl, om_config: OmConfig) -> anyhow::Result { let dir = match flake.as_local_path() { Some(path) => Some(path.canonicalize()?), None => None, }; - let cfg = DevelopConfig::from_flake(&flake).await?; - Ok(Self { dir, flake, cfg }) + Ok(Self { + dir, + flake, + om_config, + }) } } @@ -41,7 +44,7 @@ pub async fn develop_on(prj: &Project) -> anyhow::Result<()> { pub async fn develop_on_pre_shell(prj: &Project) -> anyhow::Result<()> { // Run relevant `om health` checks - let health = NixHealth::from_flake(&prj.flake).await?; + let health = NixHealth::from_om_config(&prj.om_config)?; let nix_info = NixInfo::get() .await .as_ref() @@ -89,7 +92,8 @@ pub async fn develop_on_post_shell(prj: &Project) -> anyhow::Result<()> { eprintln!(); let pwd = current_dir()?; let dir = prj.dir.as_ref().unwrap_or(&pwd); - print_markdown(dir, prj.cfg.readme.get_markdown()).await?; + let cfg = DevelopConfig::from_om_config(&prj.om_config)?; + print_markdown(dir, cfg.readme.get_markdown()).await?; Ok(()) } diff --git a/crates/omnix-health/src/lib.rs b/crates/omnix-health/src/lib.rs index 61c1e0a6..01bbd27e 100644 --- a/crates/omnix-health/src/lib.rs +++ b/crates/omnix-health/src/lib.rs @@ -66,11 +66,8 @@ impl NixHealth { /// /// Fallback to using the default health check config if the flake doesn't /// override it. - pub async fn from_flake(url: &FlakeUrl) -> Result { - let cmd = NixCmd::get().await; - let cfg = - OmConfig::::from_flake_url(cmd, url, &["om.health", "nix-health"]).await?; - let (cfg, _rest) = cfg.get_referenced()?; + pub fn from_om_config(om_config: &OmConfig) -> Result { + let (cfg, _rest) = om_config.get_sub_config_under::("health")?; Ok(cfg.clone()) } @@ -111,7 +108,10 @@ pub async fn run_all_checks_with(flake_url: Option) -> anyhow::Result< .with_context(|| "Unable to gather nix info")?; let health: NixHealth = match flake_url.as_ref() { - Some(flake_url) => NixHealth::from_flake(flake_url).await, + Some(flake_url) => { + let om_config = OmConfig::from_flake_url(NixCmd::get().await, flake_url).await?; + NixHealth::from_om_config(&om_config) + } None => Ok(NixHealth::default()), }?; diff --git a/crates/omnix-init/src/config.rs b/crates/omnix-init/src/config.rs index ca2216b6..c92e07ef 100644 --- a/crates/omnix-init/src/config.rs +++ b/crates/omnix-init/src/config.rs @@ -1,10 +1,7 @@ use std::fmt::{self, Display, Formatter}; use colored::Colorize; -use nix_rs::{ - command::NixCmd, - flake::{command::FlakeOptions, url::FlakeUrl}, -}; +use nix_rs::{command::NixCmd, flake::url::FlakeUrl}; use omnix_common::config::OmConfig; use crate::template::Template; @@ -36,14 +33,14 @@ impl<'a> Display for FlakeTemplate<'a> { /// Load templates from the given flake pub async fn load_templates<'a>(url: &FlakeUrl) -> anyhow::Result> { - let _opts = FlakeOptions { - refresh: true, - ..Default::default() - }; - let v = OmConfig::