From 8c18b6f605b086929b781e4b8f78c1f81bec6de9 Mon Sep 17 00:00:00 2001 From: Radu Racariu Date: Tue, 8 Sep 2026 14:35:16 +0300 Subject: [PATCH 1/9] feat: add additional methods for DateTimeType to enhance datetime manipulation --- src/haystack/timezone/iana.rs | 79 ++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/src/haystack/timezone/iana.rs b/src/haystack/timezone/iana.rs index d5901f6..0fdf2ca 100644 --- a/src/haystack/timezone/iana.rs +++ b/src/haystack/timezone/iana.rs @@ -4,7 +4,8 @@ //! provided by chrono_tz. use chrono::{ - DateTime as StdDateTime, FixedOffset, NaiveDateTime, Offset, TimeZone, Timelike, Utc, + DateTime as StdDateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeDelta, + TimeZone, Timelike, Utc, }; use chrono_tz::{OffsetName, Tz, UTC}; @@ -98,6 +99,82 @@ impl DateTimeType { pub fn nanosecond(&self) -> u32 { self.naive_local().nanosecond() } + + /// The date component of this datetime's local time (see `chrono::DateTime::date_naive`). + pub fn date_naive(&self) -> NaiveDate { + self.naive_local().date() + } + + /// The time-of-day component of this datetime's local time. + pub fn time(&self) -> NaiveTime { + self.naive_local().time() + } + + /// The Unix timestamp, in milliseconds. + pub fn timestamp_millis(&self) -> i64 { + self.utc.and_utc().timestamp_millis() + } + + /// The Unix timestamp, in nanoseconds, if it fits in an `i64` (see + /// `chrono::DateTime::timestamp_nanos_opt`). + pub fn timestamp_nanos_opt(&self) -> Option { + self.utc.and_utc().timestamp_nanos_opt() + } + + /// Adds a signed duration, returning `None` on overflow. The offset is re-resolved for the + /// shifted instant (from `tz`), so this is DST-safe unlike shifting a naive/local time. + pub fn checked_add_signed(&self, rhs: TimeDelta) -> Option { + Some(DateTimeType { + utc: self.utc.checked_add_signed(rhs)?, + tz: self.tz, + }) + } + + /// Subtracts a signed duration, returning `None` on overflow. See `checked_add_signed`. + pub fn checked_sub_signed(&self, rhs: TimeDelta) -> Option { + Some(DateTimeType { + utc: self.utc.checked_sub_signed(rhs)?, + tz: self.tz, + }) + } + + /// The signed duration between two instants (`self - rhs`). + pub fn signed_duration_since(&self, rhs: Self) -> TimeDelta { + self.utc - rhs.utc + } + + /// Re-expresses the same instant in a different IANA timezone, keeping the compact + /// `DateTimeType` representation. Unlike `with_timezone` (which converts to a fixed-offset + /// `chrono::DateTime`), this stays a `DateTimeType` so the timezone remains a full IANA id + /// (with correct DST behavior for datetimes computed from the result). + pub fn with_iana_timezone(&self, tz: Tz) -> Self { + DateTimeType { utc: self.utc, tz } + } +} + +/// Mirrors `chrono::DateTime`'s own `Add`/`Sub` operator overloads (which also panic on +/// overflow, via `chrono::DateTime::add`/`sub`). +impl std::ops::Add for DateTimeType { + type Output = DateTimeType; + fn add(self, rhs: TimeDelta) -> DateTimeType { + self.checked_add_signed(rhs) + .expect("`DateTimeType + TimeDelta` overflowed") + } +} + +impl std::ops::Sub for DateTimeType { + type Output = DateTimeType; + fn sub(self, rhs: TimeDelta) -> DateTimeType { + self.checked_sub_signed(rhs) + .expect("`DateTimeType - TimeDelta` overflowed") + } +} + +impl std::ops::Sub for DateTimeType { + type Output = TimeDelta; + fn sub(self, rhs: DateTimeType) -> TimeDelta { + self.signed_duration_since(rhs) + } } /// Builds a compact `DateTimeType` from a fully resolved `chrono::DateTime`. From 9a8b9e24625331050ae9353eb623a9dbbef17cf1 Mon Sep 17 00:00:00 2001 From: Radu Racariu Date: Tue, 8 Sep 2026 16:37:19 +0300 Subject: [PATCH 2/9] perf: shrink Str/Symbol/Uri from 24 to 16 bytes via Box Applies the same private-field + Box + accessor treatment used previously for Ref/XStr to the remaining scalar value types that held a public String field: Str, Symbol, Uri. - Str/Symbol/Uri: value field changed from `pub value: String` to a private `value: Box`, with a new `value(&self) -> &str` accessor on all three (Str additionally keeps its pre-existing as_str()/Deref/AsRef). - Updated every direct field-access/struct-literal-construction call site across c_api, defs, json/brio/zinc/trio encode+decode, filter, dict.rs/dis_macro.rs, and tests to use the new accessor/constructors. - Fixed one doctest and one PartialEq impl pair (Str vs str/String) that needed adjusting for the new Box field type. Verified via a throwaway size-probe test: Str/Symbol/Uri 24 -> 16 bytes each; Value stays at 40 bytes (Dict/Ref/XStr remain the tied largest variants at 32B). cargo build --all-targets, cargo test --all-targets (816 tests), cargo test --doc (131), and cargo clippy --all-targets all clean. --- src/c_api/str.rs | 4 +-- src/c_api/symbol.rs | 4 +-- src/c_api/uri.rs | 4 +-- src/haystack/defs/containment_refs.rs | 4 +-- src/haystack/defs/misc.rs | 2 +- src/haystack/defs/namespace.rs | 22 ++++++++-------- src/haystack/encoding/brio/decode.rs | 10 +++----- src/haystack/encoding/brio/encode.rs | 10 +++----- src/haystack/encoding/json/decode.rs | 22 ++++++++-------- src/haystack/encoding/json/encode.rs | 6 ++--- src/haystack/encoding/trio/decode.rs | 5 +--- src/haystack/encoding/trio/encode.rs | 2 +- .../encoding/zinc/decode/complex/grid.rs | 2 +- .../encoding/zinc/decode/scalar/reference.rs | 2 +- .../encoding/zinc/decode/scalar/str.rs | 4 +-- .../encoding/zinc/decode/scalar/symbol.rs | 4 +-- .../encoding/zinc/decode/scalar/uri.rs | 4 +-- src/haystack/encoding/zinc/decode/value.rs | 2 +- src/haystack/encoding/zinc/encode.rs | 6 ++--- src/haystack/filter/nodes.rs | 6 ++--- src/haystack/val/dict.rs | 6 ++--- src/haystack/val/dis_macro.rs | 2 +- src/haystack/val/string.rs | 25 ++++++++++++------- src/haystack/val/symbol.rs | 15 ++++++++--- src/haystack/val/uri.rs | 15 ++++++++--- tests/defs/namespace.rs | 10 +++----- tests/values/test_string.rs | 4 +-- tests/values/test_symbol.rs | 2 +- tests/values/test_uri.rs | 4 +-- tests/values/test_value.rs | 8 +++--- 30 files changed, 110 insertions(+), 106 deletions(-) diff --git a/src/c_api/str.rs b/src/c_api/str.rs index 411cf0e..52a7889 100644 --- a/src/c_api/str.rs +++ b/src/c_api/str.rs @@ -35,7 +35,7 @@ use crate::haystack::val::Value; pub unsafe extern "C" fn haystack_value_get_str_len(val: *const Value) -> usize { match unsafe { val.as_ref() } { Some(value) => match value { - Value::Str(str) => return str.value.len(), + Value::Str(str) => return str.value().len(), _ => new_error("Not a Str Value"), }, None => new_error("Invalid Value reference"), @@ -69,7 +69,7 @@ pub unsafe extern "C" fn haystack_value_get_str_len(val: *const Value) -> usize pub unsafe extern "C" fn haystack_value_get_str_value(val: *const Value) -> *const c_char { match unsafe { val.as_ref() } { Some(value) => match value { - Value::Str(str) => match CString::new(str.value.as_bytes()) { + Value::Str(str) => match CString::new(str.value().as_bytes()) { Ok(str) => return str.into_raw(), Err(err) => update_last_error(err), }, diff --git a/src/c_api/symbol.rs b/src/c_api/symbol.rs index d3ddcee..873c0f1 100644 --- a/src/c_api/symbol.rs +++ b/src/c_api/symbol.rs @@ -35,7 +35,7 @@ use crate::haystack::val::Value; pub unsafe extern "C" fn haystack_value_get_symbol_value_len(val: *const Value) -> usize { match unsafe { val.as_ref() } { Some(value) => match value { - Value::Symbol(symbol) => return symbol.value.len(), + Value::Symbol(symbol) => return symbol.value().len(), _ => new_error("Not a Symbol Value"), }, None => new_error("Invalid Value reference"), @@ -68,7 +68,7 @@ pub unsafe extern "C" fn haystack_value_get_symbol_value_len(val: *const Value) pub unsafe extern "C" fn haystack_value_get_symbol_value(val: *const Value) -> *const c_char { match unsafe { val.as_ref() } { Some(value) => match value { - Value::Symbol(symbol) => match CString::new(symbol.value.as_bytes()) { + Value::Symbol(symbol) => match CString::new(symbol.value().as_bytes()) { Ok(str) => return str.into_raw(), Err(err) => update_last_error(err), }, diff --git a/src/c_api/uri.rs b/src/c_api/uri.rs index d0d2404..535972d 100644 --- a/src/c_api/uri.rs +++ b/src/c_api/uri.rs @@ -35,7 +35,7 @@ use crate::haystack::val::Value; pub unsafe extern "C" fn haystack_value_get_uri_value_len(val: *const Value) -> usize { match unsafe { val.as_ref() } { Some(value) => match value { - Value::Uri(uri) => return uri.value.len(), + Value::Uri(uri) => return uri.value().len(), _ => new_error("Not a Uri Value"), }, None => new_error("Invalid Value reference"), @@ -69,7 +69,7 @@ pub unsafe extern "C" fn haystack_value_get_uri_value_len(val: *const Value) -> pub unsafe extern "C" fn haystack_value_get_uri_value(val: *const Value) -> *const c_char { match unsafe { val.as_ref() } { Some(value) => match value { - Value::Uri(uri) => match CString::new(uri.value.as_bytes()) { + Value::Uri(uri) => match CString::new(uri.value().as_bytes()) { Ok(str) => return str.into_raw(), Err(err) => update_last_error(err), }, diff --git a/src/haystack/defs/containment_refs.rs b/src/haystack/defs/containment_refs.rs index 5e3346e..8f0d8d1 100644 --- a/src/haystack/defs/containment_refs.rs +++ b/src/haystack/defs/containment_refs.rs @@ -55,7 +55,7 @@ pub fn get_contained_by_refs_for_super_type( .filter(|def| options.deprecated || !def.has("deprecated")) .filter_map(|def| { let contained_by_sym = def.get_symbol("containedBy")?; - if namespace.fits(&Symbol::make(contained_by_sym.value.as_str()), &super_sym) { + if namespace.fits(&Symbol::make(contained_by_sym.value()), &super_sym) { Some(def.def_name().to_string()) } else { None @@ -129,7 +129,7 @@ pub fn add_containment_refs( let entity_type_name = namespace .def_of_dict(parent) .get_symbol("def") - .map(|s| s.value.clone()) + .map(|s| s.value().to_string()) .unwrap_or_default(); if entity_type_name.is_empty() { diff --git a/src/haystack/defs/misc.rs b/src/haystack/defs/misc.rs index 945a9d3..891d1a1 100644 --- a/src/haystack/defs/misc.rs +++ b/src/haystack/defs/misc.rs @@ -15,7 +15,7 @@ use crate::{ /// A list of dicts. /// pub(super) fn parse_multi_line_string_to_dicts(val: &Str) -> Vec { - val.value + val.value() .split('\n') .map(|line| line.trim()) .filter(|line| !line.is_empty() && !line.starts_with("//")) diff --git a/src/haystack/defs/namespace.rs b/src/haystack/defs/namespace.rs index 46954d2..02cc0b6 100644 --- a/src/haystack/defs/namespace.rs +++ b/src/haystack/defs/namespace.rs @@ -29,8 +29,8 @@ pub trait DefDict: HaystackDict { } /// Return the `def` [Symbol](crate::val::Symbol) name - fn def_name(&self) -> &String { - &self.def_symbol().value + fn def_name(&self) -> &str { + self.def_symbol().value() } } @@ -169,12 +169,12 @@ impl Namespace { /// True if the name is for a conjunct. pub fn is_conjunct(symbol: &Symbol) -> bool { - symbol.value.contains('-') + symbol.value().contains('-') } /// Decomposes a conjunct into its respective defs and returns them pub fn conjuncts_defs(&self, symbol: &Symbol) -> Vec<&Dict> { - self.all_matching_names(&symbol.value.split('-').collect::>()) + self.all_matching_names(&symbol.value().split('-').collect::>()) } /// Computes a list of feature defs. @@ -195,7 +195,7 @@ impl Namespace { /// True if the name is for a feature. pub fn is_feature(symbol: &Symbol) -> bool { - symbol.value.contains(':') + symbol.value().contains(':') } /// Computes a list of all the libs implemented by this namespace. @@ -314,7 +314,7 @@ impl Namespace { let mut features = HashSet::<&str>::new(); for sym in self.defs.keys() { if Namespace::is_feature(sym) - && let Some((first, _second)) = sym.value.split_once(':') + && let Some((first, _second)) = sym.value().split_once(':') { features.insert(first); } @@ -330,7 +330,7 @@ impl Namespace { for def in self.defs.values() { if let Some(tag_on) = def.get_list("tagOn") { let names = tag_on.iter().filter_map(|v| match v { - Value::Symbol(sym) => Some(sym.value.as_str()), + Value::Symbol(sym) => Some(sym.value()), _ => None, }); @@ -391,7 +391,7 @@ impl Namespace { if !association_def.has("computedFromReciprocal") { return self .get(parent) - .and_then(|def| def.get_list(&association.value)) + .and_then(|def| def.get_list(association.value())) .unwrap_or(&Vec::default()) .iter() .filter_map(|value| match value { @@ -423,7 +423,7 @@ impl Namespace { let mut matches = HashSet::<&Dict>::new(); for def in self.defs.values() { - if let Some(Value::List(list)) = def.get(&reciprocal_of.value) { + if let Some(Value::List(list)) = def.get(reciprocal_of.value()) { list.iter() .filter_map(|value| match value { Value::Symbol(sym) => self.get(sym), @@ -803,7 +803,7 @@ impl Namespace { let id = cur_subject.get_ref("id").cloned(); while let Some((subject_key, subject_val)) = cur_subject.pop_first() { let subject_def = self.get_by_name(&subject_key); - let mut rel_val = subject_def.and_then(|def| def.get(&rel_name.value)); + let mut rel_val = subject_def.and_then(|def| def.get(rel_name.value())); // Handle a reciprocal relationship. A reciprocal relationship can only // be inverted when a ref is specified. @@ -812,7 +812,7 @@ impl Namespace { && subject_val.is_ref() && let Some(reciprocal_of) = reciprocal_of { - rel_val = subject_def.and_then(|def| def.get(&reciprocal_of.value)); + rel_val = subject_def.and_then(|def| def.get(reciprocal_of.value())); if rel_val.is_some() && let Value::Ref(ref val) = subject_val diff --git a/src/haystack/encoding/brio/decode.rs b/src/haystack/encoding/brio/decode.rs index ca1f5a4..1ebb520 100644 --- a/src/haystack/encoding/brio/decode.rs +++ b/src/haystack/encoding/brio/decode.rs @@ -450,9 +450,7 @@ impl FromBrio for Value { let dis = decode_str_chars(reader)?; Ok(Value::from(Ref::make(&id, non_empty(dis.as_str())))) } - CTRL_URI => Ok(Value::from(Uri { - value: decode_str(reader)?, - })), + CTRL_URI => Ok(Value::from(Uri::from(decode_str(reader)?))), CTRL_DATE => { let year = read_i16(reader)? as i32; let month = read_u8(reader)? as u32; @@ -668,9 +666,7 @@ mod tests { #[test] fn test_uri() { - let v = Value::from(Uri { - value: "https://project-haystack.org".into(), - }); + let v = Value::from(Uri::from("https://project-haystack.org")); assert_eq!(round_trip(&v), v); } @@ -1161,7 +1157,7 @@ mod tests { "n" => Value::from(Number::make(123.0)), "s" => Value::from("hi"), "r" => Value::from(Ref::make("1deb31b8-7508b187", None)), - "u" => Value::from(Uri { value: "a/b".to_string() }), + "u" => Value::from(Uri::from("a/b")), "d" => Value::from(Date::from_ymd(2021, 6, 15).unwrap()), "dt" => Value::from(DateTime::parse_from_rfc3339("2021-06-15T12:00:00Z").unwrap()) }); diff --git a/src/haystack/encoding/brio/encode.rs b/src/haystack/encoding/brio/encode.rs index f8807f0..d7341d5 100644 --- a/src/haystack/encoding/brio/encode.rs +++ b/src/haystack/encoding/brio/encode.rs @@ -265,7 +265,7 @@ impl ToBrio for Bool { impl ToBrio for Str { fn to_brio(&self, writer: &mut W) -> Result<()> { writer.write_all(&[CTRL_STR])?; - encode_str(writer, &self.value)?; + encode_str(writer, self.value())?; Ok(()) } } @@ -273,7 +273,7 @@ impl ToBrio for Str { impl ToBrio for Uri { fn to_brio(&self, writer: &mut W) -> Result<()> { writer.write_all(&[CTRL_URI])?; - encode_str(writer, &self.value)?; + encode_str(writer, self.value())?; Ok(()) } } @@ -281,7 +281,7 @@ impl ToBrio for Uri { impl ToBrio for Symbol { fn to_brio(&self, writer: &mut W) -> Result<()> { writer.write_all(&[CTRL_SYMBOL])?; - encode_str(writer, &self.value)?; + encode_str(writer, self.value())?; Ok(()) } } @@ -679,9 +679,7 @@ mod tests { #[test] fn test_uri() { - let v = Value::from(Uri { - value: "http://example.com".into(), - }); + let v = Value::from(Uri::from("http://example.com")); let bytes = enc(&v); assert_eq!(bytes[0], CTRL_URI); } diff --git a/src/haystack/encoding/json/decode.rs b/src/haystack/encoding/json/decode.rs index bd59b0f..e3fa5f9 100644 --- a/src/haystack/encoding/json/decode.rs +++ b/src/haystack/encoding/json/decode.rs @@ -176,7 +176,7 @@ impl<'de> Visitor<'de> for JsonValueDecoderVisitor { if key == "_kind" { match value { HVal::Str(str_kind) => { - kind = str_kind.value; + kind = str_kind.value().to_string(); match kind.as_str() { "marker" => return Ok(HVal::make_marker()), "remove" => return Ok(HVal::make_remove()), @@ -285,8 +285,8 @@ fn parse_number(dict: &Dict) -> Result { fn parse_ref(dict: &Dict) -> Result { match dict.get_str("val") { Some(val) => { - let dis = dict.get_str("dis").map(|d| d.value.as_str()); - Ok(Ref::make(val.value.as_str(), dis).into()) + let dis = dict.get_str("dis").map(|d| d.value()); + Ok(Ref::make(val.value(), dis).into()) } None => Err(JsonErr::custom("Missing or invalid 'val'")), } @@ -294,14 +294,14 @@ fn parse_ref(dict: &Dict) -> Result { fn parse_symbol(dict: &Dict) -> Result { match dict.get_str("val") { - Some(val) => Ok(HVal::make_symbol(&val.value)), + Some(val) => Ok(HVal::make_symbol(val.value())), None => Err(JsonErr::custom("Missing or invalid 'val'")), } } fn parse_uri(dict: &Dict) -> Result { match dict.get_str("val") { - Some(val) => Ok(HVal::make_uri(&val.value)), + Some(val) => Ok(HVal::make_uri(val.value())), None => Err(JsonErr::custom("Missing or invalid 'val'")), } } @@ -328,11 +328,11 @@ fn parse_time(dict: &Dict) -> Result { fn parse_datetime(dict: &Dict) -> Result { match dict.get_str("val") { - Some(val) => match DateTime::parse_from_rfc3339(&val.value) { + Some(val) => match DateTime::parse_from_rfc3339(val.value()) { Ok(date) => match dict.get_str("tz") { Some(tz) => { let datetime = - make_date_time_with_tz(&date.with_timezone(&Utc.fix()), &tz.value); + make_date_time_with_tz(&date.with_timezone(&Utc.fix()), tz.value()); match datetime { Ok(datetime) => Ok(HVal::DateTime(datetime.into())), Err(err) => Err(JsonErr::custom(err)), @@ -359,7 +359,7 @@ fn parse_coord(dict: &Dict) -> Result { fn parse_xstr(dict: &Dict) -> Result { match dict.get_str("type") { Some(r#type) => match dict.get_str("val") { - Some(val) => Ok(HVal::make_xstr_from(&r#type.value, &val.value)), + Some(val) => Ok(HVal::make_xstr_from(r#type.value(), val.value())), None => Err(JsonErr::custom("Missing or invalid 'val'")), }, None => Err(JsonErr::custom("Missing or invalid 'type'")), @@ -391,7 +391,7 @@ fn parse_grid_meta_and_ver(dict: &Dict) -> (Option, String) { if let Some(ref mut meta_dict) = meta { if let Some(ver) = meta_dict.get_str(VER) { - grid_ver = ver.value.to_owned(); + grid_ver = ver.value().to_owned(); meta_dict.remove(VER); } @@ -410,12 +410,12 @@ fn parse_grid_columns(cols: &List) -> Result, JsonErr> { HVal::Dict(dict) => match dict.get_str("name") { Some(name) => match dict.get("meta") { Some(HVal::Dict(meta)) => Ok(Column { - name: name.value.clone(), + name: name.value().to_string(), meta: Some(meta.clone()), }), Some(_) => Err(JsonErr::custom("Invalid 'meta'")), None => Ok(Column { - name: name.value.clone(), + name: name.value().to_string(), meta: None, }), }, diff --git a/src/haystack/encoding/json/encode.rs b/src/haystack/encoding/json/encode.rs index a22fda0..e052f19 100644 --- a/src/haystack/encoding/json/encode.rs +++ b/src/haystack/encoding/json/encode.rs @@ -70,7 +70,7 @@ impl Serialize for Uri { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(Some(2))?; map.serialize_entry("_kind", "uri")?; - map.serialize_entry("val", &self.value)?; + map.serialize_entry("val", self.value())?; map.end() } } @@ -79,7 +79,7 @@ impl Serialize for Symbol { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(Some(2))?; map.serialize_entry("_kind", "symbol")?; - map.serialize_entry("val", &self.value)?; + map.serialize_entry("val", self.value())?; map.end() } } @@ -208,7 +208,7 @@ impl Serialize for HVal { HVal::Number(val) => Number::serialize(val, serializer), - HVal::Str(val) => serializer.serialize_str(val.value.as_str()), + HVal::Str(val) => serializer.serialize_str(val.value()), HVal::Ref(val) => Ref::serialize(val, serializer), diff --git a/src/haystack/encoding/trio/decode.rs b/src/haystack/encoding/trio/decode.rs index 7087f44..d2eb410 100644 --- a/src/haystack/encoding/trio/decode.rs +++ b/src/haystack/encoding/trio/decode.rs @@ -819,10 +819,7 @@ mod tests { assert_eq!(dicts[1].get("type"), Some(&Value::make_str("dict"))); let inner = dicts[1].get_dict("val").expect("inner dict"); assert!(inner.has("foo")); - assert_eq!( - inner.get_str("dis").map(|s| s.value.as_str()), - Some("Dict!") - ); + assert_eq!(inner.get_str("dis").map(|s| s.value()), Some("Dict!")); assert_eq!(dicts[2].get("type"), Some(&Value::make_str("grid"))); let grid = dicts[2].get_grid("val").expect("grid"); diff --git a/src/haystack/encoding/trio/encode.rs b/src/haystack/encoding/trio/encode.rs index b4fc6bb..7c3da0b 100644 --- a/src/haystack/encoding/trio/encode.rs +++ b/src/haystack/encoding/trio/encode.rs @@ -252,7 +252,7 @@ fn encode_tag(name: &str, value: &Value, multiline_strings: bool) -> String { // String with multiline option: indented multi-line format. Value::Str(s) if multiline_strings => { - let indented = s.value.replace('\n', NL_INDENT); + let indented = s.value().replace('\n', NL_INDENT); format!("{}: \n{}{}", name, INDENT, indented) } diff --git a/src/haystack/encoding/zinc/decode/complex/grid.rs b/src/haystack/encoding/zinc/decode/complex/grid.rs index 30f5627..bc00049 100644 --- a/src/haystack/encoding/zinc/decode/complex/grid.rs +++ b/src/haystack/encoding/zinc/decode/complex/grid.rs @@ -123,7 +123,7 @@ fn parse_grid_ver(parser: &mut ParserType) -> Result let ver = parser.lexer.expect_value()?; match ver { - Value::Str(str) => Ok(str.value), + Value::Str(str) => Ok(String::from(str)), _ => parser .lexer .make_generic_err(&format!("Expecting 'ver' to be a Str, got '{ver:?}'.")), diff --git a/src/haystack/encoding/zinc/decode/scalar/reference.rs b/src/haystack/encoding/zinc/decode/scalar/reference.rs index cab1f46..1abaf24 100644 --- a/src/haystack/encoding/zinc/decode/scalar/reference.rs +++ b/src/haystack/encoding/zinc/decode/scalar/reference.rs @@ -26,7 +26,7 @@ pub(crate) fn parse_ref(scanner: &mut Scanner) -> Result let mut dis: Option = None; if !scanner.is_eof && scanner.cur == b' ' && scanner.peek()? == b'"' { scanner.read()?; - dis = Some(parse_str(scanner)?.value); + dis = Some(parse_str(scanner)?.into()); } Ok(Ref::make( diff --git a/src/haystack/encoding/zinc/decode/scalar/str.rs b/src/haystack/encoding/zinc/decode/scalar/str.rs index 56fbb16..5e392bf 100644 --- a/src/haystack/encoding/zinc/decode/scalar/str.rs +++ b/src/haystack/encoding/zinc/decode/scalar/str.rs @@ -33,9 +33,7 @@ pub(crate) fn parse_str(scanner: &mut Scanner) -> Result scanner.advance()?; - Ok(Str { - value: String::from_utf8_lossy(&str).to_string(), - }) + Ok(Str::from(String::from_utf8_lossy(&str).into_owned())) } // Parse a Str escape sequence diff --git a/src/haystack/encoding/zinc/decode/scalar/symbol.rs b/src/haystack/encoding/zinc/decode/scalar/symbol.rs index 9abe84d..d872f7d 100644 --- a/src/haystack/encoding/zinc/decode/scalar/symbol.rs +++ b/src/haystack/encoding/zinc/decode/scalar/symbol.rs @@ -26,9 +26,7 @@ pub(crate) fn parse_symbol(scanner: &mut Scanner) -> Result(scanner: &mut Scanner) -> Result scanner.advance()?; - Ok(Uri { - value: String::from_utf8_lossy(&str).to_string(), - }) + Ok(Uri::from(String::from_utf8_lossy(&str).into_owned())) } #[cfg(test)] diff --git a/src/haystack/encoding/zinc/decode/value.rs b/src/haystack/encoding/zinc/decode/value.rs index b54f2c1..a401ce9 100644 --- a/src/haystack/encoding/zinc/decode/value.rs +++ b/src/haystack/encoding/zinc/decode/value.rs @@ -15,7 +15,7 @@ use std::io::{Cursor, Error}; /// let val = from_str("`/a/sample/uri`").expect("A Value"); /// assert!(&val.is_uri()); /// -/// assert_eq!(Uri::try_from(&val).unwrap().value, "/a/sample/uri"); +/// assert_eq!(Uri::try_from(&val).unwrap().value(), "/a/sample/uri"); /// ``` pub fn from_str(str: &str) -> Result { let mut input = Cursor::new(str.as_bytes()); diff --git a/src/haystack/encoding/zinc/encode.rs b/src/haystack/encoding/zinc/encode.rs index b7e5525..fa6f461 100644 --- a/src/haystack/encoding/zinc/encode.rs +++ b/src/haystack/encoding/zinc/encode.rs @@ -189,7 +189,7 @@ impl ToZinc for Str { fn to_zinc(&self, writer: &mut W) -> Result<()> { writer.write_all(b"\"")?; let mut buf = [0; 4]; - for c in self.value.chars() { + for c in self.value().chars() { if c < ' ' || c == '"' || c == '\\' { match c { '"' => writer.write_all(br#"\""#)?, @@ -224,7 +224,7 @@ impl ToZinc for Ref { impl ToZinc for Symbol { fn to_zinc(&self, writer: &mut W) -> Result<()> { - writer.write_fmt(format_args!("^{}", self.value))?; + writer.write_fmt(format_args!("^{}", self.value()))?; Ok(()) } } @@ -232,7 +232,7 @@ impl ToZinc for Symbol { impl ToZinc for Uri { fn to_zinc(&self, writer: &mut W) -> Result<()> { writer.write_all(b"`")?; - for c in self.value.chars() { + for c in self.value().chars() { if c < ' ' { continue; } diff --git a/src/haystack/filter/nodes.rs b/src/haystack/filter/nodes.rs index 3cd66f2..6f06b2d 100644 --- a/src/haystack/filter/nodes.rs +++ b/src/haystack/filter/nodes.rs @@ -315,7 +315,7 @@ impl Eval for IsA { impl Display for IsA { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.write_str("^")?; - f.write_str(&self.symbol.value) + f.write_str(self.symbol.value()) } } @@ -392,7 +392,7 @@ impl Eval for Relation { context.ns.has_relationship( context.dict, - &Symbol::from(self.rel.value.as_str()), + &Symbol::from(self.rel.value()), &self.rel_term, &self.ref_value, &resolve, @@ -402,7 +402,7 @@ impl Eval for Relation { impl Display for Relation { fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "{}?", self.rel.value)?; + write!(f, "{}?", self.rel.value())?; if let Some(rel_term) = &self.rel_term { write!(f, " {rel_term}")?; diff --git a/src/haystack/val/dict.rs b/src/haystack/val/dict.rs index 2b0505a..162e4cc 100644 --- a/src/haystack/val/dict.rs +++ b/src/haystack/val/dict.rs @@ -653,7 +653,7 @@ where if let Some(val) = dict.get("disMacro") { return if let Value::Str(val) = val { dis_macro( - &val.value, + val.value(), |val| dict.get(val).map(Cow::Borrowed), get_localized, ) @@ -664,7 +664,7 @@ where if let Some(val) = dict.get("disKey") { if let Value::Str(val_str) = val - && let Some(val_str) = get_localized(&val_str.value) + && let Some(val_str) = get_localized(val_str.value()) { return val_str; } @@ -700,7 +700,7 @@ where fn decode_str_from_value(val: &'_ Value) -> Cow<'_, str> { match val { - Value::Str(val) => Cow::Borrowed(&val.value), + Value::Str(val) => Cow::Borrowed(val.value()), _ => Cow::Owned(val.to_string()), } } diff --git a/src/haystack/val/dis_macro.rs b/src/haystack/val/dis_macro.rs index ec202da..fe75825 100644 --- a/src/haystack/val/dis_macro.rs +++ b/src/haystack/val/dis_macro.rs @@ -32,7 +32,7 @@ where if let Value::Ref(val) = value.as_ref() { dst.push_str(val.dis().unwrap_or(val.value())); } else if let Value::Str(val) = value.as_ref() { - dst.push_str(&val.value); + dst.push_str(val.value()); } else { dst.push_str(&value.to_string()); } diff --git a/src/haystack/val/string.rs b/src/haystack/val/string.rs index adc3350..d33331f 100644 --- a/src/haystack/val/string.rs +++ b/src/haystack/val/string.rs @@ -29,7 +29,7 @@ use crate::haystack::val::{ConversionError, Value}; /// ``` #[derive(PartialEq, PartialOrd, Eq, Ord, Hash, Clone, Debug, Default)] pub struct Str { - pub value: String, + value: Box, } impl Str { @@ -38,16 +38,23 @@ impl Str { Str { value: val.into() } } - /// Get a `&str` slice of the underlying `String` payload + /// Get a `&str` slice of the underlying payload pub fn as_str(&self) -> &str { - self.value.as_str() + &self.value + } + + /// Get a `&str` slice of the underlying payload + pub fn value(&self) -> &str { + &self.value } } // Make a Haystack `Str` from a `String` impl From for Str { fn from(value: String) -> Self { - Str { value } + Str { + value: value.into(), + } } } @@ -55,7 +62,7 @@ impl From for Str { impl From<&str> for Str { fn from(value: &str) -> Self { Str { - value: value.to_owned(), + value: value.into(), } } } @@ -79,7 +86,7 @@ impl TryFrom<&Value> for String { type Error = ConversionError; fn try_from(value: &Value) -> Result { match value { - Value::Str(v) => Ok(v.value.clone()), + Value::Str(v) => Ok(v.value.to_string()), _ => Err("Value is not an `Str`"), } } @@ -121,7 +128,7 @@ impl AsRef for Str { /// Extracts the owned `String` from a `Str` impl From for String { fn from(s: Str) -> String { - s.value + s.value.into() } } @@ -135,13 +142,13 @@ impl From for Value { /// Allows comparing `Str` with `str` directly: `some_str == "foo"` impl PartialEq for Str { fn eq(&self, other: &str) -> bool { - self.value == other + self.value.as_ref() == other } } /// Allows comparing `Str` with `String` directly: `some_str == owned` impl PartialEq for Str { fn eq(&self, other: &String) -> bool { - self.value == *other + self.value.as_ref() == other.as_str() } } diff --git a/src/haystack/val/symbol.rs b/src/haystack/val/symbol.rs index f14180b..cbe6f98 100644 --- a/src/haystack/val/symbol.rs +++ b/src/haystack/val/symbol.rs @@ -22,7 +22,7 @@ use crate::haystack::val::{ConversionError, Value}; /// ``` #[derive(PartialEq, PartialOrd, Eq, Ord, Hash, Clone, Debug, Default)] pub struct Symbol { - pub value: String, + value: Box, } impl Symbol { @@ -30,19 +30,28 @@ impl Symbol { pub fn make(val: &str) -> Self { Symbol { value: val.into() } } + + /// Get a `&str` slice of the underlying payload + pub fn value(&self) -> &str { + &self.value + } } // Make a Haystack `Symbol` from a string value impl From<&str> for Symbol { fn from(value: &str) -> Self { - Symbol::from(value.to_owned()) + Symbol { + value: value.into(), + } } } // Make a Haystack `Symbol` from a String value impl From for Symbol { fn from(value: String) -> Self { - Symbol { value } + Symbol { + value: value.into(), + } } } diff --git a/src/haystack/val/uri.rs b/src/haystack/val/uri.rs index 1548215..c7563c2 100644 --- a/src/haystack/val/uri.rs +++ b/src/haystack/val/uri.rs @@ -20,7 +20,7 @@ use crate::haystack::val::{ConversionError, Value}; /// ``` #[derive(PartialEq, PartialOrd, Eq, Ord, Hash, Clone, Debug, Default)] pub struct Uri { - pub value: String, + value: Box, } impl Uri { @@ -28,19 +28,28 @@ impl Uri { pub fn make(val: &str) -> Self { Uri { value: val.into() } } + + /// Get a `&str` slice of the underlying payload + pub fn value(&self) -> &str { + &self.value + } } // Make a Haystack `Uri` from a String value impl From for Uri { fn from(value: String) -> Self { - Uri { value } + Uri { + value: value.into(), + } } } // Make a Haystack `Uri` from a string value impl From<&str> for Uri { fn from(value: &str) -> Self { - Uri::from(value.to_owned()) + Uri { + value: value.into(), + } } } diff --git a/tests/defs/namespace.rs b/tests/defs/namespace.rs index bd8e926..c3b5ee7 100644 --- a/tests/defs/namespace.rs +++ b/tests/defs/namespace.rs @@ -288,11 +288,7 @@ fn test_namespace_choices_for() { fn test_namespace_choices() { let ns = Namespace::make(parse_def()); - let mut choices = ns - .choices - .keys() - .map(|k| k.value.as_str()) - .collect::>(); + let mut choices = ns.choices.keys().map(|k| k.value()).collect::>(); choices.sort(); @@ -417,7 +413,7 @@ fn test_namespace_tag_on_defs() { let mut tag_on_defs = ns .tag_on_defs .keys() - .map(|k| k.value.as_str()) + .map(|k| k.value()) .collect::>(); tag_on_defs.sort(); @@ -731,7 +727,7 @@ fn test_namespace_reflect() { let names = reflect .defs .iter() - .map(|def| def.def_name().as_str()) + .map(|def| def.def_name()) .collect::>(); assert!( diff --git a/tests/values/test_string.rs b/tests/values/test_string.rs index 9860ca1..d8fa35c 100644 --- a/tests/values/test_string.rs +++ b/tests/values/test_string.rs @@ -20,13 +20,13 @@ fn test_str_make_value() { #[test] fn test_ref_from() { let str = Str::from("id"); - assert_eq!(str.value, "id".to_string()); + assert_eq!(str.value(), "id".to_string()); } #[test] fn test_ref_from_string() { let str = Str::from("id".to_string()); - assert_eq!(str.value, "id".to_string()); + assert_eq!(str.value(), "id".to_string()); } #[test] diff --git a/tests/values/test_symbol.rs b/tests/values/test_symbol.rs index 7b356f4..69f8af4 100644 --- a/tests/values/test_symbol.rs +++ b/tests/values/test_symbol.rs @@ -20,5 +20,5 @@ fn test_symbol_make_value() { #[test] fn test_symbol_from() { let sym = Symbol::from("some-sym"); - assert_eq!(sym.value, "some-sym".to_string()); + assert_eq!(sym.value(), "some-sym".to_string()); } diff --git a/tests/values/test_uri.rs b/tests/values/test_uri.rs index bd1b76a..b549786 100644 --- a/tests/values/test_uri.rs +++ b/tests/values/test_uri.rs @@ -20,11 +20,11 @@ fn test_uri_make_value() { #[test] fn test_uri_from() { let uri = Uri::from("/foo/baz/bar.txt"); - assert_eq!(uri.value, "/foo/baz/bar.txt".to_string()); + assert_eq!(uri.value(), "/foo/baz/bar.txt".to_string()); } #[test] fn test_uri_from_string() { let uri = Uri::from("/foo/baz/bar.txt".to_string()); - assert_eq!(uri.value, "/foo/baz/bar.txt".to_string()); + assert_eq!(uri.value(), "/foo/baz/bar.txt".to_string()); } diff --git a/tests/values/test_value.rs b/tests/values/test_value.rs index 414fa0a..c4721dd 100644 --- a/tests/values/test_value.rs +++ b/tests/values/test_value.rs @@ -125,9 +125,7 @@ fn test_value_num() { #[test] fn test_value_str() { - let value = Value::Str(Str { - value: String::from("Foo"), - }); + let value = Value::Str(Str::from("Foo")); assert!(value.is_str()); assert_eq!(Value::from("Foo"), value); @@ -155,7 +153,7 @@ fn test_value_symbol() { let value = Value::make_symbol("foo"); assert!(value.is_symbol()); assert_eq!(&Symbol::try_from(&value).unwrap(), &Symbol::from("foo")); - assert_eq!(Symbol::try_from(&value).unwrap().value, "foo"); + assert_eq!(Symbol::try_from(&value).unwrap().value(), "foo"); assert_eq!( Value::from(Symbol::from("symbol")), Value::make_symbol("symbol") @@ -173,7 +171,7 @@ fn test_value_uri() { &Uri::try_from(&value).unwrap(), &Uri::from("http://zoo.bar") ); - assert_eq!(Uri::try_from(&value).unwrap().value, "http://zoo.bar"); + assert_eq!(Uri::try_from(&value).unwrap().value(), "http://zoo.bar"); assert_eq!(Value::from(Uri::from("uri")), Value::make_uri("uri")); let value = Value::Uri(Uri::make("/a/b/c")); From 011a82e5acf0691b149b8f0031434d0d880617e3 Mon Sep 17 00:00:00 2001 From: Radu Racariu Date: Tue, 8 Sep 2026 18:07:50 +0300 Subject: [PATCH 3/9] perf: add optimized inherent Str::to_string(), bypassing Display Str's Display impl goes through the generic write!/format_args! machinery. Since Str just wraps a Box, add an inherent to_string() that derefs to &str first and delegates to str's own specialized ToString impl (a direct byte copy via String::from), which the blanket ToString impl for Box does not get (that specialization only applies to str/String/char/Cow, not Box itself). The inherent method intentionally shadows ToString::to_string (same output, verified by a new test), so clippy::inherent_to_string_shadow_display is allowed with a comment. Also routes the existing TryFrom<&Value> for String impl through it for consistency. --- src/haystack/val/string.rs | 10 +++++++++- tests/values/test_string.rs | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/haystack/val/string.rs b/src/haystack/val/string.rs index d33331f..3bc6706 100644 --- a/src/haystack/val/string.rs +++ b/src/haystack/val/string.rs @@ -47,6 +47,14 @@ impl Str { pub fn value(&self) -> &str { &self.value } + + /// Converts to an owned `String` via a direct byte copy, bypassing the + /// `Display`/formatter machinery used by the blanket `ToString` impl. + /// Shadows `ToString::to_string` (same output, just faster). + #[allow(clippy::inherent_to_string_shadow_display)] + pub fn to_string(&self) -> String { + self.value.as_ref().to_string() + } } // Make a Haystack `Str` from a `String` @@ -86,7 +94,7 @@ impl TryFrom<&Value> for String { type Error = ConversionError; fn try_from(value: &Value) -> Result { match value { - Value::Str(v) => Ok(v.value.to_string()), + Value::Str(v) => Ok(v.to_string()), _ => Err("Value is not an `Str`"), } } diff --git a/tests/values/test_string.rs b/tests/values/test_string.rs index d8fa35c..a1b15d6 100644 --- a/tests/values/test_string.rs +++ b/tests/values/test_string.rs @@ -41,11 +41,20 @@ fn test_str_display() { assert_eq!(format!("{}", str), "foo"); } +#[test] +fn test_str_to_string_matches_display() { + let str = Str::make("foo"); + assert_eq!(str.to_string(), format!("{}", str)); + assert_eq!(str.to_string(), "foo"); +} + #[test] fn test_str_deref() { let str = Str::make("hello world"); // &Str coerces to &str; all str methods are available + assert!(!str.is_empty()); assert_eq!(str.len(), 11); + assert!(str.is_ascii()); assert!(str.contains("world")); assert!(str.starts_with("hello")); } From 563ec60aa60d5f965b2bf02e2e6725c81541b781 Mon Sep 17 00:00:00 2001 From: Radu Racariu Date: Tue, 8 Sep 2026 18:26:11 +0300 Subject: [PATCH 4/9] refactor: simplify string handling in C API and encoding modules --- src/c_api/str.rs | 4 ++-- src/haystack/defs/containment_refs.rs | 6 +++--- src/haystack/encoding/json/decode.rs | 8 ++++---- src/haystack/encoding/trio/encode.rs | 2 +- src/haystack/encoding/zinc/encode.rs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/c_api/str.rs b/src/c_api/str.rs index 52a7889..3c7aad0 100644 --- a/src/c_api/str.rs +++ b/src/c_api/str.rs @@ -35,7 +35,7 @@ use crate::haystack::val::Value; pub unsafe extern "C" fn haystack_value_get_str_len(val: *const Value) -> usize { match unsafe { val.as_ref() } { Some(value) => match value { - Value::Str(str) => return str.value().len(), + Value::Str(str) => return str.len(), _ => new_error("Not a Str Value"), }, None => new_error("Invalid Value reference"), @@ -69,7 +69,7 @@ pub unsafe extern "C" fn haystack_value_get_str_len(val: *const Value) -> usize pub unsafe extern "C" fn haystack_value_get_str_value(val: *const Value) -> *const c_char { match unsafe { val.as_ref() } { Some(value) => match value { - Value::Str(str) => match CString::new(str.value().as_bytes()) { + Value::Str(str) => match CString::new(str.as_bytes()) { Ok(str) => return str.into_raw(), Err(err) => update_last_error(err), }, diff --git a/src/haystack/defs/containment_refs.rs b/src/haystack/defs/containment_refs.rs index 8f0d8d1..95fd4e3 100644 --- a/src/haystack/defs/containment_refs.rs +++ b/src/haystack/defs/containment_refs.rs @@ -129,15 +129,15 @@ pub fn add_containment_refs( let entity_type_name = namespace .def_of_dict(parent) .get_symbol("def") - .map(|s| s.value().to_string()) + .map(|s| s.value()) .unwrap_or_default(); if entity_type_name.is_empty() { return String::new(); } - let ref_name = find_containment_ref_for_type(namespace, &entity_type_name) - .map(|def| def.def_name().to_string()) + let ref_name = find_containment_ref_for_type(namespace, entity_type_name) + .map(|def| def.def_name().to_owned()) .unwrap_or_default(); if !ref_name.is_empty() diff --git a/src/haystack/encoding/json/decode.rs b/src/haystack/encoding/json/decode.rs index e3fa5f9..2f7247a 100644 --- a/src/haystack/encoding/json/decode.rs +++ b/src/haystack/encoding/json/decode.rs @@ -176,7 +176,7 @@ impl<'de> Visitor<'de> for JsonValueDecoderVisitor { if key == "_kind" { match value { HVal::Str(str_kind) => { - kind = str_kind.value().to_string(); + kind = str_kind.to_string(); match kind.as_str() { "marker" => return Ok(HVal::make_marker()), "remove" => return Ok(HVal::make_remove()), @@ -391,7 +391,7 @@ fn parse_grid_meta_and_ver(dict: &Dict) -> (Option, String) { if let Some(ref mut meta_dict) = meta { if let Some(ver) = meta_dict.get_str(VER) { - grid_ver = ver.value().to_owned(); + grid_ver = ver.to_string(); meta_dict.remove(VER); } @@ -410,12 +410,12 @@ fn parse_grid_columns(cols: &List) -> Result, JsonErr> { HVal::Dict(dict) => match dict.get_str("name") { Some(name) => match dict.get("meta") { Some(HVal::Dict(meta)) => Ok(Column { - name: name.value().to_string(), + name: name.to_string(), meta: Some(meta.clone()), }), Some(_) => Err(JsonErr::custom("Invalid 'meta'")), None => Ok(Column { - name: name.value().to_string(), + name: name.to_string(), meta: None, }), }, diff --git a/src/haystack/encoding/trio/encode.rs b/src/haystack/encoding/trio/encode.rs index 7c3da0b..383933e 100644 --- a/src/haystack/encoding/trio/encode.rs +++ b/src/haystack/encoding/trio/encode.rs @@ -252,7 +252,7 @@ fn encode_tag(name: &str, value: &Value, multiline_strings: bool) -> String { // String with multiline option: indented multi-line format. Value::Str(s) if multiline_strings => { - let indented = s.value().replace('\n', NL_INDENT); + let indented = s.replace('\n', NL_INDENT); format!("{}: \n{}{}", name, INDENT, indented) } diff --git a/src/haystack/encoding/zinc/encode.rs b/src/haystack/encoding/zinc/encode.rs index fa6f461..e905f25 100644 --- a/src/haystack/encoding/zinc/encode.rs +++ b/src/haystack/encoding/zinc/encode.rs @@ -189,7 +189,7 @@ impl ToZinc for Str { fn to_zinc(&self, writer: &mut W) -> Result<()> { writer.write_all(b"\"")?; let mut buf = [0; 4]; - for c in self.value().chars() { + for c in self.chars() { if c < ' ' || c == '"' || c == '\\' { match c { '"' => writer.write_all(br#"\""#)?, From 6f6e91540c94be7468f31cab998a4e13e7d6d5e0 Mon Sep 17 00:00:00 2001 From: Radu Racariu Date: Wed, 9 Sep 2026 11:34:15 +0300 Subject: [PATCH 5/9] feat: add set_dis method to Ref for updating display name --- src/haystack/val/reference.rs | 12 ++++++++++++ tests/values/test_reference.rs | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/src/haystack/val/reference.rs b/src/haystack/val/reference.rs index 9d2b69f..d56ff35 100644 --- a/src/haystack/val/reference.rs +++ b/src/haystack/val/reference.rs @@ -58,6 +58,11 @@ impl Ref { pub fn dis(&self) -> Option<&str> { self.dis.as_deref() } + + /// Set the optional display name in place + pub fn set_dis(&mut self, dis: Option) { + self.dis = dis.map(|s| s.into()); + } } /// Implement equality operator for Ref @@ -115,6 +120,13 @@ impl From for Value { } } +/// Extracts the owned id payload `String` from a `Ref`, discarding the display name +impl From for String { + fn from(value: Ref) -> String { + value.value.into() + } +} + /// Tries to convert from `Value` to a `Ref` impl TryFrom<&Value> for Ref { type Error = ConversionError; diff --git a/tests/values/test_reference.rs b/tests/values/test_reference.rs index 7964b13..dd25d1b 100644 --- a/tests/values/test_reference.rs +++ b/tests/values/test_reference.rs @@ -40,6 +40,14 @@ fn test_ref_make_with_dis_none() { assert_eq!(id.dis(), None); } +#[test] +fn test_ref_with_dis() { + let mut id = Ref::make("id", Some("dis")); + id.set_dis(Some("new_dis".to_owned())); + assert_eq!(id.value(), "id"); + assert_eq!(id.dis(), Some("new_dis")); +} + #[test] fn test_ref_from_string() { let id = Ref::from("id".to_string()); From cc864abe27dead85d5d52cdd72483ed16eb9d57b Mon Sep 17 00:00:00 2001 From: Radu Racariu Date: Wed, 9 Sep 2026 18:55:41 +0300 Subject: [PATCH 6/9] feat: add Str::into_inner() to expose the underlying Box Lets consumers take ownership of the internal Box directly without going through a String round-trip. --- src/haystack/val/string.rs | 5 +++++ tests/values/test_string.rs | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/src/haystack/val/string.rs b/src/haystack/val/string.rs index 3bc6706..4931e72 100644 --- a/src/haystack/val/string.rs +++ b/src/haystack/val/string.rs @@ -55,6 +55,11 @@ impl Str { pub fn to_string(&self) -> String { self.value.as_ref().to_string() } + + /// Consumes the `Str`, returning the inner `Box` with no reallocation + pub fn into_inner(self) -> Box { + self.value + } } // Make a Haystack `Str` from a `String` diff --git a/tests/values/test_string.rs b/tests/values/test_string.rs index a1b15d6..4810bce 100644 --- a/tests/values/test_string.rs +++ b/tests/values/test_string.rs @@ -48,6 +48,13 @@ fn test_str_to_string_matches_display() { assert_eq!(str.to_string(), "foo"); } +#[test] +fn test_str_into_inner() { + let str = Str::make("foo"); + let inner: Box = str.into_inner(); + assert_eq!(&*inner, "foo"); +} + #[test] fn test_str_deref() { let str = Str::make("hello world"); From daa75d18887dbf6207f537283effde7710f216b5 Mon Sep 17 00:00:00 2001 From: Radu Racariu Date: Wed, 9 Sep 2026 19:03:08 +0300 Subject: [PATCH 7/9] test: add coverage for From> for Str --- src/haystack/val/string.rs | 7 +++++++ tests/values/test_string.rs | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/haystack/val/string.rs b/src/haystack/val/string.rs index 4931e72..e1a80b9 100644 --- a/src/haystack/val/string.rs +++ b/src/haystack/val/string.rs @@ -80,6 +80,13 @@ impl From<&str> for Str { } } +// Make a Haystack `Str` from a `Box` +impl From> for Str { + fn from(value: Box) -> Self { + Str { value } + } +} + /// Converts from `&str` slice to a `Str` `Value` impl From<&str> for Value { fn from(value: &str) -> Self { diff --git a/tests/values/test_string.rs b/tests/values/test_string.rs index 4810bce..0973898 100644 --- a/tests/values/test_string.rs +++ b/tests/values/test_string.rs @@ -55,6 +55,13 @@ fn test_str_into_inner() { assert_eq!(&*inner, "foo"); } +#[test] +fn test_str_from_box_str() { + let boxed: Box = "foo".into(); + let str = Str::from(boxed); + assert_eq!(str.value(), "foo"); +} + #[test] fn test_str_deref() { let str = Str::make("hello world"); From 9785823dd221f9300a77e7c79e06d35ae6cd9412 Mon Sep 17 00:00:00 2001 From: Radu Racariu Date: Thu, 10 Sep 2026 10:16:13 +0300 Subject: [PATCH 8/9] feat: add into_inner() to Uri/Symbol/Ref/XStr, From> for Uri/Symbol/Ref, with tests Mirrors the Str precedent from the previous commits: - Uri/Symbol: into_inner(self) -> Box, From> - Ref: into_inner(self) -> (Box, Option>), From> (dis: None) - XStr: into_inner(self) -> (Box, Box) for (type, value); no From> since XStr requires two fields Adds 8 new tests (329 total, up from 321) covering the new methods. Validated with cargo build --all-targets, cargo test --all-targets, cargo test --doc, and cargo clippy --all-targets (all clean). --- src/haystack/val/reference.rs | 12 ++++++++++++ src/haystack/val/symbol.rs | 12 ++++++++++++ src/haystack/val/uri.rs | 12 ++++++++++++ src/haystack/val/xstr.rs | 5 +++++ tests/values/test_reference.rs | 16 ++++++++++++++++ tests/values/test_symbol.rs | 14 ++++++++++++++ tests/values/test_uri.rs | 14 ++++++++++++++ tests/values/test_xstr.rs | 8 ++++++++ 8 files changed, 93 insertions(+) diff --git a/src/haystack/val/reference.rs b/src/haystack/val/reference.rs index d56ff35..9e2defd 100644 --- a/src/haystack/val/reference.rs +++ b/src/haystack/val/reference.rs @@ -63,6 +63,11 @@ impl Ref { pub fn set_dis(&mut self, dis: Option) { self.dis = dis.map(|s| s.into()); } + + /// Consumes the `Ref` and returns the underlying id and optional display name as a tuple. + pub fn into_inner(self) -> (Box, Option>) { + (self.value, self.dis) + } } /// Implement equality operator for Ref @@ -113,6 +118,13 @@ impl From for Ref { } } +/// Make a Haystack `Ref` from a `Box` value +impl From> for Ref { + fn from(value: Box) -> Self { + Ref { value, dis: None } + } +} + /// Converts from `Ref` to a `Ref` `Value` impl From for Value { fn from(value: Ref) -> Self { diff --git a/src/haystack/val/symbol.rs b/src/haystack/val/symbol.rs index cbe6f98..75efc67 100644 --- a/src/haystack/val/symbol.rs +++ b/src/haystack/val/symbol.rs @@ -35,6 +35,11 @@ impl Symbol { pub fn value(&self) -> &str { &self.value } + + /// Consumes the `Symbol` and returns the underlying `Box` value. + pub fn into_inner(self) -> Box { + self.value + } } // Make a Haystack `Symbol` from a string value @@ -55,6 +60,13 @@ impl From for Symbol { } } +// Make a Haystack `Symbol` from a Box value +impl From> for Symbol { + fn from(value: Box) -> Self { + Symbol { value } + } +} + /// Converts from `Symbol` to a `Symbol` `Value` impl From for Value { fn from(value: Symbol) -> Self { diff --git a/src/haystack/val/uri.rs b/src/haystack/val/uri.rs index c7563c2..19ebfcb 100644 --- a/src/haystack/val/uri.rs +++ b/src/haystack/val/uri.rs @@ -33,6 +33,11 @@ impl Uri { pub fn value(&self) -> &str { &self.value } + + /// Consumes the `Uri` and returns the underlying `Box` value. + pub fn into_inner(self) -> Box { + self.value + } } // Make a Haystack `Uri` from a String value @@ -53,6 +58,13 @@ impl From<&str> for Uri { } } +// Make a Haystack `Uri` from a Box value +impl From> for Uri { + fn from(value: Box) -> Self { + Uri { value } + } +} + /// Converts from `Uri` to a `Uri` `Value` impl From for Value { fn from(value: Uri) -> Self { diff --git a/src/haystack/val/xstr.rs b/src/haystack/val/xstr.rs index cc45ce2..89310ef 100644 --- a/src/haystack/val/xstr.rs +++ b/src/haystack/val/xstr.rs @@ -40,6 +40,11 @@ impl XStr { pub fn value(&self) -> &str { &self.value } + + /// Consumes the `XStr` and returns the underlying `type` and `value` as a tuple. + pub fn into_inner(self) -> (Box, Box) { + (self.r#type, self.value) + } } /// Converts from `XStr` to a `XStr` `Value` diff --git a/tests/values/test_reference.rs b/tests/values/test_reference.rs index dd25d1b..93e0b90 100644 --- a/tests/values/test_reference.rs +++ b/tests/values/test_reference.rs @@ -64,3 +64,19 @@ fn test_ref_value() { fn test_ref_cmp() { assert!(Ref::from("abc") < Ref::from("xyz")); } + +#[test] +fn test_ref_from_box_str() { + let boxed: Box = "id".into(); + let id = Ref::from(boxed); + assert_eq!(id.value(), "id"); + assert_eq!(id.dis(), None); +} + +#[test] +fn test_ref_into_inner() { + let id = Ref::make("id", Some("dis")); + let (value, dis) = id.into_inner(); + assert_eq!(&*value, "id"); + assert_eq!(dis.as_deref(), Some("dis")); +} diff --git a/tests/values/test_symbol.rs b/tests/values/test_symbol.rs index 69f8af4..05a916c 100644 --- a/tests/values/test_symbol.rs +++ b/tests/values/test_symbol.rs @@ -22,3 +22,17 @@ fn test_symbol_from() { let sym = Symbol::from("some-sym"); assert_eq!(sym.value(), "some-sym".to_string()); } + +#[test] +fn test_symbol_from_box_str() { + let boxed: Box = "some-sym".into(); + let sym = Symbol::from(boxed); + assert_eq!(sym.value(), "some-sym"); +} + +#[test] +fn test_symbol_into_inner() { + let sym = Symbol::make("some-sym"); + let inner: Box = sym.into_inner(); + assert_eq!(&*inner, "some-sym"); +} diff --git a/tests/values/test_uri.rs b/tests/values/test_uri.rs index b549786..4c7311d 100644 --- a/tests/values/test_uri.rs +++ b/tests/values/test_uri.rs @@ -28,3 +28,17 @@ fn test_uri_from_string() { let uri = Uri::from("/foo/baz/bar.txt".to_string()); assert_eq!(uri.value(), "/foo/baz/bar.txt".to_string()); } + +#[test] +fn test_uri_from_box_str() { + let boxed: Box = "/foo/baz/bar.txt".into(); + let uri = Uri::from(boxed); + assert_eq!(uri.value(), "/foo/baz/bar.txt"); +} + +#[test] +fn test_uri_into_inner() { + let uri = Uri::make("/a/b"); + let inner: Box = uri.into_inner(); + assert_eq!(&*inner, "/a/b"); +} diff --git a/tests/values/test_xstr.rs b/tests/values/test_xstr.rs index d70dd33..5c203cb 100644 --- a/tests/values/test_xstr.rs +++ b/tests/values/test_xstr.rs @@ -16,3 +16,11 @@ fn test_xstr_make_value() { assert_eq!(XStr::try_from(&value), Ok(XStr::make("type", "value"))); } + +#[test] +fn test_xstr_into_inner() { + let xstr = XStr::make("type", "value"); + let (r#type, value) = xstr.into_inner(); + assert_eq!(&*r#type, "type"); + assert_eq!(&*value, "value"); +} From c509626b9733dd3e57e8d9264f3aad042fbec60e Mon Sep 17 00:00:00 2001 From: Radu Racariu Date: Thu, 10 Sep 2026 11:32:27 +0300 Subject: [PATCH 9/9] perf: eliminate avoidable string clones in decoders; rename Ref/XStr into_inner to into_parts Several decode paths (zinc scalar, brio, trio, filter lexer) built a Str/Uri/Symbol/Ref/XStr from a byte buffer or another owned String by first borrowing it as &str (sometimes via from_utf8_lossy(..).into_owned(), which always allocates even when the bytes are already valid UTF-8), even though the source buffer/String was locally owned and about to be dropped. Fixed by moving the owned data in directly: - zinc scalar decoders (str/uri/symbol/reference/id): use String::from_utf8(buf) to reuse the already-owned buffer in the common valid-UTF8 case, falling back to from_utf8_lossy only on invalid input. - zinc reference decoder: build via Ref::from(value) + set_dis(dis) instead of Ref::make(&str, Option<&str>), avoiding a clone of both the id and dis. - zinc xstr decoder: use Str::into_inner() to move the parsed value's Box straight into XStr::make instead of re-cloning via value.as_str(). - brio decoder: CTRL_STR/CTRL_SYMBOL now move the decoded String directly via From instead of Value::from(&str)/Symbol::make(&str); CTRL_REF_STR/ CTRL_REF_I8 use a new make_ref() helper built on Ref::from()+set_dis(); CTRL_XSTR and decode_buf_as_xstr pass owned Strings directly. - trio decoder: Str::from(lines.join("\n")) instead of Str::make(&lines.join(..)). - filter lexer: Id gained a Box-backed into_inner(); the Rel-symbol path uses Symbol::from(id.into_inner()) for a true zero-copy handoff instead of going through Display/to_string(). Widened XStr::make() to accept impl Into> for both parameters (was &str only) so callers can pass an already-owned String/Box without an extra allocation; existing &str call sites are unaffected since &str already implements Into>. Renamed Ref::into_inner/XStr::into_inner to into_parts (they return a tuple of fields, unlike the single-Box into_inner on Str/Uri/Symbol), added Ref::from_parts as the inverse constructor, and added tests for into_parts/from_parts and their round-trip on Ref, plus into_parts on XStr. Validated with cargo build --all-targets, cargo test --all-targets (331 passed), cargo test --doc (131 passed), and cargo clippy --all-targets (clean). --- src/haystack/encoding/brio/decode.rs | 24 +++++++++++++------ src/haystack/encoding/trio/decode.rs | 2 +- src/haystack/encoding/zinc/decode/id.rs | 20 +++++++++++----- .../encoding/zinc/decode/scalar/reference.rs | 13 ++++++---- .../encoding/zinc/decode/scalar/str.rs | 7 +++++- .../encoding/zinc/decode/scalar/symbol.rs | 7 +++++- .../encoding/zinc/decode/scalar/uri.rs | 7 +++++- .../encoding/zinc/decode/scalar/xstr.rs | 2 +- src/haystack/filter/lexer.rs | 5 ++-- src/haystack/val/reference.rs | 9 ++++++- src/haystack/val/xstr.rs | 6 +++-- tests/values/test_reference.rs | 20 ++++++++++++++-- tests/values/test_xstr.rs | 4 ++-- 13 files changed, 94 insertions(+), 32 deletions(-) diff --git a/src/haystack/encoding/brio/decode.rs b/src/haystack/encoding/brio/decode.rs index 1ebb520..3ce33f3 100644 --- a/src/haystack/encoding/brio/decode.rs +++ b/src/haystack/encoding/brio/decode.rs @@ -258,7 +258,7 @@ fn decode_buf_as_xstr(reader: &mut R) -> Result { .read_exact(&mut bytes) .map_err(|e| Error::Message(e.to_string()))?; let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect(); - Ok(XStr::make("Bin", &hex)) + Ok(XStr::make("Bin", hex)) } /// Decode a non-empty Dict payload: `'{' varint(count) (key value)* '}'` @@ -439,16 +439,16 @@ impl FromBrio for Value { let u = decode_str(reader)?; Ok(Value::from(make_number(v, &u))) } - CTRL_STR => Ok(Value::from(decode_str(reader)?.as_str())), + CTRL_STR => Ok(Value::from(decode_str(reader)?)), CTRL_REF_STR => { let id = decode_str(reader)?; let dis = decode_str_chars(reader)?; - Ok(Value::from(Ref::make(&id, non_empty(dis.as_str())))) + Ok(Value::from(make_ref(id, dis))) } CTRL_REF_I8 => { let id = i8_to_ref_id(read_i64(reader)?); let dis = decode_str_chars(reader)?; - Ok(Value::from(Ref::make(&id, non_empty(dis.as_str())))) + Ok(Value::from(make_ref(id, dis))) } CTRL_URI => Ok(Value::from(Uri::from(decode_str(reader)?))), CTRL_DATE => { @@ -486,10 +486,10 @@ impl FromBrio for Value { Ok(Value::from(Coord::make(lat, lng))) } CTRL_XSTR => Ok(Value::from(XStr::make( - &decode_str(reader)?, - &decode_str(reader)?, + decode_str(reader)?, + decode_str(reader)?, ))), - CTRL_SYMBOL => Ok(Value::from(Symbol::make(&decode_str(reader)?))), + CTRL_SYMBOL => Ok(Value::from(Symbol::from(decode_str(reader)?))), CTRL_BUF => decode_buf_as_xstr(reader).map(Value::from), CTRL_DICT_EMPTY => Ok(Value::from(Dict::default())), CTRL_DICT => decode_dict_payload(reader).map(Value::from), @@ -545,6 +545,16 @@ fn non_empty(s: &str) -> Option<&str> { if s.is_empty() { None } else { Some(s) } } +/// Build a `Ref` from an owned id and an owned (possibly empty) display name, +/// moving both strings in directly to avoid re-allocating them. +fn make_ref(id: String, dis: String) -> Ref { + let mut r = Ref::from(id); + if non_empty(&dis).is_some() { + r.set_dis(Some(dis)); + } + r +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/src/haystack/encoding/trio/decode.rs b/src/haystack/encoding/trio/decode.rs index d2eb410..6294e82 100644 --- a/src/haystack/encoding/trio/decode.rs +++ b/src/haystack/encoding/trio/decode.rs @@ -254,7 +254,7 @@ impl TrioReader { // Colon with nothing after it -> multi-line string. Some("") => { let lines = self.read_indented_lines(); - Value::Str(Str::make(&lines.join("\n"))) + Value::Str(Str::from(lines.join("\n"))) } // Colon followed by `[` (possibly with trailing whitespace) -> multi-line Zinc list. diff --git a/src/haystack/encoding/zinc/decode/id.rs b/src/haystack/encoding/zinc/decode/id.rs index 8c9386f..bed2e5e 100644 --- a/src/haystack/encoding/zinc/decode/id.rs +++ b/src/haystack/encoding/zinc/decode/id.rs @@ -5,26 +5,31 @@ use super::scanner::Scanner; use std::fmt::Display; use std::io::{Error, Read}; -use std::string::ToString; /// Zinc identifier #[derive(PartialEq, Eq, PartialOrd, Clone, Debug)] pub struct Id { - pub(super) value: String, + pub(super) value: Box, +} + +impl Id { + pub fn into_inner(self) -> Box { + self.value + } } impl From<&str> for Id { //! Converts from `&str` to an `Id` fn from(value: &str) -> Self { Id { - value: String::from(value), + value: Box::from(value), } } } impl Display for Id { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.value.clone()) + write!(f, "{}", self.value) } } @@ -35,7 +40,9 @@ pub(crate) fn parse_id(scanner: &mut Scanner) -> Result { } let value = parse_literal(scanner)?; - Ok(Id { value }) + Ok(Id { + value: Box::from(value), + }) } /// Parse a Zinc literal, such as `NA` @@ -48,7 +55,8 @@ pub(super) fn parse_literal(scanner: &mut Scanner) -> Result(scanner: &mut Scanner) -> Result dis = Some(parse_str(scanner)?.into()); } - Ok(Ref::make( - &String::from_utf8_lossy(&ref_chars), - dis.as_deref(), - )) + // Reuse the buffer directly when valid UTF-8 (the common case) instead of + // cloning via `from_utf8_lossy(..).into_owned()`. + let value = String::from_utf8(ref_chars) + .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()); + + let mut r = Ref::from(value); + r.set_dis(dis); + + Ok(r) } #[cfg(test)] diff --git a/src/haystack/encoding/zinc/decode/scalar/str.rs b/src/haystack/encoding/zinc/decode/scalar/str.rs index 5e392bf..a59f4a0 100644 --- a/src/haystack/encoding/zinc/decode/scalar/str.rs +++ b/src/haystack/encoding/zinc/decode/scalar/str.rs @@ -33,7 +33,12 @@ pub(crate) fn parse_str(scanner: &mut Scanner) -> Result scanner.advance()?; - Ok(Str::from(String::from_utf8_lossy(&str).into_owned())) + // Reuse the buffer directly when valid UTF-8 (the common case) instead of + // cloning via `from_utf8_lossy(..).into_owned()`. + let value = String::from_utf8(str) + .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()); + + Ok(Str::from(value)) } // Parse a Str escape sequence diff --git a/src/haystack/encoding/zinc/decode/scalar/symbol.rs b/src/haystack/encoding/zinc/decode/scalar/symbol.rs index d872f7d..c0638fb 100644 --- a/src/haystack/encoding/zinc/decode/scalar/symbol.rs +++ b/src/haystack/encoding/zinc/decode/scalar/symbol.rs @@ -26,7 +26,12 @@ pub(crate) fn parse_symbol(scanner: &mut Scanner) -> Result(scanner: &mut Scanner) -> Result scanner.advance()?; - Ok(Uri::from(String::from_utf8_lossy(&str).into_owned())) + // Reuse the buffer directly when valid UTF-8 (the common case) instead of + // cloning via `from_utf8_lossy(..).into_owned()`. + let value = String::from_utf8(str) + .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()); + + Ok(Uri::from(value)) } #[cfg(test)] diff --git a/src/haystack/encoding/zinc/decode/scalar/xstr.rs b/src/haystack/encoding/zinc/decode/scalar/xstr.rs index 3d33291..bc72d66 100644 --- a/src/haystack/encoding/zinc/decode/scalar/xstr.rs +++ b/src/haystack/encoding/zinc/decode/scalar/xstr.rs @@ -20,7 +20,7 @@ pub(crate) fn parse_xstr_body( scanner.consume_spaces()?; scanner.expect_and_consume(b')')?; - Ok(XStr::make(name, value.as_str())) + Ok(XStr::make(name, value.into_inner())) } #[cfg(test)] diff --git a/src/haystack/filter/lexer.rs b/src/haystack/filter/lexer.rs index a2842f8..f0b5788 100644 --- a/src/haystack/filter/lexer.rs +++ b/src/haystack/filter/lexer.rs @@ -187,9 +187,8 @@ impl<'a, R: Read> Lexer> { b'?' => { self.scanner.read().ok(); - self.cur = LexerToken::make(TokenValue::Rel(Symbol::from( - id.to_string().as_str(), - ))) + self.cur = + LexerToken::make(TokenValue::Rel(Symbol::from(id.into_inner()))) } b'-' => { self.scanner.read()?; diff --git a/src/haystack/val/reference.rs b/src/haystack/val/reference.rs index 9e2defd..25a7097 100644 --- a/src/haystack/val/reference.rs +++ b/src/haystack/val/reference.rs @@ -65,9 +65,16 @@ impl Ref { } /// Consumes the `Ref` and returns the underlying id and optional display name as a tuple. - pub fn into_inner(self) -> (Box, Option>) { + pub fn into_parts(self) -> (Box, Option>) { (self.value, self.dis) } + + /// Constructs a `Ref` directly from its id and optional display name, + /// the inverse of `into_parts`, avoiding reallocation when both are + /// already owned `Box`. + pub fn from_parts(value: Box, dis: Option>) -> Self { + Ref { value, dis } + } } /// Implement equality operator for Ref diff --git a/src/haystack/val/xstr.rs b/src/haystack/val/xstr.rs index 89310ef..3dbdf38 100644 --- a/src/haystack/val/xstr.rs +++ b/src/haystack/val/xstr.rs @@ -24,7 +24,9 @@ pub struct XStr { value: Box, } impl XStr { - pub fn make(r#type: &str, value: &str) -> XStr { + /// Accepts anything convertible to `Box` (e.g. `&str`, `String`, `Box`) + /// so an already-owned string can be moved in without an extra clone. + pub fn make(r#type: impl Into>, value: impl Into>) -> XStr { XStr { r#type: r#type.into(), value: value.into(), @@ -42,7 +44,7 @@ impl XStr { } /// Consumes the `XStr` and returns the underlying `type` and `value` as a tuple. - pub fn into_inner(self) -> (Box, Box) { + pub fn into_parts(self) -> (Box, Box) { (self.r#type, self.value) } } diff --git a/tests/values/test_reference.rs b/tests/values/test_reference.rs index 93e0b90..a0caf43 100644 --- a/tests/values/test_reference.rs +++ b/tests/values/test_reference.rs @@ -74,9 +74,25 @@ fn test_ref_from_box_str() { } #[test] -fn test_ref_into_inner() { +fn test_ref_into_parts() { let id = Ref::make("id", Some("dis")); - let (value, dis) = id.into_inner(); + let (value, dis) = id.into_parts(); assert_eq!(&*value, "id"); assert_eq!(dis.as_deref(), Some("dis")); } + +#[test] +fn test_ref_from_parts() { + let value: Box = "id".into(); + let dis: Box = "dis".into(); + let id = Ref::from_parts(value, Some(dis)); + assert_eq!(id.value(), "id"); + assert_eq!(id.dis(), Some("dis")); +} + +#[test] +fn test_ref_into_parts_from_parts_round_trip() { + let id = Ref::make("id", Some("dis")); + let (value, dis) = id.clone().into_parts(); + assert_eq!(Ref::from_parts(value, dis), id); +} diff --git a/tests/values/test_xstr.rs b/tests/values/test_xstr.rs index 5c203cb..bb96f56 100644 --- a/tests/values/test_xstr.rs +++ b/tests/values/test_xstr.rs @@ -18,9 +18,9 @@ fn test_xstr_make_value() { } #[test] -fn test_xstr_into_inner() { +fn test_xstr_into_parts() { let xstr = XStr::make("type", "value"); - let (r#type, value) = xstr.into_inner(); + let (r#type, value) = xstr.into_parts(); assert_eq!(&*r#type, "type"); assert_eq!(&*value, "value"); }