From 796e711a4107811dc1a4355b763def1b6f12ede0 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 9 Feb 2026 19:40:33 -0800 Subject: [PATCH 1/9] Add ZeroTrie cursor into_suffix_trie --- utils/zerotrie/src/cursor.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/utils/zerotrie/src/cursor.rs b/utils/zerotrie/src/cursor.rs index 5711baaf33c..d4eec543402 100644 --- a/utils/zerotrie/src/cursor.rs +++ b/utils/zerotrie/src/cursor.rs @@ -159,7 +159,7 @@ pub struct AsciiProbeResult { pub total_siblings: u8, } -impl ZeroTrieSimpleAsciiCursor<'_> { +impl<'a> ZeroTrieSimpleAsciiCursor<'a> { /// Steps the cursor one character into the trie based on the character's byte value. /// /// # Examples @@ -341,6 +341,31 @@ impl ZeroTrieSimpleAsciiCursor<'_> { pub fn is_empty(&self) -> bool { self.trie.is_empty() } + + /// Returns a trie for all suffixes that begin with the previously stepped + /// bytes. + /// + /// # Examples + /// + /// ``` + /// use zerotrie::ZeroTrieSimpleAscii; + /// + /// // A trie with two values: "abc" and "abcdef" + /// let trie = ZeroTrieSimpleAscii::from_bytes(b"abc\x80def\x81"); + /// + /// // Consume the prefix "ab" + /// let mut cursor = trie.cursor(); + /// cursor.step(b'a'); + /// cursor.step(b'b'); + /// let suffix_trie = cursor.into_suffix_trie(); + /// + /// // The suffix trie contains the strings "c" and "cdef" + /// assert_eq!(suffix_trie.get("c"), Some(0)); + /// assert_eq!(suffix_trie.get("cdef"), Some(1)); + /// ``` + pub fn into_suffix_trie(self) -> ZeroTrieSimpleAscii<&'a [u8]> { + self.trie + } } impl ZeroAsciiIgnoreCaseTrieCursor<'_> { From fa709cbda033c4ce38a84ec9a8eba883e39f007b Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 9 Feb 2026 19:41:28 -0800 Subject: [PATCH 2/9] Add BoundLocaleDataProvider trait and required types --- provider/core/src/data_provider.rs | 46 ++++++++++++++++++++++++++++++ provider/core/src/lib.rs | 11 ++++++- provider/core/src/request.rs | 7 +++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/provider/core/src/data_provider.rs b/provider/core/src/data_provider.rs index d859a04efcf..52896f07877 100644 --- a/provider/core/src/data_provider.rs +++ b/provider/core/src/data_provider.rs @@ -9,6 +9,7 @@ use yoke::Yokeable; use alloc::boxed::Box; use crate::prelude::*; +use crate::request::DataAttributesRequest; /// A data provider that loads data for a specific [`DataMarkerInfo`]. pub trait DataProvider @@ -460,6 +461,51 @@ where } } +#[derive(Debug)] +#[allow(clippy::exhaustive_structs)] // exported in an unstable module +pub struct BindLocaleResponse { + pub metadata: DataResponseMetadata, + pub bound_provider: T, +} + +pub trait BindLocale +where + M: DynamicDataMarker, +{ + type BoundLocaleDataProvider: BoundLocaleDataProvider; + fn bind_locale( + &self, + marker: DataMarkerInfo, + req: DataRequest, + ) -> Result, DataError>; +} + +pub trait BoundLocaleDataProvider +where + M: DynamicDataMarker, +{ + /// Query the provider for data, returning the result. + /// + /// Returns [`Ok`] if the request successfully loaded data. If data failed to load, returns an + /// Error with more information. + fn load_bound(&self, req: DataAttributesRequest) -> Result, DataError>; + // /// Returns the [`DataMarkerInfo`] that this provider uses for loading data. + // fn bound_marker(&self) -> DataMarkerInfo; + // /// Returns the [`DataLocale`] that this provider uses for loading data. + // fn bound_locale(&self) -> DataLocale; +} + +impl BoundLocaleDataProvider for &P +where + M: DynamicDataMarker, + P: BoundLocaleDataProvider + ?Sized, +{ + #[inline] + fn load_bound(&self, req: DataAttributesRequest) -> Result, DataError> { + (**self).load_bound(req) + } +} + #[cfg(test)] mod test { diff --git a/provider/core/src/lib.rs b/provider/core/src/lib.rs index 60510e25c57..a173008feda 100644 --- a/provider/core/src/lib.rs +++ b/provider/core/src/lib.rs @@ -120,7 +120,10 @@ mod error; pub use error::{DataError, DataErrorKind, ResultDataError}; mod request; -pub use request::{DataLocale, DataMarkerAttributes, DataRequest, DataRequestMetadata, *}; +pub use request::{ + AttributeParseError, DataIdentifierBorrowed, DataIdentifierCow, DataLocale, + DataMarkerAttributes, DataRequest, DataRequestMetadata, +}; mod response; #[doc(hidden)] // TODO(#4467): establish this as an internal API @@ -182,6 +185,12 @@ pub mod prelude { pub use zerofrom; } +/// Additional traits and types currently being incubated +pub mod unstable { + pub use super::data_provider::{BindLocale, BindLocaleResponse, BoundLocaleDataProvider}; + pub use super::request::DataAttributesRequest; +} + #[doc(hidden)] // internal pub mod fallback; diff --git a/provider/core/src/request.rs b/provider/core/src/request.rs index 6016cadba08..cec252b920a 100644 --- a/provider/core/src/request.rs +++ b/provider/core/src/request.rs @@ -35,6 +35,13 @@ pub struct DataRequest<'a> { pub metadata: DataRequestMetadata, } +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[allow(clippy::exhaustive_structs)] // exported in an unstable module +pub struct DataAttributesRequest<'a> { + pub marker_attributes: &'a DataMarkerAttributes, + pub metadata: DataRequestMetadata, +} + /// Metadata for data requests. This is currently empty, but it may be extended with options /// for tuning locale fallback, buffer layout, and so forth. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] From 0d73021b155c088d7af1f7568d4a2c224d5fd24c Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 9 Feb 2026 19:41:44 -0800 Subject: [PATCH 3/9] Add serde converter --- provider/core/src/buf/serde.rs | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/provider/core/src/buf/serde.rs b/provider/core/src/buf/serde.rs index 5fa989a0374..3d275265ea8 100644 --- a/provider/core/src/buf/serde.rs +++ b/provider/core/src/buf/serde.rs @@ -15,6 +15,7 @@ use crate::buf::BufferFormat; use crate::buf::BufferProvider; use crate::data_provider::DynamicDryDataProvider; use crate::prelude::*; +use crate::unstable::BoundLocaleDataProvider; use crate::DryDataProvider; use serde::de::Deserialize; use yoke::Yokeable; @@ -59,6 +60,22 @@ where } } +impl<'a, P> DeserializingBufferProvider<'a, P> { + /// Wraps the given provider in a [`DeserializingBufferProvider`]. + /// + /// This requires enabling the deserialization Cargo feature + /// for the expected format(s): + /// + /// - `deserialize_json` + /// - `deserialize_postcard_1` + /// - `deserialize_bincode_1` + /// + /// ✨ *Enabled with the `serde` Cargo feature.* + pub fn new(inner: &'a P) -> Self { + Self(inner) + } +} + fn deserialize_impl<'data, M>( // Allow `bytes` to be unused in case all buffer formats are disabled #[allow(unused_variables)] bytes: &'data [u8], @@ -230,6 +247,32 @@ where } } +impl BoundLocaleDataProvider for DeserializingBufferProvider<'_, P> +where + M: DynamicDataMarker, + P: BoundLocaleDataProvider + ?Sized, + for<'de> >::Output: Deserialize<'de>, +{ + fn load_bound( + &self, + req: crate::request::DataAttributesRequest, + ) -> Result, DataError> { + let buffer_response = self.0.load_bound(req)?; + let buffer_format = buffer_response.metadata.buffer_format.ok_or_else(|| { + DataErrorKind::Deserialize + .with_str_context("BufferProvider didn't set BufferFormat") + .with_debug_context(&req) + })?; + Ok(DataResponse { + metadata: buffer_response.metadata, + payload: buffer_response + .payload + .into_deserialized(buffer_format) + .map_err(|e| e.with_debug_context(&req))?, + }) + } +} + #[cfg(feature = "deserialize_json")] impl From for DataError { fn from(e: serde_json::error::Error) -> Self { From 0651106960d73ca9fab2349a7e0d6e0e6f51ab50 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 9 Feb 2026 19:42:10 -0800 Subject: [PATCH 4/9] Implement BoundLocaleDataProvider for icu_provider_blob --- provider/blob/src/blob_data_provider.rs | 90 +++++++++++++++++ provider/blob/src/blob_schema.rs | 127 ++++++++++++++++++++---- provider/blob/src/lib.rs | 5 + 3 files changed, 200 insertions(+), 22 deletions(-) diff --git a/provider/blob/src/blob_data_provider.rs b/provider/blob/src/blob_data_provider.rs index f998168a174..db4439d2835 100644 --- a/provider/blob/src/blob_data_provider.rs +++ b/provider/blob/src/blob_data_provider.rs @@ -2,11 +2,16 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). +use crate::blob_schema::BlobBoundLocaleSchema; use crate::blob_schema::BlobSchema; #[cfg(feature = "alloc")] use alloc::boxed::Box; use icu_provider::buf::BufferFormat; use icu_provider::prelude::*; +use icu_provider::unstable::BindLocale; +use icu_provider::unstable::BindLocaleResponse; +use icu_provider::unstable::BoundLocaleDataProvider; +use icu_provider::unstable::DataAttributesRequest; use icu_provider::Cart; use icu_provider::DynamicDryDataProvider; use yoke::*; @@ -166,6 +171,91 @@ impl IterableDynamicDataProvider for BlobDataProvider { } } +/// A [`BlobDataProvider`] that returns data for a specific marker and locale. +/// +/// # Examples +/// +/// ``` +/// use icu_locale_core::locale; +/// use icu_provider::prelude::*; +/// use icu_provider::buf::DeserializingBufferProvider; +/// use icu_provider::hello_world::HelloWorldV1; +/// use icu_provider::hello_world::HelloWorldFormatterPreferences; +/// use icu_provider::unstable::BindLocale; +/// use icu_provider::unstable::BoundLocaleDataProvider; +/// use icu_provider::unstable::DataAttributesRequest; +/// use icu_provider_blob::BlobDataProvider; +/// use writeable::assert_writeable_eq; +/// +/// // Read an ICU4X data blob statically: +/// const HELLO_WORLD_BLOB: &[u8] = include_bytes!("../tests/data/v3.postcard"); +/// +/// // Create a DataProvider from it: +/// let provider = BlobDataProvider::try_new_from_static_blob(HELLO_WORLD_BLOB) +/// .expect("Deserialization should succeed"); +/// +/// // Bind a specific marker and locale: +/// let locale = HelloWorldV1::INFO.make_locale(HelloWorldFormatterPreferences::from(locale!("en")).locale_preferences); +/// let bound_provider = provider.bind_locale(HelloWorldV1::INFO, DataRequest { +/// metadata: Default::default(), +/// id: DataIdentifierBorrowed::for_locale(&locale) +/// }).unwrap().bound_provider; +/// +/// // Now load a specific attribute: +/// let response = BoundLocaleDataProvider::::load_bound( +/// &DeserializingBufferProvider::new(&bound_provider), +/// DataAttributesRequest { +/// marker_attributes: DataMarkerAttributes::try_from_str("reverse").unwrap(), +/// metadata: Default::default() +/// } +/// ).unwrap(); +/// +/// assert_writeable_eq!(response.payload.get().message, "Olleh Dlrow"); +/// ``` +#[derive(Debug)] +pub struct BlobBoundLocaleDataProvider { + pub(crate) data: Yoke, Option>, +} + +impl BindLocale for BlobDataProvider { + type BoundLocaleDataProvider = BlobBoundLocaleDataProvider; + fn bind_locale( + &self, + marker: DataMarkerInfo, + req: DataRequest, + ) -> Result, DataError> { + let payload: Yoke<(BlobBoundLocaleSchema, Option), Option> = self + .data + .try_map_project_cloned(|blob, _| blob.bind_locale(marker, req))?; + let mut metadata = DataResponseMetadata::default(); + metadata.buffer_format = Some(BufferFormat::Postcard1); + metadata.checksum = payload.get().1; + Ok(BindLocaleResponse { + metadata, + bound_provider: BlobBoundLocaleDataProvider { + data: payload.map_project(|(inner, _), _| inner), + }, + }) + } +} + +impl BoundLocaleDataProvider for BlobBoundLocaleDataProvider { + fn load_bound( + &self, + req: DataAttributesRequest, + ) -> Result, DataError> { + let payload: Yoke<&[u8], Option> = + self.data.try_map_project_cloned(|blob, _| blob.load(req))?; + let mut metadata = DataResponseMetadata::default(); + metadata.buffer_format = Some(BufferFormat::Postcard1); + // Note: the checksum is returned by `bind_locale()` instead of `load_bound()` + Ok(DataResponse { + metadata, + payload: DataPayload::from_yoked_buffer(payload), + }) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/provider/blob/src/blob_schema.rs b/provider/blob/src/blob_schema.rs index 577e10db40f..15a3506b12b 100644 --- a/provider/blob/src/blob_schema.rs +++ b/provider/blob/src/blob_schema.rs @@ -3,10 +3,12 @@ // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). use core::fmt::Write; -use icu_provider::{marker::DataMarkerIdHash, prelude::*}; +use icu_provider::marker::DataMarkerIdHash; +use icu_provider::prelude::*; +use icu_provider::unstable::DataAttributesRequest; use serde::Deserialize; use writeable::Writeable; -use zerotrie::ZeroTrieSimpleAscii; +use zerotrie::{cursor::ZeroTrieSimpleAsciiCursor, ZeroTrieSimpleAscii}; use zerovec::vecs::{Index16, Index32, VarZeroSlice, VarZeroVecFormat, ZeroSlice}; /// A versioned Serde schema for ICU4X data blobs. @@ -52,6 +54,20 @@ impl<'data> BlobSchema<'data> { } } + pub(crate) fn bind_locale( + &self, + marker: DataMarkerInfo, + req: DataRequest, + ) -> Result<(BlobBoundLocaleSchema<'data>, Option), DataError> { + match self { + BlobSchema::V001(..) | BlobSchema::V002(..) | BlobSchema::V002Bigger(..) => { + unreachable!("Unreachable blob schema") + } + BlobSchema::V003(s) => s.bind_locale(marker, req), + BlobSchema::V003Bigger(s) => s.bind_locale(marker, req), + } + } + #[cfg(feature = "alloc")] pub fn iter_ids( &self, @@ -125,15 +141,28 @@ impl Default for BlobSchemaV1<'_, LocaleVecFo } } +fn load_attributes( + mut cursor: ZeroTrieSimpleAsciiCursor, + marker_attributes: &DataMarkerAttributes, + metadata: DataRequestMetadata, +) -> Option { + let _infallible_ascii = marker_attributes.write_to(&mut cursor); + loop { + let index = cursor.take_value(); + if index.is_some() || !metadata.attributes_prefix_match { + break index; + } + // Match the shortest attribute sharing a prefix. + cursor.probe(0); + } +} + impl<'data, LocaleVecFormat: VarZeroVecFormat> BlobSchemaV1<'data, LocaleVecFormat> { - pub fn load( + pub(crate) fn get_trie( &self, marker: DataMarkerInfo, req: DataRequest, - ) -> Result<(&'data [u8], Option), DataError> { - if marker.is_singleton && !req.id.locale.is_unknown() { - return Err(DataErrorKind::InvalidRequest.with_req(marker, req)); - } + ) -> Result, DataError> { let marker_index = self .markers .binary_search(&marker.id.hashed()) @@ -143,22 +172,23 @@ impl<'data, LocaleVecFormat: VarZeroVecFormat> BlobSchemaV1<'data, LocaleVecForm .locales .get(marker_index) .ok_or_else(|| DataError::custom("Invalid blob bytes").with_req(marker, req))?; - let mut cursor = ZeroTrieSimpleAscii::from_store(zerotrie).into_cursor(); + Ok(ZeroTrieSimpleAscii::from_store(zerotrie)) + } + + pub(crate) fn load( + &self, + marker: DataMarkerInfo, + req: DataRequest, + ) -> Result<(&'data [u8], Option), DataError> { + if marker.is_singleton && !req.id.locale.is_unknown() { + return Err(DataErrorKind::InvalidRequest.with_req(marker, req)); + } + let zerotrie = self.get_trie(marker, req)?; + let mut cursor = zerotrie.into_cursor(); let _infallible_ascii = req.id.locale.write_to(&mut cursor); let blob_index = if !req.id.marker_attributes.is_empty() { let _infallible_ascii = cursor.write_char(REQUEST_SEPARATOR); - req.id - .marker_attributes - .write_to(&mut cursor) - .map_err(|_| DataErrorKind::IdentifierNotFound.with_req(marker, req))?; - loop { - if let Some(v) = cursor.take_value() { - break Some(v); - } - if !req.metadata.attributes_prefix_match || cursor.probe(0).is_none() { - break None; - } - } + load_attributes(cursor, req.id.marker_attributes, req.metadata) } else { cursor.take_value() } @@ -176,8 +206,36 @@ impl<'data, LocaleVecFormat: VarZeroVecFormat> BlobSchemaV1<'data, LocaleVecForm )) } - fn get_checksum(&self, zerotrie: &[u8]) -> Option { - ZeroTrieSimpleAscii::from_store(zerotrie) + pub(crate) fn bind_locale( + &self, + marker: DataMarkerInfo, + req: DataRequest, + ) -> Result<(BlobBoundLocaleSchema<'data>, Option), DataError> { + // Note: singleton markers do not make sense with this function + if marker.is_singleton || req.id.locale.is_unknown() { + return Err(DataErrorKind::InvalidRequest.with_req(marker, req)); + } + let zerotrie = self.get_trie(marker, req)?; + let mut cursor = zerotrie.into_cursor(); + let _infallible_ascii = req.id.locale.write_to(&mut cursor); + let _infallible_ascii = cursor.write_char(REQUEST_SEPARATOR); + if cursor.is_empty() { + return Err(DataErrorKind::IdentifierNotFound.with_req(marker, req)); + } + Ok(( + BlobBoundLocaleSchema { + attributes_trie: cursor.into_suffix_trie(), + buffers: self.buffers, + }, + marker + .has_checksum + .then(|| self.get_checksum(zerotrie)) + .flatten(), + )) + } + + fn get_checksum(&self, zerotrie: ZeroTrieSimpleAscii<&'data [u8]>) -> Option { + zerotrie .get(CHECKSUM_KEY) .and_then(|cs| Some(u64::from_le_bytes(self.buffers.get(cs)?.try_into().ok()?))) } @@ -241,3 +299,28 @@ impl<'data, LocaleVecFormat: VarZeroVecFormat> BlobSchemaV1<'data, LocaleVecForm debug_assert!(seen_max); } } + +#[derive(Clone, Copy, Debug, yoke::Yokeable)] +pub(crate) struct BlobBoundLocaleSchema<'data> { + attributes_trie: ZeroTrieSimpleAscii<&'data [u8]>, + buffers: &'data VarZeroSlice<[u8], Index32>, +} + +impl<'data> BlobBoundLocaleSchema<'data> { + pub(crate) fn load(&self, req: DataAttributesRequest) -> Result<&'data [u8], DataError> { + let blob_index = load_attributes( + self.attributes_trie.cursor(), + req.marker_attributes, + req.metadata, + ) + .ok_or_else(|| { + DataErrorKind::IdentifierNotFound + .into_error() + .with_debug_context(req.marker_attributes) + })?; + let buffer = self.buffers.get(blob_index).ok_or_else(|| { + DataError::custom("Invalid blob bytes").with_debug_context(req.marker_attributes) + })?; + Ok(buffer) + } +} diff --git a/provider/blob/src/lib.rs b/provider/blob/src/lib.rs index 2d13bdb3345..44329dba3e4 100644 --- a/provider/blob/src/lib.rs +++ b/provider/blob/src/lib.rs @@ -38,3 +38,8 @@ mod blob_schema; pub mod export; pub use blob_data_provider::BlobDataProvider; + +/// Additional types for mostly internal usage. +pub mod unstable { + pub use super::blob_data_provider::BlobBoundLocaleDataProvider; +} From df2d3ac482e57b531c18f496266bed91bcc55869 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 9 Feb 2026 20:59:31 -0800 Subject: [PATCH 5/9] Add HelloWorldAttributeFormatter and fix things for it --- provider/blob/src/blob_data_provider.rs | 15 ++- provider/core/src/buf/serde.rs | 23 ++-- provider/core/src/data_provider.rs | 40 +++++- provider/core/src/hello_world.rs | 168 +++++++++++++++++++++++- provider/core/src/lib.rs | 5 +- 5 files changed, 228 insertions(+), 23 deletions(-) diff --git a/provider/blob/src/blob_data_provider.rs b/provider/blob/src/blob_data_provider.rs index db4439d2835..64d7dc1a35f 100644 --- a/provider/blob/src/blob_data_provider.rs +++ b/provider/blob/src/blob_data_provider.rs @@ -8,9 +8,10 @@ use crate::blob_schema::BlobSchema; use alloc::boxed::Box; use icu_provider::buf::BufferFormat; use icu_provider::prelude::*; -use icu_provider::unstable::BindLocale; +use icu_provider::unstable::BindLocaleDataProvider; use icu_provider::unstable::BindLocaleResponse; use icu_provider::unstable::BoundLocaleDataProvider; +use icu_provider::unstable::BoundLocaleDataResponse; use icu_provider::unstable::DataAttributesRequest; use icu_provider::Cart; use icu_provider::DynamicDryDataProvider; @@ -217,7 +218,7 @@ pub struct BlobBoundLocaleDataProvider { pub(crate) data: Yoke, Option>, } -impl BindLocale for BlobDataProvider { +impl BindLocaleDataProvider for BlobDataProvider { type BoundLocaleDataProvider = BlobBoundLocaleDataProvider; fn bind_locale( &self, @@ -243,15 +244,15 @@ impl BoundLocaleDataProvider for BlobBoundLocaleDataProvider { fn load_bound( &self, req: DataAttributesRequest, - ) -> Result, DataError> { - let payload: Yoke<&[u8], Option> = - self.data.try_map_project_cloned(|blob, _| blob.load(req))?; + ) -> Result, DataError> { + let blob = self.data.get(); + let payload = blob.load(req)?; let mut metadata = DataResponseMetadata::default(); metadata.buffer_format = Some(BufferFormat::Postcard1); // Note: the checksum is returned by `bind_locale()` instead of `load_bound()` - Ok(DataResponse { + Ok(BoundLocaleDataResponse { metadata, - payload: DataPayload::from_yoked_buffer(payload), + payload: payload, }) } } diff --git a/provider/core/src/buf/serde.rs b/provider/core/src/buf/serde.rs index 3d275265ea8..8c245d65a1a 100644 --- a/provider/core/src/buf/serde.rs +++ b/provider/core/src/buf/serde.rs @@ -13,6 +13,7 @@ use crate::buf::BufferFormat; use crate::buf::BufferProvider; +use crate::data_provider::BoundLocaleDataResponse; use crate::data_provider::DynamicDryDataProvider; use crate::prelude::*; use crate::unstable::BoundLocaleDataProvider; @@ -60,8 +61,12 @@ where } } -impl<'a, P> DeserializingBufferProvider<'a, P> { - /// Wraps the given provider in a [`DeserializingBufferProvider`]. +/// A [`BufferProvider`] that deserializes its data using Serde. +#[derive(Debug)] +pub struct DeserializingOwnedBufferProvider

