1#![allow(
2 unknown_lints,
3 clippy::match_same_arms,
4 clippy::similar_names,
5 clippy::only_used_in_recursion,
6 clippy::option_if_let_else,
7 clippy::collapsible_else_if,
8 clippy::branches_sharing_code,
9 clippy::explicit_iter_loop,
10 clippy::manual_let_else,
11 clippy::hashset_insert_after_contains,
12 clippy::set_contains_or_insert
13)]
14
15pub mod annotation;
37pub mod api;
39pub mod config;
41pub mod data_schema;
43pub mod data_science;
45pub mod database;
47pub mod domain;
49pub mod emit;
51pub mod error;
53pub mod raw_file;
55pub mod serialization;
57pub mod theories;
59pub mod web_document;
61
62use panproto_schema::Schema;
63
64pub use error::ProtocolError;
65
66pub use web_document::atproto;
68
69pub fn parse_schema_bundle(
90 protocol: &str,
91 docs: &[serde_json::Value],
92) -> Result<Schema, ProtocolError> {
93 match protocol {
94 "atproto" => atproto::parse_lexicon_bundle(docs),
95 other => Err(ProtocolError::Parse(format!(
96 "no bundle parser registered for protocol {other:?}; supported: [\"atproto\"]"
97 ))),
98 }
99}
100
101#[must_use]
106pub const fn bundle_parser_protocols() -> &'static [&'static str] {
107 &["atproto"]
108}
109
110pub fn parse_schema_bundle_project(
130 protocol: &str,
131 docs: &[(std::path::PathBuf, serde_json::Value)],
132) -> Result<atproto::LexiconProject, ProtocolError> {
133 match protocol.replace('_', "-").as_str() {
134 "atproto" => {
135 let lexicon_docs: Vec<atproto::LexiconDoc> = docs
136 .iter()
137 .map(|(path, value)| atproto::LexiconDoc {
138 path: path.clone(),
139 value: value.clone(),
140 })
141 .collect();
142 atproto::parse_lexicon_project(&lexicon_docs)
143 }
144 other => Err(ProtocolError::Parse(format!(
145 "no per-file bundle parser registered for protocol {other:?}; supported: [\"atproto\"]"
146 ))),
147 }
148}
149
150#[must_use]
153pub const fn bundle_project_protocols() -> &'static [&'static str] {
154 &["atproto"]
155}
156
157pub fn parse_schema_document(
180 protocol: &str,
181 doc: &serde_json::Value,
182) -> Result<Schema, ProtocolError> {
183 match protocol.replace('_', "-").as_str() {
184 "amr" => annotation::amr::parse_amr_schema(doc),
186 "bead" => annotation::bead::parse_bead(doc),
187 "brat" => annotation::brat::parse_brat(doc),
188 "concrete" => annotation::concrete::parse_concrete_schema(doc),
189 "decomp" => annotation::decomp::parse_decomp(doc),
190 "elan" => annotation::elan::parse_elan(doc),
191 "folia" => annotation::folia::parse_folia(doc),
192 "fovea" => annotation::fovea::parse_fovea(doc),
193 "iso-space" => annotation::iso_space::parse_iso_space(doc),
194 "laf-graf" => annotation::laf_graf::parse_laf_graf(doc),
195 "naf" => annotation::naf::parse_naf(doc),
196 "nif" => annotation::nif::parse_nif_schema(doc),
197 "paula" => annotation::paula::parse_paula_schema(doc),
198 "tei" => annotation::tei::parse_tei(doc),
199 "timeml" => annotation::timeml::parse_timeml(doc),
200 "ucca" => annotation::ucca::parse_ucca(doc),
201 "uima" | "uima-cas" => annotation::uima::parse_uima_schema(doc),
202 "web-annotation" => annotation::web_annotation::parse_web_annotation_schema(doc),
203 "asyncapi" => api::asyncapi::parse_asyncapi(doc),
205 "jsonapi" => api::jsonapi::parse_jsonapi(doc),
206 "openapi" => api::openapi::parse_openapi(doc),
207 "raml" => api::raml::parse_raml_schema(doc),
208 "ansible" => config::ansible::parse_ansible_schema(doc),
210 "cloudformation" => config::cloudformation::parse_cfn_schema(doc),
211 "k8s-crd" => config::k8s_crd::parse_k8s_crd_schema(doc),
212 "bson" => data_schema::bson::parse_bson_schema(doc),
214 "json-schema" => data_schema::json_schema::parse_json_schema(doc),
215 "arrow" => data_science::arrow::parse_arrow_schema(doc),
217 "dataframe" => data_science::dataframe::parse_dataframe_schema(doc),
218 "parquet" => data_science::parquet::parse_parquet_schema(doc),
219 "dynamodb" => database::dynamodb::parse_dynamodb(doc),
221 "mongodb" => database::mongodb::parse_mongodb_schema(doc),
222 "edi-x12" => domain::edi_x12::parse_edi_schema(doc),
224 "fhir" => domain::fhir::parse_fhir_schema(doc),
225 "geojson" => domain::geojson::parse_geojson_schema(doc),
226 "rss-atom" => domain::rss_atom::parse_rss_atom_schema(doc),
227 "swift-mt" => domain::swift_mt::parse_swift_mt_schema(doc),
228 "vcard-ical" => domain::vcard_ical::parse_vcard_ical_schema(doc),
229 "avro" => serialization::avro::parse_avsc(doc),
231 "msgpack-schema" => serialization::msgpack_schema::parse_msgpack_schema(doc),
232 "atproto" => web_document::atproto::parse_lexicon(doc),
234 "docx" => web_document::docx::parse_docx_schema(doc),
235 "odf" => web_document::odf::parse_odf_schema(doc),
236 other => Err(ProtocolError::Parse(format!(
237 "no document parser registered for protocol {other:?}; \
238 a text-source schema (SQL DDL, GraphQL SDL, .proto, CDDL, and \
239 the like) is loaded with parse_schema_source instead"
240 ))),
241 }
242}
243
244pub fn parse_schema_source(protocol: &str, source: &str) -> Result<Schema, ProtocolError> {
259 match protocol.replace('_', "-").as_str() {
260 "conllu" => annotation::conllu::parse_conllu(source),
261 "cddl" => data_schema::cddl::parse_cddl(source),
262 "cassandra" => database::cassandra::parse_cql(source),
263 "neo4j" => database::neo4j::parse_cypher_schema(source),
264 "redis" => database::redis::parse_redis_schema(source),
265 "asn1" => serialization::asn1::parse_asn1(source),
266 "bond" => serialization::bond::parse_bond(source),
267 "flatbuffers" => serialization::flatbuffers::parse_fbs(source),
268 "graphql" => api::graphql::parse_sdl(source),
269 "sql" => database::sql::parse_ddl(source),
270 "protobuf" => serialization::protobuf::parse_proto(source),
271 other => Err(ProtocolError::Parse(format!(
272 "no source parser registered for protocol {other:?}; \
273 a JSON-document schema is loaded with parse_schema_document instead"
274 ))),
275 }
276}
277
278#[must_use]
281pub const fn document_parser_protocols() -> &'static [&'static str] {
282 &[
283 "amr",
284 "bead",
285 "brat",
286 "concrete",
287 "decomp",
288 "elan",
289 "folia",
290 "fovea",
291 "iso-space",
292 "laf-graf",
293 "naf",
294 "nif",
295 "paula",
296 "tei",
297 "timeml",
298 "ucca",
299 "uima-cas",
300 "web-annotation",
301 "asyncapi",
302 "jsonapi",
303 "openapi",
304 "raml",
305 "ansible",
306 "cloudformation",
307 "k8s-crd",
308 "bson",
309 "json-schema",
310 "arrow",
311 "dataframe",
312 "parquet",
313 "dynamodb",
314 "mongodb",
315 "edi-x12",
316 "fhir",
317 "geojson",
318 "rss-atom",
319 "swift-mt",
320 "vcard-ical",
321 "avro",
322 "msgpack-schema",
323 "atproto",
324 "docx",
325 "odf",
326 ]
327}
328
329#[must_use]
332pub const fn source_parser_protocols() -> &'static [&'static str] {
333 &[
334 "conllu",
335 "cddl",
336 "cassandra",
337 "neo4j",
338 "redis",
339 "asn1",
340 "bond",
341 "flatbuffers",
342 "graphql",
343 "sql",
344 "protobuf",
345 ]
346}
347
348#[cfg(test)]
349#[allow(clippy::expect_used)]
350mod dispatch_tests {
351 use super::*;
352
353 #[test]
354 fn document_dispatch_routes_json_schema() {
355 let doc = serde_json::json!({
356 "type": "object",
357 "properties": { "name": { "type": "string" }, "age": { "type": "integer" } }
358 });
359 let schema = parse_schema_document("json-schema", &doc).expect("json-schema should parse");
360 assert!(schema.has_vertex("root"));
361 assert!(schema.has_vertex("root.name"));
362 assert!(schema.has_vertex("root.age"));
363 }
364
365 #[test]
366 fn document_dispatch_normalizes_underscore_to_hyphen() {
367 let doc = serde_json::json!({ "type": "object" });
370 let via_hyphen = parse_schema_document("json-schema", &doc).expect("hyphen form");
371 let via_underscore = parse_schema_document("json_schema", &doc).expect("underscore form");
372 assert_eq!(via_hyphen.vertex_count(), via_underscore.vertex_count());
373 }
374
375 #[test]
376 fn source_dispatch_routes_graphql_sql_protobuf() {
377 let g = parse_schema_source("graphql", "type Query { hello: String }")
378 .expect("graphql sdl should parse");
379 assert!(g.has_vertex("Query"));
380
381 let s = parse_schema_source("sql", "CREATE TABLE users (id INTEGER PRIMARY KEY);")
382 .expect("sql ddl should parse");
383 assert!(s.has_vertex("users"));
384
385 let p = parse_schema_source("protobuf", "message User { string name = 1; }")
386 .expect("proto should parse");
387 assert!(p.has_vertex("User"));
388 }
389
390 #[test]
391 fn uima_is_accepted_under_both_names() {
392 let doc = serde_json::json!({});
395 for name in ["uima", "uima-cas"] {
397 if let Err(ProtocolError::Parse(msg)) = parse_schema_document(name, &doc) {
398 assert!(
399 !msg.contains("no document parser"),
400 "{name} must route to the uima parser, got: {msg}"
401 );
402 }
403 }
404 }
405
406 #[test]
407 fn cross_category_calls_point_at_the_other_dispatch() {
408 let doc = serde_json::json!({});
411 let err = parse_schema_document("sql", &doc).expect_err("sql is text-source");
412 assert!(err.to_string().contains("parse_schema_source"));
413
414 let err = parse_schema_source("json-schema", "{}").expect_err("json-schema is a document");
415 assert!(err.to_string().contains("parse_schema_document"));
416 }
417
418 #[test]
419 fn parser_protocol_lists_have_expected_sizes() {
420 assert_eq!(document_parser_protocols().len(), 43);
421 assert_eq!(source_parser_protocols().len(), 11);
422 assert!(document_parser_protocols().contains(&"json-schema"));
423 assert!(source_parser_protocols().contains(&"graphql"));
424 assert!(source_parser_protocols().contains(&"sql"));
425 assert!(source_parser_protocols().contains(&"protobuf"));
426 }
427}