11use std:: convert:: TryFrom ;
22
3+ use percent_encoding:: { utf8_percent_encode, AsciiSet , CONTROLS } ;
34use rocket:: http:: uri:: Origin ;
5+ use rocket:: http:: { HeaderMap , RawStr } ;
46use rocket:: response:: status:: Created ;
57use rocket:: State ;
68use serde:: Deserialize ;
79
810use crate :: application:: Config ;
911use crate :: errors:: ApiError ;
10- use crate :: storage:: { Changeset , DateTime , Snippet , Storage } ;
11- use crate :: web:: { BearerAuth , Input , NegotiatedContentType , Output } ;
12+ use crate :: storage:: { Changeset , DateTime , Direction , ListSnippetsQuery , Snippet , Storage } ;
13+ use crate :: web:: {
14+ BearerAuth , Input , NegotiatedContentType , Output , PaginationLimit , WithHttpHeaders ,
15+ } ;
1216
1317fn create_snippet_impl (
1418 storage : & dyn Storage ,
@@ -21,6 +25,68 @@ fn create_snippet_impl(
2125 Ok ( Created ( location, Some ( Output ( new_snippet) ) ) )
2226}
2327
28+ fn create_link_header (
29+ origin : & Origin ,
30+ next_marker : Option < String > ,
31+ prev_marker : Option < String > ,
32+ prev_needed : bool ,
33+ ) -> String {
34+ const QUERY_ENCODE_SET : & AsciiSet = & CONTROLS
35+ . add ( b' ' )
36+ . add ( b'"' )
37+ . add ( b'#' )
38+ . add ( b'<' )
39+ . add ( b'>' )
40+ . add ( b'&' ) ;
41+
42+ let query_wo_marker = origin. query ( ) . map ( |q| {
43+ q. split ( '&' )
44+ . filter_map ( |v| {
45+ let v = RawStr :: from_str ( v) . percent_decode_lossy ( ) ;
46+ if !v. starts_with ( "marker=" ) {
47+ Some ( utf8_percent_encode ( & v, QUERY_ENCODE_SET ) . to_string ( ) )
48+ } else {
49+ None
50+ }
51+ } )
52+ . collect :: < Vec < _ > > ( )
53+ . join ( "&" )
54+ } ) ;
55+ let query_first = query_wo_marker. clone ( ) ;
56+ let mut query_next = next_marker. map ( |marker| format ! ( "marker={}" , marker) ) ;
57+ let mut query_prev = prev_marker. map ( |marker| format ! ( "marker={}" , marker) ) ;
58+
59+ // If a request URL does contain query parameters (other than marker), we
60+ // must reuse them together with next/prev markers.
61+ if let Some ( query_wo_marker) = query_wo_marker {
62+ query_next = query_next. map ( |query| format ! ( "{}&{}" , query_wo_marker, query) ) ;
63+ query_prev = query_prev. map ( |query| format ! ( "{}&{}" , query_wo_marker, query) ) ;
64+ }
65+
66+ // If a previous page is the first page, we don't have 'prev_marker' set
67+ // yet the link must be generated. If that's the case, reuse query
68+ // parameters we are using to generate a link to the first page.
69+ if query_prev. is_none ( ) && prev_needed {
70+ query_prev = query_first. clone ( ) ;
71+ }
72+
73+ vec ! [
74+ // (query string, rel, is_required)
75+ ( & query_first, "first" , true ) ,
76+ ( & query_next, "next" , query_next. is_some( ) ) ,
77+ ( & query_prev, "prev" , prev_needed) ,
78+ ]
79+ . into_iter ( )
80+ . filter ( |item| item. 2 )
81+ . map ( |item| match item. 0 {
82+ Some ( query) => ( vec ! [ origin. path( ) , query. as_str( ) ] . join ( "?" ) , item. 1 ) ,
83+ None => ( origin. path ( ) . to_owned ( ) , item. 1 ) ,
84+ } )
85+ . map ( |item| format ! ( "<{}>; rel=\" {}\" " , item. 0 , item. 1 ) )
86+ . collect :: < Vec < _ > > ( )
87+ . join ( ", " )
88+ }
89+
2490#[ derive( Deserialize ) ]
2591#[ serde( deny_unknown_fields) ]
2692pub struct NewSnippet {
@@ -77,6 +143,68 @@ pub fn create_snippet(
77143 create_snippet_impl ( & * * storage, & snippet, origin. path ( ) )
78144}
79145
146+ fn split_marker ( mut snippets : Vec < Snippet > , limit : usize ) -> ( Option < String > , Vec < Snippet > ) {
147+ if snippets. len ( ) > limit {
148+ snippets. truncate ( limit) ;
149+ ( snippets. last ( ) . map ( |m| m. id . to_owned ( ) ) , snippets)
150+ } else {
151+ ( None , snippets)
152+ }
153+ }
154+
155+ #[ allow( clippy:: too_many_arguments) ]
156+ #[ get( "/snippets?<title>&<syntax>&<tag>&<marker>&<limit>" ) ]
157+ pub fn list_snippets < ' o , ' h > (
158+ storage : State < Box < dyn Storage > > ,
159+ origin : & ' o Origin ,
160+ title : Option < String > ,
161+ syntax : Option < String > ,
162+ tag : Option < String > ,
163+ limit : Option < Result < PaginationLimit , ApiError > > ,
164+ marker : Option < String > ,
165+ _content_type : NegotiatedContentType ,
166+ _user : BearerAuth ,
167+ ) -> Result < WithHttpHeaders < ' h , Output < Vec < Snippet > > > , ApiError > {
168+ let mut criteria = ListSnippetsQuery {
169+ title,
170+ syntax,
171+ tags : tag. map ( |v| vec ! [ v] ) ,
172+ ..Default :: default ( )
173+ } ;
174+
175+ // Fetch one more record in order to detect if there's a next page, and
176+ // generate appropriate Link entry accordingly.
177+ let limit = limit
178+ . unwrap_or_else ( || Ok ( <PaginationLimit as Default >:: default ( ) ) ) ?
179+ . 0 ;
180+ criteria. pagination . limit = limit + 1 ;
181+ criteria. pagination . marker = marker;
182+
183+ let snippets = storage. list ( criteria. clone ( ) ) ?;
184+ let mut prev_needed = false ;
185+ let ( next_marker, snippets) = split_marker ( snippets, limit) ;
186+ let prev_marker = if criteria. pagination . marker . is_some ( ) && !snippets. is_empty ( ) {
187+ // In order to generate Link entry for previous page we have no choice
188+ // but to issue the query one more time into opposite direction.
189+ criteria. pagination . direction = Direction :: Asc ;
190+ criteria. pagination . marker = Some ( snippets[ 0 ] . id . to_owned ( ) ) ;
191+ let prev_snippets = storage. list ( criteria) ?;
192+ prev_needed = !prev_snippets. is_empty ( ) ;
193+
194+ prev_snippets. get ( limit) . map ( |m| m. id . to_owned ( ) )
195+ } else {
196+ None
197+ } ;
198+
199+ let mut headers_map = HeaderMap :: new ( ) ;
200+ headers_map. add_raw (
201+ "Link" ,
202+ create_link_header ( origin, next_marker, prev_marker, prev_needed) ,
203+ ) ;
204+
205+ Ok ( WithHttpHeaders ( headers_map, Some ( Output ( snippets) ) ) )
206+ }
207+
80208#[ derive( Deserialize ) ]
81209#[ serde( deny_unknown_fields) ]
82210pub struct ImportSnippet {
0 commit comments