use cratefield_core::{Action, Audience, Outcome, Surface};
use cratefield_manifest::Access;
use http::Method;
use crate::access::TableApi;
#[must_use]
pub fn surface(tables: &[TableApi]) -> Surface {
let mut out = Surface::new();
for api in tables {
let name = &api.table.name;
let audience = audience(api.access);
out = out.action(
Action::new(format!("list-{name}"), Method::GET, format!("/{name}"))
.audience(audience)
.outcome(Outcome::Json)
.output_schema(page_schema(&api.table)),
);
let by_path = api.table.primary_key.len() == 1;
let row_path = if by_path {
format!("/{name}/{{key}}")
} else {
format!("/{name}/__by")
};
let mut read = Action::new(format!("read-{name}"), Method::GET, row_path.clone())
.audience(audience)
.outcome(Outcome::Json)
.output_schema(body_schema(&api.table));
if !by_path {
read = read.input_schema(by_query_schema(&api.table));
}
out = out.action(read);
if !writable(api.access) {
continue;
}
out = out.action(
Action::new(format!("create-{name}"), Method::POST, format!("/{name}"))
.audience(audience)
.input_schema(body_schema(&api.table))
.outcome(Outcome::Json),
);
out = out.action(
Action::new(format!("replace-{name}"), Method::PUT, row_path.clone())
.audience(audience)
.input_schema(if by_path {
body_schema(&api.table)
} else {
replace_input(&api.table)
})
.outcome(Outcome::Json),
);
let mut delete = Action::new(format!("delete-{name}"), Method::DELETE, row_path)
.audience(audience)
.outcome(Outcome::Json);
if !by_path {
delete = delete.input_schema(by_query_schema(&api.table));
}
out = out.action(delete);
}
out
}
fn audience(access: Access) -> Audience {
match access {
Access::PublicRead => Audience::Public,
Access::Admin => Audience::Admin,
Access::Owner | Access::TenantMembers | _ => Audience::Subject,
}
}
fn writable(access: Access) -> bool {
!matches!(access, Access::PublicRead)
}
fn body_schema(table: &cratefield_tables::TableDef) -> schemars::Schema {
schemars::Schema::try_from(cratefield_tables::json_schema(table))
.unwrap_or_else(|_never| schemars::Schema::default())
}
fn page_schema(table: &cratefield_tables::TableDef) -> schemars::Schema {
let mut row = cratefield_tables::json_schema(table);
if let Some(object) = row.as_object_mut() {
object.remove("$schema");
}
let page = serde_json::json!({
"type": "object",
"properties": {
"rows": { "type": "array", "items": row },
"next": {
"type": ["object", "null"],
"description": "The cursor to send back as `?after=`; null on the last page.",
},
},
"required": ["rows", "next"],
});
schemars::Schema::try_from(page).unwrap_or_else(|_never| schemars::Schema::default())
}
fn by_query_schema(table: &cratefield_tables::TableDef) -> schemars::Schema {
schemars::Schema::try_from(by_query_value(table))
.unwrap_or_else(|_never| schemars::Schema::default())
}
fn by_query_value(table: &cratefield_tables::TableDef) -> serde_json::Value {
let row = cratefield_tables::json_schema(table);
let mut properties = serde_json::Map::new();
let mut required = Vec::with_capacity(table.primary_key.len());
for column in &table.primary_key {
if let Some(property) = row.get("properties").and_then(|props| props.get(column)) {
properties.insert(column.clone(), property.clone());
}
required.push(serde_json::Value::String(column.clone()));
}
serde_json::json!({
"type": "object",
"properties": serde_json::Value::Object(properties),
"required": required,
"additionalProperties": false,
})
}
fn replace_input(table: &cratefield_tables::TableDef) -> schemars::Schema {
let mut row = cratefield_tables::json_schema(table);
if let Some(object) = row.as_object_mut() {
object.insert("x-cf-query".to_owned(), by_query_value(table));
}
schemars::Schema::try_from(row).unwrap_or_else(|_never| schemars::Schema::default())
}