diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..1ebfac7 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,24 @@ +name: Publish + +on: + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: systemic-engineering/ci/actions/nix-setup@main + + - name: Download deps + run: nix develop -c gleam deps download + + - name: Test + run: nix develop -c gleam test + + - name: Publish to Hex.pm + run: nix develop -c gleam hex publish --yes + env: + HEXPM_API_KEY: ${{ secrets.HEX_API_KEY }} diff --git a/gleam.toml b/gleam.toml index f51bbe2..bf2c597 100644 --- a/gleam.toml +++ b/gleam.toml @@ -7,6 +7,7 @@ repository = { type = "github", user = "systemic-engineering", repo = "fragmenta [dependencies] gleam_stdlib = ">= 0.44.0 and < 2.0.0" +simplifile = ">= 2.0.0 and < 3.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index ba887ee..91354dc 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,10 +2,13 @@ # You typically do not need to edit this file packages = [ + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, { name = "gleam_stdlib", version = "0.69.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "AAB0962BEBFAA67A2FBEE9EEE218B057756808DC9AF77430F5182C6115B3A315" }, { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, + { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" }, ] [requirements] gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +simplifile = { version = ">= 2.0.0 and < 3.0.0" } diff --git a/src/fragmentation/git.gleam b/src/fragmentation/git.gleam new file mode 100644 index 0000000..c6d2c28 --- /dev/null +++ b/src/fragmentation/git.gleam @@ -0,0 +1,28 @@ +/// git: content-addressed fragment persistence. +/// +/// Writes fragments to disk named by their SHA. +/// The store is a directory. Each fragment becomes a file. +/// File name = SHA-256 of canonical serialization. +/// Idempotent: same SHA, same content, same file. +import fragmentation +import simplifile + +// --------------------------------------------------------------------------- +// Operations +// --------------------------------------------------------------------------- + +/// Write a fragment to disk under `dir`, named by its content-addressed SHA. +/// +/// Computes the SHA via `fragmentation.hash_fragment`, serializes via +/// `fragmentation.serialize`, then writes to `/`. +/// Returns Ok(Nil) on success, Error(FileError) on failure. +/// Idempotent: writing the same fragment twice produces the same file. +pub fn write( + fragment: fragmentation.Fragment, + dir: String, +) -> Result(Nil, simplifile.FileError) { + let sha = fragmentation.hash_fragment(fragment) + let content = fragmentation.serialize(fragment) + let path = dir <> "/" <> sha + simplifile.write(path, content) +} diff --git a/test/fragmentation_git_test.gleam b/test/fragmentation_git_test.gleam new file mode 100644 index 0000000..86f76d5 --- /dev/null +++ b/test/fragmentation_git_test.gleam @@ -0,0 +1,88 @@ +import fragmentation +import fragmentation/git +import gleeunit/should +import simplifile + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn test_witnessed() -> fragmentation.Witnessed { + fragmentation.witnessed( + fragmentation.author("alex"), + fragmentation.committer("reed"), + fragmentation.timestamp("2026-03-01T00:00:00Z"), + fragmentation.message("test"), + ) +} + +fn make_shard(data: String) -> fragmentation.Fragment { + let r = fragmentation.ref(fragmentation.hash(data), "self") + fragmentation.shard(r, test_witnessed(), data) +} + +// --------------------------------------------------------------------------- +// write_fragment_creates_file_test +// +// write_fragment writes a file named by SHA containing serialized fragment. +// --------------------------------------------------------------------------- + +pub fn write_fragment_creates_file_test() { + let dir = "/tmp/fragmentation_git_test_write" + let _ = simplifile.create_directory(dir) + let frag = make_shard("hello-world") + let sha = fragmentation.hash_fragment(frag) + + let result = git.write(frag, dir) + result |> should.be_ok() + + let path = dir <> "/" <> sha + simplifile.is_file(path) |> should.equal(Ok(True)) +} + +// --------------------------------------------------------------------------- +// write_fragment_idempotent_test +// +// write_fragment is idempotent — same SHA, same content, no error on repeat. +// --------------------------------------------------------------------------- + +pub fn write_fragment_idempotent_test() { + let dir = "/tmp/fragmentation_git_test_idempotent" + let _ = simplifile.create_directory(dir) + let frag = make_shard("idempotent-shard") + let sha = fragmentation.hash_fragment(frag) + + let r1 = git.write(frag, dir) + let r2 = git.write(frag, dir) + + r1 |> should.be_ok() + r2 |> should.be_ok() + + // Only one file: idempotent means same path, same content + let path = dir <> "/" <> sha + simplifile.is_file(path) |> should.equal(Ok(True)) +} + +// --------------------------------------------------------------------------- +// write_two_fragments_test +// +// Writing two different fragments creates two files. +// --------------------------------------------------------------------------- + +pub fn write_two_fragments_test() { + let dir = "/tmp/fragmentation_git_test_two" + let _ = simplifile.create_directory(dir) + let frag_a = make_shard("fragment-alpha") + let frag_b = make_shard("fragment-beta") + let sha_a = fragmentation.hash_fragment(frag_a) + let sha_b = fragmentation.hash_fragment(frag_b) + + git.write(frag_a, dir) |> should.be_ok() + git.write(frag_b, dir) |> should.be_ok() + + // SHAs must be distinct + sha_a |> should.not_equal(sha_b) + + simplifile.is_file(dir <> "/" <> sha_a) |> should.equal(Ok(True)) + simplifile.is_file(dir <> "/" <> sha_b) |> should.equal(Ok(True)) +}