logo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
use chrono::{DateTime, FixedOffset};
use futures::Future;
use std::{collections::HashMap, pin::Pin};

use super::{callback, Callback, Context, Response};

/// Struct to represent a resource in webmachine
#[derive(Clone)]
pub struct Resource<'a> {
    /// This is called just before the final response is constructed and sent. It allows the resource
    /// an opportunity to modify the response after the webmachine has executed.
    pub finalise_response: Option<Callback<'a, ()>>,
    /// This is invoked to render the response for the resource
    pub render_response: Callback<'a, Option<String>>,
    /// Is the resource available? Returning false will result in a '503 Service Not Available'
    /// response. Defaults to true. If the resource is only temporarily not available,
    /// add a 'Retry-After' response header.
    pub available: Callback<'a, bool>,
    /// HTTP methods that are known to the resource. Default includes all standard HTTP methods.
    /// One could override this to allow additional methods
    pub known_methods: Vec<&'a str>,
    /// If the URI is too long to be processed, this should return true, which will result in a
    /// '414 Request URI Too Long' response. Defaults to false.
    pub uri_too_long: Callback<'a, bool>,
    /// HTTP methods that are allowed on this resource. Defaults to GET','HEAD and 'OPTIONS'.
    pub allowed_methods: Vec<&'a str>,
    /// If the request is malformed, this should return true, which will result in a
    /// '400 Malformed Request' response. Defaults to false.
    pub malformed_request: Callback<'a, bool>,
    /// Is the client or request not authorized? Returning a Some<String>
    /// will result in a '401 Unauthorized' response.  Defaults to None. If a Some(String) is
    /// returned, the string will be used as the value in the WWW-Authenticate header.
    pub not_authorized: Callback<'a, Option<String>>,
    /// Is the request or client forbidden? Returning true will result in a '403 Forbidden' response.
    /// Defaults to false.
    pub forbidden: Callback<'a, bool>,
    /// If the request includes any invalid Content-* headers, this should return true, which will
    /// result in a '501 Not Implemented' response. Defaults to false.
    pub unsupported_content_headers: Callback<'a, bool>,
    /// The list of acceptable content types. Defaults to 'application/json'. If the content type
    /// of the request is not in this list, a '415 Unsupported Media Type' response is returned.
    pub acceptable_content_types: Vec<&'a str>,
    /// If the entity length on PUT or POST is invalid, this should return false, which will result
    /// in a '413 Request Entity Too Large' response. Defaults to true.
    pub valid_entity_length: Callback<'a, bool>,
    /// This is called just before the final response is constructed and sent. This allows the
    /// response to be modified. The default implementation adds CORS headers to the response
    pub finish_request: Callback<'a, ()>,
    /// If the OPTIONS method is supported and is used, this returns a HashMap of headers that
    /// should appear in the response. Defaults to CORS headers.
    pub options: Callback<'a, Option<HashMap<String, Vec<String>>>>,
    /// The list of content types that this resource produces. Defaults to 'application/json'. If
    /// more than one is provided, and the client does not supply an Accept header, the first one
    /// will be selected.
    pub produces: Vec<&'a str>,
    /// The list of content languages that this resource provides. Defaults to an empty list,
    /// which represents all languages. If more than one is provided, and the client does not
    /// supply an Accept-Language header, the first one will be selected.
    pub languages_provided: Vec<&'a str>,
    /// The list of charsets that this resource provides. Defaults to an empty list,
    /// which represents all charsets with ISO-8859-1 as the default. If more than one is provided,
    /// and the client does not supply an Accept-Charset header, the first one will be selected.
    pub charsets_provided: Vec<&'a str>,
    /// The list of encodings your resource wants to provide. The encoding will be applied to the
    /// response body automatically by Webmachine. Default includes only the 'identity' encoding.
    pub encodings_provided: Vec<&'a str>,
    /// The list of header names that should be included in the response's Vary header. The standard
    /// content negotiation headers (Accept, Accept-Encoding, Accept-Charset, Accept-Language) do
    /// not need to be specified here as Webmachine will add the correct elements of those
    /// automatically depending on resource behavior. Default is an empty list.
    pub variances: Vec<&'a str>,
    /// Does the resource exist? Returning a false value will result in a '404 Not Found' response
    /// unless it is a PUT or POST. Defaults to true.
    pub resource_exists: Callback<'a, bool>,
    /// If this resource is known to have existed previously, this should return true. Default is false.
    pub previously_existed: Callback<'a, bool>,
    /// If this resource has moved to a new location permanently, this should return the new
    /// location as a String. Default is to return None
    pub moved_permanently: Callback<'a, Option<String>>,
    /// If this resource has moved to a new location temporarily, this should return the new
    /// location as a String. Default is to return None
    pub moved_temporarily: Callback<'a, Option<String>>,
    /// If this returns true, the client will receive a '409 Conflict' response. This is only
    /// called for PUT requests. Default is false.
    pub is_conflict: Callback<'a, bool>,
    /// Return true if the resource accepts POST requests to nonexistent resources. Defaults to false.
    pub allow_missing_post: Callback<'a, bool>,
    /// If this returns a value, it will be used as the value of the ETag header and for
    /// comparison in conditional requests. Default is None.
    pub generate_etag: Callback<'a, Option<String>>,
    /// Returns the last modified date and time of the resource which will be added as the
    /// Last-Modified header in the response and used in negotiating conditional requests.
    /// Default is None
    pub last_modified: Callback<'a, Option<DateTime<FixedOffset>>>,
    /// Called when a DELETE request should be enacted. Return `Ok(true)` if the deletion succeeded,
    /// and `Ok(false)` if the deletion was accepted but cannot yet be guaranteed to have finished.
    /// If the delete fails for any reason, return an Err with the status code you wish returned
    /// (a 500 status makes sense).
    /// Defaults to `Ok(true)`.
    pub delete_resource: Callback<'a, Result<bool, u16>>,
    /// If POST requests should be treated as a request to put content into a (potentially new)
    /// resource as opposed to a generic submission for processing, then this should return true.
    /// If it does return true, then `create_path` will be called and the rest of the request will
    /// be treated much like a PUT to the path returned by that call. Default is false.
    pub post_is_create: Callback<'a, bool>,
    /// If `post_is_create` returns false, then this will be called to process any POST request.
    /// If it succeeds, return `Ok(true)`, `Ok(false)` otherwise. If it fails for any reason,
    /// return an Err with the status code you wish returned (e.g., a 500 status makes sense).
    /// Default is false. If you want the result of processing the POST to be a redirect, set
    /// `context.redirect` to true.
    pub process_post: Callback<'a, Result<bool, u16>>,
    /// This will be called on a POST request if `post_is_create` returns true. It should create
    /// the new resource and return the path as a valid URI part following the dispatcher prefix.
    /// That path will replace the previous one in the return value of `WebmachineRequest.request_path`
    /// for all subsequent resource function calls in the course of this request and will be set
    /// as the value of the Location header of the response. If it fails for any reason,
    /// return an Err with the status code you wish returned (e.g., a 500 status makes sense).
    /// Default will return an `Ok(WebmachineRequest.request_path)`. If you want the result of
    /// processing the POST to be a redirect, set `context.redirect` to true.
    pub create_path: Callback<'a, Result<String, u16>>,
    /// This will be called to process any PUT request. If it succeeds, return `Ok(true)`,
    /// `Ok(false)` otherwise. If it fails for any reason, return an Err with the status code
    /// you wish returned (e.g., a 500 status makes sense). Default is `Ok(true)`
    pub process_put: Callback<'a, Result<bool, u16>>,
    /// If this returns true, then it is assumed that multiple representations of the response are
    /// possible and a single one cannot be automatically chosen, so a 300 Multiple Choices will
    /// be sent instead of a 200. Default is false.
    pub multiple_choices: Callback<'a, bool>,
    /// If the resource expires, this should return the date/time it expires. Default is None.
    pub expires: Callback<'a, Option<DateTime<FixedOffset>>>,
}

