hotfix_codegen/
lib.rs

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
9/// Creates a [`String`] that contains a multiline Rust "Doc" comment explaining
10/// that all subsequent code was automatically generated.
11///
12/// The following example is for illustrative purposes only and the actual
13/// contents might change. The string is guaranteed not to have any trailing or
14/// leading whitespace.
15///
16/// ```text
17/// // Generated automatically by FerrumFIX. Do not modify manually.
18/// ```
19pub 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
36/// Generates the Rust code for an `enum` that has variants that map 1:1 the
37/// available values for `field`.
38pub 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/// Code generation settings. Instantiate with [`Default::default`] and then
95/// change field values if necessary.
96#[derive(Debug, Clone)]
97pub struct Settings {
98    phantom: PhantomData<()>,
99
100    /// The indentation prefix of all generated Rust code. Four
101    /// spaces by default.
102    pub indentation: String,
103    /// The indentation level of all generated Rust code. Zero by default.
104    pub indentation_depth: u32,
105    /// The name of the `fefix` crate for imports. `fefix` by default.
106    pub fefix_crate_name: String,
107    /// A list of derive macros on top of all generated FIX datatype `enum`s. E.g.:
108    ///
109    /// ```
110    /// // #[derive(Foobar, Spam)]
111    /// enum FoodOrDrink {
112    ///     Food,
113    ///     Drink,
114    /// }
115    /// ```
116    ///
117    /// Contains [`Debug`], [`Copy`], [`PartialEq`], [`Eq`], [`Hash`],
118    /// `FieldType` by default.
119    pub derives_for_allowed_values: Vec<String>,
120    /// A list of attribute macros for generated `enum`s variants. E.g.:
121    ///
122    /// ```
123    /// enum FoodOrDrink {
124    ///     // #[foobar]
125    ///     Food,
126    ///     // #[spam]
127    ///     Drink,
128    /// }
129    /// ```
130    ///
131    /// Empty by default.
132    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
156/// Generates the Rust code for a FIX field definition.
157pub 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
184/// Generates `const` implementors of `IsFieldDefinition`.
185///
186/// The generated module will contain:
187///
188/// - A generated code notice ([generated_code_notice]).
189/// - `enum` definitions for FIX field types.
190/// - A constant implementor of `IsFieldDefinition` for each FIX field.
191///
192/// The Rust code will be free of any leading and trailing whitespace.
193/// An effort is made to provide good formatting, but users shouldn't rely on it
194/// and assume that formatting might be bad.
195pub 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}