Repository: oxidecomputer/typify
Pull request #413 (2023-09-28 22:21:25)

simplify merging of subschemas


Patch:
diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml
index 8c80874..fea85e4 100644
--- a/typify-impl/Cargo.toml
+++ b/typify-impl/Cargo.toml
@@ -27,4 +27,5 @@ rustfmt-wrapper = "0.2"
 schema = "0.1.0"
 schemars = { version = "0.8.13", features = ["uuid1"] }
 serde = "1.0"
+syn = { version = "2.0", features = ["full", "extra-traits"] }
 uuid = "1.4.1"
diff --git a/typify-impl/src/merge.rs b/typify-impl/src/merge.rs
index dfccef4..bc49210 100644
--- a/typify-impl/src/merge.rs
+++ b/typify-impl/src/merge.rs
@@ -21,6 +21,11 @@ pub(crate) fn merge_all(schemas: &[Schema], defs: &BTreeMap<RefKey, Schema>) ->
         panic!("merge_all requires at least two schemas")
     };
 
+    debug!(
+        "merge all {}",
+        serde_json::to_string_pretty(schemas).unwrap(),
+    );
+
     try_merge_schema(a, b, defs)
         .and_then(|start| {
             ss.try_fold(start, |schema, other| {
@@ -93,13 +98,8 @@ fn try_merge_schema(a: &Schema, b: &Schema, defs: &BTreeMap<RefKey, Schema>) ->
         // remembers what we've already looked up; if we hit a cycle we can
         // consider the proper handling, but it might be to ignore it--a
         // circular allOf chain is a bit hard to reason about.
-        //
-        // TODO if we merge a referenced schema with another schema **and**
-        // the resulting schema is identical to the referenced schema (i.e.
-        // the other schema is strictly more permissive) then we should just
-        // return the reference schema.
         (
-            Schema::Object(SchemaObject {
+            ref_schema @ Schema::Object(SchemaObject {
                 reference: Some(ref_name),
                 ..
             }),
@@ -107,14 +107,26 @@ fn try_merge_schema(a: &Schema, b: &Schema, defs: &BTreeMap<RefKey, Schema>) ->
         )
         | (
             other,
-            Schema::Object(SchemaObject {
+            ref_schema @ Schema::Object(SchemaObject {
                 reference: Some(ref_name),
                 ..
             }),
         ) => {
             let key = ref_key(ref_name);
-            let resolved = defs.get(&key).unwrap();
-            try_merge_schema(resolved, other, defs)
+            let resolved = defs
+                .get(&key)
+                .unwrap_or_else(|| panic!("unresolved reference: {}", ref_name));
+            let merged_schema = try_merge_schema(resolved, other, defs)?;
+
+            // If we merge a referenced schema with another schema **and**
+            // the resulting schema is equivalent to the referenced schema
+            // (i.e. the other schema is identical or less permissive) then we
+            // just return the reference schema rather than its contents.
+            if merged_schema.roughly(resolved) {
+                Ok(ref_schema.clone())
+            } else {
+                Ok(merged_schema)
+            }
         }
 
         (Schema::Object(aa), Schema::Object(bb)) => Ok(merge_schema_object(aa, bb, defs)?.into()),
@@ -126,12 +138,6 @@ fn merge_schema_object(
     b: &SchemaObject,
     defs: &BTreeMap<RefKey, Schema>,
 ) -> Result<SchemaObject, ()> {
-    debug!(
-        "merging {}\n{}",
-        serde_json::to_string_pretty(a).unwrap(),
-        serde_json::to_string_pretty(b).unwrap(),
-    );
-
     assert!(a.reference.is_none());
     assert!(b.reference.is_none());
 
@@ -181,7 +187,9 @@ fn merge_schema_object(
     )?;
 
     debug!(
-        "merged {}",
+        "merging {}\n{}\n|\nv\n{}",
+        serde_json::to_string_pretty(a).unwrap(),
+        serde_json::to_string_pretty(b).unwrap(),
         serde_json::to_string_pretty(&merged_schema).unwrap(),
     );
 
@@ -299,6 +307,7 @@ fn try_merge_with_subschemas(
             then_schema: None,
             else_schema: None,
         }) => {
+            let schema = Schema::Object(schema_object.clone());
             // First we do a pairwise merge the schemas; if the result is
             // invalid / unresolvable / never / whatever, we exclude it
             // from the list. If it is valid, *then* we do the join to preserve
@@ -306,44 +315,53 @@ fn try_merge_with_subschemas(
             // one schema contains a ref). This could probably be an
             // opportunity for memoization, but this is an infrequent
             // construction so... whatever.
-            let joined_schemas = Some(
-                subschemas
-                    .iter()
-                    .filter_map(|other| {
-                        // Skip if the merged schema is unsatisfiable.
-                        let _ =
-                            try_merge_schema(&schema_object.clone().into(), other, defs).ok()?;
-                        // TODO if the merged result is equal to either whole,
-                        // we should just use that.
+            let joined_schemas = subschemas
+                .iter()
+                .filter_map(|other| {
+                    // Skip if the merged schema is unsatisfiable.
+                    let merged_schema = try_merge_schema(&schema, other, defs).ok()?;
+                    if merged_schema.roughly(&schema) {
+                        Some(schema.clone())
+                    } else if merged_schema.roughly(other) {
+                        Some(other.clone())
+                    } else {
                         Some(join_schema(&schema_object, other))
+                    }
+                })
+                .collect::<Vec<_>>();
+
+            match (joined_schemas.first(), joined_schemas.len()) {
+                // All merges failed so this is unsatisfiable.
+                (None, _) => Err(()),
+
+                // If there's a single schema left, just return that as our
+                // only option
+                (Some(only), 1) => Ok(only.clone().into_object()),
+
+                // Otherwise return the appropriate `oneOf` or `anyOf`
+                _ => {
+                    let subschemas = match maybe_subschemas {
+                        Some(SubschemaValidation {
+                            any_of: Some(_), ..
+                        }) => SubschemaValidation {
+                            any_of: Some(joined_schemas),
+                            ..Default::default()
+                        },
+                        Some(SubschemaValidation {
+                            one_of: Some(_), ..
+                        }) => SubschemaValidation {
+                            one_of: Some(joined_schemas),
+                            ..Default::default()
+                        },
+                        _ => unreachable!(),
+                    };
+                    Ok(SchemaObject {
+                        metadata: schema_object.metadata,
+                        subschemas: Some(Box::new(subschemas)),
+                        ..Default::default()
                     })
-                    .collect(),
-            );
-
-            // TODO we'd want to do something special here? maybe return an
-            // error?
-            assert!(!subschemas.is_empty());
-
-            let subschemas = match maybe_subschemas {
-                Some(SubschemaValidation {
-                    any_of: Some(_), ..
-                }) => SubschemaValidation {
-                    any_of: joined_schemas,
-                    ..Default::default()
-                },
-                Some(SubschemaValidation {
-                    one_of: Some(_), ..
-                }) => SubschemaValidation {
-                    one_of: joined_schemas,
-                    ..Default::default()
-                },
-                _ => unreachable!(),
-            };
-            Ok(SchemaObject {
-                metadata: schema_object.metadata,
-                subschemas: Some(Box::new(subschemas)),
-                ..Default::default()
-            })
+                }
+            }
         }
 
         Some(SubschemaValidation {
@@ -967,6 +985,138 @@ fn merge_additional(additional: Option<&Schema>, prop_schema: &Schema) -> Result
     }
 }
 
+trait Roughly {
+    fn roughly(&self, other: &Self) -> bool;
+}
+
+impl Roughly for schemars::schema::Schema {
+    fn roughly(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Schema::Bool(a), Schema::Bool(b)) => a == b,
+            (Schema::Bool(false), _) | (_, Schema::Bool(false)) => false,
+
+            (Schema::Bool(true), Schema::Object(other))
+            | (Schema::Object(other), Schema::Bool(true)) => matches!(
+                other,
+                SchemaObject {
+                    metadata: _,
+                    instance_type: None,
+                    format: None,
+                    enum_values: None,
+                    const_value: None,
+                    subschemas: None,
+                    number: None,
+                    string: None,
+                    array: None,
+                    object: None,
+                    reference: None,
+                    extensions: _,
+                }
+            ),
+
+            (Schema::Object(a), Schema::Object(b)) => {
+                a.instance_type == b.instance_type
+                    && a.format == b.format
+                    && a.enum_values == b.enum_values
+                    && a.const_value == b.const_value
+                    && roughly_subschemas(a.subschemas.as_deref(), b.subschemas.as_deref())
+                    && a.number == b.number
+                    && a.string == b.string
+                    && roughly_array(a.array.as_deref(), b.array.as_deref())
+                    && roughly_object(a.object.as_deref(), b.object.as_deref())
+                    && a.reference == b.reference
+            }
+        }
+    }
+}
+
+fn roughly_subschemas(a: Option<&SubschemaValidation>, b: Option<&SubschemaValidation>) -> bool {
+    match (a, b) {
+        (None, None) => true,
+        (None, Some(_)) => false,
+        (Some(_), None) => false,
+        (Some(aa), Some(bb)) => {
+            roughly_schema_array(aa.all_of.as_deref(), bb.all_of.as_deref())
+                && roughly_schema_array(aa.any_of.as_deref(), bb.any_of.as_deref())
+                && roughly_schema_array(aa.one_of.as_deref(), bb.one_of.as_deref())
+                && roughly_schema_option(aa.not.as_deref(), bb.not.as_deref())
+                && roughly_schema_option(aa.if_schema.as_deref(), bb.if_schema.as_deref())
+                && roughly_schema_option(aa.then_schema.as_deref(), bb.then_schema.as_deref())
+                && roughly_schema_option(aa.else_schema.as_deref(), bb.else_schema.as_deref())
+        }
+    }
+}
+
+fn roughly_schema_option(a: Option<&Schema>, b: Option<&Schema>) -> bool {
+    match (a, b) {
+        (None, None) => true,
+        (None, Some(_)) => false,
+        (Some(_), None) => false,
+        (Some(aa), Some(bb)) => aa.roughly(bb),
+    }
+}
+
+fn roughly_schema_array(a: Option<&[Schema]>, b: Option<&[Schema]>) -> bool {
+    match (a, b) {
+        (None, None) => true,
+        (None, Some(_)) => false,
+        (Some(_), None) => false,
+        (Some(aa), Some(bb)) => {
+            // TODO We'll do it pairwise, but we should be looser..
+            aa.len() == bb.len() && aa.iter().zip(bb.iter()).all(|(aaa, bbb)| aaa.roughly(bbb))
+        }
+    }
+}
+
+fn roughly_array(a: Option<&ArrayValidation>, b: Option<&ArrayValidation>) -> bool {
+    match (a, b) {
+        (None, None) => true,
+        (None, Some(_)) => false,
+        (Some(_), None) => false,
+        (Some(aa), Some(bb)) => match (&aa.items, &bb.items) {
+            (None, None) => true,
+            (None, Some(_)) => false,
+            (Some(_), None) => false,
+            (Some(SingleOrVec::Single(_)), Some(SingleOrVec::Vec(_))) => false,
+            (Some(SingleOrVec::Vec(_)), Some(SingleOrVec::Single(_))) => false,
+
+            (Some(SingleOrVec::Single(aaa)), Some(SingleOrVec::Single(bbb))) => aaa.roughly(bbb),
+            (Some(SingleOrVec::Vec(aaa)), Some(SingleOrVec::Vec(bbb))) => {
+                roughly_schema_array(Some(aaa), Some(bbb))
+            }
+        },
+    }
+}
+
+fn roughly_object(a: Option<&ObjectValidation>, b: Option<&ObjectValidation>) -> bool {
+    match (a, b) {
+        (None, None) => true,
+        (None, Some(_)) => false,
+        (Some(_), None) => false,
+        (Some(aa), Some(bb)) => {
+            aa.max_properties == bb.max_properties
+                && aa.min_properties == bb.min_properties
+                && aa.required == bb.required
+                && roughly_properties(&aa.properties, &bb.properties)
+                && roughly_properties(&aa.pattern_properties, &bb.pattern_properties)
+                && roughly_schema_option(
+                    aa.additional_properties.as_deref(),
+                    bb.additional_properties.as_deref(),
+                )
+                && roughly_schema_option(aa.property_names.as_deref(), bb.property_names.as_deref())
+        }
+    }
+}
+
+fn roughly_properties(a: &BTreeMap<String, Schema>, b: &BTreeMap<String, Schema>) -> bool {
+    a.len() == b.len()
+        && a.iter()
+            .zip(b.iter())
+            .all(|((aa_name, aa_schema), (bb_name, bb_schema))| {
+                aa_name == bb_name && aa_schema.roughly(bb_schema)
+            })
+}
+
 #[cfg(test)]
 mod tests {
     use std::collections::BTreeMap;
@@ -974,7 +1124,7 @@ mod tests {
     use schemars::schema::InstanceType;
     use serde_json::json;
 
-    use crate::merge::merge_so_instance_type;
+    use crate::{merge::merge_so_instance_type, RefKey};
 
     use super::try_merge_schema;
 
@@ -1435,4 +1585,101 @@ mod tests {
             serde_json::to_string_pretty(&merged).unwrap(),
         )
     }
+
+    #[test]
+    fn test_match_one_of() {
+        let a = json!({
+            "$ref": "#/definitions/x"
+        });
+        let b = json!({
+            "oneOf": [
+                {
+                    "$ref": "#/definitions/x"
+                },
+                {
+                    "type": "null"
+                }
+            ]
+        });
+        let x = json!({
+            "type": "string"
+        });
+        let ab = json!({
+            "$ref": "#/definitions/x"
+        });
+
+        let a = serde_json::from_value(a).unwrap();
+        let b = serde_json::from_value(b).unwrap();
+        let x: schemars::schema::Schema = serde_json::from_value(x).unwrap();
+        let ab = serde_json::from_value(ab).unwrap();
+
+        let merged = try_merge_schema(
+            &a,
+            &b,
+            &[(RefKey::Def("x".to_string()), x)].into_iter().collect(),
+        )
+        .unwrap();
+        assert_eq!(
+            merged,
+            ab,
+            "{}",
+            serde_json::to_string_pretty(&merged).unwrap(),
+        )
+    }
+
+    #[test]
+    fn test_all_of_one_of_identity() {
+        let a = json!({
+            "oneOf": [
+                {
+                    "$ref": "#/definitions/x"
+                },
+                {
+                    "type": "null"
+                }
+            ]
+        });
+        let b = json!({
+            "oneOf": [
+                {
+                    "$ref": "#/definitions/x"
+                },
+                {
+                    "type": "null"
+                }
+            ]
+        });
+        let x = json!({
+            "title": "x",
+            "type": "string"
+        });
+        let ab = json!({
+            "oneOf": [
+                {
+                    "$ref": "#/definitions/x"
+                },
+                {
+                    "type": "null"
+                }
+            ]
+        });
+
+        let a = serde_json::from_value(a).unwrap();
+        let b = serde_json::from_value(b).unwrap();
+        let x: schemars::schema::Schema = serde_json::from_value(x).unwrap();
+        let ab = serde_json::from_value(ab).unwrap();
+
+        let merged = try_merge_schema(
+            &a,
+            &b,
+            &[(RefKey::Def("x".to_string()), x)].into_iter().collect(),
+        )
+        .unwrap();
+        assert_eq!(
+            merged,
+            ab,
+            "{}",
+            serde_json::to_string_pretty(&merged).unwrap(),
+        )
+    }
 }
diff --git a/typify-impl/src/util.rs b/typify-impl/src/util.rs
index 2195515..707d4e4 100644
--- a/typify-impl/src/util.rs
+++ b/typify-impl/src/util.rs
@@ -433,7 +433,8 @@ fn array_schemas_mutually_exclusive(
             }
 
             match (&aa.items, &aa.max_items, &bb.items, &bb.max_items) {
-                //
+                // If thee's a single item schema and it's mutually exclusive
+                // then we're done.
                 (Some(SingleOrVec::Single(a_items)), _, Some(SingleOrVec::Single(b_items)), _)
                     if schemas_mutually_exclusive(a_items, b_items) =>
                 {
@@ -443,7 +444,7 @@ fn array_schemas_mutually_exclusive(
                 _ => (),
             }
             debug!(
-                "givin up on mututal exclusivity check {} {}",
+                "giving up on mutual exclusivity check {} {}",
                 serde_json::to_string_pretty(aa).unwrap(),
                 serde_json::to_string_pretty(bb).unwrap(),
             );


Test patch:
diff --git a/typify-impl/tests/github.out b/typify-impl/tests/github.out
index b037d05..dd82c4a 100644
--- a/typify-impl/tests/github.out
+++ b/typify-impl/tests/github.out
@@ -19153,7 +19153,7 @@ impl std::convert::TryFrom<String> for IssueCommentCreatedAction {
 #[serde(deny_unknown_fields)]
 pub struct IssueCommentCreatedIssue {
     pub active_lock_reason: Option<IssueCommentCreatedIssueActiveLockReason>,
-    pub assignee: IssueCommentCreatedIssueAssignee,
+    pub assignee: Option<IssueCommentCreatedIssueAssignee>,
     pub assignees: Vec<User>,
     pub author_association: AuthorAssociation,
     #[doc = "Contents of the issue"]
@@ -19246,34 +19246,31 @@ impl std::convert::TryFrom<String> for IssueCommentCreatedIssueActiveLockReason
     }
 }
 #[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(untagged, deny_unknown_fields)]
-pub enum IssueCommentCreatedIssueAssignee {
-    Variant0 {
-        avatar_url: String,
-        #[serde(default, skip_serializing_if = "Option::is_none")]
-        email: Option<String>,
-        events_url: String,
-        followers_url: String,
-        following_url: String,
-        gists_url: String,
-        gravatar_id: String,
-        html_url: String,
-        id: i64,
-        login: String,
-        #[serde(default, skip_serializing_if = "Option::is_none")]
-        name: Option<String>,
-        node_id: String,
-        organizations_url: String,
-        received_events_url: String,
-        repos_url: String,
-        site_admin: bool,
-        starred_url: String,
-        subscriptions_url: String,
-        #[serde(rename = "type")]
-        type_: IssueCommentCreatedIssueAssigneeVariant0Type,
-        url: String,
-    },
-    Variant1,
+#[serde(deny_unknown_fields)]
+pub struct IssueCommentCreatedIssueAssignee {
+    pub avatar_url: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub email: Option<String>,
+    pub events_url: String,
+    pub followers_url: String,
+    pub following_url: String,
+    pub gists_url: String,
+    pub gravatar_id: String,
+    pub html_url: String,
+    pub id: i64,
+    pub login: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub name: Option<String>,
+    pub node_id: String,
+    pub organizations_url: String,
+    pub received_events_url: String,
+    pub repos_url: String,
+    pub site_admin: bool,
+    pub starred_url: String,
+    pub subscriptions_url: String,
+    #[serde(rename = "type")]
+    pub type_: IssueCommentCreatedIssueAssigneeType,
+    pub url: String,
 }
 impl From<&IssueCommentCreatedIssueAssignee> for IssueCommentCreatedIssueAssignee {
     fn from(value: &IssueCommentCreatedIssueAssignee) -> Self {
@@ -19281,19 +19278,17 @@ impl From<&IssueCommentCreatedIssueAssignee> for IssueCommentCreatedIssueAssigne
     }
 }
 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum IssueCommentCreatedIssueAssigneeVariant0Type {
+pub enum IssueCommentCreatedIssueAssigneeType {
     Bot,
     User,
     Organization,
 }
-impl From<&IssueCommentCreatedIssueAssigneeVariant0Type>
-    for IssueCommentCreatedIssueAssigneeVariant0Type
-{
-    fn from(value: &IssueCommentCreatedIssueAssigneeVariant0Type) -> Self {
+impl From<&IssueCommentCreatedIssueAssigneeType> for IssueCommentCreatedIssueAssigneeType {
+    fn from(value: &IssueCommentCreatedIssueAssigneeType) -> Self {
         value.clone()
     }
 }
-impl ToString for IssueCommentCreatedIssueAssigneeVariant0Type {
+impl ToString for IssueCommentCreatedIssueAssigneeType {
     fn to_string(&self) -> String {
         match *self {
             Self::Bot => "Bot".to_string(),
@@ -19302,7 +19297,7 @@ impl ToString for IssueCommentCreatedIssueAssigneeVariant0Type {
         }
     }
 }
-impl std::str::FromStr for IssueCommentCreatedIssueAssigneeVariant0Type {
+impl std::str::FromStr for IssueCommentCreatedIssueAssigneeType {
     type Err = &'static str;
     fn from_str(value: &str) -> Result<Self, &'static str> {
         match value {
@@ -19313,19 +19308,19 @@ impl std::str::FromStr for IssueCommentCreatedIssueAssigneeVariant0Type {
         }
     }
 }
-impl std::convert::TryFrom<&str> for IssueCommentCreatedIssueAssigneeVariant0Type {
+impl std::convert::TryFrom<&str> for IssueCommentCreatedIssueAssigneeType {
     type Error = &'static str;
     fn try_from(value: &str) -> Result<Self, &'static str> {
         value.parse()
     }
 }
-impl std::convert::TryFrom<&String> for IssueCommentCreatedIssueAssigneeVariant0Type {
+impl std::convert::TryFrom<&String> for IssueCommentCreatedIssueAssigneeType {
     type Error = &'static str;
     fn try_from(value: &String) -> Result<Self, &'static str> {
         value.parse()
     }
 }
-impl std::convert::TryFrom<String> for IssueCommentCreatedIssueAssigneeVariant0Type {
+impl std::convert::TryFrom<String> for IssueCommentCreatedIssueAssigneeType {
     type Error = &'static str;
     fn try_from(value: String) -> Result<Self, &'static str> {
         value.parse()
@@ -19459,7 +19454,7 @@ impl std::convert::TryFrom<String> for IssueCommentDeletedAction {
 #[serde(deny_unknown_fields)]
 pub struct IssueCommentDeletedIssue {
     pub active_lock_reason: Option<IssueCommentDeletedIssueActiveLockReason>,
-    pub assignee: IssueCommentDeletedIssueAssignee,
+    pub assignee: Option<IssueCommentDeletedIssueAssignee>,
     pub assignees: Vec<User>,
     pub author_association: AuthorAssociation,
     #[doc = "Contents of the issue"]
@@ -19552,34 +19547,31 @@ impl std::convert::TryFrom<String> for IssueCommentDeletedIssueActiveLockReason
     }
 }
 #[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(untagged, deny_unknown_fields)]
-pub enum IssueCommentDeletedIssueAssignee {
-    Variant0 {
-        avatar_url: String,
-        #[serde(default, skip_serializing_if = "Option::is_none")]
-        email: Option<String>,
-        events_url: String,
-        followers_url: String,
-        following_url: String,
-        gists_url: String,
-        gravatar_id: String,
-        html_url: String,
-        id: i64,
-        login: String,
-        #[serde(default, skip_serializing_if = "Option::is_none")]
-        name: Option<String>,
-        node_id: String,
-        organizations_url: String,
-        received_events_url: String,
-        repos_url: String,
-        site_admin: bool,
-        starred_url: String,
-        subscriptions_url: String,
-        #[serde(rename = "type")]
-        type_: IssueCommentDeletedIssueAssigneeVariant0Type,
-        url: String,
-    },
-    Variant1,
+#[serde(deny_unknown_fields)]
+pub struct IssueCommentDeletedIssueAssignee {
+    pub avatar_url: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub email: Option<String>,
+    pub events_url: String,
+    pub followers_url: String,
+    pub following_url: String,
+    pub gists_url: String,
+    pub gravatar_id: String,
+    pub html_url: String,
+    pub id: i64,
+    pub login: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub name: Option<String>,
+    pub node_id: String,
+    pub organizations_url: String,
+    pub received_events_url: String,
+    pub repos_url: String,
+    pub site_admin: bool,
+    pub starred_url: String,
+    pub subscriptions_url: String,
+    #[serde(rename = "type")]
+    pub type_: IssueCommentDeletedIssueAssigneeType,
+    pub url: String,
 }
 impl From<&IssueCommentDeletedIssueAssignee> for IssueCommentDeletedIssueAssignee {
     fn from(value: &IssueCommentDeletedIssueAssignee) -> Self {
@@ -19587,19 +19579,17 @@ impl From<&IssueCommentDeletedIssueAssignee> for IssueCommentDeletedIssueAssigne
     }
 }
 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum IssueCommentDeletedIssueAssigneeVariant0Type {
+pub enum IssueCommentDeletedIssueAssigneeType {
     Bot,
     User,
     Organization,
 }
-impl From<&IssueCommentDeletedIssueAssigneeVariant0Type>
-    for IssueCommentDeletedIssueAssigneeVariant0Type
-{
-    fn from(value: &IssueCommentDeletedIssueAssigneeVariant0Type) -> Self {
+impl From<&IssueCommentDeletedIssueAssigneeType> for IssueCommentDeletedIssueAssigneeType {
+    fn from(value: &IssueCommentDeletedIssueAssigneeType) -> Self {
         value.clone()
     }
 }
-impl ToString for IssueCommentDeletedIssueAssigneeVariant0Type {
+impl ToString for IssueCommentDeletedIssueAssigneeType {
     fn to_string(&self) -> String {
         match *self {
             Self::Bot => "Bot".to_string(),
@@ -19608,7 +19598,7 @@ impl ToString for IssueCommentDeletedIssueAssigneeVariant0Type {
         }
     }
 }
-impl std::str::FromStr for IssueCommentDeletedIssueAssigneeVariant0Type {
+impl std::str::FromStr for IssueCommentDeletedIssueAssigneeType {
     type Err = &'static str;
     fn from_str(value: &str) -> Result<Self, &'static str> {
         match value {
@@ -19619,19 +19609,19 @@ impl std::str::FromStr for IssueCommentDeletedIssueAssigneeVariant0Type {
         }
     }
 }
-impl std::convert::TryFrom<&str> for IssueCommentDeletedIssueAssigneeVariant0Type {
+impl std::convert::TryFrom<&str> for IssueCommentDeletedIssueAssigneeType {
     type Error = &'static str;
     fn try_from(value: &str) -> Result<Self, &'static str> {
         value.parse()
     }
 }
-impl std::convert::TryFrom<&String> for IssueCommentDeletedIssueAssigneeVariant0Type {
+impl std::convert::TryFrom<&String> for IssueCommentDeletedIssueAssigneeType {
     type Error = &'static str;
     fn try_from(value: &String) -> Result<Self, &'static str> {
         value.parse()
     }
 }
-impl std::convert::TryFrom<String> for IssueCommentDeletedIssueAssigneeVariant0Type {
+impl std::convert::TryFrom<String> for IssueCommentDeletedIssueAssigneeType {
     type Error = &'static str;
     fn try_from(value: String) -> Result<Self, &'static str> {
         value.parse()
@@ -19789,7 +19779,7 @@ impl From<&IssueCommentEditedChangesBody> for IssueCommentEditedChangesBody {
 #[serde(deny_unknown_fields)]
 pub struct IssueCommentEditedIssue {
     pub active_lock_reason: Option<IssueCommentEditedIssueActiveLockReason>,
-    pub assignee: IssueCommentEditedIssueAssignee,
+    pub assignee: Option<IssueCommentEditedIssueAssignee>,
     pub assignees: Vec<User>,
     pub author_association: AuthorAssociation,
     #[doc = "Contents of the issue"]
@@ -19882,34 +19872,31 @@ impl std::convert::TryFrom<String> for IssueCommentEditedIssueActiveLockReason {
     }
 }
 #[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(untagged, deny_unknown_fields)]
-pub enum IssueCommentEditedIssueAssignee {
-    Variant0 {
-        avatar_url: String,
-        #[serde(default, skip_serializing_if = "Option::is_none")]
-        email: Option<String>,
-        events_url: String,
-        followers_url: String,
-        following_url: String,
-        gists_url: String,
-        gravatar_id: String,
-        html_url: String,
-        id: i64,
-        login: String,
-        #[serde(default, skip_serializing_if = "Option::is_none")]
-        name: Option<String>,
-        node_id: String,
-        organizations_url: String,
-        received_events_url: String,
-        repos_url: String,
-        site_admin: bool,
-        starred_url: String,
-        subscriptions_url: String,
-        #[serde(rename = "type")]
-        type_: IssueCommentEditedIssueAssigneeVariant0Type,
-        url: String,
-    },
-    Variant1,
+#[serde(deny_unknown_fields)]
+pub struct IssueCommentEditedIssueAssignee {
+    pub avatar_url: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub email: Option<String>,
+    pub events_url: String,
+    pub followers_url: String,
+    pub following_url: String,
+    pub gists_url: String,
+    pub gravatar_id: String,
+    pub html_url: String,
+    pub id: i64,
+    pub login: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub name: Option<String>,
+    pub node_id: String,
+    pub organizations_url: String,
+    pub received_events_url: String,
+    pub repos_url: String,
+    pub site_admin: bool,
+    pub starred_url: String,
+    pub subscriptions_url: String,
+    #[serde(rename = "type")]
+    pub type_: IssueCommentEditedIssueAssigneeType,
+    pub url: String,
 }
 impl From<&IssueCommentEditedIssueAssignee> for IssueCommentEditedIssueAssignee {
     fn from(value: &IssueCommentEditedIssueAssignee) -> Self {
@@ -19917,19 +19904,17 @@ impl From<&IssueCommentEditedIssueAssignee> for IssueCommentEditedIssueAssignee
     }
 }
 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum IssueCommentEditedIssueAssigneeVariant0Type {
+pub enum IssueCommentEditedIssueAssigneeType {
     Bot,
     User,
     Organization,
 }
-impl From<&IssueCommentEditedIssueAssigneeVariant0Type>
-    for IssueCommentEditedIssueAssigneeVariant0Type
-{
-    fn from(value: &IssueCommentEditedIssueAssigneeVariant0Type) -> Self {
+impl From<&IssueCommentEditedIssueAssigneeType> for IssueCommentEditedIssueAssigneeType {
+    fn from(value: &IssueCommentEditedIssueAssigneeType) -> Self {
         value.clone()
     }
 }
-impl ToString for IssueCommentEditedIssueAssigneeVariant0Type {
+impl ToString for IssueCommentEditedIssueAssigneeType {
     fn to_string(&self) -> String {
         match *self {
             Self::Bot => "Bot".to_string(),
@@ -19938,7 +19923,7 @@ impl ToString for IssueCommentEditedIssueAssigneeVariant0Type {
         }
     }
 }
-impl std::str::FromStr for IssueCommentEditedIssueAssigneeVariant0Type {
+impl std::str::FromStr for IssueCommentEditedIssueAssigneeType {
     type Err = &'static str;
     fn from_str(value: &str) -> Result<Self, &'static str> {
         match value {
@@ -19949,19 +19934,19 @@ impl std::str::FromStr for IssueCommentEditedIssueAssigneeVariant0Type {
         }
     }
 }
-impl std::convert::TryFrom<&str> for IssueCommentEditedIssueAssigneeVariant0Type {
+impl std::convert::TryFrom<&str> for IssueCommentEditedIssueAssigneeType {
     type Error = &'static str;
     fn try_from(value: &str) -> Result<Self, &'static str> {
         value.parse()
     }
 }
-impl std::convert::TryFrom<&String> for IssueCommentEditedIssueAssigneeVariant0Type {
+impl std::convert::TryFrom<&String> for IssueCommentEditedIssueAssigneeType {
     type Error = &'static str;
     fn try_from(value: &String) -> Result<Self, &'static str> {
         value.parse()
     }
 }
-impl std::convert::TryFrom<String> for IssueCommentEditedIssueAssigneeVariant0Type {
+impl std::convert::TryFrom<String> for IssueCommentEditedIssueAssigneeType {
     type Error = &'static str;
     fn try_from(value: String) -> Result<Self, &'static str> {
         value.parse()
@@ -38682,7 +38667,7 @@ pub struct WorkflowJobStartedWorkflowJob {
     pub run_url: String,
     pub started_at: String,
     pub status: WorkflowJobStartedWorkflowJobStatus,
-    pub steps: [WorkflowJobStartedWorkflowJobStepsItem; 1usize],
+    pub steps: [WorkflowStepInProgress; 1usize],
     pub url: String,
 }
 impl From<&WorkflowJobStartedWorkflowJob> for WorkflowJobStartedWorkflowJob {
@@ -38779,67 +38764,6 @@ impl std::convert::TryFrom<String> for WorkflowJobStartedWorkflowJobStatus {
         value.parse()
     }
 }
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(deny_unknown_fields)]
-pub struct WorkflowJobStartedWorkflowJobStepsItem {
-    pub completed_at: (),
-    pub conclusion: (),
-    pub name: String,
-    pub number: i64,
-    pub started_at: String,
-    pub status: WorkflowJobStartedWorkflowJobStepsItemStatus,
-}
-impl From<&WorkflowJobStartedWorkflowJobStepsItem> for WorkflowJobStartedWorkflowJobStepsItem {
-    fn from(value: &WorkflowJobStartedWorkflowJobStepsItem) -> Self {
-        value.clone()
-    }
-}
-#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum WorkflowJobStartedWorkflowJobStepsItemStatus {
-    #[serde(rename = "in_progress")]
-    InProgress,
-}
-impl From<&WorkflowJobStartedWorkflowJobStepsItemStatus>
-    for WorkflowJobStartedWorkflowJobStepsItemStatus
-{
-    fn from(value: &WorkflowJobStartedWorkflowJobStepsItemStatus) -> Self {
-        value.clone()
-    }
-}
-impl ToString for WorkflowJobStartedWorkflowJobStepsItemStatus {
-    fn to_string(&self) -> String {
-        match *self {
-            Self::InProgress => "in_progress".to_string(),
-        }
-    }
-}
-impl std::str::FromStr for WorkflowJobStartedWorkflowJobStepsItemStatus {
-    type Err = &'static str;
-    fn from_str(value: &str) -> Result<Self, &'static str> {
-        match value {
-            "in_progress" => Ok(Self::InProgress),
-            _ => Err("invalid value"),
-        }
-    }
-}
-impl std::convert::TryFrom<&str> for WorkflowJobStartedWorkflowJobStepsItemStatus {
-    type Error = &'static str;
-    fn try_from(value: &str) -> Result<Self, &'static str> {
-        value.parse()
-    }
-}
-impl std::convert::TryFrom<&String> for WorkflowJobStartedWorkflowJobStepsItemStatus {
-    type Error = &'static str;
-    fn try_from(value: &String) -> Result<Self, &'static str> {
-        value.parse()
-    }
-}
-impl std::convert::TryFrom<String> for WorkflowJobStartedWorkflowJobStepsItemStatus {
-    type Error = &'static str;
-    fn try_from(value: String) -> Result<Self, &'static str> {
-        value.parse()
-    }
-}
 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
 pub enum WorkflowJobStatus {
     #[serde(rename = "queued")]