use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum SchemaDef {
Record(RecordSchema),
Query(QuerySchema),
Procedure(ProcedureSchema),
Subscription(SubscriptionSchema),
#[serde(rename = "permission-set")]
PermissionSet(PermissionSetSchema),
Boolean(BooleanSchema),
Integer(IntegerSchema),
String(StringSchema),
Bytes(BytesSchema),
#[serde(rename = "cid-link")]
CidLink(CidLinkSchema),
Array(ArraySchema),
Object(ObjectSchema),
Blob(BlobSchema),
Params(ParamsSchema),
Ref(RefSchema),
Union(UnionSchema),
Unknown(UnknownSchema),
Token(TokenSchema),
}
impl SchemaDef {
pub fn type_name(&self) -> &'static str {
match self {
SchemaDef::Record(_) => "record",
SchemaDef::Query(_) => "query",
SchemaDef::Procedure(_) => "procedure",
SchemaDef::Subscription(_) => "subscription",
SchemaDef::PermissionSet(_) => "permission-set",
SchemaDef::Boolean(_) => "boolean",
SchemaDef::Integer(_) => "integer",
SchemaDef::String(_) => "string",
SchemaDef::Bytes(_) => "bytes",
SchemaDef::CidLink(_) => "cid-link",
SchemaDef::Array(_) => "array",
SchemaDef::Object(_) => "object",
SchemaDef::Blob(_) => "blob",
SchemaDef::Params(_) => "params",
SchemaDef::Ref(_) => "ref",
SchemaDef::Union(_) => "union",
SchemaDef::Unknown(_) => "unknown",
SchemaDef::Token(_) => "token",
}
}
pub fn is_primary(&self) -> bool {
matches!(
self,
SchemaDef::Record(_)
| SchemaDef::Query(_)
| SchemaDef::Procedure(_)
| SchemaDef::Subscription(_)
| SchemaDef::PermissionSet(_)
)
}
pub fn expand_local_refs(&mut self, nsid: &str) {
match self {
SchemaDef::Record(r) => {
r.record.expand_local_refs(nsid);
}
SchemaDef::Query(q) => {
if let Some(params) = &mut q.parameters {
params.expand_local_refs(nsid);
}
if let Some(output) = &mut q.output
&& let Some(schema) = &mut output.schema
{
schema.expand_local_refs(nsid);
}
}
SchemaDef::Procedure(p) => {
if let Some(params) = &mut p.parameters {
params.expand_local_refs(nsid);
}
if let Some(input) = &mut p.input
&& let Some(schema) = &mut input.schema
{
schema.expand_local_refs(nsid);
}
if let Some(output) = &mut p.output
&& let Some(schema) = &mut output.schema
{
schema.expand_local_refs(nsid);
}
}
SchemaDef::Subscription(s) => {
if let Some(params) = &mut s.parameters {
params.expand_local_refs(nsid);
}
if let Some(message) = &mut s.message {
message.schema.expand_local_refs(nsid);
}
}
SchemaDef::Array(a) => {
a.items.expand_local_refs(nsid);
}
SchemaDef::Object(o) => {
for prop in o.properties.values_mut() {
prop.expand_local_refs(nsid);
}
}
SchemaDef::Params(p) => {
for prop in p.properties.values_mut() {
prop.expand_local_refs(nsid);
}
}
SchemaDef::Ref(r) => {
if r.ref_path.starts_with('#') {
r.ref_path = format!("{}{}", nsid, r.ref_path);
}
}
SchemaDef::Union(u) => {
for ref_path in &mut u.refs {
if ref_path.starts_with('#') {
*ref_path = format!("{}{}", nsid, ref_path);
}
}
}
SchemaDef::PermissionSet(_)
| SchemaDef::Boolean(_)
| SchemaDef::Integer(_)
| SchemaDef::String(_)
| SchemaDef::Bytes(_)
| SchemaDef::CidLink(_)
| SchemaDef::Blob(_)
| SchemaDef::Unknown(_)
| SchemaDef::Token(_) => {}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RecordSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
pub record: Box<SchemaDef>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct QuerySchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Box<SchemaDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<OutputSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<ErrorSchema>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ProcedureSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Box<SchemaDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<InputSchema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<OutputSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<ErrorSchema>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct SubscriptionSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Box<SchemaDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<MessageSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<ErrorSchema>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct PermissionSetSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(
rename = "title:lang",
default,
skip_serializing_if = "IndexMap::is_empty"
)]
pub title_lang: IndexMap<String, String>,
#[serde(
rename = "detail:lang",
default,
skip_serializing_if = "IndexMap::is_empty"
)]
pub detail_lang: IndexMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub permissions: Vec<Permission>,
}
impl PermissionSetSchema {
pub fn get_detail(&self) -> Option<&str> {
self.detail.as_deref().or(self.description.as_deref())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Permission {
#[serde(rename = "type")]
pub type_field: String,
pub resource: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collection: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lxm: Option<Vec<String>>,
#[serde(rename = "inheritAud", skip_serializing_if = "Option::is_none")]
pub inherit_aud: Option<bool>,
}
pub const PERMISSION_RESOURCES: &[&str] = &["repo", "rpc", "blob", "identity", "account"];
pub const REPO_ACTIONS: &[&str] = &["create", "update", "delete"];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InputSchema {
pub encoding: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<Box<SchemaDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OutputSchema {
pub encoding: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<Box<SchemaDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MessageSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub encoding: Option<String>,
pub schema: Box<SchemaDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ErrorSchema {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct BooleanSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
#[serde(rename = "const", skip_serializing_if = "Option::is_none")]
pub const_value: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct IntegerSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<i64>,
#[serde(rename = "const", skip_serializing_if = "Option::is_none")]
pub const_value: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<i64>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<i64>>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct StringSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(rename = "const", skip_serializing_if = "Option::is_none")]
pub const_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
pub min_length: Option<usize>,
#[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
pub max_length: Option<usize>,
#[serde(rename = "minGraphemes", skip_serializing_if = "Option::is_none")]
pub min_graphemes: Option<usize>,
#[serde(rename = "maxGraphemes", skip_serializing_if = "Option::is_none")]
pub max_graphemes: Option<usize>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<String>>,
#[serde(rename = "knownValues", skip_serializing_if = "Option::is_none")]
pub known_values: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct BytesSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
pub min_length: Option<usize>,
#[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
pub max_length: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct CidLinkSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ArraySchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub items: Box<SchemaDef>,
#[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
pub min_length: Option<usize>,
#[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
pub max_length: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ObjectSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub nullable: Vec<String>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub properties: IndexMap<String, SchemaDef>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct BlobSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub accept: Vec<String>,
#[serde(rename = "maxSize", skip_serializing_if = "Option::is_none")]
pub max_size: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ParamsSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required: Vec<String>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub properties: IndexMap<String, SchemaDef>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RefSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "ref")]
pub ref_path: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UnionSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub refs: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub closed: bool,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct UnknownSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct TokenSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schema_def_type_names() {
assert_eq!(
SchemaDef::Boolean(BooleanSchema::default()).type_name(),
"boolean"
);
assert_eq!(
SchemaDef::Integer(IntegerSchema::default()).type_name(),
"integer"
);
assert_eq!(
SchemaDef::String(StringSchema::default()).type_name(),
"string"
);
}
#[test]
fn test_schema_def_is_primary() {
let record = SchemaDef::Record(RecordSchema {
description: None,
key: None,
record: Box::new(SchemaDef::Object(ObjectSchema::default())),
});
assert!(record.is_primary());
let boolean = SchemaDef::Boolean(BooleanSchema::default());
assert!(!boolean.is_primary());
}
#[test]
fn test_deserialize_boolean_schema() {
let json = r#"{"type": "boolean", "default": true}"#;
let schema: SchemaDef = serde_json::from_str(json).unwrap();
if let SchemaDef::Boolean(b) = schema {
assert_eq!(b.default, Some(true));
} else {
panic!("Expected Boolean schema");
}
}
#[test]
fn test_deserialize_string_schema() {
let json = r#"{"type": "string", "format": "datetime", "minLength": 1, "maxLength": 100}"#;
let schema: SchemaDef = serde_json::from_str(json).unwrap();
if let SchemaDef::String(s) = schema {
assert_eq!(s.format, Some("datetime".to_string()));
assert_eq!(s.min_length, Some(1));
assert_eq!(s.max_length, Some(100));
} else {
panic!("Expected String schema");
}
}
#[test]
fn test_deserialize_object_schema() {
let json = r#"{
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"count": {"type": "integer"}
}
}"#;
let schema: SchemaDef = serde_json::from_str(json).unwrap();
if let SchemaDef::Object(o) = schema {
assert_eq!(o.required, vec!["name"]);
assert!(o.properties.contains_key("name"));
assert!(o.properties.contains_key("count"));
} else {
panic!("Expected Object schema");
}
}
#[test]
fn test_deserialize_array_schema() {
let json = r#"{
"type": "array",
"items": {"type": "string"},
"minLength": 1,
"maxLength": 10
}"#;
let schema: SchemaDef = serde_json::from_str(json).unwrap();
if let SchemaDef::Array(a) = schema {
assert_eq!(a.min_length, Some(1));
assert_eq!(a.max_length, Some(10));
assert!(matches!(*a.items, SchemaDef::String(_)));
} else {
panic!("Expected Array schema");
}
}
#[test]
fn test_deserialize_ref_schema() {
let json = r##"{"type": "ref", "ref": "#defs/myType"}"##;
let schema: SchemaDef = serde_json::from_str(json).unwrap();
if let SchemaDef::Ref(r) = schema {
assert_eq!(r.ref_path, "#defs/myType");
} else {
panic!("Expected Ref schema");
}
}
#[test]
fn test_deserialize_union_schema() {
let json = r##"{
"type": "union",
"refs": ["#defs/typeA", "#defs/typeB"],
"closed": true
}"##;
let schema: SchemaDef = serde_json::from_str(json).unwrap();
if let SchemaDef::Union(u) = schema {
assert_eq!(u.refs.len(), 2);
assert!(u.closed);
} else {
panic!("Expected Union schema");
}
}
#[test]
fn test_deserialize_blob_schema() {
let json = r#"{
"type": "blob",
"accept": ["image/png", "image/jpeg"],
"maxSize": 1000000
}"#;
let schema: SchemaDef = serde_json::from_str(json).unwrap();
if let SchemaDef::Blob(b) = schema {
assert_eq!(b.accept, vec!["image/png", "image/jpeg"]);
assert_eq!(b.max_size, Some(1000000));
} else {
panic!("Expected Blob schema");
}
}
#[test]
fn test_deserialize_cid_link_schema() {
let json = r#"{"type": "cid-link"}"#;
let schema: SchemaDef = serde_json::from_str(json).unwrap();
assert!(matches!(schema, SchemaDef::CidLink(_)));
}
}