fn true_fn(
    _: &mut Context,
    _: &Resource,
) -> Pin<Box<dyn Future<Output = bool> + Send>> {
    Box::pin(async { true })
}

fn false_fn(
    _: &mut Context,
    _: &Resource,
) -> Pin<Box<dyn Future<Output = bool> + Send>> {
    Box::pin(async { false })
}

fn none_fn<T>(
    _: &mut Context,
    _: &Resource,
) -> Pin<Box<dyn Future<Output = Option<T>> + Send>> {
    Box::pin(async { None })
}

impl<'a> Default for Resource<'a> {
    fn default() -> Resource<'a> {
        Resource {
            finalise_response: None,
            available: callback(&true_fn),
            known_methods: vec![
                "OPTIONS", "GET", "POST", "PUT", "DELETE", "HEAD", "TRACE", "CONNECT", "PATCH",
            ],
            uri_too_long: callback(&false_fn),
            allowed_methods: vec!["OPTIONS", "GET", "HEAD"],
            malformed_request: callback(&false_fn),
            not_authorized: callback(&none_fn),
            forbidden: callback(&false_fn),
            unsupported_content_headers: callback(&false_fn),
            acceptable_content_types: vec!["application/json"],
            valid_entity_length: callback(&true_fn),
            finish_request: callback(&|context, resource| {
                context.response.add_cors_headers(&resource.allowed_methods);
                Box::pin(async {})
            }),
            options: callback(&|_, resource| {
                let res = Response::cors_headers(&resource.allowed_methods);
                Box::pin(async {
                    Some(res)
                })
            }),
            produces: vec!["application/json"],
            languages_provided: Vec::new(),
            charsets_provided: Vec::new(),
            encodings_provided: vec!["identity"],
            variances: Vec::new(),
            resource_exists: callback(&true_fn),
            previously_existed: callback(&false_fn),
            moved_permanently: callback(&none_fn),
            moved_temporarily: callback(&none_fn),
            is_conflict: callback(&false_fn),
            allow_missing_post: callback(&false_fn),
            generate_etag: callback(&none_fn),
            last_modified: callback(&none_fn),
            delete_resource: callback(&|_, _| Box::pin(async { Ok(true) })),
            post_is_create: callback(&false_fn),
            process_post: callback(&|_, _| Box::pin(async { Ok(false) })),
            process_put: callback(&|_, _| Box::pin(async { Ok(true) })),
            multiple_choices: callback(&false_fn),
            create_path: callback(&|context, _| {
                let path = context.request.request_path.clone();
                Box::pin(async { Ok(path) })
            }),
            expires: callback(&none_fn),
            render_response: callback(&none_fn),
        }
    }
}