use crate::{ColumnDefinition, ColumnType, TableDefinition};
use serde_json::{json, Map, Value};
pub(super) fn schema_names(table: &TableDefinition) -> [String; 3] {
let schema_name = to_pascal_case(&table.name);
let create_schema_name = format!("{}Create", schema_name);
let update_schema_name = format!("{}Update", schema_name);
[schema_name, create_schema_name, update_schema_name]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Nullable {
Flag,
TypeUnion,
}
impl Nullable {
fn apply(self, mut prop: Value) -> Value {
if let Some(obj) = prop.as_object_mut() {
match self {
Nullable::Flag => {
obj.insert("nullable".into(), json!(true));
}
Nullable::TypeUnion => {
if let Some(kind) = obj.remove("type") {
obj.insert("type".into(), json!([kind, "null"]));
}
}
}
}
prop
}
}
pub(super) fn table_schemas(table: &TableDefinition, nullable: Nullable) -> [(String, Value); 3] {
let [schema_name, create_schema_name, update_schema_name] = schema_names(table);
[
(schema_name, table_to_schema(table, nullable)),
(create_schema_name, table_to_create_schema(table, nullable)),
(update_schema_name, table_to_update_schema(table, nullable)),
]
}
pub(super) fn pagination() -> Value {
json!({
"type": "object",
"properties": {
"current_page": { "type": "integer" },
"per_page": { "type": "integer" },
"total_records": { "type": "integer" },
"total_pages": { "type": "integer" }
}
})
}
pub(super) fn column_to_openapi_type(col_type: &ColumnType) -> Value {
match col_type {
ColumnType::Uuid => json!({"type": "string", "format": "uuid"}),
ColumnType::Text => json!({"type": "string"}),
ColumnType::Integer => json!({"type": "integer", "format": "int32"}),
ColumnType::BigInteger => json!({"type": "integer", "format": "int64"}),
ColumnType::Float => json!({"type": "number", "format": "float"}),
ColumnType::Double => json!({"type": "number", "format": "double"}),
ColumnType::Boolean => json!({"type": "boolean"}),
ColumnType::Timestamp => json!({"type": "string", "format": "date-time"}),
ColumnType::Date => json!({"type": "string", "format": "date"}),
ColumnType::Jsonb => json!({}),
ColumnType::Numeric { .. } => json!({"type": "string", "format": "decimal"}),
ColumnType::Varchar { length } => json!({"type": "string", "maxLength": length}),
ColumnType::TimestampNaive => json!({"type": "string", "format": "date-time"}),
ColumnType::Time => json!({"type": "string", "format": "time"}),
ColumnType::Bytea => json!({"type": "string", "format": "byte"}),
ColumnType::TextArray => array_of(&ColumnType::Text),
ColumnType::UuidArray => array_of(&ColumnType::Uuid),
ColumnType::IntegerArray => array_of(&ColumnType::Integer),
ColumnType::BigIntegerArray => array_of(&ColumnType::BigInteger),
ColumnType::JsonbArray => array_of(&ColumnType::Jsonb),
}
}
fn array_of(element: &ColumnType) -> Value {
json!({"type": "array", "items": column_to_openapi_type(element)})
}
const ACCESS_EXTENSION: &str = "x-cufflink-access";
fn column_schema(table: &TableDefinition, col: &ColumnDefinition, nullable: Nullable) -> Value {
let prop = column_to_openapi_type(&col.column_type);
let mut prop = if col.nullable {
nullable.apply(prop)
} else {
prop
};
if let (Some(permissions), Some(fields)) = (table.permissions_of(col), prop.as_object_mut()) {
fields.insert(ACCESS_EXTENSION.to_string(), json!(permissions));
}
prop
}
fn table_to_schema(table: &TableDefinition, nullable: Nullable) -> Value {
let mut properties = Map::new();
let mut required = Vec::new();
for col in &table.columns {
properties.insert(col.name.clone(), column_schema(table, col, nullable));
if !table.withholds(col) {
required.push(json!(col.name));
}
}
json!({
"type": "object",
"properties": Value::Object(properties),
"required": required
})
}
fn table_to_create_schema(table: &TableDefinition, nullable: Nullable) -> Value {
let mut properties = Map::new();
let mut required = Vec::new();
for col in &table.columns {
if col.auto_generate {
continue;
}
properties.insert(col.name.clone(), column_schema(table, col, nullable));
let owner = table.owner_field.as_deref() == Some(col.name.as_str());
if !col.nullable && col.default_value.is_none() && !owner {
required.push(json!(col.name));
}
}
json!({
"type": "object",
"properties": Value::Object(properties),
"required": required
})
}
fn table_to_update_schema(table: &TableDefinition, nullable: Nullable) -> Value {
let properties: Map<String, Value> = table
.columns
.iter()
.filter(|col| !col.auto_generate)
.map(|col| (col.name.clone(), column_schema(table, col, nullable)))
.collect();
json!({
"type": "object",
"properties": Value::Object(properties),
"required": []
})
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
Some(c) => c.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
None => String::new(),
}
})
.collect()
}