#![allow(
unknown_lints,
clippy::match_same_arms,
clippy::similar_names,
clippy::only_used_in_recursion,
clippy::option_if_let_else,
clippy::collapsible_else_if,
clippy::branches_sharing_code,
clippy::explicit_iter_loop,
clippy::manual_let_else,
clippy::hashset_insert_after_contains,
clippy::set_contains_or_insert
)]
pub mod annotation;
pub mod api;
pub mod config;
pub mod data_schema;
pub mod data_science;
pub mod database;
pub mod domain;
pub mod emit;
pub mod error;
pub mod raw_file;
pub mod registry;
fn measure(value: &serde_json::Value) -> u64 {
match value {
serde_json::Value::Null => 4,
serde_json::Value::Bool(_) => 5,
serde_json::Value::Number(_) => 8,
serde_json::Value::String(s) => s.len() as u64 + 2,
serde_json::Value::Array(items) => {
2 + items.iter().map(measure).sum::<u64>() + items.len() as u64
}
serde_json::Value::Object(entries) => {
2 + entries
.iter()
.map(|(k, v)| k.len() as u64 + 4 + measure(v))
.sum::<u64>()
}
}
}
pub mod serialization;
pub mod theories;
pub mod web_document;
use panproto_expr::limits::{Budget, Resource};
use panproto_schema::Schema;
pub use error::ProtocolError;
pub use web_document::atproto;
pub fn parse_schema_bundle(
protocol: &str,
docs: &[serde_json::Value],
) -> Result<Schema, ProtocolError> {
parse_schema_bundle_within(protocol, docs, &Budget::with_defaults())
}
pub fn parse_schema_bundle_within(
protocol: &str,
docs: &[serde_json::Value],
budget: &Budget,
) -> Result<Schema, ProtocolError> {
budget.charge(Resource::BundleEntries, docs.len() as u64)?;
for doc in docs {
budget.charge(Resource::InputBytes, measure(doc))?;
}
match protocol.replace('_', "-").as_str() {
"atproto" => atproto::parse_lexicon_bundle(docs),
"openapi" => api::openapi::parse_openapi_bundle(docs),
"json-schema" => data_schema::json_schema::parse_json_schema_bundle(docs),
"avro" => serialization::avro::parse_avsc_bundle(docs),
other => Err(ProtocolError::Parse(format!(
"no bundle parser registered for protocol {other:?}; supported: {:?}",
bundle_parser_protocols()
))),
}
}
#[must_use]
pub fn bundle_parser_protocols() -> Vec<&'static str> {
registry::names_where(|d| d.bundle)
}
pub fn parse_schema_bundle_project(
protocol: &str,
docs: &[(std::path::PathBuf, serde_json::Value)],
) -> Result<atproto::LexiconProject, ProtocolError> {
match protocol.replace('_', "-").as_str() {
"atproto" => {
let lexicon_docs: Vec<atproto::LexiconDoc> = docs
.iter()
.map(|(path, value)| atproto::LexiconDoc {
path: path.clone(),
value: value.clone(),
})
.collect();
atproto::parse_lexicon_project(&lexicon_docs)
}
other => Err(ProtocolError::Parse(format!(
"no per-file bundle parser registered for protocol {other:?}; supported: [\"atproto\"]"
))),
}
}
#[must_use]
pub fn bundle_project_protocols() -> Vec<&'static str> {
registry::names_where(|d| d.bundle_project)
}
pub fn parse_schema_document(
protocol: &str,
doc: &serde_json::Value,
) -> Result<Schema, ProtocolError> {
parse_schema_document_within(protocol, doc, &Budget::with_defaults())
}
pub fn parse_schema_document_within(
protocol: &str,
doc: &serde_json::Value,
budget: &Budget,
) -> Result<Schema, ProtocolError> {
budget.charge(Resource::InputBytes, measure(doc))?;
match registry::descriptor(protocol) {
Some(d) => match d.parser {
registry::Parser::Document(parse) => parse(doc),
registry::Parser::Source(_) => Err(ProtocolError::Parse(format!(
"protocol {protocol:?} is read from source text, not a JSON document; \
use parse_schema_source"
))),
},
None => Err(ProtocolError::Parse(format!(
"no document parser registered for protocol {protocol:?}; supported: {:?}",
document_parser_protocols()
))),
}
}
pub fn parse_schema_source(protocol: &str, source: &str) -> Result<Schema, ProtocolError> {
parse_schema_source_within(protocol, source, &Budget::with_defaults())
}
pub fn parse_schema_source_within(
protocol: &str,
source: &str,
budget: &Budget,
) -> Result<Schema, ProtocolError> {
budget.charge(Resource::InputBytes, source.len() as u64)?;
match registry::descriptor(protocol) {
Some(d) => match d.parser {
registry::Parser::Source(parse) => parse(source),
registry::Parser::Document(_) => Err(ProtocolError::Parse(format!(
"protocol {protocol:?} is read from a JSON document, not source text; \
use parse_schema_document"
))),
},
None => Err(ProtocolError::Parse(format!(
"no source parser registered for protocol {protocol:?}; supported: {:?}",
source_parser_protocols()
))),
}
}
#[must_use]
pub fn document_parser_protocols() -> Vec<&'static str> {
registry::names_where(|d| matches!(d.parser, registry::Parser::Document(_)))
}
#[must_use]
pub fn source_parser_protocols() -> Vec<&'static str> {
registry::names_where(|d| matches!(d.parser, registry::Parser::Source(_)))
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod dispatch_tests {
use super::*;
#[test]
fn document_dispatch_routes_json_schema() {
let doc = serde_json::json!({
"type": "object",
"properties": { "name": { "type": "string" }, "age": { "type": "integer" } }
});
let schema = parse_schema_document("json-schema", &doc).expect("json-schema should parse");
assert!(schema.has_vertex("root"));
assert!(schema.has_vertex("root.name"));
assert!(schema.has_vertex("root.age"));
}
#[test]
fn document_dispatch_normalizes_underscore_to_hyphen() {
let doc = serde_json::json!({ "type": "object" });
let via_hyphen = parse_schema_document("json-schema", &doc).expect("hyphen form");
let via_underscore = parse_schema_document("json_schema", &doc).expect("underscore form");
assert_eq!(via_hyphen.vertex_count(), via_underscore.vertex_count());
}
#[test]
fn source_dispatch_routes_graphql_sql_protobuf() {
let g = parse_schema_source("graphql", "type Query { hello: String }")
.expect("graphql sdl should parse");
assert!(g.has_vertex("Query"));
let s = parse_schema_source("sql", "CREATE TABLE users (id INTEGER PRIMARY KEY);")
.expect("sql ddl should parse");
assert!(s.has_vertex("users"));
let p = parse_schema_source("protobuf", "message User { string name = 1; }")
.expect("proto should parse");
assert!(p.has_vertex("User"));
}
#[test]
fn uima_is_accepted_under_both_names() {
let doc = serde_json::json!({});
for name in ["uima", "uima-cas"] {
if let Err(ProtocolError::Parse(msg)) = parse_schema_document(name, &doc) {
assert!(
!msg.contains("no document parser"),
"{name} must route to the uima parser, got: {msg}"
);
}
}
}
#[test]
fn cross_category_calls_point_at_the_other_dispatch() {
let doc = serde_json::json!({});
let err = parse_schema_document("sql", &doc).expect_err("sql is text-source");
assert!(err.to_string().contains("parse_schema_source"));
let err = parse_schema_source("json-schema", "{}").expect_err("json-schema is a document");
assert!(err.to_string().contains("parse_schema_document"));
}
#[test]
fn parser_protocol_lists_have_expected_sizes() {
assert_eq!(document_parser_protocols().len(), 43);
assert_eq!(source_parser_protocols().len(), 11);
assert!(document_parser_protocols().contains(&"json-schema"));
assert!(source_parser_protocols().contains(&"graphql"));
assert!(source_parser_protocols().contains(&"sql"));
assert!(source_parser_protocols().contains(&"protobuf"));
}
}