use super::feature::{Endpoint, Feature, Lookup};
use super::layout::Uses;
use super::operation::{Header, Operation, Security, access_errors, column_description};
use super::relations::{Action, Relation};
use crate::ApiAccess;
use crate::schema::RelationshipKind;
const LOCATION: Header = ("Location", "The created row's URL.");
fn insert_conflicts(feature: &Feature<'_>) -> bool {
let table = feature.table;
!table.primary_key.is_empty() || !table.unique_keys.is_empty() || update_conflicts(feature)
}
fn update_conflicts(feature: &Feature<'_>) -> bool {
!feature.table.unique_keys.is_empty() || !feature.table.foreign_keys.is_empty()
}
fn restore_conflicts(feature: &Feature<'_>) -> bool {
!feature.table.unique_keys.is_empty()
}
fn delete_conflicts(feature: &Feature<'_>) -> bool {
feature
.table
.relationships
.iter()
.any(|relationship| relationship.kind != RelationshipKind::BelongsTo)
}
fn endpoint_operation(feature: &Feature<'_>, endpoint: Endpoint) -> Operation {
let path = endpoint.path(&feature.key_path());
let op = Operation::new(
feature,
endpoint.method(),
&path,
endpoint.name().to_owned(),
feature.caller_of(endpoint),
);
let response = feature.response();
let page = format!("PaginatedResponse<{response}>");
match endpoint {
Endpoint::List => {
op.query(feature.list_query())
.answers(200, "One page of rows.", Some(page))
}
Endpoint::Search => op.json(feature.search_body()).answers(
200,
"One page of the rows the filter matches.",
Some(page),
),
Endpoint::Create => {
let mut op = op
.json(feature.create_dto())
.answers(201, "The row, created.", Some(response))
.conflicts(insert_conflicts(feature));
if feature.key.is_some() {
op.headers.push(LOCATION);
}
op.etag(feature)
}
Endpoint::GetOne => {
let op = op
.keyed(feature)
.errors(&[404])
.answers(200, "The row.", Some(response))
.etag(feature);
with_includes(op, feature)
}
Endpoint::Update => {
let mut op = op.keyed(feature);
if feature.version.is_some() {
op.params.push(
"(\"If-Match\" = String, Header, description = \"The row's `ETag`.\")"
.to_owned(),
);
op.errors.extend([412, 428]);
}
op.json(feature.update_dto())
.errors(&[404])
.answers(200, "The row, changed.", Some(response))
.etag(feature)
.conflicts(update_conflicts(feature))
}
Endpoint::Remove => {
let op = op.keyed(feature).errors(&[404]);
let op = if feature.soft_delete {
op.query(feature.remove_query()).answers(
204,
"The row, soft-deleted (deleted with `?permanent=true`).",
None,
)
} else {
op.answers(204, "The row, deleted.", None)
};
op.conflicts(delete_conflicts(feature))
}
Endpoint::Restore => op
.keyed(feature)
.errors(&[404])
.answers(200, "The row, restored.", Some(response))
.etag(feature)
.conflicts(restore_conflicts(feature)),
}
}
fn with_includes(mut op: Operation, feature: &Feature<'_>) -> Operation {
let includes = feature.includes();
if includes.is_empty() {
return op;
}
op = op.query(feature.get_query());
op.description = "The row, with the relationship `?include=` names embedded.";
for relation in includes {
let caller = relation.caller(feature);
op.errors.extend(access_errors(caller.access));
if op.security == Security::None && caller.access != ApiAccess::Public {
op.security = Security::Optional;
}
}
op
}
fn lookup_operation(feature: &Feature<'_>, lookup: &Lookup<'_>) -> Operation {
Operation::new(
feature,
"get",
&lookup.path(),
lookup.function.clone(),
feature.caller(),
)
.errors(&[404])
.answers(200, "The row.", Some(feature.response()))
.etag(feature)
.path_param(
"value",
lookup.column,
&column_description(feature.table, lookup.column),
)
}
fn relation_operation(feature: &Feature<'_>, relation: &Relation<'_>, action: Action) -> Operation {
let op = Operation::new(
feature,
action.method(),
&relation.path(feature, action),
relation.handler(action),
relation.caller(feature),
)
.keyed(feature)
.errors(&[404]);
let op = match relation.writes().filter(|_| action.names_other()) {
Some(writes) => {
let key = relation.target.key_columns();
let description = key
.first()
.map_or_else(String::new, |key| column_description(relation.target, key));
op.path_param("other", writes.other, &description)
}
None => op,
};
match action {
Action::List => op.answers(
200,
"The related rows.",
Some(format!("Vec<{}>", relation.response())),
),
Action::Sync => op
.json(format!("IdList<{}>", relation.target_key()))
.errors(&[409])
.answers(204, "The row, paired with exactly the rows named.", None),
Action::Attach => op.errors(&[409]).answers(204, "The rows, paired.", None),
Action::Detach => op.answers(204, "The rows, unpaired.", None),
}
}
pub fn push_endpoint(
output: &mut String,
uses: &mut Uses,
feature: &Feature<'_>,
endpoint: Endpoint,
) {
if feature.openapi {
endpoint_operation(feature, endpoint).push(output, uses);
}
}
pub fn push_lookup(
output: &mut String,
uses: &mut Uses,
feature: &Feature<'_>,
lookup: &Lookup<'_>,
) {
if feature.openapi {
lookup_operation(feature, lookup).push(output, uses);
}
}
pub fn push_relation(
output: &mut String,
uses: &mut Uses,
feature: &Feature<'_>,
relation: &Relation<'_>,
action: Action,
) {
if feature.openapi {
relation_operation(feature, relation, action).push(output, uses);
}
}
pub fn handler_names(feature: &Feature<'_>) -> Vec<String> {
let mut names: Vec<String> = feature
.endpoints
.iter()
.map(|endpoint| endpoint.name().to_owned())
.collect();
names.extend(feature.lookups.iter().map(|lookup| lookup.function.clone()));
for relation in &feature.relations {
names.extend(
relation
.actions()
.into_iter()
.map(|action| relation.handler(action)),
);
}
names
}