1
+ extern crate reqwest;
2
+
1
3
use reqwest:: {
2
4
header:: { HeaderMap , HeaderValue } ,
3
5
Client , Error , Method , Response ,
4
6
} ;
5
7
6
- macro_rules! filter {
7
- ( $( $op: ident ) ,* ) => {
8
- $(
9
- pub fn $op( mut self , column: & str , param: & str ) -> Self {
10
- self . queries. push( ( column. to_string( ) ,
11
- format!( "{}.{}" , stringify!( $op) , param) ) ) ;
12
- self
13
- }
14
- ) *
15
- }
16
- }
17
-
18
8
#[ derive( Default ) ]
19
9
pub struct Builder {
20
10
method : Method ,
21
11
url : String ,
22
12
schema : Option < String > ,
23
- queries : Vec < ( String , String ) > ,
13
+ pub ( crate ) queries : Vec < ( String , String ) > ,
24
14
headers : HeaderMap ,
25
15
body : Option < String > ,
26
16
is_rpc : bool ,
@@ -30,21 +20,32 @@ pub struct Builder {
30
20
// TODO: Exact, planned, estimated count (HEAD verb)
31
21
// TODO: Response format
32
22
// TODO: Embedded resources
23
+ // TODO: Content type (csv, etc.)
33
24
impl Builder {
34
- pub fn new ( url : & str , schema : Option < String > ) -> Self {
35
- Builder {
25
+ pub fn new < S > ( url : S , schema : Option < String > ) -> Self
26
+ where
27
+ S : Into < String > ,
28
+ {
29
+ let mut builder = Builder {
36
30
method : Method :: GET ,
37
- url : url. to_string ( ) ,
31
+ url : url. into ( ) ,
38
32
schema,
39
33
headers : HeaderMap :: new ( ) ,
40
34
..Default :: default ( )
41
- }
35
+ } ;
36
+ builder
37
+ . headers
38
+ . insert ( "Accept" , HeaderValue :: from_static ( "application/json" ) ) ;
39
+ builder
42
40
}
43
41
44
- pub fn auth ( mut self , token : & str ) -> Self {
42
+ pub fn auth < S > ( mut self , token : S ) -> Self
43
+ where
44
+ S : Into < String > ,
45
+ {
45
46
self . headers . append (
46
47
"Authorization" ,
47
- HeaderValue :: from_str ( & format ! ( "Bearer {}" , token) ) . unwrap ( ) ,
48
+ HeaderValue :: from_str ( & format ! ( "Bearer {}" , token. into ( ) ) ) . unwrap ( ) ,
48
49
) ;
49
50
self
50
51
}
@@ -55,33 +56,42 @@ impl Builder {
55
56
// TODO: JSON columns
56
57
// TODO: Computed (virtual) columns
57
58
// TODO: Investigate character corner cases (Unicode, [ .,:()])
58
- pub fn select ( mut self , column : & str ) -> Self {
59
+ pub fn select < S > ( mut self , column : S ) -> Self
60
+ where
61
+ S : Into < String > ,
62
+ {
59
63
self . method = Method :: GET ;
60
- self . queries
61
- . push ( ( "select" . to_string ( ) , column. to_string ( ) ) ) ;
64
+ self . queries . push ( ( "select" . to_string ( ) , column. into ( ) ) ) ;
62
65
self
63
66
}
64
67
65
68
// TODO: desc/asc
66
69
// TODO: nullsfirst/nullslast
67
70
// TODO: Multiple columns
68
71
// TODO: Computed columns
69
- pub fn order ( mut self , column : & str ) -> Self {
70
- self . queries . push ( ( "order" . to_string ( ) , column. to_string ( ) ) ) ;
72
+ pub fn order < S > ( mut self , column : S ) -> Self
73
+ where
74
+ S : Into < String > ,
75
+ {
76
+ self . queries . push ( ( "order" . to_string ( ) , column. into ( ) ) ) ;
71
77
self
72
78
}
73
79
74
80
pub fn limit ( mut self , count : usize ) -> Self {
75
- self . headers . append (
76
- "Content-Range" ,
81
+ self . headers
82
+ . insert ( "Range-Unit" , HeaderValue :: from_static ( "items" ) ) ;
83
+ self . headers . insert (
84
+ "Range" ,
77
85
HeaderValue :: from_str ( & format ! ( "0-{}" , count - 1 ) ) . unwrap ( ) ,
78
86
) ;
79
87
self
80
88
}
81
89
82
90
pub fn range ( mut self , low : usize , high : usize ) -> Self {
83
- self . headers . append (
84
- "Content-Range" ,
91
+ self . headers
92
+ . insert ( "Range-Unit" , HeaderValue :: from_static ( "items" ) ) ;
93
+ self . headers . insert (
94
+ "Range" ,
85
95
HeaderValue :: from_str ( & format ! ( "{}-{}" , low, high) ) . unwrap ( ) ,
86
96
) ;
87
97
self
@@ -98,48 +108,56 @@ impl Builder {
98
108
// TODO: Write-only tables
99
109
// TODO: URL-encoded payload
100
110
// TODO: Allow specifying columns
101
- pub fn insert ( mut self , body : & str ) -> Self {
111
+ pub fn insert < S > ( mut self , body : S ) -> Self
112
+ where
113
+ S : Into < String > ,
114
+ {
102
115
self . method = Method :: POST ;
103
116
self . headers
104
- . append ( "Prefer" , HeaderValue :: from_static ( "return=representation" ) ) ;
105
- self . body = Some ( body. to_string ( ) ) ;
117
+ . insert ( "Prefer" , HeaderValue :: from_static ( "return=representation" ) ) ;
118
+ self . body = Some ( body. into ( ) ) ;
106
119
self
107
120
}
108
121
109
- pub fn insert_csv ( mut self , body : & str ) -> Self {
110
- self . headers
111
- . append ( "Content-Type" , HeaderValue :: from_static ( "text/csv" ) ) ;
112
- self . insert ( body)
113
- }
114
-
115
122
// TODO: Allow Prefer: resolution=ignore-duplicates
116
123
// TODO: on_conflict (make UPSERT work on UNIQUE columns)
117
- pub fn upsert ( mut self , body : & str ) -> Self {
124
+ pub fn upsert < S > ( mut self , body : S ) -> Self
125
+ where
126
+ S : Into < String > ,
127
+ {
118
128
self . method = Method :: POST ;
119
129
self . headers . append (
120
130
"Prefer" ,
121
131
// Maybe check if this works as intended...
122
132
HeaderValue :: from_static ( "return=representation; resolution=merge-duplicates" ) ,
123
133
) ;
124
- self . body = Some ( body. to_string ( ) ) ;
134
+ self . body = Some ( body. into ( ) ) ;
125
135
self
126
136
}
127
137
128
- pub fn single_upsert ( mut self , primary_column : & str , key : & str , body : & str ) -> Self {
138
+ pub fn single_upsert < S , T , U > ( mut self , primary_column : S , key : T , body : U ) -> Self
139
+ where
140
+ S : Into < String > ,
141
+ T : Into < String > ,
142
+ U : Into < String > ,
143
+ {
129
144
self . method = Method :: PUT ;
130
145
self . headers
131
146
. append ( "Prefer" , HeaderValue :: from_static ( "return=representation" ) ) ;
132
147
self . queries
133
- . push ( ( primary_column. to_string ( ) , format ! ( "eq.{}" , key) ) ) ;
134
- self . body = Some ( body. to_string ( ) ) ;
148
+ . push ( ( primary_column. into ( ) , format ! ( "eq.{}" , key. into ( ) ) ) ) ;
149
+ self . body = Some ( body. into ( ) ) ;
135
150
self
136
151
}
137
152
138
- pub fn update ( mut self , body : & str ) -> Self {
153
+ pub fn update < S > ( mut self , body : S ) -> Self
154
+ where
155
+ S : Into < String > ,
156
+ {
139
157
self . method = Method :: PATCH ;
140
158
self . headers
141
159
. append ( "Prefer" , HeaderValue :: from_static ( "return=representation" ) ) ;
142
- self . body = Some ( body. to_string ( ) ) ;
160
+ self . body = Some ( body. into ( ) ) ;
143
161
self
144
162
}
145
163
@@ -150,32 +168,20 @@ impl Builder {
150
168
self
151
169
}
152
170
153
- // It's unfortunate that `in` is a keyword, otherwise it'd belong in the
154
- // collection of filters below
155
- filter ! (
156
- eq, gt, gte, lt, lte, neq, like, ilike, is, fts, plfts, phfts, wfts, cs, cd, ov, sl, sr,
157
- nxr, nxl, adj, not
158
- ) ;
159
-
160
- pub fn in_ ( mut self , column : & str , param : & str ) -> Self {
161
- self . queries
162
- . push ( ( column. to_string ( ) , format ! ( "in.{}" , param) ) ) ;
163
- self
164
- }
165
-
166
- pub fn rpc ( mut self , params : & str ) -> Self {
171
+ pub fn rpc < S > ( mut self , params : S ) -> Self
172
+ where
173
+ S : Into < String > ,
174
+ {
167
175
self . method = Method :: POST ;
168
- self . body = Some ( params. to_string ( ) ) ;
176
+ self . body = Some ( params. into ( ) ) ;
169
177
self . is_rpc = true ;
170
178
self
171
179
}
172
180
173
181
pub async fn execute ( mut self ) -> Result < Response , Error > {
174
182
let mut req = Client :: new ( ) . request ( self . method . clone ( ) , & self . url ) ;
175
183
if let Some ( schema) = self . schema {
176
- // NOTE: Upstream bug: RPC only works with Accept-Profile
177
- // Will change when upstream is fixed
178
- let key = if !self . is_rpc || self . method == Method :: GET || self . method == Method :: HEAD {
184
+ let key = if self . method == Method :: GET || self . method == Method :: HEAD {
179
185
"Accept-Profile"
180
186
} else {
181
187
"Content-Profile"
@@ -188,8 +194,113 @@ impl Builder {
188
194
req = req. body ( body) ;
189
195
}
190
196
191
- let resp = req. send ( ) . await ?;
197
+ req. send ( ) . await
198
+ }
199
+ }
200
+
201
+ #[ cfg( test) ]
202
+ mod tests {
203
+ use super :: * ;
204
+
205
+ const TABLE_URL : & str = "http://localhost:3000/table" ;
206
+ const RPC_URL : & str = "http://localhost/rpc" ;
207
+
208
+ #[ test]
209
+ fn only_accept_json ( ) {
210
+ let builder = Builder :: new ( TABLE_URL , None ) ;
211
+ assert_eq ! (
212
+ builder. headers. get( "Accept" ) . unwrap( ) ,
213
+ HeaderValue :: from_static( "application/json" )
214
+ ) ;
215
+ }
216
+
217
+ #[ test]
218
+ fn auth_with_token ( ) {
219
+ let builder = Builder :: new ( TABLE_URL , None ) . auth ( "$Up3rS3crET" ) ;
220
+ assert_eq ! (
221
+ builder. headers. get( "Authorization" ) . unwrap( ) ,
222
+ HeaderValue :: from_static( "Bearer $Up3rS3crET" )
223
+ ) ;
224
+ }
225
+
226
+ #[ test]
227
+ fn select_assert_query ( ) {
228
+ let builder = Builder :: new ( TABLE_URL , None ) . select ( "some_table" ) ;
229
+ assert_eq ! ( builder. method, Method :: GET ) ;
230
+ assert_eq ! (
231
+ builder
232
+ . queries
233
+ . contains( & ( "select" . to_string( ) , "some_table" . to_string( ) ) ) ,
234
+ true
235
+ ) ;
236
+ }
237
+
238
+ #[ test]
239
+ fn order_assert_query ( ) {
240
+ let builder = Builder :: new ( TABLE_URL , None ) . order ( "id" ) ;
241
+ assert_eq ! (
242
+ builder
243
+ . queries
244
+ . contains( & ( "order" . to_string( ) , "id" . to_string( ) ) ) ,
245
+ true
246
+ ) ;
247
+ }
248
+
249
+ #[ test]
250
+ fn limit_assert_range_header ( ) {
251
+ let builder = Builder :: new ( TABLE_URL , None ) . limit ( 20 ) ;
252
+ assert_eq ! (
253
+ builder. headers. get( "Range" ) . unwrap( ) ,
254
+ HeaderValue :: from_static( "0-19" )
255
+ ) ;
256
+ }
257
+
258
+ #[ test]
259
+ fn range_assert_range_header ( ) {
260
+ let builder = Builder :: new ( TABLE_URL , None ) . range ( 10 , 20 ) ;
261
+ assert_eq ! (
262
+ builder. headers. get( "Range" ) . unwrap( ) ,
263
+ HeaderValue :: from_static( "10-20" )
264
+ ) ;
265
+ }
266
+
267
+ #[ test]
268
+ fn single_assert_accept_header ( ) {
269
+ let builder = Builder :: new ( TABLE_URL , None ) . single ( ) ;
270
+ assert_eq ! (
271
+ builder. headers. get( "Accept" ) . unwrap( ) ,
272
+ HeaderValue :: from_static( "application/vnd.pgrst.object+json" )
273
+ ) ;
274
+ }
275
+
276
+ #[ test]
277
+ fn upsert_assert_prefer_header ( ) {
278
+ let builder = Builder :: new ( TABLE_URL , None ) . upsert ( "ignored" ) ;
279
+ assert_eq ! (
280
+ builder. headers. get( "Prefer" ) . unwrap( ) ,
281
+ HeaderValue :: from_static( "return=representation; resolution=merge-duplicates" )
282
+ ) ;
283
+ }
284
+
285
+ #[ test]
286
+ fn single_upsert_assert_prefer_header ( ) {
287
+ let builder = Builder :: new ( TABLE_URL , None ) . single_upsert ( "ignored" , "ignored" , "ignored" ) ;
288
+ assert_eq ! (
289
+ builder. headers. get( "Prefer" ) . unwrap( ) ,
290
+ HeaderValue :: from_static( "return=representation" )
291
+ ) ;
292
+ }
293
+
294
+ #[ test]
295
+ fn not_rpc_should_not_have_flag ( ) {
296
+ let builder = Builder :: new ( TABLE_URL , None ) . select ( "ignored" ) ;
297
+ assert_eq ! ( builder. is_rpc, false ) ;
298
+ }
192
299
193
- Ok ( resp)
300
+ #[ test]
301
+ fn rpc_should_have_body_and_flag ( ) {
302
+ let builder = Builder :: new ( RPC_URL , None ) . rpc ( "{\" a\" : 1, \" b\" : 2}" ) ;
303
+ assert_eq ! ( builder. body. unwrap( ) , "{\" a\" : 1, \" b\" : 2}" ) ;
304
+ assert_eq ! ( builder. is_rpc, true ) ;
194
305
}
195
306
}
0 commit comments