1
- use crate :: ChannelId ;
2
- use crate :: DomainError ;
1
+ use crate :: { ChannelId , DomainError } ;
2
+ use parse_display :: Display ;
3
3
use serde:: { Deserialize , Serialize } ;
4
4
5
5
pub const ANALYTICS_QUERY_LIMIT : u32 = 200 ;
@@ -21,8 +21,13 @@ pub struct AnalyticsResponse {
21
21
22
22
#[ cfg( feature = "postgres" ) ]
23
23
pub mod postgres {
24
- use super :: AnalyticsData ;
25
- use tokio_postgres:: Row ;
24
+ use super :: { AnalyticsData , OperatingSystem } ;
25
+ use bytes:: BytesMut ;
26
+ use std:: error:: Error ;
27
+ use tokio_postgres:: {
28
+ types:: { accepts, to_sql_checked, FromSql , IsNull , ToSql , Type } ,
29
+ Row ,
30
+ } ;
26
31
27
32
impl From < & Row > for AnalyticsData {
28
33
fn from ( row : & Row ) -> Self {
@@ -33,6 +38,34 @@ pub mod postgres {
33
38
}
34
39
}
35
40
}
41
+
42
+ impl < ' a > FromSql < ' a > for OperatingSystem {
43
+ fn from_sql ( ty : & Type , raw : & ' a [ u8 ] ) -> Result < Self , Box < dyn Error + Sync + Send > > {
44
+ let str_slice = <& str as FromSql >:: from_sql ( ty, raw) ?;
45
+ let os = match str_slice {
46
+ "Other" => OperatingSystem :: Other ,
47
+ "Linux" => OperatingSystem :: Linux ,
48
+ _ => OperatingSystem :: Whitelisted ( str_slice. to_string ( ) ) ,
49
+ } ;
50
+
51
+ Ok ( os)
52
+ }
53
+
54
+ accepts ! ( TEXT , VARCHAR ) ;
55
+ }
56
+
57
+ impl ToSql for OperatingSystem {
58
+ fn to_sql (
59
+ & self ,
60
+ ty : & Type ,
61
+ w : & mut BytesMut ,
62
+ ) -> Result < IsNull , Box < dyn Error + Sync + Send > > {
63
+ self . to_string ( ) . to_sql ( ty, w)
64
+ }
65
+
66
+ accepts ! ( TEXT , VARCHAR ) ;
67
+ to_sql_checked ! ( ) ;
68
+ }
36
69
}
37
70
38
71
#[ derive( Debug , Deserialize ) ]
@@ -49,6 +82,95 @@ pub struct AnalyticsQuery {
49
82
pub segment_by_channel : Option < String > ,
50
83
}
51
84
85
+ #[ derive( Debug , Clone , Serialize , Deserialize , PartialEq , Display , Hash , Eq ) ]
86
+ #[ serde( untagged, into = "String" , from = "String" ) ]
87
+ pub enum OperatingSystem {
88
+ Linux ,
89
+ #[ display( "{0}" ) ]
90
+ Whitelisted ( String ) ,
91
+ Other ,
92
+ }
93
+
94
+ impl Default for OperatingSystem {
95
+ fn default ( ) -> Self {
96
+ Self :: Other
97
+ }
98
+ }
99
+
100
+ impl From < String > for OperatingSystem {
101
+ fn from ( operating_system : String ) -> Self {
102
+ match operating_system. as_str ( ) {
103
+ "Linux" => OperatingSystem :: Linux ,
104
+ "Other" => OperatingSystem :: Other ,
105
+ _ => OperatingSystem :: Whitelisted ( operating_system) ,
106
+ }
107
+ }
108
+ }
109
+
110
+ impl From < OperatingSystem > for String {
111
+ fn from ( os : OperatingSystem ) -> String {
112
+ os. to_string ( )
113
+ }
114
+ }
115
+
116
+ impl OperatingSystem {
117
+ pub const LINUX_DISTROS : [ & ' static str ; 17 ] = [
118
+ "Arch" ,
119
+ "CentOS" ,
120
+ "Slackware" ,
121
+ "Fedora" ,
122
+ "Debian" ,
123
+ "Deepin" ,
124
+ "elementary OS" ,
125
+ "Gentoo" ,
126
+ "Mandriva" ,
127
+ "Manjaro" ,
128
+ "Mint" ,
129
+ "PCLinuxOS" ,
130
+ "Raspbian" ,
131
+ "Sabayon" ,
132
+ "SUSE" ,
133
+ "Ubuntu" ,
134
+ "RedHat" ,
135
+ ] ;
136
+ pub const WHITELISTED : [ & ' static str ; 18 ] = [
137
+ "Android" ,
138
+ "Android-x86" ,
139
+ "iOS" ,
140
+ "BlackBerry" ,
141
+ "Chromium OS" ,
142
+ "Fuchsia" ,
143
+ "Mac OS" ,
144
+ "Windows" ,
145
+ "Windows Phone" ,
146
+ "Windows Mobile" ,
147
+ "Linux" ,
148
+ "NetBSD" ,
149
+ "Nintendo" ,
150
+ "OpenBSD" ,
151
+ "PlayStation" ,
152
+ "Tizen" ,
153
+ "Symbian" ,
154
+ "KAIOS" ,
155
+ ] ;
156
+
157
+ pub fn map_os ( os_name : & str ) -> OperatingSystem {
158
+ if OperatingSystem :: LINUX_DISTROS
159
+ . iter ( )
160
+ . any ( |distro| os_name. eq ( * distro) )
161
+ {
162
+ OperatingSystem :: Linux
163
+ } else if OperatingSystem :: WHITELISTED
164
+ . iter ( )
165
+ . any ( |whitelisted| os_name. eq ( * whitelisted) )
166
+ {
167
+ OperatingSystem :: Whitelisted ( os_name. into ( ) )
168
+ } else {
169
+ OperatingSystem :: Other
170
+ }
171
+ }
172
+ }
173
+
52
174
impl AnalyticsQuery {
53
175
pub fn is_valid ( & self ) -> Result < ( ) , DomainError > {
54
176
let valid_event_types = [ "IMPRESSION" , "CLICK" ] ;
@@ -96,3 +218,99 @@ fn default_metric() -> String {
96
218
fn default_timeframe ( ) -> String {
97
219
"hour" . into ( )
98
220
}
221
+
222
+ #[ cfg( test) ]
223
+ mod test {
224
+ use super :: * ;
225
+ use crate :: postgres:: POSTGRES_POOL ;
226
+ use once_cell:: sync:: Lazy ;
227
+ use serde_json:: { from_value, to_value, Value } ;
228
+ use std:: collections:: HashMap ;
229
+
230
+ static TEST_CASES : Lazy < HashMap < String , ( OperatingSystem , Value ) > > = Lazy :: new ( || {
231
+ vec ! [
232
+ // Whitelisted - Android
233
+ (
234
+ OperatingSystem :: WHITELISTED [ 0 ] . to_string( ) ,
235
+ (
236
+ OperatingSystem :: Whitelisted ( "Android" . into( ) ) ,
237
+ Value :: String ( "Android" . into( ) ) ,
238
+ ) ,
239
+ ) ,
240
+ // Linux - Arch
241
+ (
242
+ OperatingSystem :: LINUX_DISTROS [ 0 ] . to_string( ) ,
243
+ ( OperatingSystem :: Linux , Value :: String ( "Linux" . into( ) ) ) ,
244
+ ) ,
245
+ // Other - OS xxxxx
246
+ (
247
+ "OS xxxxx" . into( ) ,
248
+ ( OperatingSystem :: Other , Value :: String ( "Other" . into( ) ) ) ,
249
+ ) ,
250
+ ]
251
+ . into_iter ( )
252
+ . collect ( )
253
+ } ) ;
254
+
255
+ #[ cfg( feature = "postgres" ) ]
256
+ #[ tokio:: test]
257
+ async fn os_to_from_sql ( ) {
258
+ let client = POSTGRES_POOL . get ( ) . await . unwrap ( ) ;
259
+ let sql_type = "VARCHAR" ;
260
+
261
+ for ( input, _) in TEST_CASES . iter ( ) {
262
+ let actual_os = OperatingSystem :: map_os ( input) ;
263
+
264
+ // from SQL
265
+ {
266
+ let row_os: OperatingSystem = client
267
+ . query_one ( & * format ! ( "SELECT '{}'::{}" , actual_os, sql_type) , & [ ] )
268
+ . await
269
+ . unwrap ( )
270
+ . get ( 0 ) ;
271
+
272
+ assert_eq ! (
273
+ & actual_os, & row_os,
274
+ "expected and actual FromSql differ for {}" ,
275
+ input
276
+ ) ;
277
+ }
278
+
279
+ // to SQL
280
+ {
281
+ let row_os: OperatingSystem = client
282
+ . query_one ( & * format ! ( "SELECT $1::{}" , sql_type) , & [ & actual_os] )
283
+ . await
284
+ . unwrap ( )
285
+ . get ( 0 ) ;
286
+ assert_eq ! (
287
+ & actual_os, & row_os,
288
+ "expected and actual ToSql differ for {}" ,
289
+ input
290
+ ) ;
291
+ }
292
+ }
293
+ }
294
+
295
+ #[ test]
296
+ fn test_operating_system ( ) {
297
+ for ( input, ( expect_os, expect_json) ) in TEST_CASES . iter ( ) {
298
+ let actual_os = OperatingSystem :: map_os ( input) ;
299
+
300
+ assert_eq ! (
301
+ expect_os, & actual_os,
302
+ "expected and actual differ for {}" ,
303
+ input
304
+ ) ;
305
+
306
+ let actual_json = to_value ( & actual_os) . expect ( "Should serialize it" ) ;
307
+
308
+ assert_eq ! ( expect_json, & actual_json) ;
309
+
310
+ let from_json: OperatingSystem =
311
+ from_value ( actual_json) . expect ( "Should deserialize it" ) ;
312
+
313
+ assert_eq ! ( expect_os, & from_json, "error processing {}" , input) ;
314
+ }
315
+ }
316
+ }
0 commit comments