use serde_json::Value;
use super::error::{Result, SodaError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyAssignment {
Uuid,
Sequence,
Client,
EmbeddedOid,
Guid,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContentSqlType {
Blob,
Clob,
Varchar2,
Json,
Raw,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionMethod {
Uuid,
Timestamp,
Hash(String),
Sequential,
None,
}
#[derive(Debug, Clone)]
pub struct SodaCollectionMetadata {
pub table_name: String,
pub schema_name: Option<String>,
pub key_column: String,
pub key_sql_type: String,
pub key_assignment: KeyAssignment,
pub key_path: Option<String>,
pub content_column: String,
pub content_sql_type: ContentSqlType,
pub version_column: Option<String>,
pub version_method: VersionMethod,
pub last_modified_column: Option<String>,
pub creation_time_column: Option<String>,
pub media_type_column: Option<String>,
pub read_only: bool,
pub native: bool,
}
impl SodaCollectionMetadata {
pub fn quoted_table(&self) -> String {
quote_ident(&self.table_name)
}
pub fn content_is_native_json(&self) -> bool {
matches!(self.content_sql_type, ContentSqlType::Json)
}
pub fn key_is_embedded_oid(&self) -> bool {
matches!(self.key_assignment, KeyAssignment::EmbeddedOid)
}
}
pub fn parse_metadata(json: &Value) -> Result<SodaCollectionMetadata> {
let obj = json
.as_object()
.ok_or_else(|| SodaError::InvalidMetadata("descriptor is not an object".to_string()))?;
let table_name = obj
.get("tableName")
.and_then(Value::as_str)
.ok_or_else(|| SodaError::InvalidMetadata("missing tableName".to_string()))?
.to_string();
let schema_name = obj
.get("schemaName")
.and_then(Value::as_str)
.map(str::to_string);
let key = obj
.get("keyColumn")
.and_then(Value::as_object)
.ok_or_else(|| SodaError::InvalidMetadata("missing keyColumn".to_string()))?;
let key_column = key
.get("name")
.and_then(Value::as_str)
.ok_or_else(|| SodaError::InvalidMetadata("missing keyColumn.name".to_string()))?
.to_string();
let key_sql_type = key
.get("sqlType")
.and_then(Value::as_str)
.unwrap_or("VARCHAR2")
.to_string();
let key_assignment = match key
.get("assignmentMethod")
.and_then(Value::as_str)
.unwrap_or("UUID")
.to_ascii_uppercase()
.as_str()
{
"UUID" => KeyAssignment::Uuid,
"SEQUENCE" => KeyAssignment::Sequence,
"CLIENT" => KeyAssignment::Client,
"EMBEDDED_OID" => KeyAssignment::EmbeddedOid,
"GUID" => KeyAssignment::Guid,
other => {
return Err(SodaError::NotSupported(format!(
"key assignment method {other} is not supported"
)));
}
};
let key_path = key.get("path").and_then(Value::as_str).map(str::to_string);
let content = obj
.get("contentColumn")
.and_then(Value::as_object)
.ok_or_else(|| SodaError::InvalidMetadata("missing contentColumn".to_string()))?;
let content_column = content
.get("name")
.and_then(Value::as_str)
.ok_or_else(|| SodaError::InvalidMetadata("missing contentColumn.name".to_string()))?
.to_string();
let content_sql_type = match content
.get("sqlType")
.and_then(Value::as_str)
.unwrap_or("BLOB")
.to_ascii_uppercase()
.as_str()
{
"BLOB" => ContentSqlType::Blob,
"CLOB" => ContentSqlType::Clob,
"VARCHAR2" => ContentSqlType::Varchar2,
"JSON" => ContentSqlType::Json,
"RAW" => ContentSqlType::Raw,
other => {
return Err(SodaError::NotSupported(format!(
"content SQL type {other} is not supported"
)));
}
};
let (version_column, version_method) = match obj.get("versionColumn").and_then(Value::as_object)
{
Some(v) => {
let name = v.get("name").and_then(Value::as_str).map(str::to_string);
let method = match v
.get("method")
.and_then(Value::as_str)
.unwrap_or("NONE")
.to_ascii_uppercase()
.as_str()
{
"UUID" => VersionMethod::Uuid,
"TIMESTAMP" => VersionMethod::Timestamp,
"SEQUENTIAL" => VersionMethod::Sequential,
"NONE" => VersionMethod::None,
hash @ ("MD5" | "SHA1" | "SHA256") => VersionMethod::Hash(hash.to_string()),
other => {
return Err(SodaError::NotSupported(format!(
"version method {other} is not supported"
)));
}
};
(name, method)
}
None => (None, VersionMethod::None),
};
let last_modified_column = obj
.get("lastModifiedColumn")
.and_then(Value::as_object)
.and_then(|c| c.get("name"))
.and_then(Value::as_str)
.map(str::to_string);
let creation_time_column = obj
.get("creationTimeColumn")
.and_then(Value::as_object)
.and_then(|c| c.get("name"))
.and_then(Value::as_str)
.map(str::to_string);
let media_type_column = obj
.get("mediaTypeColumn")
.and_then(Value::as_object)
.and_then(|c| c.get("name"))
.and_then(Value::as_str)
.map(str::to_string);
let read_only = obj
.get("readOnly")
.and_then(Value::as_bool)
.unwrap_or(false);
let native = obj.get("native").and_then(Value::as_bool).unwrap_or(false)
|| key_assignment == KeyAssignment::EmbeddedOid;
Ok(SodaCollectionMetadata {
table_name,
schema_name,
key_column,
key_sql_type,
key_assignment,
key_path,
content_column,
content_sql_type,
version_column,
version_method,
last_modified_column,
creation_time_column,
media_type_column,
read_only,
native,
})
}
pub(crate) fn quote_ident(name: &str) -> String {
format!("\"{}\"", name.replace('"', "\"\""))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_native_default_descriptor() {
let desc = json!({
"schemaName": "PYTHONTEST",
"tableName": "MYCOLL",
"keyColumn": {
"name": "RESID",
"sqlType": "RAW",
"assignmentMethod": "EMBEDDED_OID",
"path": "_id"
},
"contentColumn": {"name": "DATA", "sqlType": "JSON"},
"versionColumn": {"name": "ETAG", "method": "NONE"},
"readOnly": false,
"native": true
});
let m = parse_metadata(&desc).unwrap();
assert_eq!(m.table_name, "MYCOLL");
assert_eq!(m.key_column, "RESID");
assert_eq!(m.key_assignment, KeyAssignment::EmbeddedOid);
assert_eq!(m.key_path.as_deref(), Some("_id"));
assert_eq!(m.content_column, "DATA");
assert!(m.content_is_native_json());
assert!(m.native);
assert_eq!(m.version_column.as_deref(), Some("ETAG"));
assert_eq!(m.version_method, VersionMethod::None);
assert!(!m.read_only);
}
#[test]
fn parse_legacy_blob_descriptor() {
let desc = json!({
"schemaName": "PYTHONTEST",
"tableName": "MYLEGACY",
"keyColumn": {
"name": "ID",
"sqlType": "VARCHAR2",
"maxLength": 255,
"assignmentMethod": "UUID"
},
"contentColumn": {
"name": "JSON_DOCUMENT",
"sqlType": "BLOB",
"validation": "STANDARD"
},
"lastModifiedColumn": {"name": "LAST_MODIFIED"},
"versionColumn": {"name": "VERSION", "method": "UUID"},
"creationTimeColumn": {"name": "CREATED_ON"},
"readOnly": false
});
let m = parse_metadata(&desc).unwrap();
assert_eq!(m.table_name, "MYLEGACY");
assert_eq!(m.key_column, "ID");
assert_eq!(m.key_assignment, KeyAssignment::Uuid);
assert_eq!(m.content_column, "JSON_DOCUMENT");
assert_eq!(m.content_sql_type, ContentSqlType::Blob);
assert!(!m.content_is_native_json());
assert!(!m.native);
assert_eq!(m.version_column.as_deref(), Some("VERSION"));
assert_eq!(m.version_method, VersionMethod::Uuid);
assert_eq!(m.last_modified_column.as_deref(), Some("LAST_MODIFIED"));
assert_eq!(m.creation_time_column.as_deref(), Some("CREATED_ON"));
}
#[test]
fn missing_table_name_errors() {
let desc = json!({"keyColumn": {"name": "ID"}, "contentColumn": {"name": "DATA"}});
assert!(matches!(
parse_metadata(&desc),
Err(SodaError::InvalidMetadata(_))
));
}
#[test]
fn media_type_column_parsed() {
let desc = json!({
"tableName": "MIXED",
"keyColumn": {"name": "ID", "sqlType": "VARCHAR2", "assignmentMethod": "UUID"},
"contentColumn": {"name": "DATA", "sqlType": "BLOB"},
"mediaTypeColumn": {"name": "MEDIA_TYPE"}
});
let m = parse_metadata(&desc).unwrap();
assert_eq!(m.media_type_column.as_deref(), Some("MEDIA_TYPE"));
}
}