nu_plugin_formats/from/
vcf.rs

1use crate::FormatCmdsPlugin;
2
3use ical::{parser::vcard::component::*, property::Property};
4use indexmap::IndexMap;
5use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand};
6use nu_protocol::{
7    Category, Example, LabeledError, ShellError, Signature, Span, Type, Value, record,
8};
9
10pub struct FromVcf;
11
12impl SimplePluginCommand for FromVcf {
13    type Plugin = FormatCmdsPlugin;
14
15    fn name(&self) -> &str {
16        "from vcf"
17    }
18
19    fn description(&self) -> &str {
20        "Parse text as .vcf and create table."
21    }
22
23    fn signature(&self) -> Signature {
24        Signature::build(self.name())
25            .input_output_types(vec![(Type::String, Type::table())])
26            .category(Category::Formats)
27    }
28
29    fn examples(&self) -> Vec<Example<'_>> {
30        examples()
31    }
32
33    fn run(
34        &self,
35        _plugin: &FormatCmdsPlugin,
36        _engine: &EngineInterface,
37        call: &EvaluatedCall,
38        input: &Value,
39    ) -> Result<Value, LabeledError> {
40        let span = input.span();
41        let input_string = input.coerce_str()?;
42        let head = call.head;
43
44        let input_string = input_string
45            .lines()
46            .enumerate()
47            .map(|(i, x)| {
48                if i == 0 {
49                    x.trim().to_string()
50                } else if x.len() > 1 && (x.starts_with(' ') || x.starts_with('\t')) {
51                    x[1..].trim_end().to_string()
52                } else {
53                    format!("\n{}", x.trim())
54                }
55            })
56            .collect::<String>();
57
58        let input_bytes = input_string.as_bytes();
59        let cursor = std::io::Cursor::new(input_bytes);
60        let parser = ical::VcardParser::new(cursor);
61
62        let iter = parser.map(move |contact| match contact {
63            Ok(c) => contact_to_value(c, head),
64            Err(e) => Value::error(
65                ShellError::UnsupportedInput {
66                    msg: format!("input cannot be parsed as .vcf ({e})"),
67                    input: "value originates from here".into(),
68                    msg_span: head,
69                    input_span: span,
70                },
71                span,
72            ),
73        });
74
75        let collected: Vec<_> = iter.collect();
76        Ok(Value::list(collected, head))
77    }
78}
79
80pub fn examples() -> Vec<Example<'static>> {
81    vec![Example {
82        example: "'BEGIN:VCARD
83N:Foo
84FN:Bar
85EMAIL:foo@bar.com
86END:VCARD' | from vcf",
87        description: "Converts ics formatted string to table",
88        result: Some(Value::test_list(vec![Value::test_record(record! {
89            "properties" => Value::test_list(
90                vec![
91                    Value::test_record(record! {
92                            "name" =>   Value::test_string("N"),
93                            "value" =>  Value::test_string("Foo"),
94                            "params" => Value::nothing(Span::test_data()),
95                    }),
96                    Value::test_record(record! {
97                            "name" =>   Value::test_string("FN"),
98                            "value" =>  Value::test_string("Bar"),
99                            "params" => Value::nothing(Span::test_data()),
100                    }),
101                    Value::test_record(record! {
102                            "name" =>   Value::test_string("EMAIL"),
103                            "value" =>  Value::test_string("foo@bar.com"),
104                            "params" => Value::nothing(Span::test_data()),
105                    }),
106                ],
107            ),
108        })])),
109    }]
110}
111
112fn contact_to_value(contact: VcardContact, span: Span) -> Value {
113    Value::record(
114        record! { "properties" => properties_to_value(contact.properties, span) },
115        span,
116    )
117}
118
119fn properties_to_value(properties: Vec<Property>, span: Span) -> Value {
120    Value::list(
121        properties
122            .into_iter()
123            .map(|prop| {
124                let name = Value::string(prop.name, span);
125                let value = match prop.value {
126                    Some(val) => Value::string(val, span),
127                    None => Value::nothing(span),
128                };
129                let params = match prop.params {
130                    Some(param_list) => params_to_value(param_list, span),
131                    None => Value::nothing(span),
132                };
133
134                Value::record(
135                    record! {
136                        "name" => name,
137                        "value" => value,
138                        "params" => params,
139                    },
140                    span,
141                )
142            })
143            .collect::<Vec<Value>>(),
144        span,
145    )
146}
147
148fn params_to_value(params: Vec<(String, Vec<String>)>, span: Span) -> Value {
149    let mut row = IndexMap::new();
150
151    for (param_name, param_values) in params {
152        let values: Vec<Value> = param_values
153            .into_iter()
154            .map(|val| Value::string(val, span))
155            .collect();
156        let values = Value::list(values, span);
157        row.insert(param_name, values);
158    }
159
160    Value::record(row.into_iter().collect(), span)
161}
162
163#[test]
164fn test_examples() -> Result<(), nu_protocol::ShellError> {
165    use nu_plugin_test_support::PluginTest;
166
167    PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromVcf)
168}