Skip to content

Commit

Permalink
Merge pull request #201 from helsing-ai/mara/manifest-version
Browse files Browse the repository at this point in the history
Implement a canary edition system
  • Loading branch information
mara-schulke authored Dec 26, 2023
2 parents 9ee33a6 + 06b830a commit d9a8f58
Show file tree
Hide file tree
Showing 14 changed files with 302 additions and 31 deletions.
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* [Continuous Integration](migration/continuous-integration.md)

* [Buffrs Reference](reference/index.md)
* [Editions](reference/editions.md)
* [Specifying Dependencies]()
* [Overriding Dependencies]()
* [The Manifest Format]()
Expand Down
40 changes: 40 additions & 0 deletions docs/src/reference/editions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Editions

Editions of buffrs mark a specific evolutionary state of the package manager.
The edition system exists to allow for fast development of buffrs while
allowing you to already migrate existing protobufs to buffrs even though it
has not yet reached a stable version.

Editions can be either explicitly stated in the `Proto.toml` or are
automatically inlined once a package is created using buffrs. This ensures that
you dont need to care about them as a user but get the benefits.

> Note: If you release a package with an edition that is incompatible with
> another one (e.g. if `0.7` is incompatible with `0.8`) you will need to
> re-release the package for the new edition (by bumping the version, or
> overriding the existing package) to regain compatibility.
You may see errors like this if you try to consume (or work on) a package of
another edition.

```
Error: × could not deserialize Proto.toml
╰─▶ TOML parse error at line 1, column 1
|
1 | edition = "0.7"
| ^^^^^^^^^^^^^^^
unsupported manifest edition, supported editions of 0.8.0 are: 0.8
```

### Canary Editions

```toml
edition = "0.7"
```

Canary editions are short-lived editions that are attached to a specific
minor release of buffrs in the `0.x.x` version range. The edition name contains
the minor version it is usable for. E.g. the edition `0.7` is usable /
supported by all `0.7.x` buffrs releases. Compatibility beyond minor releases
is not guaranteed as fundamental breaking changes may be introduced between
editions.
1 change: 1 addition & 0 deletions docs/src/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ This section covers detailed documentation on the Buffrs Ecosystem / Framework.

## Index

* [Editions](editions.md)
* [Protocol Buffer Rules](protocol-buffer-rules.md)
2 changes: 1 addition & 1 deletion registry/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,7 @@ pub async fn init(kind: Option<PackageType>, name: Option<PackageName>) -> miett
})
.transpose()?;

let manifest = Manifest {
package,
dependencies: vec![],
};
let manifest = Manifest::new(package, vec![]);

manifest.write().await?;

Expand Down
246 changes: 233 additions & 13 deletions src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,213 @@ use crate::{
/// The name of the manifest file
pub const MANIFEST_FILE: &str = "Proto.toml";

/// A `buffrs` manifest format used for serialization and deserialization.
/// The canary edition supported by this version of buffrs
pub const CANARY_EDITION: &str = concat!("0.", env!("CARGO_PKG_VERSION_MINOR"));

/// Edition of the buffrs manifest
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Edition {
/// The canary edition of manifests
///
/// This indicates that breaking changes and unstable behavior can occur
/// at any time. Users are responsible for consulting documentation and
/// help channels if errors occur.
Canary,
/// Unknown edition of manifests
///
/// This is unrecommended as breaking changes could be introduced due to being
/// in the beta release channel
Unknown,
}

impl Edition {
/// The current / latest edition of buffrs
pub fn latest() -> Self {
Self::Canary
}
}

/// A buffrs manifest format used for serialization and deserialization.
///
/// This contains the exact structure of the `Proto.toml` and skips
/// empty fields.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct RawManifest {
package: Option<PackageManifest>,
#[serde(default)]
dependencies: DependencyMap,
#[derive(Debug, Clone, PartialEq, Eq)]
enum RawManifest {
Canary {
package: Option<PackageManifest>,
dependencies: DependencyMap,
},
Unknown {
package: Option<PackageManifest>,
dependencies: DependencyMap,
},
}

impl RawManifest {
fn package(&self) -> Option<&PackageManifest> {
match self {
Self::Canary { package, .. } => package.as_ref(),
Self::Unknown { package, .. } => package.as_ref(),
}
}

fn dependencies(&self) -> &DependencyMap {
match self {
Self::Canary { dependencies, .. } => dependencies,
Self::Unknown { dependencies, .. } => dependencies,
}
}

fn edition(&self) -> Edition {
match self {
Self::Canary { .. } => Edition::Canary,
Self::Unknown { .. } => Edition::Unknown,
}
}
}

mod serializer {
use super::*;
use serde::{ser::SerializeStruct, Serializer};

impl Serialize for Edition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Canary => serializer.serialize_str(CANARY_EDITION),
Self::Unknown => serializer.serialize_str("unknown"),
}
}
}

