1use clap::{Arg, ArgAction, ArgMatches, Command};
2
3use crate::Data;
4
5use crate::cmd::convert::detect_format;
6
7pub fn command() -> Command {
8 Command::new("put")
9 .about("Put scholarly metadata into a service")
10 .long_about(
11 "Convert scholarly metadata between formats and register a single record\n\
12 with a service. Registration is currently only supported with InvenioRDM.\n\n\
13 The input is a file path, DOI, or URL. When --from is omitted the format\n\
14 is auto-detected: DOIs are resolved via the DOI RA API; JSON files are\n\
15 inspected for schema markers.\n\n\
16 This performs a real, network-visible write: a live record is created or\n\
17 updated and published on --host using --token for authentication.\n\n\
18 Examples:\n\n\
19 commonmeta put 10.5555/12345678 --from crossref --to inveniordm --host rogue-scholar.org --token TOKEN\n\
20 commonmeta put record.json --from commonmeta --to inveniordm --host my.invenio.host --token TOKEN",
21 )
22 .arg(
23 Arg::new("input")
24 .help("File path, DOI, or URL")
25 .required(true)
26 .index(1),
27 )
28 .arg(
29 Arg::new("from")
30 .long("from")
31 .short('f')
32 .help("Input format; auto-detected if omitted"),
33 )
34 .arg(
35 Arg::new("to")
36 .long("to")
37 .short('t')
38 .help("Target service to register with")
39 .default_value("inveniordm"),
40 )
41 .arg(Arg::new("doi").long("doi").help("DOI to assign"))
42 .arg(Arg::new("prefix").long("prefix").help("DOI prefix"))
43 .arg(
44 Arg::new("depositor")
45 .long("depositor")
46 .help("Depositor name (used for Crossref XML registration)"),
47 )
48 .arg(
49 Arg::new("email")
50 .long("email")
51 .help("Depositor email (used for Crossref XML registration)"),
52 )
53 .arg(
54 Arg::new("registrant")
55 .long("registrant")
56 .help("Registrant name (used for Crossref XML registration)"),
57 )
58 .arg(
59 Arg::new("login-id")
60 .long("login-id")
61 .help("Login ID for Crossref XML deposit"),
62 )
63 .arg(
64 Arg::new("login-passwd")
65 .long("login-passwd")
66 .help("Login password for Crossref XML deposit"),
67 )
68 .arg(
69 Arg::new("test-mode")
70 .long("test-mode")
71 .help("Use test mode for Crossref XML deposit")
72 .action(ArgAction::SetTrue),
73 )
74 .arg(
75 Arg::new("legacy-conn")
76 .long("legacy-conn")
77 .help("Legacy connection string"),
78 )
79 .arg(
80 Arg::new("host")
81 .long("host")
82 .help("InvenioRDM host (e.g. rogue-scholar.org)"),
83 )
84 .arg(Arg::new("token").long("token").help("InvenioRDM API token"))
85 .arg(
86 Arg::new("show-errors")
87 .long("show-errors")
88 .help("Print validation errors")
89 .action(ArgAction::SetTrue),
90 )
91}
92
93pub fn execute(matches: &ArgMatches) -> Result<(), String> {
94 let input_arg = matches.get_one::<String>("input").expect("required");
95 let to = matches
96 .get_one::<String>("to")
97 .map(String::as_str)
98 .unwrap_or("inveniordm");
99
100 let input = if std::path::Path::new(input_arg).exists() {
101 std::fs::read_to_string(input_arg)
102 .map_err(|e| format!("failed to read '{}': {}", input_arg, e))?
103 } else {
104 input_arg.clone()
105 };
106
107 let via = match matches.get_one::<String>("from") {
108 Some(f) => f.clone(),
109 None => detect_format(&input, false),
110 };
111
112 let data = crate::read(&via, &input).map_err(|e| e.to_string())?;
113
114 match to {
115 "inveniordm" => put_to_inveniordm(&data, matches),
116 "crossref_xml" | "datacite" => Err(format!(
117 "put: --to {} is not yet implemented (registration is currently only supported with --to inveniordm)",
118 to
119 )),
120 other => Err(format!("put: unsupported --to target: {}", other)),
121 }
122}
123
124fn put_to_inveniordm(data: &Data, matches: &ArgMatches) -> Result<(), String> {
125 let host = matches
126 .get_one::<String>("host")
127 .map(String::as_str)
128 .filter(|s| !s.is_empty())
129 .ok_or_else(|| "put: --to inveniordm requires --host <host>".to_string())?;
130 let token = matches
131 .get_one::<String>("token")
132 .map(String::as_str)
133 .filter(|s| !s.is_empty())
134 .ok_or_else(|| "put: --to inveniordm requires --token <token>".to_string())?;
135
136 let result = crate::put_inveniordm(data, host, token);
137 let output = serde_json::to_string_pretty(&result).map_err(|e| e.to_string())?;
138 println!("{}", output);
139 Ok(())
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_execute_requires_host_for_inveniordm() {
148 let dir = std::env::temp_dir().join("commonmeta_put_test_host");
149 std::fs::create_dir_all(&dir).unwrap();
150 let path = dir.join("record.json");
151 std::fs::write(
152 &path,
153 r#"{"id":"https://doi.org/10.1/a","type":"JournalArticle","schema_version":"https://commonmeta.org/commonmeta_v1.0.json"}"#,
154 )
155 .unwrap();
156
157 let matches =
158 command().get_matches_from(vec!["put", path.to_str().unwrap(), "--from", "commonmeta"]);
159 let err = execute(&matches).unwrap_err();
160 assert!(err.contains("requires --host"));
161
162 std::fs::remove_dir_all(&dir).ok();
163 }
164
165 #[test]
166 fn test_execute_rejects_unimplemented_to() {
167 let dir = std::env::temp_dir().join("commonmeta_put_test_to");
168 std::fs::create_dir_all(&dir).unwrap();
169 let path = dir.join("record.json");
170 std::fs::write(
171 &path,
172 r#"{"id":"https://doi.org/10.1/a","type":"JournalArticle","schema_version":"https://commonmeta.org/commonmeta_v1.0.json"}"#,
173 )
174 .unwrap();
175
176 let matches = command().get_matches_from(vec![
177 "put",
178 path.to_str().unwrap(),
179 "--from",
180 "commonmeta",
181 "--to",
182 "crossref_xml",
183 ]);
184 let err = execute(&matches).unwrap_err();
185 assert!(err.contains("not yet implemented"));
186
187 std::fs::remove_dir_all(&dir).ok();
188 }
189
190 #[test]
191 fn test_execute_rejects_missing_token() {
192 let dir = std::env::temp_dir().join("commonmeta_put_test_token");
193 std::fs::create_dir_all(&dir).unwrap();
194 let path = dir.join("record.json");
195 std::fs::write(
196 &path,
197 r#"{"id":"https://doi.org/10.1/a","type":"JournalArticle","schema_version":"https://commonmeta.org/commonmeta_v1.0.json"}"#,
198 )
199 .unwrap();
200
201 let matches = command().get_matches_from(vec![
202 "put",
203 path.to_str().unwrap(),
204 "--from",
205 "commonmeta",
206 "--host",
207 "example.invenio.host",
208 ]);
209 let err = execute(&matches).unwrap_err();
210 assert!(err.contains("requires --token"));
211
212 std::fs::remove_dir_all(&dir).ok();
213 }
214
215 #[test]
216 fn test_execute_file_not_found_reads_as_literal_input() {
217 let matches = command().get_matches_from(vec![
220 "put",
221 "not-a-real-path-or-doi",
222 "--from",
223 "commonmeta",
224 ]);
225 let err = execute(&matches).unwrap_err();
229 assert!(!err.contains("failed to read"));
230 }
231}