1use std::cmp::Ordering;
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11
12use serde_json::{Map, Value};
13
14use crate::network::{
15 Branch, BranchCharging, BranchRatingSet, Bus, BusId, BusType, Extras, GenCost, Generator, Hvdc,
16 Load, Network, Shunt, SourceFormat, TransformerControl, TransformerControlMode,
17};
18use crate::normalize;
19use crate::{Error, Result};
20
21const FMT: &str = "GO Challenge 3 JSON";
22
23#[derive(Debug)]
24struct Goc3BusMap {
25 by_uid: HashMap<String, BusId>,
26}
27
28impl Goc3BusMap {
29 fn get(&self, uid: &str) -> Result<BusId> {
30 self.by_uid
31 .get(uid)
32 .copied()
33 .ok_or_else(|| bad(format!("unknown bus uid `{uid}`")))
34 }
35}
36
37pub fn parse_goc3_json(content: &str) -> Result<super::Parsed> {
39 let mut warnings = Vec::new();
40 let network = parse_goc3_source(Arc::new(content.to_owned()), None, &mut warnings)?;
41 Ok(super::Parsed { network, warnings })
42}
43
44#[allow(clippy::too_many_lines)]
45pub(crate) fn parse_goc3_source(
46 source: Arc<String>,
47 name_hint: Option<&str>,
48 warnings: &mut Vec<String>,
49) -> Result<Network> {
50 let root: Value = serde_json::from_str(&source).map_err(|e| bad(e.to_string()))?;
51 let root = root
52 .as_object()
53 .ok_or_else(|| bad("top level is not a JSON object"))?;
54 let network = root
55 .get("network")
56 .and_then(Value::as_object)
57 .ok_or_else(|| bad("missing object `network`"))?;
58
59 let base_mva = network
60 .get("general")
61 .and_then(Value::as_object)
62 .and_then(|general| number(general, "base_norm_mva"))
63 .unwrap_or_else(|| {
64 push_once(
65 warnings,
66 "missing `network.general.base_norm_mva`; using 100.0 MVA",
67 );
68 100.0
69 });
70 if !base_mva.is_finite() || base_mva <= 0.0 {
71 return Err(Error::InvalidBaseMva { base: base_mva });
72 }
73
74 let name = root
75 .get("uid")
76 .and_then(Value::as_str)
77 .or_else(|| {
78 network
79 .get("general")
80 .and_then(Value::as_object)
81 .and_then(|general| general.get("uid"))
82 .and_then(Value::as_str)
83 })
84 .or(name_hint)
85 .unwrap_or("goc3")
86 .to_owned();
87
88 warn_static_reduction(root, network, warnings);
89
90 let (mut buses, bus_map) = read_buses(network)?;
91 let bus_pos: HashMap<BusId, usize> = buses
92 .iter()
93 .enumerate()
94 .map(|(index, bus)| (bus.id, index))
95 .collect();
96 let time_series = root.get("time_series_input").and_then(Value::as_object);
97 let device_ts = device_time_series(time_series)?;
98
99 let mut branches = Vec::new();
100 branches.extend(read_branches(network, "ac_line", false, &bus_map)?);
101 branches.extend(read_branches(
102 network,
103 "two_winding_transformer",
104 true,
105 &bus_map,
106 )?);
107
108 let shunts = read_shunts(network, base_mva, &bus_map)?;
109 let mut loads = Vec::new();
110 let mut generators = Vec::new();
111 let mut generator_buses = HashSet::new();
112 let mut reference_candidate: Option<(BusId, f64)> = None;
113
114 for device in device_rows(network)? {
115 let obj = device.obj;
116 let bus = bus_ref(obj, "bus", &bus_map)?;
117 let ts = device
118 .uid
119 .as_deref()
120 .and_then(|key| device_ts.get(key).copied());
121
122 match device.table {
123 DeviceTable::Generators => {
124 let generator = read_producer(obj, ts, bus, base_mva, device.uid.clone());
125 generator_buses.insert(bus);
126 if reference_candidate
127 .as_ref()
128 .is_none_or(|(_, pmax)| generator.pmax > *pmax)
129 {
130 reference_candidate = Some((bus, generator.pmax));
131 }
132 generators.push(generator);
133 }
134 DeviceTable::Loads => {
135 loads.push(read_consumer(obj, ts, bus, base_mva, device.uid.clone()));
136 }
137 }
138 }
139
140 assign_bus_types(
141 &mut buses,
142 &bus_pos,
143 &generator_buses,
144 reference_candidate,
145 warnings,
146 );
147
148 let hvdc = read_hvdc(network, base_mva, &bus_map)?;
149
150 let net = Network {
151 name,
152 base_mva,
153 base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
154 buses,
155 loads,
156 shunts,
157 branches,
158 switches: Vec::new(),
159 generators,
160 storage: Vec::new(),
161 hvdc,
162 transformers_3w: Vec::new(),
163 areas: Vec::new(),
164 solver: None,
165 source_format: SourceFormat::Goc3Json,
166 source: Some(source),
167 };
168 net.check_references(FMT)?;
169 Ok(net)
170}
171
172fn read_buses(network: &Map<String, Value>) -> Result<(Vec<Bus>, Goc3BusMap)> {
173 let items = section(network, "bus")?;
174 if items.is_empty() {
175 return Err(bad("missing non-empty `network.bus` section"));
176 }
177 let mut records = Vec::with_capacity(items.len());
178 let mut seen_uids = HashSet::new();
179 for item in items {
180 let obj = item_object(item, "bus")?;
181 let uid = item_uid(item, obj).ok_or_else(|| bad("bus record missing `uid`"))?;
182 if !seen_uids.insert(uid.clone()) {
183 return Err(bad(format!("duplicate bus uid `{uid}`")));
184 }
185 records.push((uid, obj));
186 }
187
188 let suffixes: Option<Vec<usize>> = records
189 .iter()
190 .map(|(uid, _)| official_bus_suffix(uid))
191 .collect();
192 let suffixes_unique = suffixes
193 .as_ref()
194 .is_some_and(|values| values.iter().copied().collect::<HashSet<_>>().len() == values.len());
195
196 let mut by_uid = HashMap::with_capacity(records.len());
197 let mut buses = Vec::with_capacity(records.len());
198 for (index, (uid, obj)) in records.into_iter().enumerate() {
199 let id = if suffixes_unique {
200 BusId(official_bus_suffix(&uid).expect("suffix checked above") + 1)
201 } else {
202 BusId(index + 1)
203 };
204 by_uid.insert(uid.clone(), id);
205 let initial = initial_status(obj);
206 buses.push(Bus {
207 id,
208 kind: BusType::Pq,
209 vm: initial.and_then(|s| number(s, "vm")).unwrap_or(1.0),
210 va: initial.and_then(|s| number(s, "va")).unwrap_or(0.0) * normalize::RAD_TO_DEG,
211 base_kv: number(obj, "base_nom_volt").unwrap_or(0.0),
212 vmax: number(obj, "vm_ub").unwrap_or(1.1),
213 vmin: number(obj, "vm_lb").unwrap_or(0.9),
214 evhi: None,
215 evlo: None,
216 area: 1,
217 zone: 1,
218 name: Some(uid.clone()),
219 uid: Some(uid),
220 extras: extras(
221 obj,
222 &["uid", "base_nom_volt", "vm_ub", "vm_lb", "initial_status"],
223 ),
224 });
225 }
226 Ok((buses, Goc3BusMap { by_uid }))
227}
228
229fn read_branches(
230 network: &Map<String, Value>,
231 section_name: &'static str,
232 transformer: bool,
233 buses: &Goc3BusMap,
234) -> Result<Vec<Branch>> {
235 section(network, section_name)?
236 .into_iter()
237 .map(|item| {
238 let obj = item_object(item, section_name)?;
239 let from = bus_ref(obj, "fr_bus", buses)?;
240 let to = bus_ref(obj, "to_bus", buses)?;
241 let initial = initial_status(obj);
242 let b = number(obj, "b").unwrap_or(0.0);
243 let rate_a = number(obj, "mva_ub_nom").unwrap_or(0.0);
244 let rate_b = number(obj, "mva_ub_em").unwrap_or(rate_a);
245 let charging = if number(obj, "additional_shunt").unwrap_or(0.0) == 0.0 {
248 BranchCharging::from_total_b(b)
249 } else {
250 BranchCharging {
251 g_fr: number(obj, "g_fr").unwrap_or(0.0),
252 b_fr: b / 2.0 + number(obj, "b_fr").unwrap_or(0.0),
253 g_to: number(obj, "g_to").unwrap_or(0.0),
254 b_to: b / 2.0 + number(obj, "b_to").unwrap_or(0.0),
255 }
256 };
257 let tap = if transformer {
258 initial
259 .and_then(|s| number(s, "tm"))
260 .or_else(|| equal_bounds(obj, "tm_lb", "tm_ub"))
261 .unwrap_or(1.0)
262 } else {
263 0.0
264 };
265 let shift = if transformer {
266 initial.and_then(|s| number(s, "ta")).unwrap_or(0.0) * normalize::RAD_TO_DEG
267 } else {
268 0.0
269 };
270 Ok(Branch {
271 from,
272 to,
273 r: number(obj, "r").unwrap_or(0.0),
274 x: number(obj, "x").unwrap_or(0.0),
275 b,
276 charging: Some(charging),
277 rate_a,
278 rate_b,
279 rate_c: rate_b,
280 rating_sets: (rate_b != 0.0 && (rate_b - rate_a).abs() > f64::EPSILON)
281 .then(|| BranchRatingSet::new("mva_ub_em", rate_b))
282 .into_iter()
283 .collect(),
284 current_ratings: None,
285 tap,
286 shift,
287 in_service: initial_status_flag(obj, true),
288 angmin: -360.0,
289 angmax: 360.0,
290 control: shifter_control(obj, transformer),
291 solution: None,
292 uid: item_uid(item, obj),
293 extras: extras(
294 obj,
295 &[
296 "uid",
297 "fr_bus",
298 "to_bus",
299 "r",
300 "x",
301 "b",
302 "mva_ub_nom",
303 "mva_ub_em",
304 "initial_status",
305 "additional_shunt",
306 "g_fr",
307 "g_to",
308 "b_fr",
309 "b_to",
310 "tm_lb",
311 "tm_ub",
312 "ta_lb",
313 "ta_ub",
314 ],
315 ),
316 })
317 })
318 .collect()
319}
320
321fn shifter_control(obj: &Map<String, Value>, transformer: bool) -> Option<TransformerControl> {
326 if !transformer {
327 return None;
328 }
329 let lb = number(obj, "ta_lb");
330 let ub = number(obj, "ta_ub");
331 if lb.is_none() && ub.is_none() {
332 return None;
333 }
334 let mut control = TransformerControl::new(TransformerControlMode::ActiveFlow);
335 control.tap_min = lb.unwrap_or(-std::f64::consts::TAU) * normalize::RAD_TO_DEG;
336 control.tap_max = ub.unwrap_or(std::f64::consts::TAU) * normalize::RAD_TO_DEG;
337 Some(control)
338}
339
340fn read_shunts(
341 network: &Map<String, Value>,
342 base_mva: f64,
343 buses: &Goc3BusMap,
344) -> Result<Vec<Shunt>> {
345 section(network, "shunt")?
346 .into_iter()
347 .map(|item| {
348 let obj = item_object(item, "shunt")?;
349 let step = initial_status(obj)
350 .and_then(|s| number(s, "step"))
351 .unwrap_or(1.0);
352 Ok(Shunt {
353 bus: bus_ref(obj, "bus", buses)?,
354 g: number(obj, "gs").unwrap_or(0.0) * step * base_mva,
355 b: number(obj, "bs").unwrap_or(0.0) * step * base_mva,
356 in_service: step != 0.0,
357 control: None,
358 uid: item_uid(item, obj),
359 extras: extras(
360 obj,
361 &[
362 "uid",
363 "bus",
364 "gs",
365 "bs",
366 "step_lb",
367 "step_ub",
368 "initial_status",
369 ],
370 ),
371 })
372 })
373 .collect()
374}
375
376fn read_producer(
377 obj: &Map<String, Value>,
378 ts: Option<&Value>,
379 bus: BusId,
380 base_mva: f64,
381 uid: Option<String>,
382) -> Generator {
383 let initial = initial_status(obj);
384 Generator {
385 bus,
386 pg: initial.and_then(|s| number(s, "p")).unwrap_or(0.0) * base_mva,
387 qg: initial.and_then(|s| number(s, "q")).unwrap_or(0.0) * base_mva,
388 pmax: first_number(ts, "p_ub").unwrap_or(0.0) * base_mva,
389 pmin: first_number(ts, "p_lb").unwrap_or(0.0) * base_mva,
390 qmax: first_number(ts, "q_ub").unwrap_or(0.0) * base_mva,
391 qmin: first_number(ts, "q_lb").unwrap_or(0.0) * base_mva,
392 vg: 1.0,
393 mbase: base_mva,
394 in_service: initial_status_flag(obj, true),
395 cost: cost_at(obj, ts, 0, base_mva),
396 caps: [None; crate::network::GEN_EXTRA_KEYS.len()],
397 regulated_bus: None,
398 uid,
399 }
400}
401
402fn read_consumer(
403 obj: &Map<String, Value>,
404 ts: Option<&Value>,
405 bus: BusId,
406 base_mva: f64,
407 uid: Option<String>,
408) -> Load {
409 let initial = initial_status(obj);
410 let p = initial
411 .and_then(|s| number(s, "p"))
412 .or_else(|| first_number(ts, "p_ub"))
413 .unwrap_or(0.0)
414 .abs()
415 * base_mva;
416 let q = initial
417 .and_then(|s| number(s, "q"))
418 .or_else(|| first_number(ts, "q_ub"))
419 .unwrap_or(0.0)
420 .abs()
421 * base_mva;
422 Load {
423 bus,
424 p,
425 q,
426 voltage_model: None,
427 in_service: initial_status_flag(obj, true),
428 uid,
429 extras: extras(
430 obj,
431 &[
432 "uid",
433 "bus",
434 "device_type",
435 "initial_status",
436 "startup_cost",
437 "shutdown_cost",
438 ],
439 ),
440 }
441}
442
443fn read_hvdc(network: &Map<String, Value>, base_mva: f64, buses: &Goc3BusMap) -> Result<Vec<Hvdc>> {
444 section(network, "dc_line")?
445 .into_iter()
446 .map(|item| {
447 let obj = item_object(item, "dc_line")?;
448 let initial = initial_status(obj);
449 let pdc = initial.and_then(|s| number(s, "pdc_fr")).unwrap_or(0.0) * base_mva;
450 Ok(Hvdc {
451 from: bus_ref(obj, "fr_bus", buses)?,
452 to: bus_ref(obj, "to_bus", buses)?,
453 in_service: initial_status_flag(obj, true),
454 pf: pdc,
455 pt: -pdc,
456 qf: initial.and_then(|s| number(s, "qdc_fr")).unwrap_or(0.0) * base_mva,
457 qt: initial.and_then(|s| number(s, "qdc_to")).unwrap_or(0.0) * base_mva,
458 vf: 1.0,
459 vt: 1.0,
460 pmin: -number(obj, "pdc_ub").unwrap_or(0.0) * base_mva,
461 pmax: number(obj, "pdc_ub").unwrap_or(0.0) * base_mva,
462 qminf: number(obj, "qdc_fr_lb").unwrap_or(0.0) * base_mva,
463 qmaxf: number(obj, "qdc_fr_ub").unwrap_or(0.0) * base_mva,
464 qmint: number(obj, "qdc_to_lb").unwrap_or(0.0) * base_mva,
465 qmaxt: number(obj, "qdc_to_ub").unwrap_or(0.0) * base_mva,
466 loss0: 0.0,
467 loss1: 0.0,
468 cost: None,
469 uid: item_uid(item, obj),
470 extras: extras(
471 obj,
472 &[
473 "uid",
474 "fr_bus",
475 "to_bus",
476 "pdc_ub",
477 "qdc_fr_lb",
478 "qdc_fr_ub",
479 "qdc_to_lb",
480 "qdc_to_ub",
481 "initial_status",
482 ],
483 ),
484 })
485 })
486 .collect()
487}
488
489fn assign_bus_types(
490 buses: &mut [Bus],
491 bus_pos: &HashMap<BusId, usize>,
492 generator_buses: &HashSet<BusId>,
493 reference_candidate: Option<(BusId, f64)>,
494 warnings: &mut Vec<String>,
495) {
496 for bus in generator_buses {
497 super::set_bus_kind(buses, bus_pos, *bus, BusType::Pv);
498 }
499 if let Some((bus, _)) = reference_candidate
500 && bus_pos.contains_key(&bus)
501 {
502 super::set_bus_kind(buses, bus_pos, bus, BusType::Ref);
503 warnings.push(format!(
504 "GO Challenge 3 has no explicit reference bus; selected bus {} from the largest producer pmax",
505 bus.0
506 ));
507 }
508}
509
510#[derive(Clone, Copy, Debug, PartialEq, Eq)]
512pub enum DeviceTable {
513 Generators,
514 Loads,
515}
516
517pub struct DeviceRow<'a> {
520 pub table: DeviceTable,
521 pub row: usize,
522 pub uid: Option<String>,
523 pub obj: &'a Map<String, Value>,
524}
525
526pub fn device_rows(network: &Map<String, Value>) -> Result<Vec<DeviceRow<'_>>> {
532 let mut rows = Vec::new();
533 let mut generators = 0usize;
534 let mut loads = 0usize;
535 for item in section(network, "simple_dispatchable_device")? {
536 let obj = item_object(item, "simple_dispatchable_device")?;
537 let uid = item_uid(item, obj);
538 let (table, row) = match string(obj, "device_type").unwrap_or("producer") {
539 "producer" => {
540 generators += 1;
541 (DeviceTable::Generators, generators - 1)
542 }
543 "consumer" => {
544 loads += 1;
545 (DeviceTable::Loads, loads - 1)
546 }
547 other => {
548 return Err(bad(format!(
549 "simple_dispatchable_device `{}` has unsupported `device_type` `{other}`",
550 uid.unwrap_or_else(|| "?".into())
551 )));
552 }
553 };
554 rows.push(DeviceRow {
555 table,
556 row,
557 uid,
558 obj,
559 });
560 }
561 Ok(rows)
562}
563
564pub fn cost_at(
569 obj: &Map<String, Value>,
570 ts: Option<&Value>,
571 index: usize,
572 base_mva: f64,
573) -> Option<GenCost> {
574 let periods = ts?.get("cost")?.as_array()?;
575 let curve = periods.get(index)?.as_array()?;
576 let mut coeffs = vec![0.0, 0.0];
577 let mut p = 0.0;
578 let mut y = 0.0;
579 for segment in curve {
580 let values = segment.as_array()?;
581 let marginal = values.first()?.as_f64()?;
582 let width = values.get(1)?.as_f64()?;
583 if !marginal.is_finite() || !width.is_finite() || width <= 0.0 {
584 continue;
585 }
586 p += width * base_mva;
587 y += marginal * width;
588 coeffs.push(p);
589 coeffs.push(y);
590 }
591 (coeffs.len() >= 4).then_some(GenCost {
592 model: 1,
593 startup: number(obj, "startup_cost").unwrap_or(0.0),
594 shutdown: number(obj, "shutdown_cost").unwrap_or(0.0),
595 ncost: coeffs.len() / 2,
596 coeffs,
597 })
598}
599
600fn device_time_series(time_series: Option<&Map<String, Value>>) -> Result<HashMap<String, &Value>> {
601 let Some(time_series) = time_series else {
602 return Ok(HashMap::new());
603 };
604 let mut out = HashMap::new();
605 for item in section(time_series, "simple_dispatchable_device")? {
606 if let Some(key) = item.key {
607 out.insert(key.to_owned(), item.value);
608 }
609 if let Some(obj) = item.value.as_object() {
610 if let Some(uid) = string(obj, "uid") {
611 out.insert(uid.to_owned(), item.value);
612 }
613 }
614 }
615 Ok(out)
616}
617
618fn warn_static_reduction(
619 root: &Map<String, Value>,
620 network: &Map<String, Value>,
621 warnings: &mut Vec<String>,
622) {
623 if root.get("time_series_input").is_some() {
624 warnings.push(
625 "time_series_input reduced to the first interval for static Network dispatch and limits"
626 .into(),
627 );
628 }
629 if root.get("reliability").is_some() {
630 warnings.push("reliability contingencies retained in source only".into());
631 }
632 for section in [
633 "active_zonal_reserve",
634 "reactive_zonal_reserve",
635 "violation_cost",
636 ] {
637 if network.get(section).is_some() {
638 warnings.push(format!("network.{section} retained in source only"));
639 }
640 }
641 if !section(network, "simple_dispatchable_device")
642 .unwrap_or_default()
643 .is_empty()
644 {
645 warnings.push(
646 "simple dispatchable device commitment, ramp, reserve, and multi-interval cost data retained in source only"
647 .into(),
648 );
649 }
650}
651
652#[derive(Clone, Copy)]
653pub struct SectionItem<'a> {
654 pub key: Option<&'a str>,
655 pub value: &'a Value,
656}
657
658pub fn section<'a>(
659 parent: &'a Map<String, Value>,
660 name: &'static str,
661) -> Result<Vec<SectionItem<'a>>> {
662 let Some(value) = parent.get(name) else {
663 return Ok(Vec::new());
664 };
665 match value {
666 Value::Array(items) => Ok(items
667 .iter()
668 .map(|value| SectionItem { key: None, value })
669 .collect()),
670 Value::Object(map) => {
671 let mut items: Vec<_> = map
672 .iter()
673 .map(|(key, value)| SectionItem {
674 key: Some(key.as_str()),
675 value,
676 })
677 .collect();
678 items.sort_by(|a, b| compare_keys(a.key.unwrap_or(""), b.key.unwrap_or("")));
679 Ok(items)
680 }
681 other => Err(bad(format!(
682 "`network.{name}` is not an array or object, got {}",
683 kind(other)
684 ))),
685 }
686}
687
688fn item_object<'a>(
689 item: SectionItem<'a>,
690 section_name: &'static str,
691) -> Result<&'a Map<String, Value>> {
692 item.value.as_object().ok_or_else(|| {
693 bad(format!(
694 "`network.{section_name}` record is not an object, got {}",
695 kind(item.value)
696 ))
697 })
698}
699
700pub fn item_uid(item: SectionItem<'_>, obj: &Map<String, Value>) -> Option<String> {
701 string(obj, "uid")
702 .map(str::to_owned)
703 .or_else(|| item.key.map(str::to_owned))
704 .filter(|uid| !uid.is_empty())
705}
706
707fn compare_keys(a: &str, b: &str) -> Ordering {
712 match (a.parse::<u64>(), b.parse::<u64>()) {
713 (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num).then_with(|| a.cmp(b)),
714 (Ok(_), Err(_)) => Ordering::Less,
715 (Err(_), Ok(_)) => Ordering::Greater,
716 (Err(_), Err(_)) => a.cmp(b),
717 }
718}
719
720fn bus_ref(obj: &Map<String, Value>, key: &'static str, buses: &Goc3BusMap) -> Result<BusId> {
721 let uid = string(obj, key).ok_or_else(|| bad(format!("missing string `{key}`")))?;
722 buses.get(uid)
723}
724
725fn official_bus_suffix(uid: &str) -> Option<usize> {
726 let rest = uid.strip_prefix("bus_")?;
727 (!rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()))
728 .then(|| rest.parse::<usize>().ok())
729 .flatten()
730}
731
732fn string<'a>(obj: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
733 obj.get(key).and_then(Value::as_str)
734}
735
736pub fn number(obj: &Map<String, Value>, key: &str) -> Option<f64> {
737 obj.get(key).and_then(Value::as_f64)
738}
739
740fn first_number(value: Option<&Value>, key: &str) -> Option<f64> {
741 value?.get(key)?.as_array()?.first().and_then(Value::as_f64)
742}
743
744fn initial_status(obj: &Map<String, Value>) -> Option<&Map<String, Value>> {
745 obj.get("initial_status").and_then(Value::as_object)
746}
747
748fn initial_status_flag(obj: &Map<String, Value>, default: bool) -> bool {
749 initial_status(obj)
750 .and_then(|status| number(status, "on_status"))
751 .map_or(default, |v| v != 0.0)
752}
753
754fn equal_bounds(obj: &Map<String, Value>, low: &str, high: &str) -> Option<f64> {
755 let lo = number(obj, low)?;
756 let hi = number(obj, high)?;
757 ((lo - hi).abs() <= f64::EPSILON).then_some(lo)
758}
759
760fn extras(obj: &Map<String, Value>, known: &[&str]) -> Extras {
761 obj.iter()
762 .filter(|(key, _)| !known.contains(&key.as_str()))
763 .map(|(key, value)| (key.clone(), value.clone()))
764 .collect()
765}
766
767fn push_once(warnings: &mut Vec<String>, warning: &str) {
768 if !warnings.iter().any(|w| w == warning) {
769 warnings.push(warning.to_owned());
770 }
771}
772
773fn kind(value: &Value) -> &'static str {
774 match value {
775 Value::Null => "null",
776 Value::Bool(_) => "bool",
777 Value::Number(_) => "number",
778 Value::String(_) => "string",
779 Value::Array(_) => "array",
780 Value::Object(_) => "object",
781 }
782}
783
784fn bad(message: impl Into<String>) -> Error {
785 Error::FormatRead {
786 format: FMT,
787 message: message.into(),
788 }
789}
790
791pub mod bridge {
796 pub use super::{
797 DeviceRow, DeviceTable, SectionItem, cost_at, device_rows, item_uid, number, section,
798 };
799}