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