simploxide-bindgen 0.4.0

SimpleX-Chat API types and client generator
Documentation
use crate::types::{
    Field, discriminated_union_type::DiscriminatedUnionVariant, enum_type::EnumVariant,
};

// pub const H1: &str = "# ";
pub const H2: &str = "## ";
pub const H3: &str = "### ";
pub const BOLD: &str = "**";
pub const LIST_ITEM: &str = "- ";

const FIELD_PAT: &str = LIST_ITEM;

/// Skips lines while condition `cond` holds. Returns the first line that broke the condition. The
/// iterator will point to the line after it. Returns None if the iterator was fully consumed in
/// the process.
#[must_use]
pub fn skip_while<'i, 's, F: Fn(&str) -> bool>(
    lines: &'i mut impl Iterator<Item = &'s str>,
    cond: F,
) -> Option<&'s str> {
    lines.find(|&s| !cond(s))
}

/// Skips empty lines returning the first non empty line if met or None if all lines were empty.
/// The iterator will point to the line after the returned non-empty line.
#[must_use]
pub fn skip_empty<'i, 's>(lines: &'i mut impl Iterator<Item = &'s str>) -> Option<&'s str> {
    skip_while(lines, str::is_empty)
}

/// Treats all lines as the doc lines and appends them to the `out_buffer` until the `breaker`
/// condition triggers. If the `breaker` always returns false the iterator will be consumed
/// completely. Returns the line that triggered the breaker condition. The iterator will point to
/// the line after it.
#[must_use]
pub fn parse_doc_lines<'a, 'b, B: Fn(&str) -> bool>(
    lines: &'a mut impl Iterator<Item = &'b str>,
    out_buffer: &mut Vec<String>,
    breaker: B,
) -> Option<&'b str> {
    let mut ret_line = None;
    for line in lines {
        if breaker(line) {
            ret_line = Some(line);
            break;
        }

        // skip trailing empty lines
        if line.is_empty() && out_buffer.is_empty() {
            continue;
        }

        out_buffer.push(line.to_owned());
    }

    // Remove trailing empty lines from end
    while out_buffer
        .last()
        .map(|s| s.trim().is_empty())
        .unwrap_or(false)
    {
        out_buffer.pop();
    }

    ret_line
}

/// Extracts and appends a syntax line from a block like this to the `out_buffer`:
/// \`\`\`
/// <syntax_line>
/// \`\`\`
///
/// Returns an error if block doesn't start or doesn't end with triple backticks. Returns a first
/// line after the block if any. The iterator will point to the line after it.
pub fn parse_syntax<'a, 'b>(
    lines: &'a mut impl Iterator<Item = &'b str>,
    out_buffer: &mut String,
) -> Result<Option<&'b str>, String> {
    let next = skip_empty(lines).map(|s| s.trim());

    if next != Some("```") {
        return Err(format!(
            "Expected opening syntax quotes but found line {next:?}"
        ));
    }

    let syntax = lines
        .next()
        .map(|s| s.trim())
        .ok_or_else(|| "Expected syntax line but found nothing".to_owned())?;

    out_buffer.push_str(syntax);

    let next = lines.next().map(|s| s.trim());

    if next != Some("```") {
        return Err(format!(
            "Expected closing syntax quotes but found line {next:?}"
        ));
    }

    Ok(lines.next())
}

/// Treats all lines as enum variant definitions appending them to the `out_buffer`. The enum
/// variants have the following format:
/// ```text
/// - "variant1"
/// - "variant2"
/// - "variant3"
/// ```
///
/// Skips all lines that doesn't start with the FIELD_PAT('- ').
///
/// Returns an error if the format doesn't match the expected variant definition format.
/// Returns a line that triggered the breaker condition or None if the iterator was consumed completely.
/// The iterator will point to the line after the returned line.
pub fn parse_enum_variants<'a, 'b, B: Fn(&str) -> bool>(
    lines: &'a mut impl Iterator<Item = &'b str>,
    out_buffer: &mut Vec<EnumVariant>,
    breaker: B,
) -> Result<Option<&'b str>, String> {
    for line in lines {
        if breaker(line) {
            return Ok(Some(line));
        }

        if let Some(name) = line.strip_prefix(FIELD_PAT) {
            out_buffer.push(name.parse()?);
        }
    }

    Ok(None)
}

