1
1
//! This program demonstrates implementing WriteAsIon using Ion 1.1's e-expressions for a more
2
- //! compact encoding. It uses raw-level writer APIs that end users are unlikely to leverage.
3
- //! Ion 1.1 is not yet finalized; the encoding produced by this example and the APIs it uses
4
- //! are very likely to change.
5
-
2
+ //! compact encoding.
6
3
use ion_rs:: * ;
7
4
8
5
fn main ( ) -> IonResult < ( ) > {
9
6
#[ cfg( not( feature = "experimental" ) ) ]
10
- panic ! ( "This example requires the 'experimental' feature to work." ) ;
7
+ {
8
+ eprintln ! ( "This example requires the 'experimental' feature to work. Rebuild it with the flag `--features experimental`." ) ;
9
+ }
11
10
12
11
#[ cfg( feature = "experimental" ) ]
13
- example:: write_log_events ( )
12
+ example:: write_log_events ( ) ?;
13
+
14
+ Ok ( ( ) )
14
15
}
15
16
16
17
#[ cfg( feature = "experimental" ) ]
17
18
mod example {
18
19
use chrono:: { DateTime , FixedOffset } ;
20
+ use ion_rs:: v1_1:: Macro ;
19
21
use ion_rs:: * ;
20
22
use rand:: rngs:: StdRng ;
21
23
use rand:: seq:: SliceRandom ;
@@ -55,29 +57,49 @@ mod example {
55
57
ion_1_1_file. path( ) . to_string_lossy( ) ,
56
58
) ;
57
59
58
- // Encode the log events as Ion 1.0 data
59
- let buf_writer = BufWriter :: new ( ion_1_0_file. as_file ( ) ) ;
60
- let mut ion_writer = v1_0:: RawBinaryWriter :: new ( buf_writer) ?;
61
- for event in & events {
62
- ion_writer. write ( SerializeWithoutMacros ( event) ) ?;
63
- }
64
- ion_writer. flush ( ) ?;
65
- drop ( ion_writer) ;
66
-
67
- // Encode the log events as Ion 1.1 data
68
- let buf_writer = BufWriter :: new ( ion_1_1_file. as_file ( ) ) ;
69
- let mut ion_writer = v1_1:: RawBinaryWriter :: new ( buf_writer) ?;
70
- for event in & events {
71
- ion_writer. write ( SerializeWithMacros ( event) ) ?;
72
- }
73
- ion_writer. flush ( ) ?;
74
- drop ( ion_writer) ;
60
+ // === Encode the log events as Ion 1.0 data ===
61
+ // First, we initialize a writer...
62
+ let mut ion_writer = Writer :: new ( v1_0:: Binary , BufWriter :: new ( ion_1_0_file. as_file ( ) ) ) ?;
63
+ // ...then we encode all of the events...
64
+ ion_writer. write_all ( events. iter ( ) . map ( |e| SerializeWithoutMacros ( e) ) ) ?;
65
+ // ...finally, we close the writer, consuming it.
66
+ ion_writer. close ( ) ?;
67
+
68
+ // === Encode the log events as Ion 1.1 data ===
69
+ // First, we initialize a writer...
70
+ let mut ion_writer = Writer :: new ( v1_1:: Binary , BufWriter :: new ( ion_1_1_file. as_file ( ) ) ) ?;
71
+
72
+ // ...then we define some macros that we intend to use to encode our log data...
73
+ let macros = LogEventMacros {
74
+ thread_name : ion_writer. compile_macro (
75
+ // This macro includes the prefix common to all thread names, allowing the writer to only encode
76
+ // the suffix of each.
77
+ & format ! (
78
+ r#"
79
+ (macro thread_name (suffix) (.make_string {THREAD_NAME_PREFIX} (%suffix) ))
80
+ "#
81
+ ) ,
82
+ ) ?,
83
+ log_statements : log_statements
84
+ . iter ( )
85
+ // As you'll see later, each LogStatement has an associated macro definition in text.
86
+ . map ( |log_statement| ion_writer. compile_macro ( & log_statement. macro_source ) )
87
+ . collect :: < IonResult < Vec < Macro > > > ( ) ?,
88
+ } ;
89
+
90
+ // ...then we encode all of the events using the macros we just defined...
91
+ ion_writer. write_all ( events. iter ( ) . map ( |e| SerializeWithMacros ( e, & macros) ) ) ?;
92
+ // ...finally, we close the writer, consuming it.
93
+ ion_writer. close ( ) ?;
94
+
95
+ // === Encoded file size comparison ===
75
96
76
97
let size_in_1_0 = ion_1_0_file
77
98
. as_file ( )
78
99
. metadata ( )
79
100
. expect ( "failed to read Ion 1.0 file length" )
80
101
. len ( ) ;
102
+
81
103
let size_in_1_1 = ion_1_1_file
82
104
. as_file ( )
83
105
. metadata ( )
@@ -100,18 +122,13 @@ mod example {
100
122
101
123
// A log statement in the fictional codebase
102
124
#[ derive( Debug ) ]
103
- // This struct has several fields that get populated but which are not (yet) used: `logger_name`
104
- // `log_level`, and `format`. Currently, the encoded output for Ion 1.0 writes these as symbol
105
- // IDs and Ion 1.1 refers to them as part of a macro. In both cases, however, the encoding
106
- // context is not written out in the resulting Ion stream.
107
- // TODO: Include the symbol/macro table definitions in the resulting output stream.
108
- #[ allow( dead_code) ]
109
125
struct LogStatement {
110
126
index : usize ,
111
127
logger_name : String ,
112
128
log_level : String ,
113
129
format : String ,
114
130
parameter_types : Vec < ParameterType > ,
131
+ macro_source : String ,
115
132
}
116
133
117
134
impl LogStatement {
@@ -122,12 +139,37 @@ mod example {
122
139
format : impl Into < String > ,
123
140
parameter_types : impl Into < Vec < ParameterType > > ,
124
141
) -> Self {
142
+ let format = format. into ( ) ;
143
+ let macro_source = format ! (
144
+ // Note that there are two levels of interpolation in this string literal.
145
+ // The `format!` macro will process it first, replacing variable names in single
146
+ // braces (like `{class_name}`) with the corresponding text, and replacing
147
+ // double braces (like the `{{...}}` surrounding the template) with single braces.
148
+ // The resulting string is our macro source, which will be compiled by the Writer.
149
+ r#"
150
+ (macro
151
+ ls{index:<42} // Name
152
+ (timestamp thread_id thread_name parameters) // Signature
153
+ {{ // Template
154
+ loggerName: "{class_name}",
155
+ logLevel: {log_level},
156
+ format: "{format}",
157
+ timestamp: (%timestamp),
158
+ thread_id: (%thread_id),
159
+ thread_name: (%thread_name),
160
+ parameters: (%parameters)
161
+ }}
162
+ )
163
+ "#
164
+ ) ;
165
+ println ! ( "{macro_source}" ) ;
125
166
Self {
126
167
index,
127
168
logger_name : format ! ( "{PACKAGE_NAME}.{class_name}" ) ,
128
169
log_level : log_level. to_string ( ) ,
129
170
format : format. into ( ) ,
130
171
parameter_types : parameter_types. into ( ) ,
172
+ macro_source,
131
173
}
132
174
}
133
175
}
@@ -160,6 +202,13 @@ mod example {
160
202
161
203
// ===== Serialization logic for the above types =====
162
204
205
+ // A simple container to store macros related to serializing LogEvent
206
+ struct LogEventMacros {
207
+ thread_name : Macro ,
208
+ log_statements : Vec < Macro > ,
209
+ }
210
+
211
+ // Defines how a `Parameter` is serialized as Ion
163
212
impl WriteAsIon for Parameter {
164
213
fn write_as_ion < V : ValueWriter > ( & self , writer : V ) -> IonResult < ( ) > {
165
214
match self {
@@ -173,71 +222,79 @@ mod example {
173
222
// the future, as types will be able to define both a macro-ized serialization and a no-macros
174
223
// serialization, allowing the writer to choose whichever is more appropriate.
175
224
struct SerializeWithoutMacros < ' a , ' b > ( & ' a LogEvent < ' b > ) ;
176
- struct SerializeWithMacros < ' a , ' b > ( & ' a LogEvent < ' b > ) ;
225
+ struct SerializeWithMacros < ' a , ' b > ( & ' a LogEvent < ' b > , & ' a LogEventMacros ) ;
177
226
178
227
// When serializing without macros (usually in Ion 1.0), we write out a struct with each
179
- // field name/value pair. In the case of recurring strings, we take the liberty of writing
180
- // out symbol IDs instead of the full text; this silent type coercion from string to symbol
181
- // is technically data loss, but results in a much more compact encoding.
228
+ // field name/value pair.
182
229
impl WriteAsIon for SerializeWithoutMacros < ' _ , ' _ > {
183
230
fn write_as_ion < V : ValueWriter > ( & self , writer : V ) -> IonResult < ( ) > {
184
231
let event = self . 0 ;
185
232
let mut struct_ = writer. struct_writer ( ) ?;
186
233
struct_
187
234
// v--- Each field name is a symbol ID
188
- . write ( 10 , event. timestamp ) ?
189
- . write ( 11 , event. thread_id ) ?
190
- . write ( 12 , & event. thread_name ) ?
191
- // v--- The fixed strings from the log statement are also SIDs
192
- . write ( 13 , RawSymbolRef :: SymbolId ( 17 ) ) ? // logger name
193
- . write ( 14 , RawSymbolRef :: SymbolId ( 18 ) ) ? // log level
194
- . write ( 15 , RawSymbolRef :: SymbolId ( 19 ) ) ? // format
195
- . write ( 16 , & event. parameters ) ?;
235
+ . write ( "timestamp" , event. timestamp ) ?
236
+ . write ( "threadId" , event. thread_id ) ?
237
+ . write ( "threadName" , & event. thread_name ) ?
238
+ . write (
239
+ "loggerName" ,
240
+ SymbolRef :: with_text ( & event. statement . logger_name ) ,
241
+ ) ?
242
+ . write ( "logLevel" , SymbolRef :: with_text ( & event. statement . log_level ) ) ?
243
+ . write ( "format" , SymbolRef :: with_text ( & event. statement . format ) ) ?
244
+ . write ( "parameters" , & event. parameters ) ?;
196
245
struct_. close ( )
197
246
}
198
247
}
199
248
249
+ impl WriteAsIon for SerializeWithMacros < ' _ , ' _ > {
250
+ fn write_as_ion < V : ValueWriter > ( & self , writer : V ) -> IonResult < ( ) > {
251
+ let SerializeWithMacros ( event, macros) = * self ;
252
+
253
+ // Create an e-expression writer to invoke the macro corresponding to this log statement.
254
+ let mut eexp = writer. eexp_writer ( & macros. log_statements [ event. statement . index ] ) ?;
255
+ eexp. write ( event. timestamp ) ?
256
+ . write ( event. thread_id ) ?
257
+ // Wrap the thread name in the `ThreadName` wrapper to change its serialization.
258
+ . write ( ThreadName ( & event. thread_name , & macros. thread_name ) ) ?
259
+ . write ( & event. parameters ) ?;
260
+ eexp. close ( )
261
+ }
262
+ }
263
+
200
264
// When leveraging macros, the thread name's recurring prefix can be elided from the output.
201
- // This wrapper type is used by the `SerializeWithMacros` type to change to serialization
265
+ // This wrapper type is used by the `SerializeWithMacros` type to change the serialization
202
266
// behavior for the thread name.
203
- struct ThreadName < ' a > ( & ' a str ) ;
267
+ struct ThreadName < ' a > ( & ' a str , & ' a Macro ) ;
204
268
205
269
impl WriteAsIon for ThreadName < ' _ > {
206
270
fn write_as_ion < V : ValueWriter > ( & self , writer : V ) -> IonResult < ( ) > {
271
+ let thread_name_macro = self . 1 ;
207
272
// ID 12 chosen arbitrarily, but aligns with Ion 1.0 encoding above
208
- let mut eexp = writer. eexp_writer ( 12 ) ?;
273
+ let mut eexp = writer. eexp_writer ( thread_name_macro ) ?;
209
274
eexp
210
275
// Ignore the part of the thread name that starts with the recurring prefix.
211
276
. write ( & self . 0 [ THREAD_NAME_PREFIX . len ( ) ..] ) ?;
212
277
eexp. close ( )
213
278
}
214
279
}
215
280
216
- impl WriteAsIon for SerializeWithMacros < ' _ , ' _ > {
217
- fn write_as_ion < V : ValueWriter > ( & self , writer : V ) -> IonResult < ( ) > {
218
- let event = self . 0 ;
219
- let mut eexp = writer. eexp_writer ( event. statement . index ) ?;
220
- eexp. write ( event. timestamp ) ?
221
- . write ( event. thread_id ) ?
222
- // Wrap the thread name in the `ThreadName` wrapper to change its serialization.
223
- . write ( ThreadName ( & event. thread_name ) ) ?
224
- . write ( & event. parameters ) ?;
225
- eexp. close ( )
226
- }
227
- }
228
-
229
281
// ===== Random generation of sample data =====
230
282
283
+ // Any time we need an integer, we'll generate a random one between 0 and 5,000.
231
284
const INT_PARAMETER_RANGE : Range < i64 > = 0 ..5_000 ;
232
285
fn generate_int_parameter ( rng : & mut StdRng ) -> Parameter {
233
286
Parameter :: Int ( rng. gen_range ( INT_PARAMETER_RANGE ) )
234
287
}
235
288
289
+ // Any time we need a string, we'll select one at random from this collection of plural nouns.
236
290
fn generate_string_parameter ( rng : & mut StdRng ) -> Parameter {
237
291
const WORDS : & [ & str ] = & [ "users" , "transactions" , "accounts" , "customers" , "waffles" ] ;
238
292
Parameter :: String ( WORDS . choose ( rng) . unwrap ( ) . to_string ( ) )
239
293
}
240
294
295
+ // These are the log statements that our fictional program contains.
296
+ // Each log event will be associated with a randomly selected log statement and its parameters
297
+ // will be populated using the methods above.
241
298
fn log_statements ( ) -> Vec < LogStatement > {
242
299
use ParameterType :: * ;
243
300
vec ! [
0 commit comments