Adding plist support (#13545)

# Description
Provides the ability convert from and to plist format.

<img width="1250" alt="Screenshot 2024-08-05 at 10 21 26"
src="https://github.com/user-attachments/assets/970f3366-eb70-4d74-a396-649374556f66">

<img width="730" alt="Screenshot 2024-08-05 at 10 22 38"
src="https://github.com/user-attachments/assets/6ec317d0-686e-47c6-bf35-8ab6e5d802db">

# User-Facing Changes
- Introduction of `from plist` command
- Introduction of `to plist`command

---------

Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
Jack Wright
2024-08-05 14:07:15 -07:00
committed by GitHub
parent 9172b22985
commit 2f44801414
12 changed files with 418 additions and 32 deletions

View File

@ -1,4 +1,4 @@
use crate::FromCmds;
use crate::FormatCmdsPlugin;
use eml_parser::eml::*;
use eml_parser::EmlParser;
use indexmap::IndexMap;
@ -12,7 +12,7 @@ const DEFAULT_BODY_PREVIEW: usize = 50;
pub struct FromEml;
impl SimplePluginCommand for FromEml {
type Plugin = FromCmds;
type Plugin = FormatCmdsPlugin;
fn name(&self) -> &str {
"from eml"
@ -40,7 +40,7 @@ impl SimplePluginCommand for FromEml {
fn run(
&self,
_plugin: &FromCmds,
_plugin: &FormatCmdsPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: &Value,
@ -176,5 +176,5 @@ fn from_eml(input: &Value, body_preview: usize, head: Span) -> Result<Value, Lab
fn test_examples() -> Result<(), nu_protocol::ShellError> {
use nu_plugin_test_support::PluginTest;
PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromEml)
PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromEml)
}

View File

@ -1,4 +1,4 @@
use crate::FromCmds;
use crate::FormatCmdsPlugin;
use ical::{parser::ical::component::*, property::Property};
use indexmap::IndexMap;
@ -11,7 +11,7 @@ use std::io::BufReader;
pub struct FromIcs;
impl SimplePluginCommand for FromIcs {
type Plugin = FromCmds;
type Plugin = FormatCmdsPlugin;
fn name(&self) -> &str {
"from ics"
@ -33,7 +33,7 @@ impl SimplePluginCommand for FromIcs {
fn run(
&self,
_plugin: &FromCmds,
_plugin: &FormatCmdsPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: &Value,
@ -274,5 +274,5 @@ fn params_to_value(params: Vec<(String, Vec<String>)>, span: Span) -> Value {
fn test_examples() -> Result<(), nu_protocol::ShellError> {
use nu_plugin_test_support::PluginTest;
PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromIcs)
PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromIcs)
}

View File

@ -1,4 +1,4 @@
use crate::FromCmds;
use crate::FormatCmdsPlugin;
use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand};
use nu_protocol::{
@ -8,7 +8,7 @@ use nu_protocol::{
pub struct FromIni;
impl SimplePluginCommand for FromIni {
type Plugin = FromCmds;
type Plugin = FormatCmdsPlugin;
fn name(&self) -> &str {
"from ini"
@ -30,7 +30,7 @@ impl SimplePluginCommand for FromIni {
fn run(
&self,
_plugin: &FromCmds,
_plugin: &FormatCmdsPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: &Value,
@ -101,5 +101,5 @@ b=2' | from ini",
fn test_examples() -> Result<(), nu_protocol::ShellError> {
use nu_plugin_test_support::PluginTest;
PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromIni)
PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromIni)
}

View File

@ -1,4 +1,5 @@
pub mod eml;
pub mod ics;
pub mod ini;
pub mod vcf;
pub(crate) mod eml;
pub(crate) mod ics;
pub(crate) mod ini;
pub(crate) mod plist;
pub(crate) mod vcf;

View File

@ -0,0 +1,240 @@
use std::time::SystemTime;
use chrono::{DateTime, FixedOffset, Offset, Utc};
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand, SimplePluginCommand};
use nu_protocol::{
record, Category, Example, LabeledError, Record, Signature, Span, Value as NuValue,
};
use plist::{Date as PlistDate, Dictionary, Value as PlistValue};
use crate::FormatCmdsPlugin;
pub struct FromPlist;
impl SimplePluginCommand for FromPlist {
type Plugin = FormatCmdsPlugin;
fn name(&self) -> &str {
"from plist"
}
fn usage(&self) -> &str {
"Convert plist to Nushell values"
}
fn examples(&self) -> Vec<Example> {
vec![Example {
example: r#"'<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>a</key>
<integer>3</integer>
</dict>
</plist>' | from plist"#,
description: "Convert a table into a plist file",
result: Some(NuValue::test_record(record!( "a" => NuValue::test_int(3)))),
}]
}
fn signature(&self) -> Signature {
Signature::build(PluginCommand::name(self)).category(Category::Formats)
}
fn run(
&self,
_plugin: &FormatCmdsPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: &NuValue,
) -> Result<NuValue, LabeledError> {
match input {
NuValue::String { val, .. } => {
let plist = plist::from_bytes(val.as_bytes())
.map_err(|e| build_label_error(format!("{}", e), input.span()))?;
let converted = convert_plist_value(&plist, call.head)?;
Ok(converted)
}
NuValue::Binary { val, .. } => {
let plist = plist::from_bytes(val)
.map_err(|e| build_label_error(format!("{}", e), input.span()))?;
let converted = convert_plist_value(&plist, call.head)?;
Ok(converted)
}
_ => Err(build_label_error(
format!("Invalid input, must be string not: {:?}", input),
call.head,
)),
}
}
}
fn build_label_error(msg: impl Into<String>, span: Span) -> LabeledError {
LabeledError::new("Could not load plist").with_label(msg, span)
}
fn convert_plist_value(plist_val: &PlistValue, span: Span) -> Result<NuValue, LabeledError> {
match plist_val {
PlistValue::String(s) => Ok(NuValue::string(s.to_owned(), span)),
PlistValue::Boolean(b) => Ok(NuValue::bool(*b, span)),
PlistValue::Real(r) => Ok(NuValue::float(*r, span)),
PlistValue::Date(d) => Ok(NuValue::date(convert_date(d), span)),
PlistValue::Integer(i) => {
let signed = i
.as_signed()
.ok_or_else(|| build_label_error(format!("Cannot convert {i} to i64"), span))?;
Ok(NuValue::int(signed, span))
}
PlistValue::Uid(uid) => Ok(NuValue::float(uid.get() as f64, span)),
PlistValue::Data(data) => Ok(NuValue::binary(data.to_owned(), span)),
PlistValue::Array(arr) => Ok(NuValue::list(convert_array(arr, span)?, span)),
PlistValue::Dictionary(dict) => Ok(convert_dict(dict, span)?),
_ => Ok(NuValue::nothing(span)),
}
}
fn convert_dict(dict: &Dictionary, span: Span) -> Result<NuValue, LabeledError> {
let cols: Vec<String> = dict.keys().cloned().collect();
let vals: Result<Vec<NuValue>, LabeledError> = dict
.values()
.map(|v| convert_plist_value(v, span))
.collect();
Ok(NuValue::record(
Record::from_raw_cols_vals(cols, vals?, span, span)?,
span,
))
}
fn convert_array(plist_array: &[PlistValue], span: Span) -> Result<Vec<NuValue>, LabeledError> {
plist_array
.iter()
.map(|v| convert_plist_value(v, span))
.collect()
}
pub fn convert_date(plist_date: &PlistDate) -> DateTime<FixedOffset> {
// In the docs the plist date object is listed as a utc timestamp, so this
// conversion should be fine
let plist_sys_time: SystemTime = plist_date.to_owned().into();
let utc_date: DateTime<Utc> = plist_sys_time.into();
let utc_offset = utc_date.offset().fix();
utc_date.with_timezone(&utc_offset)
}
#[cfg(test)]
mod test {
use super::*;
use chrono::Datelike;
use plist::Uid;
use std::time::SystemTime;
use nu_plugin_test_support::PluginTest;
use nu_protocol::ShellError;
#[test]
fn test_convert_string() {
let plist_val = PlistValue::String("hello".to_owned());
let result = convert_plist_value(&plist_val, Span::test_data());
assert_eq!(
result,
Ok(NuValue::string("hello".to_owned(), Span::test_data()))
);
}
#[test]
fn test_convert_boolean() {
let plist_val = PlistValue::Boolean(true);
let result = convert_plist_value(&plist_val, Span::test_data());
assert_eq!(result, Ok(NuValue::bool(true, Span::test_data())));
}
#[test]
fn test_convert_real() {
let plist_val = PlistValue::Real(3.14);
let result = convert_plist_value(&plist_val, Span::test_data());
assert_eq!(result, Ok(NuValue::float(3.14, Span::test_data())));
}
#[test]
fn test_convert_integer() {
let plist_val = PlistValue::Integer(42.into());
let result = convert_plist_value(&plist_val, Span::test_data());
assert_eq!(result, Ok(NuValue::int(42, Span::test_data())));
}
#[test]
fn test_convert_uid() {
let v = 12345678_u64;
let uid = Uid::new(v);
let plist_val = PlistValue::Uid(uid);
let result = convert_plist_value(&plist_val, Span::test_data());
assert_eq!(result, Ok(NuValue::float(v as f64, Span::test_data())));
}
#[test]
fn test_convert_data() {
let data = vec![0x41, 0x42, 0x43];
let plist_val = PlistValue::Data(data.clone());
let result = convert_plist_value(&plist_val, Span::test_data());
assert_eq!(result, Ok(NuValue::binary(data, Span::test_data())));
}
#[test]
fn test_convert_date() {
let epoch = SystemTime::UNIX_EPOCH;
let plist_date = epoch.into();
let datetime = convert_date(&plist_date);
assert_eq!(1970, datetime.year());
assert_eq!(1, datetime.month());
assert_eq!(1, datetime.day());
}
#[test]
fn test_convert_dict() {
let mut dict = Dictionary::new();
dict.insert("a".to_string(), PlistValue::String("c".to_string()));
dict.insert("b".to_string(), PlistValue::String("d".to_string()));
let nu_dict = convert_dict(&dict, Span::test_data()).unwrap();
assert_eq!(
nu_dict,
NuValue::record(
Record::from_raw_cols_vals(
vec!["a".to_string(), "b".to_string()],
vec![
NuValue::string("c".to_string(), Span::test_data()),
NuValue::string("d".to_string(), Span::test_data())
],
Span::test_data(),
Span::test_data(),
)
.expect("failed to create record"),
Span::test_data(),
)
);
}
#[test]
fn test_convert_array() {
let mut arr = Vec::new();
arr.push(PlistValue::String("a".to_string()));
arr.push(PlistValue::String("b".to_string()));
let nu_arr = convert_array(&arr, Span::test_data()).unwrap();
assert_eq!(
nu_arr,
vec![
NuValue::string("a".to_string(), Span::test_data()),
NuValue::string("b".to_string(), Span::test_data())
]
);
}
#[test]
fn test_examples() -> Result<(), ShellError> {
let plugin = FormatCmdsPlugin {};
let cmd = FromPlist {};
let mut plugin_test = PluginTest::new("polars", plugin.into())?;
plugin_test.test_command_examples(&cmd)
}
}

View File

@ -1,4 +1,4 @@
use crate::FromCmds;
use crate::FormatCmdsPlugin;
use ical::{parser::vcard::component::*, property::Property};
use indexmap::IndexMap;
@ -10,7 +10,7 @@ use nu_protocol::{
pub struct FromVcf;
impl SimplePluginCommand for FromVcf {
type Plugin = FromCmds;
type Plugin = FormatCmdsPlugin;
fn name(&self) -> &str {
"from vcf"
@ -32,7 +32,7 @@ impl SimplePluginCommand for FromVcf {
fn run(
&self,
_plugin: &FromCmds,
_plugin: &FormatCmdsPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: &Value,
@ -164,5 +164,5 @@ fn params_to_value(params: Vec<(String, Vec<String>)>, span: Span) -> Value {
fn test_examples() -> Result<(), nu_protocol::ShellError> {
use nu_plugin_test_support::PluginTest;
PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromVcf)
PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromVcf)
}

View File

@ -1,15 +1,18 @@
mod from;
mod to;
use nu_plugin::{Plugin, PluginCommand};
pub use from::eml::FromEml;
pub use from::ics::FromIcs;
pub use from::ini::FromIni;
pub use from::vcf::FromVcf;
use from::eml::FromEml;
use from::ics::FromIcs;
use from::ini::FromIni;
use from::plist::FromPlist;
use from::vcf::FromVcf;
use to::plist::IntoPlist;
pub struct FromCmds;
pub struct FormatCmdsPlugin;
impl Plugin for FromCmds {
impl Plugin for FormatCmdsPlugin {
fn version(&self) -> String {
env!("CARGO_PKG_VERSION").into()
}
@ -20,6 +23,8 @@ impl Plugin for FromCmds {
Box::new(FromIcs),
Box::new(FromIni),
Box::new(FromVcf),
Box::new(FromPlist),
Box::new(IntoPlist),
]
}
}

View File

@ -1,6 +1,6 @@
use nu_plugin::{serve_plugin, MsgPackSerializer};
use nu_plugin_formats::FromCmds;
use nu_plugin_formats::FormatCmdsPlugin;
fn main() {
serve_plugin(&FromCmds, MsgPackSerializer {})
serve_plugin(&FormatCmdsPlugin, MsgPackSerializer {})
}

View File

@ -0,0 +1 @@
pub(crate) mod plist;

View File

@ -0,0 +1,113 @@
use std::time::SystemTime;
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand, SimplePluginCommand};
use nu_protocol::{Category, Example, LabeledError, Record, Signature, Span, Value as NuValue};
use plist::{Integer, Value as PlistValue};
use crate::FormatCmdsPlugin;
pub(crate) struct IntoPlist;
impl SimplePluginCommand for IntoPlist {
type Plugin = FormatCmdsPlugin;
fn name(&self) -> &str {
"to plist"
}
fn usage(&self) -> &str {
"Convert Nu values into plist"
}
fn examples(&self) -> Vec<Example> {
vec![Example {
example: "{ a: 3 } | to plist",
description: "Convert a table into a plist file",
result: None,
}]
}
fn signature(&self) -> Signature {
Signature::build(PluginCommand::name(self))
.switch("binary", "Output plist in binary format", Some('b'))
.category(Category::Formats)
}
fn run(
&self,
_plugin: &FormatCmdsPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: &NuValue,
) -> Result<NuValue, LabeledError> {
let plist_val = convert_nu_value(input)?;
let mut out = Vec::new();
if call.has_flag("binary")? {
plist::to_writer_binary(&mut out, &plist_val)
.map_err(|e| build_label_error(format!("{}", e), input.span()))?;
Ok(NuValue::binary(out, input.span()))
} else {
plist::to_writer_xml(&mut out, &plist_val)
.map_err(|e| build_label_error(format!("{}", e), input.span()))?;
Ok(NuValue::string(
String::from_utf8(out)
.map_err(|e| build_label_error(format!("{}", e), input.span()))?,
input.span(),
))
}
}
}
fn build_label_error(msg: String, span: Span) -> LabeledError {
LabeledError::new("Cannot convert plist").with_label(msg, span)
}
fn convert_nu_value(nu_val: &NuValue) -> Result<PlistValue, LabeledError> {
let span = Span::test_data();
match nu_val {
NuValue::String { val, .. } => Ok(PlistValue::String(val.to_owned())),
NuValue::Bool { val, .. } => Ok(PlistValue::Boolean(*val)),
NuValue::Float { val, .. } => Ok(PlistValue::Real(*val)),
NuValue::Int { val, .. } => Ok(PlistValue::Integer(Into::<Integer>::into(*val))),
NuValue::Binary { val, .. } => Ok(PlistValue::Data(val.to_owned())),
NuValue::Record { val, .. } => convert_nu_dict(val),
NuValue::List { vals, .. } => Ok(PlistValue::Array(
vals.iter()
.map(convert_nu_value)
.collect::<Result<_, _>>()?,
)),
NuValue::Date { val, .. } => Ok(PlistValue::Date(SystemTime::from(val.to_owned()).into())),
NuValue::Filesize { val, .. } => Ok(PlistValue::Integer(Into::<Integer>::into(*val))),
_ => Err(build_label_error(
format!("{:?} is not convertible", nu_val),
span,
)),
}
}
fn convert_nu_dict(record: &Record) -> Result<PlistValue, LabeledError> {
Ok(PlistValue::Dictionary(
record
.iter()
.map(|(k, v)| convert_nu_value(v).map(|v| (k.to_owned(), v)))
.collect::<Result<_, _>>()?,
))
}
#[cfg(test)]
mod test {
use nu_plugin_test_support::PluginTest;
use nu_protocol::ShellError;
use super::*;
#[test]
fn test_examples() -> Result<(), ShellError> {
let plugin = FormatCmdsPlugin {};
let cmd = IntoPlist {};
let mut plugin_test = PluginTest::new("polars", plugin.into())?;
plugin_test.test_command_examples(&cmd)
}
}