Skip to content

Commit

Permalink
Merge pull request #96 from svix/mendy/horrible-op-webhooks-hack
Browse files Browse the repository at this point in the history
Add op webhooks body schema to generated models
  • Loading branch information
svix-gabriel authored Mar 11, 2025
2 parents 684c1d6 + 6532e23 commit 09781fc
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 34 deletions.
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ RUN echo "${BIOME_HASH} biome" > biome.sha256 && \
mv biome /usr/bin/ && \
chmod +x /usr/bin/biome

# openapi-codegen
COPY --from=openapi-codegen-builder /app/target/${RUST_TARGET}/release/openapi-codegen /usr/bin/

# Ruby
COPY --from=rubyfmt-builder /app/target/release/rubyfmt-main /usr/bin/rubyfmt

Expand Down Expand Up @@ -117,3 +114,6 @@ RUN apk add --no-cache binutils && \
rm -rf /root/.rustup/toolchains/nightly-*/share && \
strip /root/.rustup/toolchains/nightly-*/lib/librustc_driver-*.so && \
apk del binutils

# openapi-codegen
COPY --from=openapi-codegen-builder /app/target/${RUST_TARGET}/release/openapi-codegen /usr/bin/
10 changes: 8 additions & 2 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,14 @@ impl Api {
.flat_map(Resource::referenced_components)
}

pub(crate) fn types(&self, schemas: &mut IndexMap<String, openapi::SchemaObject>) -> Types {
Types::from_referenced_components(schemas, self.referenced_components())
pub(crate) fn types(
&self,
schemas: &mut IndexMap<String, openapi::SchemaObject>,
webhooks: Vec<String>,
) -> Types {
let mut referenced_components: Vec<&str> = webhooks.iter().map(|s| &**s).collect();
referenced_components.extend(self.referenced_components());
Types::from_referenced_components(schemas, referenced_components.into_iter())
}
}

Expand Down
29 changes: 27 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ fn analyze_and_generate(
path: &Utf8Path,
flags: GenerateFlags,
) -> anyhow::Result<()> {
let webhooks = get_webhooks(&spec);
let mut components = spec.components.unwrap_or_default();

if let Some(paths) = spec.paths {
let api = Api::new(paths, &components.schemas, flags.include_hidden).unwrap();
let types = api.types(&mut components.schemas);
let types = api.types(&mut components.schemas, webhooks);

if flags.debug {
let mut api_file = BufWriter::new(File::create("api.ron")?);
Expand Down Expand Up @@ -161,3 +161,28 @@ fn write_codegen_metadata(input_sha256sum: String, output_dir: &Utf8Path) -> any
std::fs::write(metadata_path, &encoded_metadata)?;
Ok(())
}

fn get_webhooks(spec: &OpenApi) -> Vec<String> {
let empty_obj = serde_json::json!({});
let empty_obj = empty_obj.as_object().unwrap();
let mut referenced_components = std::collections::BTreeSet::<String>::new();
if let Some(webhooks) = spec.extensions.get("x-webhooks") {
for req in webhooks.as_object().unwrap_or(empty_obj).values() {
for method in req.as_object().unwrap_or(empty_obj).values() {
if let Some(schema_ref) = method
.get("requestBody")
.and_then(|v| v.get("content"))
.and_then(|v| v.get("application/json"))
.and_then(|v| v.get("schema"))
.and_then(|v| v.get("$ref"))
.and_then(|v| v.as_str())
{
if let Some(schema_name) = schema_ref.split('/').next_back() {
referenced_components.insert(schema_name.to_string());
}
}
}
}
}
referenced_components.into_iter().collect::<Vec<String>>()
}
75 changes: 48 additions & 27 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,10 +614,8 @@ impl FieldType {
Self::List(field_type) | Self::Set(field_type) => {
format!("List<{}>", field_type.to_csharp_typename()).into()
}
Self::SchemaRef(name) => name.clone().into(),
Self::StringConst(_) => {
unreachable!("FieldType::const should never be exposed to template code")
}
Self::SchemaRef(name) => filter_schema_ref(name, "Object"),
Self::StringConst(_) => "string".into(),
}
}

Expand All @@ -636,10 +634,8 @@ impl FieldType {
Self::List(field_type) | Self::Set(field_type) => {
format!("[]{}", field_type.to_go_typename()).into()
}
Self::SchemaRef(name) => name.clone().into(),
Self::StringConst(_) => {
unreachable!("FieldType::const should never be exposed to template code")
}
Self::SchemaRef(name) => filter_schema_ref(name, "map[string]any"),
Self::StringConst(_) => "string".into(),
}
}

