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