Skip to main content

conjure_cp_cli/utils/
simplified_json.rs

1//! Conjure's simplified JSON format for parameters and solutions.
2//!
3//! Matches Conjure `--output-format=json`: integers and booleans are JSON scalars, sets and
4//! tuples are arrays, records are objects, and int-indexed matrices are objects whose keys are
5//! the printed index values. Non-int-indexed matrices encode as arrays.
6
7use std::collections::BTreeMap;
8use std::fmt::Write as _;
9
10use anyhow::{Context as _, anyhow, bail};
11use conjure_cp::Model;
12use conjure_cp::ast::{
13    AbstractLiteral, DeclarationPtr, DomainPtr, Field, GroundDomain, Literal, Moo, Name, Range,
14};
15use serde_json::{Map, Number, Value as JsonValue};
16
17use crate::utils::json::sort_json_object;
18
19/// Render solutions as a JSON array of assignment objects (Conjure `--solutions-in-one-file`).
20pub fn solutions_to_simplified_json(
21    solutions: &[BTreeMap<Name, Literal>],
22) -> anyhow::Result<JsonValue> {
23    let mut sorted = solutions.to_vec();
24    sorted.sort_by(solution_key_cmp);
25    let mut items = Vec::with_capacity(sorted.len());
26    for solution in &sorted {
27        items.push(solution_to_simplified_json(solution)?);
28    }
29    Ok(JsonValue::Array(items))
30}
31
32/// Pretty-print solutions in Conjure's simplified JSON layout.
33pub fn solutions_to_simplified_json_string(
34    solutions: &[BTreeMap<Name, Literal>],
35) -> anyhow::Result<String> {
36    let value = solutions_to_simplified_json(solutions)?;
37    Ok(format!("{}\n", serde_json::to_string_pretty(&value)?))
38}
39
40/// Parse a Conjure-style solutions JSON document (array, or a single object).
41pub fn solutions_from_simplified_json(
42    value: &JsonValue,
43    domains: &BTreeMap<Name, DomainPtr>,
44) -> anyhow::Result<Vec<BTreeMap<Name, Literal>>> {
45    match value {
46        JsonValue::Array(items) => items
47            .iter()
48            .map(|item| solution_from_simplified_json(item, domains))
49            .collect(),
50        JsonValue::Object(_) => Ok(vec![solution_from_simplified_json(value, domains)?]),
51        _ => bail!("expected a JSON array or object of solutions"),
52    }
53}
54
55/// Parse solutions JSON text.
56pub fn solutions_from_simplified_json_str(
57    text: &str,
58    domains: &BTreeMap<Name, DomainPtr>,
59) -> anyhow::Result<Vec<BTreeMap<Name, Literal>>> {
60    let value: JsonValue = serde_json::from_str(text).context("parsing solutions JSON")?;
61    solutions_from_simplified_json(&value, domains)
62}
63
64/// Render a single assignment as a simplified JSON object.
65pub fn solution_to_simplified_json(
66    solution: &BTreeMap<Name, Literal>,
67) -> anyhow::Result<JsonValue> {
68    let mut object = Map::new();
69    for (name, value) in solution {
70        object.insert(name.to_string(), literal_to_simplified_json(value)?);
71    }
72    Ok(sort_json_object(&JsonValue::Object(object), false))
73}
74
75/// Parse a single assignment object.
76pub fn solution_from_simplified_json(
77    value: &JsonValue,
78    domains: &BTreeMap<Name, DomainPtr>,
79) -> anyhow::Result<BTreeMap<Name, Literal>> {
80    let object = value
81        .as_object()
82        .ok_or_else(|| anyhow!("each solution must be a JSON object"))?;
83    let mut solution = BTreeMap::new();
84    for (name, json_value) in object {
85        let name = Name::user(name.as_str());
86        let domain = domains.get(&name);
87        let literal = literal_from_simplified_json(json_value, domain)
88            .with_context(|| format!("parsing value for `{name}`"))?;
89        solution.insert(name, literal);
90    }
91    Ok(solution)
92}
93
94/// Render a parameter file as a single simplified JSON object.
95pub fn params_to_simplified_json(params: &BTreeMap<Name, Literal>) -> anyhow::Result<JsonValue> {
96    solution_to_simplified_json(params)
97}
98
99/// Parse a parameter JSON object using given domains from the problem model.
100pub fn params_from_simplified_json(
101    value: &JsonValue,
102    given_domains: &BTreeMap<Name, DomainPtr>,
103) -> anyhow::Result<BTreeMap<Name, Literal>> {
104    solution_from_simplified_json(value, given_domains)
105}
106
107/// Parse parameter JSON text.
108pub fn params_from_simplified_json_str(
109    text: &str,
110    given_domains: &BTreeMap<Name, DomainPtr>,
111) -> anyhow::Result<BTreeMap<Name, Literal>> {
112    let value: JsonValue = serde_json::from_str(text).context("parsing parameter JSON")?;
113    params_from_simplified_json(&value, given_domains)
114}
115
116/// Encode a literal in Conjure's simplified JSON format.
117pub fn literal_to_simplified_json(literal: &Literal) -> anyhow::Result<JsonValue> {
118    match literal {
119        Literal::Bool(b) => Ok(JsonValue::Bool(*b)),
120        Literal::Int(i) => Ok(JsonValue::Number(Number::from(*i))),
121        Literal::AbstractLiteral(abs) => abstract_literal_to_simplified_json(abs),
122    }
123}
124
125fn abstract_literal_to_simplified_json(
126    abs: &AbstractLiteral<Literal>,
127) -> anyhow::Result<JsonValue> {
128    match abs {
129        AbstractLiteral::Set(elems) | AbstractLiteral::MSet(elems) => elems_as_json_array(elems),
130        AbstractLiteral::Tuple(elems) | AbstractLiteral::Sequence(elems) => {
131            elems_as_json_array(elems)
132        }
133        AbstractLiteral::Record(fields) => {
134            let mut object = Map::new();
135            for field in fields {
136                object.insert(
137                    field.name.to_string(),
138                    literal_to_simplified_json(&field.value)?,
139                );
140            }
141            Ok(JsonValue::Object(object))
142        }
143        AbstractLiteral::Variant(field) => {
144            let mut object = Map::new();
145            object.insert(
146                field.name.to_string(),
147                literal_to_simplified_json(&field.value)?,
148            );
149            Ok(JsonValue::Object(object))
150        }
151        AbstractLiteral::Matrix(elems, index_domain) => {
152            matrix_to_simplified_json(elems, index_domain.as_ref())
153        }
154        AbstractLiteral::Function(pairs) => function_to_simplified_json(pairs),
155        AbstractLiteral::Relation(rows) => {
156            let mut items = Vec::with_capacity(rows.len());
157            for row in rows {
158                items.push(elems_as_json_array(row)?);
159            }
160            Ok(JsonValue::Array(items))
161        }
162        AbstractLiteral::Partition(parts) => {
163            let mut items = Vec::with_capacity(parts.len());
164            for part in parts {
165                items.push(elems_as_json_array(part)?);
166            }
167            Ok(JsonValue::Array(items))
168        }
169        AbstractLiteral::Permutation(cycles) => {
170            let mut items = Vec::with_capacity(cycles.len());
171            for cycle in cycles {
172                items.push(elems_as_json_array(cycle)?);
173            }
174            Ok(JsonValue::Array(items))
175        }
176    }
177}
178
179fn matrix_to_simplified_json(
180    elems: &[Literal],
181    index_domain: &GroundDomain,
182) -> anyhow::Result<JsonValue> {
183    let GroundDomain::Int(_) = index_domain else {
184        return elems_as_json_array(elems);
185    };
186
187    let indices: Vec<Literal> = match index_domain.values() {
188        Ok(iter) => iter.collect(),
189        Err(_) => return elems_as_json_array(elems),
190    };
191
192    if indices.len() != elems.len() {
193        return elems_as_json_array(elems);
194    }
195
196    dictionary_from_pairs(indices.into_iter().zip(elems.iter().cloned()))
197}
198
199fn function_to_simplified_json(pairs: &[(Literal, Literal)]) -> anyhow::Result<JsonValue> {
200    dictionary_from_pairs(pairs.iter().cloned())
201}
202
203fn dictionary_from_pairs(
204    pairs: impl IntoIterator<Item = (Literal, Literal)>,
205) -> anyhow::Result<JsonValue> {
206    let mut object_entries = Vec::new();
207    let mut array_entries = Vec::new();
208    let mut all_keys_ok = true;
209
210    for (key, value) in pairs {
211        let key_json = literal_to_simplified_json(&key)?;
212        let value_json = literal_to_simplified_json(&value)?;
213        array_entries.push(JsonValue::Array(vec![key_json.clone(), value_json.clone()]));
214        match &key_json {
215            JsonValue::Bool(_) | JsonValue::Number(_) | JsonValue::String(_) => {
216                object_entries.push((json_key_string(&key_json)?, value_json));
217            }
218            _ => {
219                all_keys_ok = false;
220            }
221        }
222    }
223
224    if all_keys_ok {
225        Ok(JsonValue::Object(object_entries.into_iter().collect()))
226    } else {
227        Ok(JsonValue::Array(array_entries))
228    }
229}
230
231fn elems_as_json_array(elems: &[Literal]) -> anyhow::Result<JsonValue> {
232    let mut items = Vec::with_capacity(elems.len());
233    for elem in elems {
234        items.push(literal_to_simplified_json(elem)?);
235    }
236    Ok(JsonValue::Array(items))
237}
238
239fn json_key_string(value: &JsonValue) -> anyhow::Result<String> {
240    match value {
241        JsonValue::Bool(b) => Ok(b.to_string()),
242        JsonValue::Number(n) => Ok(n.to_string()),
243        JsonValue::String(s) => Ok(s.clone()),
244        _ => bail!("JSON object keys must be bool, number, or string"),
245    }
246}
247
248/// Decode a simplified JSON value, using `domain` to disambiguate arrays and objects.
249pub fn literal_from_simplified_json(
250    value: &JsonValue,
251    domain: Option<&DomainPtr>,
252) -> anyhow::Result<Literal> {
253    if let Some(domain) = domain {
254        if let Some(ground) = domain.as_ground() {
255            return literal_from_simplified_json_with_ground(value, ground);
256        }
257        // Domain lettings (e.g. `find x : R`) are unresolved until resolved through the symbol
258        // table; resolve them so records/sets/etc. are not misread as matrices.
259        if let Ok(ground) = domain.resolve() {
260            return literal_from_simplified_json_with_ground(value, ground.as_ref());
261        }
262    }
263    literal_from_simplified_json_unguided(value)
264}
265
266fn literal_from_simplified_json_with_ground(
267    value: &JsonValue,
268    domain: &GroundDomain,
269) -> anyhow::Result<Literal> {
270    match domain {
271        GroundDomain::Empty(_) => bail!("cannot parse a value for an empty domain"),
272        GroundDomain::Bool => match value {
273            JsonValue::Bool(b) => Ok(Literal::Bool(*b)),
274            JsonValue::Number(n) => match n.as_i64() {
275                Some(1) => Ok(Literal::Bool(true)),
276                Some(0) => Ok(Literal::Bool(false)),
277                _ => bail!("expected a boolean"),
278            },
279            _ => bail!("expected a boolean"),
280        },
281        GroundDomain::Int(_) => Ok(Literal::Int(json_to_i32(value)?)),
282        GroundDomain::Tuple(inners) => {
283            let JsonValue::Array(items) = value else {
284                bail!("expected a JSON array for a tuple");
285            };
286            if items.len() != inners.len() {
287                bail!(
288                    "tuple arity mismatch: expected {}, got {}",
289                    inners.len(),
290                    items.len()
291                );
292            }
293            let mut elems = Vec::with_capacity(items.len());
294            for (item, inner) in items.iter().zip(inners) {
295                elems.push(literal_from_simplified_json_with_ground(item, inner)?);
296            }
297            Ok(Literal::AbstractLiteral(AbstractLiteral::Tuple(elems)))
298        }
299        GroundDomain::Record(fields) => {
300            let JsonValue::Object(object) = value else {
301                bail!("expected a JSON object for a record");
302            };
303            let mut entries = Vec::with_capacity(fields.len());
304            for field in fields {
305                let key = field.name.to_string();
306                let Some(item) = object.get(&key) else {
307                    bail!("missing record field `{key}`");
308                };
309                entries.push(Field {
310                    name: field.name.clone(),
311                    value: literal_from_simplified_json_with_ground(item, field.value.as_ref())?,
312                });
313            }
314            Ok(Literal::AbstractLiteral(AbstractLiteral::Record(entries)))
315        }
316        GroundDomain::Variant(fields) => {
317            let JsonValue::Object(object) = value else {
318                bail!("expected a JSON object for a variant");
319            };
320            if object.len() != 1 {
321                bail!(
322                    "variant value must contain exactly one alternative, got {}",
323                    object.len()
324                );
325            }
326            let (key, item) = object.iter().next().expect("object has one entry");
327            let field = fields
328                .iter()
329                .find(|field| field.name.to_string() == *key)
330                .ok_or_else(|| anyhow!("unknown variant alternative `{key}`"))?;
331            Ok(Literal::AbstractLiteral(AbstractLiteral::Variant(
332                Moo::new(Field {
333                    name: field.name.clone(),
334                    value: literal_from_simplified_json_with_ground(item, field.value.as_ref())?,
335                }),
336            )))
337        }
338        GroundDomain::Matrix(inner, index_domains) => {
339            matrix_from_simplified_json(value, inner.as_ref(), index_domains)
340        }
341        GroundDomain::Sequence(_, inner) => match value {
342            JsonValue::Array(items) => {
343                let mut elems = Vec::with_capacity(items.len());
344                for item in items {
345                    elems.push(literal_from_simplified_json_with_ground(item, inner)?);
346                }
347                Ok(Literal::AbstractLiteral(AbstractLiteral::Sequence(elems)))
348            }
349            JsonValue::Object(object) => sequence_from_object(object, inner),
350            _ => bail!("expected a JSON array for a sequence"),
351        },
352        GroundDomain::Set(_, inner) => {
353            let JsonValue::Array(items) = value else {
354                bail!("expected a JSON array for a set");
355            };
356            let mut elems = Vec::with_capacity(items.len());
357            for item in items {
358                elems.push(literal_from_simplified_json_with_ground(item, inner)?);
359            }
360            Ok(Literal::AbstractLiteral(AbstractLiteral::Set(elems)))
361        }
362        GroundDomain::MSet(_, inner) => {
363            let JsonValue::Array(items) = value else {
364                bail!("expected a JSON array for a multiset");
365            };
366            let mut elems = Vec::with_capacity(items.len());
367            for item in items {
368                elems.push(literal_from_simplified_json_with_ground(item, inner)?);
369            }
370            Ok(Literal::AbstractLiteral(AbstractLiteral::MSet(elems)))
371        }
372        GroundDomain::Function(_, from, to) => {
373            function_from_simplified_json(value, from.as_ref(), to.as_ref())
374        }
375        GroundDomain::Relation(_, inner_doms) => relation_from_simplified_json(value, inner_doms),
376        GroundDomain::Partition(_, inner) => partition_from_simplified_json(value, inner),
377        GroundDomain::Permutation(_, inner) => permutation_from_simplified_json(value, inner),
378    }
379}
380
381fn partition_from_simplified_json(
382    value: &JsonValue,
383    inner: &GroundDomain,
384) -> anyhow::Result<Literal> {
385    let JsonValue::Array(items) = value else {
386        bail!("expected a JSON array for a partition");
387    };
388    let mut parts = Vec::with_capacity(items.len());
389    for item in items {
390        let JsonValue::Array(elems) = item else {
391            bail!("expected a JSON array for a partition part");
392        };
393        let mut part = Vec::with_capacity(elems.len());
394        for elem in elems {
395            part.push(literal_from_simplified_json_with_ground(elem, inner)?);
396        }
397        parts.push(part);
398    }
399    Ok(Literal::AbstractLiteral(AbstractLiteral::Partition(parts)))
400}
401
402fn permutation_from_simplified_json(
403    value: &JsonValue,
404    inner: &GroundDomain,
405) -> anyhow::Result<Literal> {
406    let JsonValue::Array(items) = value else {
407        bail!("expected a JSON array for a permutation");
408    };
409    let mut cycles = Vec::with_capacity(items.len());
410    for item in items {
411        let JsonValue::Array(elems) = item else {
412            bail!("expected a JSON array for a permutation cycle");
413        };
414        let mut cycle = Vec::with_capacity(elems.len());
415        for elem in elems {
416            cycle.push(literal_from_simplified_json_with_ground(elem, inner)?);
417        }
418        cycles.push(cycle);
419    }
420    Ok(Literal::AbstractLiteral(AbstractLiteral::Permutation(
421        cycles,
422    )))
423}
424
425fn relation_from_simplified_json(
426    value: &JsonValue,
427    inner_doms: &[Moo<GroundDomain>],
428) -> anyhow::Result<Literal> {
429    let JsonValue::Array(items) = value else {
430        bail!("expected a JSON array for a relation");
431    };
432    let mut tuples = Vec::with_capacity(items.len());
433    for item in items {
434        let JsonValue::Array(fields) = item else {
435            bail!("expected a JSON array for a relation tuple");
436        };
437        if fields.len() != inner_doms.len() {
438            bail!(
439                "relation tuple arity mismatch: expected {}, got {}",
440                inner_doms.len(),
441                fields.len()
442            );
443        }
444        let mut tuple = Vec::with_capacity(fields.len());
445        for (field, dom) in fields.iter().zip(inner_doms) {
446            tuple.push(literal_from_simplified_json_with_ground(field, dom)?);
447        }
448        tuples.push(tuple);
449    }
450    Ok(Literal::AbstractLiteral(AbstractLiteral::Relation(tuples)))
451}
452
453fn matrix_from_simplified_json(
454    value: &JsonValue,
455    inner: &GroundDomain,
456    index_domains: &[Moo<GroundDomain>],
457) -> anyhow::Result<Literal> {
458    let (first_index, rest) = index_domains
459        .split_first()
460        .ok_or_else(|| anyhow!("matrix domain has no index domains"))?;
461
462    let elem_domain: GroundDomain = if rest.is_empty() {
463        inner.clone()
464    } else {
465        GroundDomain::Matrix(Moo::new(inner.clone()), rest.to_vec())
466    };
467
468    match value {
469        JsonValue::Object(object) => {
470            let mut pairs = Vec::with_capacity(object.len());
471            for (key, item) in object {
472                let index = index_key_to_literal(key, first_index)?;
473                let elem = literal_from_simplified_json_with_ground(item, &elem_domain)?;
474                pairs.push((index, elem));
475            }
476            pairs.sort_by(|a, b| a.0.essence_cmp(&b.0));
477            let keys: Vec<i32> = object
478                .keys()
479                .map(|k| k.parse::<i32>())
480                .collect::<Result<Vec<_>, _>>()
481                .unwrap_or_default();
482            let elems: Vec<Literal> = pairs.into_iter().map(|(_, v)| v).collect();
483            let index_domain = if let Ok(expected) = first_index.values() {
484                let expected: Vec<_> = expected.collect();
485                if expected.len() == elems.len() {
486                    first_index.as_ref().clone()
487                } else {
488                    infer_int_index_domain(&keys)
489                }
490            } else {
491                infer_int_index_domain(&keys)
492            };
493            Ok(Literal::AbstractLiteral(AbstractLiteral::Matrix(
494                elems,
495                index_domain.into(),
496            )))
497        }
498        JsonValue::Array(items) => {
499            let mut elems = Vec::with_capacity(items.len());
500            for item in items {
501                elems.push(literal_from_simplified_json_with_ground(
502                    item,
503                    &elem_domain,
504                )?);
505            }
506            let n = i32::try_from(elems.len()).context("matrix too large")?;
507            let index_domain = GroundDomain::Int(vec![Range::Bounded(1, n)]);
508            Ok(Literal::AbstractLiteral(AbstractLiteral::Matrix(
509                elems,
510                index_domain.into(),
511            )))
512        }
513        _ => bail!("expected a JSON object or array for a matrix"),
514    }
515}
516
517fn infer_int_index_domain(keys: &[i32]) -> GroundDomain {
518    if keys.is_empty() {
519        return GroundDomain::Int(vec![]);
520    }
521    let mut ints = keys.to_vec();
522    ints.sort_unstable();
523    let min = ints[0];
524    let max = *ints.last().expect("non-empty");
525    if max - min + 1 == ints.len() as i32 {
526        GroundDomain::Int(vec![Range::Bounded(min, max)])
527    } else {
528        GroundDomain::Int(ints.into_iter().map(Range::Single).collect())
529    }
530}
531
532fn index_key_to_literal(key: &str, index_domain: &GroundDomain) -> anyhow::Result<Literal> {
533    match index_domain {
534        GroundDomain::Bool => match key {
535            "false" => Ok(Literal::Bool(false)),
536            "true" => Ok(Literal::Bool(true)),
537            _ => bail!("expected boolean matrix index key"),
538        },
539        _ => Ok(Literal::Int(key.parse::<i32>().with_context(|| {
540            format!("expected integer matrix index key, got `{key}`")
541        })?)),
542    }
543}
544
545fn sequence_from_object(
546    object: &Map<String, JsonValue>,
547    inner: &GroundDomain,
548) -> anyhow::Result<Literal> {
549    let mut pairs = Vec::with_capacity(object.len());
550    for (key, item) in object {
551        let index: i32 = key
552            .parse()
553            .with_context(|| format!("sequence index `{key}` is not an integer"))?;
554        pairs.push((
555            index,
556            literal_from_simplified_json_with_ground(item, inner)?,
557        ));
558    }
559    pairs.sort_by_key(|(i, _)| *i);
560    Ok(Literal::AbstractLiteral(AbstractLiteral::Sequence(
561        pairs.into_iter().map(|(_, v)| v).collect(),
562    )))
563}
564
565fn function_from_simplified_json(
566    value: &JsonValue,
567    from: &GroundDomain,
568    to: &GroundDomain,
569) -> anyhow::Result<Literal> {
570    match value {
571        JsonValue::Object(object) => {
572            let mut pairs = Vec::with_capacity(object.len());
573            for (key, item) in object {
574                let domain_key = index_key_to_literal(key, from)?;
575                let mapped = literal_from_simplified_json_with_ground(item, to)?;
576                pairs.push((domain_key, mapped));
577            }
578            Ok(Literal::AbstractLiteral(AbstractLiteral::Function(pairs)))
579        }
580        JsonValue::Array(items) => {
581            let mut pairs = Vec::with_capacity(items.len());
582            for item in items {
583                let JsonValue::Array(pair) = item else {
584                    bail!("function array entries must be [from, to] pairs");
585                };
586                if pair.len() != 2 {
587                    bail!("function array entries must be [from, to] pairs");
588                }
589                pairs.push((
590                    literal_from_simplified_json_with_ground(&pair[0], from)?,
591                    literal_from_simplified_json_with_ground(&pair[1], to)?,
592                ));
593            }
594            Ok(Literal::AbstractLiteral(AbstractLiteral::Function(pairs)))
595        }
596        _ => bail!("expected a JSON object or array for a function"),
597    }
598}
599
600fn literal_from_simplified_json_unguided(value: &JsonValue) -> anyhow::Result<Literal> {
601    match value {
602        JsonValue::Bool(b) => Ok(Literal::Bool(*b)),
603        JsonValue::Number(_) | JsonValue::String(_) => Ok(Literal::Int(json_to_i32(value)?)),
604        JsonValue::Array(items) => {
605            let mut elems = Vec::with_capacity(items.len());
606            for item in items {
607                elems.push(literal_from_simplified_json_unguided(item)?);
608            }
609            Ok(Literal::AbstractLiteral(AbstractLiteral::Set(elems)))
610        }
611        JsonValue::Object(object) => {
612            let all_int_keys = object.keys().all(|key| key.parse::<i32>().is_ok());
613            if all_int_keys {
614                let mut pairs = Vec::with_capacity(object.len());
615                for (key, item) in object {
616                    let index: i32 = key.parse().with_context(|| {
617                        format!("unguided object key `{key}` is not an integer")
618                    })?;
619                    pairs.push((index, literal_from_simplified_json_unguided(item)?));
620                }
621                pairs.sort_by_key(|(i, _)| *i);
622                let keys: Vec<i32> = pairs.iter().map(|(i, _)| *i).collect();
623                let elems: Vec<_> = pairs.into_iter().map(|(_, v)| v).collect();
624                return Ok(Literal::AbstractLiteral(AbstractLiteral::Matrix(
625                    elems,
626                    infer_int_index_domain(&keys).into(),
627                )));
628            }
629
630            // Non-integer keys: treat as a record (Conjure simplified JSON).
631            let mut entries = Vec::with_capacity(object.len());
632            for (key, item) in object {
633                entries.push(Field {
634                    name: Name::user(key.as_str()),
635                    value: literal_from_simplified_json_unguided(item)?,
636                });
637            }
638            entries.sort_by(|a, b| a.name.cmp(&b.name));
639            Ok(Literal::AbstractLiteral(AbstractLiteral::Record(entries)))
640        }
641        JsonValue::Null => bail!("null is not a valid Essence literal"),
642    }
643}
644
645fn json_to_i32(value: &JsonValue) -> anyhow::Result<i32> {
646    match value {
647        JsonValue::Number(n) => n
648            .as_i64()
649            .and_then(|i| i32::try_from(i).ok())
650            .ok_or_else(|| anyhow!("expected a 32-bit integer")),
651        JsonValue::String(s) => s
652            .parse::<i32>()
653            .with_context(|| format!("expected an integer string, got `{s}`")),
654        _ => bail!("expected an integer"),
655    }
656}
657
658fn solution_key_cmp(
659    lhs: &BTreeMap<Name, Literal>,
660    rhs: &BTreeMap<Name, Literal>,
661) -> std::cmp::Ordering {
662    lhs.iter()
663        .zip(rhs)
664        .find_map(|((lhs_name, lhs_value), (rhs_name, rhs_value))| {
665            let ordering = lhs_name.cmp(rhs_name);
666            (ordering != std::cmp::Ordering::Equal)
667                .then_some(ordering)
668                .or_else(|| {
669                    let ordering = lhs_value.essence_cmp(rhs_value);
670                    (ordering != std::cmp::Ordering::Equal).then_some(ordering)
671                })
672        })
673        .unwrap_or_else(|| lhs.len().cmp(&rhs.len()))
674}
675
676/// Collect find/given domains from a model for JSON parsing guidance.
677pub fn domains_from_model(model: &Model) -> BTreeMap<Name, DomainPtr> {
678    let mut domains = BTreeMap::new();
679    for (name, decl) in model.symbols().clone().into_iter() {
680        if let Some(domain) = decl.domain() {
681            domains.insert(name, domain);
682        }
683    }
684    domains
685}
686
687/// Build a parameter model of value-lettings from simplified JSON assignments.
688pub fn param_model_from_assignments(
689    params: BTreeMap<Name, Literal>,
690    given_domains: &BTreeMap<Name, DomainPtr>,
691    context: std::sync::Arc<std::sync::RwLock<conjure_cp::context::Context<'static>>>,
692) -> Model {
693    let mut model = Model::new(context);
694    for (name, literal) in params {
695        let decl = match given_domains.get(&name) {
696            Some(domain) => {
697                DeclarationPtr::new_value_letting_with_domain(name, literal.into(), domain.clone())
698            }
699            None => DeclarationPtr::new_value_letting(name, literal.into()),
700        };
701        model.symbols_mut().update_insert(decl);
702    }
703    model
704}
705
706/// Render pretty JSON with stable key order for golden comparisons.
707pub fn canonical_simplified_json_string(value: &JsonValue) -> anyhow::Result<String> {
708    let sorted = sort_json_object(value, true);
709    Ok(format!("{}\n", serde_json::to_string_pretty(&sorted)?))
710}
711
712/// Append pretty JSON to a string buffer.
713pub fn write_simplified_json(value: &JsonValue, out: &mut String) -> anyhow::Result<()> {
714    write!(out, "{}", serde_json::to_string_pretty(value)?)?;
715    out.push('\n');
716    Ok(())
717}
718
719#[cfg(test)]
720mod tests {
721    use super::*;
722    use conjure_cp::ast::{Domain, SetAttr};
723
724    fn int_dom(lo: i32, hi: i32) -> DomainPtr {
725        Domain::int(vec![Range::Bounded(lo, hi)])
726    }
727
728    #[test]
729    fn round_trips_scalars_and_set() {
730        let mut domains = BTreeMap::new();
731        domains.insert(Name::user("x"), int_dom(1, 3));
732        domains.insert(
733            Name::user("s"),
734            Domain::set(SetAttr::<i32>::default(), int_dom(1, 3)),
735        );
736
737        let json = serde_json::json!([{"s": [1, 3], "x": 2}]);
738        let solutions = solutions_from_simplified_json(&json, &domains).unwrap();
739        assert_eq!(solutions.len(), 1);
740        assert_eq!(solutions[0].get(&Name::user("x")), Some(&Literal::Int(2)));
741        let Literal::AbstractLiteral(AbstractLiteral::Set(elems)) =
742            solutions[0].get(&Name::user("s")).unwrap()
743        else {
744            panic!("expected set");
745        };
746        assert_eq!(elems, &vec![Literal::Int(1), Literal::Int(3)]);
747
748        let rendered = solutions_to_simplified_json(&solutions).unwrap();
749        let again = solutions_from_simplified_json(&rendered, &domains).unwrap();
750        assert_eq!(again, solutions);
751    }
752
753    #[test]
754    fn round_trips_int_indexed_matrix() {
755        let matrix_dom = Domain::matrix(int_dom(1, 3), vec![int_dom(1, 2)]);
756        let mut domains = BTreeMap::new();
757        domains.insert(Name::user("m"), matrix_dom);
758
759        let json = serde_json::json!([{"m": {"1": 1, "2": 2}}]);
760        let solutions = solutions_from_simplified_json(&json, &domains).unwrap();
761        let rendered = solutions_to_simplified_json(&solutions).unwrap();
762        let again = solutions_from_simplified_json(&rendered, &domains).unwrap();
763        assert_eq!(again, solutions);
764    }
765
766    #[test]
767    fn guided_variant_object_parses_as_variant() {
768        let mut domains = BTreeMap::new();
769        domains.insert(
770            Name::user("x"),
771            Domain::variant(vec![
772                Field {
773                    name: Name::user("flag"),
774                    value: Domain::bool(),
775                },
776                Field {
777                    name: Name::user("value"),
778                    value: int_dom(1, 3),
779                },
780            ]),
781        );
782
783        let json = serde_json::json!([{"x": {"value": 2}}]);
784        let solutions = solutions_from_simplified_json(&json, &domains).unwrap();
785        let expected = Literal::AbstractLiteral(AbstractLiteral::Variant(Moo::new(Field {
786            name: Name::user("value"),
787            value: Literal::Int(2),
788        })));
789        assert_eq!(solutions[0].get(&Name::user("x")), Some(&expected));
790
791        let rendered = solutions_to_simplified_json(&solutions).unwrap();
792        assert_eq!(
793            solutions_from_simplified_json(&rendered, &domains).unwrap(),
794            solutions
795        );
796    }
797
798    #[test]
799    fn unguided_object_with_named_fields_parses_as_record() {
800        let json = serde_json::json!({"a": true, "b": 3});
801        let literal = literal_from_simplified_json(&json, None).unwrap();
802        let Literal::AbstractLiteral(AbstractLiteral::Record(fields)) = literal else {
803            panic!("expected record, got {literal:?}");
804        };
805        assert_eq!(fields.len(), 2);
806        assert_eq!(fields[0].name, Name::user("a"));
807        assert_eq!(fields[0].value, Literal::Bool(true));
808        assert_eq!(fields[1].name, Name::user("b"));
809        assert_eq!(fields[1].value, Literal::Int(3));
810    }
811}