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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 418 additions and 32 deletions

30
Cargo.lock generated
View file

@ -605,7 +605,7 @@ dependencies = [
"encoding_rs",
"log",
"once_cell",
"quick-xml",
"quick-xml 0.31.0",
"serde",
"zip",
]
@ -3094,7 +3094,7 @@ dependencies = [
"pretty_assertions",
"print-positions",
"procfs",
"quick-xml",
"quick-xml 0.31.0",
"quickcheck",
"quickcheck_macros",
"rand",
@ -3476,12 +3476,14 @@ dependencies = [
name = "nu_plugin_formats"
version = "0.96.2"
dependencies = [
"chrono",
"eml-parser",
"ical",
"indexmap",
"nu-plugin",
"nu-plugin-test-support",
"nu-protocol",
"plist",
"rust-ini",
]
@ -4148,6 +4150,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "plist"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
"base64 0.22.1",
"indexmap",
"quick-xml 0.32.0",
"serde",
"time",
]
[[package]]
name = "polars"
version = "0.41.2"
@ -4816,6 +4831,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
dependencies = [
"memchr",
]
[[package]]
name = "quickcheck"
version = "1.0.3"
@ -6872,7 +6896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283"
dependencies = [
"proc-macro2",
"quick-xml",
"quick-xml 0.31.0",
"quote",
]

View file

@ -16,6 +16,8 @@ indexmap = { workspace = true }
eml-parser = "0.1"
ical = "0.11"
rust-ini = "0.21.0"
plist = "1.7"
chrono = "0.4"
[dev-dependencies]
nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.96.2" }

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)
}
}