Skip to content

Commit 6460661

Browse files
authored
Add support for types that can deserialize from str (#543)
1 parent 8a000bf commit 6460661

4 files changed

Lines changed: 95 additions & 9 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎v-api-param/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ json = ["dep:serde_json"]
1717
toml = ["dep:toml"]
1818

1919
[dev-dependencies]
20+
newtype-uuid = { workspace = true }
2021
tempfile = { workspace = true }
2122
toml = { workspace = true }

‎v-api-param/src/lib.rs‎

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
//! - [`SerializedParam`] resolves to an arbitrary `T`, deserializing the file
1414
//! contents using a [`ParamFormat`] marker (for example [`Json`] or
1515
//! [`Toml`]). This is useful when the file contents are themselves a
16-
//! structured document whose shape and size are only known at runtime.
16+
//! structured document whose shape and size are only known at runtime. The
17+
//! [`Raw`] marker covers the simpler case of a file holding a single bare
18+
//! value such as a uuid.
1719
//!
1820
//! # TOML Usage
1921
//!
@@ -239,6 +241,8 @@ pub trait ParamFormat {
239241
#[derive(Debug, Error)]
240242
#[non_exhaustive]
241243
pub enum FormatError {
244+
#[error("Failed to deserialize as a raw value")]
245+
Raw(#[from] serde::de::value::Error),
242246
#[cfg(feature = "json")]
243247
#[error("Failed to deserialize as JSON")]
244248
Json(#[from] serde_json::Error),
@@ -247,6 +251,20 @@ pub enum FormatError {
247251
Toml(#[from] toml::de::Error),
248252
}
249253

254+
#[derive(Debug, Clone, Copy)]
255+
pub struct Raw;
256+
257+
impl ParamFormat for Raw {
258+
fn deserialize_str<T: DeserializeOwned>(content: &str) -> Result<T, FormatError> {
259+
let deserializer =
260+
serde::de::value::StrDeserializer::<serde::de::value::Error>::new(content.trim());
261+
Ok(T::deserialize(deserializer)?)
262+
}
263+
}
264+
265+
/// A [`SerializedParam`] that reads its file contents as a single raw value.
266+
pub type RawParam<T> = SerializedParam<T, Raw>;
267+
250268
/// Marker type selecting JSON deserialization for a [`SerializedParam`].
251269
#[cfg(feature = "json")]
252270
#[derive(Debug, Clone, Copy)]
@@ -297,7 +315,7 @@ fn read_to_string(path: &Path) -> Result<String, ParamResolutionError> {
297315
mod tests {
298316
use super::*;
299317
use secrecy::ExposeSecret;
300-
use std::io::Write;
318+
use std::{io::Write, str::FromStr};
301319
use tempfile::NamedTempFile;
302320

303321
#[test]
@@ -389,6 +407,61 @@ mod tests {
389407
);
390408
}
391409

410+
#[test]
411+
fn test_serialized_raw_from_path() {
412+
// A file holding nothing but the value, with trailing whitespace as a
413+
// file written by `echo` would have.
414+
let mut file = NamedTempFile::new().unwrap();
415+
writeln!(file, " raw-value").unwrap();
416+
417+
let param: RawParam<String> = SerializedParam::from_path(file.path());
418+
assert_eq!(param.resolve(None).unwrap(), "raw-value");
419+
}
420+
421+
#[test]
422+
fn test_serialized_raw_inline() {
423+
use newtype_uuid::{TypedUuid, TypedUuidKind, TypedUuidTag};
424+
425+
enum TestKind {}
426+
impl TypedUuidKind for TestKind {
427+
fn tag() -> TypedUuidTag {
428+
const TAG: TypedUuidTag = TypedUuidTag::new("test");
429+
TAG
430+
}
431+
}
432+
433+
#[derive(Deserialize)]
434+
struct Config {
435+
key: RawParam<TypedUuid<TestKind>>,
436+
}
437+
438+
let raw = "1a1e02f3-5d09-4b7f-b8f1-7b8a2c4d5e6f";
439+
let parsed = TypedUuid::from_str(raw).unwrap();
440+
441+
let config: Config = toml::from_str(&format!("key = \"{}\"", raw)).unwrap();
442+
assert_eq!(config.key.resolve(None).unwrap(), parsed);
443+
}
444+
445+
#[test]
446+
fn test_serialized_raw_typed_uuid() {
447+
use newtype_uuid::{TypedUuid, TypedUuidKind, TypedUuidTag};
448+
449+
enum TestKind {}
450+
impl TypedUuidKind for TestKind {
451+
fn tag() -> TypedUuidTag {
452+
const TAG: TypedUuidTag = TypedUuidTag::new("test");
453+
TAG
454+
}
455+
}
456+
457+
let raw = "1a1e02f3-5d09-4b7f-b8f1-7b8a2c4d5e6f";
458+
let mut file = NamedTempFile::new().unwrap();
459+
writeln!(file, "{}", raw).unwrap();
460+
461+
let param: RawParam<TypedUuid<TestKind>> = SerializedParam::from_path(file.path());
462+
assert_eq!(param.resolve(None).unwrap().to_string(), raw);
463+
}
464+
392465
#[cfg(feature = "json")]
393466
#[test]
394467
fn test_serialized_json_from_path() {

‎v-api/src/config.rs‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use serde::{
2323
};
2424
use std::path::{Path, PathBuf};
2525
use thiserror::Error;
26-
use v_api_param::{ParamResolutionError, StringParam};
26+
use v_api_param::{ParamResolutionError, Raw, SerializedParam, StringParam};
2727
use v_model::OAuthClientId;
2828

2929
use crate::{
@@ -171,16 +171,19 @@ pub struct OAuthConfig {
171171
#[partial(ResolvedOAuthDeviceConfig)]
172172
#[derive(Clone, Debug, Deserialize)]
173173
pub struct OAuthDeviceConfig {
174-
pub client_id: TypedUuid<OAuthClientId>,
175-
pub remote_client_id: String,
174+
#[partial(ResolvedOAuthDeviceConfig(retype = TypedUuid<OAuthClientId>))]
175+
pub client_id: SerializedParam<TypedUuid<OAuthClientId>, Raw>,
176+
#[partial(ResolvedOAuthDeviceConfig(retype = String))]
177+
pub remote_client_id: StringParam,
176178
#[partial(ResolvedOAuthDeviceConfig(retype = SecretString))]
177179
pub remote_client_secret: StringParam,
178180
}
179181

180182
#[partial(ResolvedOAuthWebConfig)]
181183
#[derive(Clone, Debug, Deserialize)]
182184
pub struct OAuthWebConfig {
183-
pub remote_client_id: String,
185+
#[partial(ResolvedOAuthWebConfig(retype = String))]
186+
pub remote_client_id: StringParam,
184187
#[partial(ResolvedOAuthWebConfig(retype = SecretString))]
185188
pub remote_client_secret: StringParam,
186189
}
@@ -219,8 +222,12 @@ impl OAuthDeviceConfig {
219222
) -> Result<ResolvedOAuthDeviceConfig, ParamResolutionError> {
220223
let remote_client_secret = self.remote_client_secret.resolve(base)?;
221224
Ok(ResolvedOAuthDeviceConfig {
222-
client_id: self.client_id,
223-
remote_client_id: self.remote_client_id.clone(),
225+
client_id: self.client_id.resolve(base)?,
226+
remote_client_id: self
227+
.remote_client_id
228+
.resolve(base)?
229+
.expose_secret()
230+
.to_string(),
224231
remote_client_secret,
225232
})
226233
}
@@ -232,7 +239,11 @@ impl OAuthWebConfig {
232239
) -> Result<ResolvedOAuthWebConfig, ParamResolutionError> {
233240
let remote_client_secret = self.remote_client_secret.resolve(base)?;
234241
Ok(ResolvedOAuthWebConfig {
235-
remote_client_id: self.remote_client_id.clone(),
242+
remote_client_id: self
243+
.remote_client_id
244+
.resolve(base)?
245+
.expose_secret()
246+
.to_string(),
236247
remote_client_secret,
237248
})
238249
}

0 commit comments

Comments
 (0)