use std::{collections::BTreeMap, str::FromStr};
use derive_more::From;
#[cfg(feature = "schema")] use schemars::schema::Schema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Rule {
pub rule: String,
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<Message>,
#[serde(skip_serializing_if = "Option::is_none")]
pub field_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<Reason>,
}
impl Rule {
pub fn new(rule: impl Into<String>) -> Self {
Self {
rule: rule.into(),
..Default::default()
}
}
pub fn message(mut self, message: impl Into<Message>) -> Self {
self.message = Some(message.into());
self
}
pub fn reason(mut self, reason: impl Into<Reason>) -> Self {
self.reason = Some(reason.into());
self
}
pub fn field_path(mut self, field_path: impl Into<String>) -> Self {
self.field_path = Some(field_path.into());
self
}
}
impl From<&str> for Rule {
fn from(value: &str) -> Self {
Self {
rule: value.into(),
..Default::default()
}
}
}
impl From<(&str, &str)> for Rule {
fn from((rule, msg): (&str, &str)) -> Self {
Self {
rule: rule.into(),
message: Some(msg.into()),
..Default::default()
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Message {
Message(String),
#[serde(rename = "messageExpression")]
Expression(String),
}
impl From<&str> for Message {
fn from(value: &str) -> Self {
Message::Message(value.to_string())
}
}
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
pub enum Reason {
#[default]
FieldValueInvalid,
FieldValueForbidden,
FieldValueRequired,
FieldValueDuplicate,
}
impl FromStr for Reason {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
#[cfg(feature = "schema")]
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
pub fn validate(s: &mut Schema, rule: impl Into<Rule>) -> Result<(), serde_json::Error> {
let rule: Rule = rule.into();
match s {
Schema::Bool(_) => (),
Schema::Object(schema_object) => {
let rule = serde_json::to_value(rule)?;
schema_object
.extensions
.entry("x-kubernetes-validations".into())
.and_modify(|rules| {
if let Value::Array(rules) = rules {
rules.push(rule.clone());
}
})
.or_insert(serde_json::to_value(&[rule])?);
}
};
Ok(())
}
#[cfg(feature = "schema")]
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
pub fn validate_property(
s: &mut Schema,
property_index: usize,
rule: impl Into<Rule>,
) -> Result<(), serde_json::Error> {
match s {
Schema::Bool(_) => (),
Schema::Object(schema_object) => {
let obj = schema_object.object();
for (n, (_, schema)) in obj.properties.iter_mut().enumerate() {
if n == property_index {
return validate(schema, rule);
}
}
}
};
Ok(())
}
#[cfg(feature = "schema")]
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
pub fn merge_properties(s: &mut Schema, merge: &mut Schema) {
match s {
schemars::schema::Schema::Bool(_) => (),
schemars::schema::Schema::Object(schema_object) => {
let obj = schema_object.object();
for (k, v) in &merge.clone().into_object().object().properties {
obj.properties.insert(k.clone(), v.clone());
}
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ListMerge {
Atomic,
Set,
Map(Vec<String>),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum MapMerge {
Atomic,
Granular,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum StructMerge {
Atomic,
Granular,
}
#[derive(From, Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum MergeStrategy {
#[serde(rename = "x-kubernetes-list-type")]
ListType(ListMerge),
#[serde(rename = "x-kubernetes-map-type")]
MapType(MapMerge),
#[serde(rename = "x-kubernetes-struct-type")]
StructType(StructMerge),
}
impl MergeStrategy {
fn keys(self) -> Result<BTreeMap<String, Value>, serde_json::Error> {
if let Self::ListType(ListMerge::Map(keys)) = self {
let mut data = BTreeMap::new();
data.insert("x-kubernetes-list-type".into(), "map".into());
data.insert("x-kubernetes-list-map-keys".into(), serde_json::to_value(&keys)?);
return Ok(data);
}
let value = serde_json::to_value(self)?;
serde_json::from_value(value)
}
}
#[cfg(feature = "schema")]
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
pub fn merge_strategy_property(
s: &mut Schema,
property_index: usize,
strategy: impl Into<MergeStrategy>,
) -> Result<(), serde_json::Error> {
match s {
Schema::Bool(_) => (),
Schema::Object(schema_object) => {
let obj = schema_object.object();
for (n, (_, schema)) in obj.properties.iter_mut().enumerate() {
if n == property_index {
return merge_strategy(schema, strategy.into());
}
}
}
};
Ok(())
}
#[cfg(feature = "schema")]
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
pub fn merge_strategy(s: &mut Schema, strategy: MergeStrategy) -> Result<(), serde_json::Error> {
match s {
Schema::Bool(_) => (),
Schema::Object(schema_object) => {
for (key, value) in strategy.keys()? {
schema_object.extensions.insert(key, value);
}
}
};
Ok(())
}