//// What a source holds, beyond the one document the model is loaded from. //// //// The model is one language of one document. Two questions a reader asks //// are about the file instead: which languages it holds, and, for a //// database, what else it carries. They are here rather than in a reader //// because the answer depends on the input form, which is the library's to //// know, and so that a reader asking them does not learn which form it was //// handed. A mirror of the D library's `ocda.inventory`. //// //// `languages` answers for every form, and answers the same: the languages //// in the order the manifest gave them (which every `.ssp` records, in //// `@source languages`), each with the markup file it was made from and the //// sha256 of its `.ssp`. The database stores that digest; for a `.ssp` and a //// pod it is taken from the bytes, which are the same bytes. //// //// `carried` is the database's: a `.ocda.db` holds the markup, images, //// configuration and catalogues it was built from, a `.ssp` holds none of //// them, and a pod's are its archive entries rather than anything the //// abstraction says. import gleam/bit_array import gleam/dict import gleam/list import gleam/result import gleam/string import ocda/load import ocda/load_db import ocda/load_pod import ocda/load_ssp import ocda/zip import simplifile pub type Language { Language(lang: String, source_filename: String, ssp_digest: String) } pub type Carried { Carried( role: String, compression: String, count: String, bytes: String, stored: String, ) } pub fn languages(path: String) -> Result(List(Language), String) { case load.source_of(path) { load.OcdaDb -> { use held <- result.try(load_db.languages_of_file(path)) Ok( list.map(held, fn(l) { Language(l.lang, l.source_filename, l.ssp_digest) }), ) } load.Ssp -> { use bytes <- result.try( simplifile.read_bits(path) |> result.map_error(fn(e) { path <> ": " <> simplifile.describe_error(e) }), ) use text <- result.try( bit_array.to_string(bytes) |> result.replace_error(path <> ": not valid UTF-8"), ) let doc = load_ssp.parse(text) Ok([ Language( source_value(doc.source, "language"), doc.source_filename, sha256_hex(bytes), ), ]) } load.Pod -> { use archive <- result.try(load_pod.archive_bytes(path)) use entries <- result.try(zip.entries(archive)) use found <- result.try( zip.find_suffix(entries, ".ssp") |> list.try_map(fn(e) { use bytes <- result.try(zip.read(archive, e)) use text <- result.try( bit_array.to_string(bytes) |> result.replace_error(e.name <> ": not valid UTF-8"), ) let doc = load_ssp.parse(text) let lang = result.unwrap(load_pod.lang_of(e.name), "") Ok(#( Language(lang, doc.source_filename, sha256_hex(bytes)), string.split(source_value(doc.source, "languages"), " "), )) }), ) let order = case found { [#(_, o), ..] -> list.filter(o, fn(l) { l != "" }) [] -> [] } let by_lang = list.map(found, fn(f) { #({ f.0 }.lang, f.0) }) |> dict.from_list // the manifest's order, and anything it did not name after it let named = list.filter_map(order, fn(l) { dict.get(by_lang, l) }) let rest = list.filter(found, fn(f) { !list.contains(order, { f.0 }.lang) }) |> list.map(fn(f) { f.0 }) Ok(list.append(named, rest)) } load.Unknown -> Error(path <> ": " <> load.source_name(load.Unknown)) } } /// Only a `.ocda.db` carries files. pub fn is_carrier(path: String) -> Bool { load.source_of(path) == load.OcdaDb } pub fn carried(path: String) -> Result(List(Carried), String) { case is_carrier(path) { False -> Ok([]) True -> { use rows <- result.try(load_db.carried_of_file(path)) Ok( list.filter_map(rows, fn(r) { case r { [role, compression, n, bytes, stored] -> Ok(Carried(role, compression, n, bytes, stored)) _ -> Error(Nil) } }), ) } } } fn source_value(source: dict.Dict(String, String), key: String) -> String { result.unwrap(dict.get(source, key), "") } @external(erlang, "ocda_ffi", "sha256_hex") fn sha256_hex(bytes: BitArray) -> String