hyle 0.2.0

Framework-agnostic schema and query planning core for hyle.
Documentation
use serde::{Deserialize, Serialize};

use crate::blueprint::Blueprint;
use crate::error::{Error, HyleResult};
use crate::field::{Field, FieldType, Primitive};
use crate::query::Manifest;
use crate::raw::{Outcome, Value, value_to_lookup_key};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Column {
	pub key: String,
	pub field: Field,
	pub label: String,
}

pub fn derive_columns(blueprint: &Blueprint, manifest: &Manifest) -> HyleResult<Vec<Column>> {
	let model = blueprint
		.models
		.get(&manifest.base)
		.ok_or_else(|| Error::UnknownModel(manifest.base.clone()))?;

	manifest
		.fields
		.iter()
		.map(|field_name| {
			let field = model
				.fields
				.get(field_name)
				.ok_or_else(|| Error::UnknownField {
					model: manifest.base.clone(),
					field: field_name.clone(),
				})?;

			Ok(Column {
				key: field_name.clone(),
				field: field.clone(),
				label: field.label.clone(),
			})
		})
		.collect()
}

#[cfg(feature = "wasm")]
pub(crate) fn derive_filter_layout(
	blueprint: &Blueprint,
	manifest: &Manifest,
) -> HyleResult<Vec<Vec<Column>>> {
	let model = blueprint
		.models
		.get(&manifest.base)
		.ok_or_else(|| Error::UnknownModel(manifest.base.clone()))?;

	let layout = manifest
		.filter_fields
		.iter()
		.map(|line| {
			line.iter()
				.filter_map(|field_name| {
					let field = model.fields.get(field_name)?;
					Some(Column {
						key: field_name.clone(),
						field: field.clone(),
						label: field.label.clone(),
					})
				})
				.collect()
		})
		.collect();

	Ok(layout)
}

pub fn display_value(
	blueprint: &Blueprint,
	outcome: &Outcome,
	model_name: &str,
	field_name: &str,
	value: &Value,
) -> String {
	if matches!(value, Value::Null) {
		return String::new();
	}

	let Some(model) = blueprint.models.get(model_name) else {
		return value_to_display_text(value);
	};

	let Some(field) = model.fields.get(field_name) else {
		return value_to_display_text(value);
	};

	display_value_for_type(blueprint, outcome, model_name, &field.field_type, value)
}

fn display_value_for_type(
	blueprint: &Blueprint,
	outcome: &Outcome,
	model_name: &str,
	field_type: &FieldType,
	value: &Value,
) -> String {
	match field_type {
		FieldType::Reference { reference } => {
			let lookup_key = value_to_lookup_key(value);
			if let Some(related) = lookup_key.and_then(|key| {
				outcome
					.lookups
					.get(&reference.entity)
					.and_then(|lookup| lookup.get(&key))
			}) {
				if let Some(display) = related.get(&reference.display_field) {
					return value_to_display_text(display);
				}
			}
			value_to_display_text(value)
		}

		FieldType::Primitive { primitive } => match primitive {
			Primitive::Boolean => {
				if let Value::Bool(b) = value {
					return if *b { "Yes" } else { "No" }.to_owned();
				}
				value_to_display_text(value)
			}
			_ => value_to_display_text(value),
		},

		FieldType::Array { item } => {
			if let Value::Array(arr) = value {
				arr.iter()
					.map(|v| display_value_for_type(blueprint, outcome, model_name, item, v))
					.collect::<Vec<_>>()
					.join(", ")
			} else {
				value_to_display_text(value)
			}
		}

		FieldType::Shape { fields } => {
			if let Value::Map(map) = value {
				fields
					.iter()
					.filter_map(|(key, shape_field)| {
						let sub_val = map.get(key)?;
						if matches!(sub_val, Value::Null) {
							return None;
						}
						let displayed = display_value_for_type(
							blueprint,
							outcome,
							model_name,
							&shape_field.field_type,
							sub_val,
						);
						Some(format!("{}: {}", shape_field.label, displayed))
					})
					.collect::<Vec<_>>()
					.join("; ")
			} else {
				value_to_display_text(value)
			}
		}
	}
}

pub fn display_value_from_outcome(outcome: &Outcome, _key: &str, val: &Value) -> String {
	if let Value::String(s) = val {
		for lookup in outcome.lookups.values() {
			if let Some(ref_row) = lookup.get(s.as_str()) {
				for (k, v) in ref_row {
					if k != "id" {
						if let Value::String(label) = v {
							return label.clone();
						}
					}
				}
			}
		}
		return s.clone();
	}
	match val {
		Value::Bool(b) => if *b { "Yes" } else { "No" }.to_owned(),
		Value::Int(n) => n.to_string(),
		Value::Float(n) => n.to_string(),
		Value::Null => String::new(),
		Value::String(s) => s.clone(),
		Value::Array(a) => {
			#[cfg(feature = "json")]
			{ serde_json::to_string(a).unwrap_or_default() }
			#[cfg(not(feature = "json"))]
			{ a.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ") }
		}
		Value::Map(m) => {
			#[cfg(feature = "json")]
			{ serde_json::to_string(m).unwrap_or_default() }
			#[cfg(not(feature = "json"))]
			{ format!("{{{}}}", m.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join(", ")) }
		}
		Value::Bytes(b) => {
			b.iter().map(|byte| format!("{byte:02x}")).collect::<Vec<_>>().join(" ")
		}
	}
}

pub fn value_to_display_text(value: &Value) -> String {
	match value {
		Value::Null => String::new(),
		Value::Bool(b) => b.to_string(),
		Value::Int(n) => n.to_string(),
		Value::Float(n) => n.to_string(),
		Value::String(s) => s.clone(),
		Value::Bytes(b) => b.iter().map(|byte| format!("{byte:02x}")).collect::<Vec<_>>().join(""),
		Value::Array(arr) => arr.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "),
		Value::Map(m) => format!("{{{}}}", m.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join(", ")),
	}
}