use crate::{CustomRouteDefinition, ServiceManifest, TableDefinition};
use std::collections::BTreeSet;
pub const ROUTE_POSTURE: &str = "route_posture";
pub const DB_V2: &str = "db_v2";
pub const CACHE: &str = "cache";
pub const SERVICE_CALL: &str = "service_call";
pub const RESPONSE_V2: &str = "response_v2";
pub const SSE_BRIDGE: &str = "sse_bridge";
pub const ROUTE_MATCHING: &str = "route_matching";
pub const ROUTE_LIMITS: &str = "route_limits";
pub const ERROR_PROFILE: &str = "error_profile";
pub const INTERNAL_POSTURE: &str = "internal_posture";
pub const API_KEY_POSTURE: &str = "api_key_posture";
pub const SCOPED_PERMISSIONS: &str = "scoped_permissions";
pub const SCOPED_POSTURE: &str = "scoped_posture";
pub const TYPED_ROUTES: &str = "typed_routes";
impl ServiceManifest {
pub fn derived_features(&self) -> BTreeSet<&'static str> {
self.custom_routes
.iter()
.flat_map(CustomRouteDefinition::derived_features)
.chain(
self.tables
.iter()
.flat_map(TableDefinition::derived_features),
)
.chain(self.service_features())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::fixtures::sample_manifest;
use crate::RoutePosture;
use serde_json::Value;
const STARMOIRE: &str = include_str!("../fixtures/starmoire_manifest.json");
const TODO: &str = include_str!("../fixtures/todo_manifest.json");
const BLOG: &str = include_str!("../fixtures/blog_manifest.json");
fn manifest(json: &str) -> ServiceManifest {
serde_json::from_str(json).expect("fixture manifest")
}
fn route(handler: &str, posture: Option<RoutePosture>) -> CustomRouteDefinition {
CustomRouteDefinition {
method: "GET".into(),
path: format!("/{handler}"),
handler: handler.into(),
job: None,
posture,
forward_auth: false,
..Default::default()
}
}
#[test]
fn starmoire_manifest_derives_only_route_posture() {
let starmoire = manifest(STARMOIRE);
assert_eq!(
starmoire.derived_features(),
BTreeSet::from([ROUTE_POSTURE])
);
assert_eq!(starmoire.required_platform_features, vec![ROUTE_POSTURE]);
for crud_only in [TODO, BLOG] {
assert!(manifest(crud_only).derived_features().is_empty());
}
}
#[test]
fn route_posture_is_derived_once_from_any_declared_posture() {
let mut m = sample_manifest();
assert!(m.derived_features().is_empty());
m.custom_routes.push(route("legacy", None));
assert!(m.derived_features().is_empty());
m.custom_routes
.push(route("me", Some(RoutePosture::Authenticated)));
m.custom_routes.push(route(
"hook",
Some(RoutePosture::Public {
reason: "signed webhook".into(),
}),
));
assert_eq!(m.derived_features(), BTreeSet::from([ROUTE_POSTURE]));
}
#[test]
fn strict_routing_demands_route_matching() {
let mut m = sample_manifest();
m.routing = crate::RoutingMode::Strict;
assert_eq!(m.derived_features(), BTreeSet::from([ROUTE_MATCHING]));
}
#[test]
fn an_error_profile_demands_error_profile() {
let mut m = sample_manifest();
m.errors = Some(crate::ErrorProfile {
key: "error_message".into(),
});
assert_eq!(m.derived_features(), BTreeSet::from([ERROR_PROFILE]));
}
#[test]
fn deploy_hash_unchanged_for_legacy_manifest() {
for (json, deploy, schema) in [
(
STARMOIRE,
"32b79ba0e4b4697a505007ae19a1ab55a1f4fbefb26fe8fcd86a53847eccba28",
"422f205e16c949bd532f63707b78d8f6ccc4c9ba4eda0b2bd275c479a8d998f2",
),
(
TODO,
"927dfafc66a426e311dc92eee91f2964e6ad450f7d57266a8cab4300a1359bab",
"aa0463d8a7e1fd9e3c6bca62fa0480e4ee6ce4083d17441f02061dca699e9907",
),
(
BLOG,
"9d931c651c3bff3d9be5105b7867ffae01913757bdd8db0a13b1337dd17b7ae5",
"98cd636af82779af1d42ccc7f7daff5b1fd0e70cb553a15233ce4a4e3655e43f",
),
] {
let m = manifest(json);
assert_eq!(m.deploy_hash().expect("hash"), deploy, "{}", m.name);
assert_eq!(m.schema_hash().expect("schema hash"), schema, "{}", m.name);
}
}
#[test]
fn legacy_manifest_json_round_trips_unchanged() {
for json in [STARMOIRE, TODO, BLOG] {
let stored: Value = serde_json::from_str(json).expect("fixture json");
let reserialized = serde_json::to_value(manifest(json)).expect("serialize");
assert_eq!(reserialized, stored);
}
}
}