use std::io::Read;
use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
use scema_world::WorldState;
use crate::observer::Observer;
pub const MAX_IMPORT_BYTES: u64 = 16 * 1024 * 1024;
#[derive(Clone, Copy, Debug, Default)]
pub struct ImportObserver;
impl ImportObserver {
pub fn new() -> Self {
ImportObserver
}
pub fn from_json(text: &str, source: &str) -> Result<WorldState> {
let mut world: WorldState = serde_json::from_str(text).with_context(|| {
format!("{source} is not a scema-world WorldState (see scema-world's JSON shape)")
})?;
check(&world).with_context(|| format!("{source} parsed but is not internally consistent"))?;
world.observer = stamp(&world.observer);
Ok(world)
}
pub fn from_stdin() -> Result<WorldState> {
let mut text = String::new();
std::io::stdin()
.take(MAX_IMPORT_BYTES)
.read_to_string(&mut text)
.context("reading a world from stdin")?;
if text.trim().is_empty() {
bail!(
"nothing arrived on stdin. A producer that printed its help text or failed \
silently looks exactly like this — check its exit code."
);
}
ImportObserver::from_json(&text, "stdin")
}
pub fn from_file(path: &Path) -> Result<WorldState> {
let meta = std::fs::metadata(path)
.with_context(|| format!("reading {}", path.display()))?;
if meta.len() > MAX_IMPORT_BYTES {
bail!(
"{} is {} bytes, over the {MAX_IMPORT_BYTES}-byte import cap. A world is a \
description of an environment, not a dump of it.",
path.display(),
meta.len()
);
}
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading {}", path.display()))?;
ImportObserver::from_json(&text, &path.display().to_string())
}
}
fn stamp(observer: &str) -> String {
let name = observer.trim();
if name.is_empty() {
return "imported:unknown".to_string();
}
if name.starts_with("imported:") {
return name.to_string();
}
format!("imported:{name}")
}
fn check(w: &WorldState) -> Result<()> {
use crate::conform::{conform, has_failure, Level};
let findings = conform(w);
if !has_failure(&findings) {
return Ok(());
}
let mut msg = String::new();
for f in findings.iter().filter(|f| f.level == Level::Fail) {
msg.push_str("
- ");
msg.push_str(&f.message);
if let Some(fix) = &f.fix {
msg.push_str("
fix: ");
msg.push_str(fix);
}
}
msg.push_str("
`scema check <file>` prints the full report.");
bail!("{msg}");
}
impl Observer for ImportObserver {
fn name(&self) -> &str {
"import"
}
fn about(&self) -> &str {
"a WorldState produced elsewhere: `-` for stdin, or a path to a .json file"
}
fn handles(&self, locator: &str) -> bool {
let l = locator.trim();
l == "-" || l.eq_ignore_ascii_case("stdin") || l.to_ascii_lowercase().ends_with(".json")
}
fn observe(&self, locator: &str) -> Result<WorldState> {
let l = locator.trim();
if l == "-" || l.eq_ignore_ascii_case("stdin") {
return ImportObserver::from_stdin();
}
if !self.handles(l) {
return Err(anyhow!(
"`{l}` is not something this observer handles; it takes `-` or a path ending .json"
));
}
ImportObserver::from_file(Path::new(l))
}
}
#[cfg(test)]
mod tests {
use super::*;
use scema_world::{Domain, Entity, EntityKind, Extent, Polarity, Provenance, Signal};
use std::fs;
fn minimal() -> serde_json::Value {
serde_json::json!({
"schema": scema_world::WORLD_SCHEMA,
"observer": "mesh",
"entity": { "kind": "service", "locator": "/bot", "label": "bot" },
"domain": "trading",
"observed_at": 1_700_000_000i64,
"objects": [],
"facts": [],
"signals": [],
"extent": { "observed": 3, "total": 3, "note": "collected" },
"blind_spots": []
})
}
fn with_signals(signals: serde_json::Value) -> String {
let mut v = minimal();
v["signals"] = signals;
v.to_string()
}
#[test]
fn an_imported_world_can_never_claim_it_was_observed_here() {
let w = ImportObserver::from_json(&minimal().to_string(), "t").unwrap();
assert_eq!(w.observer, "imported:mesh");
}
#[test]
fn importing_twice_does_not_stack_prefixes() {
let mut v = minimal();
v["observer"] = serde_json::json!("imported:mesh");
let w = ImportObserver::from_json(&v.to_string(), "t").unwrap();
assert_eq!(w.observer, "imported:mesh");
}
#[test]
fn a_world_with_no_observer_name_is_attributed_to_nobody_rather_than_to_us() {
let mut v = minimal();
v["observer"] = serde_json::json!(" ");
let w = ImportObserver::from_json(&v.to_string(), "t").unwrap();
assert_eq!(w.observer, "imported:unknown");
}
#[test]
fn a_counted_signal_that_cites_nothing_is_refused() {
let text = with_signals(serde_json::json!([{
"id": "a", "polarity": "risk", "label": "x", "detail": "",
"magnitude": 0.5, "measured": true, "targets": [], "evidence": []
}]));
let err = ImportObserver::from_json(&text, "t").unwrap_err().to_string();
let chain = format!("{:#}", ImportObserver::from_json(&text, "t").unwrap_err());
assert!(chain.contains("cites no evidence"), "{err} / {chain}");
}
#[test]
fn an_estimated_signal_may_cite_nothing() {
let text = with_signals(serde_json::json!([{
"id": "a", "polarity": "risk", "label": "x", "detail": "",
"magnitude": 0.5, "measured": false, "targets": [], "evidence": []
}]));
assert!(ImportObserver::from_json(&text, "t").is_ok());
}
#[test]
fn a_magnitude_outside_the_unit_interval_is_refused_with_the_signal_named() {
for bad in [1.5, -0.2] {
let text = with_signals(serde_json::json!([{
"id": "loud", "polarity": "risk", "label": "x", "detail": "",
"magnitude": bad, "measured": true, "targets": [], "evidence": ["counted"]
}]));
let chain = format!("{:#}", ImportObserver::from_json(&text, "t").unwrap_err());
assert!(chain.contains("loud"), "{chain}");
assert!(chain.contains("outside [0,1]"), "{chain}");
}
}
#[test]
fn duplicate_signal_ids_are_refused_because_ground_could_not_name_one() {
let sig = |id: &str| {
serde_json::json!({
"id": id, "polarity": "risk", "label": "x", "detail": "",
"magnitude": 0.5, "measured": true, "targets": [], "evidence": ["counted"]
})
};
let text = with_signals(serde_json::json!([sig("a"), sig("a")]));
let chain = format!("{:#}", ImportObserver::from_json(&text, "t").unwrap_err());
assert!(chain.contains("share the id"), "{chain}");
}
#[test]
fn an_extent_whose_numerator_exceeds_its_denominator_is_refused() {
let mut v = minimal();
v["extent"] = serde_json::json!({ "observed": 9, "total": 3, "note": "?" });
let chain = format!("{:#}", ImportObserver::from_json(&v.to_string(), "t").unwrap_err());
assert!(chain.contains("not a smaller number"), "{chain}");
}
#[test]
fn an_unknown_denominator_is_accepted_and_is_the_correct_way_to_say_so() {
let mut v = minimal();
v["extent"] = serde_json::json!({ "observed": 9, "total": null, "note": "capped" });
let w = ImportObserver::from_json(&v.to_string(), "t").unwrap();
assert_eq!(w.extent.fraction(), None);
}
#[test]
fn an_entity_with_no_locator_is_refused() {
let mut v = minimal();
v["entity"]["locator"] = serde_json::json!("");
assert!(ImportObserver::from_json(&v.to_string(), "t").is_err());
}
#[test]
fn the_locator_grammar_is_narrow_so_repo_observer_still_wins_a_directory() {
let o = ImportObserver;
assert!(o.handles("-"));
assert!(o.handles("stdin"));
assert!(o.handles("mesh.json"));
assert!(o.handles("/tmp/World.JSON"));
assert!(!o.handles("."));
assert!(!o.handles("/some/project"));
assert!(!o.handles("crates/scema-tools"));
}
#[test]
fn a_file_that_is_not_json_says_what_it_should_have_been() {
let dir = std::env::temp_dir().join(format!("scema-import-{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let path = dir.join("bad.json");
fs::write(&path, "not json").unwrap();
let chain = format!("{:#}", ImportObserver.observe(path.to_str().unwrap()).unwrap_err());
assert!(chain.contains("WorldState"), "{chain}");
fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_real_world_round_trips_through_the_importer_unchanged_but_for_the_stamp() {
let original = WorldState {
schema: Some(scema_world::WORLD_SCHEMA.into()),
observer: "mesh".into(),
entity: Entity {
kind: EntityKind::Service,
locator: "/bot".into(),
label: "sniper".into(),
},
domain: Domain::Trading,
observed_at: 1_700_000_000,
objects: vec![],
facts: vec![],
signals: vec![Signal {
id: "veto:dqstar".into(),
polarity: Polarity::Risk,
label: "DQ* is suppressing buys".into(),
detail: String::new(),
magnitude: 0.8,
measured: true,
targets: vec!["learner.dqstar".into()],
evidence: vec!["counted 12 consecutive vetoes".into()],
}],
extent: Extent::complete(7, "collected"),
blind_spots: vec!["scematica-metrics.json: absent".into()],
};
let text = serde_json::to_string(&original).unwrap();
let back = ImportObserver::from_json(&text, "t").unwrap();
assert_eq!(back.observer, "imported:mesh");
assert_eq!(back.entity, original.entity);
assert_eq!(back.signals, original.signals);
assert_eq!(back.blind_spots, original.blind_spots);
assert_eq!(back.extent, original.extent);
}
fn fixture(name: &str) -> String {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("fixtures")
.join(name);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("reading {}: {e}", path.display()))
}
#[test]
fn every_producer_fixture_imports() {
for (file, observer) in [
("mesh-world.json", "imported:mesh"),
("alchem-world.json", "imported:alchem-link"),
("page-world.json", "imported:page"),
] {
let w = ImportObserver::from_json(&fixture(file), file)
.unwrap_or_else(|e| panic!("{file}: {e:#}"));
assert_eq!(w.observer, observer, "{file}");
assert!(!w.entity.locator.trim().is_empty(), "{file}");
}
}
#[test]
fn every_producer_reports_what_it_could_not_see() {
for file in ["mesh-world.json", "alchem-world.json", "page-world.json"] {
let w = ImportObserver::from_json(&fixture(file), file).unwrap();
assert!(
!w.blind_spots.is_empty(),
"{file} reports perfect visibility, which no real observation has"
);
}
}
#[test]
fn no_producer_claims_a_measurement_it_cannot_cite() {
for file in ["mesh-world.json", "alchem-world.json", "page-world.json"] {
let w = ImportObserver::from_json(&fixture(file), file).unwrap();
for s in &w.signals {
if s.measured {
assert!(!s.evidence.is_empty(), "{file}: `{}` cites nothing", s.id);
}
assert!((0.0..=1.0).contains(&s.magnitude), "{file}: `{}`", s.id);
}
}
}
#[test]
fn stale_and_absent_survive_the_wire() {
let mesh = ImportObserver::from_json(&fixture("mesh-world.json"), "mesh").unwrap();
assert!(
mesh.objects.iter().any(|o| matches!(o.provenance, Provenance::Stale { .. })),
"the mesh fixture should carry at least one stale unit"
);
assert!(
mesh.objects.iter().any(|o| matches!(o.provenance, Provenance::Absent)),
"the mesh fixture should carry at least one unseen unit"
);
let feeds = ImportObserver::from_json(&fixture("alchem-world.json"), "alchem").unwrap();
assert!(
feeds.objects.iter().any(|o| matches!(o.provenance, Provenance::Stale { .. })),
"the oracle fixture should carry a feed past its own heartbeat"
);
for o in feeds.objects.iter().filter(|o| o.provenance == Provenance::Absent) {
assert!(o.attrs.is_empty(), "an unread feed must carry no values: {}", o.id);
}
}
#[test]
fn a_perceived_page_carries_no_query_string() {
let w = ImportObserver::from_json(&fixture("page-world.json"), "page").unwrap();
assert!(!w.entity.locator.contains('?'), "{}", w.entity.locator);
assert!(!w.entity.locator.contains("SECRET"), "{}", w.entity.locator);
}
#[test]
fn the_domain_lets_a_specialist_decline_correctly() {
let mesh = ImportObserver::from_json(&fixture("mesh-world.json"), "mesh").unwrap();
assert_eq!(mesh.domain, scema_world::Domain::Trading);
let feeds = ImportObserver::from_json(&fixture("alchem-world.json"), "a").unwrap();
assert_eq!(feeds.domain, scema_world::Domain::Data);
let page = ImportObserver::from_json(&fixture("page-world.json"), "p").unwrap();
assert_eq!(page.domain, scema_world::Domain::Web);
assert_ne!(feeds.domain, page.domain, "two different worlds must not read alike");
}
#[test]
fn every_producer_declares_the_contract_it_was_written_against() {
for file in ["mesh-world.json", "alchem-world.json", "page-world.json"] {
let w = ImportObserver::from_json(&fixture(file), file).unwrap();
assert_eq!(w.schema.as_deref(), Some(scema_world::WORLD_SCHEMA), "{file}");
}
}
#[test]
fn an_undeclared_contract_is_refused_with_the_line_to_paste() {
let mut v: serde_json::Value =
serde_json::from_str(&fixture("mesh-world.json")).unwrap();
v.as_object_mut().unwrap().remove("schema");
let err = ImportObserver::from_json(&v.to_string(), "t").unwrap_err();
let chain = format!("{err:#}");
assert!(chain.contains("scema.world/1"), "{chain}");
assert!(chain.contains("scema check"), "{chain}");
}
#[test]
fn a_producer_with_several_problems_is_told_about_all_of_them() {
let mut v = minimal();
v.as_object_mut().unwrap().remove("schema");
v["entity"]["locator"] = serde_json::json!(" ");
v["signals"] = serde_json::json!([
{ "id": "dup", "polarity": "risk", "label": "l", "detail": "",
"magnitude": 0.5, "measured": true, "targets": [], "evidence": [] },
{ "id": "dup", "polarity": "risk", "label": "l", "detail": "",
"magnitude": 4.0, "measured": false, "targets": [], "evidence": ["e"] }
]);
let chain = format!("{:#}", ImportObserver::from_json(&v.to_string(), "t").unwrap_err());
for expected in ["schema", "locator", "cites no evidence", "share the id", "outside [0,1]"] {
assert!(chain.contains(expected), "missing `{expected}` in:
{chain}");
}
}
}