oreq 0.2.6

The tool for interactively creating curl arguments from OpenAPI.
Documentation
use openapiv3::{ArrayType, OpenAPI, ReferenceOr, Schema};
use promptuity::{
    event::{KeyCode, KeyModifiers},
    Prompt, PromptBody, PromptState, RenderPayload, Validator,
};
use serde_json::{json, Value};

use super::{
    prompt_builder,
    utils::{fmt_body, fmt_input},
};
use crate::schema::reference::ReferenceOrExt;

pub trait ArrayFormatter {
    fn fmt_prompt(&self, submitted: String, input: String) -> String;
    fn fmt_submitted_item(&self, idx: usize, value: Value) -> String;
    fn fmt_input(&self, payload: RenderPayload) -> String;
    fn fmt_submit(&self, value: Vec<Value>) -> String;
}

pub struct DefaultArrayFormatter;
impl ArrayFormatter for DefaultArrayFormatter {
    fn fmt_prompt(&self, submitted: String, input: String) -> String {
        format!("{}{}", submitted, input)
    }

    fn fmt_submitted_item(&self, _: usize, value: Value) -> String {
        format!("{},\n", value)
    }

    fn fmt_input(&self, payload: RenderPayload) -> String {
        let prompt_input = fmt_input(&payload.input);
        let prompt_body = fmt_body(&payload.body);

        format!("{}\n{}", prompt_input, prompt_body)
    }

    fn fmt_submit(&self, value: Vec<Value>) -> String {
        let value = value
            .iter()
            .enumerate()
            .map(|(_, v)| format!("{}", v))
            .collect::<Vec<_>>()
            .join(",\n");

        format!("{}\n", value)
    }
}

pub struct PromptOption {
    items: Option<ReferenceOr<Box<Schema>>>,
    min_items: Option<usize>,
    max_items: Option<usize>,
    unique_items: bool,
}

impl PromptOption {
    pub fn new(schema: ArrayType) -> Self {
        Self {
            items: schema.items,
            min_items: schema.min_items,
            max_items: schema.max_items,
            unique_items: schema.unique_items,
        }
    }
}

impl Validator<Vec<Value>> for PromptOption {
    fn validate(&self, value: &Vec<Value>) -> Result<(), String> {
        if let Some(min) = self.min_items {
            if value.len() < min {
                return Err(format!("Array must have at least {} items", min));
            }
        }

        Ok(())
    }
}

pub struct Array {
    message: String,
    option: PromptOption,
    formatter: Box<dyn ArrayFormatter>,
    api: OpenAPI,
    value: Vec<Value>,
    current_prompt: Option<Box<dyn Prompt<Output = Value>>>,
}

impl Array {
    pub fn new(message: String, api: &OpenAPI, schema: ArrayType) -> Self {
        Self {
            message,
            option: PromptOption::new(schema),
            formatter: Box::new(DefaultArrayFormatter),
            api: api.clone(),
            value: Vec::new(),
            current_prompt: None,
        }
    }

    fn create_prompt(&mut self) -> Result<(), promptuity::Error> {
        match &self.option.items {
            Some(refs) => {
                let refs = refs.clone().unbox();
                let item = refs
                    .item(&self.api)
                    .map_err(|x| promptuity::Error::Config(x.to_string()))?;

                let idx = self.value.len();
                let msg = format!("{}[{}]", self.message, idx);
                let mut prompt = prompt_builder(&self.api, item, msg, None);
                prompt.setup()?;
                self.current_prompt = Some(prompt);

                Ok(())
            }
            None => Err(promptuity::Error::Config(
                "No items schema found".to_owned(),
            )),
        }
    }

    fn check_unique(&self, value: &Value) -> bool {
        if !self.option.unique_items {
            return true;
        }

        !self.value.contains(value)
    }
}

impl Prompt for Array {
    type Output = Value;

    fn setup(&mut self) -> Result<(), promptuity::Error> {
        self.create_prompt()
    }

    fn handle(&mut self, code: KeyCode, modifiers: KeyModifiers) -> PromptState {
        if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (code, modifiers) {
            return PromptState::Cancel;
        }

        let prompt = self.current_prompt.as_mut().unwrap();

        let state = prompt.handle(code, modifiers);
        match state {
            PromptState::Submit => {
                let is_valid = prompt.validate();
                if let Err(err) = is_valid {
                    return PromptState::Error(err);
                }

                let value = prompt.submit();
                if self.check_unique(&value) {
                    return PromptState::Error("Value already exists".to_owned());
                }
                if let Some(max) = self.option.max_items {
                    if self.value.len() >= max {
                        return PromptState::Error("Array is full".to_owned());
                    }
                }

                self.value.push(value);

                match self.create_prompt() {
                    Ok(_) => PromptState::Active,
                    Err(err) => PromptState::Fatal(err.to_string()),
                }
            }
            PromptState::Cancel => PromptState::Submit,
            _ => state,
        }
    }

    fn submit(&mut self) -> Self::Output {
        json!(self.value)
    }

    fn render(&mut self, state: &PromptState) -> Result<RenderPayload, String> {
        match state {
            PromptState::Submit => Ok(RenderPayload::new(self.message.clone(), None, None).body(
                PromptBody::Raw(self.formatter.fmt_submit(self.value.clone())),
            )),
            _ => {
                let prompt = self.current_prompt.as_mut().unwrap();

                let prompt_payload = prompt.render(state)?;
                let input = self.formatter.fmt_input(prompt_payload);

                let submitted = self
                    .value
                    .iter()
                    .enumerate()
                    .map(|(idx, value)| self.formatter.fmt_submitted_item(idx, value.clone()))
                    .collect::<Vec<_>>()
                    .join("");

                let payload = RenderPayload::new(self.message.clone(), None, None)
                    .body(PromptBody::Raw(self.formatter.fmt_prompt(submitted, input)));

                Ok(payload)
            }
        }
    }

    fn validate(&self) -> Result<(), String> {
        self.option.validate(&self.value)
    }
}