rm-lisa 0.3.2

A logging library for rem-verse, with support for inputs, tasks, and more.
Documentation
//! Functions related to formatting artbirary tracing fields, into proper
//! message components.

use crate::display::{
	renderers::color::helpers::{chunk_string_into_width, pad_to_width},
	tracing::FlattenedTracingField,
};
use fnv::FnvHashMap;
use std::{collections::hash_map::Iter as MapIter, slice::Iter as SliceIter};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

/// Create the series of tailer lines that fully cover all of the fields in a
/// log message.
///
/// This is used when logging messages don't have a lot of fields, and are not
/// marked to force 'combine'. So they get rendered in the 'tailer' part of the
/// log line or the third part of a message.
pub fn create_field_tailers(
	mut tailer_width: usize,
	fields: &FnvHashMap<&'static str, FlattenedTracingField>,
	skip_cause: bool,
	skip_fields: bool,
) -> Vec<String> {
	tailer_width -= 1;
	if skip_fields || fields.is_empty() {
		return Vec::with_capacity(0);
	}

	let mut tailer_lines = Vec::new();
	let mut current_buff = String::with_capacity(tailer_width);
	current_buff.push('|');

	for (unrep_key, unformatted_unrep_value) in fields {
		let mut actual_key = unrep_key.replace('\n', "  ").replace('\r', "");
		if actual_key == "cause" && skip_cause {
			continue;
		}
		let actual_value = format!("{unformatted_unrep_value}")
			.replace('\n', "")
			.replace('\r', "");

		// Current buff always has '|' as a prefix, so len > 1 always checks if we
		// are not empty.
		let buff_used = current_buff.len() > 1;
		let comma_width = usize::from(buff_used);
		let needed_width = actual_key.width() + actual_value.width() + comma_width;
		let mut current_buff_width = current_buff.width();

		// We can append the whole thing without worry...
		if current_buff_width + needed_width < tailer_width {
			if buff_used {
				current_buff.push(',');
			}
			current_buff += &actual_key;
			current_buff.push('=');
			current_buff += &actual_value;
			continue;
		}

		// Append a comma if we already have data...
		if buff_used {
			if current_buff_width + comma_width < tailer_width {
				current_buff.push(',');
			} else {
				tailer_lines.push(pad_to_width(current_buff, tailer_width));
				current_buff = String::with_capacity(tailer_width);
				// We don't need to append comma cause it cleanly wraps.
				current_buff.push('|');
				current_buff_width = 1;
			}
		}

		// Key should wrap so equal doesn't _start_ a line, so we can't call append
		// yet.
		let mut key_width = actual_key.width();
		while key_width > tailer_width {
			let character = actual_key.remove(0);
			let character_width = character.width().unwrap_or_default();
			key_width -= character_width;

			if current_buff_width + character_width >= tailer_width {
				tailer_lines.push(pad_to_width(current_buff, tailer_width));
				current_buff = String::with_capacity(tailer_width);
				current_buff.push('|');
				current_buff_width = 1;
			}
			current_buff.push(character);
			current_buff_width += character_width;
		}
		// Now  we can safely fit the equals on one line...
		actual_key.push('=');
		append_to_tailer(
			tailer_width,
			&mut tailer_lines,
			&mut current_buff,
			&mut current_buff_width,
			&actual_key,
		);

		append_to_tailer(
			tailer_width,
			&mut tailer_lines,
			&mut current_buff,
			&mut current_buff_width,
			&actual_value,
		);
	}

	if current_buff.len() > 1 {
		tailer_lines.push(pad_to_width(current_buff, tailer_width));
	}

	tailer_lines
}

