1use fnv::FnvHashSet;
4use heck::{ToPascalCase, ToShoutySnakeCase};
5use hotfix_dictionary::{self as dict, TagU32};
6use indoc::indoc;
7use std::marker::PhantomData;
8
9const HOTFIX_VERSION: &str = env!("CARGO_PKG_VERSION");
10
11pub fn generated_code_notice() -> String {
22 use chrono::prelude::*;
23
24 format!(
25 indoc!(
26 r#"
27 // Generated automatically by HotFIX {} on {}.
28 //
29 // DO NOT MODIFY MANUALLY.
30 // DO NOT COMMIT TO VERSION CONTROL.
31 // ALL CHANGES WILL BE OVERWRITTEN."#
32 ),
33 HOTFIX_VERSION,
34 Utc::now().to_rfc2822(),
35 )
36}
37
38pub fn codegen_field_type_enum(field: dict::Field, settings: &Settings) -> String {
41 let derives = settings.derives_for_allowed_values.join(", ");
42 let attributes = settings.attributes_for_allowed_values.join("\n");
43 let variants = field
44 .enums()
45 .unwrap()
46 .map(|v| codegen_field_type_enum_variant(v, settings))
47 .collect::<Vec<String>>()
48 .join("\n");
49 format!(
50 indoc!(
51 r#"
52 /// Field type variants for [`{field_name}`].
53 #[derive({derives})]
54 {attributes}
55 pub enum {identifier} {{
56 {variants}
57 }}"#
58 ),
59 field_name = field.name().to_pascal_case(),
60 derives = derives,
61 attributes = attributes,
62 identifier = field.name().to_pascal_case(),
63 variants = variants,
64 )
65}
66
67fn codegen_field_type_enum_variant(allowed_value: dict::FieldEnum, settings: &Settings) -> String {
68 let mut identifier = allowed_value.description().to_pascal_case();
69 let identifier_needs_prefix = !allowed_value
70 .description()
71 .chars()
72 .next()
73 .unwrap_or('_')
74 .is_ascii_alphabetic();
75 if identifier_needs_prefix {
76 identifier = format!("_{identifier}");
77 }
78 let value_literal = allowed_value.value();
79 indent_string(
80 format!(
81 indoc!(
82 r#"
83 /// {doc}
84 #[hotfix(variant = "{value_literal}")]
85 {identifier},"#
86 ),
87 doc = format!("Field variant '{}'.", value_literal),
88 value_literal = value_literal,
89 identifier = identifier,
90 )
91 .as_str(),
92 settings.indentation.as_str(),
93 )
94}
95
96#[derive(Debug, Clone)]
99pub struct Settings {
100 phantom: PhantomData<()>,
101
102 pub indentation: String,
105 pub indentation_depth: u32,
107 pub hotfix_crate_name: String,
109 pub derives_for_allowed_values: Vec<String>,
122 pub attributes_for_allowed_values: Vec<String>,
135}
136
137impl Default for Settings {
138 fn default() -> Self {
139 Self {
140 indentation: " ".to_string(),
141 indentation_depth: 0,
142 derives_for_allowed_values: vec![
143 "Debug".to_string(),
144 "Copy".to_string(),
145 "Clone".to_string(),
146 "PartialEq".to_string(),
147 "Eq".to_string(),
148 "Hash".to_string(),
149 "FieldType".to_string(),
150 ],
151 attributes_for_allowed_values: vec![],
152 hotfix_crate_name: "hotfix".to_string(),
153 phantom: PhantomData,
154 }
155 }
156}
157
158pub fn codegen_field_definition_struct(
160 fix_dictionary: &dict::Dictionary,
161 field: dict::Field,
162) -> String {
163 let mut header = FnvHashSet::default();
164 let mut trailer = FnvHashSet::default();
165 for item in fix_dictionary
166 .component_by_name("StandardHeader")
167 .unwrap()
168 .items()
169 {
170 if let dict::LayoutItemKind::Field(f) = item.kind() {
171 header.insert(f.tag());
172 }
173 }
174 for item in fix_dictionary
175 .component_by_name("StandardTrailer")
176 .unwrap()
177 .items()
178 {
179 if let dict::LayoutItemKind::Field(f) = item.kind() {
180 trailer.insert(f.tag());
181 }
182 }
183 gen_field_definition_with_hashsets(fix_dictionary, &header, &trailer, field)
184}
185
186pub fn gen_definitions(fix_dictionary: &dict::Dictionary, settings: &Settings) -> String {
198 let enums = fix_dictionary
199 .fields()
200 .iter()
201 .filter(|f| f.enums().is_some())
202 .map(|f| codegen_field_type_enum(*f, settings))
203 .collect::<Vec<String>>()
204 .join("\n\n");
205 let field_defs = fix_dictionary
206 .fields()
207 .iter()
208 .map(|field| codegen_field_definition_struct(fix_dictionary, *field))
209 .collect::<Vec<String>>()
210 .join("\n");
211 let top_comment = onixs_link_to_dictionary(fix_dictionary.version()).unwrap_or_default();
212 let code = format!(
213 indoc!(
214 r#"
215 {notice}
216
217 // {top_comment}
218
219 use {hotfix_path}::dict::FieldLocation;
220 use {hotfix_path}::dict::FixDatatype;
221 use {hotfix_path}::{{FieldType, HardCodedFixFieldDefinition}};
222
223 {enum_definitions}
224
225 {field_defs}"#
226 ),
227 notice = generated_code_notice(),
228 top_comment = top_comment,
229 enum_definitions = enums,
230 field_defs = field_defs,
231 hotfix_path = settings.hotfix_crate_name,
232 );
233 code
234}
235
236fn indent_string(s: &str, prefix: &str) -> String {
237 s.lines().fold(String::new(), |mut s, line| {
238 if line.contains(char::is_whitespace) {
239 s.push_str(prefix);
240 }
241 s.push_str(line);
242 s.push('\n');
243 s
244 })
245}
246
247fn onixs_link_to_field(fix_version: &str, field: dict::Field) -> Option<String> {
248 Some(format!(
249 "https://www.onixs.biz/fix-dictionary/{}/tagnum_{}.html",
250 onixs_dictionary_id(fix_version)?,
251 field.tag().get()
252 ))
253}
254
255fn onixs_link_to_dictionary(fix_version: &str) -> Option<String> {
256 Some(format!(
257 "https://www.onixs.biz/fix-dictionary/{}/index.html",
258 onixs_dictionary_id(fix_version)?
259 ))
260}
261
262fn onixs_dictionary_id(fix_version: &str) -> Option<&str> {
263 Some(match fix_version {
264 "FIX.4.0" => "4.0",
265 "FIX.4.1" => "4.1",
266 "FIX.4.2" => "4.2",
267 "FIX.4.3" => "4.3",
268 "FIX.4.4" => "4.4",
269 "FIX.5.0" => "5.0",
270 "FIX.5.0-SP1" => "5.0.sp1",
271 "FIX.5.0-SP2" => "5.0.sp2",
272 "FIXT.1.1" => "fixt1.1",
273 _ => return None,
274 })
275}
276
277fn gen_field_definition_with_hashsets(
278 fix_dictionary: &dict::Dictionary,
279 header_tags: &FnvHashSet<TagU32>,
280 trailer_tags: &FnvHashSet<TagU32>,
281 field: dict::Field,
282) -> String {
283 let name = field.name().to_shouty_snake_case();
284 let tag = field.tag().to_string();
285 let field_location = if header_tags.contains(&field.tag()) {
286 "Header"
287 } else if trailer_tags.contains(&field.tag()) {
288 "Trailer"
289 } else {
290 "Body"
291 };
292 let doc_link = onixs_link_to_field(fix_dictionary.version(), field);
293 let doc = if let Some(doc_link) = doc_link {
294 format!("/// Field attributes for [`{name} <{tag}>`]({doc_link}).")
295 } else {
296 format!("/// Field attributes for `{name} <{tag}>`.")
297 };
298
299 format!(
300 indoc!(
301 r#"
302 {doc}
303 pub const {identifier}: &HardCodedFixFieldDefinition = &HardCodedFixFieldDefinition {{
304 name: "{name}",
305 tag: {tag},
306 data_type: FixDatatype::{data_type},
307 location: FieldLocation::{field_location},
308 }};"#
309 ),
310 doc = doc,
311 identifier = name,
312 name = field.name(),
313 tag = tag,
314 field_location = field_location,
315 data_type = <&'static str as From<dict::FixDatatype>>::from(field.data_type().basetype()),
316 )
317}
318
319#[cfg(test)]
320mod test {
321 use super::*;
322
323 #[test]
324 fn syntax_of_field_definitions_is_ok() {
325 let codegen_settings = Settings::default();
326 for dict in dict::Dictionary::common_dictionaries().into_iter() {
327 let code = gen_definitions(&dict, &codegen_settings);
328 syn::parse_file(code.as_str()).unwrap();
329 }
330 }
331
332 #[test]
333 fn generated_code_notice_is_trimmed() {
334 let notice = generated_code_notice();
335 assert_eq!(notice, notice.trim());
336 }
337}