Skip to content

Commit 2da90a9

Browse files
committed
feat: Add stackable-shared crate
This crate initially contains code to be able to use our custom YAML serialization across workspace members. Previously, to get access to the specialized helper functions and traits one needed to import the complete stackable-operator crate. That's why this commit moves that piece of code into a shared place, which is way lighter to be imported by other workspace members. It additionally reworks the helpers to be slightly more generic. It still contains the docs URL replacer, which replaces the placeholder with the correct Stackable doc URL in doc comments and thus in OpenAPI schema descriptions.
1 parent 32a749d commit 2da90a9

File tree

5 files changed

+213
-0
lines changed

5 files changed

+213
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Please see the relevant crate changelogs:
66
- [stackable-certs](./crates/stackable-certs/CHANGELOG.md)
77
- [stackable-operator](./crates/stackable-operator/CHANGELOG.md)
88
- [stackable-operator-derive](./crates/stackable-operator-derive/CHANGELOG.md)
9+
- [stackable-shared](./crates/stackable-shared/CHANGELOG.md)
910
- [stackable-telemetry](./crates/stackable-telemetry/CHANGELOG.md)
1011
- [stackable-versioned](./crates/stackable-versioned/CHANGELOG.md)
1112
- [stackable-webhook](./crates/stackable-webhook/CHANGELOG.md)

crates/stackable-shared/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## [0.0.1]

crates/stackable-shared/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "stackable-shared"
3+
version = "0.0.1"
4+
authors.workspace = true
5+
license.workspace = true
6+
edition.workspace = true
7+
repository.workspace = true
8+
9+
[dependencies]
10+
kube.workspace = true
11+
semver.workspace = true
12+
serde.workspace = true
13+
serde_yaml.workspace = true
14+
snafu.workspace = true
15+
16+
[dev-dependencies]
17+
k8s-openapi.workspace = true

