diff --git a/Cargo.lock b/Cargo.lock index 43f0510b..a1faffb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,9 +175,9 @@ checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cache_diff" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b58ba1160088b5306b19fa156e9be5736043243ee6197346bf8ecd55e5366c" +checksum = "a17abe0c554c4e2cf77682e0105b35832255d8425d977f221f8ea9e726b7201f" dependencies = [ "bullet_stream", "cache_diff_derive", @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "cache_diff_derive" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273bbd860603cb240372b460f8dc2440528ba95593f944b549194eb8600285a8" +checksum = "497f88518f336c54b7d17f652024d6ee7769846d5c6f93291878a12afa3cddfc" dependencies = [ "proc-macro2", "quote", @@ -945,13 +945,26 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "magic_migrate" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a173b20fd1f2e55ecc1fe680c88e039f9962dcf84b2531009bcdf6f2d589186" +checksum = "77953038e9939c18810bf6448bcfe47f6884e01b0447512edde77b82815f3795" dependencies = [ + "magic_migrate_derive", "serde", ] +[[package]] +name = "magic_migrate_derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee63d062a466a9e9df66ef1114f6dc3bafcc4bf22070d9ebc9ca284a39052ae5" +dependencies = [ + "proc-macro2", + "quote", + "strum", + "syn 2.0.90", +] + [[package]] name = "memchr" version = "2.7.1" diff --git a/Cargo.toml b/Cargo.toml index b439a058..df5ab323 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,9 @@ resolver = "2" members = ["buildpacks/ruby", "commons"] +[workspace.dependencies] +cache_diff = { version = "1.1", features = ["bullet_stream"] } + [workspace.package] edition = "2021" rust-version = "1.82" diff --git a/buildpacks/ruby/Cargo.toml b/buildpacks/ruby/Cargo.toml index 372f9590..bda37099 100644 --- a/buildpacks/ruby/Cargo.toml +++ b/buildpacks/ruby/Cargo.toml @@ -30,7 +30,7 @@ ureq = { version = "2", default-features = false, features = ["tls"] } url = "2" magic_migrate = "1.0" toml = "0.8" -cache_diff = { version = "1.0.0", features = ["bullet_stream"] } +cache_diff.workspace = true [dev-dependencies] libcnb-test = "=0.26.1" diff --git a/buildpacks/ruby/src/layers/bundle_download_layer.rs b/buildpacks/ruby/src/layers/bundle_download_layer.rs index 2ba953c2..b7902eed 100644 --- a/buildpacks/ruby/src/layers/bundle_download_layer.rs +++ b/buildpacks/ruby/src/layers/bundle_download_layer.rs @@ -16,7 +16,7 @@ use libcnb::data::layer_name; use libcnb::layer::{EmptyLayerCause, LayerState}; use libcnb::layer_env::{LayerEnv, ModificationBehavior, Scope}; use libcnb::Env; -use magic_migrate::{try_migrate_deserializer_chain, TryMigrate}; +use magic_migrate::TryMigrate; use serde::{Deserialize, Serialize}; use std::io::Stdout; use std::path::Path; @@ -70,24 +70,15 @@ pub(crate) fn handle( } pub(crate) type Metadata = MetadataV1; -try_migrate_deserializer_chain!( - deserializer: toml::Deserializer::new, - error: MetadataError, - chain: [MetadataV1], -); -#[derive(Deserialize, Serialize, Debug, Clone, CacheDiff)] +#[derive(Deserialize, Serialize, Debug, Clone, CacheDiff, TryMigrate)] +#[try_migrate(from = None)] #[serde(deny_unknown_fields)] pub(crate) struct MetadataV1 { #[cache_diff(rename = "Bundler version")] pub(crate) version: ResolvedBundlerVersion, } -#[derive(Debug, thiserror::Error)] -pub(crate) enum MetadataError { - // Update if migrating between a metadata version can error -} - fn download_bundler( bullet: Print>, env: &Env, diff --git a/buildpacks/ruby/src/layers/bundle_install_layer.rs b/buildpacks/ruby/src/layers/bundle_install_layer.rs index 060a14b5..7bf8e6f3 100644 --- a/buildpacks/ruby/src/layers/bundle_install_layer.rs +++ b/buildpacks/ruby/src/layers/bundle_install_layer.rs @@ -30,7 +30,7 @@ use libcnb::{ layer_env::{LayerEnv, ModificationBehavior, Scope}, Env, }; -use magic_migrate::{try_migrate_deserializer_chain, TryMigrate}; +use magic_migrate::TryMigrate; use serde::{Deserialize, Serialize}; use std::io::Stdout; use std::{path::Path, process::Command}; @@ -121,14 +121,10 @@ pub(crate) fn handle( } pub(crate) type Metadata = MetadataV3; -try_migrate_deserializer_chain!( - chain: [MetadataV1, MetadataV2, MetadataV3], - error: MetadataMigrateError, - deserializer: toml::Deserializer::new, -); -#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, TryMigrate)] #[serde(deny_unknown_fields)] +#[try_migrate(from = None)] pub(crate) struct MetadataV1 { pub(crate) stack: String, pub(crate) ruby_version: ResolvedRubyVersion, @@ -136,7 +132,8 @@ pub(crate) struct MetadataV1 { pub(crate) digest: MetadataDigest, // Must be last for serde to be happy https://github.com/toml-rs/toml-rs/issues/142 } -#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, TryMigrate)] +#[try_migrate(from = MetadataV1)] #[serde(deny_unknown_fields)] pub(crate) struct MetadataV2 { pub(crate) distro_name: String, @@ -147,7 +144,8 @@ pub(crate) struct MetadataV2 { pub(crate) digest: MetadataDigest, // Must be last for serde to be happy https://github.com/toml-rs/toml-rs/issues/142 } -#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff)] +#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff, TryMigrate)] +#[try_migrate(from = MetadataV2)] #[serde(deny_unknown_fields)] pub(crate) struct MetadataV3 { #[cache_diff(rename = "OS Distribution")] diff --git a/buildpacks/ruby/src/layers/ruby_install_layer.rs b/buildpacks/ruby/src/layers/ruby_install_layer.rs index 80dd7b46..1b8e0ef5 100644 --- a/buildpacks/ruby/src/layers/ruby_install_layer.rs +++ b/buildpacks/ruby/src/layers/ruby_install_layer.rs @@ -25,7 +25,7 @@ use flate2::read::GzDecoder; use libcnb::data::layer_name; use libcnb::layer::{EmptyLayerCause, LayerState}; use libcnb::layer_env::LayerEnv; -use magic_migrate::{try_migrate_deserializer_chain, TryMigrate}; +use magic_migrate::TryMigrate; use serde::{Deserialize, Serialize}; use std::io::{self, Stdout}; use std::path::Path; @@ -85,14 +85,16 @@ fn install_ruby(metadata: &Metadata, layer_path: &Path) -> Result<(), RubyBuildp Ok(()) } -#[derive(Deserialize, Serialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone, TryMigrate)] +#[try_migrate(from = None)] #[serde(deny_unknown_fields)] pub(crate) struct MetadataV1 { pub(crate) stack: String, pub(crate) version: ResolvedRubyVersion, } -#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, TryMigrate)] +#[try_migrate(from = MetadataV1)] #[serde(deny_unknown_fields)] pub(crate) struct MetadataV2 { pub(crate) distro_name: String, @@ -101,7 +103,8 @@ pub(crate) struct MetadataV2 { pub(crate) ruby_version: ResolvedRubyVersion, } -#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff)] +#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff, TryMigrate)] +#[try_migrate(from = MetadataV2)] #[serde(deny_unknown_fields)] pub(crate) struct MetadataV3 { #[cache_diff(rename = "OS Distribution")] @@ -123,11 +126,6 @@ impl MetadataV3 { } pub(crate) type Metadata = MetadataV3; -try_migrate_deserializer_chain!( - chain: [MetadataV1, MetadataV2, MetadataV3], - error: MetadataMigrateError, - deserializer: toml::Deserializer::new, -); #[derive(thiserror::Error, Debug)] pub(crate) enum MetadataMigrateError { diff --git a/commons/Cargo.toml b/commons/Cargo.toml index cae74a13..30da05c8 100644 --- a/commons/Cargo.toml +++ b/commons/Cargo.toml @@ -25,7 +25,7 @@ walkdir = "2" filetime = "0.2" magic_migrate = "1.0.1" toml = "0.8" -cache_diff = "1.0" +cache_diff.workspace = true [dev-dependencies] filetime = "0.2" diff --git a/commons/src/layer/diff_migrate.rs b/commons/src/layer/diff_migrate.rs index aaf398c7..f9fc334c 100644 --- a/commons/src/layer/diff_migrate.rs +++ b/commons/src/layer/diff_migrate.rs @@ -93,7 +93,7 @@ use bullet_stream as _; /// When a `CacheDiff::diff` is empty, the layer is kept and the old data is returned. Otherwise, /// the layer is deleted and the changes are returned. /// -/// **TUTORIAL:** In the [`diff_migrate`] module docs +/// **TUTORIAL:** In the [`crate::layer::diff_migrate`] module docs #[derive(Debug, Clone, Eq, PartialEq)] pub struct DiffMigrateLayer { /// Whether the layer is intended for build. @@ -138,7 +138,7 @@ impl DiffMigrateLayer { /// When given a prior [`LayerRename::from`] that exists, but the [`LayerRename::to`] /// does not, then the contents of the prior layer will be copied before being deleted. /// - /// After that this function callse [`cached_layer`] on the new layer. + /// After that this function calls [`crate::layer::diff_migrate::DiffMigrateLayer::cached_layer`] on the new layer. /// /// # Panics /// @@ -320,10 +320,11 @@ mod tests { use libcnb::data::layer_name; use libcnb::generic::{GenericMetadata, GenericPlatform}; use libcnb::layer::{EmptyLayerCause, InvalidMetadataAction, LayerState, RestoredLayerAction}; - use magic_migrate::{migrate_toml_chain, try_migrate_deserializer_chain, Migrate, TryMigrate}; + use magic_migrate::TryMigrate; use std::convert::Infallible; /// Struct for asserting the behavior of `CacheBuddy` - #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] + #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, TryMigrate)] + #[try_migrate(from = None)] #[serde(deny_unknown_fields)] struct TestMetadata { value: String, @@ -337,7 +338,6 @@ mod tests { } } } - migrate_toml_chain! {TestMetadata} struct FakeBuildpack; impl libcnb::Buildpack for FakeBuildpack { @@ -555,13 +555,15 @@ mod tests { } /// Struct for asserting the behavior of `invalid_metadata_action` - #[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] + #[derive(serde::Deserialize, serde::Serialize, Debug, Clone, TryMigrate)] + #[try_migrate(from = None)] #[serde(deny_unknown_fields)] struct PersonV1 { name: String, } /// Struct for asserting the behavior of `invalid_metadata_action` - #[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] + #[derive(serde::Deserialize, serde::Serialize, Debug, Clone, TryMigrate)] + #[try_migrate(from = PersonV1)] #[serde(deny_unknown_fields)] struct PersonV2 { name: String, @@ -583,25 +585,11 @@ mod tests { } } } - #[derive(Debug, Eq, PartialEq)] + #[derive(Debug, Eq, PartialEq, thiserror::Error)] + #[error("Not Richard")] struct NotRichard { name: String, } - impl From for PersonMigrationError { - fn from(value: NotRichard) -> Self { - PersonMigrationError::NotRichard(value) - } - } - #[derive(Debug, Eq, PartialEq, thiserror::Error)] - enum PersonMigrationError { - #[error("Not Richard")] - NotRichard(NotRichard), - } - try_migrate_deserializer_chain!( - deserializer: toml::Deserializer::new, - error: PersonMigrationError, - chain: [PersonV1, PersonV2], - ); #[test] fn test_invalid_metadata_action() { diff --git a/commons/src/layer/fixtures/metadata_migration_example.md b/commons/src/layer/fixtures/metadata_migration_example.md index 24315314..bdbdbc00 100644 --- a/commons/src/layer/fixtures/metadata_migration_example.md +++ b/commons/src/layer/fixtures/metadata_migration_example.md @@ -12,9 +12,11 @@ In a layer file, define a metadata struct: ```rust use cache_diff::CacheDiff; +use magic_migrate::TryMigrate; use serde::{Deserialize, Serialize}; - #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff)] + #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff, TryMigrate)] + #[try_migrate(from = None)] #[serde(deny_unknown_fields)] pub(crate) struct MetadataV1 { #[cache_diff(rename = "Ruby version")] @@ -27,54 +29,14 @@ pub(crate) type Metadata = MetadataV1; This code: - Allows the struct to be [`serde::Serialize`]/[`serde::Deserialize`] as toml -- Sets some convenient traits [`Debug`], [`Clone`], [`Eq`], [`PartialEq`] -- Defines how the metadata is used to invalidate the cache with the [`CacheDiff`] derive -- Sets a convienece type alias for the latest Metadata +- Sets some convenient traits: [`Debug`], [`Clone`], [`Eq`], [`PartialEq`] +- Defines behavior for communicating and handling all possible cache states: + - The [`CacheDiff`] trait is used to handle cache invalidation (and related communication). + - The [`TryMigrate`] trait is used to handle invalid metadata (and related communication). +- Sets a convenience type alias for the latest Metadata In this code if the `version` field changes then the cache will be invalidated. -Now tell it how to migrate invalid metadata: - - -```rust -use magic_migrate::TryMigrate; -// ... -# use cache_diff::CacheDiff; -# use serde::{Deserialize, Serialize}; -# -# #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff)] -# #[serde(deny_unknown_fields)] -# pub(crate) struct MetadataV1 { -# #[cache_diff(rename = "Ruby version")] -# pub(crate) version: String, -# } -# -# pub(crate) type Metadata = MetadataV1; - - #[derive(Debug)] -pub(crate) enum MigrationError {} - -impl std::fmt::Display for MigrationError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - todo!() - } -} - -magic_migrate::try_migrate_toml_chain!( - error: MigrationError, - chain: [MetadataV1] -); -``` - -This code: - -- Defines an error type (so we can populate it when we need to add a failable migration) -- The error type needs to be `Display` and `Debug` -- Uses the `magic_migrate::try_migrate_toml_chain` macro to tell our code how it can migrate from one type to the next. - This will implement `TryMigrate` on every struct in the `chain` argument. In this case there's only one metadata value, - but we will implement this behavior now so it's easy to extend later. - - At this point we've implemented `CacheDiff` and `TryMigrate` on our metadata, so we can define a layer: ```rust @@ -94,28 +56,15 @@ use libcnb::layer_env::LayerEnv; # use cache_diff::CacheDiff; # use serde::{Deserialize, Serialize}; # -# #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff)] -# #[serde(deny_unknown_fields)] +# #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff, TryMigrate)] +# #[try_migrate(from = None)] +# #[serde(deny_unknown_fields)] # pub(crate) struct MetadataV1 { # #[cache_diff(rename = "Ruby version")] # pub(crate) version: String, # } # # pub(crate) type Metadata = MetadataV1; -# -# #[derive(Debug)] -# pub(crate) enum MigrationError {} -# -# impl std::fmt::Display for MigrationError { -# fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -# todo!() -# } -# } -# -# magic_migrate::try_migrate_toml_chain!( -# error: MigrationError, -# chain: [MetadataV1] -# ); fn install_ruby(version: &str, path: &std::path::Path) { todo!() @@ -188,23 +137,20 @@ use magic_migrate::TryMigrate; use cache_diff::CacheDiff; use serde::{Deserialize, Serialize}; -# #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] -# #[serde(deny_unknown_fields)] +// ... +# #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff, TryMigrate)] +# #[try_migrate(from = None)] +# #[serde(deny_unknown_fields)] # pub(crate) struct MetadataV1 { +# #[cache_diff(rename = "Ruby version")] # pub(crate) version: String, # } # -# -# #[derive(Debug)] -# pub(crate) enum MigrationError {} -# -# impl std::fmt::Display for MigrationError { -# fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -# todo!() -# } -# } -# - #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff)] +# fn get_distro_from_current_os() -> String { unimplemented!() } +# fn get_arch_from_current_cpu() -> String { unimplemented!() } + + #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff, TryMigrate)] + #[try_migrate(from = MetadataV1)] #[serde(deny_unknown_fields)] pub(crate) struct MetadataV2 { #[cache_diff(rename = "Ruby version")] @@ -214,13 +160,8 @@ pub(crate) struct MetadataV2 { pub(crate) distro: String } -fn get_distro_from_current_os() -> String { - // Just pretend, ok - todo!(); -} - impl TryFrom for MetadataV2 { - type Error = MigrationError; + type Error = std::convert::Infallible; fn try_from(old: MetadataV1) -> Result { Ok(Self { @@ -231,20 +172,15 @@ impl TryFrom for MetadataV2 { } pub(crate) type Metadata = MetadataV2; - -magic_migrate::try_migrate_toml_chain!( - error: MigrationError, - chain: [MetadataV1, MetadataV2] -); ``` Here we added: - A new struct `MetadataV2` with a new field `distro` that `V1` does not have. - Updated the `type Metadata = MetadataV2` to `V2` -- Added `MetadataV2` to the end of our migration chain. +- Taught `TryMigrate` that it can build a `MetadataV2` from a `MetadataV1` serialized toml value. -Now when our layer logic is called, it will first try to deserialize the contents into `MetadataV2` if it can it will return that and continue on to the cache invalidation logic. If not, it will try to deserialize the old toml into `MetadataV1`. If it can, then it will and then migrate from `MetadataV1` to `MetadataV2` using the `TryFrom` and `TryMigrate` traits. +Now when our layer logic is called, it will first try to deserialize the contents into `MetadataV2`. If it fails, it will try to deserialize the old toml into `MetadataV1`. If it can, then it will and then migrate from `MetadataV1` to `MetadataV2` using the [`TryFrom`] and [`TryMigrate`] traits. ## Handle migration errors @@ -256,41 +192,31 @@ use magic_migrate::TryMigrate; use cache_diff::CacheDiff; use serde::{Deserialize, Serialize}; -# #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] -# #[serde(deny_unknown_fields)] +// ... +# #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff, TryMigrate)] +# #[try_migrate(from = None)] +# #[serde(deny_unknown_fields)] # pub(crate) struct MetadataV1 { +# #[cache_diff(rename = "Ruby version")] # pub(crate) version: String, # } # +# fn get_distro_from_current_os() -> String { unimplemented!() } +# fn get_arch_from_current_cpu() -> String { unimplemented!() } # - #[derive(Debug)] -pub(crate) enum MigrationError { - InvalidVersionArch { - version: String, - arch: String, - } -} -# -# impl std::fmt::Display for MigrationError { -# fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -# todo!() -# } -# } -# -# #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +# #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff, TryMigrate)] +# #[try_migrate(from = MetadataV1)] # #[serde(deny_unknown_fields)] # pub(crate) struct MetadataV2 { +# #[cache_diff(rename = "Ruby version")] # pub(crate) version: String, -# pub(crate) distro: String -# } # -# fn get_distro_from_current_os() -> String { -# // Just pretend, ok -# todo!(); +# #[cache_diff(rename = "OS distribution")] +# pub(crate) distro: String # } # # impl TryFrom for MetadataV2 { -# type Error = MigrationError; +# type Error = std::convert::Infallible; # # fn try_from(old: MetadataV1) -> Result { # Ok(Self { @@ -299,9 +225,9 @@ pub(crate) enum MigrationError { # }) # } # } -# - #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff)] + #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, CacheDiff, TryMigrate)] + #[try_migrate(from = MetadataV2)] #[serde(deny_unknown_fields)] pub(crate) struct MetadataV3 { #[cache_diff(rename = "Ruby version")] @@ -314,17 +240,22 @@ pub(crate) struct MetadataV3 { pub(crate) arch: String } -# fn get_arch_from_current_cpu() -> String { todo!(); } + #[derive(Debug, thiserror::Error)] + #[error("Invalid distro {version} ({arch})")] +pub(crate) struct InvalidVersionArch { + version: String, + arch: String, +} impl TryFrom for MetadataV3 { - type Error = MigrationError; + type Error = InvalidVersionArch; fn try_from(old: MetadataV2) -> Result { let distro = get_distro_from_current_os(); let arch = get_arch_from_current_cpu(); if old.version.starts_with("1.") && &arch == "arm64" { Err( - MigrationError::InvalidVersionArch { + InvalidVersionArch { version: old.version, arch: arch } @@ -340,17 +271,12 @@ impl TryFrom for MetadataV3 { } pub(crate) type Metadata = MetadataV3; - -magic_migrate::try_migrate_toml_chain!( - error: MigrationError, - chain: [MetadataV1, MetadataV2, MetadataV3] -); ``` What did we do? We added: - A new `MetadataV3` with a new field `Arch` -- A new error variant to our `MigrationError` named `InvalidVersionArch`. +- A new error struct that implements `std::error::Error`. - A new `TryFrom` to `MetadataV3` that fails if we try to re-use version 1.x on an `arm64` CPU (an arbitrary specification made for this example). Then we: @@ -366,11 +292,11 @@ The two traits `CacheDiff` and `TryMigrate` are relatively simple, but combined, ## Q&A -- Q: Wait, do I have to support metadata schemas (structs) forever? -- A: No. You can drop old structs whenver you feel it's necessary or invalidate the metadata at any time you like. The key with making your metadata migrate-able is that you don't HAVE to invalidate with every change. It makes it easier to ship the behavior that's best for you and your users. +- Q: I don't want to use migration! +- A: That's more of a comment than a question. Even if you don't plan on implementing metadata migration, the `TryMigrate` trait bounds still require that you implement `#[try_migrate(from = None)]` on your struct. This communicates clearly that any string that cannot deserialize to that struct should trigger a cache invalidation. -- Q: Can I use default logic without having to implement both traits? It seems odd to add a `TryMigrate` trait for a scenario where we might never need one. -- A: If you put in work adopting this migration pattern and never need it, it's one crate, one trait, and one struct. Not that much work. But a co-worker or contributor new to buildpacks needs to modify it, or a future tired you needs to modify it...it's easier to extend an existing pattern than remember the esoteric rules and edge cases of what will and won't serialize into a struct. +- Q: If I add a migration do I have to support it forever? +- A: No. You can drop old structs whenver you feel it's necessary or invalidate the metadata at any time you like. The key with making your metadata migrate-able is that you don't HAVE to invalidate with every change. It makes it easier to ship the behavior that's best for you and your users. - Q: You used `Metadata` as a type alias for use outside of the module. If you have multiple modules wouldn't they all have the same import? Shouldn't you namespace them somehow? - A: Having to remember a naming convention for metadata in various layer modules is needless creativity. Instead of importing the struct, import the module and use that as a namespace, for example: @@ -399,6 +325,6 @@ When you rev your metadata version, you'll need to add or modify any attributes - A: Sure! - Make sure to `#[serde(deny_unknown_fields)]` on your metadata structs - Don't use overly flexible types such as `Option` unless you really have to. Metadata can be loaded wither with or without that attribute which might not be exactly what you want when you're deserializing old metadata. - - For layers that need to execute commands (such as `bundle install`), you can [use the `fun_run` crate](https://docs.rs/fun_run/0.2.0/fun_run/) which helps clearly print what's happening and gives lots of information when things fail. + - For layers that need to execute commands (such as `bundle install`), you can [use the `fun_run` crate](https://docs.rs/fun_run/latest/) which helps clearly print what's happening and gives lots of information when things fail. - Beware if v1 and v3 have the same named attributes, but different semantics rust will happily serialize the values stored from v1 into v3 and you'll never get an error or warning and your `TryFrom` code won't fire. This is also a problem when not using the `TryMigrate` pattern, so stay on the lookout. - For extremly important cache invalidation logic, add unit tests.