json_arg/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8//! CLIs (especially those designed to be called by automation) often take
9//! structured data as inputs. This ends up creating a ton of boilerplate where
10//! a CLI arg is declared as a [PathBuf] and then is quickly opened, read from
11//! and deserialized with some [serde] format crate.
12//! This crate provides two newtype wrappers ([Serde] and [SerdeFile]) that can
13//! deserialize any Serde-compatible arguments with no extra effort.
14
15use std::cmp::Ordering;
16use std::fmt::Debug;
17use std::fmt::Display;
18use std::hash::Hash;
19use std::hash::Hasher;
20use std::io::Cursor;
21use std::io::Read;
22use std::marker::PhantomData;
23use std::ops::Deref;
24use std::ops::DerefMut;
25use std::path::Path;
26use std::path::PathBuf;
27use std::str::FromStr;
28
29use serde::Deserialize;
30
31pub trait DeserializeReader<T> {
32    type Error;
33
34    fn deserialize<R: Read>(reader: R) -> Result<T, Self::Error>;
35}
36
37/// Deserialize the argument as JSON
38pub struct JsonFormat;
39
40impl<'de, T> DeserializeReader<T> for JsonFormat
41where
42    T: Deserialize<'de>,
43{
44    type Error = serde_json::Error;
45
46    fn deserialize<R: Read>(reader: R) -> Result<T, Self::Error> {
47        let mut deser = serde_json::Deserializer::from_reader(reader);
48        T::deserialize(&mut deser)
49    }
50}
51
52/// Deserialize the argument as TOML
53pub struct TomlFormat;
54
55impl<T> DeserializeReader<T> for TomlFormat
56where
57    T: for<'de> Deserialize<'de>,
58{
59    type Error = std::io::Error;
60
61    fn deserialize<R: Read>(reader: R) -> Result<T, Self::Error> {
62        // [toml] has no way to deserialize from a reader :(
63        let str = std::io::read_to_string(reader)?;
64        toml::from_str(&str)
65            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
66    }
67}
68
69/// Inline JSON string. The argument provided by the caller is the raw JSON
70/// string (and the caller must consequently deal with shell quoting
71/// ahead-of-time).
72pub struct Serde<T, D>(T, PhantomData<D>);
73
74pub type Json<T> = Serde<T, JsonFormat>;
75pub type Toml<T> = Serde<T, TomlFormat>;
76
77impl<'de, T, D> FromStr for Serde<T, D>
78where
79    T: Deserialize<'de>,
80    D: DeserializeReader<T>,
81{
82    type Err = D::Error;
83
84    fn from_str(s: &str) -> Result<Self, D::Error> {
85        D::deserialize(Cursor::new(s)).map(|v| Self(v, PhantomData))
86    }
87}
88
89impl<T, D> Deref for Serde<T, D> {
90    type Target = T;
91
92    #[inline]
93    fn deref(&self) -> &Self::Target {
94        &self.0
95    }
96}
97
98impl<T, D> DerefMut for Serde<T, D> {
99    #[inline]
100    fn deref_mut(&mut self) -> &mut Self::Target {
101        &mut self.0
102    }
103}
104
105impl<T, D> Debug for Serde<T, D>
106where
107    T: Debug,
108{
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        Debug::fmt(&self.0, f)
111    }
112}
113
114impl<T, D> Clone for Serde<T, D>
115where
116    T: Clone,
117{
118    fn clone(&self) -> Self {
119        Self(self.0.clone(), PhantomData)
120    }
121}
122
123impl<T, D> Serde<T, D> {
124    #[inline]
125    pub fn into_inner(self) -> T {
126        self.0
127    }
128
129    #[inline]
130    pub fn as_inner(&self) -> &T {
131        &self.0
132    }
133}
134
135/// Argument that represents a serialized file. The argument provided by the
136/// caller is the path to the file that is deserialized immediately on load.
137/// The original path is preserved and accessible with [SerdeFile::path]
138pub struct SerdeFile<T, D> {
139    value: T,
140    path: PathBuf,
141    deser: PhantomData<D>,
142}
143
144pub type JsonFile<T> = SerdeFile<T, JsonFormat>;
145pub type TomlFile<T> = SerdeFile<T, TomlFormat>;
146
147impl<'de, T, D> FromStr for SerdeFile<T, D>
148where
149    T: Deserialize<'de>,
150    D: DeserializeReader<T>,
151    D::Error: Display,
152{
153    type Err = std::io::Error;
154
155    fn from_str(path: &str) -> std::io::Result<Self> {
156        let f = std::fs::File::open(path)?;
157        D::deserialize(f)
158            .map(|value| Self {
159                path: path.into(),
160                value,
161                deser: PhantomData,
162            })
163            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
164    }
165}
166
167impl<T, D> Deref for SerdeFile<T, D> {
168    type Target = T;
169
170    #[inline]
171    fn deref(&self) -> &Self::Target {
172        &self.value
173    }
174}
175
176impl<T, D> DerefMut for SerdeFile<T, D> {
177    #[inline]
178    fn deref_mut(&mut self) -> &mut Self::Target {
179        &mut self.value
180    }
181}
182
183impl<T, D> Debug for SerdeFile<T, D>
184where
185    T: Debug,
186{
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        f.debug_struct("SerdeFile")
189            .field("path", &self.path)
190            .field("value", &self.value)
191            .finish()
192    }
193}
194
195impl<T, D> Clone for SerdeFile<T, D>
196where
197    T: Clone,
198{
199    fn clone(&self) -> Self {
200        Self {
201            path: self.path.clone(),
202            value: self.value.clone(),
203            deser: PhantomData,
204        }
205    }
206}
207
208impl<T, D> SerdeFile<T, D> {
209    #[inline]
210    pub fn path(&self) -> &Path {
211        &self.path
212    }
213
214    #[inline]
215    pub fn as_inner(&self) -> &T {
216        self
217    }
218
219    #[inline]
220    pub fn into_inner(self) -> T {
221        self.value
222    }
223}
224
225macro_rules! common_impl {
226    ($i:ident) => {
227        impl<T, D> AsRef<T> for $i<T, D> {
228            #[inline]
229            fn as_ref(&self) -> &T {
230                self
231            }
232        }
233
234        impl<T, D> PartialEq for $i<T, D>
235        where
236            T: PartialEq,
237        {
238            fn eq(&self, rhs: &Self) -> bool {
239                self.as_inner() == rhs.as_inner()
240            }
241        }
242
243        impl<T, D> PartialEq<T> for $i<T, D>
244        where
245            T: PartialEq,
246        {
247            fn eq(&self, rhs: &T) -> bool {
248                self.as_inner() == rhs
249            }
250        }
251
252        impl<T, D> Eq for $i<T, D> where T: Eq {}
253
254        impl<T, D> PartialOrd for $i<T, D>
255        where
256            T: PartialOrd,
257        {
258            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
259                self.as_inner().partial_cmp(other.as_inner())
260            }
261        }
262
263        impl<T, D> PartialOrd<T> for $i<T, D>
264        where
265            T: PartialOrd,
266        {
267            fn partial_cmp(&self, other: &T) -> Option<Ordering> {
268                self.as_inner().partial_cmp(other)
269            }
270        }
271
272        impl<T, D> Ord for $i<T, D>
273        where
274            T: Ord,
275        {
276            fn cmp(&self, other: &Self) -> Ordering {
277                self.as_inner().cmp(other.as_inner())
278            }
279        }
280
281        impl<T, D> Hash for $i<T, D>
282        where
283            T: Hash,
284        {
285            fn hash<H>(&self, state: &mut H)
286            where
287                H: Hasher,
288            {
289                self.as_inner().hash(state)
290            }
291        }
292    };
293}
294
295common_impl!(Serde);
296common_impl!(SerdeFile);
297
298#[cfg(test)]
299mod tests {
300    use std::ffi::OsStr;
301
302    use clap::Parser;
303    use serde::Serialize;
304    use similar_asserts::assert_eq;
305    use tempfile::NamedTempFile;
306
307    use super::*;
308
309    #[derive(Debug, PartialEq, Deserialize, Serialize)]
310    struct Example {
311        foo: String,
312        bar: u32,
313    }
314
315    #[derive(Debug, Parser)]
316    struct Args {
317        #[clap(long)]
318        inline: Option<Json<Example>>,
319        #[clap(long)]
320        file: Option<JsonFile<Example>>,
321    }
322
323    #[test]
324    fn inline() {
325        let example = Example {
326            foo: "baz".into(),
327            bar: 42,
328        };
329        let inline_str = serde_json::to_string(&example).expect("failed to serialize");
330        let args = Args::parse_from(vec!["inline", "--inline", &inline_str]);
331        assert_eq!(args.inline.expect("definitely here"), example,);
332    }
333
334    #[test]
335    fn file() {
336        let example = Example {
337            foo: "baz".into(),
338            bar: 42,
339        };
340        let mut tmp = NamedTempFile::new().expect("failed to create tmp file");
341        serde_json::to_writer(&mut tmp, &example).expect("failed to serialize");
342        let args = Args::parse_from(vec![
343            OsStr::new("file"),
344            OsStr::new("--file"),
345            tmp.path().as_os_str(),
346        ]);
347        assert_eq!(args.file.expect("definitely here"), example);
348    }
349}