webmachine_rust/
lib.rs

1/*!
2# webmachine-rust
3
4Port of Webmachine-Ruby (https://github.com/webmachine/webmachine-ruby) to Rust.
5
6webmachine-rust is a port of the Ruby version of webmachine. It implements a finite state machine for the HTTP protocol
7that provides semantic HTTP handling (based on the [diagram from the webmachine project](https://webmachine.github.io/images/http-headers-status-v3.png)).
8It is basically a HTTP toolkit for building HTTP-friendly applications using the [Hyper](https://crates.io/crates/hyper) rust crate.
9
10Webmachine-rust works with Hyper and sits between the Hyper Handler and your application code. It provides a resource struct
11with callbacks to handle the decisions required as the state machine is executed against the request with the following sequence.
12
13REQUEST -> Hyper Handler -> WebmachineDispatcher -> WebmachineResource -> Your application code -> WebmachineResponse -> Hyper -> RESPONSE
14
15## Features
16
17- Handles the hard parts of content negotiation, conditional requests, and response codes for you.
18- Provides a resource struct with points of extension to let you describe what is relevant about your particular resource.
19
20## Missing Features
21
22Currently, the following features from webmachine-ruby have not been implemented:
23
24- Visual debugger
25- Streaming response bodies
26
27## Implementation Deficiencies:
28
29This implementation has the following deficiencies:
30
31- Automatically decoding request bodies and encoding response bodies.
32- No easy mechanism to generate bodies with different content types (e.g. JSON vs. XML).
33- No easy mechanism for handling sub-paths in a resource.
34- Dynamically determining the methods allowed on the resource.
35
36## Getting started with Hyper
37
38Follow the getting started documentation from the Hyper crate to setup a Hyper service for your server.
39You need to define a WebmachineDispatcher that maps resource paths to your webmachine resources (WebmachineResource).
40Each WebmachineResource defines all the callbacks (via Closures) and values required to implement a resource.
41
42Note: This example uses the maplit crate to provide the `btreemap` macro and the log crate for the logging macros.
43
44 ```no_run
45 use webmachine_rust::*;
46 use webmachine_rust::context::*;
47 use webmachine_rust::headers::*;
48 use bytes::Bytes;
49 use serde_json::{Value, json};
50 use std::io::Read;
51 use std::net::SocketAddr;
52 use std::convert::Infallible;
53 use std::sync::Arc;
54 use maplit::btreemap;
55 use tracing::error;
56 use hyper_util::rt::TokioIo;
57 use tokio::net::TcpListener;
58 use hyper::server::conn::http1;
59 use hyper::service::service_fn;
60 use hyper::{body, Request};
61
62 # fn main() {}
63
64 async fn start_server() -> anyhow::Result<()> {
65   // setup the dispatcher, which maps paths to resources. We wrap it in an Arc so we can
66   // use it in the loop below.
67   let dispatcher = Arc::new(WebmachineDispatcher {
68       routes: btreemap!{
69          "/myresource" => WebmachineResource {
70            // Methods allowed on this resource
71            allowed_methods: owned_vec(&["OPTIONS", "GET", "HEAD", "POST"]),
72            // if the resource exists callback
73            resource_exists: callback(|_, _| true),
74            // callback to render the response for the resource
75            render_response: callback(|_, _| {
76                let json_response = json!({
77                   "data": [1, 2, 3, 4]
78                });
79                Some(Bytes::from(json_response.to_string()))
80            }),
81            // callback to process the post for the resource
82            process_post: callback(|_, _|  /* Handle the post here */ Ok(true) ),
83            // default everything else
84            .. WebmachineResource::default()
85          }
86      }
87   });
88
89   // Create a Hyper server that delegates to the dispatcher. See https://hyper.rs/guides/1/server/hello-world/
90   let addr: SocketAddr = "0.0.0.0:8080".parse()?;
91   let listener = TcpListener::bind(addr).await?;
92   loop {
93        let dispatcher = dispatcher.clone();
94        let (stream, _) = listener.accept().await?;
95        let io = TokioIo::new(stream);
96        tokio::task::spawn(async move {
97            if let Err(err) = http1::Builder::new()
98                .serve_connection(io, service_fn(|req: Request<body::Incoming>| dispatcher.dispatch(req)))
99                .await
100            {
101                error!("Error serving connection: {:?}", err);
102            }
103        });
104   }
105   Ok(())
106 }
107 ```
108
109## Example implementations
110
111For an example of a project using this crate, have a look at the [Pact Mock Server](https://github.com/pact-foundation/pact-core-mock-server/tree/main/pact_mock_server_cli) from the Pact reference implementation.
112*/
113
114#![warn(missing_docs)]
115
116use std::collections::{BTreeMap, HashMap};
117
118use bytes::Bytes;
119use chrono::{DateTime, FixedOffset, Utc};
120use http::{HeaderMap, Request, Response};
121use http_body_util::{BodyExt, Full};
122use hyper::body::Incoming;
123use itertools::Itertools;
124use lazy_static::lazy_static;
125use maplit::hashmap;
126use tracing::{debug, error, trace};
127
128use context::{WebmachineContext, WebmachineRequest, WebmachineResponse};
129use headers::HeaderValue;
130
131#[macro_use] pub mod headers;
132pub mod context;
133pub mod content_negotiation;
134
135/// Type of a Webmachine resource callback
136pub type WebmachineCallback<T> = Box<dyn Fn(&mut WebmachineContext, &WebmachineResource) -> T + Send + Sync>;
137
138/// Wrap a callback in a structure that is safe to call between threads
139pub fn callback<T, RT>(cb: T) -> WebmachineCallback<RT>
140  where T: Fn(&mut WebmachineContext, &WebmachineResource) -> RT + Send + Sync + 'static {
141  Box::new(cb)
142}
143
144/// Convenience function to create a vector of string structs from a slice of strings
145pub fn owned_vec(strings: &[&str]) -> Vec<String> {
146  strings.iter().map(|s| s.to_string()).collect()
147}
148
149/// Struct to represent a resource in webmachine
150pub struct WebmachineResource {
151  /// This is called just before the final response is constructed and sent. It allows the resource
152  /// an opportunity to modify the response after the webmachine has executed.
153  pub finalise_response: Option<WebmachineCallback<()>>,
154  /// This is invoked to render the response for the resource
155  pub render_response: WebmachineCallback<Option<Bytes>>,
156  /// Is the resource available? Returning false will result in a '503 Service Not Available'
157  /// response. Defaults to true. If the resource is only temporarily not available,
158  /// add a 'Retry-After' response header.
159  pub available: WebmachineCallback<bool>,
160  /// HTTP methods that are known to the resource. Default includes all standard HTTP methods.
161  /// One could override this to allow additional methods
162  pub known_methods: Vec<String>,
163  /// If the URI is too long to be processed, this should return true, which will result in a
164  /// '414 Request URI Too Long' response. Defaults to false.
165  pub uri_too_long: WebmachineCallback<bool>,
166  /// HTTP methods that are allowed on this resource. Defaults to GET','HEAD and 'OPTIONS'.
167  pub allowed_methods: Vec<String>,
168  /// If the request is malformed, this should return true, which will result in a
169  /// '400 Malformed Request' response. Defaults to false.
170  pub malformed_request: WebmachineCallback<bool>,
171  /// Is the client or request not authorized? Returning a Some<String>
172  /// will result in a '401 Unauthorized' response.  Defaults to None. If a Some(String) is
173  /// returned, the string will be used as the value in the WWW-Authenticate header.
174  pub not_authorized: WebmachineCallback<Option<String>>,
175  /// Is the request or client forbidden? Returning true will result in a '403 Forbidden' response.
176  /// Defaults to false.
177  pub forbidden: WebmachineCallback<bool>,
178  /// If the request includes any invalid Content-* headers, this should return true, which will
179  /// result in a '501 Not Implemented' response. Defaults to false.
180  pub unsupported_content_headers: WebmachineCallback<bool>,
181  /// The list of acceptable content types. Defaults to 'application/json'. If the content type
182  /// of the request is not in this list, a '415 Unsupported Media Type' response is returned.
183  pub acceptable_content_types: Vec<String>,
184  /// If the entity length on PUT or POST is invalid, this should return false, which will result
185  /// in a '413 Request Entity Too Large' response. Defaults to true.
186  pub valid_entity_length: WebmachineCallback<bool>,
187  /// This is called just before the final response is constructed and sent. This allows the
188  /// response to be modified. The default implementation adds CORS headers to the response
189  pub finish_request: WebmachineCallback<()>,
190  /// If the OPTIONS method is supported and is used, this returns a HashMap of headers that
191  /// should appear in the response. Defaults to CORS headers.
192  pub options: WebmachineCallback<Option<HashMap<String, Vec<String>>>>,
193  /// The list of content types that this resource produces. Defaults to 'application/json'. If
194  /// more than one is provided, and the client does not supply an Accept header, the first one
195  /// will be selected.
196  pub produces: Vec<String>,
197  /// The list of content languages that this resource provides. Defaults to an empty list,
198  /// which represents all languages. If more than one is provided, and the client does not
199  /// supply an Accept-Language header, the first one will be selected.
200  pub languages_provided: Vec<String>,
201  /// The list of charsets that this resource provides. Defaults to an empty list,
202  /// which represents all charsets with ISO-8859-1 as the default. If more than one is provided,
203  /// and the client does not supply an Accept-Charset header, the first one will be selected.
204  pub charsets_provided: Vec<String>,
205  /// The list of encodings your resource wants to provide. The encoding will be applied to the
206  /// response body automatically by Webmachine. Default includes only the 'identity' encoding.
207  pub encodings_provided: Vec<String>,
208  /// The list of header names that should be included in the response's Vary header. The standard
209  /// content negotiation headers (Accept, Accept-Encoding, Accept-Charset, Accept-Language) do
210  /// not need to be specified here as Webmachine will add the correct elements of those
211  /// automatically depending on resource behavior. Default is an empty list.
212  pub variances: Vec<String>,
213  /// Does the resource exist? Returning a false value will result in a '404 Not Found' response
214  /// unless it is a PUT or POST. Defaults to true.
215  pub resource_exists: WebmachineCallback<bool>,
216  /// If this resource is known to have existed previously, this should return true. Default is false.
217  pub previously_existed: WebmachineCallback<bool>,
218  /// If this resource has moved to a new location permanently, this should return the new
219  /// location as a String. Default is to return None
220  pub moved_permanently: WebmachineCallback<Option<String>>,
221  /// If this resource has moved to a new location temporarily, this should return the new
222  /// location as a String. Default is to return None
223  pub moved_temporarily: WebmachineCallback<Option<String>>,
224  /// If this returns true, the client will receive a '409 Conflict' response. This is only
225  /// called for PUT requests. Default is false.
226  pub is_conflict: WebmachineCallback<bool>,
227  /// Return true if the resource accepts POST requests to nonexistent resources. Defaults to false.
228  pub allow_missing_post: WebmachineCallback<bool>,
229  /// If this returns a value, it will be used as the value of the ETag header and for
230  /// comparison in conditional requests. Default is None.
231  pub generate_etag: WebmachineCallback<Option<String>>,
232  /// Returns the last modified date and time of the resource which will be added as the
233  /// Last-Modified header in the response and used in negotiating conditional requests.
234  /// Default is None
235  pub last_modified: WebmachineCallback<Option<DateTime<FixedOffset>>>,
236  /// Called when a DELETE request should be enacted. Return `Ok(true)` if the deletion succeeded,
237  /// and `Ok(false)` if the deletion was accepted but cannot yet be guaranteed to have finished.
238  /// If the delete fails for any reason, return an Err with the status code you wish returned
239  /// (a 500 status makes sense).
240  /// Defaults to `Ok(true)`.
241  pub delete_resource: WebmachineCallback<Result<bool, u16>>,
242  /// If POST requests should be treated as a request to put content into a (potentially new)
243  /// resource as opposed to a generic submission for processing, then this should return true.
244  /// If it does return true, then `create_path` will be called and the rest of the request will
245  /// be treated much like a PUT to the path returned by that call. Default is false.
246  pub post_is_create: WebmachineCallback<bool>,
247  /// If `post_is_create` returns false, then this will be called to process any POST request.
248  /// If it succeeds, return `Ok(true)`, `Ok(false)` otherwise. If it fails for any reason,
249  /// return an Err with the status code you wish returned (e.g., a 500 status makes sense).
250  /// Default is false. If you want the result of processing the POST to be a redirect, set
251  /// `context.redirect` to true.
252  pub process_post: WebmachineCallback<Result<bool, u16>>,
253  /// This will be called on a POST request if `post_is_create` returns true. It should create
254  /// the new resource and return the path as a valid URI part following the dispatcher prefix.
255  /// That path will replace the previous one in the return value of `WebmachineRequest.request_path`
256  /// for all subsequent resource function calls in the course of this request and will be set
257  /// as the value of the Location header of the response. If it fails for any reason,
258  /// return an Err with the status code you wish returned (e.g., a 500 status makes sense).
259  /// Default will return an `Ok(WebmachineRequest.request_path)`. If you want the result of
260  /// processing the POST to be a redirect, set `context.redirect` to true.
261  pub create_path: WebmachineCallback<Result<String, u16>>,
262  /// This will be called to process any PUT request. If it succeeds, return `Ok(true)`,
263  /// `Ok(false)` otherwise. If it fails for any reason, return an Err with the status code
264  /// you wish returned (e.g., a 500 status makes sense). Default is `Ok(true)`
265  pub process_put: WebmachineCallback<Result<bool, u16>>,
266  /// If this returns true, then it is assumed that multiple representations of the response are
267  /// possible and a single one cannot be automatically chosen, so a 300 Multiple Choices will
268  /// be sent instead of a 200. Default is false.
269  pub multiple_choices: WebmachineCallback<bool>,
270  /// If the resource expires, this should return the date/time it expires. Default is None.
271  pub expires: WebmachineCallback<Option<DateTime<FixedOffset>>>
272}
273
274fn true_fn(_: &mut WebmachineContext, _: &WebmachineResource) -> bool {
275  true
276}
277
278fn false_fn(_: &mut WebmachineContext, _: &WebmachineResource) -> bool {
279  false
280}
281
282fn none_fn<T>(_: &mut WebmachineContext, _: &WebmachineResource) -> Option<T> {
283  None
284}
285
286impl Default for WebmachineResource {
287  fn default() -> WebmachineResource {
288    WebmachineResource {
289      finalise_response: None,
290      available: callback(true_fn),
291      known_methods: owned_vec(&["OPTIONS", "GET", "POST", "PUT", "DELETE", "HEAD", "TRACE", "CONNECT", "PATCH"]),
292      uri_too_long: callback(false_fn),
293      allowed_methods: owned_vec(&["OPTIONS", "GET", "HEAD"]),
294      malformed_request: callback(false_fn),
295      not_authorized: callback(none_fn),
296      forbidden: callback(false_fn),
297      unsupported_content_headers: callback(false_fn),
298      acceptable_content_types: owned_vec(&["application/json"]),
299      valid_entity_length: callback(true_fn),
300      finish_request: callback(|context, resource| context.response.add_cors_headers(&resource.allowed_methods)),
301      options: callback(|_, resource| Some(WebmachineResponse::cors_headers(&resource.allowed_methods))),
302      produces: vec!["application/json".to_string()],
303      languages_provided: Vec::new(),
304      charsets_provided: Vec::new(),
305      encodings_provided: vec!["identity".to_string()],
306      variances: Vec::new(),
307      resource_exists: callback(true_fn),
308      previously_existed: callback(false_fn),
309      moved_permanently: callback(none_fn),
310      moved_temporarily: callback(none_fn),
311      is_conflict: callback(false_fn),
312      allow_missing_post: callback(false_fn),
313      generate_etag: callback(none_fn),
314      last_modified: callback(none_fn),
315      delete_resource: callback(|_, _| Ok(true)),
316      post_is_create: callback(false_fn),
317      process_post: callback(|_, _| Ok(false)),
318      process_put: callback(|_, _| Ok(true)),
319      multiple_choices: callback(false_fn),
320      create_path: callback(|context, _| Ok(context.request.request_path.clone())),
321      expires: callback(none_fn),
322      render_response: callback(none_fn)
323    }
324  }
325}
326
327fn sanitise_path(path: &str) -> Vec<String> {
328  path.split("/").filter(|p| !p.is_empty()).map(|p| p.to_string()).collect()
329}
330
331fn join_paths(base: &Vec<String>, path: &Vec<String>) -> String {
332  let mut paths = base.clone();
333  paths.extend_from_slice(path);
334  let filtered: Vec<String> = paths.iter().cloned().filter(|p| !p.is_empty()).collect();
335  if filtered.is_empty() {
336    "/".to_string()
337  } else {
338    let new_path = filtered.iter().join("/");
339    if new_path.starts_with("/") {
340      new_path
341    } else {
342      "/".to_owned() + &new_path
343    }
344  }
345}
346
347const MAX_STATE_MACHINE_TRANSITIONS: u8 = 100;
348
349#[derive(Debug, Clone, PartialEq, Eq, Hash)]
350enum Decision {
351    Start,
352    End(u16),
353    A3Options,
354    B3Options,
355    B4RequestEntityTooLarge,
356    B5UnknownContentType,
357    B6UnsupportedContentHeader,
358    B7Forbidden,
359    B8Authorized,
360    B9MalformedRequest,
361    B10MethodAllowed,
362    B11UriTooLong,
363    B12KnownMethod,
364    B13Available,
365    C3AcceptExists,
366    C4AcceptableMediaTypeAvailable,
367    D4AcceptLanguageExists,
368    D5AcceptableLanguageAvailable,
369    E5AcceptCharsetExists,
370    E6AcceptableCharsetAvailable,
371    F6AcceptEncodingExists,
372    F7AcceptableEncodingAvailable,
373    G7ResourceExists,
374    G8IfMatchExists,
375    G9IfMatchStarExists,
376    G11EtagInIfMatch,
377    H7IfMatchStarExists,
378    H10IfUnmodifiedSinceExists,
379    H11IfUnmodifiedSinceValid,
380    H12LastModifiedGreaterThanUMS,
381    I4HasMovedPermanently,
382    I12IfNoneMatchExists,
383    I13IfNoneMatchStarExists,
384    I7Put,
385    J18GetHead,
386    K5HasMovedPermanently,
387    K7ResourcePreviouslyExisted,
388    K13ETagInIfNoneMatch,
389    L5HasMovedTemporarily,
390    L7Post,
391    L13IfModifiedSinceExists,
392    L14IfModifiedSinceValid,
393    L15IfModifiedSinceGreaterThanNow,
394    L17IfLastModifiedGreaterThanMS,
395    M5Post,
396    M7PostToMissingResource,
397    M16Delete,
398    M20DeleteEnacted,
399    N5PostToMissingResource,
400    N11Redirect,
401    N16Post,
402    O14Conflict,
403    O16Put,
404    O18MultipleRepresentations,
405    O20ResponseHasBody,
406    P3Conflict,
407    P11NewResource
408}
409
410impl Decision {
411    fn is_terminal(&self) -> bool {
412        match self {
413            &Decision::End(_) => true,
414            &Decision::A3Options => true,
415            _ => false
416        }
417    }
418}
419
420enum Transition {
421  To(Decision),
422  Branch(Decision, Decision)
423}
424
425#[derive(Debug, Clone, PartialEq, Eq, Hash)]
426enum DecisionResult {
427  True(String),
428  False(String),
429  StatusCode(u16)
430}
431
432impl DecisionResult {
433  fn wrap(result: bool, reason: &str) -> DecisionResult {
434    if result {
435      DecisionResult::True(format!("is: {}", reason))
436    } else {
437      DecisionResult::False(format!("is not: {}", reason))
438    }
439  }
440}
441
442lazy_static! {
443    static ref TRANSITION_MAP: HashMap<Decision, Transition> = hashmap!{
444        Decision::Start => Transition::To(Decision::B13Available),
445        Decision::B3Options => Transition::Branch(Decision::A3Options, Decision::C3AcceptExists),
446        Decision::B4RequestEntityTooLarge => Transition::Branch(Decision::End(413), Decision::B3Options),
447        Decision::B5UnknownContentType => Transition::Branch(Decision::End(415), Decision::B4RequestEntityTooLarge),
448        Decision::B6UnsupportedContentHeader => Transition::Branch(Decision::End(501), Decision::B5UnknownContentType),
449        Decision::B7Forbidden => Transition::Branch(Decision::End(403), Decision::B6UnsupportedContentHeader),
450        Decision::B8Authorized => Transition::Branch(Decision::B7Forbidden, Decision::End(401)),
451        Decision::B9MalformedRequest => Transition::Branch(Decision::End(400), Decision::B8Authorized),
452        Decision::B10MethodAllowed => Transition::Branch(Decision::B9MalformedRequest, Decision::End(405)),
453        Decision::B11UriTooLong => Transition::Branch(Decision::End(414), Decision::B10MethodAllowed),
454        Decision::B12KnownMethod => Transition::Branch(Decision::B11UriTooLong, Decision::End(501)),
455        Decision::B13Available => Transition::Branch(Decision::B12KnownMethod, Decision::End(503)),
456        Decision::C3AcceptExists => Transition::Branch(Decision::C4AcceptableMediaTypeAvailable, Decision::D4AcceptLanguageExists),
457        Decision::C4AcceptableMediaTypeAvailable => Transition::Branch(Decision::D4AcceptLanguageExists, Decision::End(406)),
458        Decision::D4AcceptLanguageExists => Transition::Branch(Decision::D5AcceptableLanguageAvailable, Decision::E5AcceptCharsetExists),
459        Decision::D5AcceptableLanguageAvailable => Transition::Branch(Decision::E5AcceptCharsetExists, Decision::End(406)),
460        Decision::E5AcceptCharsetExists => Transition::Branch(Decision::E6AcceptableCharsetAvailable, Decision::F6AcceptEncodingExists),
461        Decision::E6AcceptableCharsetAvailable => Transition::Branch(Decision::F6AcceptEncodingExists, Decision::End(406)),
462        Decision::F6AcceptEncodingExists => Transition::Branch(Decision::F7AcceptableEncodingAvailable, Decision::G7ResourceExists),
463        Decision::F7AcceptableEncodingAvailable => Transition::Branch(Decision::G7ResourceExists, Decision::End(406)),
464        Decision::G7ResourceExists => Transition::Branch(Decision::G8IfMatchExists, Decision::H7IfMatchStarExists),
465        Decision::G8IfMatchExists => Transition::Branch(Decision::G9IfMatchStarExists, Decision::H10IfUnmodifiedSinceExists),
466        Decision::G9IfMatchStarExists => Transition::Branch(Decision::H10IfUnmodifiedSinceExists, Decision::G11EtagInIfMatch),
467        Decision::G11EtagInIfMatch => Transition::Branch(Decision::H10IfUnmodifiedSinceExists, Decision::End(412)),
468        Decision::H7IfMatchStarExists => Transition::Branch(Decision::End(412), Decision::I7Put),
469        Decision::H10IfUnmodifiedSinceExists => Transition::Branch(Decision::H11IfUnmodifiedSinceValid, Decision::I12IfNoneMatchExists),
470        Decision::H11IfUnmodifiedSinceValid => Transition::Branch(Decision::H12LastModifiedGreaterThanUMS, Decision::I12IfNoneMatchExists),
471        Decision::H12LastModifiedGreaterThanUMS => Transition::Branch(Decision::End(412), Decision::I12IfNoneMatchExists),
472        Decision::I4HasMovedPermanently => Transition::Branch(Decision::End(301), Decision::P3Conflict),
473        Decision::I7Put => Transition::Branch(Decision::I4HasMovedPermanently, Decision::K7ResourcePreviouslyExisted),
474        Decision::I12IfNoneMatchExists => Transition::Branch(Decision::I13IfNoneMatchStarExists, Decision::L13IfModifiedSinceExists),
475        Decision::I13IfNoneMatchStarExists => Transition::Branch(Decision::J18GetHead, Decision::K13ETagInIfNoneMatch),
476        Decision::J18GetHead => Transition::Branch(Decision::End(304), Decision::End(412)),
477        Decision::K13ETagInIfNoneMatch => Transition::Branch(Decision::J18GetHead, Decision::L13IfModifiedSinceExists),
478        Decision::K5HasMovedPermanently => Transition::Branch(Decision::End(301), Decision::L5HasMovedTemporarily),
479        Decision::K7ResourcePreviouslyExisted => Transition::Branch(Decision::K5HasMovedPermanently, Decision::L7Post),
480        Decision::L5HasMovedTemporarily => Transition::Branch(Decision::End(307), Decision::M5Post),
481        Decision::L7Post => Transition::Branch(Decision::M7PostToMissingResource, Decision::End(404)),
482        Decision::L13IfModifiedSinceExists => Transition::Branch(Decision::L14IfModifiedSinceValid, Decision::M16Delete),
483        Decision::L14IfModifiedSinceValid => Transition::Branch(Decision::L15IfModifiedSinceGreaterThanNow, Decision::M16Delete),
484        Decision::L15IfModifiedSinceGreaterThanNow => Transition::Branch(Decision::M16Delete, Decision::L17IfLastModifiedGreaterThanMS),
485        Decision::L17IfLastModifiedGreaterThanMS => Transition::Branch(Decision::M16Delete, Decision::End(304)),
486        Decision::M5Post => Transition::Branch(Decision::N5PostToMissingResource, Decision::End(410)),
487        Decision::M7PostToMissingResource => Transition::Branch(Decision::N11Redirect, Decision::End(404)),
488        Decision::M16Delete => Transition::Branch(Decision::M20DeleteEnacted, Decision::N16Post),
489        Decision::M20DeleteEnacted => Transition::Branch(Decision::O20ResponseHasBody, Decision::End(202)),
490        Decision::N5PostToMissingResource => Transition::Branch(Decision::N11Redirect, Decision::End(410)),
491        Decision::N11Redirect => Transition::Branch(Decision::End(303), Decision::P11NewResource),
492        Decision::N16Post => Transition::Branch(Decision::N11Redirect, Decision::O16Put),
493        Decision::O14Conflict => Transition::Branch(Decision::End(409), Decision::P11NewResource),
494        Decision::O16Put => Transition::Branch(Decision::O14Conflict, Decision::O18MultipleRepresentations),
495        Decision::P3Conflict => Transition::Branch(Decision::End(409), Decision::P11NewResource),
496        Decision::P11NewResource => Transition::Branch(Decision::End(201), Decision::O20ResponseHasBody),
497        Decision::O18MultipleRepresentations => Transition::Branch(Decision::End(300), Decision::End(200)),
498        Decision::O20ResponseHasBody => Transition::Branch(Decision::O18MultipleRepresentations, Decision::End(204))
499    };
500}
501
502fn resource_etag_matches_header_values(
503  resource: &WebmachineResource,
504  context: &mut WebmachineContext,
505  header: &str
506) -> bool {
507  let header_values = context.request.find_header(header);
508  match (resource.generate_etag)(context, resource) {
509    Some(etag) => {
510      header_values.iter().find(|val| {
511        if val.value.starts_with("W/") {
512          val.weak_etag().unwrap() == etag
513        } else {
514          val.value == etag
515        }
516      }).is_some()
517    },
518    None => false
519  }
520}
521
522fn validate_header_date(
523  request: &WebmachineRequest,
524  header: &str,
525  context_meta: &mut Option<DateTime<FixedOffset>>
526) -> bool {
527  let header_values = request.find_header(header);
528  if let Some(date_value) = header_values.first() {
529    match DateTime::parse_from_rfc2822(&date_value.value) {
530      Ok(datetime) => {
531        *context_meta = Some(datetime.clone());
532        true
533      },
534      Err(err) => {
535        debug!("Failed to parse '{}' header value '{:?}' - {}", header, date_value, err);
536        false
537      }
538    }
539  } else {
540    false
541  }
542}
543
544fn execute_decision(
545  decision: &Decision,
546  context: &mut WebmachineContext,
547  resource: &WebmachineResource
548) -> DecisionResult {
549  match decision {
550    Decision::B10MethodAllowed => {
551      match resource.allowed_methods
552        .iter().find(|m| m.to_uppercase() == context.request.method.to_uppercase()) {
553        Some(_) => DecisionResult::True("method is in the list of allowed methods".to_string()),
554        None => {
555          context.response.add_header("Allow", resource.allowed_methods
556            .iter()
557            .cloned()
558            .map(HeaderValue::basic)
559            .collect());
560          DecisionResult::False("method is not in the list of allowed methods".to_string())
561        }
562      }
563    },
564    Decision::B11UriTooLong => {
565      DecisionResult::wrap((resource.uri_too_long)(context, resource), "URI too long")
566    },
567    Decision::B12KnownMethod => DecisionResult::wrap(resource.known_methods
568      .iter().find(|m| m.to_uppercase() == context.request.method.to_uppercase()).is_some(),
569      "known method"),
570    Decision::B13Available => {
571      DecisionResult::wrap((resource.available)(context, resource), "available")
572    },
573    Decision::B9MalformedRequest => {
574      DecisionResult::wrap((resource.malformed_request)(context, resource), "malformed request")
575    },
576    Decision::B8Authorized => {
577      match (resource.not_authorized)(context, resource) {
578        Some(realm) => {
579          context.response.add_header("WWW-Authenticate", vec![HeaderValue::parse_string(realm.as_str())]);
580          DecisionResult::False("is not authorized".to_string())
581        },
582        None => DecisionResult::True("is not authorized".to_string())
583      }
584    },
585    Decision::B7Forbidden => {
586      DecisionResult::wrap((resource.forbidden)(context, resource), "forbidden")
587    },
588    Decision::B6UnsupportedContentHeader => {
589      DecisionResult::wrap((resource.unsupported_content_headers)(context, resource), "unsupported content headers")
590    },
591    Decision::B5UnknownContentType => {
592      DecisionResult::wrap(context.request.is_put_or_post() && resource.acceptable_content_types
593        .iter().find(|ct| context.request.content_type().to_uppercase() == ct.to_uppercase() )
594        .is_none(), "acceptable content types")
595    },
596    Decision::B4RequestEntityTooLarge => {
597      DecisionResult::wrap(context.request.is_put_or_post() && !(resource.valid_entity_length)(context, resource),
598        "valid entity length")
599    },
600    Decision::B3Options => DecisionResult::wrap(context.request.is_options(), "options"),
601    Decision::C3AcceptExists => DecisionResult::wrap(context.request.has_accept_header(), "has accept header"),
602    Decision::C4AcceptableMediaTypeAvailable => match content_negotiation::matching_content_type(resource, &context.request) {
603      Some(media_type) => {
604        context.selected_media_type = Some(media_type);
605        DecisionResult::True("acceptable media type is available".to_string())
606      },
607      None => DecisionResult::False("acceptable media type is not available".to_string())
608    },
609    Decision::D4AcceptLanguageExists => DecisionResult::wrap(context.request.has_accept_language_header(),
610                                                             "has accept language header"),
611    Decision::D5AcceptableLanguageAvailable => match content_negotiation::matching_language(resource, &context.request) {
612      Some(language) => {
613        if language != "*" {
614          context.selected_language = Some(language.clone());
615          context.response.add_header("Content-Language", vec![HeaderValue::parse_string(&language)]);
616        }
617        DecisionResult::True("acceptable language is available".to_string())
618      },
619      None => DecisionResult::False("acceptable language is not available".to_string())
620    },
621    Decision::E5AcceptCharsetExists => DecisionResult::wrap(context.request.has_accept_charset_header(),
622                                                            "accept charset exists"),
623    Decision::E6AcceptableCharsetAvailable => match content_negotiation::matching_charset(resource, &context.request) {
624      Some(charset) => {
625        if charset != "*" {
626            context.selected_charset = Some(charset.clone());
627        }
628        DecisionResult::True("acceptable charset is available".to_string())
629      },
630      None => DecisionResult::False("acceptable charset is not available".to_string())
631    },
632    Decision::F6AcceptEncodingExists => DecisionResult::wrap(context.request.has_accept_encoding_header(),
633                                                             "accept encoding exists"),
634    Decision::F7AcceptableEncodingAvailable => match content_negotiation::matching_encoding(resource, &context.request) {
635      Some(encoding) => {
636        context.selected_encoding = Some(encoding.clone());
637        if encoding != "identity" {
638            context.response.add_header("Content-Encoding", vec![HeaderValue::parse_string(&encoding)]);
639        }
640        DecisionResult::True("acceptable encoding is available".to_string())
641      },
642      None => DecisionResult::False("acceptable encoding is not available".to_string())
643    },
644    Decision::G7ResourceExists => {
645      DecisionResult::wrap((resource.resource_exists)(context, resource), "resource exists")
646    },
647    Decision::G8IfMatchExists => DecisionResult::wrap(context.request.has_header("If-Match"),
648                                                      "match exists"),
649    Decision::G9IfMatchStarExists | &Decision::H7IfMatchStarExists => DecisionResult::wrap(
650        context.request.has_header_value("If-Match", "*"), "match star exists"),
651    Decision::G11EtagInIfMatch => DecisionResult::wrap(resource_etag_matches_header_values(resource, context, "If-Match"),
652                                                       "etag in if match"),
653    Decision::H10IfUnmodifiedSinceExists => DecisionResult::wrap(context.request.has_header("If-Unmodified-Since"),
654                                                                 "unmodified since exists"),
655    Decision::H11IfUnmodifiedSinceValid => DecisionResult::wrap(validate_header_date(&context.request, "If-Unmodified-Since", &mut context.if_unmodified_since),
656                                                                "unmodified since valid"),
657    Decision::H12LastModifiedGreaterThanUMS => {
658      match context.if_unmodified_since {
659        Some(unmodified_since) => {
660          match (resource.last_modified)(context, resource) {
661            Some(datetime) => DecisionResult::wrap(datetime > unmodified_since,
662                                                   "resource last modified date is greater than unmodified since"),
663            None => DecisionResult::False("resource has no last modified date".to_string())
664          }
665        },
666        None => DecisionResult::False("resource does not provide last modified date".to_string())
667      }
668    },
669    Decision::I7Put => if context.request.is_put() {
670      context.new_resource = true;
671      DecisionResult::True("is a PUT request".to_string())
672    } else {
673      DecisionResult::False("is not a PUT request".to_string())
674    },
675    Decision::I12IfNoneMatchExists => DecisionResult::wrap(context.request.has_header("If-None-Match"),
676                                                           "none match exists"),
677    Decision::I13IfNoneMatchStarExists => DecisionResult::wrap(context.request.has_header_value("If-None-Match", "*"),
678                                                               "none match star exists"),
679    Decision::J18GetHead => DecisionResult::wrap(context.request.is_get_or_head(),
680                                                 "is GET or HEAD request"),
681    Decision::K7ResourcePreviouslyExisted => {
682      DecisionResult::wrap((resource.previously_existed)(context, resource), "resource previously existed")
683    },
684    Decision::K13ETagInIfNoneMatch => DecisionResult::wrap(resource_etag_matches_header_values(resource, context, "If-None-Match"),
685                                                           "ETag in if none match"),
686    Decision::L5HasMovedTemporarily => {
687      match (resource.moved_temporarily)(context, resource) {
688        Some(location) => {
689          context.response.add_header("Location", vec![HeaderValue::basic(&location)]);
690          DecisionResult::True("resource has moved temporarily".to_string())
691        },
692        None => DecisionResult::False("resource has not moved temporarily".to_string())
693      }
694    },
695    Decision::L7Post | &Decision::M5Post | &Decision::N16Post => DecisionResult::wrap(context.request.is_post(),
696                                                                                      "a POST request"),
697    Decision::L13IfModifiedSinceExists => DecisionResult::wrap(context.request.has_header("If-Modified-Since"),
698                                                               "if modified since exists"),
699    Decision::L14IfModifiedSinceValid => DecisionResult::wrap(validate_header_date(&context.request,
700        "If-Modified-Since", &mut context.if_modified_since), "modified since valid"),
701    Decision::L15IfModifiedSinceGreaterThanNow => {
702        let datetime = context.if_modified_since.unwrap();
703        let timezone = datetime.timezone();
704        DecisionResult::wrap(datetime > Utc::now().with_timezone(&timezone),
705                             "modified since greater than now")
706    },
707    Decision::L17IfLastModifiedGreaterThanMS => {
708      match context.if_modified_since {
709        Some(unmodified_since) => {
710          match (resource.last_modified)(context, resource) {
711            Some(datetime) => DecisionResult::wrap(datetime > unmodified_since,
712                                                   "last modified greater than modified since"),
713            None => DecisionResult::False("resource has no last modified date".to_string())
714          }
715        },
716        None => DecisionResult::False("resource does not return if_modified_since".to_string())
717      }
718    },
719    Decision::I4HasMovedPermanently | &Decision::K5HasMovedPermanently => {
720      match (resource.moved_permanently)(context, resource) {
721        Some(location) => {
722          context.response.add_header("Location", vec![HeaderValue::basic(&location)]);
723          DecisionResult::True("resource has moved permanently".to_string())
724        },
725        None => DecisionResult::False("resource has not moved permanently".to_string())
726      }
727    },
728    Decision::M7PostToMissingResource | &Decision::N5PostToMissingResource => {
729      if (resource.allow_missing_post)(context, resource) {
730        context.new_resource = true;
731        DecisionResult::True("resource allows POST to missing resource".to_string())
732      } else {
733        DecisionResult::False("resource does not allow POST to missing resource".to_string())
734      }
735    },
736    Decision::M16Delete => DecisionResult::wrap(context.request.is_delete(),
737                                                "a DELETE request"),
738    Decision::M20DeleteEnacted => {
739      match (resource.delete_resource)(context, resource) {
740        Ok(result) => DecisionResult::wrap(result, "resource DELETE succeeded"),
741        Err(status) => DecisionResult::StatusCode(status)
742      }
743    },
744    Decision::N11Redirect => {
745      if (resource.post_is_create)(context, resource) {
746        match (resource.create_path)(context, resource) {
747          Ok(path) => {
748            let base_path = sanitise_path(&context.request.base_path);
749            let new_path = join_paths(&base_path, &sanitise_path(&path));
750            context.request.request_path = path.clone();
751            context.response.add_header("Location", vec![HeaderValue::basic(&new_path)]);
752            DecisionResult::wrap(context.redirect, "should redirect")
753          },
754          Err(status) => DecisionResult::StatusCode(status)
755        }
756      } else {
757        match (resource.process_post)(context, resource) {
758          Ok(_) => DecisionResult::wrap(context.redirect, "processing POST succeeded"),
759          Err(status) => DecisionResult::StatusCode(status)
760        }
761      }
762    },
763    Decision::P3Conflict | &Decision::O14Conflict => {
764      DecisionResult::wrap((resource.is_conflict)(context, resource), "resource conflict")
765    },
766    Decision::P11NewResource => {
767      if context.request.is_put() {
768        match (resource.process_put)(context, resource) {
769          Ok(_) => DecisionResult::wrap(context.new_resource, "process PUT succeeded"),
770          Err(status) => DecisionResult::StatusCode(status)
771        }
772      } else {
773        DecisionResult::wrap(context.new_resource, "new resource creation succeeded")
774      }
775    },
776    Decision::O16Put => DecisionResult::wrap(context.request.is_put(), "a PUT request"),
777    Decision::O18MultipleRepresentations => {
778      DecisionResult::wrap((resource.multiple_choices)(context, resource), "multiple choices exist")
779    },
780    Decision::O20ResponseHasBody => DecisionResult::wrap(context.response.has_body(), "response has a body"),
781    _ => DecisionResult::False("default decision is false".to_string())
782  }
783}
784
785fn execute_state_machine(context: &mut WebmachineContext, resource: &WebmachineResource) {
786  let mut state = Decision::Start;
787  let mut decisions: Vec<(Decision, bool, Decision)> = Vec::new();
788  let mut loop_count = 0;
789  while !state.is_terminal() {
790    loop_count += 1;
791    if loop_count >= MAX_STATE_MACHINE_TRANSITIONS {
792      panic!("State machine has not terminated within {} transitions!", loop_count);
793    }
794    trace!("state is {:?}", state);
795    state = match TRANSITION_MAP.get(&state) {
796      Some(transition) => match transition {
797        Transition::To(decision) => {
798          trace!("Transitioning to {:?}", decision);
799          decision.clone()
800        },
801        Transition::Branch(decision_true, decision_false) => {
802          match execute_decision(&state, context, resource) {
803            DecisionResult::True(reason) => {
804              trace!("Transitioning from {:?} to {:?} as decision is true -> {}", state, decision_true, reason);
805              decisions.push((state, true, decision_true.clone()));
806              decision_true.clone()
807            },
808            DecisionResult::False(reason) => {
809              trace!("Transitioning from {:?} to {:?} as decision is false -> {}", state, decision_false, reason);
810              decisions.push((state, false, decision_false.clone()));
811              decision_false.clone()
812            },
813            DecisionResult::StatusCode(code) => {
814              let decision = Decision::End(code);
815              trace!("Transitioning from {:?} to {:?} as decision is a status code", state, decision);
816              decisions.push((state, false, decision.clone()));
817              decision.clone()
818            }
819          }
820        }
821      },
822      None => {
823        error!("Error transitioning from {:?}, the TRANSITION_MAP is mis-configured", state);
824        decisions.push((state, false, Decision::End(500)));
825        Decision::End(500)
826      }
827    }
828  }
829  trace!("Final state is {:?}", state);
830  match state {
831    Decision::End(status) => context.response.status = status,
832    Decision::A3Options => {
833      context.response.status = 204;
834      match (resource.options)(context, resource) {
835        Some(headers) => context.response.add_headers(headers),
836        None => ()
837      }
838    },
839    _ => ()
840  }
841}
842
843fn update_paths_for_resource(request: &mut WebmachineRequest, base_path: &str) {
844  request.base_path = base_path.into();
845  if request.request_path.len() > base_path.len() {
846    let request_path = request.request_path.clone();
847    let subpath = request_path.split_at(base_path.len()).1;
848    if subpath.starts_with("/") {
849      request.request_path = subpath.to_string();
850    } else {
851      request.request_path = "/".to_owned() + subpath;
852    }
853  } else {
854    request.request_path = "/".to_string();
855  }
856}
857
858fn parse_header_values(value: &str) -> Vec<HeaderValue> {
859  if value.is_empty() {
860    Vec::new()
861  } else {
862    value.split(',').map(|s| HeaderValue::parse_string(s.trim())).collect()
863  }
864}
865
866fn headers_from_http_request(headers: &HeaderMap<http::HeaderValue>) -> HashMap<String, Vec<HeaderValue>> {
867  headers.iter()
868    .map(|(name, value)| (name.to_string(), parse_header_values(value.to_str().unwrap_or_default())))
869    .collect()
870}
871
872fn decode_query(query: &str) -> String {
873  let mut chars = query.chars();
874  let mut ch = chars.next();
875  let mut result = String::new();
876
877  while ch.is_some() {
878    let c = ch.unwrap();
879    if c == '%' {
880      let c1 = chars.next();
881      let c2 = chars.next();
882      match (c1, c2) {
883        (Some(v1), Some(v2)) => {
884          let mut s = String::new();
885          s.push(v1);
886          s.push(v2);
887          let decoded: Result<Vec<u8>, _> = hex::decode(s);
888          match decoded {
889            Ok(n) => result.push(n[0] as char),
890            Err(_) => {
891              result.push('%');
892              result.push(v1);
893              result.push(v2);
894            }
895          }
896        },
897        (Some(v1), None) => {
898          result.push('%');
899          result.push(v1);
900        },
901        _ => result.push('%')
902      }
903    } else if c == '+' {
904      result.push(' ');
905    } else {
906      result.push(c);
907    }
908
909    ch = chars.next();
910  }
911
912  result
913}
914
915fn parse_query(query: &str) -> HashMap<String, Vec<String>> {
916  if !query.is_empty() {
917    query.split("&").map(|kv| {
918      if kv.is_empty() {
919        vec![]
920      } else if kv.contains("=") {
921        kv.splitn(2, "=").collect::<Vec<&str>>()
922      } else {
923        vec![kv]
924      }
925    }).fold(HashMap::new(), |mut map, name_value| {
926      if !name_value.is_empty() {
927        let name = decode_query(name_value[0]);
928        let value = if name_value.len() > 1 {
929          decode_query(name_value[1])
930        } else {
931          String::new()
932        };
933        map.entry(name).or_insert(vec![]).push(value);
934      }
935      map
936    })
937  } else {
938    HashMap::new()
939  }
940}
941
942async fn request_from_http_request(req: Request<Incoming>) -> WebmachineRequest {
943  let request_path = req.uri().path().to_string();
944  let method = req.method().to_string();
945  let query = match req.uri().query() {
946    Some(query) => parse_query(query),
947    None => HashMap::new()
948  };
949  let headers = headers_from_http_request(req.headers());
950
951  let body = match req.collect().await {
952    Ok(body) => {
953      let body = body.to_bytes();
954      if body.is_empty() {
955        None
956      } else {
957        Some(body.clone())
958      }
959    }
960    Err(err) => {
961      error!("Failed to read the request body: {}", err);
962      None
963    }
964  };
965
966  WebmachineRequest {
967    request_path,
968    base_path: "/".to_string(),
969    method,
970    headers,
971    body,
972    query
973  }
974}
975
976fn finalise_response(context: &mut WebmachineContext, resource: &WebmachineResource) {
977  if !context.response.has_header("Content-Type") {
978    let media_type = match &context.selected_media_type {
979      &Some(ref media_type) => media_type.clone(),
980      &None => "application/json".to_string()
981    };
982    let charset = match &context.selected_charset {
983      &Some(ref charset) => charset.clone(),
984      &None => "ISO-8859-1".to_string()
985    };
986    let header = HeaderValue {
987      value: media_type,
988      params: hashmap!{ "charset".to_string() => charset },
989      quote: false
990    };
991    context.response.add_header("Content-Type", vec![header]);
992  }
993
994  let mut vary_header = if !context.response.has_header("Vary") {
995    resource.variances
996      .iter()
997      .map(|h| HeaderValue::parse_string(h))
998      .collect()
999  } else {
1000    Vec::new()
1001  };
1002
1003  if resource.languages_provided.len() > 1 {
1004    vary_header.push(h!("Accept-Language"));
1005  }
1006  if resource.charsets_provided.len() > 1 {
1007    vary_header.push(h!("Accept-Charset"));
1008  }
1009  if resource.encodings_provided.len() > 1 {
1010    vary_header.push(h!("Accept-Encoding"));
1011  }
1012  if resource.produces.len() > 1 {
1013    vary_header.push(h!("Accept"));
1014  }
1015
1016  if vary_header.len() > 1 {
1017    context.response.add_header("Vary", vary_header.iter().cloned().unique().collect());
1018  }
1019
1020  if context.request.is_get_or_head() {
1021    {
1022      match (resource.generate_etag)(context, resource) {
1023        Some(etag) => context.response.add_header("ETag", vec![HeaderValue::basic(&etag).quote()]),
1024        None => ()
1025      }
1026    }
1027    {
1028      match (resource.expires)(context, resource) {
1029        Some(datetime) => context.response.add_header("Expires", vec![HeaderValue::basic(datetime.to_rfc2822()).quote()]),
1030        None => ()
1031      }
1032    }
1033    {
1034      match (resource.last_modified)(context, resource) {
1035        Some(datetime) => context.response.add_header("Last-Modified", vec![HeaderValue::basic(datetime.to_rfc2822()).quote()]),
1036        None => ()
1037      }
1038    }
1039  }
1040
1041  if context.response.body.is_none() && context.response.status == 200 && context.request.is_get() {
1042    match (resource.render_response)(context, resource) {
1043      Some(body) => context.response.body = Some(body),
1044      None => ()
1045    }
1046  }
1047
1048  match &resource.finalise_response {
1049    Some(callback) => {
1050      callback(context, resource);
1051    },
1052    None => ()
1053  }
1054
1055  debug!("Final response: {:?}", context.response);
1056}
1057
1058fn generate_http_response(context: &WebmachineContext) -> http::Result<Response<Full<Bytes>>> {
1059  let mut response = Response::builder().status(context.response.status);
1060
1061  for (header, values) in context.response.headers.clone() {
1062    let header_values = values.iter().map(|h| h.to_string()).join(", ");
1063    response = response.header(&header, &header_values);
1064  }
1065  match context.response.body.clone() {
1066    Some(body) => response.body(Full::new(body.into())),
1067    None => response.body(Full::new(Bytes::default()))
1068  }
1069}
1070
1071/// The main hyper dispatcher
1072pub struct WebmachineDispatcher {
1073  /// Map of routes to webmachine resources
1074  pub routes: BTreeMap<&'static str, WebmachineResource>
1075}
1076
1077impl WebmachineDispatcher {
1078  /// Main dispatch function for the Webmachine. This will look for a matching resource
1079  /// based on the request path. If one is not found, a 404 Not Found response is returned
1080  pub async fn dispatch(&self, req: Request<Incoming>) -> http::Result<Response<Full<Bytes>>> {
1081    let mut context = self.context_from_http_request(req).await;
1082    self.dispatch_to_resource(&mut context);
1083    generate_http_response(&context)
1084  }
1085
1086  async fn context_from_http_request(&self, req: Request<Incoming>) -> WebmachineContext {
1087    let request = request_from_http_request(req).await;
1088    WebmachineContext {
1089      request,
1090      response: WebmachineResponse::default(),
1091      .. WebmachineContext::default()
1092    }
1093  }
1094
1095  fn match_paths(&self, request: &WebmachineRequest) -> Vec<String> {
1096    let request_path = sanitise_path(&request.request_path);
1097    self.routes
1098      .keys()
1099      .filter(|k| request_path.starts_with(&sanitise_path(k)))
1100      .map(|k| k.to_string())
1101      .collect()
1102  }
1103
1104  fn lookup_resource(&self, path: &str) -> Option<&WebmachineResource> {
1105    self.routes.get(path)
1106  }
1107
1108  /// Dispatches to the matching webmachine resource. If there is no matching resource, returns
1109  /// 404 Not Found response
1110  pub fn dispatch_to_resource(&self, context: &mut WebmachineContext) {
1111    let matching_paths = self.match_paths(&context.request);
1112    let ordered_by_length: Vec<String> = matching_paths.iter()
1113      .cloned()
1114      .sorted_by(|a, b| Ord::cmp(&b.len(), &a.len())).collect();
1115    match ordered_by_length.first() {
1116      Some(path) => {
1117        update_paths_for_resource(&mut context.request, path);
1118        if let Some(resource) = self.lookup_resource(path) {
1119          execute_state_machine(context, &resource);
1120          finalise_response(context, &resource);
1121        } else {
1122          context.response.status = 404;
1123        }
1124      },
1125      None => context.response.status = 404
1126    };
1127  }
1128}
1129
1130#[cfg(test)]
1131mod tests;
1132
1133#[cfg(test)]
1134mod content_negotiation_tests;