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 "additionalProperties",
262 "minLength",
263 "maxLength",
264 "enum",
265 "maxItems",
266 "items",
267 "oneOf",
268 "const",
269 ],
270 RenderContext::Properties => return ordered_property_keys(object),
271 };
272 let mut keys = Vec::with_capacity(object.len());
273 for key in preferred {
274 if object.contains_key(*key) {
275 keys.push(*key);
276 }
277 }
278 let mut remaining: Vec<_> = object
279 .keys()
280 .map(String::as_str)
281 .filter(|key| !keys.contains(key))
282 .collect();
283 remaining.sort_unstable();
284 keys.extend(remaining);
285 keys
286}
287
288fn ordered_property_keys(object: &Map<String, Value>) -> Vec<&str> {
289 object.keys().map(String::as_str).collect()
290}
291
292fn render_properties(
293 value: &Value,
294 output: &mut String,
295 indent: usize,
296) -> Result<(), CodegenError> {
297 let Some(properties) = value.as_object() else {
298 return render_value(value, output, indent, RenderContext::Properties);
299 };
300 if properties.is_empty() {
301 output.push_str("{}");
302 return Ok(());
303 }
304 let keys: Vec<_> = properties.keys().map(String::as_str).collect();
305
306 output.push_str("{\n");
307 for (index, key) in keys.iter().enumerate() {
308 push_indent(output, indent + 1);
309 output.push_str(&serialize_scalar(&Value::String((*key).to_owned()))?);
310 output.push_str(": ");
311 render_value(&properties[*key], output, indent + 1, RenderContext::Schema)?;
312 if index + 1 != keys.len() {
313 output.push(',');
314 }
315 output.push('\n');
316 }
317 push_indent(output, indent);
318 output.push('}');
319 Ok(())
320}
321
322fn serialize_scalar(value: &Value) -> Result<String, CodegenError> {
323 serde_json::to_string(value).map_err(|error| CodegenError::InvalidDescriptor {
324 detail: format!("derived snapshot could not serialize: {error}"),
325 })
326}
327
328fn push_indent(output: &mut String, indent: usize) {
329 let _ = write!(output, "{:width$}", "", width = indent * 2);
330}