Expand All @@ -659,10 +655,8 @@ impl FieldType {
Self::JsonObject => "Map<String,Any>".into(),
Self::List(field_type) => format!("List<{}>", field_type.to_kotlin_typename()).into(),
Self::Set(field_type) => format!("Set<{}>", field_type.to_kotlin_typename()).into(),
Self::SchemaRef(name) => name.clone().into(),
Self::StringConst(_) => {
unreachable!("FieldType::const should never be exposed to template code")
}
Self::SchemaRef(name) => filter_schema_ref(name, "Map<String,Any>"),
Self::StringConst(_) => "String".into(),
}
}

Expand All @@ -681,10 +675,8 @@ impl FieldType {
Self::Map { value_ty } => {
format!("{{ [key: string]: {} }}", value_ty.to_js_typename()).into()
}
Self::SchemaRef(name) => name.clone().into(),
Self::StringConst(_) => {
unreachable!("FieldType::const should never be exposed to template code")
}
Self::SchemaRef(name) => filter_schema_ref(name, "any"),
Self::StringConst(_) => "string".into(),
}
}

Expand All @@ -710,14 +702,23 @@ impl FieldType {
value_ty.to_rust_typename(),
)
.into(),
Self::SchemaRef(name) => name.clone().into(),
Self::StringConst(_) => unreachable!("FieldType::const should never be exposed to template code"),
Self::SchemaRef(name) => filter_schema_ref(name, "serde_json::Value"),
Self::StringConst(_) => "String".into()
}
}

pub(crate) fn referenced_schema(&self) -> Option<&str> {
match self {
Self::SchemaRef(v) => Some(v),
Self::SchemaRef(v) => {
// TODO(10055): the `BackgroundTaskFinishedEvent2` struct has a field with type of `Data`
// this corresponds to a `#[serde(untagged)]` enum `svix_server::v1::endpoints::background_tasks::Data`
// we should change this server side, but for now I am changing it here
if v == "Data" {
None
} else {
Some(v)
}
}
Self::List(ty) | Self::Set(ty) | Self::Map { value_ty: ty } => ty.referenced_schema(),
_ => None,
}
Expand All @@ -729,7 +730,7 @@ impl FieldType {
Self::Int16 | Self::UInt16 | Self::Int32 | Self::Int64 | Self::UInt64 => "int".into(),
Self::String => "str".into(),
Self::DateTime => "datetime".into(),
Self::SchemaRef(name) => name.clone().into(),
Self::SchemaRef(name) => filter_schema_ref(name, "t.Dict[str, t.Any]"),
Self::Uri => "str".into(),
Self::JsonObject => "t.Dict[str, t.Any]".into(),
Self::Set(field_type) | Self::List(field_type) => {
Expand All @@ -738,9 +739,7 @@ impl FieldType {
Self::Map { value_ty } => {
format!("t.Dict[str, {}]", value_ty.to_python_typename()).into()
}
Self::StringConst(_) => {
unreachable!("FieldType::const should never be exposed to template code")
}
Self::StringConst(_) => "str".into(),
}
}

Expand All @@ -762,10 +761,9 @@ impl FieldType {
FieldType::Map { value_ty } => {
format!("Map<String,{}>", value_ty.to_java_typename()).into()
}
FieldType::SchemaRef(name) => name.clone().into(),
FieldType::StringConst(_) => {
unreachable!("FieldType::const should never be exposed to template code")
}
FieldType::SchemaRef(name) => filter_schema_ref(name, "Object"),
// backwards compat
FieldType::StringConst(_) => "TypeEnum".into(),
}
}

Expand Down Expand Up @@ -853,6 +851,10 @@ impl minijinja::value::Object for FieldType {
ensure_no_args(args, "is_json_object")?;
Ok(matches!(**self, Self::JsonObject).into())
}
"is_string_const" => {
ensure_no_args(args, "is_string_const")?;
Ok(matches!(**self, Self::StringConst(_)).into())
}

// Returns the inner type of a list or set
"inner_type" => {
Expand All @@ -878,6 +880,14 @@ impl minijinja::value::Object for FieldType {
};
Ok(ty.into())
}
"string_const_val" => {
ensure_no_args(args, "string_const_val")?;
let val = match &**self {
Self::StringConst(val) => Some(minijinja::Value::from_safe_string(val.clone())),
_ => None,
};
Ok(val.into())
}
_ => Err(minijinja::Error::from(minijinja::ErrorKind::UnknownMethod)),
}
}
Expand All @@ -901,3 +911,14 @@ impl serde::Serialize for FieldType {
minijinja::Value::from_object(self.clone()).serialize(serializer)
}
}

fn filter_schema_ref<'a>(name: &'a String, json_obj_typename: &'a str) -> Cow<'a, str> {
// TODO(10055): the `BackgroundTaskFinishedEvent2` struct has a field with type of `Data`
// this corresponds to a `#[serde(untagged)]` enum `svix_server::v1::endpoints::background_tasks::Data`
// we should change this server side, but for now I am changing it here
if name == "Data" {
json_obj_typename.into()
} else {
name.clone().into()
}
}

0 comments on commit 09781fc

Please sign in to comment.