···11+// SPDX-FileCopyrightText: 2025 Łukasz Niemier <#@hauleth.dev>
22+//
33+// SPDX-License-Identifier: EUPL-1.2
44+55+use std::str::FromStr;
66+use std::fmt;
77+88+use serde::{de, Deserialize, Deserializer};
99+1010+pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
1111+where
1212+ T: Deserialize<'de> + FromStr,
1313+ D: Deserializer<'de>,
1414+ <T as FromStr>::Err: fmt::Display,
1515+{
1616+ // This is a Visitor that forwards string types to T's `FromStr` impl and
1717+ // forwards map types to T's `Deserialize` impl. The `PhantomData` is to
1818+ // keep the compiler from complaining about T being an unused generic type
1919+ // parameter. We need T in order to know the Value type for the Visitor
2020+ // impl.
2121+ struct StringOrStruct<T>(std::marker::PhantomData<fn() -> T>);
2222+2323+ impl<'de, T> de::Visitor<'de> for StringOrStruct<T>
2424+ where
2525+ T: Deserialize<'de> + FromStr,
2626+ <T as FromStr>::Err: fmt::Display,
2727+ {
2828+ type Value = T;
2929+3030+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
3131+ formatter.write_str("string or map")
3232+ }
3333+3434+ fn visit_str<E>(self, value: &str) -> Result<T, E>
3535+ where
3636+ E: de::Error,
3737+ {
3838+ FromStr::from_str(value).map_err(de::Error::custom)
3939+ }
4040+4141+ fn visit_map<M>(self, map: M) -> Result<T, M::Error>
4242+ where
4343+ M: de::MapAccess<'de>,
4444+ {
4545+ // `MapAccessDeserializer` is a wrapper that turns a `MapAccess`
4646+ // into a `Deserializer`, allowing it to be used as the input to T's
4747+ // `Deserialize` implementation. T then deserializes itself using
4848+ // the entries from the map visitor.
4949+ Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
5050+ }
5151+ }
5252+5353+ deserializer.deserialize_any(StringOrStruct(Default::default()))
5454+}
5555+5656+pub fn from_str<'de, T, D>(deserializer: D) -> Result<T, D::Error>
5757+where
5858+ T: FromStr,
5959+ D: Deserializer<'de>,
6060+ <T as FromStr>::Err: fmt::Display,
6161+{
6262+ struct DeFromStr<T>(std::marker::PhantomData<T>);
6363+6464+ impl<'de, T> de::Visitor<'de> for DeFromStr<T>
6565+ where
6666+ T: FromStr,
6767+ <T as FromStr>::Err: fmt::Display,
6868+ {
6969+ type Value = T;
7070+7171+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
7272+ formatter.write_str("string or map")
7373+ }
7474+7575+ fn visit_str<E>(self, value: &str) -> Result<T, E>
7676+ where
7777+ E: de::Error,
7878+ {
7979+ FromStr::from_str(value).map_err(de::Error::custom)
8080+ }
8181+ }
8282+8383+ deserializer.deserialize_any(DeFromStr(Default::default()))
8484+}
···4343 _url_ for equivalent entries. In order these would be _github.com_,
4444 _gitlab.com_, _meta.sr.ht_, and _codeberg.org_.
45454646+*tangled*
4747+ Support for _tangled.sh_ keys stored in the ATProto profile. Accepts
4848+ either string, which should be either DID or handle (with or without
4949+ preceding `@` sign), or a structure with `handle` key and optional
5050+ `host` key. Default host is _https://bsky.social_.
5151+4652Example configuration:
47534854```