kopium icon indicating copy to clipboard operation
kopium copied to clipboard

One-of required fields should generate Rust enums, not Option<T>

Open alex-hunt-materialize opened this issue 3 years ago • 3 comments

As only one value can be supplied at a time, it would be better to generate a Rust enum, so that the type system can enforce the one-of constraint.

Example:

              podSelector:
                description: Selects pods in the same namespace.
                oneOf:
                - required:
                  - matchExpressions
                - required:
                  - matchLabels
                properties:
                  matchExpressions:
                    items:
                      properties:
                        key:
                          type: string
                        operator:
                          enum:
                          - In
                          - NotIn
                          - Exists
                          - DoesNotExist
                          type: string
                        values:
                          items:
                            type: string
                          type: array
                      required:
                      - key
                      - operator
                      type: object
                    type: array
                  matchLabels:
                    type: object
                    x-kubernetes-preserve-unknown-fields: true
                type: object

This generates:

/// Selects pods in the same namespace.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelector {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub matchExpressions: Vec<ServerPodSelectorMatchExpressions>,
    pub matchLabels: Option<ServerPodSelectorMatchLabels>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelectorMatchExpressions {
    pub key: String,
    pub operator: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub values: Vec<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelectorMatchLabels {
}

Complete CRD https://gist.github.com/alex-hunt-materialize/2743b1e2e58a49c4df0a11ecb39f46ab

alex-hunt-materialize avatar Apr 01 '22 20:04 alex-hunt-materialize

Had a bit of a look at this in #65. I'm expecting we will want to generate something like:

pub enum ServerPodSelector {
    MatchExpressions(Vec<ServerPodSelectorMatchExpressions>),
    MatchLabels(BTreeMap<String, serde_json::Value>), 
}

pub struct ServerPodSelectorMatchExpressions {
    pub key: String,
    pub operator: ServerPodSelectorMatchExpressionsOperator,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub values: Option<Vec<String>>,
}

pub enum ServerPodSelectorMatchExpressionsOperator {
    In,
    NotIn,
    Exists,
    DoesNotExists,
}

depending on choice in #31 for the preserve prop.. need to cross-reference with kube-derive to see what it is generating.

clux avatar May 01 '22 13:05 clux

Ok, verified against kube-derive. The following example:

use kube::{CustomResource, CustomResourceExt};
use schemars::JsonSchema;
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};

#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[kube(
    group = "clux.dev",
    version = "v1",
    kind = "Server",
    namespaced,
)]
#[serde(rename_all = "camelCase")]
pub struct ServerSpec {
    pod_selector: ServerPodSelector
}

#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum ServerPodSelector {
  MatchExpressions(Vec<ServerPodSelectorMatchExpressions>),
  MatchLabels(BTreeMap<String, String>),
}

#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ServerPodSelectorMatchExpressions {
  pub key: String,
  pub operator: ServerPodSelectorMatchExpressionsOperator,
  #[serde(default, skip_serializing_if = "Option::is_none")]
  pub values: Option<Vec<String>>,
}

#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
pub enum ServerPodSelectorMatchExpressionsOperator {
  In,
  NotIn,
  Exists,
  DoesNotExists,
}


fn main() {
    let crd = serde_yaml::to_string(&Server::crd()).unwrap();
    println!("CRD: \n{}", crd);
}

prints the crd with the following schema:

          description: "Auto-generated derived type for ServerSpec via `CustomResource`"
          properties:
            spec:
              properties:
                podSelector:
                  oneOf:
                    - required:
                        - matchExpressions
                    - required:
                        - matchLabels
                  properties:
                    matchExpressions:
                      items:
                        properties:
                          key:
                            type: string
                          operator:
                            enum:
                              - In
                              - NotIn
                              - Exists
                              - DoesNotExists
                            type: string
                          values:
                            items:
                              type: string
                            nullable: true
                            type: array
                        required:
                          - key
                          - operator
                        type: object
                      type: array
                    matchLabels:
                      additionalProperties: true
                      type: object
                  type: object
              required:
                - podSelector
              type: object
          required:
            - spec
          title: Server
          type: object

which matches almost perfectly.

clux avatar May 01 '22 15:05 clux

That output looks fantastic!

alex-hunt-materialize avatar May 02 '22 17:05 alex-hunt-materialize