crates/stackable-shared/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//! This crate contains various shared helpers and utilities used across other crates in this
2+
//! workspace.
3+
4+
pub mod yaml;
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//! Utility functions for processing data in the YAML file format
2+
use std::{io::Write, path::Path, str::FromStr};
3+
4+
use semver::Version;
5+
use snafu::{ResultExt, Snafu};
6+
7+
const STACKABLE_DOCS_HOME_URL_PLACEHOLDER: &str = "DOCS_BASE_URL_PLACEHOLDER";
8+
const STACKABLE_DOCS_HOME_BASE_URL: &str = "https://docs.stackable.tech/home";
9+
10+
type Result<T, E = Error> = std::result::Result<T, E>;
11+
12+
/// Represents every error which can be encountered during YAML serialization.
13+
#[derive(Debug, Snafu)]
14+
pub enum Error {
15+
#[snafu(display("failed to serialize YAML"))]
16+
SerializeYaml { source: serde_yaml::Error },
17+
18+
#[snafu(display("failed to write YAML document separator"))]
19+
WriteDocumentSeparator { source: std::io::Error },
20+
21+
#[snafu(display("failed to write YAML to file"))]
22+
WriteToFile { source: std::io::Error },
23+
24+
#[snafu(display("failed to write YAML to stdout"))]
25+
WriteToStdout { source: std::io::Error },
26+
27+
#[snafu(display("failed to parse {input:?} as semantic version"))]
28+
ParseSemanticVersion {
29+
source: semver::Error,
30+
input: String,
31+
},
32+
33+
#[snafu(display("failed to parse bytes as valid UTF-8 string"))]
34+
ParseUtf8Bytes { source: std::string::FromUtf8Error },
35+
}
36+
37+
pub(crate) struct DocUrlReplacer<'a>(&'a str);
38+
39+
impl<'a> DocUrlReplacer<'a> {
40+
pub(crate) fn new(operator_version: &'a str) -> Self {
41+
Self(operator_version)
42+
}
43+
44+
fn replace(&self, input: &str) -> Result<String> {
45+
let docs_version = match self.0 {
46+
"0.0.0-dev" => "nightly".to_owned(),
47+
ver => {
48+
let v = Version::from_str(ver).context(ParseSemanticVersionSnafu { input })?;
49+
format!("{major}.{minor}", major = v.major, minor = v.minor)
50+
}
51+
};
52+
53+
Ok(input.replace(
54+
STACKABLE_DOCS_HOME_URL_PLACEHOLDER,
55+
&format!("{STACKABLE_DOCS_HOME_BASE_URL}/{docs_version}"),
56+
))
57+
}
58+
}
59+
60+
/// Provides configurable options during YAML serialization.
61+
///
62+
/// For most people the default implementation [`SerializeOptions::default()`] is sufficient as it
63+
/// enables explicit document and singleton map serialization.
64+
pub struct SerializeOptions {
65+
/// Adds leading triple dashes (`---`) to the output string.
66+
pub explicit_document: bool,
67+
68+
/// Serialize enum variants as YAML maps using the variant name as the key.
69+
pub singleton_map: bool,
70+
}
71+
72+
impl Default for SerializeOptions {
73+
fn default() -> Self {
74+
Self {
75+
explicit_document: true,
76+
singleton_map: true,
77+
}
78+
}
79+
}
80+
81+
/// Serializes any type `T` which is [serializable](serde::Serialize) as YAML using the provided
82+
/// [`SerializeOptions`].
83+
///
84+
/// It additionally replaces the documentation URL placeholder with the correct value based on the
85+
/// provided `operator_version`.
86+
pub trait YamlSchema: Sized + serde::Serialize {
87+
/// Generates the YAML schema of `self` using the provided [`SerializeOptions`].
88+
fn generate_yaml_schema(
89+
&self,
90+
operator_version: &str,
91+
options: SerializeOptions,
92+
) -> Result<String> {
93+
let mut buffer = Vec::new();
94+
95+
serialize(&self, &mut buffer, options)?;
96+
97+
let yaml_string = String::from_utf8(buffer).context(ParseUtf8BytesSnafu)?;
98+
99+
let replacer = DocUrlReplacer::new(operator_version);
100+
let yaml_string = replacer.replace(&yaml_string)?;
101+
102+
Ok(yaml_string)
103+
}
104+
105+
/// Generates and write the YAML schema of `self` to a file at `path` using the provided
106+
/// [`SerializeOptions`].
107+
fn write_yaml_schema<P: AsRef<Path>>(
108+
&self,
109+
path: P,
110+
operator_version: &str,
111+
options: SerializeOptions,
112+
) -> Result<()> {
113+
let schema = self.generate_yaml_schema(operator_version, options)?;
114+
std::fs::write(path, schema).context(WriteToFileSnafu)
115+
}
116+
117+
/// Generates and prints the YAML schema of `self` to stdout at `path` using the provided
118+
/// [`SerializeOptions`].
119+
fn print_yaml_schema(&self, operator_version: &str, options: SerializeOptions) -> Result<()> {
120+
let schema = self.generate_yaml_schema(operator_version, options)?;
121+
122+
let mut writer = std::io::stdout();
123+
writer
124+
.write_all(schema.as_bytes())
125+
.context(WriteToStdoutSnafu)
126+
}
127+
}
128+
129+
impl<T> YamlSchema for T where T: serde::ser::Serialize {}
130+
131+
/// Provides YAML schema generation and output capabilities for Kubernetes custom resources.
132+
pub trait CustomResourceExt: kube::CustomResourceExt {
133+
/// Generates the YAML schema of a `CustomResourceDefinition` and writes it to the specified
134+
/// file at `path`.
135+
///
136+
/// It additionally replaces the documentation URL placeholder with the correct value based on
137+
/// the provided `operator_version`. The written YAML string is an explicit document with
138+
/// leading dashes (`---`).
139+
fn write_yaml_schema<P: AsRef<Path>>(path: P, operator_version: &str) -> Result<()> {
140+
Self::crd().write_yaml_schema(path, operator_version, SerializeOptions::default())
141+
}
142+
143+
/// Generates the YAML schema of a `CustomResourceDefinition` and prints it to [stdout].
144+
///
145+
/// It additionally replaces the documentation URL placeholder with the correct value based on
146+
/// the provided `operator_version`. The written YAML string is an explicit document with
147+
/// leading dashes (`---`).
148+
///
149+
/// [stdout]: std::io::stdout
150+
fn print_yaml_schema(operator_version: &str) -> Result<()> {
151+
Self::crd().print_yaml_schema(operator_version, SerializeOptions::default())
152+
}
153+
154+
/// Generates the YAML schema of a `CustomResourceDefinition` and returns it as a [`String`].
155+
fn yaml_schema(operator_version: &str) -> Result<String> {
156+
Self::crd().generate_yaml_schema(operator_version, SerializeOptions::default())
157+
}
158+
}
159+
160+
impl<T> CustomResourceExt for T where T: kube::CustomResourceExt {}
161+
162+
/// Serializes the given data structure and writes it to a [`Writer`](Write).
163+
pub fn serialize<T, W>(value: &T, mut writer: W, options: SerializeOptions) -> Result<()>
164+
where
165+
T: serde::Serialize,
166+
W: std::io::Write,
167+
{
168+
if options.explicit_document {
169+
writer
170+
.write_all(b"---\n")
171+
.context(WriteDocumentSeparatorSnafu)?;
172+
}
173+
174+
let mut serializer = serde_yaml::Serializer::new(writer);
175+
176+
if options.singleton_map {
177+
serde_yaml::with::singleton_map_recursive::serialize(value, &mut serializer)
178+
.context(SerializeYamlSnafu)?;
179+
} else {
180+
value
181+
.serialize(&mut serializer)
182+
.context(SerializeYamlSnafu)?;
183+
}
184+
185+
Ok(())
186+
}

0 commit comments

Comments
 (0)