/// Create a 'combined' message where there are so many fields we just render
/// it with a separator + tailer.
///
/// In order to make log messages look good, sometimes shoving lots of fields
/// into a very small tailer can cause very bad display issues. So we create
/// what we call a 'combined' message, where the message, and tailer width
/// are shoved into one.
///
/// ## Params
///
/// - `width`: the width we can take this should be
///   `message_width` + `tailer_width`.
/// - `fields`: The fields we need to display, and render.
/// - `message`: The actual message part of the log line.
/// - `hide_fields`: If the user has requested that all fields be hidden.
/// - `skip_cause`: If we should skip rendering the `cause` field.
#[must_use]
pub fn create_combined_message(
	width: usize,
	fields: &FnvHashMap<&'static str, FlattenedTracingField>,
	mut message: String,
	hide_fields: bool,
	skip_cause: bool,
) -> Vec<String> {
	if hide_fields || fields.is_empty() {
		return chunk_string_into_width(width, &message);
	}
	message.push_str("\n\n");

	let mut iterators = vec![FieldIterator::Object(fields.iter())];
	while let Some(current_iterator) = iterators.last_mut() {
		match current_iterator {
			FieldIterator::Object(object_iter) => {
				if let Some((unrep_key, unformatted_unrep_value)) = object_iter.next() {
					format_object_iterated_field(
						unrep_key,
						unformatted_unrep_value,
						&mut message,
						&mut iterators,
						skip_cause,
					);
				} else {
					iterators.pop();
					message.push('\n');
				}
			}
			FieldIterator::NestedObject(object_iter) => {
				if let Some((unrep_key, unformatted_unrep_value)) = object_iter.next() {
					format_object_iterated_field(
						unrep_key,
						unformatted_unrep_value,
						&mut message,
						&mut iterators,
						skip_cause,
					);
				} else {
					iterators.pop();
					message.push('\n');
				}
			}
			FieldIterator::List(list_iter) => {
				if let Some(unformatted_unrep_value) = list_iter.next() {
					match unformatted_unrep_value {
						FlattenedTracingField::List(inner_list) => {
							iterators.push(FieldIterator::List(inner_list.iter()));
						}
						FlattenedTracingField::Object(inner_obj) => {
							iterators.push(FieldIterator::NestedObject(inner_obj.iter()));
						}
						_ => {
							let actual_value = format!("{unformatted_unrep_value}");
							message.push_str(&create_field_indent_padding(iterators.len()));
							message.push_str(&actual_value);
							message.push('\n');
						}
					}
				} else {
					iterators.pop();
					message.push('\n');
				}
			}
		}
	}

	chunk_string_into_width(width, &message)
}

fn format_object_iterated_field<'subiter>(
	unrep_key: &str,
	unformatted_unrep_value: &'subiter FlattenedTracingField,
	message: &mut String,
	iterators: &mut Vec<FieldIterator<'subiter>>,
	skip_cause: bool,
) {
	let actual_key = unrep_key.replace('\n', "  ").replace('\r', "");
	if actual_key == "cause" && skip_cause {
		return;
	}

	message.push_str(&create_field_indent_padding(iterators.len()));
	message.push_str(&actual_key);
	message.push('=');

	match unformatted_unrep_value {
		FlattenedTracingField::List(inner_list) => {
			iterators.push(FieldIterator::List(inner_list.iter()));
		}
		FlattenedTracingField::Object(inner_obj) => {
			iterators.push(FieldIterator::NestedObject(inner_obj.iter()));
		}
		_ => {
			let actual_value = format!("{unformatted_unrep_value}");
			message.push_str(&actual_value);
		}
	}

	message.push('\n');
}

#[must_use]
fn create_field_indent_padding(padding: usize) -> String {
	// padding only kicks in past 1, and for each padding it's 2 characters per pad
	// then we add one final space...
	let mut message = String::with_capacity(((padding - 1) * 2) + 1);
	for _ in 1_usize..padding {
		message.push_str(" |");
	}
	message.push(' ');
	message
}

fn append_to_tailer(
	tailer_width: usize,
	tailers: &mut Vec<String>,
	current_buff: &mut String,
	current_buff_width: &mut usize,
	to_append: &str,
) {
	for character in to_append.chars() {
		let character_width = character.width().unwrap_or_default();

		if *current_buff_width + character_width >= tailer_width {
			let mut buff_as_string = String::with_capacity(tailer_width);
			std::mem::swap(&mut buff_as_string, current_buff);
			tailers.push(pad_to_width(buff_as_string, tailer_width));
			current_buff.push('|');
			current_buff.push(character);
			*current_buff_width = 1;
		} else {
			current_buff.push(character);
			*current_buff_width += character_width;
		}
	}
}

/// A shared type over all types of Flattened iterators.
enum FieldIterator<'items> {
	/// An iterator an object.
	Object(MapIter<'items, &'static str, FlattenedTracingField>),
	/// An iterator over a nested object.
	NestedObject(MapIter<'items, String, FlattenedTracingField>),
	/// An iterator over a list.
	List(SliceIter<'items, FlattenedTracingField>),
}

#[cfg(test)]
mod unit_tests {
	use super::*;
	use crate::display::renderers::color::helpers::calculate_tailer_width;

	#[test]
	pub fn calculate_fields() {
		let mut fields = FnvHashMap::default();
		fields.insert("ip", FlattenedTracingField::Str("192.168.7.42".to_owned()));
		fields.insert("port", FlattenedTracingField::UnsignedInt(7500));

		assert_eq!(
			create_field_tailers(calculate_tailer_width(40), &fields, false, false),
			vec![
				"|ip=192.1 ".to_owned(),
				"|68.7.42,p".to_owned(),
				"|ort=7500 ".to_owned(),
			],
		);

		assert_eq!(
			create_field_tailers(calculate_tailer_width(80), &fields, false, false),
			vec![
				"|ip=192.168.7.42,por".to_owned(),
				"|t=7500             ".to_owned(),
			],
		);
	}
}