impl Serialize for RawManifest {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
RawManifest::Canary {
ref package,
ref dependencies,
} => {
let mut s = serializer.serialize_struct("Canary", 3)?;
s.serialize_field("edition", CANARY_EDITION)?;
s.serialize_field("package", package)?;
s.serialize_field("dependencies", dependencies)?;
s.end()
}
RawManifest::Unknown {
ref package,
ref dependencies,
} => {
let mut s = serializer.serialize_struct("Unknown", 2)?;
s.serialize_field("package", package)?;
s.serialize_field("dependencies", dependencies)?;
s.end()
}
}
}
}
}

mod deserializer {
use serde::{
de::{self, MapAccess, Visitor},
Deserializer,
};

use super::*;

impl<'de> Deserialize<'de> for Edition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct EditionVisitor;

impl<'de> serde::de::Visitor<'de> for EditionVisitor {
type Value = Edition;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid edition string")
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match value {
c if c == CANARY_EDITION => Ok(Edition::Canary),
_ => Ok(Edition::Unknown),
}
}
}

deserializer.deserialize_str(EditionVisitor)
}
}

impl<'de> Deserialize<'de> for RawManifest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
static FIELDS: &[&str] = &["package", "dependencies"];

struct ManifestVisitor;

impl<'de> Visitor<'de> for ManifestVisitor {
type Value = RawManifest;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a buffrs manifest (`Proto.toml`)")
}

fn visit_map<V>(self, mut map: V) -> Result<RawManifest, V::Error>
where
V: MapAccess<'de>,
{
let mut edition: Option<String> = None;
let mut package: Option<PackageManifest> = None;
let mut dependencies: Option<HashMap<PackageName, DependencyManifest>> = None;

while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"package" => package = Some(map.next_value()?),
"dependencies" => dependencies = Some(map.next_value()?),
"edition" => edition = Some(map.next_value()?),
_ => return Err(de::Error::unknown_field(&key, FIELDS)),
}
}

let dependencies = dependencies.unwrap_or_default();

let Some(edition) = edition else {
return Ok(RawManifest::Unknown {
package,
dependencies,
});
};

let edition = serde_typename::from_str(&edition);

match edition {
Ok(Edition::Canary) => Ok(RawManifest::Canary {
package,
dependencies,
}),
Ok(Edition::Unknown) | Err(_) => Err(de::Error::custom(
format!("unsupported manifest edition, supported editions of {} are: {CANARY_EDITION}", env!("CARGO_PKG_VERSION"))
)),
}
}
}

deserializer.deserialize_map(ManifestVisitor)
}
}
}

impl From<Manifest> for RawManifest {
Expand All @@ -52,9 +250,15 @@ impl From<Manifest> for RawManifest {
.map(|dep| (dep.package, dep.manifest))
.collect();

Self {
package: manifest.package,
dependencies,
match manifest.edition {
Edition::Canary => RawManifest::Canary {
package: manifest.package,
dependencies,
},
Edition::Unknown => RawManifest::Unknown {
package: manifest.package,
dependencies,
},
}
}
}
Expand All @@ -70,17 +274,28 @@ impl FromStr for RawManifest {
/// Map representation of the dependency list
pub type DependencyMap = HashMap<PackageName, DependencyManifest>;

/// The `buffrs` manifest format used for internal processing, contains a parsed
/// The buffrs manifest format used for internal processing, contains a parsed
/// version of the `RawManifest` for easier use.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Manifest {
/// Edition of this manifest
pub edition: Edition,
/// Metadata about the root package
pub package: Option<PackageManifest>,
/// List of packages the root package depends on
pub dependencies: Vec<Dependency>,
}

impl Manifest {
/// Create a new manifest of the current edition
pub fn new(package: Option<PackageManifest>, dependencies: Vec<Dependency>) -> Self {
Self {
edition: Edition::latest(),
package,
dependencies,
}
}

/// Checks if the manifest file exists in the filesystem
pub async fn exists() -> miette::Result<bool> {
fs::try_exists(MANIFEST_FILE)
Expand Down Expand Up @@ -120,7 +335,11 @@ impl Manifest {

/// Persists the manifest into the current directory
pub async fn write(&self) -> miette::Result<()> {
let raw = RawManifest::from(self.to_owned());
// hint: create a canary manifest from the current one
let raw = RawManifest::from(Manifest::new(
self.package.clone(),
self.dependencies.clone(),
));

fs::write(
MANIFEST_FILE,
Expand All @@ -138,7 +357,7 @@ impl Manifest {
impl From<RawManifest> for Manifest {
fn from(raw: RawManifest) -> Self {
let dependencies = raw
.dependencies
.dependencies()
.iter()
.map(|(package, manifest)| Dependency {
package: package.to_owned(),
Expand All @@ -147,7 +366,8 @@ impl From<RawManifest> for Manifest {
.collect();

Self {
package: raw.package,
edition: raw.edition(),
package: raw.package().cloned(),
dependencies,
}
}
Expand Down
Loading

0 comments on commit d9a8f58

Please sign in to comment.