1use heck::{ToPascalCase, ToShoutySnakeCase};
2use indoc::formatdoc;
3use rustc_hash::FxHashSet;
4use rustyfix_dictionary::{self as dict, TagU32};
5use smartstring::alias::String as SmartString;
6
7const RUSTYFIX_VERSION: &str = env!("CARGO_PKG_VERSION");
8
9pub fn generated_code_notice() -> SmartString {
20 formatdoc!(
21 r#"
22 // Generated automatically by RustyFix {} on {}.
23 //
24 // DO NOT MODIFY MANUALLY.
25 // DO NOT COMMIT TO VERSION CONTROL.
26 // ALL CHANGES WILL BE OVERWRITTEN."#,
27 RUSTYFIX_VERSION,
28 chrono::Utc::now().to_rfc2822(),
29 )
30 .into()
31}
32
33pub fn codegen_field_type_enum(field: dict::Field, settings: &Settings) -> String {
36 let derives = settings.derives_for_allowed_values.join(", ");
37 let attributes_str = if settings.attributes_for_allowed_values.is_empty() {
38 String::new()
39 } else {
40 format!("{}\n", settings.attributes_for_allowed_values.join("\n"))
41 };
42 let mut variant_identifiers = FxHashSet::default();
43 let variants = field
44 .enums()
45 .unwrap()
46 .map(|v| codegen_field_type_enum_variant(v, settings, &mut variant_identifiers))
47 .collect::<Vec<_>>()
48 .join("\n");
49 formatdoc!(
50 r#"
51 /// Field type variants for [`{field_name}`].
52 #[derive({derives})]
53 #[allow(clippy::enum_variant_names)]
54 {attributes}pub enum {identifier} {{
55 {variants}
56 }}"#,
57 field_name = field.name().to_pascal_case(),
58 derives = derives,
59 attributes = attributes_str,
60 identifier = field.name().to_pascal_case(),
61 variants = variants,
62 )
63}
64
65fn codegen_field_type_enum_variant(
66 allowed_value: dict::FieldEnum,
67 settings: &Settings,
68 variant_identifiers: &mut FxHashSet<String>,
69) -> SmartString {
70 let mut identifier = allowed_value.description().to_pascal_case();
71 let original_identifier = identifier.clone();
72 let identifier_needs_prefix = !allowed_value
73 .description()
74 .chars()
75 .next()
76 .unwrap_or('_')
77 .is_ascii_alphabetic();
78 if identifier_needs_prefix {
79 identifier = format!("_{identifier}");
80 }
81 if let Some(s) = identifier.strip_suffix("Tick")
83 && !s.is_empty()
84 {
85 identifier = s.to_string();
86 }
87 if let Some(s) = identifier.strip_prefix("CancelFor")
89 && !s.is_empty()
90 {
91 identifier = s.to_string();
92 }
93 if let Some(s) = identifier.strip_prefix("Request")
95 && !s.is_empty()
96 {
97 identifier = s.to_string();
98 }
99 if let Some(s) = identifier.strip_suffix("Security")
101 && !s.is_empty()
102 {
103 identifier = s.to_string();
104 }
105 if let Some(s) = identifier.strip_prefix("RelatedTo")
106 && !s.is_empty()
107 {
108 identifier = s.to_string();
109 }
110 if let Some(s) = identifier.strip_suffix("Price")
111 && !s.is_empty()
112 {
113 identifier = s.to_string();
114 }
115 if let Some(s) = identifier.strip_prefix("No")
116 && !s.is_empty()
117 {
118 identifier = s.to_string();
119 }
120 if let Some(s) = identifier.strip_suffix("Trade")
121 && !s.is_empty()
122 {
123 identifier = s.to_string();
124 }
125 if let Some(s) = identifier.strip_prefix("At")
126 && !s.is_empty()
127 {
128 identifier = s.to_string();
129 }
130 if let Some(s) = identifier.strip_suffix("Deal")
131 && !s.is_empty()
132 {
133 identifier = s.to_string();
134 }
135 if let Some(s) = identifier.strip_suffix("Sale")
136 && !s.is_empty()
137 {
138 identifier = s.to_string();
139 }
140 if let Some(s) = identifier.strip_prefix("As")
141 && !s.is_empty()
142 {
143 identifier = s.to_string();
144 }
145 if let Some(s) = identifier.strip_prefix("Of")
146 && !s.is_empty()
147 {
148 identifier = s.to_string();
149 }
150 let mut final_identifier = identifier.to_pascal_case();
152 if final_identifier
153 .chars()
154 .next()
155 .is_none_or(|c| !c.is_ascii_alphabetic())
156 {
157 final_identifier = format!("_{final_identifier}");
158 }
159
160 if variant_identifiers.contains(&final_identifier) {
162 final_identifier = original_identifier.to_pascal_case();
164 if final_identifier
165 .chars()
166 .next()
167 .is_none_or(|c| !c.is_ascii_alphabetic())
168 {
169 final_identifier = format!("_{final_identifier}");
170 }
171
172 let mut counter = 2;
174 let base_identifier = final_identifier.clone();
175 while variant_identifiers.contains(&final_identifier) {
176 final_identifier = format!("{base_identifier}{counter}");
177 counter += 1;
178 }
179 }
180 variant_identifiers.insert(final_identifier.clone());
181 let value_literal = allowed_value.value();
182 indent_string(
183 formatdoc!(
184 r#"
185 /// {doc}
186 #[rustyfix(variant = "{value_literal}")]
187 {identifier},"#,
188 doc = format!("Field variant '{}'.", value_literal),
189 value_literal = value_literal,
190 identifier = final_identifier,
191 )
192 .as_str(),
193 settings.indentation.as_str(),
194 )
195}
196
197#[derive(Debug, Clone)]
200#[non_exhaustive]
201pub struct Settings {
202 pub indentation: String,
205 pub indentation_depth: u32,
207 pub rustyfix_crate_name: String,
209 pub derives_for_allowed_values: Vec<String>,
222 pub attributes_for_allowed_values: Vec<String>,
235}
236
237impl Default for Settings {
238 fn default() -> Self {
239 Self {
240 indentation: " ".to_string(),
241 indentation_depth: 0,
242 derives_for_allowed_values: vec![
243 "Debug".to_string(),
244 "Copy".to_string(),
245 "Clone".to_string(),
246 "PartialEq".to_string(),
247 "Eq".to_string(),
248 "Hash".to_string(),
249 "FieldType".to_string(),
250 ],
251 attributes_for_allowed_values: vec![],
252 rustyfix_crate_name: "rustyfix".to_string(),
253 }
254 }
255}
256
257pub fn codegen_field_definition_struct(
259 fix_dictionary: &dict::Dictionary,
260 field: dict::Field,
261) -> String {
262 let mut header = FxHashSet::default();
263 let mut trailer = FxHashSet::default();
264 for item in fix_dictionary
265 .component_by_name("StandardHeader")
266 .unwrap()
267 .items()
268 {
269 if let dict::LayoutItemKind::Field(f) = item.kind() {
270 header.insert(f.tag());
271 }
272 }
273 for item in fix_dictionary
274 .component_by_name("StandardTrailer")
275 .unwrap()
276 .items()
277 {
278 if let dict::LayoutItemKind::Field(f) = item.kind() {
279 trailer.insert(f.tag());
280 }
281 }
282 gen_field_definition_with_hashsets(fix_dictionary, &header, &trailer, field)
283}
284
285pub fn gen_definitions(fix_dictionary: &dict::Dictionary, settings: &Settings) -> String {
300 let enums = fix_dictionary
301 .fields()
302 .iter()
303 .filter(|f| f.enums().is_some())
304 .map(|f| codegen_field_type_enum(*f, settings))
305 .collect::<Vec<String>>()
306 .join("\n\n");
307 let field_defs = fix_dictionary
308 .fields()
309 .iter()
310 .map(|field| codegen_field_definition_struct(fix_dictionary, *field))
311 .collect::<Vec<String>>()
312 .join("\n\n");
313 let top_comment = onixs_link_to_dictionary(fix_dictionary.version()).unwrap_or_default();
314 let code = formatdoc!(
315 r#"
316 {notice}
317
318 // {top_comment}
319
320 use {rustyfix_path}::dict::{{FieldLocation, FixDatatype}};
321 use {rustyfix_path}::definitions::HardCodedFixFieldDefinition;
322 use {rustyfix_path}::FieldType;
323
324 {enum_definitions}
325
326 {field_defs}"#,
327 notice = generated_code_notice(),
328 top_comment = top_comment,
329 enum_definitions = enums,
330 field_defs = field_defs,
331 rustyfix_path = settings.rustyfix_crate_name,
332 );
333 code
334}
335
336fn indent_string(s: &str, prefix: &str) -> SmartString {
337 s.lines()
338 .map(|line| format!("{prefix}{line}"))
339 .collect::<Vec<String>>()
340 .join("\n")
341 .into()
342}
343
344fn onixs_link_to_field(fix_version: &str, field: dict::Field) -> Option<SmartString> {
345 Some(
346 format!(
347 "https://www.onixs.biz/fix-dictionary/{}/tagnum_{}.html",
348 onixs_dictionary_id(fix_version)?,
349 field.tag().get()
350 )
351 .into(),
352 )
353}
354
355fn onixs_link_to_dictionary(fix_version: &str) -> Option<SmartString> {
356 Some(
357 format!(
358 "https://www.onixs.biz/fix-dictionary/{}/index.html",
359 onixs_dictionary_id(fix_version)?
360 )
361 .into(),
362 )
363}
364
365fn onixs_dictionary_id(fix_version: &str) -> Option<&str> {
366 Some(match fix_version {
367 "FIX.4.0" => "4.0",
368 "FIX.4.1" => "4.1",
369 "FIX.4.2" => "4.2",
370 "FIX.4.3" => "4.3",
371 "FIX.4.4" => "4.4",
372 "FIX.5.0" => "5.0",
373 "FIX.5.0-SP1" => "5.0.sp1",
374 "FIX.5.0-SP2" => "5.0.sp2",
375 "FIXT.1.1" => "fixt1.1",
376 _ => return None,
377 })
378}
379
380fn gen_field_definition_with_hashsets(
381 fix_dictionary: &dict::Dictionary,
382 header_tags: &FxHashSet<TagU32>,
383 trailer_tags: &FxHashSet<TagU32>,
384 field: dict::Field,
385) -> String {
386 let name = field.name().to_shouty_snake_case();
387 let tag = field.tag().to_string();
388 let field_location = if header_tags.contains(&field.tag()) {
389 "Header"
390 } else if trailer_tags.contains(&field.tag()) {
391 "Trailer"
392 } else {
393 "Body"
394 };
395 let doc_link = onixs_link_to_field(fix_dictionary.version(), field);
396 let doc = if let Some(doc_link) = doc_link {
397 format!("/// Field attributes for [`{name} <{tag}>`]({doc_link}).")
398 } else {
399 format!("/// Field attributes for `{name} <{tag}>`.")
400 };
401
402 formatdoc!(
403 r#"
404 {doc}
405 pub const {identifier}: &HardCodedFixFieldDefinition = &HardCodedFixFieldDefinition {{
406 name: "{name}",
407 tag: {tag},
408 data_type: FixDatatype::{data_type},
409 location: FieldLocation::{field_location},
410 }};"#,
411 doc = doc,
412 identifier = name,
413 name = field.name(),
414 tag = tag,
415 field_location = field_location,
416 data_type = <&'static str as From<dict::FixDatatype>>::from(field.data_type().basetype()),
417 )
418}
419
420#[cfg(test)]
421mod test {
422 use super::*;
423
424 #[test]
425 fn syntax_of_field_definitions_is_ok() {
426 let codegen_settings = Settings::default();
427 for dict in dict::Dictionary::common_dictionaries().into_iter() {
428 let code = gen_definitions(&dict, &codegen_settings);
429 syn::parse_file(code.as_str()).unwrap();
430 }
431 }
432
433 #[test]
434 fn generated_code_notice_is_trimmed() {
435 let notice = generated_code_notice();
436 assert_eq!(notice, notice.trim());
437 }
438}