/// Treats all lines as record field definitions appending them to the `out_buffer`. The record
/// field definition has the following format:
/// ```text
/// - <name1>: <type1>
/// - <name2>: <type2>
/// - <name3>: <type3>
/// ```
///
/// Skips all lines that doesn't start with the FIELD_PAT('- ').
///
/// Returns an error if the format doesn't match the expected record field format.
/// Returns a line that triggered the breaker condition or None if the iterator was consumed completely.
/// The iterator will point to the line after the returned line.
pub fn parse_record_fields<'a, 'b, B: Fn(&str) -> bool>(
    lines: &'a mut impl Iterator<Item = &'b str>,
    out_buffer: &mut Vec<Field>,
    breaker: B,
) -> Result<Option<&'b str>, String> {
    for line in lines {
        if breaker(line) {
            return Ok(Some(line));
        }

        if let Some(field) = line.strip_prefix(FIELD_PAT) {
            out_buffer.push(field.parse()?);
        }
    }

    Ok(None)
}

/// Treats next lines as a single discriminated union variant definition. The expected format is:
///
/// ```text
/// - type: "<variant_name>"
/// - <member1>: <type1>
/// - <member2>: <type2>
/// ```
///
/// Stops processing as soon as iterator encounters a line that doesn't start with the FIELD_PAT('-
/// ')
///
/// Returns an error if the first line is not a "type:" line or if parsing of any of the variant
/// components fails.
///
/// Returns parsed [`DiscriminatedUnionVariant`] and the first line that doesn't start with the
/// FIELD_PAT('- '). The iterator will point to the line after it.
pub fn parse_discriminated_union_variant<'a, 'b>(
    lines: &'a mut impl Iterator<Item = &'b str>,
) -> Result<(DiscriminatedUnionVariant, Option<&'b str>), String> {
    let enum_variant: EnumVariant = lines
        .next()
        .and_then(|s| s.strip_prefix(FIELD_PAT))
        .and_then(|s| s.strip_prefix("type: "))
        .ok_or_else(|| {
            "Haven't found the \"- type:\" field. It should come before any other discriminated variant fields"
                .to_owned()
        })
        .and_then(|s| s.parse())?;

    let mut fields = Vec::new();

    for line in lines {
        if !line.starts_with(FIELD_PAT) {
            let ret = DiscriminatedUnionVariant {
                api_name: enum_variant.api_name,
                rust_name: enum_variant.rust_name,
                doc_comments: Vec::new(),
                fields,
            };

            return Ok((ret, Some(line)));
        }

        let field = line.strip_prefix(FIELD_PAT).unwrap();
        fields.push(field.parse()?);
    }

    let ret = DiscriminatedUnionVariant {
        api_name: enum_variant.api_name,
        rust_name: enum_variant.rust_name,
        doc_comments: Vec::new(),
        fields,
    };

    Ok((ret, None))
}