(P); + +impl

DeserializingOwnedBufferProvider

{ + /// Wraps the given provider in a [`DeserializingOwnedBufferProvider`]. /// /// This requires enabling the deserialization Cargo feature /// for the expected format(s): @@ -71,7 +76,7 @@ impl<'a, P> DeserializingBufferProvider<'a, P> { /// - `deserialize_bincode_1` /// /// ✨ *Enabled with the `serde` Cargo feature.* - pub fn new(inner: &'a P) -> Self { + pub fn new(inner: P) -> Self { Self(inner) } } @@ -247,27 +252,25 @@ where } } -impl BoundLocaleDataProvider for DeserializingBufferProvider<'_, P> +impl BoundLocaleDataProvider for DeserializingOwnedBufferProvider

where M: DynamicDataMarker, - P: BoundLocaleDataProvider + ?Sized, + P: BoundLocaleDataProvider, for<'de> >::Output: Deserialize<'de>, { fn load_bound( &self, req: crate::request::DataAttributesRequest, - ) -> Result, DataError> { + ) -> Result, DataError> { let buffer_response = self.0.load_bound(req)?; let buffer_format = buffer_response.metadata.buffer_format.ok_or_else(|| { DataErrorKind::Deserialize .with_str_context("BufferProvider didn't set BufferFormat") .with_debug_context(&req) })?; - Ok(DataResponse { + Ok(BoundLocaleDataResponse { metadata: buffer_response.metadata, - payload: buffer_response - .payload - .into_deserialized(buffer_format) + payload: deserialize_impl::(buffer_response.payload, buffer_format) .map_err(|e| e.with_debug_context(&req))?, }) } diff --git a/provider/core/src/data_provider.rs b/provider/core/src/data_provider.rs index 52896f07877..15137ab5610 100644 --- a/provider/core/src/data_provider.rs +++ b/provider/core/src/data_provider.rs @@ -2,6 +2,7 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). +use core::fmt::Debug; use core::marker::PhantomData; use yoke::Yokeable; @@ -468,10 +469,11 @@ pub struct BindLocaleResponse { pub bound_provider: T, } -pub trait BindLocale +pub trait BindLocaleDataProvider where M: DynamicDataMarker, { + // TODO: This should be allowed to borrow from &self. type BoundLocaleDataProvider: BoundLocaleDataProvider; fn bind_locale( &self, @@ -480,6 +482,32 @@ where ) -> Result, DataError>; } +#[allow(clippy::exhaustive_structs)] // exported in an unstable module +pub struct BoundLocaleDataResponse<'data, M> +where + M: DynamicDataMarker, +{ + /// Metadata about the returned object. + pub metadata: DataResponseMetadata, + + /// The object itself + pub payload: >::Output, +} + +impl<'data, M> Debug for BoundLocaleDataResponse<'data, M> +where + M: DynamicDataMarker, + >::Output: Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "DataResponse {{ metadata: {:?}, payload: {:?} }}", + self.metadata, self.payload + ) + } +} + pub trait BoundLocaleDataProvider where M: DynamicDataMarker, @@ -488,7 +516,10 @@ where /// /// Returns [`Ok`] if the request successfully loaded data. If data failed to load, returns an /// Error with more information. - fn load_bound(&self, req: DataAttributesRequest) -> Result, DataError>; + fn load_bound( + &self, + req: DataAttributesRequest, + ) -> Result, DataError>; // /// Returns the [`DataMarkerInfo`] that this provider uses for loading data. // fn bound_marker(&self) -> DataMarkerInfo; // /// Returns the [`DataLocale`] that this provider uses for loading data. @@ -501,7 +532,10 @@ where P: BoundLocaleDataProvider + ?Sized, { #[inline] - fn load_bound(&self, req: DataAttributesRequest) -> Result, DataError> { + fn load_bound( + &self, + req: DataAttributesRequest, + ) -> Result, DataError> { (**self).load_bound(req) } } diff --git a/provider/core/src/hello_world.rs b/provider/core/src/hello_world.rs index 1b68b8578ef..3d3b56f4444 100644 --- a/provider/core/src/hello_world.rs +++ b/provider/core/src/hello_world.rs @@ -7,6 +7,11 @@ #![allow(clippy::exhaustive_structs)] // data struct module use crate as icu_provider; +use crate::buf::{DeserializingBufferProvider, DeserializingOwnedBufferProvider}; +use crate::request::DataAttributesRequest; +use crate::unstable::{BindLocaleDataProvider, BoundLocaleDataProvider}; +#[cfg(feature = "deserialize_json")] +use crate::unstable::{BindLocaleResponse, BoundLocaleDataResponse}; use crate::prelude::*; use alloc::borrow::Cow; @@ -14,6 +19,8 @@ use alloc::collections::BTreeSet; use alloc::string::String; use core::fmt::Debug; use icu_locale_core::preferences::define_preferences; +#[cfg(feature = "deserialize_json")] +use std::collections::BTreeMap; use writeable::Writeable; use yoke::*; use zerofrom::*; @@ -243,6 +250,72 @@ impl DynamicDataProvider for HelloWorldJsonProvider { } } +#[cfg(feature = "deserialize_json")] +#[derive(Debug)] +pub struct HelloWorldJsonBoundLocaleProvider { + json_strings: BTreeMap<&'static DataMarkerAttributes, String>, +} + +#[cfg(feature = "deserialize_json")] +impl BindLocaleDataProvider for HelloWorldJsonProvider { + type BoundLocaleDataProvider = HelloWorldJsonBoundLocaleProvider; + fn bind_locale( + &self, + marker: DataMarkerInfo, + req: DataRequest, + ) -> Result, DataError> { + marker.match_marker(HelloWorldV1::INFO)?; + let json_strings = HelloWorldProvider::DATA + .iter() + .filter_map(|(l, a, v)| { + if req.id.locale.strict_cmp(l.as_bytes()).is_eq() && !a.is_empty() { + let attributes = DataMarkerAttributes::from_str_or_panic(*a); + let json_string = serde_json::to_string(&HelloWorld { + message: Cow::Borrowed(v), + }) + .unwrap(); + Some((attributes, json_string)) + } else { + None + } + }) + .collect::>(); + if json_strings.is_empty() { + return Err(DataErrorKind::IdentifierNotFound.with_req(HelloWorldV1::INFO, req)); + } + Ok(BindLocaleResponse { + bound_provider: HelloWorldJsonBoundLocaleProvider { json_strings }, + metadata: Default::default(), + }) + } +} + +#[cfg(feature = "deserialize_json")] +impl BoundLocaleDataProvider for HelloWorldJsonBoundLocaleProvider { + fn load_bound<'data>( + &'data self, + req: DataAttributesRequest, + ) -> Result, DataError> { + // TODO: Implement logic for attributes_prefix_match + let json_string = self + .json_strings + .get(req.marker_attributes) + .ok_or_else(|| { + DataErrorKind::IdentifierNotFound + .into_error() + .with_debug_context(&req) + })?; + Ok(BoundLocaleDataResponse { + metadata: DataResponseMetadata { + buffer_format: Some(icu_provider::buf::BufferFormat::Json), + ..Default::default() + }, + #[expect(clippy::unwrap_used)] // HelloWorld::serialize is infallible + payload: json_string.as_bytes(), + }) + } +} + impl IterableDataProvider for HelloWorldProvider { fn iter_ids(&self) -> Result>, DataError> { #[expect(clippy::unwrap_used)] // hello-world @@ -291,12 +364,37 @@ pub struct HelloWorldFormatter { data: DataPayload, } +/// A type that formats variants of localized "hello world" strings. +/// +/// This type is intended to take the shape of an ICU4X formatter that lazily +/// loads data marker attributes. +/// +/// # Examples +/// +/// ``` +/// use icu_locale_core::locale; +/// use icu_provider::hello_world::{HelloWorldAttributeFormatter, HelloWorldProvider}; +/// use writeable::assert_writeable_eq; +/// +/// let fmt = HelloWorldAttributeFormatter::try_new_with_buffer_provider( +/// &HelloWorldProvider.into_json_provider(), +/// locale!("en").into(), +/// ) +/// .expect("locale exists and has attributes"); +/// +/// assert_writeable_eq!(fmt.format("reverse").unwrap(), "Olleh Dlrow"); +/// ``` +#[derive(Debug)] +pub struct HelloWorldAttributeFormatter> { + provider: P, +} + /// A formatted hello world message. Implements [`Writeable`]. /// /// For an example, see [`HelloWorldFormatter`]. #[derive(Debug)] pub struct FormattedHelloWorld<'l> { - data: &'l HelloWorld<'l>, + data: HelloWorld<'l>, } impl HelloWorldFormatter { @@ -336,7 +434,7 @@ impl HelloWorldFormatter { /// Formats a hello world message, returning a [`FormattedHelloWorld`]. pub fn format<'l>(&'l self) -> FormattedHelloWorld<'l> { FormattedHelloWorld { - data: self.data.get(), + data: self.data.get().clone(), // clones cows whose fields are borrowed } } @@ -346,6 +444,72 @@ impl HelloWorldFormatter { } } +impl> + HelloWorldAttributeFormatter> +{ + pub fn try_new_with_buffer_provider( + provider: &P1, + prefs: HelloWorldFormatterPreferences, + ) -> Result + where + P1: BindLocaleDataProvider, + { + let locale = HelloWorldV1::INFO.make_locale(prefs.locale_preferences); + let response = provider.bind_locale( + HelloWorldV1::INFO, + DataRequest { + id: DataIdentifierBorrowed::for_locale(&locale), + metadata: Default::default(), + }, + )?; + Ok(Self { + provider: DeserializingOwnedBufferProvider::new(response.bound_provider), + }) + } +} + +impl> HelloWorldAttributeFormatter

{ + /// Formats a hello world message with the specified attribute, + /// returning a [`FormattedHelloWorld`]. + pub fn format<'l>(&'l self, attribute: &str) -> Result, DataError> { + self.format_internal(attribute, false) + } + + /// Formats a hello world message with the attribute best matching a prefix, + /// returning a [`FormattedHelloWorld`]. + pub fn format_for_prefix<'l>( + &'l self, + prefix: &str, + ) -> Result, DataError> { + self.format_internal(prefix, true) + } + + fn format_internal<'l>( + &'l self, + attribute: &str, + attributes_prefix_match: bool, + ) -> Result, DataError> { + let marker_attributes = DataMarkerAttributes::try_from_str(attribute) + .map_err(|_| DataError::custom("invalid attribute").with_debug_context(attribute))?; + let mut metadata = DataRequestMetadata::default(); + metadata.attributes_prefix_match = attributes_prefix_match; + let result = self.provider.load_bound(DataAttributesRequest { + marker_attributes, + metadata, + })?; + Ok(FormattedHelloWorld { + data: result.payload, + }) + } + + /// Formats a hello world message with the specified attribute, + /// returning a [`String`]. + pub fn format_to_string(&self, attribute: &str) -> Result { + self.format(attribute) + .map(|ok| ok.write_to_string().into_owned()) + } +} + impl Writeable for FormattedHelloWorld<'_> { fn write_to(&self, sink: &mut W) -> core::fmt::Result { self.data.message.write_to(sink) diff --git a/provider/core/src/lib.rs b/provider/core/src/lib.rs index a173008feda..ce142a8348b 100644 --- a/provider/core/src/lib.rs +++ b/provider/core/src/lib.rs @@ -187,7 +187,10 @@ pub mod prelude { /// Additional traits and types currently being incubated pub mod unstable { - pub use super::data_provider::{BindLocale, BindLocaleResponse, BoundLocaleDataProvider}; + pub use super::data_provider::{ + BindLocaleDataProvider, BindLocaleResponse, BoundLocaleDataProvider, + BoundLocaleDataResponse, + }; pub use super::request::DataAttributesRequest; } From 6332af977b8e377fafb25e68d7a3e98b9ca1144e Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 9 Feb 2026 21:03:26 -0800 Subject: [PATCH 6/9] Add an optional lifetime to BoundLocaleDataProvider --- provider/blob/src/blob_data_provider.rs | 4 ++-- provider/core/src/data_provider.rs | 9 ++++----- provider/core/src/hello_world.rs | 10 +++++----- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/provider/blob/src/blob_data_provider.rs b/provider/blob/src/blob_data_provider.rs index 64d7dc1a35f..84fcd8d8241 100644 --- a/provider/blob/src/blob_data_provider.rs +++ b/provider/blob/src/blob_data_provider.rs @@ -219,12 +219,12 @@ pub struct BlobBoundLocaleDataProvider { } impl BindLocaleDataProvider for BlobDataProvider { - type BoundLocaleDataProvider = BlobBoundLocaleDataProvider; + type BoundLocaleDataProvider<'data> = BlobBoundLocaleDataProvider; fn bind_locale( &self, marker: DataMarkerInfo, req: DataRequest, - ) -> Result, DataError> { + ) -> Result>, DataError> { let payload: Yoke<(BlobBoundLocaleSchema, Option), Option> = self .data .try_map_project_cloned(|blob, _| blob.bind_locale(marker, req))?; diff --git a/provider/core/src/data_provider.rs b/provider/core/src/data_provider.rs index 15137ab5610..d91f001e072 100644 --- a/provider/core/src/data_provider.rs +++ b/provider/core/src/data_provider.rs @@ -473,13 +473,12 @@ pub trait BindLocaleDataProvider where M: DynamicDataMarker, { - // TODO: This should be allowed to borrow from &self. - type BoundLocaleDataProvider: BoundLocaleDataProvider; - fn bind_locale( - &self, + type BoundLocaleDataProvider<'data>: BoundLocaleDataProvider + 'data where Self: 'data; + fn bind_locale<'data>( + &'data self, marker: DataMarkerInfo, req: DataRequest, - ) -> Result, DataError>; + ) -> Result>, DataError>; } #[allow(clippy::exhaustive_structs)] // exported in an unstable module diff --git a/provider/core/src/hello_world.rs b/provider/core/src/hello_world.rs index 3d3b56f4444..1f659310f88 100644 --- a/provider/core/src/hello_world.rs +++ b/provider/core/src/hello_world.rs @@ -258,12 +258,12 @@ pub struct HelloWorldJsonBoundLocaleProvider { #[cfg(feature = "deserialize_json")] impl BindLocaleDataProvider for HelloWorldJsonProvider { - type BoundLocaleDataProvider = HelloWorldJsonBoundLocaleProvider; + type BoundLocaleDataProvider<'data> = HelloWorldJsonBoundLocaleProvider; fn bind_locale( &self, marker: DataMarkerInfo, req: DataRequest, - ) -> Result, DataError> { + ) -> Result>, DataError> { marker.match_marker(HelloWorldV1::INFO)?; let json_strings = HelloWorldProvider::DATA .iter() @@ -447,12 +447,12 @@ impl HelloWorldFormatter { impl> HelloWorldAttributeFormatter> { - pub fn try_new_with_buffer_provider( - provider: &P1, + pub fn try_new_with_buffer_provider<'data, P1: ?Sized>( + provider: &'data P1, prefs: HelloWorldFormatterPreferences, ) -> Result where - P1: BindLocaleDataProvider, + P1: BindLocaleDataProvider = P>, { let locale = HelloWorldV1::INFO.make_locale(prefs.locale_preferences); let response = provider.bind_locale( From 131cc934262cfd3b1119a4aded5b98953575d600 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 9 Feb 2026 21:11:01 -0800 Subject: [PATCH 7/9] Simply the BlobBoundLocaleDataProvider docs --- provider/blob/src/blob_data_provider.rs | 40 +++++++------------------ 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/provider/blob/src/blob_data_provider.rs b/provider/blob/src/blob_data_provider.rs index 84fcd8d8241..2f9c7c9b55a 100644 --- a/provider/blob/src/blob_data_provider.rs +++ b/provider/blob/src/blob_data_provider.rs @@ -178,13 +178,7 @@ impl IterableDynamicDataProvider for BlobDataProvider { /// /// ``` /// use icu_locale_core::locale; -/// use icu_provider::prelude::*; -/// use icu_provider::buf::DeserializingBufferProvider; -/// use icu_provider::hello_world::HelloWorldV1; -/// use icu_provider::hello_world::HelloWorldFormatterPreferences; -/// use icu_provider::unstable::BindLocale; -/// use icu_provider::unstable::BoundLocaleDataProvider; -/// use icu_provider::unstable::DataAttributesRequest; +/// use icu_provider::hello_world::HelloWorldAttributeFormatter; /// use icu_provider_blob::BlobDataProvider; /// use writeable::assert_writeable_eq; /// @@ -195,23 +189,14 @@ impl IterableDynamicDataProvider for BlobDataProvider { /// let provider = BlobDataProvider::try_new_from_static_blob(HELLO_WORLD_BLOB) /// .expect("Deserialization should succeed"); /// -/// // Bind a specific marker and locale: -/// let locale = HelloWorldV1::INFO.make_locale(HelloWorldFormatterPreferences::from(locale!("en")).locale_preferences); -/// let bound_provider = provider.bind_locale(HelloWorldV1::INFO, DataRequest { -/// metadata: Default::default(), -/// id: DataIdentifierBorrowed::for_locale(&locale) -/// }).unwrap().bound_provider; -/// -/// // Now load a specific attribute: -/// let response = BoundLocaleDataProvider::::load_bound( -/// &DeserializingBufferProvider::new(&bound_provider), -/// DataAttributesRequest { -/// marker_attributes: DataMarkerAttributes::try_from_str("reverse").unwrap(), -/// metadata: Default::default() -/// } +/// // Use it to query a HelloWorld formatter: +/// let formatter = HelloWorldAttributeFormatter::try_new_with_buffer_provider( +/// &provider, +/// locale!("en").into() /// ).unwrap(); /// -/// assert_writeable_eq!(response.payload.get().message, "Olleh Dlrow"); +/// assert_writeable_eq!(formatter.format("reverse").unwrap(), "Olleh Dlrow"); +/// assert_writeable_eq!(formatter.format_for_prefix("rev").unwrap(), "Olleh Dlrow"); /// ``` #[derive(Debug)] pub struct BlobBoundLocaleDataProvider { @@ -241,19 +226,16 @@ impl BindLocaleDataProvider for BlobDataProvider { } impl BoundLocaleDataProvider for BlobBoundLocaleDataProvider { - fn load_bound( - &self, + fn load_bound<'data>( + &'data self, req: DataAttributesRequest, - ) -> Result, DataError> { + ) -> Result, DataError> { let blob = self.data.get(); let payload = blob.load(req)?; let mut metadata = DataResponseMetadata::default(); metadata.buffer_format = Some(BufferFormat::Postcard1); // Note: the checksum is returned by `bind_locale()` instead of `load_bound()` - Ok(BoundLocaleDataResponse { - metadata, - payload: payload, - }) + Ok(BoundLocaleDataResponse { metadata, payload }) } } From c350640cac1c1ca52f4b8f162f7d4dcb11cbe1b5 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 9 Feb 2026 21:11:16 -0800 Subject: [PATCH 8/9] Minor docs fixes --- provider/core/src/hello_world.rs | 39 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/provider/core/src/hello_world.rs b/provider/core/src/hello_world.rs index 1f659310f88..95eced7de5f 100644 --- a/provider/core/src/hello_world.rs +++ b/provider/core/src/hello_world.rs @@ -7,7 +7,7 @@ #![allow(clippy::exhaustive_structs)] // data struct module use crate as icu_provider; -use crate::buf::{DeserializingBufferProvider, DeserializingOwnedBufferProvider}; +use crate::buf::{DeserializingOwnedBufferProvider}; use crate::request::DataAttributesRequest; use crate::unstable::{BindLocaleDataProvider, BoundLocaleDataProvider}; #[cfg(feature = "deserialize_json")] @@ -250,6 +250,10 @@ impl DynamicDataProvider for HelloWorldJsonProvider { } } +/// A data provider returning Hello World strings for attributes in a specific language +/// as JSON blobs. +/// +/// Mostly useful for testing. #[cfg(feature = "deserialize_json")] #[derive(Debug)] pub struct HelloWorldJsonBoundLocaleProvider { @@ -368,22 +372,6 @@ pub struct HelloWorldFormatter { /// /// This type is intended to take the shape of an ICU4X formatter that lazily /// loads data marker attributes. -/// -/// # Examples -/// -/// ``` -/// use icu_locale_core::locale; -/// use icu_provider::hello_world::{HelloWorldAttributeFormatter, HelloWorldProvider}; -/// use writeable::assert_writeable_eq; -/// -/// let fmt = HelloWorldAttributeFormatter::try_new_with_buffer_provider( -/// &HelloWorldProvider.into_json_provider(), -/// locale!("en").into(), -/// ) -/// .expect("locale exists and has attributes"); -/// -/// assert_writeable_eq!(fmt.format("reverse").unwrap(), "Olleh Dlrow"); -/// ``` #[derive(Debug)] pub struct HelloWorldAttributeFormatter> { provider: P, @@ -447,6 +435,23 @@ impl HelloWorldFormatter { impl> HelloWorldAttributeFormatter> { + /// Creates one of these formatters from a buffer provider. + /// + /// # Examples + /// + /// ``` + /// use icu_locale_core::locale; + /// use icu_provider::hello_world::{HelloWorldAttributeFormatter, HelloWorldProvider}; + /// use writeable::assert_writeable_eq; + /// + /// let fmt = HelloWorldAttributeFormatter::try_new_with_buffer_provider( + /// &HelloWorldProvider.into_json_provider(), + /// locale!("en").into(), + /// ) + /// .expect("locale exists and has attributes"); + /// + /// assert_writeable_eq!(fmt.format("reverse").unwrap(), "Olleh Dlrow"); + /// ``` pub fn try_new_with_buffer_provider<'data, P1: ?Sized>( provider: &'data P1, prefs: HelloWorldFormatterPreferences, From 53d624e86ac1fa780f67f60d6182a2bd53c1cd09 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Tue, 10 Feb 2026 14:02:58 -0800 Subject: [PATCH 9/9] CI cleanup --- provider/core/src/buf/serde.rs | 2 +- provider/core/src/data_provider.rs | 24 ++++++++++++++++-------- provider/core/src/hello_world.rs | 16 +++++++++------- provider/core/src/request.rs | 5 ++++- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/provider/core/src/buf/serde.rs b/provider/core/src/buf/serde.rs index 8c245d65a1a..952e1410458 100644 --- a/provider/core/src/buf/serde.rs +++ b/provider/core/src/buf/serde.rs @@ -261,7 +261,7 @@ where fn load_bound( &self, req: crate::request::DataAttributesRequest, - ) -> Result, DataError> { + ) -> Result, DataError> { let buffer_response = self.0.load_bound(req)?; let buffer_format = buffer_response.metadata.buffer_format.ok_or_else(|| { DataErrorKind::Deserialize diff --git a/provider/core/src/data_provider.rs b/provider/core/src/data_provider.rs index d91f001e072..68cc96ad924 100644 --- a/provider/core/src/data_provider.rs +++ b/provider/core/src/data_provider.rs @@ -462,18 +462,28 @@ where } } +/// The result of [`BindLocaleDataProvider::bind_locale`]. #[derive(Debug)] #[allow(clippy::exhaustive_structs)] // exported in an unstable module pub struct BindLocaleResponse { + /// Metadata from the data bind operation. pub metadata: DataResponseMetadata, + /// The provider on which attributes can be loaded. pub bound_provider: T, } +/// A data provider that can be bound to a particular marker and locale. +/// +/// See [`BoundLocaleDataProvider`]. pub trait BindLocaleDataProvider where M: DynamicDataMarker, { + /// Type of the [`BoundLocaleDataProvider`]. type BoundLocaleDataProvider<'data>: BoundLocaleDataProvider + 'data where Self: 'data; + /// Bind this provider to the given marker and locale. + /// + /// This performs a data load for the marker and locale, but not attributes. fn bind_locale<'data>( &'data self, marker: DataMarkerInfo, @@ -481,6 +491,7 @@ where ) -> Result>, DataError>; } +/// Like [`DataResponse`] but for [`BoundLocaleDataProvider`]. #[allow(clippy::exhaustive_structs)] // exported in an unstable module pub struct BoundLocaleDataResponse<'data, M> where @@ -488,7 +499,6 @@ where { /// Metadata about the returned object. pub metadata: DataResponseMetadata, - /// The object itself pub payload: >::Output, } @@ -507,6 +517,8 @@ where } } +/// A data provider that is bound to a particular marker locale. Can be queried for +/// different marker attributes within that marker and locale. pub trait BoundLocaleDataProvider where M: DynamicDataMarker, @@ -518,11 +530,7 @@ where fn load_bound( &self, req: DataAttributesRequest, - ) -> Result, DataError>; - // /// Returns the [`DataMarkerInfo`] that this provider uses for loading data. - // fn bound_marker(&self) -> DataMarkerInfo; - // /// Returns the [`DataLocale`] that this provider uses for loading data. - // fn bound_locale(&self) -> DataLocale; + ) -> Result, DataError>; } impl BoundLocaleDataProvider for &P @@ -534,7 +542,7 @@ where fn load_bound( &self, req: DataAttributesRequest, - ) -> Result, DataError> { + ) -> Result, DataError> { (**self).load_bound(req) } } @@ -718,4 +726,4 @@ mod test { let provider = DataProvider2::from(warehouse); check_v1_v2(&provider); } -} +} \ No newline at end of file diff --git a/provider/core/src/hello_world.rs b/provider/core/src/hello_world.rs index 95eced7de5f..a9203b35bfb 100644 --- a/provider/core/src/hello_world.rs +++ b/provider/core/src/hello_world.rs @@ -273,7 +273,8 @@ impl BindLocaleDataProvider for HelloWorldJsonProvider { .iter() .filter_map(|(l, a, v)| { if req.id.locale.strict_cmp(l.as_bytes()).is_eq() && !a.is_empty() { - let attributes = DataMarkerAttributes::from_str_or_panic(*a); + let attributes = DataMarkerAttributes::from_str_or_panic(a); + #[expect(clippy::unwrap_used)] // HelloWorld::serialize is infallible let json_string = serde_json::to_string(&HelloWorld { message: Cow::Borrowed(v), }) @@ -314,7 +315,6 @@ impl BoundLocaleDataProvider for HelloWorldJsonBoundLocaleProvider buffer_format: Some(icu_provider::buf::BufferFormat::Json), ..Default::default() }, - #[expect(clippy::unwrap_used)] // HelloWorld::serialize is infallible payload: json_string.as_bytes(), }) } @@ -452,12 +452,12 @@ impl> /// /// assert_writeable_eq!(fmt.format("reverse").unwrap(), "Olleh Dlrow"); /// ``` - pub fn try_new_with_buffer_provider<'data, P1: ?Sized>( + pub fn try_new_with_buffer_provider<'data, P1>( provider: &'data P1, prefs: HelloWorldFormatterPreferences, ) -> Result where - P1: BindLocaleDataProvider = P>, + P1: BindLocaleDataProvider = P> + ?Sized, { let locale = HelloWorldV1::INFO.make_locale(prefs.locale_preferences); let response = provider.bind_locale( @@ -496,8 +496,10 @@ impl> HelloWorldAttributeFormatter

{ ) -> Result, DataError> { let marker_attributes = DataMarkerAttributes::try_from_str(attribute) .map_err(|_| DataError::custom("invalid attribute").with_debug_context(attribute))?; - let mut metadata = DataRequestMetadata::default(); - metadata.attributes_prefix_match = attributes_prefix_match; + let metadata = DataRequestMetadata { + attributes_prefix_match, + ..Default::default() + }; let result = self.provider.load_bound(DataAttributesRequest { marker_attributes, metadata, @@ -545,4 +547,4 @@ fn test_iter() { DataMarkerAttributes::from_str_or_panic("reverse"), locale!("en").into() ))); -} +} \ No newline at end of file diff --git a/provider/core/src/request.rs b/provider/core/src/request.rs index cec252b920a..e5ce1541535 100644 --- a/provider/core/src/request.rs +++ b/provider/core/src/request.rs @@ -35,10 +35,13 @@ pub struct DataRequest<'a> { pub metadata: DataRequestMetadata, } +/// A request for data, but with the locale pre-resolved. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #[allow(clippy::exhaustive_structs)] // exported in an unstable module pub struct DataAttributesRequest<'a> { + /// Marker-specific request attributes pub marker_attributes: &'a DataMarkerAttributes, + /// Metadata that may affect the behavior of the data provider. pub metadata: DataRequestMetadata, } @@ -381,4 +384,4 @@ fn test_data_marker_attributes_from_utf8() { let marker = DataMarkerAttributes::try_from_utf8(bytes).unwrap(); assert_eq!(marker.to_string().as_bytes(), bytes); } -} +} \ No newline at end of file