diff --git a/crates/web5_cli/src/main.rs b/crates/web5_cli/src/main.rs index a28a2215..537f167d 100644 --- a/crates/web5_cli/src/main.rs +++ b/crates/web5_cli/src/main.rs @@ -1,4 +1,5 @@ mod dids; +mod pds; mod test; mod utils; mod vcs; @@ -25,6 +26,10 @@ enum Commands { #[command(subcommand)] vc_command: vcs::Commands, }, + Pd { + #[command(subcommand)] + pd_command: pds::Commands, + }, } #[tokio::main] @@ -34,5 +39,6 @@ async fn main() { match cli.command { Commands::Did { did_command } => did_command.command().await, Commands::Vc { vc_command } => vc_command.command().await, + Commands::Pd { pd_command } => pd_command.command().await, } } diff --git a/crates/web5_cli/src/pds/create.rs b/crates/web5_cli/src/pds/create.rs new file mode 100644 index 00000000..d9573bfa --- /dev/null +++ b/crates/web5_cli/src/pds/create.rs @@ -0,0 +1,234 @@ +use clap::{arg, Args, FromArgMatches}; +use web5::credentials::presentation_definition::*; + +#[derive(Debug)] +pub struct CreatePresentationDefinition(PresentationDefinition); + +impl CreatePresentationDefinition { + pub fn get_output(self) -> String { + serde_json::to_string(&self.0).unwrap() + } +} + +impl Args for CreatePresentationDefinition { + fn augment_args(cmd: clap::Command) -> clap::Command { + cmd.arg(arg!( "A unique identifier that distinguishes this PD from others")) + .arg(arg!(-n --name [name] "A human-friendly name for easier identification of the PD").required(false)) + .arg(arg!(-p --purpose [purpose] "A description outlining why the information requested by the PD is needed").required(false)) + .arg(arg!(-i --"input-descriptor" [input_descriptor] ... "Required claims and specifications on exactly how they will be evaluated").required(false)) + .next_help_heading("Input Descriptor Info") + .arg(arg!(-d --field [field] ... "Represents a specific piece of data that the PD is interested in. Each field can have its own constraints.").required(false)) + .next_help_heading("Field Constraint Info") + .arg(arg!(-f --filter [filter] ... "The specific conditions that the data in the specified path must satisfy").required(false)) + } + + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + Self::augment_args(cmd) + } +} + +impl FromArgMatches for CreatePresentationDefinition { + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + let id = matches.get_one::("id").ok_or(clap::Error::new( + clap::error::ErrorKind::MissingRequiredArgument, + ))?; + let name = matches.get_one::("name"); + let purpose = matches.get_one::("purpose"); + + let ids_idx = matches + .indices_of("input-descriptor") + .unwrap_or_default() + .collect::>(); + let ids = matches + .get_many::("input-descriptor") + .unwrap_or_default() + .collect::>(); + let ids_count = ids.len(); + let fds_idx = matches + .indices_of("field") + .unwrap_or_default() + .collect::>(); + let fds = matches + .get_many::("field") + .unwrap_or_default() + .collect::>(); + let fds_count = fds.len(); + let fts_idx = matches + .indices_of("filter") + .unwrap_or_default() + .collect::>(); + let fts = matches + .get_many::("filter") + .unwrap_or_default() + .collect::>(); + let fts_count = fts.len(); + + // for each input descriptor location on CLI + let mut current_fd = 0; + let mut current_ft = 0; + let mut input_descriptors = Vec::new(); + for (i, _) in ids_idx.iter().enumerate() { + let mut input_descriptor = parse_input_descriptor(ids[i]); + // for each field within current and next input descriptor index + while i + 1 < ids_count && fds_idx[current_fd] < ids_idx[i + 1] { + let mut field = parse_field(fds[current_fd]); + // for each filter within current and next field index + while current_fd + 1 < fds_count + && current_ft + 1 < fts_count + && fts_idx[current_ft] < fds_idx[current_fd + 1] + && fts_idx[current_ft] < ids_idx[i + 1] + { + let filter = parse_filter(fts[current_ft]); + field.filter = Some(filter); + current_ft += 1; + } + + input_descriptor.constraints.fields.push(field); + current_fd += 1; + } + + input_descriptors.push(input_descriptor); + } + + let definition = PresentationDefinition { + id: id.clone(), + name: name.cloned(), + purpose: purpose.cloned(), + input_descriptors, + submission_requirements: None, + }; + + Ok(CreatePresentationDefinition(definition)) + } + + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + self.0.id = matches + .get_one::("id") + .ok_or(clap::Error::new( + clap::error::ErrorKind::MissingRequiredArgument, + ))? + .to_string(); + self.0.name = matches.get_one::("name").cloned(); + self.0.purpose = matches.get_one::("purpose").cloned(); + + let ids_idx = matches + .indices_of("input-descriptor") + .unwrap_or_default() + .collect::>(); + let ids = matches + .get_many::("input-descriptor") + .unwrap_or_default() + .collect::>(); + let ids_count = ids.len(); + let fds_idx = matches + .indices_of("field") + .unwrap_or_default() + .collect::>(); + let fds = matches + .get_many::("field") + .unwrap_or_default() + .collect::>(); + let fds_count = fds.len(); + let fts_idx = matches + .indices_of("filter") + .unwrap_or_default() + .collect::>(); + let fts = matches + .get_many::("filter") + .unwrap_or_default() + .collect::>(); + let fts_count = fts.len(); + + // for each input descriptor location on CLI + let mut current_fd = 0; + let mut current_ft = 0; + for (i, _) in ids_idx.iter().enumerate() { + let mut input_descriptor = parse_input_descriptor(ids[i]); + // for each field within current and next input descriptor index + while i + 1 < ids_count && fds_idx[current_fd] < ids_idx[i + 1] { + let mut field = parse_field(fds[current_fd]); + // for each filter within current and next field index + while current_fd + 1 < fds_count + && current_ft + 1 < fts_count + && fts_idx[current_ft] < fds_idx[current_fd + 1] + && fts_idx[current_ft] < ids_idx[i + 1] + { + let filter = parse_filter(fts[current_ft]); + field.filter = Some(filter); + current_ft += 1; + } + + input_descriptor.constraints.fields.push(field); + current_fd += 1; + } + + self.0.input_descriptors.push(input_descriptor); + } + + Ok(()) + } +} + +fn parse_input_descriptor(value: &String) -> InputDescriptor { + let mut splits = value.split(":"); + let id = splits.next().unwrap(); + let name = splits.next().unwrap_or_default(); + let purpose = splits.next().unwrap_or_default(); + + InputDescriptor { + id: id.to_string(), + name: str_to_option_string(name), + purpose: str_to_option_string(purpose), + constraints: Constraints { fields: Vec::new() }, + } +} + +fn parse_field(value: &String) -> Field { + let mut splits = value.split(":"); + let id = splits.next().unwrap_or_default(); + let name = splits.next().unwrap_or_default(); + let path = splits.next().unwrap_or_default(); + let purpose = splits.next().unwrap_or_default(); + let optional = splits.next().unwrap_or_default(); + let predicate = splits.next().unwrap_or_default(); + + Field { + id: str_to_option_string(id), + name: str_to_option_string(name), + path: path.split(',').map(String::from).collect(), + purpose: str_to_option_string(purpose), + filter: None, + optional: (!optional.is_empty()).then(|| optional == "true"), + predicate: str_to_optionality(predicate), + } +} + +fn parse_filter(value: &str) -> Filter { + let mut splits = value.split(":"); + let kind = splits.next().unwrap_or_default(); + let pattern = splits.next().unwrap_or_default(); + let value = splits.next().unwrap_or_default(); + + Filter { + r#type: str_to_option_string(kind), + pattern: str_to_option_string(pattern), + const_value: str_to_option_string(value), + contains: None, + } +} + +fn str_to_optionality(value: &str) -> Option { + match value { + "required" => Some(Optionality::Required), + "preferred" => Some(Optionality::Preferred), + _ => None, + } +} + +fn str_to_option_string(value: &str) -> Option { + (!value.is_empty()).then(|| value.to_string()) +} + +pub fn run_create_command(args: CreatePresentationDefinition) { + println!("{}", args.get_output()); +} diff --git a/crates/web5_cli/src/pds/mod.rs b/crates/web5_cli/src/pds/mod.rs new file mode 100644 index 00000000..18cc83f2 --- /dev/null +++ b/crates/web5_cli/src/pds/mod.rs @@ -0,0 +1,45 @@ +use clap::Subcommand; + +mod create; + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Creates a Presentation Definition + /// + /// The position of the input-descriptor, field, and filter flags matter in this command. + /// Nested attributes such as field and filter are associated and nested within the + /// last input-descriptor or field seen on the CLI. + /// + /// The values of input descriptor are expected to be a colon-separated string in the + /// order of "id:name:purpose". Values can be ommitted if they aren't needed. + /// + /// The values of field are expected to be a colon-separated string in the + /// order of "id:name:path:purpose:optional:predicate". Values can be ommitted + /// if they aren't needed. + /// + /// The values of filter are expected to be a colon-separated string in the + /// order of "kind:pattern:value". Values can be ommitted if they aren't needed. + /// + /// Example: + /// + /// web5 pd create my-pd-3 \ + /// --name "Complex PD" \ + /// --purpose "Comprehensive Verification" \ + /// --input-descriptor "input-1:Personal Info:Verify personal information" \ + /// --field "field-1:Name:$.credentialSubject.name:Verify name:false:required" \ + /// --filter "string:^[A-Za-z ]+$" \ + /// --field "field-2:Age:$.credentialSubject.age:Verify age::preferred" \ + /// --filter "number:^[0-9]+$" \ + /// --input-descriptor "input-2:Address:Verify address" \ + /// --field "field-3:Street:$.credentialSubject.address.street:Verify street:false:required" + #[command(verbatim_doc_comment)] + Create(create::CreatePresentationDefinition), +} + +impl Commands { + pub async fn command(self) { + match self { + Commands::Create(args) => create::run_create_command(args), + }; + } +}