Define the v0 publish manifest, parse it via std.json into a typed
struct, and validate it.
- src/manifest/manifest.sx: typed Manifest { app, version, channel,
artifacts: List(ManifestArtifact) } + ManifestArtifact { platform,
path, filename, content_type, metadata }. parse_manifest walks the
std.json Value tree into the struct, surfacing every malformed/
missing/wrong-type field as a distinct typed ManifestErr (BadJson,
WrongType, MissingField, UnknownPlatform, MissingArtifact, Io) — never
a silent default. Reuses P2.1 parse_platform for the platform id.
validate_manifest checks each artifact path exists on disk (fs.exists),
resolved relative to the manifest's directory. Strings are copied into
the caller's allocator (long-lived-container rule).
- examples/dist.json: representative valid manifest (android_apk + ios).
- examples/fixtures/: tiny stand-in artifact byte files referenced by it.
- tests/manifest_parse.sx: parses dist.json and asserts fields; asserts
the three failure classes surface distinct typed errors.
92 lines
4.2 KiB
Plaintext
92 lines
4.2 KiB
Plaintext
// Acceptance for P3.2 — the v0 publish manifest model + parse + validate.
|
|
//
|
|
// 1. Parses `examples/dist.json` into a typed `Manifest` and asserts the
|
|
// fields (app/version/channel, the artifact count, and each artifact's
|
|
// platform + path).
|
|
// 2. Asserts each failure class surfaces a DISTINCT machine-readable error
|
|
// (never a silent default):
|
|
// - a missing required field (a manifest lacking `version`) -> MissingField
|
|
// - an unknown platform ("psvita") -> UnknownPlatform
|
|
// - a non-existent artifact path -> MissingArtifact
|
|
//
|
|
// Prints a PASS/FAIL line per case and exits non-zero if any case behaves
|
|
// unexpectedly, so the test runner counts it as a failure.
|
|
#import "modules/std.sx";
|
|
#import "../src/domain/platform.sx";
|
|
#import "../src/manifest/manifest.sx";
|
|
|
|
// ── happy path: examples/dist.json -> typed Manifest ────────────────────
|
|
check_parse_valid :: (alloc: Allocator) -> bool {
|
|
m, e := load_manifest("examples/dist.json", alloc);
|
|
if e { return false; }
|
|
if m.app != "acme-app" { return false; }
|
|
if m.version != "1.2.3" { return false; }
|
|
if m.channel != "stable" { return false; }
|
|
if m.artifacts.len != 2 { return false; }
|
|
|
|
a0 := m.artifacts.items[0];
|
|
if a0.platform != .android_apk { return false; }
|
|
if a0.path != "fixtures/acme-1.2.3-android.apk" { return false; }
|
|
if a0.filename != "acme.apk" { return false; }
|
|
if a0.content_type != "application/vnd.android.package-archive" { return false; }
|
|
|
|
a1 := m.artifacts.items[1];
|
|
if a1.platform != .ios { return false; }
|
|
if a1.path != "fixtures/acme-1.2.3-ios.ipa" { return false; }
|
|
if a1.filename != "" { return false; } // optional override, absent -> ""
|
|
return true;
|
|
}
|
|
|
|
// ── failure: a required field (version) is absent -> MissingField ───────
|
|
check_missing_version :: (alloc: Allocator) -> bool {
|
|
src := "{\"app\":\"acme-app\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"ios\",\"path\":\"fixtures/acme-1.2.3-ios.ipa\"}]}";
|
|
_, e := parse_manifest(src, alloc);
|
|
return e == error.MissingField;
|
|
}
|
|
|
|
// ── failure: an artifact platform id is unknown -> UnknownPlatform ──────
|
|
check_unknown_platform :: (alloc: Allocator) -> bool {
|
|
src := "{\"app\":\"acme-app\",\"version\":\"1.2.3\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"psvita\",\"path\":\"fixtures/acme-1.2.3-ios.ipa\"}]}";
|
|
_, e := parse_manifest(src, alloc);
|
|
return e == error.UnknownPlatform;
|
|
}
|
|
|
|
// ── failure: an artifact path does not exist on disk -> MissingArtifact ─
|
|
// Parse SUCCEEDS (parse is filesystem-free); the missing file surfaces only
|
|
// at validate time, against the manifest's base directory.
|
|
check_missing_artifact_path :: (alloc: Allocator) -> bool {
|
|
src := "{\"app\":\"acme-app\",\"version\":\"1.2.3\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"ios\",\"path\":\"fixtures/does-not-exist.ipa\"}]}";
|
|
m, pe := parse_manifest(src, alloc);
|
|
if pe { return false; } // parse must not fail here
|
|
raised := false;
|
|
matched := false;
|
|
validate_manifest(m, "examples") catch err { raised = true; matched = (err == error.MissingArtifact); };
|
|
return raised and matched;
|
|
}
|
|
|
|
run_case :: (label: string, ok: bool) -> s32 {
|
|
if ok { print(" PASS {}\n", label); return 0; }
|
|
print(" FAIL {}\n", label);
|
|
return 1;
|
|
}
|
|
|
|
main :: () -> s32 {
|
|
gpa := GPA.init();
|
|
arena := Arena.init(xx gpa, 65536);
|
|
defer arena.deinit();
|
|
|
|
failures : s32 = 0;
|
|
failures += run_case("parse valid dist.json -> fields", check_parse_valid(xx arena));
|
|
failures += run_case("missing version -> MissingField", check_missing_version(xx arena));
|
|
failures += run_case("unknown platform 'psvita' -> UnknownPlatform", check_unknown_platform(xx arena));
|
|
failures += run_case("nonexistent path -> MissingArtifact", check_missing_artifact_path(xx arena));
|
|
|
|
print("------------------------------------------------\n");
|
|
if failures == 0 {
|
|
print("manifest_parse: ALL CASES PASS\n");
|
|
return 0;
|
|
}
|
|
print("manifest_parse: {} CASE(S) FAILED\n", failures);
|
|
return 1;
|
|
}
|