1use std::{collections::BTreeMap, fmt::Write as _, path::Path};
4
5use lenso_contract_authoring::CapabilitySnapshot;
6use serde_json::{Map, Value, json};
7
8use crate::{CodegenError, check_artifact, load_descriptor, write_artifact};
9
10pub fn write_source_snapshot(
12 snapshot: &CapabilitySnapshot,
13 descriptor_path: &Path,
14) -> Result<(), CodegenError> {
15 for (path, source) in snapshot_artifacts(snapshot, descriptor_path)? {
16 write_artifact(&path, &source)?;
17 }
18 load_descriptor(descriptor_path).map(|_| ())
19}
20
21pub fn check_source_snapshot(
23 snapshot: &CapabilitySnapshot,
24 descriptor_path: &Path,
25) -> Result<(), CodegenError> {
26 for (path, source) in snapshot_artifacts(snapshot, descriptor_path)? {
27 check_artifact(&path, &source)?;
28 }
29 load_descriptor(descriptor_path).map(|_| ())
30}
31
32fn snapshot_artifacts(
33 snapshot: &CapabilitySnapshot,
34 descriptor_path: &Path,
35) -> Result<BTreeMap<std::path::PathBuf, String>, CodegenError> {
36 if snapshot.operations.is_empty() {
37 return Err(CodegenError::InvalidDescriptor {
38 detail: "a source Capability must declare at least one Operation".to_owned(),
39 });
40 }
41 let root = descriptor_path.parent().unwrap_or_else(|| Path::new("."));
42 let mut artifacts = BTreeMap::new();
43 let mut operations = Vec::with_capacity(snapshot.operations.len());
44 for operation in &snapshot.operations {
45 let stem = schema_stem(&operation.name)?;
46 let (request_suffix, response_suffix) = match operation.interaction.as_str() {
47 "request" => ("request", "response"),
48 "stream" => ("open", "message"),
49 interaction => {
50 return Err(CodegenError::InvalidDescriptor {
51 detail: format!(
52 "source Operation `{}` has unsupported interaction `{interaction}`",
53 operation.name
54 ),
55 });
56 }
57 };
58 let request = format!("schemas/{stem}-{request_suffix}.schema.json");
59 let response = format!("schemas/{stem}-{response_suffix}.schema.json");
60 let domain_error = format!("schemas/{stem}-error.schema.json");
61 operations.push(json!({
62 "name": operation.name,
63 "interaction": operation.interaction,
64 "request_schema": request,
65 "response_schema": response,
66 "domain_error_schema": domain_error,
67 }));
68 artifacts.insert(root.join(&request), pretty_json(&operation.request_schema)?);
69 artifacts.insert(
70 root.join(&response),
71 pretty_json(&operation.response_schema)?,
72 );
73 artifacts.insert(
74 root.join(&domain_error),
75 pretty_json(&operation.domain_error_schema)?,
76 );
77 }
78 let descriptor = Value::Object(Map::from_iter([
79 (
80 "id".to_owned(),
81 Value::String(snapshot.capability_id.clone()),
82 ),
83 (
84 "version".to_owned(),
85 Value::String(snapshot.version.clone()),
86 ),
87 ("portable".to_owned(), Value::Bool(snapshot.portable)),
88 (
89 "cross_lane_transfer".to_owned(),
90 Value::Bool(snapshot.cross_lane_transfer),
91 ),
92 ("operations".to_owned(), Value::Array(operations)),
93 ]));
94 artifacts.insert(descriptor_path.to_path_buf(), pretty_json(&descriptor)?);
95 Ok(artifacts)
96}
97
98fn schema_stem(operation: &str) -> Result<String, CodegenError> {
99 if operation.is_empty()
100 || !operation
101 .chars()
102 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
103 {
104 return Err(CodegenError::InvalidDescriptor {
105 detail: format!("source Operation `{operation}` cannot form a Schema filename"),
106 });
107 }
108 Ok(operation.replace('_', "-"))
109}
110
111fn pretty_json(value: &Value) -> Result<String, CodegenError> {
112 let mut source = String::new();
113 render_value(value, &mut source, 0, RenderContext::Root)?;
114 source.push('\n');
115 Ok(source)
116}
117
118#[derive(Clone, Copy)]
119enum RenderContext {
120 Root,
121 Descriptor,
122 Operation,
123 Schema,
124 Properties,
125 RequiredArray,
126}
127
128fn render_value(
129 value: &Value,
130 output: &mut String,
131 indent: usize,
132 context: RenderContext,
133) -> Result<(), CodegenError> {
134 match value {
135 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
136 output.push_str(&serialize_scalar(value)?);
137 }
138 Value::Array(values) => render_array(values, output, indent, context)?,
139 Value::Object(object) => render_object(object, output, indent, context)?,
140 }
141 Ok(())
142}
143
144fn render_array(
145 values: &[Value],
146 output: &mut String,
147 indent: usize,
148 context: RenderContext,
149) -> Result<(), CodegenError> {
150 if values.is_empty() {
151 output.push_str("[]");
152 return Ok(());
153 }
154 if values.iter().all(Value::is_string)
155 && !(matches!(context, RenderContext::RequiredArray) && values.len() > 5)
156 {
157 output.push('[');
158 for (index, value) in values.iter().enumerate() {
159 if index > 0 {
160 output.push_str(", ");
161 }
162 output.push_str(&serialize_scalar(value)?);
163 }
164 output.push(']');
165 return Ok(());
166 }
167 output.push_str("[\n");
168 for (index, value) in values.iter().enumerate() {
169 push_indent(output, indent + 1);
170 let item_context = match context {
171 RenderContext::Descriptor => RenderContext::Operation,
172 _ => RenderContext::Schema,
173 };
174 render_value(value, output, indent + 1, item_context)?;
175 if index + 1 != values.len() {
176 output.push(',');
177 }
178 output.push('\n');
179 }
180 push_indent(output, indent);
181 output.push(']');
182 Ok(())
183}
184
185fn render_object(
186 object: &Map<String, Value>,
187 output: &mut String,
188 indent: usize,
189 context: RenderContext,
190) -> Result<(), CodegenError> {
191 if object.is_empty() {
192 output.push_str("{}");
193 return Ok(());
194 }
195 if object.len() == 1 && object.contains_key("const") {
196 output.push_str("{ \"const\": ");
197 render_value(&object["const"], output, indent, RenderContext::Schema)?;
198 output.push_str(" }");
199 return Ok(());
200 }
201
202 let object_context = match context {
203 RenderContext::Root if object.contains_key("id") && object.contains_key("operations") => {
204 RenderContext::Descriptor
205 }
206 RenderContext::Root => RenderContext::Schema,
207 other => other,
208 };
209 let keys = ordered_keys(object, object_context);
210 output.push_str("{\n");
211 for (index, key) in keys.iter().enumerate() {
212 push_indent(output, indent + 1);
213 output.push_str(&serialize_scalar(&Value::String((*key).to_owned()))?);
214 output.push_str(": ");
215 let child_context = if *key == "operations" {
216 RenderContext::Descriptor
217 } else if *key == "required" {
218 RenderContext::RequiredArray
219 } else {
220 RenderContext::Schema
221 };
222 if *key == "properties" {
223 render_properties(&object[*key], output, indent + 1)?;
224 } else {
225 render_value(&object[*key], output, indent + 1, child_context)?;
226 }
227 if index + 1 != keys.len() {
228 output.push(',');
229 }
230 output.push('\n');
231 }
232 push_indent(output, indent);
233 output.push('}');
234 Ok(())
235}
236
237fn ordered_keys(object: &Map<String, Value>, context: RenderContext) -> Vec<&str> {
238 let preferred: &[&str] = match context {
239 RenderContext::Descriptor => &[
240 "id",
241 "version",
242 "portable",
243 "cross_lane_transfer",
244 "operations",
245 ],
246 RenderContext::Operation => &[
247 "name",
248 "interaction",
249 "request_schema",
250 "response_schema",
251 "domain_error_schema",
252 ],
253 RenderContext::Schema | RenderContext::Root | RenderContext::RequiredArray => &[
254 "$schema",
255 "type",
256 "format",
257 "minimum",
258 "maximum",
259 "required",
260 "properties",
261 "propertyNames",
262 "additionalProperties",
263 "minLength",
264 "maxLength",
265 "enum",
266 "maxItems",
267 "items",
268 "oneOf",
269 "const",
270 ],
271 RenderContext::Properties => return ordered_property_keys(object),
272 };
273 let mut keys = Vec::with_capacity(object.len());
274 for key in preferred {
275 if object.contains_key(*key) {
276 keys.push(*key);
277 }
278 }
279 let mut remaining: Vec<_> = object
280 .keys()
281 .map(String::as_str)
282 .filter(|key| !keys.contains(key))
283 .collect();
284 remaining.sort_unstable();
285 keys.extend(remaining);
286 keys
287}
288
289fn ordered_property_keys(object: &Map<String, Value>) -> Vec<&str> {
290 object.keys().map(String::as_str).collect()
291}
292
293fn render_properties(
294 value: &Value,
295 output: &mut String,
296 indent: usize,
297) -> Result<(), CodegenError> {
298 let Some(properties) = value.as_object() else {
299 return render_value(value, output, indent, RenderContext::Properties);
300 };
301 if properties.is_empty() {
302 output.push_str("{}");
303 return Ok(());
304 }
305 let keys: Vec<_> = properties.keys().map(String::as_str).collect();
306
307 output.push_str("{\n");
308 for (index, key) in keys.iter().enumerate() {
309 push_indent(output, indent + 1);
310 output.push_str(&serialize_scalar(&Value::String((*key).to_owned()))?);
311 output.push_str(": ");
312 render_value(&properties[*key], output, indent + 1, RenderContext::Schema)?;
313 if index + 1 != keys.len() {
314 output.push(',');
315 }
316 output.push('\n');
317 }
318 push_indent(output, indent);
319 output.push('}');
320 Ok(())
321}
322
323fn serialize_scalar(value: &Value) -> Result<String, CodegenError> {
324 serde_json::to_string(value).map_err(|error| CodegenError::InvalidDescriptor {
325 detail: format!("derived snapshot could not serialize: {error}"),
326 })
327}
328
329fn push_indent(output: &mut String, indent: usize) {
330 let _ = write!(output, "{:width$}", "", width = indent * 2);
331}