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