1use std::collections::HashMap;
9use std::sync::Arc;
10
11use arrow::datatypes::{DataType, Field};
12
13#[derive(Debug, Clone)]
15pub struct ExtensionRow {
16 pub fields: HashMap<String, ExtensionValue>,
18}
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum ExtensionValue {
23 Str(String),
24 OptStr(Option<String>),
25 U32(u32),
26 StrList(Vec<String>),
27 Bytes(Vec<u8>),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Col {
33 Utf8,
35 U32,
37 Utf8List,
39}
40
41impl Col {
42 pub fn data_type(self) -> DataType {
43 match self {
44 Col::Utf8 => DataType::Utf8,
45 Col::U32 => DataType::UInt32,
46 Col::Utf8List => DataType::List(Arc::new(Field::new("item", DataType::Utf8, true))),
47 }
48 }
49}
50
51#[derive(Debug)]
53pub struct HandlerSpec {
54 pub name: &'static str,
56 pub aliases: &'static [&'static str],
58 pub type_id: i8,
60 pub ecosystem: &'static str,
62 pub extensions: &'static [&'static str],
64 pub description: &'static str,
66 pub fields: &'static [(&'static str, Col)],
68 pub commands: &'static [(&'static str, &'static str)],
70}
71
72impl HandlerSpec {
73 pub fn claims(&self, path: &str) -> bool {
75 self.extensions.iter().any(|e| path.ends_with(e))
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct HandlerCommand {
82 pub name: String,
84 pub about: String,
86}
87
88impl HandlerCommand {
89 pub fn new(name: impl Into<String>, about: impl Into<String>) -> Self {
90 Self { name: name.into(), about: about.into() }
91 }
92}
93
94#[derive(Debug, Clone)]
97pub struct HandlerMeta {
98 pub name: String,
99 pub aliases: Vec<String>,
100 pub type_id: i8,
101 pub ecosystem: String,
102 pub extensions: Vec<String>,
103 pub description: String,
104 pub commands: Vec<HandlerCommand>,
105}
106
107pub trait ArchiveTypePlugin: Send + Sync {
109 fn spec(&self) -> &HandlerSpec;
111
112 fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow>;
114
115 fn run_command(&self, cmd: &str, args: &[String]) -> anyhow::Result<()> {
117 default_command(self, cmd, args)
118 }
119
120 fn matches_path(&self, path: &str) -> bool {
122 self.spec().claims(path)
123 }
124
125 fn schema_fields(&self) -> Vec<Field> {
127 self.spec().fields.iter().map(|(n, c)| Field::new(*n, c.data_type(), true)).collect()
128 }
129
130 fn name(&self) -> &str {
131 self.spec().name
132 }
133
134 fn type_id(&self) -> i8 {
135 self.spec().type_id
136 }
137
138 fn meta(&self) -> HandlerMeta {
140 let s = self.spec();
141 let owned = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
142 let mut commands = Vec::new();
143 if !self.schema_fields().is_empty() {
144 commands.push(HandlerCommand::new("coords", format!("Print {} coords read from a file", s.name)));
145 }
146 commands.extend(s.commands.iter().map(|(n, a)| HandlerCommand::new(*n, *a)));
147 HandlerMeta {
148 name: s.name.into(),
149 aliases: owned(s.aliases),
150 type_id: s.type_id,
151 ecosystem: s.ecosystem.into(),
152 extensions: owned(s.extensions),
153 description: s.description.into(),
154 commands,
155 }
156 }
157}
158
159pub fn default_command<P: ArchiveTypePlugin + ?Sized>(p: &P, cmd: &str, args: &[String]) -> anyhow::Result<()> {
164 if cmd != "coords" {
165 anyhow::bail!("handler '{}' has no subcommand '{}'", p.name(), cmd);
166 }
167 let path = args.first().ok_or_else(|| anyhow::anyhow!("usage: {} coords <file>", p.name()))?;
168 let data = std::fs::read(path).unwrap_or_default();
169 let row = p
170 .extract_metadata(path, &data)
171 .ok_or_else(|| anyhow::anyhow!("{}: no coords in '{}'", p.name(), path))?;
172 let fields = p.schema_fields();
173 let end = fields.iter().position(|f| f.name() == "version").map_or(1, |i| i + 1);
174 let out: Vec<&str> = fields[..end.min(fields.len())]
175 .iter()
176 .filter_map(|f| match row.fields.get(f.name()) {
177 Some(ExtensionValue::Str(s)) | Some(ExtensionValue::OptStr(Some(s))) => Some(s.as_str()),
178 _ => None,
179 })
180 .collect();
181 println!("{}", out.join(" "));
182 Ok(())
183}
184
185pub fn file_name(path: &str) -> &str {
187 path.rsplit('/').next().unwrap_or(path)
188}
189
190pub struct PluginRegistry {
195 plugin: Option<Box<dyn ArchiveTypePlugin>>,
196}
197
198impl PluginRegistry {
199 pub fn new() -> Self {
200 Self { plugin: None }
201 }
202
203 pub fn with_plugin(plugin: Box<dyn ArchiveTypePlugin>) -> Self {
204 Self { plugin: Some(plugin) }
205 }
206
207 pub fn extract(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
209 self.extract_typed(path, data).map(|(_, row)| row)
210 }
211
212 pub fn extract_typed(&self, path: &str, data: &[u8]) -> Option<(i8, ExtensionRow)> {
214 let p = self.plugin.as_ref()?;
215 if !p.matches_path(path) {
216 return None;
217 }
218 Some((p.type_id(), p.extract_metadata(path, data)?))
219 }
220
221 pub fn type_id(&self) -> Option<i8> {
222 self.plugin.as_ref().map(|p| p.type_id())
223 }
224
225 pub fn schema_fields(&self) -> Vec<Field> {
227 self.plugin.as_ref().map(|p| p.schema_fields()).unwrap_or_default()
228 }
229
230 pub fn matches(&self, path: &str) -> bool {
231 self.plugin.as_ref().map(|p| p.matches_path(path)).unwrap_or(false)
232 }
233}
234
235impl Default for PluginRegistry {
236 fn default() -> Self {
237 Self::new()
238 }
239}