/// Treats all lines as a discriminated union variant definitions appending them to the out_buffer.
/// The expected format is:
///
/// ```text
/// - type: "<variant_name1>"
/// - <member1>: <type1>
/// - <member2>: <type2>
/// ...
/// - type: "<variant_name1>"
/// - <member1>: <type1>
/// - <member2>: <type2>
/// ```
///
/// Skips all lines that doesn't start with the FIELD_PAT('- ').
///
/// Returns an error if parsing of any of the variant components fails.
///
/// Returns the first line that met the breaker con FIELD_PAT('- '). The iterator will point to the
/// line after it.
pub fn parse_discriminated_union_variants<'a, 'b, B: Fn(&str) -> bool>(
    lines: &'a mut impl Iterator<Item = &'b str>,
    out_buffer: &mut Vec<DiscriminatedUnionVariant>,
    breaker: B,
) -> Result<Option<&'b str>, String> {
    let mut enum_variant: Option<EnumVariant> = None;
    let mut fields: Vec<Field> = Vec::new();

    for line in lines {
        if breaker(line) {
            if let Some(var) = enum_variant {
                out_buffer.push(DiscriminatedUnionVariant {
                    api_name: var.api_name,
                    rust_name: var.rust_name,
                    doc_comments: Vec::new(),
                    fields: fields.clone(),
                });
            }

            return Ok(Some(line));
        }

        if let Some(field) = line.strip_prefix(FIELD_PAT) {
            if let Some(name) = field.strip_prefix("type: ") {
                let var: EnumVariant = name.parse()?;

                if let Some(old_var) = enum_variant.replace(var) {
                    out_buffer.push(DiscriminatedUnionVariant {
                        api_name: old_var.api_name,
                        rust_name: old_var.rust_name,
                        doc_comments: Vec::new(),
                        fields: fields.clone(),
                    });

                    fields.clear();
                }
            } else {
                fields.push(field.parse()?);
            }
        }
    }

    if let Some(var) = enum_variant {
        out_buffer.push(DiscriminatedUnionVariant {
            api_name: var.api_name,
            rust_name: var.rust_name,
            doc_comments: Vec::new(),
            fields: fields.clone(),
        });
    }

    Ok(None)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::enum_type::EnumVariant;

    const HELLO: &str = r#"

Hello!
World!

## Title

"#;
    const CHAT_TYPE: &str = r#"

## ChatType

**Enum type**:
- "direct"
- "group"
- "local"

**Syntax**:

```
@|#|*|
```

```javascript
self == 'contact' ? '@' : self == 'group' ? '#' : self == 'local' ? '*' : '' // JavaScript
```

```python
'@' if str(self) == 'contact' else '#' if str(self) == 'group' else '*' if str(self) == 'local' else '' # Python
```



---
"#;

    fn to_end(_: &str) -> bool {
        false
    }

    #[test]
    fn lines_skipper() {
        let mut lines = HELLO.lines();

        let line = skip_empty(&mut lines);
        assert_eq!(line, Some("Hello!"));
        assert_eq!(lines.next(), Some("World!"));
    }

    #[test]
    fn doc_parser() {
        let mut lines = HELLO.lines();
        let mut docs = Vec::new();

        let next = parse_doc_lines(&mut lines, &mut docs, |s| s.starts_with("##")).unwrap();

        assert_eq!(docs, vec!["Hello!", "World!"]);
        assert_eq!(next, "## Title");

        docs.clear();
        let mut lines = HELLO.lines();

        let next = parse_doc_lines(&mut lines, &mut docs, to_end);
        assert!(next.is_none());
        assert_eq!(docs, vec!["Hello!", "World!", "", "## Title"]);
    }

    #[test]
    fn syntax_parser() {
        let mut lines = CHAT_TYPE.lines();

        skip_while(&mut lines, |s| s != "**Syntax**:").unwrap();

        let mut syntax = String::new();

        let _ = parse_syntax(&mut lines, &mut syntax).unwrap();

        assert_eq!(syntax, "@|#|*|");
    }

    #[test]
    fn enum_variants() {
        let mut lines = CHAT_TYPE.lines();

        skip_while(&mut lines, |s| s != "**Enum type**:").unwrap();

        let mut variants: Vec<EnumVariant> = Vec::new();
        let next = parse_enum_variants(&mut lines, &mut variants, |s| s.starts_with("**")).unwrap();

        assert_eq!(next, Some("**Syntax**:"));

        let api_names: Vec<_> = variants.iter().map(|var| var.api_name.as_str()).collect();
        let rust_names: Vec<_> = variants.iter().map(|var| var.rust_name.as_str()).collect();

        assert_eq!(api_names, vec!["direct", "group", "local"]);
        assert_eq!(rust_names, vec!["Direct", "Group", "Local"]);
    }
}