schemaui_cli/session/
builder.rs1use 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}