Skip to main content

schemaui_cli/session/
builder.rs

1use color_eyre::eyre::{Result, eyre};
2use schemaui::{DocumentFormat, schema_from_data_value};
3use serde_json::Value;
4
5use crate::cli::CommonArgs;
6use crate::io::load_value;
7
8use super::bundle::SessionBundle;
9use super::diagnostics::DiagnosticCollector;
10use super::format::resolve_format_hint;
11use super::output::{build_output_options, ensure_output_paths_available};
12
13pub fn prepare_session(args: &CommonArgs) -> Result<SessionBundle> {
14    let mut diagnostics = DiagnosticCollector::default();
15
16    let schema_spec = args.schema.as_deref();
17    let config_spec = args.config.as_deref();
18    let schema_stdin = schema_spec == Some("-");
19    let config_stdin = config_spec == Some("-");
20    if schema_stdin && config_stdin {
21        diagnostics.push_input(
22            "schema/config",
23            "cannot read schema and config from stdin simultaneously; provide inline content or files",
24        );
25    }
26
27    let schema_hint = resolve_format_hint(schema_spec, "schema", &mut diagnostics);
28    let config_hint = resolve_format_hint(config_spec, "config", &mut diagnostics);
29
30    let schema_value = load_optional_value(
31        schema_spec,
32        schema_hint.hint.format,
33        "schema",
34        schema_hint.blocked || (schema_stdin && config_stdin),
35        &mut diagnostics,
36    );
37    let config_value = load_optional_value(
38        config_spec,
39        config_hint.hint.format,
40        "config",
41        config_hint.blocked || (schema_stdin && config_stdin),
42        &mut diagnostics,
43    );
44
45    let (output_settings, output_paths) = build_output_options(
46        args,
47        config_hint.hint.extension_value(),
48        schema_hint.hint.extension_value(),
49        &mut diagnostics,
50    );
51    ensure_output_paths_available(&output_paths, args.force, &mut diagnostics);
52
53    diagnostics.into_result()?;
54
55    let mut schema_value = schema_value;
56    let mut config_value = config_value;
57    if schema_value.is_none()
58        && let Some(config_doc) = config_value.as_ref()
59        && looks_like_json_schema(config_doc)
60    {
61        eprintln!("detected JSON Schema provided via --config; treating it as the active schema");
62        schema_value = config_value.take();
63    }
64
65    if schema_value.is_none() && config_value.is_none() {
66        return Err(eyre!("provide at least --schema or --config"));
67    }
68
69    let schema = match (schema_value, config_value.as_ref()) {
70        (Some(schema), _) => schema,
71        (None, Some(defaults)) => schema_from_data_value(defaults),
72        (None, None) => unreachable!("validated above"),
73    };
74
75    Ok(SessionBundle {
76        schema,
77        defaults: config_value,
78        title: args.title.clone(),
79        output: output_settings,
80    })
81}
82
83fn load_optional_value(
84    spec: Option<&str>,
85    format: DocumentFormat,
86    label: &str,
87    skip: bool,
88    diagnostics: &mut DiagnosticCollector,
89) -> Option<Value> {
90    if skip {
91        return None;
92    }
93    let raw = spec?;
94    match load_value(raw, format, label) {
95        Ok(value) => Some(value),
96        Err(err) => {
97            diagnostics.push_input(label, err.to_string());
98            None
99        }
100    }
101}
102
103fn looks_like_json_schema(value: &Value) -> bool {
104    let obj = match value.as_object() {
105        Some(map) => map,
106        None => return false,
107    };
108
109    if obj
110        .get("properties")
111        .and_then(Value::as_object)
112        .map(|props| props.len())
113        .unwrap_or(0)
114        == 0
115    {
116        return false;
117    }
118
119    if obj.contains_key("$schema") {
120        return true;
121    }
122
123    if matches!(obj.get("type"), Some(Value::String(t)) if t == "object") {
124        return true;
125    }
126
127    if let Some(props) = obj.get("properties").and_then(Value::as_object) {
128        let mut scored = 0usize;
129
130        for value in props.values() {
131            if value.get("type").is_some() {
132                scored += 1;
133            }
134            if value.get("properties").is_some() {
135                scored += 1;
136            }
137            if value.get("enum").is_some() {
138                scored += 1;
139            }
140        }
141
142        return scored >= 2;
143    }
144
145    false
146}