add std feature (#231)

- `std` feature is enabled by default
- By default enable #![no_std] compilation
- Import std create if `std` feature is enabled or if testing
- Use core, alloc types
- Make it clear where std types are being used
- In no std, use BTreeMap in place of HashMap.
   HashMap is not available in no std due to lack of a
   secure random number generator

Note: The project does not yet compile without std feature being specified.
But it's really close to being able to do so.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2024-05-09 14:28:42 -04:00
committed by GitHub
parent e9cd6d6afc
commit 01fc234a33
36 changed files with 128 additions and 69 deletions

View File

@@ -22,6 +22,8 @@ jobs:
run: sudo apt update && sudo apt install -y musl-tools
- name: Format Check
run: cargo fmt --check
- name: Build (all features)
run: cargo build -r --all-features --verbose
- name: Build
run: cargo build -r --verbose
- name: Doc Tests

View File

@@ -40,9 +40,10 @@ jsonschema = ["dep:jsonschema"]
opa-runtime = []
regex = ["dep:regex"]
semver = ["dep:semver"]
std = ["serde_json/std"]
time = ["dep:chrono", "dep:chrono-tz"]
uuid = ["dep:uuid"]
urlquery = ["dep:url"]
time = ["dep:chrono", "dep:chrono-tz"]
yaml = ["serde_yaml"]
full-opa = [
"base64",
@@ -59,6 +60,7 @@ full-opa = [
"opa-runtime",
"regex",
"semver",
"std",
"time",
"uuid",
"urlquery",
@@ -70,8 +72,8 @@ opa-testutil = []
[dependencies]
anyhow = { version = "1.0.45", default-features=false }
serde = {version = "1.0.150", features = ["derive", "rc"] }
serde_json = "1.0.89"
serde = {version = "1.0.150", default-features = false, features = ["derive", "rc"] }
serde_json = { version = "1.0.89", default-features=false, features = ["alloc"] }
serde_yaml = {version = "0.9.16", optional = true }
lazy_static = "1.4.0"
rand = "0.8.5"

View File

@@ -11,6 +11,9 @@ if [ -f Cargo.toml ]; then
# Ensure that the public API works
cargo test -r --doc
# Ensure that we can build with all features
cargo build -r --all-features
# Ensure that all tests pass
cargo test -r

View File

@@ -3,7 +3,7 @@
use crate::lexer::*;
use crate::value::Value;
use crate::Rc;
use crate::*;
use core::{cmp, fmt, ops::Deref};

View File

@@ -7,6 +7,7 @@ use crate::builtins::utils::{ensure_args_count, ensure_numeric};
use crate::lexer::Span;
use crate::number::Number;
use crate::value::Value;
use crate::*;
use anyhow::{bail, Result};

View File

@@ -6,6 +6,7 @@ use crate::builtins;
use crate::builtins::utils::ensure_args_count;
use crate::lexer::Span;
use crate::value::Value;
use crate::*;
use anyhow::{bail, Result};

View File

@@ -5,6 +5,7 @@ use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::lexer::Span;
use crate::value::Value;
use crate::*;
use anyhow::{bail, Result};
@@ -43,8 +44,10 @@ pub fn print_to_string(
fn print(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let msg = print_to_string(span, params, args, strict)?;
#[cfg(feature = "std")]
if !msg.is_empty() {
eprintln!("{}", &msg[1..]);
std::eprintln!("{}", &msg[1..]);
}
Ok(Value::Bool(true))
}

View File

@@ -7,6 +7,7 @@ use crate::builtins::utils::{ensure_args_count, ensure_set};
use crate::builtins::BuiltinFcn;
use crate::lexer::Span;
use crate::value::Value;
use crate::*;
use anyhow::{bail, Result};
use lazy_static::lazy_static;

View File

@@ -9,6 +9,7 @@ use crate::builtins::utils::{
};
use crate::lexer::Span;
use crate::value::Value;
use crate::*;
#[allow(unused)]
use anyhow::{anyhow, bail, Context, Result};

View File

@@ -6,6 +6,7 @@ use crate::builtins;
use crate::builtins::utils::{ensure_args_count, ensure_string, ensure_string_collection};
use crate::lexer::Span;
use crate::value::Value;
use crate::*;
use anyhow::{bail, Result};
//use glob::{Pattern, MatchOptions};

View File

@@ -6,6 +6,7 @@ use crate::builtins;
use crate::builtins::utils::{ensure_args_count, ensure_object};
use crate::lexer::Span;
use crate::value::Value;
use crate::*;
use alloc::collections::{BTreeMap, BTreeSet};

View File

@@ -4,6 +4,7 @@
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::builtins::utils::{ensure_args_count, ensure_string};
use crate::*;
use crate::lexer::Span;
use crate::value::Value;

View File

@@ -47,7 +47,7 @@ use crate::ast::{Expr, Ref};
use crate::lexer::Span;
use crate::value::Value;
use std::collections::HashMap as BuiltinsMap;
use crate::Map as BuiltinsMap;
use anyhow::Result;
use lazy_static::lazy_static;

View File

@@ -7,6 +7,7 @@ use crate::builtins::utils::{ensure_args_count, ensure_numeric, ensure_string};
use crate::lexer::Span;
use crate::number::Number;
use crate::value::Value;
use crate::*;
use anyhow::{bail, Result};
use rand::{thread_rng, Rng};

View File

@@ -7,6 +7,7 @@ use crate::builtins::utils::{ensure_args_count, ensure_array, ensure_object};
use crate::lexer::Span;
use crate::Rc;
use crate::Value;
use crate::*;
use alloc::collections::{BTreeMap, BTreeSet};
use core::iter::Iterator;

View File

@@ -4,6 +4,7 @@
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::builtins::utils::ensure_args_count;
use crate::*;
use crate::lexer::Span;
use crate::value::Value;

View File

@@ -6,6 +6,7 @@ use crate::builtins;
use crate::builtins::utils::{ensure_args_count, ensure_numeric, ensure_string};
use crate::lexer::Span;
use crate::value::Value;
use crate::*;
use anyhow::{bail, Result};
use regex::Regex;

View File

@@ -6,6 +6,7 @@ use crate::builtins;
use crate::builtins::utils::{ensure_args_count, ensure_set};
use crate::lexer::Span;
use crate::value::Value;
use crate::*;
use alloc::collections::BTreeSet;

View File

@@ -10,6 +10,7 @@ use crate::builtins::utils::{
use crate::lexer::Span;
use crate::number::Number;
use crate::value::Value;
use crate::*;
use anyhow::{bail, Result};

View File

@@ -6,6 +6,7 @@ use crate::builtins;
use crate::builtins::utils::{ensure_args_count, ensure_numeric, ensure_string};
use crate::lexer::Span;
use crate::value::Value;
use crate::*;
use anyhow::{bail, Result};

View File

@@ -31,6 +31,7 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use crate::*;
use core::fmt;
use core::iter;
use std::error::Error;
@@ -1242,7 +1243,7 @@ mod tests {
];
for tc in test_cases {
println!("Test case {}", tc.name);
std::println!("Test case {}", tc.name);
let time = parse(&tc.format, &tc.value).unwrap();
check_time(time, &tc);
}
@@ -1313,7 +1314,7 @@ mod tests {
let time = PST8PDT.timestamp_nanos(1233810057012345600);
for tc in test_cases {
println!("Test case {}", tc.name);
std::println!("Test case {}", tc.name);
let result = format(time, &tc.format);
assert_eq!(result, tc.result);
}

View File

@@ -7,8 +7,9 @@ use crate::builtins::utils::{ensure_args_count, ensure_string};
use crate::lexer::Span;
use crate::number::Number;
use crate::value::Value;
use crate::*;
use anyhow::{bail, Context, Result};
use anyhow::{bail, Result};
pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("units.parse", (parse, 1));
@@ -87,12 +88,13 @@ fn parse(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Re
_ => (string, ""),
};
// Propagating the underlying error is not useful here.
let v: Value = if number_part.starts_with('.') {
serde_json::from_str(format!("0{number_part}").as_str())
} else {
serde_json::from_str(number_part)
}
.with_context(|| span.error("could not parse number"))?;
.map_err(|_| params[0].span().error("could not parse number"))?;
let mut n = match v {
Value::Number(n) => n.clone(),

View File

@@ -6,6 +6,7 @@ use crate::lexer::Span;
use crate::number::Number;
use crate::Rc;
use crate::Value;
use crate::*;
use alloc::collections::{BTreeMap, BTreeSet};

View File

@@ -6,6 +6,7 @@ use crate::builtins;
use crate::builtins::utils::{ensure_args_count, ensure_string};
use crate::lexer::Span;
use crate::value::Value;
use crate::*;
use alloc::collections::BTreeMap;

View File

@@ -8,11 +8,9 @@ use crate::parser::*;
use crate::scheduler::*;
use crate::utils::gather_functions;
use crate::value::*;
use crate::*;
use crate::{Extension, QueryResults};
use core::convert::AsRef;
use std::path::Path;
use anyhow::{bail, Result};
/// The Rego evaluation engine.
@@ -89,7 +87,8 @@ impl Engine {
/// # Ok(())
/// # }
/// ```
pub fn add_policy_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
#[cfg(feature = "std")]
pub fn add_policy_from_file<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
let source = Source::from_file(path)?;
let mut parser = Parser::new(&source)?;
self.modules.push(Ref::new(parser.parse()?));

View File

@@ -8,14 +8,13 @@ use crate::parser::Parser;
use crate::scheduler::*;
use crate::utils::*;
use crate::value::*;
use crate::Rc;
use crate::*;
use crate::{Expression, Extension, Location, QueryResult, QueryResults};
use alloc::collections::btree_map::Entry as BTreeMapEntry;
use alloc::collections::{BTreeMap, BTreeSet};
use anyhow::{anyhow, bail, Result};
use core::ops::Bound::*;
use std::collections::{hash_map::Entry, HashMap, HashSet};
type Scope = BTreeMap<SourceStr, Value>;
@@ -53,8 +52,8 @@ pub struct Interpreter {
loop_var_values: BTreeMap<ExprRef, Value>,
contexts: Vec<Context>,
functions: FunctionTable,
rules: HashMap<String, Vec<Ref<Rule>>>,
default_rules: HashMap<String, Vec<DefaultRuleInfo>>,
rules: Map<String, Vec<Ref<Rule>>>,
default_rules: Map<String, Vec<DefaultRuleInfo>>,
processed: BTreeSet<Ref<Rule>>,
processed_paths: Value,
rule_values: BTreeMap<Vec<Value>, (Value, Ref<Expr>)>,
@@ -65,16 +64,16 @@ pub struct Interpreter {
allow_deprecated: bool,
strict_builtin_errors: bool,
imports: BTreeMap<String, Ref<Expr>>,
extensions: HashMap<String, (u8, Rc<Box<dyn Extension>>)>,
extensions: Map<String, (u8, Rc<Box<dyn Extension>>)>,
#[cfg(feature = "coverage")]
coverage: HashMap<Source, Vec<bool>>,
coverage: Map<Source, Vec<bool>>,
#[cfg(feature = "coverage")]
enable_coverage: bool,
gather_prints: bool,
prints: Vec<String>,
rule_paths: HashSet<String>,
rule_paths: Set<String>,
}
impl Default for Interpreter {
@@ -178,8 +177,8 @@ impl Interpreter {
contexts: vec![],
loop_var_values: BTreeMap::new(),
functions: FunctionTable::new(),
rules: HashMap::new(),
default_rules: HashMap::new(),
rules: Map::new(),
default_rules: Map::new(),
processed: BTreeSet::new(),
processed_paths: Value::new_object(),
rule_values: BTreeMap::new(),
@@ -190,16 +189,16 @@ impl Interpreter {
allow_deprecated: true,
strict_builtin_errors: true,
imports: BTreeMap::default(),
extensions: HashMap::new(),
extensions: Map::new(),
#[cfg(feature = "coverage")]
coverage: HashMap::new(),
coverage: Map::new(),
#[cfg(feature = "coverage")]
enable_coverage: false,
gather_prints: false,
prints: Vec::default(),
rule_paths: HashSet::new(),
rule_paths: Set::new(),
}
}
@@ -3496,10 +3495,10 @@ impl Interpreter {
}
match self.rules.entry(path) {
Entry::Occupied(o) => {
MapEntry::Occupied(o) => {
o.into_mut().push(rule.clone());
}
Entry::Vacant(v) => {
MapEntry::Vacant(v) => {
v.insert(vec![rule.clone()]);
}
}
@@ -3523,7 +3522,7 @@ impl Interpreter {
}
match self.default_rules.entry(path) {
Entry::Occupied(o) => {
MapEntry::Occupied(o) => {
if idx + 1 == comps.len() {
for (_, i) in o.get() {
if index.is_some() && i.is_some() {
@@ -3539,7 +3538,7 @@ impl Interpreter {
}
o.into_mut().push((rule.clone(), index.clone()));
}
Entry::Vacant(v) => {
MapEntry::Vacant(v) => {
v.insert(vec![(rule.clone(), index.clone())]);
}
}
@@ -3562,7 +3561,8 @@ impl Interpreter {
},
Expr::Var(v) if v.0.text() == "input" => {
// Warn redundant import of input. Ignore it.
eprintln!(
#[cfg(feature = "std")]
std::eprintln!(
"{}",
import
.refr
@@ -3639,7 +3639,7 @@ impl Interpreter {
nargs: u8,
extension: Box<dyn Extension>,
) -> Result<()> {
if let std::collections::hash_map::Entry::Vacant(v) = self.extensions.entry(path) {
if let MapEntry::Vacant(v) = self.extensions.entry(path) {
v.insert((nargs, Rc::new(extension)));
Ok(())
} else {
@@ -3765,7 +3765,7 @@ impl Interpreter {
#[cfg(feature = "coverage")]
pub fn clear_coverage_data(&mut self) {
self.coverage = HashMap::new();
self.coverage = Map::new();
}
pub fn set_gather_prints(&mut self, b: bool) {

View File

@@ -1,16 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::*;
use core::cmp;
use core::convert::AsRef;
use core::fmt::{self, Debug, Formatter};
use core::iter::Peekable;
use core::str::CharIndices;
use std::hash::{Hash, Hasher};
use std::path::Path;
use crate::Rc;
use crate::Value;
use anyhow::{anyhow, bail, Result};
@@ -47,8 +43,9 @@ impl cmp::PartialEq for Source {
impl cmp::Eq for Source {}
impl Hash for Source {
fn hash<H: Hasher>(&self, state: &mut H) {
#[cfg(feature = "std")]
impl std::hash::Hash for Source {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
Rc::as_ptr(&self.src).hash(state)
}
}
@@ -156,7 +153,8 @@ impl Source {
})
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Source> {
#[cfg(feature = "std")]
pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Source> {
let contents = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => bail!("Failed to read {}. {e}", path.as_ref().display()),

View File

@@ -4,10 +4,19 @@
// Use README.md as crate documentation.
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
#![cfg_attr(docsrs, feature(doc_cfg))]
// We'll default to building for no_std - use core, alloc instead of std.
#![no_std]
extern crate alloc;
use serde::Serialize;
// Import std crate if building with std support.
// We don't import types or macros from std.
// As a result, types and macros from std must be qualified via `std::`
// making dependencies on std easier to spot.
#[cfg(any(feature = "std", test))]
extern crate std;
mod ast;
mod builtins;
mod engine;
@@ -28,6 +37,21 @@ use alloc::sync::Arc as Rc;
#[cfg(not(feature = "arc"))]
use alloc::rc::Rc;
#[cfg(feature = "std")]
use std::collections::{hash_map::Entry as MapEntry, HashMap as Map, HashSet as Set};
#[cfg(not(feature = "std"))]
use alloc::collections::{btree_map::Entry as MapEntry, BTreeMap as Map, BTreeSet as Set};
use alloc::{
borrow::ToOwned,
boxed::Box,
format,
string::{String, ToString},
vec,
vec::Vec,
};
use core::fmt;
/// Location of an [`Expression`] in a Rego query.
@@ -346,6 +370,8 @@ impl fmt::Debug for dyn Extension {
#[cfg(feature = "coverage")]
#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
pub mod coverage {
use crate::*;
#[derive(Default, serde::Serialize, serde::Deserialize)]
/// Coverage information about a rego policy file.
pub struct File {

View File

@@ -10,7 +10,7 @@ use anyhow::{anyhow, bail, Result};
use serde::ser::Serializer;
use serde::Serialize;
use crate::Rc;
use crate::*;
pub type BigInt = i128;

View File

@@ -5,6 +5,7 @@ use crate::ast::*;
use crate::lexer::*;
use crate::number::*;
use crate::value::*;
use crate::*;
use alloc::collections::BTreeMap;
use core::str::FromStr;
@@ -73,7 +74,8 @@ impl<'source> Parser<'source> {
let msg = format!(
"`{kw}` will be treated as identifier due to missing `import future.keywords.{kw}`"
);
println!(
#[cfg(feature = "std")]
std::println!(
"{}",
self.source
.message(self.tok.1.line, self.tok.1.col, "warning", &msg)

View File

@@ -1,10 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::ast::Expr::*;
use crate::ast::Expr::{Set, *};
use crate::ast::*;
use crate::lexer::*;
use crate::utils::*;
use crate::*;
use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
use alloc::string::String;
@@ -196,7 +197,8 @@ pub fn schedule<Str: Clone + cmp::Ord + fmt::Debug>(
}
if order.len() != num_statements {
eprintln!("could not schedule all statements {order:?} {orig_infos:?}");
#[cfg(feature = "std")]
std::eprintln!("could not schedule all statements {order:?} {orig_infos:?}");
return Ok(SortResult::Order(
(0..num_statements).map(|i| i as u16).collect(),
));

View File

@@ -176,7 +176,7 @@ pub fn eval_file(
let r = engine.eval_query(query.to_string(), enable_tracing)?;
let r_full = engine_full.eval_query_and_all_rules(query.to_string(), enable_tracing)?;
if r != r_full {
println!(
std::println!(
"{}\n{}",
serde_json::to_string_pretty(&r_full)?,
serde_json::to_string_pretty(&r)?
@@ -194,7 +194,7 @@ pub fn eval_file(
let r = engine.eval_query(query.to_string(), enable_tracing)?;
let r_full = engine_full.eval_query_and_all_rules(query.to_string(), enable_tracing)?;
if r != r_full {
println!(
std::println!(
"{}\n{}",
serde_json::to_string_pretty(&r_full)?,
serde_json::to_string_pretty(&r)?
@@ -277,12 +277,12 @@ fn yaml_test_impl(file: &str) -> Result<()> {
let yaml_str = std::fs::read_to_string(file)?;
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
println!("running {file}");
std::println!("running {file}");
for case in test.cases {
print!("case {} ", case.note);
std::print!("case {} ", case.note);
if case.skip == Some(true) {
println!("skipped");
std::println!("skipped");
continue;
}
@@ -329,13 +329,13 @@ fn yaml_test_impl(file: &str) -> Result<()> {
expected
);
}
println!("{actual}");
std::println!("{actual}");
}
_ => return Err(actual),
},
}
println!("passed");
std::println!("passed");
}
Ok(())

View File

@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::*;
use crate::{ast::*, lexer::*, parser::*, scheduler::*};
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
@@ -73,25 +74,25 @@ fn analyze_file(regos: &[String], expected_scopes: &[Scope]) -> Result<()> {
to_string_set(scope.inputs.iter()),
expected_scopes[idx].inputs
);
println!("scope {idx} matched.")
std::println!("scope {idx} matched.")
}
Ok(())
}
fn yaml_test_impl(file: &str) -> Result<()> {
println!("\nrunning {file}");
std::println!("\nrunning {file}");
let yaml_str = std::fs::read_to_string(file)?;
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
for case in &test.cases {
print!("\ncase {} ", case.note);
std::print!("\ncase {} ", case.note);
analyze_file(&case.modules, &case.scopes)?;
println!("passed");
std::println!("passed");
}
println!("{} cases passed.", test.cases.len());
std::println!("{} cases passed.", test.cases.len());
Ok(())
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT License.
use crate::scheduler::*;
use crate::*;
use anyhow::{bail, Result};
mod analyzer;
@@ -20,7 +21,7 @@ fn make_info(definitions: &[(&'static str, &[&'static str])]) -> StmtInfo<&'stat
fn print_stmts(stmts: &[&str], order: &[u16]) {
for idx in order.iter().cloned() {
println!("{}", stmts[idx as usize]);
std::println!("{}", stmts[idx as usize]);
}
}
@@ -29,7 +30,7 @@ fn check_result(stmts: &[&str], expected: &[&str], r: SortResult) -> Result<()>
SortResult::Order(order) => {
print_stmts(stmts, &order);
for (i, o) in order.iter().cloned().enumerate() {
println!("{:30}{}", stmts[o as usize], expected[i]);
std::println!("{:30}{}", stmts[o as usize], expected[i]);
}
for (i, o) in order.iter().cloned().enumerate() {
assert_eq!(stmts[o as usize], expected[i]);

View File

@@ -4,6 +4,7 @@
use crate::ast::*;
use crate::builtins::*;
use crate::lexer::*;
use crate::*;
use alloc::collections::BTreeMap;

View File

@@ -7,16 +7,15 @@ use alloc::collections::{BTreeMap, BTreeSet};
use core::fmt;
use core::ops;
use std::convert::AsRef;
use std::path::Path;
use std::str::FromStr;
use core::convert::AsRef;
use core::str::FromStr;
use anyhow::{anyhow, bail, Result};
use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
use serde::ser::{SerializeMap, Serializer};
use serde::{Deserialize, Serialize};
use crate::Rc;
use crate::*;
/// A value in a Rego document.
///
@@ -310,7 +309,7 @@ impl Value {
/// # }
/// ```
pub fn from_json_str(json: &str) -> Result<Value> {
Ok(serde_json::from_str(json)?)
serde_json::from_str(json).map_err(anyhow::Error::msg)
}
/// Deserialize a [`Value`] from a file containing JSON.
@@ -327,7 +326,8 @@ impl Value {
/// # Ok(())
/// # }
/// ```
pub fn from_json_file<P: AsRef<Path>>(path: P) -> Result<Value> {
#[cfg(feature = "std")]
pub fn from_json_file<P: AsRef<std::path::Path>>(path: P) -> Result<Value> {
match std::fs::read_to_string(&path) {
Ok(c) => Self::from_json_str(c.as_str()),
Err(e) => bail!("Failed to read {}. {e}", path.as_ref().display()),
@@ -394,7 +394,7 @@ impl Value {
/// # }
/// ```
pub fn to_json_str(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(self)?)
serde_json::to_string_pretty(self).map_err(anyhow::Error::msg)
}
/// Deserialize a value from YAML.
@@ -1132,9 +1132,9 @@ impl Value {
Some(pv) if *pv != *v => {
bail!(
"value for key `{}` generated multiple times: `{}` and `{}`",
serde_json::to_string_pretty(&k)?,
serde_json::to_string_pretty(&pv)?,
serde_json::to_string_pretty(&v)?,
serde_json::to_string_pretty(&k).map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&pv).map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&v).map_err(anyhow::Error::msg)?,
)
}
_ => Rc::make_mut(map).insert(k.clone(), v.clone()),