///|
pub(all) enum AssetBucket {
BucketSource
BucketDocument
BucketConfig
BucketMedia
BucketArchive
BucketGenerated
BucketRuntime
BucketOther
} derive(Eq)
///|
pub(all) struct CountRow {
key : String
count : Int
bytes : Int
} derive(Eq)
///|
pub(all) struct AssetCatalog {
manifest_name : String
manifest_version : String
entries : Int
total_bytes : Int
directories : Array[CountRow]
extensions : Array[CountRow]
kinds : Array[CountRow]
policies : Array[CountRow]
tags : Array[CountRow]
buckets : Array[CountRow]
} derive(Eq)
///|
pub fn AssetBucket::to_wire(self : AssetBucket) -> String {
match self {
BucketSource => "source"
BucketDocument => "document"
BucketConfig => "config"
BucketMedia => "media"
BucketArchive => "archive"
BucketGenerated => "generated"
BucketRuntime => "runtime"
BucketOther => "other"
}
}
///|
pub fn parse_asset_bucket(raw : String) -> AssetBucket {
match raw.trim().to_owned().to_lower() {
"source" => BucketSource
"document" => BucketDocument
"config" => BucketConfig
"media" => BucketMedia
"archive" => BucketArchive
"generated" => BucketGenerated
"runtime" => BucketRuntime
_ => BucketOther
}
}
///|
pub fn classify_asset(entry : Asset) -> AssetBucket {
if entry.policy == Runtime {
return BucketRuntime
}
match path_is_generated_like(entry.path) {
Ok(true) => return BucketGenerated
_ => ()
}
match entry.kind {
Image | Audio | Font => return BucketMedia
Binary =>
match path_is_archive_like(entry.path) {
Ok(true) => return BucketArchive
_ => ()
}
_ => ()
}
match path_is_source_like(entry.path) {
Ok(true) => return BucketSource
_ => ()
}
match path_is_config_like(entry.path) {
Ok(true) => return BucketConfig
_ => ()
}
match path_is_document_like(entry.path) {
Ok(true) => return BucketDocument
_ => ()
}
match path_is_media_like(entry.path) {
Ok(true) => return BucketMedia
_ => ()
}
match path_is_archive_like(entry.path) {
Ok(true) => return BucketArchive
_ => ()
}
BucketOther
}
///|
pub fn asset_directory(entry : Asset) -> String {
match path_dirname(entry.path) {
Ok(dirname) => dirname
Err(_) => "."
}
}
///|
pub fn asset_extension(entry : Asset) -> String {
match path_extension(entry.path) {
Ok(Some(extension)) => extension
_ => "-"
}
}
///|
pub fn asset_bucket_name(entry : Asset) -> String {
classify_asset(entry).to_wire()
}
///|
pub fn catalog_manifest(manifest : Manifest) -> AssetCatalog {
{
manifest_name: manifest.name,
manifest_version: manifest.version,
entries: manifest.entries.length(),
total_bytes: catalog_total_bytes(manifest.entries),
directories: count_by_directory(manifest),
extensions: count_by_extension(manifest),
kinds: count_by_kind(manifest),
policies: count_by_policy(manifest),
tags: count_by_tag(manifest),
buckets: count_by_bucket(manifest),
}
}
///|
pub fn count_by_directory(manifest : Manifest) -> Array[CountRow] {
count_entries_by(manifest.entries, entry => asset_directory(entry))
}
///|
pub fn count_by_extension(manifest : Manifest) -> Array[CountRow] {
count_entries_by(manifest.entries, entry => asset_extension(entry))
}
///|
pub fn count_by_kind(manifest : Manifest) -> Array[CountRow] {
count_entries_by(manifest.entries, entry => entry.kind.to_wire())
}
///|
pub fn count_by_policy(manifest : Manifest) -> Array[CountRow] {
count_entries_by(manifest.entries, entry => entry.policy.to_wire())
}
///|
pub fn count_by_bucket(manifest : Manifest) -> Array[CountRow] {
count_entries_by(manifest.entries, entry => classify_asset(entry).to_wire())
}
///|
pub fn count_by_tag(manifest : Manifest) -> Array[CountRow] {
let keys : Array[String] = []
let counts : Map[String, Int] = Map([])
let bytes : Map[String, Int] = Map([])
for entry in manifest.entries {
if entry.tags.is_empty() {
bump_count(keys, counts, bytes, "-", entry.bytes)
} else {
for tag in entry.tags {
bump_count(keys, counts, bytes, tag, entry.bytes)
}
}
}
rows_from_counts(keys, counts, bytes)
}
///|
pub fn manifest_directories(manifest : Manifest) -> Array[String] {
row_keys(count_by_directory(manifest))
}
///|
pub fn manifest_extensions(manifest : Manifest) -> Array[String] {
row_keys(count_by_extension(manifest))
}
///|
pub fn manifest_tags(manifest : Manifest) -> Array[String] {
row_keys(count_by_tag(manifest))
}
///|
pub fn assets_with_kind(manifest : Manifest, kind : AssetKind) -> Array[Asset] {
filter_entries(manifest.entries, entry => entry.kind == kind)
}
///|
pub fn assets_with_policy(
manifest : Manifest,
policy : CachePolicy,
) -> Array[Asset] {
filter_entries(manifest.entries, entry => entry.policy == policy)
}
///|
pub fn assets_with_bucket(
manifest : Manifest,
bucket : AssetBucket,
) -> Array[Asset] {
filter_entries(manifest.entries, entry => classify_asset(entry) == bucket)
}
///|
pub fn assets_with_tag(manifest : Manifest, tag : String) -> Array[Asset] {
let wanted = tag.trim().to_owned().to_lower()
filter_entries(manifest.entries, entry => entry.tags.contains(wanted))
}
///|
pub fn assets_with_extension(
manifest : Manifest,
extension : String,
) -> Array[Asset] {
let wanted = normalize_extension_query(extension)
filter_entries(manifest.entries, entry => asset_extension(entry) == wanted)
}
///|
pub fn assets_under_directory(
manifest : Manifest,
directory : String,
) -> Array[Asset] {
let normalized = match normalize_path(directory) {
Ok(value) => value
Err(_) => directory.trim().to_owned().replace_all(old="\\", new="/")
}
filter_entries(manifest.entries, entry => {
entry.path == normalized || entry.path.has_prefix(normalized + "/")
})
}
///|
pub fn assets_larger_than(manifest : Manifest, min_bytes : Int) -> Array[Asset] {
filter_entries(manifest.entries, entry => entry.bytes > min_bytes)
}
///|
pub fn assets_smaller_than(
manifest : Manifest,
max_bytes : Int,
) -> Array[Asset] {
filter_entries(manifest.entries, entry => entry.bytes < max_bytes)
}
///|
pub fn assets_between_sizes(
manifest : Manifest,
min_bytes : Int,
max_bytes : Int,
) -> Array[Asset] {
filter_entries(manifest.entries, entry => {
entry.bytes >= min_bytes && entry.bytes <= max_bytes
})
}
///|
pub fn largest_assets(manifest : Manifest, limit : Int) -> Array[Asset] {
let entries = [ for entry in manifest.entries => entry ]
entries.sort_by((a, b) => b.bytes.compare(a.bytes))
take_assets(entries, limit)
}
///|
pub fn smallest_assets(manifest : Manifest, limit : Int) -> Array[Asset] {
let entries = [ for entry in manifest.entries => entry ]
entries.sort_by((a, b) => a.bytes.compare(b.bytes))
take_assets(entries, limit)
}
///|
pub fn newest_paths_from_diff(diffs : Array[EntryDiff]) -> Array[String] {
let paths : Array[String] = []
for diff in diffs {
match diff.kind {
Added => paths.push(diff.path)
Changed => paths.push(diff.path)
Removed => ()
}
}
paths.sort()
paths
}
///|
pub fn removed_paths_from_diff(diffs : Array[EntryDiff]) -> Array[String] {
let paths : Array[String] = []
for diff in diffs {
match diff.kind {
Removed => paths.push(diff.path)
_ => ()
}
}
paths.sort()
paths
}
///|
pub fn changed_paths_from_diff(diffs : Array[EntryDiff]) -> Array[String] {
let paths : Array[String] = []
for diff in diffs {
match diff.kind {
Changed => paths.push(diff.path)
_ => ()
}
}
paths.sort()
paths
}
///|
pub fn catalog_has_path(manifest : Manifest, path : String) -> Bool {
match normalize_path(path) {
Ok(normalized) => manifest.entries.any(entry => entry.path == normalized)
Err(_) => false
}
}
///|
pub fn catalog_get_path(manifest : Manifest, path : String) -> Asset? {
match normalize_path(path) {
Ok(normalized) => {
for entry in manifest.entries {
if entry.path == normalized {
return Some(entry)
}
}
None
}
Err(_) => None
}
}
///|
pub fn catalog_fingerprint_index(manifest : Manifest) -> Array[CountRow] {
count_entries_by(manifest.entries, entry => entry.fingerprint)
}
///|
pub fn duplicate_fingerprint_rows(manifest : Manifest) -> Array[CountRow] {
let rows = catalog_fingerprint_index(manifest)
filter_rows(rows, row => row.count > 1)
}
///|
pub fn unique_asset_count(manifest : Manifest) -> Int {
let rows = catalog_fingerprint_index(manifest)
rows.length()
}
///|
pub fn duplicate_asset_count(manifest : Manifest) -> Int {
let mut total = 0
for row in duplicate_fingerprint_rows(manifest) {
total = total + row.count
}
total
}
///|
pub fn render_count_rows(rows : Array[CountRow]) -> String {
if rows.is_empty() {
return "(none)\n"
}
let out = StringBuilder()
for row in rows {
out.write_string(row.key)
out.write_string(" ")
out.write_string(row.count.to_string())
out.write_string(" ")
out.write_string(row.bytes.to_string())
out.write_string("\n")
}
out.to_string()
}
///|
pub fn render_catalog(catalog : AssetCatalog) -> String {
let out = StringBuilder()
out.write_string("catalog ")
out.write_string(catalog.manifest_name)
out.write_string("@")
out.write_string(catalog.manifest_version)
out.write_string("\nentries ")
out.write_string(catalog.entries.to_string())
out.write_string("\nbytes ")
out.write_string(catalog.total_bytes.to_string())
out.write_string("\n\n[buckets]\n")
out.write_string(render_count_rows(catalog.buckets))
out.write_string("\n[extensions]\n")
out.write_string(render_count_rows(catalog.extensions))
out.write_string("\n[directories]\n")
out.write_string(render_count_rows(catalog.directories))
out.to_string()
}
///|
fn count_entries_by(
entries : Array[Asset],
key_of : (Asset) -> String,
) -> Array[CountRow] {
let keys : Array[String] = []
let counts : Map[String, Int] = Map([])
let bytes : Map[String, Int] = Map([])
for entry in entries {
bump_count(keys, counts, bytes, key_of(entry), entry.bytes)
}
rows_from_counts(keys, counts, bytes)
}
///|
fn bump_count(
keys : Array[String],
counts : Map[String, Int],
bytes : Map[String, Int],
key : String,
entry_bytes : Int,
) -> Unit {
if !counts.contains(key) {
keys.push(key)
counts.set(key, 1)
bytes.set(key, entry_bytes)
} else {
let count = match counts.get(key) {
Some(value) => value
None => 0
}
let total = match bytes.get(key) {
Some(value) => value
None => 0
}
counts.set(key, count + 1)
bytes.set(key, total + entry_bytes)
}
}
///|
fn rows_from_counts(
keys : Array[String],
counts : Map[String, Int],
bytes : Map[String, Int],
) -> Array[CountRow] {
let rows : Array[CountRow] = []
for key in keys {
let count = match counts.get(key) {
Some(value) => value
None => 0
}
let total = match bytes.get(key) {
Some(value) => value
None => 0
}
rows.push({ key, count, bytes: total })
}
rows.sort_by((a, b) => {
if a.count == b.count {
a.key.compare(b.key)
} else {
b.count.compare(a.count)
}
})
rows
}
///|
fn row_keys(rows : Array[CountRow]) -> Array[String] {
let keys : Array[String] = []
for row in rows {
keys.push(row.key)
}
keys
}
///|
fn filter_entries(
entries : Array[Asset],
keep : (Asset) -> Bool,
) -> Array[Asset] {
let result : Array[Asset] = []
for entry in entries {
if keep(entry) {
result.push(entry)
}
}
result
}
///|
fn filter_rows(
rows : Array[CountRow],
keep : (CountRow) -> Bool,
) -> Array[CountRow] {
let result : Array[CountRow] = []
for row in rows {
if keep(row) {
result.push(row)
}
}
result
}
///|
fn take_assets(entries : Array[Asset], limit : Int) -> Array[Asset] {
let result : Array[Asset] = []
if limit <= 0 {
return result
}
let mut taken = 0
for entry in entries {
if taken >= limit {
return result
}
result.push(entry)
taken = taken + 1
}
result
}
///|
fn normalize_extension_query(extension : String) -> String {
let trimmed = extension.trim().to_owned().to_lower()
match trimmed.strip_prefix(".") {
Some(rest) => if rest == "" { "-" } else { rest.to_owned() }
None => if trimmed == "" { "-" } else { trimmed }
}
}
///|
fn catalog_total_bytes(entries : Array[Asset]) -> Int {
let mut total = 0
for entry in entries {
total = total + entry.bytes
}
total
}