1use anyhow::{Context, bail};
9use semver::Version;
10use wac_graph::{CompositionGraph, NodeId, PackageId, types::Package};
11
12use crate::InheritConfiguration;
13
14pub const MIDDLEWARE_HANDLER_INTERFACE: &str = "wasi:http/handler";
21
22pub struct Middleware {
24 pub source: Vec<u8>,
26 pub inherit: InheritConfiguration,
28}
29
30pub fn compose_middleware_pipeline(
48 primary: Vec<u8>,
49 middlewares: Vec<Middleware>,
50) -> anyhow::Result<Vec<u8>> {
51 if middlewares.is_empty() {
52 return Ok(primary);
53 }
54
55 let mut graph = CompositionGraph::new();
56 let mut stages = Vec::with_capacity(middlewares.len());
57
58 for (index, middleware) in middlewares.into_iter().enumerate() {
60 let description = format!("middleware #{}", index + 1);
61 let bytes = spin_componentize::componentize_if_necessary(&middleware.source)
62 .with_context(|| format!("failed to componentize {description}"))?;
63 let bytes = spin_capabilities::apply_deny_adapter(&bytes, middleware.inherit)?;
64 let name = format!("middleware{index}");
65 let package = Package::from_bytes(&name, None, bytes, graph.types_mut())
66 .with_context(|| format!("parsing {description}"))?;
67 let world = &graph.types()[package.ty()];
68 let import = find_handler(world.imports.keys(), "import", &description)?;
69 let export = find_handler(world.exports.keys(), "export", &description)?;
70 stages.push(Stage {
71 package: graph.register_package(package)?,
72 description,
73 import,
74 export,
75 });
76 }
77
78 let package = Package::from_bytes("primary", None, primary, graph.types_mut())
80 .context("parsing primary component")?;
81 let export = find_handler(
82 graph.types()[package.ty()].exports.keys(),
83 "export",
84 "primary component",
85 )?;
86 let primary = (graph.register_package(package)?, export);
87
88 let composed_export = stages
89 .iter()
90 .map(|stage| &stage.export)
91 .chain([&primary.1])
92 .max_by_key(|handler| &handler.version)
93 .expect("pipeline always contains the primary component")
94 .name
95 .clone();
96
97 let outermost_export = chain(&mut graph, primary, &stages)?;
99
100 graph.export(outermost_export, composed_export)?;
102
103 Ok(graph.encode(Default::default())?)
104}
105
106struct Handler {
108 name: String,
109 version: Version,
110}
111
112struct Stage {
114 package: PackageId,
115 description: String,
116 import: Handler,
117 export: Handler,
118}
119
120fn find_handler<'a>(
123 names: impl IntoIterator<Item = &'a String>,
124 kind: &str,
125 description: &str,
126) -> anyhow::Result<Handler> {
127 let mut handlers = names.into_iter().filter_map(|name| {
128 let version = name
129 .strip_prefix(MIDDLEWARE_HANDLER_INTERFACE)?
130 .strip_prefix('@')?;
131 let version = Version::parse(version).ok()?;
132 (version.major == 0 && version.minor == 3 && version.pre.is_empty()).then(|| Handler {
133 name: name.clone(),
134 version,
135 })
136 });
137 let Some(handler) = handlers.next() else {
138 bail!("{description} does not {kind} `{MIDDLEWARE_HANDLER_INTERFACE}@0.3.x`");
139 };
140 if let Some(other) = handlers.next() {
141 bail!(
142 "{description} has ambiguous `{MIDDLEWARE_HANDLER_INTERFACE}@0.3.x` {kind}s: `{}` and `{}`",
143 handler.name,
144 other.name
145 );
146 }
147 Ok(handler)
148}
149
150fn chain(
157 graph: &mut CompositionGraph,
158 (primary, primary_export): (PackageId, Handler),
159 middlewares: &[Stage],
160) -> anyhow::Result<NodeId> {
161 let instance = graph.instantiate(primary);
164 let mut upstream_handle = graph.alias_instance_export(instance, &primary_export.name)?;
165
166 for stage in middlewares.iter().rev() {
167 let instance = graph.instantiate(stage.package);
168 graph
169 .set_instantiation_argument(instance, &stage.import.name, upstream_handle)
170 .with_context(|| {
171 format!(
172 "failed to wire `{}` of {}",
173 stage.import.name, stage.description
174 )
175 })?;
176 upstream_handle = graph.alias_instance_export(instance, &stage.export.name)?;
177 }
178
179 Ok(upstream_handle)
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use wac_graph::types::Types;
186
187 fn http_wit(version: &str) -> String {
188 format!(
189 r#"
190 package wasi:http@{version};
191
192 interface types {{
193 resource fields {{ constructor(); }}
194 resource request {{ constructor(headers: fields); }}
195 resource response {{ constructor(headers: fields); }}
196 variant error-code {{ internal-error(option<string>) }}
197 }}
198
199 interface handler {{
200 use types.{{request, response, error-code}};
201 handle: async func(request: request) -> result<response, error-code>;
202 }}
203 "#
204 )
205 }
206
207 fn component(items: &[&str]) -> Vec<u8> {
210 let mut resolve = wit_parser::Resolve::default();
211 for version in ["0.3.0", "0.3.2"] {
212 resolve.push_str("http.wit", &http_wit(version)).unwrap();
213 }
214 let wit = format!(
215 "package test:middleware;\nworld test {{\n{}\n}}",
216 items.join("\n")
217 );
218 let package = resolve.push_str("test.wit", &wit).unwrap();
219 let world = resolve.select_world(&[package], Some("test")).unwrap();
220
221 let mut module = wit_component::dummy_module(
222 &resolve,
223 world,
224 wit_parser::ManglingAndAbi::Legacy(wit_parser::LiftLowerAbi::Sync),
225 );
226 wit_component::embed_component_metadata(
227 &mut module,
228 &resolve,
229 world,
230 wit_component::StringEncoding::UTF8,
231 false,
232 )
233 .unwrap();
234 wit_component::ComponentEncoder::default()
235 .validate(true)
236 .module(&module)
237 .unwrap()
238 .encode()
239 .unwrap()
240 }
241
242 fn service(version: &str) -> Vec<u8> {
243 component(&[&format!("export wasi:http/handler@{version};")])
244 }
245
246 fn middleware(import_version: &str, export_version: &str) -> Middleware {
247 Middleware {
248 source: component(&[
249 &format!("import wasi:http/handler@{import_version};"),
250 &format!("export wasi:http/handler@{export_version};"),
251 ]),
252 inherit: InheritConfiguration::All,
253 }
254 }
255
256 fn imports_and_exports(bytes: Vec<u8>) -> (Vec<String>, Vec<String>) {
258 let mut types = Types::default();
259 let package = Package::from_bytes("composed", None, bytes, &mut types).unwrap();
260 let world = &types[package.ty()];
261 let mut imports: Vec<_> = world.imports.keys().cloned().collect();
262 let mut exports: Vec<_> = world.exports.keys().cloned().collect();
263 imports.sort();
264 exports.sort();
265 (imports, exports)
266 }
267
268 fn assert_composes(
269 primary: Vec<u8>,
270 middlewares: Vec<Middleware>,
271 expected_types_import: &str,
272 expected_handler_export: &str,
273 ) {
274 let composed = compose_middleware_pipeline(primary, middlewares).unwrap();
275 let (imports, exports) = imports_and_exports(composed);
276 assert_eq!(imports, [expected_types_import]);
277 assert_eq!(exports, [expected_handler_export]);
278 }
279
280 #[test]
281 fn all_components_at_0_3_0() {
282 assert_composes(
283 service("0.3.0"),
284 vec![middleware("0.3.0", "0.3.0"), middleware("0.3.0", "0.3.0")],
285 "wasi:http/types@0.3.0",
286 "wasi:http/handler@0.3.0",
287 );
288 }
289
290 #[test]
291 fn all_components_at_0_3_2() {
292 assert_composes(
293 service("0.3.2"),
294 vec![middleware("0.3.2", "0.3.2"), middleware("0.3.2", "0.3.2")],
295 "wasi:http/types@0.3.2",
296 "wasi:http/handler@0.3.2",
297 );
298 }
299
300 #[test]
301 fn newer_primary_with_older_middleware() {
302 assert_composes(
303 service("0.3.2"),
304 vec![middleware("0.3.0", "0.3.0")],
305 "wasi:http/types@0.3.2",
306 "wasi:http/handler@0.3.2",
307 );
308 }
309
310 #[test]
311 fn older_primary_with_newer_middleware() {
312 assert_composes(
313 service("0.3.0"),
314 vec![middleware("0.3.2", "0.3.2")],
315 "wasi:http/types@0.3.2",
316 "wasi:http/handler@0.3.2",
317 );
318 }
319
320 #[test]
321 fn middleware_with_mixed_import_and_export_versions() {
322 assert_composes(
323 service("0.3.2"),
324 vec![middleware("0.3.0", "0.3.2")],
325 "wasi:http/types@0.3.2",
326 "wasi:http/handler@0.3.2",
327 );
328 assert_composes(
329 service("0.3.0"),
330 vec![
331 middleware("0.3.0", "0.3.0"),
332 middleware("0.3.0", "0.3.2"),
333 middleware("0.3.2", "0.3.0"),
334 ],
335 "wasi:http/types@0.3.2",
336 "wasi:http/handler@0.3.2",
337 );
338 }
339
340 #[test]
341 fn primary_without_0_3_handler_export_fails() {
342 let primary = component(&["export run: func();"]);
343 let err = compose_middleware_pipeline(primary, vec![middleware("0.3.0", "0.3.0")])
344 .unwrap_err()
345 .to_string();
346 assert_eq!(
347 err,
348 "primary component does not export `wasi:http/handler@0.3.x`"
349 );
350 }
351
352 #[test]
353 fn middleware_without_0_3_handler_import_fails() {
354 let middleware = Middleware {
355 source: service("0.3.0"),
356 inherit: InheritConfiguration::All,
357 };
358 let err = compose_middleware_pipeline(service("0.3.0"), vec![middleware])
359 .unwrap_err()
360 .to_string();
361 assert_eq!(
362 err,
363 "middleware #1 does not import `wasi:http/handler@0.3.x`"
364 );
365 }
366
367 #[test]
368 fn ambiguous_handler_exports_fail() {
369 let primary = component(&[
370 "export wasi:http/handler@0.3.0;",
371 "export wasi:http/handler@0.3.2;",
372 ]);
373 let err = compose_middleware_pipeline(primary, vec![middleware("0.3.0", "0.3.0")])
374 .unwrap_err()
375 .to_string();
376 assert_eq!(
377 err,
378 "primary component has ambiguous `wasi:http/handler@0.3.x` exports: \
379 `wasi:http/handler@0.3.0` and `wasi:http/handler@0.3.2`"
380 );
381 }
382
383 #[test]
384 fn no_middleware_returns_primary_unchanged() {
385 let primary = service("0.3.2");
386 let composed = compose_middleware_pipeline(primary.clone(), vec![]).unwrap();
387 assert_eq!(composed, primary);
388 }
389}