use crate::cap_urn::CapUrn;
use crate::media_spec::{resolve_media_urn, MediaSpecDef, MediaSpecError, ResolvedMediaSpec};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct ArgumentValidation {
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_values: Option<Vec<String>>,
}
impl ArgumentValidation {
fn is_empty(&self) -> bool {
self.min.is_none() &&
self.max.is_none() &&
self.min_length.is_none() &&
self.max_length.is_none() &&
self.pattern.is_none() &&
self.allowed_values.is_none()
}
pub fn numeric_range(min: Option<f64>, max: Option<f64>) -> Self {
Self {
min,
max,
min_length: None,
max_length: None,
pattern: None,
allowed_values: None,
}
}
pub fn string_length(min_length: Option<usize>, max_length: Option<usize>) -> Self {
Self {
min: None,
max: None,
min_length,
max_length,
pattern: None,
allowed_values: None,
}
}
pub fn pattern(pattern: String) -> Self {
Self {
min: None,
max: None,
min_length: None,
max_length: None,
pattern: Some(pattern),
allowed_values: None,
}
}
pub fn allowed_values(values: Vec<String>) -> Self {
Self {
min: None,
max: None,
min_length: None,
max_length: None,
pattern: None,
allowed_values: Some(values),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged, deny_unknown_fields)]
pub enum ArgSource {
Stdin {
stdin: String,
},
Position {
position: usize,
},
CliFlag {
cli_flag: String,
},
}
impl ArgSource {
pub fn get_type(&self) -> &'static str {
match self {
ArgSource::Stdin { .. } => "stdin",
ArgSource::Position { .. } => "position",
ArgSource::CliFlag { .. } => "cli_flag",
}
}
pub fn stdin_media_urn(&self) -> Option<&str> {
match self {
ArgSource::Stdin { stdin } => Some(stdin),
_ => None,
}
}
pub fn position(&self) -> Option<usize> {
match self {
ArgSource::Position { position } => Some(*position),
_ => None,
}
}
pub fn cli_flag(&self) -> Option<&str> {
match self {
ArgSource::CliFlag { cli_flag } => Some(cli_flag),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CapArg {
pub media_urn: String,
pub required: bool,
pub sources: Vec<ArgSource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arg_description: Option<String>,
#[serde(skip_serializing_if = "ArgumentValidation::is_empty", default)]
pub validation: ArgumentValidation,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_value: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl CapArg {
pub fn new(media_urn: impl Into<String>, required: bool, sources: Vec<ArgSource>) -> Self {
Self {
media_urn: media_urn.into(),
required,
sources,
arg_description: None,
validation: ArgumentValidation::default(),
default_value: None,
metadata: None,
}
}
pub fn with_description(
media_urn: impl Into<String>,
required: bool,
sources: Vec<ArgSource>,
description: impl Into<String>,
) -> Self {
Self {
media_urn: media_urn.into(),
required,
sources,
arg_description: Some(description.into()),
validation: ArgumentValidation::default(),
default_value: None,
metadata: None,
}
}
pub fn with_full_definition(
media_urn: impl Into<String>,
required: bool,
sources: Vec<ArgSource>,
description: Option<String>,
validation: ArgumentValidation,
default: Option<serde_json::Value>,
metadata: Option<serde_json::Value>,
) -> Self {
Self {
media_urn: media_urn.into(),
required,
sources,
arg_description: description,
validation,
default_value: default,
metadata,
}
}
pub fn get_media_urn(&self) -> &str {
&self.media_urn
}
pub fn resolve(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<ResolvedMediaSpec, MediaSpecError> {
resolve_media_urn(&self.media_urn, media_specs)
}
pub fn is_binary(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<bool, MediaSpecError> {
self.resolve(media_specs).map(|ms| ms.is_binary())
}
pub fn is_json(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<bool, MediaSpecError> {
self.resolve(media_specs).map(|ms| ms.is_json())
}
pub fn media_type(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<String, MediaSpecError> {
self.resolve(media_specs).map(|ms| ms.media_type)
}
pub fn profile_uri(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<Option<String>, MediaSpecError> {
self.resolve(media_specs).map(|ms| ms.profile_uri)
}
pub fn schema(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<Option<serde_json::Value>, MediaSpecError> {
self.resolve(media_specs).map(|ms| ms.schema)
}
pub fn get_metadata(&self) -> Option<&serde_json::Value> {
self.metadata.as_ref()
}
pub fn set_metadata(&mut self, metadata: serde_json::Value) {
self.metadata = Some(metadata);
}
pub fn clear_metadata(&mut self) {
self.metadata = None;
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CapOutput {
pub media_urn: String,
#[serde(skip_serializing_if = "ArgumentValidation::is_empty", default)]
pub validation: ArgumentValidation,
pub output_description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl CapOutput {
pub fn new(media_urn: impl Into<String>, description: impl Into<String>) -> Self {
Self {
media_urn: media_urn.into(),
output_description: description.into(),
validation: ArgumentValidation::default(),
metadata: None,
}
}
pub fn with_validation(media_urn: impl Into<String>, description: impl Into<String>, validation: ArgumentValidation) -> Self {
Self {
media_urn: media_urn.into(),
output_description: description.into(),
validation,
metadata: None,
}
}
pub fn with_full_definition(
media_urn: impl Into<String>,
description: impl Into<String>,
validation: ArgumentValidation,
metadata: Option<serde_json::Value>,
) -> Self {
Self {
media_urn: media_urn.into(),
output_description: description.into(),
validation,
metadata,
}
}
pub fn get_media_urn(&self) -> &str {
&self.media_urn
}
pub fn resolve(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<ResolvedMediaSpec, MediaSpecError> {
resolve_media_urn(&self.media_urn, media_specs)
}
pub fn is_binary(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<bool, MediaSpecError> {
self.resolve(media_specs).map(|ms| ms.is_binary())
}
pub fn is_json(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<bool, MediaSpecError> {
self.resolve(media_specs).map(|ms| ms.is_json())
}
pub fn media_type(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<String, MediaSpecError> {
self.resolve(media_specs).map(|ms| ms.media_type)
}
pub fn profile_uri(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<Option<String>, MediaSpecError> {
self.resolve(media_specs).map(|ms| ms.profile_uri)
}
pub fn schema(&self, media_specs: &HashMap<String, MediaSpecDef>) -> Result<Option<serde_json::Value>, MediaSpecError> {
self.resolve(media_specs).map(|ms| ms.schema)
}
pub fn get_metadata(&self) -> Option<&serde_json::Value> {
self.metadata.as_ref()
}
pub fn set_metadata(&mut self, metadata: serde_json::Value) {
self.metadata = Some(metadata);
}
pub fn clear_metadata(&mut self) {
self.metadata = None;
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RegisteredBy {
pub username: String,
pub registered_at: String,
}
impl RegisteredBy {
pub fn new(username: impl Into<String>, registered_at: impl Into<String>) -> Self {
Self {
username: username.into(),
registered_at: registered_at.into(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Cap {
pub urn: CapUrn,
pub title: String,
pub cap_description: Option<String>,
pub metadata: HashMap<String, String>,
pub command: String,
pub media_specs: HashMap<String, MediaSpecDef>,
pub args: Vec<CapArg>,
pub output: Option<CapOutput>,
pub metadata_json: Option<serde_json::Value>,
pub registered_by: Option<RegisteredBy>,
}
impl Serialize for Cap {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("Cap", 10)?;
let mut all_tags = serde_json::Map::new();
all_tags.insert("in".to_string(), serde_json::Value::String(self.urn.in_spec().to_string()));
all_tags.insert("out".to_string(), serde_json::Value::String(self.urn.out_spec().to_string()));
for (k, v) in &self.urn.tags {
all_tags.insert(k.clone(), serde_json::Value::String(v.clone()));
}
state.serialize_field("urn", &serde_json::json!({
"tags": all_tags
}))?;
state.serialize_field("title", &self.title)?;
state.serialize_field("command", &self.command)?;
if self.cap_description.is_some() {
state.serialize_field("cap_description", &self.cap_description)?;
}
if !self.metadata.is_empty() {
state.serialize_field("metadata", &self.metadata)?;
}
if !self.media_specs.is_empty() {
state.serialize_field("media_specs", &self.media_specs)?;
}
if !self.args.is_empty() {
state.serialize_field("args", &self.args)?;
}
if self.output.is_some() {
state.serialize_field("output", &self.output)?;
}
if self.metadata_json.is_some() {
state.serialize_field("metadata_json", &self.metadata_json)?;
}
if self.registered_by.is_some() {
state.serialize_field("registered_by", &self.registered_by)?;
}
state.end()
}
}
impl<'de> Deserialize<'de> for Cap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct CapRegistry {
urn: serde_json::Value,
title: String,
cap_description: Option<String>,
#[serde(default)]
metadata: HashMap<String, String>,
command: String,
#[serde(default)]
media_specs: HashMap<String, MediaSpecDef>,
#[serde(default)]
args: Vec<CapArg>,
output: Option<CapOutput>,
metadata_json: Option<serde_json::Value>,
registered_by: Option<RegisteredBy>,
}
let registry_cap = CapRegistry::deserialize(deserializer)?;
let urn = match registry_cap.urn {
serde_json::Value::String(urn_str) => {
CapUrn::from_string(&urn_str).map_err(serde::de::Error::custom)?
},
serde_json::Value::Object(urn_obj) => {
if let Some(tags) = urn_obj.get("tags").and_then(|t| t.as_object()) {
let in_spec = tags.get("in")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::custom("Missing required 'in' tag in urn - caps must declare their input type (use media:void for no input)"))?
.to_string();
let out_spec = tags.get("out")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::custom("Missing required 'out' tag in urn - caps must declare their output type"))?
.to_string();
let mut urn_builder = crate::cap_urn::CapUrnBuilder::new()
.in_spec(&in_spec)
.out_spec(&out_spec);
for (key, value) in tags {
if key == "in" || key == "out" {
continue;
}
if let Some(val_str) = value.as_str() {
urn_builder = urn_builder.tag(key, val_str);
}
}
urn_builder.build().map_err(serde::de::Error::custom)?
} else {
return Err(serde::de::Error::custom("Invalid urn object format"));
}
},
_ => return Err(serde::de::Error::custom("urn must be string or object")),
};
Ok(Cap {
urn,
title: registry_cap.title,
cap_description: registry_cap.cap_description,
metadata: registry_cap.metadata,
command: registry_cap.command,
media_specs: registry_cap.media_specs,
args: registry_cap.args,
output: registry_cap.output,
metadata_json: registry_cap.metadata_json,
registered_by: registry_cap.registered_by,
})
}
}
impl Cap {
pub fn new(urn: CapUrn, title: String, command: String) -> Self {
Self {
urn,
title,
cap_description: None,
metadata: HashMap::new(),
command,
media_specs: HashMap::new(),
args: Vec::new(),
output: None,
metadata_json: None,
registered_by: None,
}
}
pub fn with_description(urn: CapUrn, title: String, command: String, description: String) -> Self {
Self {
urn,
title,
cap_description: Some(description),
metadata: HashMap::new(),
command,
media_specs: HashMap::new(),
args: Vec::new(),
output: None,
metadata_json: None,
registered_by: None,
}
}
pub fn with_metadata(
urn: CapUrn,
title: String,
command: String,
metadata: HashMap<String, String>
) -> Self {
Self {
urn,
title,
cap_description: None,
metadata,
command,
media_specs: HashMap::new(),
args: Vec::new(),
output: None,
metadata_json: None,
registered_by: None,
}
}
pub fn with_description_and_metadata(
urn: CapUrn,
title: String,
command: String,
description: String,
metadata: HashMap<String, String>,
) -> Self {
Self {
urn,
title,
cap_description: Some(description),
metadata,
command,
media_specs: HashMap::new(),
args: Vec::new(),
output: None,
metadata_json: None,
registered_by: None,
}
}
pub fn with_args(
urn: CapUrn,
title: String,
command: String,
args: Vec<CapArg>,
) -> Self {
Self {
urn,
title,
cap_description: None,
metadata: HashMap::new(),
command,
media_specs: HashMap::new(),
args,
output: None,
metadata_json: None,
registered_by: None,
}
}
pub fn with_full_definition(
urn: CapUrn,
title: String,
description: Option<String>,
metadata: HashMap<String, String>,
command: String,
media_specs: HashMap<String, MediaSpecDef>,
args: Vec<CapArg>,
output: Option<CapOutput>,
metadata_json: Option<serde_json::Value>,
) -> Self {
Self {
urn,
title,
cap_description: description,
metadata,
command,
media_specs,
args,
output,
metadata_json,
registered_by: None,
}
}
pub fn get_stdin_media_urn(&self) -> Option<&str> {
for arg in &self.args {
for source in &arg.sources {
if let ArgSource::Stdin { stdin } = source {
return Some(stdin);
}
}
}
None
}
pub fn accepts_stdin(&self) -> bool {
self.get_stdin_media_urn().is_some()
}
pub fn get_media_specs(&self) -> &HashMap<String, MediaSpecDef> {
&self.media_specs
}
pub fn set_media_specs(&mut self, media_specs: HashMap<String, MediaSpecDef>) {
self.media_specs = media_specs;
}
pub fn add_media_spec(&mut self, spec_id: impl Into<String>, def: MediaSpecDef) {
self.media_specs.insert(spec_id.into(), def);
}
pub fn resolve_media_urn(&self, spec_id: &str) -> Result<ResolvedMediaSpec, MediaSpecError> {
resolve_media_urn(spec_id, &self.media_specs)
}
pub fn matches_request(&self, request: &str) -> bool {
let request_urn = CapUrn::from_string(request).expect("Invalid cap URN in request");
self.urn.can_handle(&request_urn)
}
pub fn urn_string(&self) -> String {
self.urn.to_string()
}
pub fn is_more_specific_than(&self, other: &Cap, request: &str) -> bool {
if !self.matches_request(request) || !other.matches_request(request) {
return false;
}
self.urn.is_more_specific_than(&other.urn)
}
pub fn get_metadata(&self, key: &str) -> Option<&String> {
self.metadata.get(key)
}
pub fn set_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
}
pub fn remove_metadata(&mut self, key: &str) -> Option<String> {
self.metadata.remove(key)
}
pub fn has_metadata(&self, key: &str) -> bool {
self.metadata.contains_key(key)
}
pub fn get_registered_by(&self) -> Option<&RegisteredBy> {
self.registered_by.as_ref()
}
pub fn set_registered_by(&mut self, registered_by: RegisteredBy) {
self.registered_by = Some(registered_by);
}
pub fn clear_registered_by(&mut self) {
self.registered_by = None;
}
pub fn get_command(&self) -> &String {
&self.command
}
pub fn set_command(&mut self, command: String) {
self.command = command;
}
pub fn get_title(&self) -> &String {
&self.title
}
pub fn set_title(&mut self, title: String) {
self.title = title;
}
pub fn get_args(&self) -> &Vec<CapArg> {
&self.args
}
pub fn set_args(&mut self, args: Vec<CapArg>) {
self.args = args;
}
pub fn add_arg(&mut self, arg: CapArg) {
self.args.push(arg);
}
pub fn get_output(&self) -> Option<&CapOutput> {
self.output.as_ref()
}
pub fn set_output(&mut self, output: CapOutput) {
self.output = Some(output);
}
pub fn get_metadata_json(&self) -> Option<&serde_json::Value> {
self.metadata_json.as_ref()
}
pub fn set_metadata_json(&mut self, metadata: serde_json::Value) {
self.metadata_json = Some(metadata);
}
pub fn clear_metadata_json(&mut self) {
self.metadata_json = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_urn(tags: &str) -> String {
format!("cap:in=media:void;out=media:object;{}", tags)
}
#[test]
fn test_cap_creation() {
let urn = CapUrn::from_string(&test_urn("op=transform;format=json;data_processing")).unwrap();
let cap = Cap::new(urn, "Transform JSON Data".to_string(), "test-command".to_string());
assert!(cap.urn_string().contains("op=transform"));
assert!(cap.urn_string().contains("in=media:void"));
assert!(cap.urn_string().contains("out=media:object"));
assert_eq!(cap.title, "Transform JSON Data");
assert!(cap.metadata.is_empty());
}
#[test]
fn test_cap_with_metadata() {
let urn = CapUrn::from_string(&test_urn("op=arithmetic;compute;subtype=math")).unwrap();
let mut metadata = HashMap::new();
metadata.insert("precision".to_string(), "double".to_string());
metadata.insert("operations".to_string(), "add,subtract,multiply,divide".to_string());
let cap = Cap::with_metadata(urn, "Perform Mathematical Operations".to_string(), "test-command".to_string(), metadata);
assert_eq!(cap.title, "Perform Mathematical Operations");
assert_eq!(cap.get_metadata("precision"), Some(&"double".to_string()));
assert_eq!(cap.get_metadata("operations"), Some(&"add,subtract,multiply,divide".to_string()));
assert!(cap.has_metadata("precision"));
assert!(!cap.has_metadata("nonexistent"));
}
#[test]
fn test_cap_matching() {
let urn = CapUrn::from_string(&test_urn("op=transform;format=json;type=data_processing")).unwrap();
let cap = Cap::new(urn, "Transform JSON Data".to_string(), "test-command".to_string());
assert!(cap.matches_request(&test_urn("op=transform;format=json;type=data_processing")));
assert!(cap.matches_request(&test_urn("op=transform;format=*;type=data_processing")));
assert!(cap.matches_request(&test_urn("type=data_processing")));
assert!(!cap.matches_request(&test_urn("type=compute")));
}
#[test]
fn test_cap_title() {
let urn = CapUrn::from_string(&test_urn("op=extract;target=metadata")).unwrap();
let mut cap = Cap::new(urn, "Extract Document Metadata".to_string(), "extract-metadata".to_string());
assert_eq!(cap.get_title(), &"Extract Document Metadata".to_string());
assert_eq!(cap.title, "Extract Document Metadata");
cap.set_title("Extract File Metadata".to_string());
assert_eq!(cap.get_title(), &"Extract File Metadata".to_string());
assert_eq!(cap.title, "Extract File Metadata");
}
#[test]
fn test_cap_definition_equality() {
let urn1 = CapUrn::from_string(&test_urn("op=transform;format=json")).unwrap();
let urn2 = CapUrn::from_string(&test_urn("op=transform;format=json")).unwrap();
let cap1 = Cap::new(urn1, "Transform JSON Data".to_string(), "transform".to_string());
let cap2 = Cap::new(urn2.clone(), "Transform JSON Data".to_string(), "transform".to_string());
let cap3 = Cap::new(urn2, "Convert JSON Format".to_string(), "transform".to_string());
assert_eq!(cap1, cap2);
assert_ne!(cap1, cap3);
assert_ne!(cap2, cap3);
}
#[test]
fn test_cap_stdin() {
let urn = CapUrn::from_string(&test_urn("op=generate;target=embeddings")).unwrap();
let mut cap = Cap::new(urn, "Generate Embeddings".to_string(), "generate".to_string());
assert!(!cap.accepts_stdin());
assert!(cap.get_stdin_media_urn().is_none());
let stdin_arg = CapArg {
media_urn: "media:text;textable".to_string(),
required: true,
sources: vec![ArgSource::Stdin { stdin: "media:text;textable".to_string() }],
arg_description: Some("Input text".to_string()),
validation: ArgumentValidation::default(),
default_value: None,
metadata: None,
};
cap.add_arg(stdin_arg);
assert!(cap.accepts_stdin());
assert_eq!(cap.get_stdin_media_urn(), Some("media:text;textable"));
let serialized = serde_json::to_string(&cap).unwrap();
assert!(serialized.contains("\"args\""));
assert!(serialized.contains("\"stdin\""));
let deserialized: Cap = serde_json::from_str(&serialized).unwrap();
assert!(deserialized.accepts_stdin());
assert_eq!(deserialized.get_stdin_media_urn(), Some("media:text;textable"));
}
#[test]
fn test_arg_source_types() {
let stdin_source = ArgSource::Stdin { stdin: "media:text".to_string() };
assert_eq!(stdin_source.get_type(), "stdin");
assert_eq!(stdin_source.stdin_media_urn(), Some("media:text"));
assert_eq!(stdin_source.position(), None);
assert_eq!(stdin_source.cli_flag(), None);
let position_source = ArgSource::Position { position: 0 };
assert_eq!(position_source.get_type(), "position");
assert_eq!(position_source.stdin_media_urn(), None);
assert_eq!(position_source.position(), Some(0));
assert_eq!(position_source.cli_flag(), None);
let cli_flag_source = ArgSource::CliFlag { cli_flag: "--input".to_string() };
assert_eq!(cli_flag_source.get_type(), "cli_flag");
assert_eq!(cli_flag_source.stdin_media_urn(), None);
assert_eq!(cli_flag_source.position(), None);
assert_eq!(cli_flag_source.cli_flag(), Some("--input"));
}
#[test]
fn test_cap_arg_serialization() {
let arg = CapArg {
media_urn: "media:string".to_string(),
required: true,
sources: vec![
ArgSource::CliFlag { cli_flag: "--name".to_string() },
ArgSource::Position { position: 0 },
],
arg_description: Some("The name argument".to_string()),
validation: ArgumentValidation::default(),
default_value: None,
metadata: None,
};
let serialized = serde_json::to_string(&arg).unwrap();
assert!(serialized.contains("\"media_urn\":\"media:string\""));
assert!(serialized.contains("\"required\":true"));
assert!(serialized.contains("\"cli_flag\":\"--name\""));
assert!(serialized.contains("\"position\":0"));
let deserialized: CapArg = serde_json::from_str(&serialized).unwrap();
assert_eq!(arg, deserialized);
}
#[test]
fn test_cap_arg_constructors() {
let arg = CapArg::new(
"media:string",
true,
vec![ArgSource::CliFlag { cli_flag: "--name".to_string() }],
);
assert_eq!(arg.media_urn, "media:string");
assert!(arg.required);
assert_eq!(arg.sources.len(), 1);
assert!(arg.arg_description.is_none());
let arg = CapArg::with_description(
"media:integer",
false,
vec![ArgSource::Position { position: 0 }],
"The count argument",
);
assert_eq!(arg.media_urn, "media:integer");
assert!(!arg.required);
assert_eq!(arg.arg_description, Some("The count argument".to_string()));
}
}