From 12802ce542bbd349ec9e86bcd32be825b970138d Mon Sep 17 00:00:00 2001 From: Deukhoofd Date: Sat, 22 Jul 2023 12:23:33 +0200 Subject: [PATCH] Support for serializing and deserializing a Pokemon --- Cargo.toml | 4 +- src/dynamic_data/models/learned_move.rs | 50 +++++- src/dynamic_data/models/mod.rs | 3 + src/dynamic_data/models/pokemon.rs | 145 +++++++++++++++++- src/dynamic_data/models/serialization/mod.rs | 4 + .../serialization/serialized_pokemon.rs | 135 ++++++++++++++++ .../script_handling/script_set.rs | 10 ++ src/ffi/dynamic_data/models/pokemon.rs | 48 ++++++ src/ffi/mod.rs | 2 +- src/lib.rs | 14 ++ src/static_data/libraries/type_library.rs | 3 +- src/static_data/species_data/ability.rs | 1 + src/static_data/species_data/gender.rs | 1 + src/static_data/statistic_set.rs | 23 +++ src/utils/string_key.rs | 42 +++++ 15 files changed, 473 insertions(+), 12 deletions(-) create mode 100644 src/dynamic_data/models/serialization/mod.rs create mode 100644 src/dynamic_data/models/serialization/serialized_pokemon.rs diff --git a/Cargo.toml b/Cargo.toml index 002e19e..2571efa 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ path = "src/lib.rs" [features] ffi = [] -serde = ["dep:serde"] +serde = ["dep:serde", "dep:serde-xml-rs", "atomig/serde"] wasm = ["dep:wasmer"] default = ["serde", "wasm", "ffi"] @@ -53,6 +53,8 @@ hashbrown = "0.13.1" indexmap = "1.8.2" parking_lot = "0.12.1" serde = { version = "1.0.137", optional = true, features = ["derive"] } +serde_repr = "0.1.12" +serde-xml-rs = { version = "0.6.0", optional = true } wasmer = { version = "3.3.0", optional = true, default-features = false, features = ["sys", "wat", "llvm"] } uuid = "1.2.2" paste = { version = "1.0.8" } diff --git a/src/dynamic_data/models/learned_move.rs b/src/dynamic_data/models/learned_move.rs index dd8f49c..3f3c165 100755 --- a/src/dynamic_data/models/learned_move.rs +++ b/src/dynamic_data/models/learned_move.rs @@ -1,3 +1,5 @@ +use crate::dynamic_data::DynamicLibrary; +use crate::PkmnError; use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::Arc; @@ -10,7 +12,7 @@ pub struct LearnedMove { /// The immutable move information of the move. move_data: Arc, /// The maximal power points for this move. - max_pp: u8, + max_pp_modification: u8, /// The amount of remaining power points. If this is 0, we can not use the move anymore. remaining_pp: AtomicU8, /// The way the move was learned. @@ -20,6 +22,7 @@ pub struct LearnedMove { /// The different ways a move can be learned. #[derive(Copy, Clone, Debug, Default)] #[repr(u8)] +#[cfg_attr(feature = "serde", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr))] pub enum MoveLearnMethod { /// We do not know the learn method. #[default] @@ -31,11 +34,11 @@ pub enum MoveLearnMethod { impl LearnedMove { /// Instantiate a new learned move. pub fn new(move_data: Arc, learn_method: MoveLearnMethod) -> Self { - let max_pp = move_data.base_usages(); + let base_usages = move_data.base_usages(); Self { move_data, - max_pp, - remaining_pp: AtomicU8::new(max_pp), + max_pp_modification: 0, + remaining_pp: AtomicU8::new(base_usages), learn_method, } } @@ -46,8 +49,15 @@ impl LearnedMove { } /// The maximal power points for this move. pub fn max_pp(&self) -> u8 { - self.max_pp + self.move_data.base_usages() + self.max_pp_modification } + + /// The amount by which the maximal power points have been modified for this move. + /// This could for example be due to PP Ups. + pub fn max_pp_modification(&self) -> u8 { + self.max_pp_modification + } + /// The amount of remaining power points. If this is 0, we can not use the move anymore. pub fn remaining_pp(&self) -> u8 { self.remaining_pp.load(Ordering::Relaxed) @@ -72,20 +82,44 @@ impl LearnedMove { /// Set the remaining PP to the max amount of PP. pub fn restore_all_uses(&self) { - self.remaining_pp.store(self.max_pp, Ordering::SeqCst); + self.remaining_pp.store(self.max_pp(), Ordering::SeqCst); } /// Restore the remaining PP by a certain amount. Will prevent it from going above max PP. pub fn restore_uses(&self, mut uses: u8) { self.remaining_pp .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| { - if x + uses > self.max_pp { - uses = self.max_pp - x; + let max = self.max_pp(); + if x + uses > max { + uses = max - x; } Some(x + uses) }) .ok(); } + + /// Deserialize a learned move from a serialized learned move. + pub fn deserialize( + library: &Arc, + value: &super::serialization::SerializedLearnedMove, + ) -> anyhow_ext::Result { + Ok(Self { + move_data: { + let move_data = + library + .static_data() + .moves() + .get(&value.move_data) + .ok_or_else(|| PkmnError::InvalidMoveName { + move_name: value.move_data.clone(), + })?; + Arc::clone(&move_data) + }, + max_pp_modification: value.max_pp_modification, + remaining_pp: AtomicU8::new(value.remaining_pp), + learn_method: value.learn_method, + }) + } } #[cfg(test)] diff --git a/src/dynamic_data/models/mod.rs b/src/dynamic_data/models/mod.rs index 1eae8d2..e414ce3 100755 --- a/src/dynamic_data/models/mod.rs +++ b/src/dynamic_data/models/mod.rs @@ -35,3 +35,6 @@ mod pokemon; mod pokemon_builder; /// Data for a group of Pokemon belonging to a trainer. mod pokemon_party; +#[cfg(feature = "serde")] +/// Data for serialization of models +pub(crate) mod serialization; diff --git a/src/dynamic_data/models/pokemon.rs b/src/dynamic_data/models/pokemon.rs index 88c3467..0dde446 100755 --- a/src/dynamic_data/models/pokemon.rs +++ b/src/dynamic_data/models/pokemon.rs @@ -188,7 +188,7 @@ impl Pokemon { ability_index: ability, override_ability: None, battle_data: RwLock::new(None), - moves: RwLock::new([None, None, None, None]), + moves: RwLock::new(Default::default()), allowed_experience: false, types: RwLock::new(form.types().to_vec()), is_egg: false, @@ -222,6 +222,10 @@ impl Pokemon { pub fn form(&self) -> Arc { self.data.form.read().clone() } + /// Whether or not the Pokemon is showing as a different species than it actually is. + pub fn has_different_display_species(&self) -> bool { + self.data.display_species.is_some() + } /// The species that should be displayed to the user. This handles stuff like the Illusion ability. pub fn display_species(&self) -> Arc { if let Some(v) = &self.data.display_species { @@ -230,6 +234,10 @@ impl Pokemon { self.species() } } + /// Whether or not the Pokemon is showing as a different form than it actually is. + pub fn has_different_display_form(&self) -> bool { + self.data.display_form.is_some() + } /// The form that should be displayed to the user. This handles stuff like the Illusion ability. pub fn display_form(&self) -> Arc { if let Some(v) = &self.data.display_form { @@ -351,6 +359,11 @@ impl Pokemon { &self.data.stat_boost } + /// Whether or not this Pokemon is still an egg, and therefore cannot battle. + pub fn is_egg(&self) -> bool { + self.data.is_egg + } + /// The stats of the Pokemon including the stat boosts pub fn boosted_stats(&self) -> &Arc> { &self.data.boosted_stats @@ -833,6 +846,136 @@ impl Pokemon { self.recalculate_flat_stats() } + /// Converts the Pokemon into a serializable form. + #[cfg(feature = "serde")] + pub fn serialize(&self) -> Result { + self.into() + } + + /// Deserializes a Pokemon from a serializable form. + #[cfg(feature = "serde")] + pub fn deserialize( + library: &Arc, + value: &super::serialization::SerializedPokemon, + ) -> Result { + let species = library + .static_data() + .species() + .get(&value.species) + .ok_or(PkmnError::InvalidSpeciesName { + species: value.species.clone(), + })?; + let form = species.get_form(&value.form).ok_or(PkmnError::InvalidFormName { + species: value.species.clone(), + form: value.form.clone(), + })?; + let display_species = if let Some(v) = &value.display_species { + Some( + library + .static_data() + .species() + .get(v) + .ok_or(PkmnError::InvalidSpeciesName { species: v.clone() })?, + ) + } else { + None + }; + let display_form = if let Some(v) = &value.display_form { + if let Some(display_species) = &display_species { + Some(display_species.get_form(v).ok_or(PkmnError::InvalidFormName { + species: display_species.name().clone(), + form: v.clone(), + })?) + } else { + None + } + } else { + None + }; + + Ok(Self { + data: Arc::new(PokemonData { + library: library.clone(), + species: RwLock::new(species), + form: RwLock::new(form), + display_species, + display_form, + level: Atomic::new(value.level), + experience: AtomicU32::new(value.experience), + personality_value: value.personality_value, + gender: RwLock::new(value.gender), + coloring: value.coloring, + held_item: RwLock::new({ + if let Some(v) = &value.held_item { + Some( + library + .static_data() + .items() + .get(v) + .ok_or(PkmnError::InvalidItemName { item: v.clone() })?, + ) + } else { + None + } + }), + current_health: AtomicU32::new(value.current_health), + weight: Atomic::new(value.weight), + height: Atomic::new(value.height), + flat_stats: Arc::new(Default::default()), + stat_boost: Arc::new(value.stat_boosts.clone()), + boosted_stats: Arc::new(Default::default()), + individual_values: Arc::new(value.individual_values.clone()), + effort_values: Arc::new(value.effort_values.clone()), + nature: { + library + .static_data() + .natures() + .get_nature(&value.nature) + .ok_or(PkmnError::InvalidNatureName { + nature: value.nature.clone(), + })? + }, + nickname: value.nickname.clone(), + ability_index: value.ability_index, + override_ability: { + if let Some(v) = &value.override_ability { + Some( + library + .static_data() + .abilities() + .get(v) + .ok_or(PkmnError::InvalidAbilityName { ability: v.clone() })?, + ) + } else { + None + } + }, + battle_data: Default::default(), + moves: { + let mut moves: [Option>; MAX_MOVES] = Default::default(); + for (i, v) in value.moves.iter().enumerate() { + if i >= MAX_MOVES { + break; + } + moves + .get_mut(i) + .replace(&mut Some(Arc::new(LearnedMove::deserialize(library, v)?))); + } + RwLock::new(moves) + }, + allowed_experience: value.allowed_experience, + types: RwLock::new(value.types.clone()), + is_egg: value.is_egg, + is_caught: false, + held_item_trigger_script: Default::default(), + ability_script: Default::default(), + status_script: Default::default(), + volatile: Arc::new(Default::default()), + script_source_data: Default::default(), + }), + }) + } + /// Take a weak reference to the Pokemon. pub fn weak(&self) -> WeakPokemonReference { WeakPokemonReference { diff --git a/src/dynamic_data/models/serialization/mod.rs b/src/dynamic_data/models/serialization/mod.rs new file mode 100644 index 0000000..f106966 --- /dev/null +++ b/src/dynamic_data/models/serialization/mod.rs @@ -0,0 +1,4 @@ +/// Serialization format for Pokemon data. +mod serialized_pokemon; + +pub use serialized_pokemon::*; diff --git a/src/dynamic_data/models/serialization/serialized_pokemon.rs b/src/dynamic_data/models/serialization/serialized_pokemon.rs new file mode 100644 index 0000000..6226f33 --- /dev/null +++ b/src/dynamic_data/models/serialization/serialized_pokemon.rs @@ -0,0 +1,135 @@ +use crate::defines::LevelInt; +use crate::dynamic_data::{MoveLearnMethod, Pokemon, VolatileScriptsOwner}; +use crate::static_data::{AbilityIndex, ClampedStatisticSet, Gender, TypeIdentifier}; +use crate::StringKey; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; + +/// The serializable version of a [Pokemon](Pokemon). +#[derive(Serialize, Deserialize)] +pub struct SerializedPokemon { + /// The name of the species of the Pokemon. + pub species: StringKey, + /// The name of the form of the Pokemon. + pub form: StringKey, + /// The name of the species of the Pokemon that is displayed, if overridden. + pub display_species: Option, + /// The name of the form of the Pokemon that is displayed, if overridden. + pub display_form: Option, + /// The level of the Pokemon. + pub level: LevelInt, + /// The experience of the Pokemon. + pub experience: u32, + /// The personality value of the Pokemon. + pub personality_value: u32, + /// The gender of the Pokemon. + pub gender: Gender, + /// The coloring of the Pokemon. + pub coloring: u8, + /// The held item of the Pokemon. + pub held_item: Option, + /// The current health of the Pokemon. + pub current_health: u32, + /// The weight of the Pokemon. + pub weight: f32, + /// The height of the Pokemon. + pub height: f32, + /// The stat boosts of the Pokemon. + pub stat_boosts: ClampedStatisticSet, + /// The individual values of the Pokemon. + pub individual_values: ClampedStatisticSet, + /// The effort values of the Pokemon. + pub effort_values: ClampedStatisticSet, + /// The nature of the Pokemon. + pub nature: StringKey, + /// The nickname of the Pokemon. + pub nickname: Option, + /// The ability index of the Pokemon. + pub ability_index: AbilityIndex, + /// The ability that is currently active on the Pokemon, if overridden. + pub override_ability: Option, + /// The moves of the Pokemon. + pub moves: Vec, + /// Whether the Pokemon is allowed to gain experience. + pub allowed_experience: bool, + /// The types of the Pokemon. + pub types: Vec, + /// Whether the Pokemon is an egg. + pub is_egg: bool, + /// The volatile scripts of the Pokemon. + pub volatile_scripts: Vec, +} + +/// The serializable version of a [LearnedMove](LearnedMove). +#[derive(Serialize, Deserialize)] +pub struct SerializedLearnedMove { + /// The name of the move. + pub move_data: StringKey, + /// A potential offset from the normal maximal power points. + pub max_pp_modification: u8, + /// The amount of remaining power points. If this is 0, we can not use the move anymore. + pub remaining_pp: u8, + /// The way the move was learned. + pub learn_method: MoveLearnMethod, +} + +#[allow(clippy::from_over_into)] // From doesn't allow for Result. +impl Into> for &Pokemon { + fn into(self) -> anyhow::Result { + Ok(SerializedPokemon { + species: self.species().name().clone(), + form: self.form().name().clone(), + display_species: if self.has_different_display_species() { + Some(self.display_species().name().clone()) + } else { + None + }, + display_form: if self.has_different_display_form() { + Some(self.display_form().name().clone()) + } else { + None + }, + level: self.level(), + experience: self.experience(), + personality_value: self.personality_value(), + gender: self.gender(), + coloring: self.coloring(), + held_item: { + let held_item = self.held_item().read(); + held_item.as_ref().map(|held_item| held_item.name().clone()) + }, + current_health: self.current_health(), + weight: self.weight(), + height: self.height(), + stat_boosts: self.stat_boosts().deref().deref().clone(), + individual_values: self.individual_values().deref().deref().clone(), + effort_values: self.effort_values().deref().deref().clone(), + nature: self.library().static_data().natures().get_nature_name(self.nature())?, + nickname: self.nickname().clone(), + ability_index: *self.real_ability(), + override_ability: { + if self.is_ability_overriden() { + Some(self.active_ability()?.name().clone()) + } else { + None + } + }, + moves: self + .learned_moves() + .read() + .iter() + .flatten() + .map(|lm| SerializedLearnedMove { + move_data: lm.move_data().name().clone(), + max_pp_modification: lm.max_pp_modification(), + remaining_pp: lm.remaining_pp(), + learn_method: lm.learn_method(), + }) + .collect(), + allowed_experience: self.allowed_experience_gain(), + types: self.types().to_vec(), + is_egg: self.is_egg(), + volatile_scripts: self.volatile_scripts().get_script_names(), + }) + } +} diff --git a/src/dynamic_data/script_handling/script_set.rs b/src/dynamic_data/script_handling/script_set.rs index ac2ab7a..fe1e301 100755 --- a/src/dynamic_data/script_handling/script_set.rs +++ b/src/dynamic_data/script_handling/script_set.rs @@ -154,4 +154,14 @@ impl ScriptSet { } v } + + /// Gets the names of all scripts in the set. + pub fn get_script_names(&self) -> Vec { + let s = self.scripts.read(); + let mut v = Vec::with_capacity(s.deref().len()); + for script in s.deref().keys() { + v.push(script.clone()); + } + v + } } diff --git a/src/ffi/dynamic_data/models/pokemon.rs b/src/ffi/dynamic_data/models/pokemon.rs index 54dd094..69b2a5a 100644 --- a/src/ffi/dynamic_data/models/pokemon.rs +++ b/src/ffi/dynamic_data/models/pokemon.rs @@ -450,3 +450,51 @@ extern "C" fn pokemon_learn_move( extern "C" fn pokemon_clear_status(handle: FFIHandle) { handle.from_ffi_handle().clear_status() } + +/// Returns a serialized version of the Pokemon as xml. +#[cfg(feature = "serde")] +#[no_mangle] +extern "C" fn pokemon_serialize_as_xml(handle: FFIHandle) -> FFIResult { + let serialized = match handle.from_ffi_handle().serialize() { + Ok(v) => v, + Err(err) => return FFIResult::err(err), + }; + let serialized = + match serde_xml_rs::to_string(&serialized).map_err(|err| anyhow!("Could not serialize Pokemon: {}", err)) { + Ok(v) => v, + Err(err) => return FFIResult::err(err), + }; + let cstring = match CString::new(serialized) { + Ok(v) => v, + Err(err) => return FFIResult::err(anyhow!("Could not convert serialized Pokemon to CString: {}", err)), + }; + FFIResult::ok(OwnedPtrString(cstring.into_raw())) +} + +/// Converts an XML representation of a Pokemon to a Pokemon. +#[cfg(feature = "serde")] +#[no_mangle] +extern "C" fn pokemon_deserialize_from_xml( + library: FFIHandle>, + str: OwnedPtrString, +) -> FFIResult> { + let str = unsafe { + match CString::from_raw(str.0).to_str() { + Ok(v) => v.to_string(), + Err(err) => return FFIResult::err(anyhow!("Could not read CString: {}", err)), + } + }; + let deserialized = match serde_xml_rs::from_str::(&str) + .map_err(|err| anyhow!("Could not deserialize Pokemon: {}", err)) + { + Ok(v) => v, + Err(err) => return FFIResult::err(err), + }; + + let library = library.from_ffi_handle(); + let pokemon = match Pokemon::deserialize(&library, &deserialized) { + Ok(v) => v, + Err(err) => return FFIResult::err(err), + }; + FFIResult::ok(FFIHandle::get_handle(pokemon.into())) +} diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index b26ed15..391f570 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -8,7 +8,7 @@ mod static_data; pub(self) use ffi_handle::*; /// Helper type for clearer functions. -#[repr(C)] +#[repr(transparent)] #[derive(Debug, Copy, Clone)] struct OwnedPtrString(*mut c_char); diff --git a/src/lib.rs b/src/lib.rs index c7045d0..3e4b9c3 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,6 +97,12 @@ pub enum PkmnError { /// The move that was requested move_name: StringKey, }, + /// Unable to get an item + #[error("Unable to get item {item}")] + InvalidItemName { + /// The item that was requested + item: StringKey, + }, /// Unable to get nature #[error("Unable to get nature {nature}")] InvalidNatureName { @@ -109,6 +115,14 @@ pub enum PkmnError { /// The species that was requested species: StringKey, }, + /// Unable to get form + #[error("Unable to get form {form} for species {species}")] + InvalidFormName { + /// The species for which the form was requested + species: StringKey, + /// The form that was requested + form: StringKey, + }, /// Unable to get type #[error("Unable to get type {type_id}")] InvalidTypeIdentifier { diff --git a/src/static_data/libraries/type_library.rs b/src/static_data/libraries/type_library.rs index 477ab47..c29bd43 100755 --- a/src/static_data/libraries/type_library.rs +++ b/src/static_data/libraries/type_library.rs @@ -9,7 +9,8 @@ use crate::{PkmnError, StringKey}; /// A unique key that can be used to store a reference to a type. Opaque reference to a byte /// internally. #[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash, Atom)] -#[repr(C)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[repr(transparent)] pub struct TypeIdentifier { /// The unique internal value. val: u8, diff --git a/src/static_data/species_data/ability.rs b/src/static_data/species_data/ability.rs index 726f3ff..15daa1a 100755 --- a/src/static_data/species_data/ability.rs +++ b/src/static_data/species_data/ability.rs @@ -53,6 +53,7 @@ impl Ability for AbilityImpl { /// An ability index allows us to find an ability on a form. It combines a bool for whether the /// ability is hidden or not, and then an index of the ability. #[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct AbilityIndex { /// Whether or not the ability we're referring to is a hidden ability. diff --git a/src/static_data/species_data/gender.rs b/src/static_data/species_data/gender.rs index ca3cdaa..9a8e6e1 100755 --- a/src/static_data/species_data/gender.rs +++ b/src/static_data/species_data/gender.rs @@ -3,6 +3,7 @@ /// Required for standard pokemon functions, but somewhat controversial nowadays. Consider adding a feature /// that allows for a more progressive gender system for those that want it? #[derive(Eq, PartialEq, Copy, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr))] #[repr(u8)] pub enum Gender { /// The Pokemon has no gender. diff --git a/src/static_data/statistic_set.rs b/src/static_data/statistic_set.rs index 30c7a16..8b05f02 100755 --- a/src/static_data/statistic_set.rs +++ b/src/static_data/statistic_set.rs @@ -205,6 +205,7 @@ where /// A clamped statistic set holds the 6 normal stats for a Pokemon, but ensures it always remains /// between two values (inclusive on the two values). #[derive(Default, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ClampedStatisticSet where T: PrimitiveAtom, @@ -376,6 +377,28 @@ where } } +impl Clone for ClampedStatisticSet +where + T: PrimitiveAtom, + T: Atom, + T: PrimitiveAtomInteger, + ::Repr: PrimitiveAtomInteger, + T: AtomInteger, + T: NumCast, + T: PrimInt, +{ + fn clone(&self) -> Self { + Self { + hp: Atomic::::new(self.hp()), + attack: Atomic::::new(self.attack()), + defense: Atomic::::new(self.defense()), + special_attack: Atomic::::new(self.special_attack()), + special_defense: Atomic::::new(self.special_defense()), + speed: Atomic::::new(self.speed()), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/utils/string_key.rs b/src/utils/string_key.rs index 4ced669..e53fcd4 100755 --- a/src/utils/string_key.rs +++ b/src/utils/string_key.rs @@ -1,6 +1,7 @@ use arcstr::ArcStr; use hashbrown::HashMap; use parking_lot::RwLock; +use serde::{Deserializer, Serializer}; use std::borrow::Borrow; use std::ffi::CStr; use std::fmt::{Display, Formatter}; @@ -170,6 +171,47 @@ const CRC_TABLE: &[u32] = &[ 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D, ]; +#[cfg(feature = "serde")] +impl serde::Serialize for StringKey { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.str()) + } +} + +/// A visitor for deserializing a StringKey. +#[cfg(feature = "serde")] +struct StringKeyVisitor; + +#[cfg(feature = "serde")] +impl<'de> serde::de::Visitor<'de> for StringKeyVisitor { + type Value = String; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(String::from(v)) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for StringKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let str = deserializer.deserialize_string(StringKeyVisitor)?; + Ok(StringKey::new(&str)) + } +} + #[cfg(test)] mod tests { extern crate test;