use std::any::{Any, TypeId};
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{Error, ExtraMap, Result, SerializationError};
type ErasedAny = dyn Any + Send + Sync;
pub trait ProviderOptions: Clone + Send + Sync + 'static {
const NAMESPACE: &'static str;
}
#[derive(Clone, Default)]
pub struct RequestOptions {
typed: HashMap<TypeId, TypedEntry>,
json: ExtraMap,
}
#[derive(Clone)]
struct TypedEntry {
value: Arc<ErasedAny>,
export: Option<Export>,
}
#[derive(Clone, Copy)]
struct Export {
namespace: &'static str,
type_name: &'static str,
to_json: fn(&ErasedAny) -> serde_json::Result<serde_json::Value>,
}
impl RequestOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert<T>(&mut self, value: T)
where
T: ProviderOptions + Serialize,
{
fn to_json<T: Serialize + 'static>(
value: &ErasedAny,
) -> serde_json::Result<serde_json::Value> {
let value = value
.downcast_ref::<T>()
.expect("an entry holds a value of the type it was inserted as");
serde_json::to_value(value)
}
self.typed.insert(
TypeId::of::<T>(),
TypedEntry {
value: Arc::new(value),
export: Some(Export {
namespace: T::NAMESPACE,
type_name: std::any::type_name::<T>(),
to_json: to_json::<T>,
}),
},
);
}
pub fn insert_opaque<T>(&mut self, value: T)
where
T: Clone + Send + Sync + 'static,
{
self.typed.insert(
TypeId::of::<T>(),
TypedEntry {
value: Arc::new(value),
export: None,
},
);
}
pub fn insert_json(&mut self, namespace: impl Into<String>, value: serde_json::Value) {
self.json.insert(namespace.into(), value);
}
#[must_use]
pub fn get<T: 'static>(&self) -> Option<&T> {
self.typed.get(&TypeId::of::<T>())?.value.downcast_ref()
}
pub fn get_mut<T: Clone + Send + Sync + 'static>(&mut self) -> Option<&mut T> {
let entry = self.typed.get_mut(&TypeId::of::<T>())?;
if Arc::get_mut(&mut entry.value).is_none() {
let cloned: T = entry.value.downcast_ref::<T>()?.clone();
entry.value = Arc::new(cloned);
}
Arc::get_mut(&mut entry.value)?.downcast_mut()
}
pub fn remove<T: Clone + Send + Sync + 'static>(&mut self) -> Option<T> {
let entry = self.typed.remove(&TypeId::of::<T>())?;
entry.value.downcast_ref::<T>().cloned()
}
#[must_use]
pub fn contains<T: 'static>(&self) -> bool {
self.typed.contains_key(&TypeId::of::<T>())
}
#[must_use]
pub fn json(&self, namespace: &str) -> Option<&serde_json::Value> {
self.json.get(namespace)
}
pub fn remove_json(&mut self, namespace: &str) -> Option<serde_json::Value> {
self.json.remove(namespace)
}
pub fn resolve<T>(&self) -> Result<Option<Cow<'_, T>>>
where
T: ProviderOptions + DeserializeOwned,
{
self.check_namespace::<T>()?;
if let Some(value) = self.get::<T>() {
return Ok(Some(Cow::Borrowed(value)));
}
match self.json.get(T::NAMESPACE) {
None => Ok(None),
Some(value) => serde_json::from_value(value.clone())
.map(|value| Some(Cow::Owned(value)))
.map_err(|err| {
Error::invalid_request(format!("invalid `{}` options: {err}", T::NAMESPACE))
}),
}
}
pub fn resolve_or_default<T>(&self) -> Result<Cow<'_, T>>
where
T: ProviderOptions + DeserializeOwned + Default,
{
Ok(self
.resolve::<T>()?
.unwrap_or_else(|| Cow::Owned(T::default())))
}
pub fn resolve_mut<T>(&mut self) -> Result<&mut T>
where
T: ProviderOptions + Serialize + DeserializeOwned + Default,
{
self.check_namespace::<T>()?;
if !self.contains::<T>() {
let value = match self.resolve::<T>()? {
Some(value) => value.into_owned(),
None => T::default(),
};
self.json.remove(T::NAMESPACE);
self.insert(value);
}
Ok(self
.get_mut::<T>()
.expect("the typed value was just found or inserted"))
}
pub fn to_json(&self) -> std::result::Result<ExtraMap, SerializationError> {
let mut out = self.json.clone();
for entry in self.typed.values() {
if let Some(export) = entry.export {
let value = (export.to_json)(entry.value.as_ref()).map_err(|err| {
let mut err = SerializationError::from(err);
err.prefix(&format!("`{}` options", export.namespace));
err
})?;
out.insert(export.namespace.to_owned(), value);
}
}
Ok(out)
}
#[must_use]
pub fn from_json(json: ExtraMap) -> Self {
Self {
typed: HashMap::new(),
json,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.typed.is_empty() && self.json.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.typed.len() + self.json.len()
}
fn check_namespace<T: ProviderOptions>(&self) -> Result<()> {
let expected = TypeId::of::<T>();
let clash = self.typed.iter().find_map(|(id, entry)| {
entry
.export
.filter(|export| *id != expected && export.namespace == T::NAMESPACE)
});
match clash {
None => Ok(()),
Some(export) => Err(Error::invalid_request(format!(
"`{}` options were attached as `{}`, but this call reads them as `{}`; \
attach one options type per namespace",
T::NAMESPACE,
export.type_name,
std::any::type_name::<T>(),
))),
}
}
}
impl fmt::Debug for RequestOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RequestOptions")
.field("typed", &self.typed.len())
.field("json", &self.json)
.finish()
}
}
impl PartialEq for RequestOptions {
fn eq(&self, other: &Self) -> bool {
match (self.to_json(), other.to_json()) {
(Ok(this), Ok(other)) => this == other,
_ => false,
}
}
}
impl Serialize for RequestOptions {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
self.to_json()
.map_err(serde::ser::Error::custom)?
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for RequestOptions {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
ExtraMap::deserialize(deserializer).map(Self::from_json)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct DemoOptions {
tier: Option<String>,
effort: Option<String>,
}
impl ProviderOptions for DemoOptions {
const NAMESPACE: &'static str = "demo";
}
#[derive(Clone)]
struct Handle(#[allow(dead_code)] u8);
#[test]
fn typed_value_resolves() {
let mut options = RequestOptions::new();
options.insert(DemoOptions {
tier: Some("flex".into()),
..Default::default()
});
let resolved = options.resolve::<DemoOptions>().unwrap().unwrap();
assert_eq!(resolved.tier.as_deref(), Some("flex"));
}
#[test]
fn resolve_mut_starts_from_what_the_caller_set() {
let mut options = RequestOptions::new();
options.insert(DemoOptions {
tier: Some("flex".into()),
..Default::default()
});
options.resolve_mut::<DemoOptions>().unwrap().effort = Some("low".into());
let typed = options.get::<DemoOptions>().unwrap();
assert_eq!(
(typed.tier.as_deref(), typed.effort.as_deref()),
(Some("flex"), Some("low"))
);
let mut options = RequestOptions::new();
options.insert_json("demo", json!({"tier": "flex"}));
options.resolve_mut::<DemoOptions>().unwrap().effort = Some("low".into());
assert_eq!(options.json("demo"), None);
assert_eq!(
options.to_json().unwrap()["demo"],
json!({"tier": "flex", "effort": "low"})
);
let mut options = RequestOptions::new();
options.resolve_mut::<DemoOptions>().unwrap().effort = Some("low".into());
assert_eq!(options.len(), 1);
assert_eq!(options.get::<DemoOptions>().unwrap().tier, None);
}
#[test]
fn resolve_mut_leaves_invalid_json_in_place() {
let mut options = RequestOptions::new();
options.insert_json("demo", json!({"unknown": true}));
let err = options.resolve_mut::<DemoOptions>().unwrap_err();
assert!(matches!(err, Error::InvalidRequest { .. }), "{err:?}");
assert!(options.json("demo").is_some());
assert!(!options.contains::<DemoOptions>());
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
struct OtherDemoOptions {
tier: Option<String>,
}
impl ProviderOptions for OtherDemoOptions {
const NAMESPACE: &'static str = "demo";
}
#[test]
fn two_types_in_one_namespace_fail_to_resolve() {
let mut options = RequestOptions::new();
options.insert(OtherDemoOptions {
tier: Some("flex".into()),
});
let err = options.resolve::<DemoOptions>().unwrap_err();
assert!(matches!(err, Error::InvalidRequest { .. }), "{err:?}");
let message = err.to_string();
assert!(message.contains("`demo` options"), "{message}");
assert!(message.contains("OtherDemoOptions"), "{message}");
assert!(message.contains("DemoOptions`"), "{message}");
options.insert(DemoOptions::default());
assert!(matches!(
options.resolve::<DemoOptions>(),
Err(Error::InvalidRequest { .. })
));
assert!(matches!(
options.resolve_or_default::<DemoOptions>(),
Err(Error::InvalidRequest { .. })
));
let mut options = RequestOptions::new();
options.insert(OtherDemoOptions::default());
options.insert_json("demo", json!({"tier": "flex"}));
let err = options.resolve_mut::<DemoOptions>().unwrap_err();
assert!(matches!(err, Error::InvalidRequest { .. }), "{err:?}");
assert!(!options.contains::<DemoOptions>());
assert!(options.json("demo").is_some());
let mut options = RequestOptions::new();
options.insert_opaque(Handle(1));
options.insert_json("demo", json!({"tier": "flex"}));
assert!(options.resolve::<DemoOptions>().unwrap().is_some());
}
#[test]
fn json_form_resolves_into_type() {
let mut options = RequestOptions::new();
options.insert_json("demo", json!({"effort": "high"}));
let resolved = options.resolve::<DemoOptions>().unwrap().unwrap();
assert_eq!(resolved.effort.as_deref(), Some("high"));
}
#[test]
fn unknown_json_field_is_invalid_request() {
let mut options = RequestOptions::new();
options.insert_json("demo", json!({"efort": "high"}));
let err = options.resolve::<DemoOptions>().unwrap_err();
assert!(matches!(err, Error::InvalidRequest { .. }), "{err:?}");
}
#[test]
fn typed_wins_over_json() {
let mut options = RequestOptions::new();
options.insert_json("demo", json!({"tier": "default"}));
options.insert(DemoOptions {
tier: Some("flex".into()),
..Default::default()
});
assert_eq!(
options
.resolve::<DemoOptions>()
.unwrap()
.unwrap()
.tier
.as_deref(),
Some("flex")
);
assert_eq!(options.to_json().unwrap()["demo"]["tier"], "flex");
}
#[test]
fn export_round_trip_is_lossless_for_data() {
let mut options = RequestOptions::new();
options.insert(DemoOptions {
tier: Some("flex".into()),
..Default::default()
});
options.insert_json("other", json!({"top_k": 5}));
options.insert_opaque(Handle(1));
let restored: RequestOptions =
serde_json::from_value(serde_json::to_value(&options).unwrap()).unwrap();
assert_eq!(restored, options);
assert!(restored.get::<Handle>().is_none());
assert_eq!(
restored
.resolve::<DemoOptions>()
.unwrap()
.unwrap()
.tier
.as_deref(),
Some("flex")
);
assert_eq!(restored.json("other"), Some(&json!({"top_k": 5})));
}
#[test]
fn a_typed_value_that_fails_to_serialize_is_an_error() {
#[derive(Debug, Clone, Default, Serialize)]
struct Keyed {
by_pair: HashMap<(u8, u8), u8>,
}
impl ProviderOptions for Keyed {
const NAMESPACE: &'static str = "keyed";
}
let mut failing = RequestOptions::new();
failing.insert(Keyed {
by_pair: HashMap::from([((1, 2), 3)]),
});
let err = failing.to_json().unwrap_err();
assert!(err.to_string().starts_with("`keyed` options: "), "{err}");
assert!(err.get_ref().downcast_ref::<serde_json::Error>().is_some());
let err = serde_json::to_value(&failing).unwrap_err();
assert!(err.to_string().contains("`keyed` options"), "{err}");
assert_ne!(failing, RequestOptions::new());
assert_ne!(RequestOptions::new(), failing);
assert_ne!(failing, failing.clone());
let mut working = RequestOptions::new();
working.insert(Keyed::default());
assert_eq!(working, working.clone());
let mut opaque = working.clone();
opaque.insert_opaque(Handle(1));
assert_eq!(opaque, working, "opaque values stay out of equality");
}
#[test]
fn get_mut_and_remove() {
let mut options = RequestOptions::new();
options.insert(DemoOptions::default());
let cloned = options.clone();
options.get_mut::<DemoOptions>().unwrap().tier = Some("x".into());
assert!(cloned.get::<DemoOptions>().unwrap().tier.is_none());
assert_eq!(
options.remove::<DemoOptions>().unwrap().tier.as_deref(),
Some("x")
);
assert!(!options.contains::<DemoOptions>());
}
}