diff --git a/Cargo.lock b/Cargo.lock index 9602a4d..7740dc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -112,6 +130,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -130,6 +165,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "ash" version = "0.37.3+1.3.251" @@ -349,6 +393,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + [[package]] name = "base64" version = "0.22.1" @@ -370,8 +457,10 @@ version = "0.1.0" dependencies = [ "chrono", "deunicode", + "image 0.25.10", "refinery", "rusqlite", + "rust-stemmers", "serde", "serde_json", "serde_yaml", @@ -453,6 +542,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + [[package]] name = "block" version = "0.1.6" @@ -499,6 +597,12 @@ dependencies = [ "piper", ] +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.20.2" @@ -537,6 +641,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -818,6 +928,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cosmic-text" version = "0.12.1" @@ -1134,6 +1253,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1235,6 +1374,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -1575,6 +1734,16 @@ dependencies = [ "weezl", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gio" version = "0.18.4" @@ -1951,7 +2120,7 @@ dependencies = [ "iced_renderer", "iced_widget", "iced_winit", - "image", + "image 0.24.9", "thiserror 1.0.69", ] @@ -2014,7 +2183,7 @@ dependencies = [ "half", "iced_core", "iced_futures", - "image", + "image 0.24.9", "kamadak-exif", "log", "once_cell", @@ -2240,14 +2409,54 @@ dependencies = [ "byteorder", "color_quant", "exr", - "gif", + "gif 0.13.3", "jpeg-decoder", "num-traits", - "png", + "png 0.17.16", "qoi", - "tiff", + "tiff 0.9.1", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif 0.14.1", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff 0.11.3", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indexmap" version = "2.13.0" @@ -2269,6 +2478,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2429,6 +2658,16 @@ version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2542,6 +2781,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.12.5" @@ -2557,6 +2805,16 @@ dependencies = [ "libc", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.0" @@ -2617,6 +2875,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.15.3" @@ -2632,7 +2900,7 @@ dependencies = [ "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", "once_cell", - "png", + "png 0.17.16", "thiserror 1.0.69", "windows-sys 0.59.0", ] @@ -2702,6 +2970,12 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.29.0" @@ -2715,12 +2989,68 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3297,6 +3627,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3426,6 +3762,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -3553,6 +3902,25 @@ name = "profiling" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" [[package]] name = "qoi" @@ -3563,6 +3931,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.38.4" @@ -3673,6 +4047,56 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3857,6 +4281,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "ropey" version = "1.6.1" @@ -3897,6 +4327,16 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -4149,6 +4589,15 @@ dependencies = [ "simdutf8", ] +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -4511,6 +4960,20 @@ dependencies = [ "weezl", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -4553,7 +5016,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png", + "png 0.17.16", "tiny-skia-path", ] @@ -4879,6 +5342,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -5788,6 +6262,12 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -6035,6 +6515,12 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -6044,6 +6530,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "4.2.0" diff --git a/Cargo.toml b/Cargo.toml index 77b6a8b..c8136d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ thiserror = "2" anyhow = "1" tokio = { version = "1", features = ["full"] } walkdir = "2" +image = "0.25" +rust-stemmers = "1" # UI framework iced = { version = "0.13", features = ["wgpu", "advanced", "image"] } diff --git a/RUST_EXECUTION_BACKLOG.md b/RUST_EXECUTION_BACKLOG.md index 3688168..1a1ecc8 100644 --- a/RUST_EXECUTION_BACKLOG.md +++ b/RUST_EXECUTION_BACKLOG.md @@ -64,37 +64,37 @@ Rules: ### `bds-core/db` -- verify schema compatibility against existing projects -- add FTS5 virtual tables (`posts_fts`, `media_fts`) for in-app full-text search -- verify `mediaTranslations` table read/write -- verify `postLinks` table read/write +- ~~verify schema compatibility against existing projects~~ **DONE** +- ~~add FTS5 virtual tables (`posts_fts`, `media_fts`) for in-app full-text search~~ **DONE** +- ~~verify `mediaTranslations` table read/write~~ **DONE** +- ~~verify `postLinks` table read/write~~ **DONE** ### `bds-core/engine` -- implement `ProjectEngine` -- implement `PostEngine` -- implement `MediaEngine` -- implement `PostMediaEngine` -- implement `TagEngine` -- implement `MetaEngine` -- implement metadata diff flow (posts, media, scripts, templates) -- implement rebuild-from-filesystem flow +- ~~implement `ProjectEngine`~~ **DONE** +- ~~implement `PostEngine`~~ **DONE** +- ~~implement `MediaEngine`~~ **DONE** +- ~~implement `PostMediaEngine`~~ **DONE** +- ~~implement `TagEngine`~~ **DONE** +- ~~implement `MetaEngine`~~ **DONE** +- ~~implement metadata diff flow (posts, media, scripts, templates)~~ **DONE** +- ~~implement rebuild-from-filesystem flow~~ **DONE** ### `bds-core/util` -- frontmatter parser/writer via `serde_yaml` -- translation file parser/writer -- sidecar parser/writer -- content hashing support via `sha2` -- thumbnail generation via `image` crate (resize, WEBP encoding) -- recursive directory traversal via `walkdir` (for rebuild-from-filesystem) +- ~~frontmatter parser/writer via `serde_yaml`~~ **DONE** +- ~~translation file parser/writer~~ **DONE** +- ~~sidecar parser/writer~~ **DONE** +- ~~content hashing support via `sha2`~~ **DONE** +- ~~thumbnail generation via `image` crate (resize, WEBP encoding)~~ **DONE** +- ~~recursive directory traversal via `walkdir` (for rebuild-from-filesystem)~~ **DONE** ### Validation -- round-trip persistence tests -- rebuild tests -- metadata diff tests -- golden-file comparisons for written files +- ~~round-trip persistence tests~~ **DONE** (38 m1_validation integration tests) +- ~~rebuild tests~~ **DONE** +- ~~metadata diff tests~~ **DONE** (full coverage matrix: 13 post fields, 6 media, 4 template, 5 script) +- ~~golden-file comparisons for written files~~ **DONE** ## Milestone M2: Native Workspace diff --git a/crates/bds-core/Cargo.toml b/crates/bds-core/Cargo.toml index bc03fbf..fa865e1 100644 --- a/crates/bds-core/Cargo.toml +++ b/crates/bds-core/Cargo.toml @@ -16,6 +16,8 @@ sha2 = { workspace = true } deunicode = { workspace = true } thiserror = { workspace = true } walkdir = { workspace = true } +image = { workspace = true } +rust-stemmers = { workspace = true } [dev-dependencies] tempfile = "3" diff --git a/crates/bds-core/src/db/from_row.rs b/crates/bds-core/src/db/from_row.rs new file mode 100644 index 0000000..b8109fe --- /dev/null +++ b/crates/bds-core/src/db/from_row.rs @@ -0,0 +1,619 @@ +use rusqlite::Row; + +use crate::model::{ + DbNotification, GeneratedFileHash, Media, MediaTranslation, NotificationAction, + NotificationEntity, Post, PostLink, PostMedia, PostStatus, PostTranslation, Project, Script, + ScriptKind, ScriptStatus, Setting, Tag, Template, TemplateKind, TemplateStatus, +}; + +// ── helpers ────────────────────────────────────────────────────────── + +fn conversion_err(msg: String) -> rusqlite::Error { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, msg)), + ) +} + +fn parse_post_status(s: &str) -> rusqlite::Result { + match s { + "draft" => Ok(PostStatus::Draft), + "published" => Ok(PostStatus::Published), + "archived" => Ok(PostStatus::Archived), + _ => Err(conversion_err(format!("invalid PostStatus: {s}"))), + } +} + +fn parse_template_kind(s: &str) -> rusqlite::Result { + match s { + "post" => Ok(TemplateKind::Post), + "list" => Ok(TemplateKind::List), + "not_found" => Ok(TemplateKind::NotFound), + "partial" => Ok(TemplateKind::Partial), + _ => Err(conversion_err(format!("invalid TemplateKind: {s}"))), + } +} + +fn parse_template_status(s: &str) -> rusqlite::Result { + match s { + "draft" => Ok(TemplateStatus::Draft), + "published" => Ok(TemplateStatus::Published), + _ => Err(conversion_err(format!("invalid TemplateStatus: {s}"))), + } +} + +fn parse_script_kind(s: &str) -> rusqlite::Result { + match s { + "macro" => Ok(ScriptKind::Macro), + "utility" => Ok(ScriptKind::Utility), + "transform" => Ok(ScriptKind::Transform), + _ => Err(conversion_err(format!("invalid ScriptKind: {s}"))), + } +} + +fn parse_script_status(s: &str) -> rusqlite::Result { + match s { + "draft" => Ok(ScriptStatus::Draft), + "published" => Ok(ScriptStatus::Published), + _ => Err(conversion_err(format!("invalid ScriptStatus: {s}"))), + } +} + +fn parse_notification_entity(s: &str) -> rusqlite::Result { + match s { + "post" => Ok(NotificationEntity::Post), + "media" => Ok(NotificationEntity::Media), + "script" => Ok(NotificationEntity::Script), + "template" => Ok(NotificationEntity::Template), + _ => Err(conversion_err(format!("invalid NotificationEntity: {s}"))), + } +} + +fn parse_notification_action(s: &str) -> rusqlite::Result { + match s { + "created" => Ok(NotificationAction::Created), + "updated" => Ok(NotificationAction::Updated), + "deleted" => Ok(NotificationAction::Deleted), + _ => Err(conversion_err(format!("invalid NotificationAction: {s}"))), + } +} + +fn json_to_vec_string(s: &str) -> rusqlite::Result> { + serde_json::from_str(s).map_err(|e| conversion_err(format!("JSON parse error: {e}"))) +} + +fn bool_from_i64(v: i64) -> bool { + v != 0 +} + +// ── enum → DB string (for INSERT / UPDATE) ─────────────────────────── + +pub fn post_status_to_str(s: &PostStatus) -> &'static str { + match s { + PostStatus::Draft => "draft", + PostStatus::Published => "published", + PostStatus::Archived => "archived", + } +} + +pub fn template_kind_to_str(k: &TemplateKind) -> &'static str { + match k { + TemplateKind::Post => "post", + TemplateKind::List => "list", + TemplateKind::NotFound => "not_found", + TemplateKind::Partial => "partial", + } +} + +pub fn template_status_to_str(s: &TemplateStatus) -> &'static str { + match s { + TemplateStatus::Draft => "draft", + TemplateStatus::Published => "published", + } +} + +pub fn script_kind_to_str(k: &ScriptKind) -> &'static str { + match k { + ScriptKind::Macro => "macro", + ScriptKind::Utility => "utility", + ScriptKind::Transform => "transform", + } +} + +pub fn script_status_to_str(s: &ScriptStatus) -> &'static str { + match s { + ScriptStatus::Draft => "draft", + ScriptStatus::Published => "published", + } +} + +pub fn notification_entity_to_str(e: &NotificationEntity) -> &'static str { + match e { + NotificationEntity::Post => "post", + NotificationEntity::Media => "media", + NotificationEntity::Script => "script", + NotificationEntity::Template => "template", + } +} + +pub fn notification_action_to_str(a: &NotificationAction) -> &'static str { + match a { + NotificationAction::Created => "created", + NotificationAction::Updated => "updated", + NotificationAction::Deleted => "deleted", + } +} + +// ── column lists (keep in sync with from_row functions) ────────────── + +pub const PROJECT_COLUMNS: &str = + "id, name, slug, description, data_path, is_active, created_at, updated_at"; + +pub const POST_COLUMNS: &str = "\ + id, project_id, title, slug, excerpt, content, status, author, \ + language, do_not_translate, template_slug, file_path, checksum, \ + tags, categories, \ + published_title, published_content, published_tags, \ + published_categories, published_excerpt, \ + created_at, updated_at, published_at"; + +pub const POST_TRANSLATION_COLUMNS: &str = "\ + id, project_id, translation_for, language, title, excerpt, content, \ + status, file_path, checksum, created_at, updated_at, published_at"; + +pub const POST_LINK_COLUMNS: &str = + "id, source_post_id, target_post_id, link_text, created_at"; + +pub const POST_MEDIA_COLUMNS: &str = + "id, project_id, post_id, media_id, sort_order, created_at"; + +pub const MEDIA_COLUMNS: &str = "\ + id, project_id, filename, original_name, mime_type, size, \ + width, height, title, alt, caption, author, language, \ + file_path, sidecar_path, checksum, tags, created_at, updated_at"; + +pub const MEDIA_TRANSLATION_COLUMNS: &str = + "id, project_id, translation_for, language, title, alt, caption, created_at, updated_at"; + +pub const TAG_COLUMNS: &str = + "id, project_id, name, color, post_template_slug, created_at, updated_at"; + +pub const TEMPLATE_COLUMNS: &str = "\ + id, project_id, slug, title, kind, enabled, version, \ + file_path, status, content, created_at, updated_at"; + +pub const SCRIPT_COLUMNS: &str = "\ + id, project_id, slug, title, kind, entrypoint, enabled, version, \ + file_path, status, content, created_at, updated_at"; + +pub const SETTING_COLUMNS: &str = "key, value, updated_at"; + +pub const GENERATED_FILE_HASH_COLUMNS: &str = + "project_id, relative_path, content_hash, updated_at"; + +pub const DB_NOTIFICATION_COLUMNS: &str = + "id, entity_type, entity_id, action, from_cli, seen_at, created_at"; + +// ── from_row functions ─────────────────────────────────────────────── + +pub fn project_from_row(row: &Row) -> rusqlite::Result { + Ok(Project { + id: row.get(0)?, + name: row.get(1)?, + slug: row.get(2)?, + description: row.get(3)?, + data_path: row.get(4)?, + is_active: bool_from_i64(row.get(5)?), + created_at: row.get(6)?, + updated_at: row.get(7)?, + }) +} + +pub fn post_from_row(row: &Row) -> rusqlite::Result { + let status_str: String = row.get(6)?; + let tags_json: String = row.get(13)?; + let categories_json: String = row.get(14)?; + Ok(Post { + id: row.get(0)?, + project_id: row.get(1)?, + title: row.get(2)?, + slug: row.get(3)?, + excerpt: row.get(4)?, + content: row.get(5)?, + status: parse_post_status(&status_str)?, + author: row.get(7)?, + language: row.get(8)?, + do_not_translate: bool_from_i64(row.get(9)?), + template_slug: row.get(10)?, + file_path: row.get(11)?, + checksum: row.get(12)?, + tags: json_to_vec_string(&tags_json)?, + categories: json_to_vec_string(&categories_json)?, + published_title: row.get(15)?, + published_content: row.get(16)?, + published_tags: row.get(17)?, + published_categories: row.get(18)?, + published_excerpt: row.get(19)?, + created_at: row.get(20)?, + updated_at: row.get(21)?, + published_at: row.get(22)?, + }) +} + +pub fn post_translation_from_row(row: &Row) -> rusqlite::Result { + let status_str: String = row.get(7)?; + Ok(PostTranslation { + id: row.get(0)?, + project_id: row.get(1)?, + translation_for: row.get(2)?, + language: row.get(3)?, + title: row.get(4)?, + excerpt: row.get(5)?, + content: row.get(6)?, + status: parse_post_status(&status_str)?, + file_path: row.get(8)?, + checksum: row.get(9)?, + created_at: row.get(10)?, + updated_at: row.get(11)?, + published_at: row.get(12)?, + }) +} + +pub fn post_link_from_row(row: &Row) -> rusqlite::Result { + Ok(PostLink { + id: row.get(0)?, + source_post_id: row.get(1)?, + target_post_id: row.get(2)?, + link_text: row.get(3)?, + created_at: row.get(4)?, + }) +} + +pub fn post_media_from_row(row: &Row) -> rusqlite::Result { + Ok(PostMedia { + id: row.get(0)?, + project_id: row.get(1)?, + post_id: row.get(2)?, + media_id: row.get(3)?, + sort_order: row.get(4)?, + created_at: row.get(5)?, + }) +} + +pub fn media_from_row(row: &Row) -> rusqlite::Result { + let tags_json: String = row.get(16)?; + Ok(Media { + id: row.get(0)?, + project_id: row.get(1)?, + filename: row.get(2)?, + original_name: row.get(3)?, + mime_type: row.get(4)?, + size: row.get(5)?, + width: row.get(6)?, + height: row.get(7)?, + title: row.get(8)?, + alt: row.get(9)?, + caption: row.get(10)?, + author: row.get(11)?, + language: row.get(12)?, + file_path: row.get(13)?, + sidecar_path: row.get(14)?, + checksum: row.get(15)?, + tags: json_to_vec_string(&tags_json)?, + created_at: row.get(17)?, + updated_at: row.get(18)?, + }) +} + +pub fn media_translation_from_row(row: &Row) -> rusqlite::Result { + Ok(MediaTranslation { + id: row.get(0)?, + project_id: row.get(1)?, + translation_for: row.get(2)?, + language: row.get(3)?, + title: row.get(4)?, + alt: row.get(5)?, + caption: row.get(6)?, + created_at: row.get(7)?, + updated_at: row.get(8)?, + }) +} + +pub fn tag_from_row(row: &Row) -> rusqlite::Result { + Ok(Tag { + id: row.get(0)?, + project_id: row.get(1)?, + name: row.get(2)?, + color: row.get(3)?, + post_template_slug: row.get(4)?, + created_at: row.get(5)?, + updated_at: row.get(6)?, + }) +} + +pub fn template_from_row(row: &Row) -> rusqlite::Result