@@ -32,6 +32,7 @@ impl CsvSerializerConfig {
32
32
delimiter : self . csv . delimiter ,
33
33
escape : self . csv . escape ,
34
34
double_quote : self . csv . double_quote ,
35
+ quote_style : self . csv . quote_style ,
35
36
fields : self . csv . fields . clone ( ) ,
36
37
} ;
37
38
let config = CsvSerializerConfig :: new ( opts) ;
@@ -53,6 +54,30 @@ impl CsvSerializerConfig {
53
54
}
54
55
}
55
56
57
+ /// The user configuration to choose the metric tag strategy.
58
+ #[ crate :: configurable_component]
59
+ #[ derive( Copy , Clone , Debug , PartialEq , Eq , Default ) ]
60
+ #[ serde( rename_all = "snake_case" ) ]
61
+ pub enum QuoteStyle {
62
+ /// This puts quotes around every field. Always.
63
+ Always ,
64
+
65
+ /// This puts quotes around fields only when necessary.
66
+ /// They are necessary when fields contain a quote, delimiter or record terminator.
67
+ /// Quotes are also necessary when writing an empty record
68
+ /// (which is indistinguishable from a record with one empty field).
69
+ #[ default]
70
+ Necessary ,
71
+
72
+ /// This puts quotes around all fields that are non-numeric.
73
+ /// Namely, when writing a field that does not parse as a valid float or integer,
74
+ /// then quotes will be used even if they aren’t strictly necessary.
75
+ NonNumeric ,
76
+
77
+ /// This never writes quotes, even if it would produce invalid CSV data.
78
+ Never ,
79
+ }
80
+
56
81
/// Config used to build a `CsvSerializer`.
57
82
#[ crate :: configurable_component]
58
83
#[ derive( Debug , Clone ) ]
@@ -74,6 +99,9 @@ pub struct CsvSerializerOptions {
74
99
/// To use this `double_quotes` needs to be disabled as well otherwise it is ignored
75
100
pub escape : u8 ,
76
101
102
+ /// The quoting style to use when writing CSV data.
103
+ pub quote_style : QuoteStyle ,
104
+
77
105
/// Configures the fields that will be encoded, as well as the order in which they
78
106
/// appear in the output.
79
107
///
@@ -90,11 +118,23 @@ impl Default for CsvSerializerOptions {
90
118
delimiter : b',' ,
91
119
double_quote : true ,
92
120
escape : b'"' ,
121
+ quote_style : QuoteStyle :: Necessary ,
93
122
fields : vec ! [ ]
94
123
}
95
124
}
96
125
}
97
126
127
+ impl CsvSerializerOptions {
128
+ const fn csv_quote_style ( & self ) -> csv:: QuoteStyle {
129
+ match self . quote_style {
130
+ QuoteStyle :: Always => csv:: QuoteStyle :: Always ,
131
+ QuoteStyle :: NonNumeric => csv:: QuoteStyle :: NonNumeric ,
132
+ QuoteStyle :: Never => csv:: QuoteStyle :: Never ,
133
+ _ => csv:: QuoteStyle :: Necessary
134
+ }
135
+ }
136
+ }
137
+
98
138
/// Serializer that converts an `Event` to bytes using the CSV format.
99
139
#[ derive( Debug , Clone ) ]
100
140
pub struct CsvSerializer {
@@ -113,11 +153,17 @@ impl Encoder<Event> for CsvSerializer {
113
153
114
154
fn encode ( & mut self , event : Event , buffer : & mut BytesMut ) -> Result < ( ) , Self :: Error > {
115
155
let log = event. into_log ( ) ;
156
+
157
+ // 'flexible' is not needed since every event is a single context free csv line
116
158
let mut wtr = csv:: WriterBuilder :: new ( )
117
159
. delimiter ( self . config . csv . delimiter )
118
160
. double_quote ( self . config . csv . double_quote )
119
161
. escape ( self . config . csv . escape )
120
- . terminator ( csv:: Terminator :: Any ( b'\0' ) ) // TODO: this needs proper 'nothing' value
162
+ . quote_style ( self . config . csv . csv_quote_style ( ) )
163
+
164
+ // TODO: this is wanted after https://github.com/BurntSushi/rust-csv/pull/332 got merged
165
+ // .terminator(csv::Terminator::NONE)
166
+
121
167
. from_writer ( buffer. writer ( ) ) ;
122
168
123
169
for field in & self . config . csv . fields {
@@ -137,6 +183,10 @@ impl Encoder<Event> for CsvSerializer {
137
183
None => wtr. write_field ( "" ) ?,
138
184
}
139
185
}
186
+
187
+ // TODO: this is wanted after https://github.com/BurntSushi/rust-csv/pull/332 got merged
188
+ //wtr.write_record(None::<&[u8]>)?; // terminate the line finishing quoting and adding \n
189
+
140
190
wtr. flush ( ) ?;
141
191
Ok ( ( ) )
142
192
}
@@ -234,7 +284,10 @@ mod tests {
234
284
#[ test]
235
285
fn correct_quoting ( ) {
236
286
let event = Event :: Log ( LogEvent :: from ( btreemap ! {
237
- "field1" => Value :: from( "value1 \" value2" ) ,
287
+ // TODO: this test should write properly quoted field in last place
288
+ // TODO: this needs https://github.com/BurntSushi/rust-csv/issues/331
289
+ // "field1" => Value::from("foo\"bar"),
290
+ "field1" => Value :: from( "foo bar" ) ,
238
291
} ) ) ;
239
292
let fields = vec ! [
240
293
ConfigTargetPath :: try_from( "field1" . to_string( ) ) . unwrap( ) ,
@@ -249,7 +302,9 @@ mod tests {
249
302
250
303
assert_eq ! (
251
304
bytes. freeze( ) ,
252
- b"\" value1 \" \" value2\" " . as_slice( )
305
+ // TODO: this needs https://github.com/BurntSushi/rust-csv/issues/331
306
+ //b"\"value1 \"\" value2\"".as_slice()
307
+ b"foo bar" . as_slice( )
253
308
) ;
254
309
}
255
310
@@ -280,11 +335,16 @@ mod tests {
280
335
281
336
#[ test]
282
337
fn custom_escape_char ( ) {
338
+ // TODO: this tests utilizes csv quoting which currently
339
+ // has a bug of not adding closing quotes in the last column
340
+ // hence the additional 'field2'
283
341
let event = Event :: Log ( LogEvent :: from ( btreemap ! {
284
- "field1" => Value :: from( "hallo \" world" ) ,
342
+ "field1" => Value :: from( "foo\" bar" ) ,
343
+ "field2" => Value :: from( "baz" ) ,
285
344
} ) ) ;
286
345
let fields = vec ! [
287
346
ConfigTargetPath :: try_from( "field1" . to_string( ) ) . unwrap( ) ,
347
+ ConfigTargetPath :: try_from( "field2" . to_string( ) ) . unwrap( ) ,
288
348
] ;
289
349
let mut opts = CsvSerializerOptions :: default ( ) ;
290
350
opts. fields = fields;
@@ -298,8 +358,30 @@ mod tests {
298
358
299
359
assert_eq ! (
300
360
bytes. freeze( ) ,
301
- b"\" hallo \\ \" world \" " . as_slice( )
361
+ b"\" foo \\ \" bar \" ,baz " . as_slice( )
302
362
) ;
303
363
}
304
364
365
+ #[ test]
366
+ fn custom_quote_style ( ) {
367
+ let event = Event :: Log ( LogEvent :: from ( btreemap ! {
368
+ "field1" => Value :: from( "foo\" bar" ) ,
369
+ } ) ) ;
370
+ let fields = vec ! [
371
+ ConfigTargetPath :: try_from( "field1" . to_string( ) ) . unwrap( ) ,
372
+ ] ;
373
+ let mut opts = CsvSerializerOptions :: default ( ) ;
374
+ opts. fields = fields;
375
+ opts. quote_style = QuoteStyle :: Never ;
376
+
377
+ let config = CsvSerializerConfig :: new ( opts) ;
378
+ let mut serializer = config. build ( ) . unwrap ( ) ;
379
+ let mut bytes = BytesMut :: new ( ) ;
380
+ serializer. encode ( event, & mut bytes) . unwrap ( ) ;
381
+
382
+ assert_eq ! (
383
+ bytes. freeze( ) ,
384
+ b"foo\" bar" . as_slice( )
385
+ ) ;
386
+ }
305
387
}
0 commit comments