flarer 0.1.0

Rust client and CLI for Cloudflare's Browser Rendering REST API (content, screenshot, PDF, snapshot, markdown, scrape, JSON extraction, links, crawl).
Documentation
//! Step-by-step interactive JSON schema builder.

use inquire::{Confirm, MultiSelect, Select, Text};
use serde_json::{Map, Value, json};
use strum::VariantNames;

use crate::ResponseFormat;
use crate::error::FlarerError;

/// JSON Schema primitive types offered to the user.
#[derive(Debug, Clone, Copy, strum::Display, strum::EnumString, strum::VariantNames)]
#[strum(serialize_all = "lowercase")]
pub enum JsonType {
    /// Object.
    Object,
    /// Array.
    Array,
    /// String.
    String,
    /// Number.
    Number,
    /// Boolean.
    Boolean,
    /// Null.
    Null,
}

#[derive(Debug, Clone)]
struct Item {
    name: String,
    type_: String,
}

fn cfg<E: std::fmt::Display>(e: E) -> FlarerError {
    FlarerError::Config(e.to_string())
}

fn properties_builder() -> crate::Result<Vec<String>> {
    let mut names: Vec<String> = Vec::new();
    println!(
        "Define one or more top-level properties. e.g. for an e-commerce page \
         you might want a `products` array."
    );
    let first = Text::new("First property name:")
        .with_help_message("e.g. products")
        .prompt()
        .map_err(cfg)?;
    names.push(first);

    loop {
        let more = Confirm::new("Add another top-level property?")
            .with_default(false)
            .prompt()
            .map_err(cfg)?;
        if !more {
            break;
        }
        let next = Text::new("Property name:").prompt().map_err(cfg)?;
        names.push(next);
    }
    Ok(names)
}

type PropertyDef = (String, Vec<Item>, Vec<String>);

fn items_builder(names: Vec<String>) -> crate::Result<Vec<PropertyDef>> {
    let mut results = Vec::new();
    for name in names {
        loop {
            let mut items: Vec<Item> = Vec::new();
            println!(
                "Defining fields for `{name}`. After each field choose whether to add another."
            );
            loop {
                let item_name = Text::new("Field name:").prompt().map_err(cfg)?;
                let item_type = Select::new("Field type:", JsonType::VARIANTS.to_vec())
                    .prompt()
                    .map_err(cfg)?;
                items.push(Item {
                    name: item_name,
                    type_: item_type.to_string(),
                });
                let more = Confirm::new("Add another field?")
                    .with_default(false)
                    .prompt()
                    .map_err(cfg)?;
                if !more {
                    break;
                }
            }
            let required = MultiSelect::new(
                "Select required fields:",
                items.iter().map(|i| i.name.clone()).collect(),
            )
            .prompt()
            .map_err(cfg)?;
            let confirm = Confirm::new(&format!(
                "Confirm fields for `{name}`: {:?} (required: {:?})",
                items, required
            ))
            .with_default(true)
            .prompt()
            .map_err(cfg)?;
            if confirm {
                results.push((name.clone(), items, required));
                break;
            }
        }
    }
    Ok(results)
}

fn assemble(values: Vec<PropertyDef>) -> Value {
    let mut root = Map::new();
    for (name, items, required) in values {
        let mut props = Map::new();
        for it in items {
            props.insert(it.name, json!({ "type": it.type_ }));
        }
        root.insert(
            name,
            json!({
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": Value::Object(props),
                    "required": required,
                }
            }),
        );
    }
    json!({
        "type": "object",
        "properties": Value::Object(root),
    })
}

/// Public entry point: walk the user through assembling a JSON schema and
/// return a typed [`ResponseFormat`].
pub async fn build_response_format_interactive() -> crate::Result<ResponseFormat> {
    let names = properties_builder()?;
    let items = items_builder(names)?;
    Ok(ResponseFormat::json_schema(assemble(items)))
}