-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add optional schema #104
Add optional schema #104
Conversation
- Schema is generated by re-serializing, so any fixups are included. - `$ref` is inlined by calling `T::schema()` and overriding any description if specified.
The "Files changed" tab lags both Firefox and Chromium to death, so this is going to be a great PR to review :D |
writeln!(writer)?; | ||
writeln!(writer, r#"#[cfg(feature = "schema")]"#)?; | ||
writeln!(writer, r#"impl {type_name} {{"#, type_name = type_name)?; | ||
writeln!(writer, r" pub fn schema() -> serde_json::Value {{")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're going to implement the thing for expanding $ref
s anyway, is there a reason to return serde_json::Value
here instead of schemars::JsonSchema
?
Also, that would allow us to emit code that directly creates a schemars::SchemaObject
, instead of round-tripping through serde_json::json!
. Then the user doesn't have to compile every invocation of that proc macro.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean, why not impl schemars::JsonSchema
?
- Making
k8s_openapi
depend onschemars
felt awkward to me. I like how it doesn't restrict users to a specific implementation.kube
currently usesschemars
, but there are some missing features that might not make sense to add to a general JSON schema library, so this might change in the future. By keeping the schemaserde_json::Value
, it can be also useful for other use cases (as you wrote in Interop with schemars crate - impl schemars::JsonSchema for resource types #86 (comment)). Also, it should be pretty easy to make use of this information forschemars
. impl schemars::JsonSchema
seems like a lot of work if we want to avoidserde_json::json!
. Methods used in derivedJsonSchema
to help are private too. Maybe I'm missing something, though. I haven't manually implementedJsonSchema
for complex types. We'll need to generate code to buildSchemaObject
fromswagger20::Schema
. For each$ref
, we'll need to do<T as schemars::JsonSchema>::add_schema_as_property
, but this is not part of the public API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If #[derive(schemars::JsonSchema]
can do it, then it's public API. Unless it's #[doc(hidden)]
because the maintainer doesn't want other people to use it. Looking at what #[derives(schemars::JsonSchema)]
expands to should answer that.
But the point about being agnostic to the crate is taken. How does a user who does want to use #[derive(schemars::JsonSchema)]
on their type that contains k8s_openapi
types use this serde_json::Value
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's #[doc(hidden)]
in the latest published version, and it was then removed (GREsau/schemars@55b8604#diff-88895b0c150fd6f777b3df6591cbb00c7b88b100f8d99b769babe68315fd2cd0).
How does a user who does want to use
#[derive(schemars::JsonSchema)]
on their type that containsk8s_openapi
types use thisserde_json::Value
?
The easiest is by using #[schemars(schema_with = "schema_fn")]
, which is already necessary when adding validations and Kubernetes extensions. The original example from #86 is possible with:
#[derive(CustomResource, Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[kube(group = "example.com", version = "v1", kind = "Example")]
#[kube(shortname = "ex", namespaced)]
pub struct ExampleSpec {
#[schemars(schema_with = "optional_pvc_spec_schema")]
pub pvcspec: Option<PersistentVolumeClaimSpec>,
}
fn optional_pvc_spec_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = PersistentVolumeClaimSpec::schema();
// for `Option`
schema
.as_object_mut()
.expect("schema object")
.insert("nullable".to_owned(), serde_json::json!(true));
serde_json::from_value(schema).unwrap()
}
(UPDATE: can ignore the rest of this comment, and see the next one)
If we add k8s_openapi::Schema
trait, we can do something like this too:
#[derive(CustomResource, Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[kube(group = "example.com", version = "v1", kind = "Example")]
#[kube(shortname = "ex", namespaced)]
pub struct ExampleSpec {
#[schemars(with = "Nullable<PersistentVolumeClaimSpec>")]
pub pvcspec: Option<PersistentVolumeClaimSpec>,
}
with
struct Nullable<'a, T> {
phantom: std::marker::PhantomData<&'a T>,
}
impl<T> schemars::JsonSchema for Nullable<'_, T>
where
T: k8s_openapi::Schema,
{
fn schema_name() -> String {
// Doesn't guarantee uniqueness, but it doesn't matter for us because
// we're not using `definitions`.
std::any::type_name::<T>().to_owned()
}
fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = T::schema();
schema
.as_object_mut()
.expect("schema object")
.insert("nullable".to_owned(), serde_json::json!(true));
serde_json::from_value(schema).unwrap()
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add the trait because it can be just:
pub struct ExampleSpec {
#[schemars(with = "Option<SchemaValue<PersistentVolumeClaimSpec>>")]
pub pvcspec: Option<PersistentVolumeClaimSpec>,
}
with
struct SchemaValue<'a, T> {
phantom: std::marker::PhantomData<&'a T>,
}
impl<T> schemars::JsonSchema for SchemaValue<'_, T>
where
T: k8s_openapi::Schema,
{
fn schema_name() -> String {
std::any::type_name::<T>().to_owned()
}
fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
serde_json::from_value(T::schema()).unwrap()
}
}
I know this has been blocked on me for some time, but I've been busy and will probably be busy for another two weeks due to IRL stuff. Sorry. |
2241b88
to
71cc027
Compare
Okay, sorry for the delay. I should have time more consistently now. I'll continue the discussion from #104 (comment) here so that it's easier to find. You said:
But AFAICT this isn't a problem. And specifically, for Anyway, going through your change, I remembered why I suggested implementing Each of these is an infinite recursion. Currently this is only a problem for Impling If we want something generic that isn't tied to trait SchemaGenerator {
// Unlike schemars::gen::SchemaGenerator::subschema_for, this doesn't need to return a value.
fn visit_subschema<T: Schema>(&mut self);
}
impl Schema for Pod {
fn schema(gen: &mut SchemaGenerator) -> (&'static str, serde_json::Value, BTreeMap<&'static str, fn() -> serde_json::Value>) {
let schema_name = "io.k8s.api.core.v1.Pod";
gen.visit_subschema::<crate::apimachinery::pkg::apis::meta::v1::ObjectMeta>();
let schema = serde_json! {{
"properties": {
"metadata": { "$ref": "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" }
},
}};
}
} But even with this, the impl of |
No problem, thanks for reviewing.
Can that Yeah, the circular reference in
I originally thought infinite recursion is still a problem even if we use If we go with If it's not as easy to do, maybe replace self referencing fields in |
Right, they're https://github.com/GREsau/schemars/blob/master/schemars_derive/src/lib.rs#L121 -> https://github.com/GREsau/schemars/blob/master/schemars_derive/src/schema_exprs.rs#L12 -> https://github.com/GREsau/schemars/blob/master/schemars_derive/src/schema_exprs.rs#L436 -> https://github.com/GREsau/schemars/blob/master/schemars/src/_private.rs#L29 -> https://github.com/GREsau/schemars/blob/master/schemars/src/_private.rs#L36 ... and for our codegen we can just do the same expansion ourselves.
The way it works with impl JsonSchema for Metadata {
fn json_schema(gen) -> Schema {
// (1)
Schema::Object(SchemaObject {
object: Some(...),
})
}
}
impl JsonSchema for Pod {
fn json_schema(gen) -> Schema {
Schema::Object(SchemaObject {
object: Some(ObjectValidation {
properties: {
"metadata": gen.subschema_for("io...Metadata"), // (2)
},
}),
})
}
} (1) is the actual schema of
Can you clarify why it would be invalid? Do you mean that you'd consider a schema with |
Thanks for expanding on
It's a valid JSON Schema/Open API, but it's invalid for our use case because |
|
No, I knew that. I meant the recursive types won't be supported (for our use case) either way. The current version has infinite recursion, and |
For impl crate::schemars::JsonSchema for Pod {
fn schema_name() -> String {
"io.k8s.api.core.v1.Pod".to_owned()
}
fn json_schema(__gen: &mut crate::schemars::gen::SchemaGenerator) -> crate::schemars::schema::Schema {
crate::schemars::schema::Schema::Object(crate::schemars::schema::SchemaObject {
metadata: Some(Box::new(crate::schemars::schema::Metadata {
description: Some("Pod is a collection of containers that can run on a host. This resource is created by clients and scheduled onto hosts.".to_owned()),
..Default::default()
})),
object: Some(Box::new(crate::schemars::schema::ObjectValidation {
properties: std::array::IntoIter::new([
(
"apiVersion".to_owned(),
crate::schemars::schema::Schema::Object(crate::schemars::schema::SchemaObject {
metadata: Some(Box::new(crate::schemars::schema::Metadata {
description: Some("APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources".to_owned()),
..Default::default()
})),
instance_type: Some(std::convert::Into::into(crate::schemars::schema::InstanceType::String)),
..Default::default()
}),
),
(
"kind".to_owned(),
crate::schemars::schema::Schema::Object(crate::schemars::schema::SchemaObject {
metadata: Some(Box::new(crate::schemars::schema::Metadata {
description: Some("Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds".to_owned()),
..Default::default()
})),
instance_type: Some(std::convert::Into::into(crate::schemars::schema::InstanceType::String)),
..Default::default()
}),
),
(
"metadata".to_owned(),
__gen.subschema_for::<crate::apimachinery::pkg::apis::meta::v1::ObjectMeta>(),
),
(
"spec".to_owned(),
__gen.subschema_for::<crate::api::core::v1::PodSpec>(),
),
(
"status".to_owned(),
__gen.subschema_for::<crate::api::core::v1::PodStatus>(),
),
]).collect(),
required: std::array::IntoIter::new([
"metadata",
]).map(std::borrow::ToOwned::to_owned).collect(),
..Default::default()
})),
..Default::default()
})
}
} JSON{
"type": "object",
"description": "Pod is a collection of containers that can run on a host. This resource is created by clients and scheduled onto hosts.",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
"description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata"
},
"spec": {
"$ref": "#/definitions/io.k8s.api.core.v1.PodSpec",
"description": "Specification of the desired behavior of the pod. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status"
},
"status": {
"$ref": "#/definitions/io.k8s.api.core.v1.PodStatus",
"description": "Most recently observed status of the pod. This data may not be up to date. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status"
}
}
}
|
I started a thing here if you want to build off of it. |
I got it mostly working locally. Output of |
I was able to override description for Extensions can be added similarly, so there's no downsides now. I'll open a new PR later. |
Superseded by #105. |
$ref
is inlined by callingT::schema()
and appending any other values, including Kubernetes extensions.schemars
Closes #86
Output of pretty printed
Pod::schema()
: https://gist.github.com/kazk/e4a56b7ff9431416a1c29a1c602a7c8a