use std::{fs, path::Path};
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use crate::io::input::{parse_document_str, schema_with_defaults};
use crate::io::{DocumentFormat, DocumentFormatProbe};
use crate::tui::model::{FormSchema, form_schema_from_ui_ast};
use crate::tui::state::LayoutNavModel;
use crate::ui_ast::{UiAst, UiAstBundle, build_ui_ast_bundle};
pub mod defaults;
pub mod layout;
#[cfg(feature = "tui")]
pub mod tui;
#[cfg(feature = "web")]
pub mod web;
pub const UI_ARTIFACT_BUNDLE_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UiArtifactFingerprint {
pub schema_sha256: String,
pub defaults_sha256: String,
pub input_sha256: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TuiArtifacts {
pub form_schema: FormSchema,
pub layout_nav: LayoutNavModel,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UiArtifactBundle {
pub artifact_version: u32,
pub fingerprint: UiArtifactFingerprint,
pub ui: UiAstBundle,
pub tui: TuiArtifacts,
}
pub fn build_ui_artifact_bundle(
schema: &Value,
defaults: Option<&Value>,
) -> Result<UiArtifactBundle> {
let defaults = defaults
.cloned()
.unwrap_or_else(|| Value::Object(Map::new()));
let enriched = schema_with_defaults(schema, &defaults);
let ui = build_ui_ast_bundle(&enriched)?;
let fingerprint = UiArtifactFingerprint {
schema_sha256: sha256_hex(&stable_value_bytes(schema)?),
defaults_sha256: sha256_hex(&stable_value_bytes(&defaults)?),
input_sha256: sha256_hex(&stable_value_bytes(&Value::Object(Map::from_iter([
("schema".to_string(), stable_value(schema)),
("defaults".to_string(), stable_value(&defaults)),
])))?),
};
Ok(UiArtifactBundle {
artifact_version: UI_ARTIFACT_BUNDLE_VERSION,
fingerprint,
tui: TuiArtifacts {
form_schema: form_schema_from_ui_ast(&ui.ui_ast),
layout_nav: LayoutNavModel::from_uilayout(&ui.layout),
},
ui,
})
}
pub fn build_ui_artifact_bundle_from_file(
schema_path: &Path,
schema_format: DocumentFormat,
defaults_path: Option<&Path>,
) -> Result<UiArtifactBundle> {
let schema = read_document_file(schema_path, schema_format)?;
let defaults = if let Some(path) = defaults_path {
let format = resolve_file_format(path, schema_format, "defaults file")?;
Some(read_document_file(path, format)?)
} else {
None
};
build_ui_artifact_bundle(&schema, defaults.as_ref())
}
pub fn build_ui_ast_from_file(path: &Path, format: DocumentFormat) -> Result<UiAst> {
Ok(build_ui_ast_bundle_from_file(path, format)?.ui_ast)
}
pub fn build_ui_ast_bundle_from_file(path: &Path, format: DocumentFormat) -> Result<UiAstBundle> {
let schema = read_document_file(path, format)?;
build_ui_ast_bundle(&schema)
}
pub fn ui_ast_to_json(ast: &UiAst) -> Result<String> {
Ok(serde_json::to_string_pretty(ast)?)
}
pub fn generate_ui_ast_rust_module(
schema_path: &Path,
format: DocumentFormat,
out_module_path: &Path,
const_name: &str,
) -> Result<()> {
let ast = build_ui_ast_from_file(schema_path, format)?;
let json = ui_ast_to_json(&ast)?;
let src = format!(
"pub const {name}: &str = r#\"{json}\"#;\n",
name = const_name,
json = json,
);
fs::write(out_module_path, src)?;
Ok(())
}
pub fn decode_ui_ast(json: &str) -> Result<UiAst> {
Ok(serde_json::from_str(json)?)
}
pub fn ui_ast_bundle_to_json(bundle: &UiAstBundle) -> Result<String> {
Ok(serde_json::to_string_pretty(bundle)?)
}
pub fn decode_ui_ast_bundle(json: &str) -> Result<UiAstBundle> {
Ok(serde_json::from_str(json)?)
}
pub fn ui_artifact_bundle_to_json(bundle: &UiArtifactBundle) -> Result<String> {
Ok(serde_json::to_string_pretty(bundle)?)
}
pub fn decode_ui_artifact_bundle(json: &str) -> Result<UiArtifactBundle> {
Ok(serde_json::from_str(json)?)
}
pub fn generate_ui_artifact_bundle_module(
schema_path: &Path,
format: DocumentFormat,
defaults_path: Option<&Path>,
out_module_path: &Path,
fn_name: &str,
) -> Result<()> {
let bundle = build_ui_artifact_bundle_from_file(schema_path, format, defaults_path)?;
let json = ui_artifact_bundle_to_json(&bundle)?;
let src = format!(
"pub fn {fn_name}() -> schemaui::precompile::UiArtifactBundle {{\n serde_json::from_str::<schemaui::precompile::UiArtifactBundle>(r#\"{json}\"#).expect(\"invalid UI artifact bundle JSON\")\n}}\n",
);
fs::write(out_module_path, src)?;
Ok(())
}
fn read_document_file(path: &Path, format: DocumentFormat) -> Result<Value> {
let contents = fs::read_to_string(path)?;
let schema: Value = parse_document_str(&contents, format)?;
Ok(schema)
}
pub(crate) fn resolve_file_format(
path: &Path,
fallback: DocumentFormat,
label: &str,
) -> Result<DocumentFormat> {
match DocumentFormat::probe_extension(path) {
DocumentFormatProbe::Known(format) => Ok(format),
DocumentFormatProbe::UnsupportedFeature {
format_name,
feature_flag,
} => Err(anyhow!(
"{label} {} requires {format_name} support, but this build lacks the '{feature_flag}' feature",
path.display()
)),
DocumentFormatProbe::Unknown => Ok(fallback),
}
}
fn stable_value_bytes(value: &Value) -> Result<Vec<u8>> {
Ok(serde_json::to_vec(&stable_value(value))?)
}
fn stable_value(value: &Value) -> Value {
match value {
Value::Object(map) => {
let mut entries = map.iter().collect::<Vec<_>>();
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
let mut normalized = Map::new();
for (key, value) in entries {
normalized.insert(key.clone(), stable_value(value));
}
Value::Object(normalized)
}
Value::Array(values) => Value::Array(values.iter().map(stable_value).collect()),
_ => value.clone(),
}
}
fn sha256_hex(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let mut out = String::with_capacity(digest.len() * 2);
for byte in digest {
use std::fmt::Write as _;
let _ = write!(&mut out, "{byte:02x}");
}
out
}