cufflink_types/
features.rs1use crate::{CustomRouteDefinition, ServiceManifest, TableDefinition};
5use std::collections::BTreeSet;
6
7pub const ROUTE_POSTURE: &str = "route_posture";
8pub const DB_V2: &str = "db_v2";
9pub const CACHE: &str = "cache";
10pub const SERVICE_CALL: &str = "service_call";
11pub const RESPONSE_V2: &str = "response_v2";
12pub const SSE_BRIDGE: &str = "sse_bridge";
13pub const ROUTE_MATCHING: &str = "route_matching";
14pub const ROUTE_LIMITS: &str = "route_limits";
15pub const ERROR_PROFILE: &str = "error_profile";
16pub const INTERNAL_POSTURE: &str = "internal_posture";
17pub const API_KEY_POSTURE: &str = "api_key_posture";
18pub const SCOPED_PERMISSIONS: &str = "scoped_permissions";
19pub const SCOPED_POSTURE: &str = "scoped_posture";
20pub const TYPED_ROUTES: &str = "typed_routes";
21
22impl ServiceManifest {
23 pub fn derived_features(&self) -> BTreeSet<&'static str> {
28 self.custom_routes
29 .iter()
30 .flat_map(CustomRouteDefinition::derived_features)
31 .chain(
32 self.tables
33 .iter()
34 .flat_map(TableDefinition::derived_features),
35 )
36 .chain(self.service_features())
37 .collect()
38 }
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use crate::manifest::fixtures::sample_manifest;
45 use crate::RoutePosture;
46 use serde_json::Value;
47
48 const STARMOIRE: &str = include_str!("../fixtures/starmoire_manifest.json");
49 const TODO: &str = include_str!("../fixtures/todo_manifest.json");
50 const BLOG: &str = include_str!("../fixtures/blog_manifest.json");
51
52 fn manifest(json: &str) -> ServiceManifest {
53 serde_json::from_str(json).expect("fixture manifest")
54 }
55
56 fn route(handler: &str, posture: Option<RoutePosture>) -> CustomRouteDefinition {
57 CustomRouteDefinition {
58 method: "GET".into(),
59 path: format!("/{handler}"),
60 handler: handler.into(),
61 job: None,
62 posture,
63 forward_auth: false,
64 ..Default::default()
65 }
66 }
67
68 #[test]
69 fn starmoire_manifest_derives_only_route_posture() {
70 let starmoire = manifest(STARMOIRE);
71 assert_eq!(
72 starmoire.derived_features(),
73 BTreeSet::from([ROUTE_POSTURE])
74 );
75 assert_eq!(starmoire.required_platform_features, vec![ROUTE_POSTURE]);
76 for crud_only in [TODO, BLOG] {
77 assert!(manifest(crud_only).derived_features().is_empty());
78 }
79 }
80
81 #[test]
82 fn route_posture_is_derived_once_from_any_declared_posture() {
83 let mut m = sample_manifest();
84 assert!(m.derived_features().is_empty());
85 m.custom_routes.push(route("legacy", None));
86 assert!(m.derived_features().is_empty());
87 m.custom_routes
88 .push(route("me", Some(RoutePosture::Authenticated)));
89 m.custom_routes.push(route(
90 "hook",
91 Some(RoutePosture::Public {
92 reason: "signed webhook".into(),
93 }),
94 ));
95 assert_eq!(m.derived_features(), BTreeSet::from([ROUTE_POSTURE]));
96 }
97
98 #[test]
99 fn strict_routing_demands_route_matching() {
100 let mut m = sample_manifest();
101 m.routing = crate::RoutingMode::Strict;
102 assert_eq!(m.derived_features(), BTreeSet::from([ROUTE_MATCHING]));
103 }
104
105 #[test]
106 fn an_error_profile_demands_error_profile() {
107 let mut m = sample_manifest();
108 m.errors = Some(crate::ErrorProfile {
109 key: "error_message".into(),
110 });
111 assert_eq!(m.derived_features(), BTreeSet::from([ERROR_PROFILE]));
112 }
113
114 #[test]
115 fn deploy_hash_unchanged_for_legacy_manifest() {
116 for (json, deploy, schema) in [
117 (
118 STARMOIRE,
119 "32b79ba0e4b4697a505007ae19a1ab55a1f4fbefb26fe8fcd86a53847eccba28",
120 "422f205e16c949bd532f63707b78d8f6ccc4c9ba4eda0b2bd275c479a8d998f2",
121 ),
122 (
123 TODO,
124 "927dfafc66a426e311dc92eee91f2964e6ad450f7d57266a8cab4300a1359bab",
125 "aa0463d8a7e1fd9e3c6bca62fa0480e4ee6ce4083d17441f02061dca699e9907",
126 ),
127 (
128 BLOG,
129 "9d931c651c3bff3d9be5105b7867ffae01913757bdd8db0a13b1337dd17b7ae5",
130 "98cd636af82779af1d42ccc7f7daff5b1fd0e70cb553a15233ce4a4e3655e43f",
131 ),
132 ] {
133 let m = manifest(json);
134 assert_eq!(m.deploy_hash().expect("hash"), deploy, "{}", m.name);
135 assert_eq!(m.schema_hash().expect("schema hash"), schema, "{}", m.name);
136 }
137 }
138
139 #[test]
140 fn legacy_manifest_json_round_trips_unchanged() {
141 for json in [STARMOIRE, TODO, BLOG] {
142 let stored: Value = serde_json::from_str(json).expect("fixture json");
143 let reserialized = serde_json::to_value(manifest(json)).expect("serialize");
144 assert_eq!(reserialized, stored);
145 }
146 }
147}