nu_plugin_formats/from/
vcf.rs1use 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}