Skip to content

Commit af93e68

Browse files
authored
Merge pull request #12 from svix/jplatte/rust-lib-types-prep
More preparations for adding type templates
2 parents 44c2f56 + 29fd984 commit af93e68

File tree

2 files changed

+244
-32
lines changed

2 files changed

+244
-32
lines changed

src/api.rs

+11-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use indexmap::IndexMap;
77
use schemars::schema::{InstanceType, Schema};
88

99
use crate::{
10-
types::{FieldType, Types},
10+
types::{FieldType, Type, Types},
1111
util::get_schema_name,
1212
};
1313

@@ -73,13 +73,18 @@ impl Api {
7373
tracing::warn!(schema_name, "schema not found");
7474
return None;
7575
};
76-
match s.json_schema {
76+
let obj = match s.json_schema {
7777
Schema::Bool(_) => {
78-
tracing::warn!("found $ref'erenced bool schema, wat?!");
79-
None
78+
tracing::warn!(schema_name, "found $ref'erenced bool schema, wat?!");
79+
return None;
8080
}
81-
Schema::Object(schema_object) => {
82-
Some((schema_name.to_owned(), schema_object))
81+
Schema::Object(o) => o,
82+
};
83+
match Type::from_schema(schema_name.to_owned(), obj) {
84+
Ok(ty) => Some((schema_name.to_owned(), ty)),
85+
Err(e) => {
86+
tracing::warn!(schema_name, "unsupported schema: {e:#}");
87+
None
8388
}
8489
}
8590
})

src/types.rs

+233-26
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,160 @@
11
use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
22

3-
use aide::openapi::{self};
3+
use aide::openapi;
44
use anyhow::{bail, ensure, Context as _};
55
use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec};
6+
use serde::Serialize;
67

78
use crate::util::get_schema_name;
89

910
/// Named types referenced by the [`Api`].
1011
///
1112
/// Intermediate representation of (some) `components` from the spec.
12-
#[allow(dead_code)] // FIXME: Remove when we generate "model" files
1313
#[derive(Debug)]
14-
pub(crate) struct Types(pub BTreeMap<String, SchemaObject>);
14+
pub(crate) struct Types(pub BTreeMap<String, Type>);
15+
16+
#[derive(Debug, Serialize)]
17+
pub(crate) struct Type {
18+
name: String,
19+
#[serde(skip_serializing_if = "Option::is_none")]
20+
description: Option<String>,
21+
deprecated: bool,
22+
#[serde(flatten)]
23+
data: TypeData,
24+
}
25+
26+
impl Type {
27+
pub(crate) fn from_schema(name: String, s: SchemaObject) -> anyhow::Result<Self> {
28+
match s.instance_type {
29+
Some(SingleOrVec::Single(it)) => match *it {
30+
InstanceType::Object => {}
31+
_ => bail!("unsupported type {it:?}"),
32+
},
33+
Some(SingleOrVec::Vec(_)) => bail!("unsupported: multiple types"),
34+
None => bail!("unsupported: no type"),
35+
}
36+
37+
let metadata = s.metadata.unwrap_or_default();
38+
39+
let obj = s
40+
.object
41+
.context("unsupported: object type without further validation")?;
42+
43+
ensure!(
44+
obj.additional_properties.is_none(),
45+
"additional_properties not yet supported"
46+
);
47+
ensure!(obj.max_properties.is_none(), "unsupported: max_properties");
48+
ensure!(obj.min_properties.is_none(), "unsupported: min_properties");
49+
ensure!(
50+
obj.pattern_properties.is_empty(),
51+
"unsupported: pattern_properties"
52+
);
53+
ensure!(obj.property_names.is_none(), "unsupported: property_names");
54+
55+
Ok(Self {
56+
name,
57+
description: metadata.description,
58+
deprecated: metadata.deprecated,
59+
data: TypeData::Struct {
60+
fields: obj
61+
.properties
62+
.into_iter()
63+
.map(|(name, schema)| {
64+
Field::from_schema(name.clone(), schema, obj.required.contains(&name))
65+
.with_context(|| format!("unsupported field {name}"))
66+
})
67+
.collect::<anyhow::Result<_>>()?,
68+
},
69+
})
70+
}
71+
}
72+
73+
#[derive(Debug, Serialize)]
74+
#[serde(untagged)]
75+
pub(crate) enum TypeData {
76+
Struct {
77+
fields: Vec<Field>,
78+
},
79+
#[allow(dead_code)] // not _yet_ supported
80+
Enum {
81+
variants: Vec<Variant>,
82+
},
83+
}
84+
85+
#[derive(Debug, Serialize)]
86+
pub(crate) struct Field {
87+
name: String,
88+
r#type: FieldType,
89+
#[serde(skip_serializing_if = "Option::is_none")]
90+
default: Option<serde_json::Value>,
91+
#[serde(skip_serializing_if = "Option::is_none")]
92+
description: Option<String>,
93+
required: bool,
94+
nullable: bool,
95+
deprecated: bool,
96+
}
97+
98+
impl Field {
99+
fn from_schema(name: String, s: Schema, required: bool) -> anyhow::Result<Self> {
100+
let obj = match s {
101+
Schema::Bool(_) => bail!("unsupported bool schema"),
102+
Schema::Object(o) => o,
103+
};
104+
let metadata = obj.metadata.clone().unwrap_or_default();
105+
106+
ensure!(obj.const_value.is_none(), "unsupported const_value");
107+
ensure!(obj.enum_values.is_none(), "unsupported enum_values");
108+
109+
let nullable = obj
110+
.extensions
111+
.get("nullable")
112+
.and_then(|v| v.as_bool())
113+
.unwrap_or(false);
114+
115+
Ok(Self {
116+
name,
117+
r#type: FieldType::from_schema_object(obj)?,
118+
default: metadata.default,
119+
description: metadata.description,
120+
required,
121+
nullable,
122+
deprecated: metadata.deprecated,
123+
})
124+
}
125+
}
126+
127+
#[derive(Debug, Serialize)]
128+
pub(crate) struct Variant {
129+
fields: Vec<Field>,
130+
}
15131

16132
/// Supported field type.
17133
///
18134
/// Equivalent to openapi's `type` + `format` + `$ref`.
19135
#[derive(Clone, Debug)]
20136
pub(crate) enum FieldType {
21137
Bool,
138+
Int16,
139+
UInt16,
140+
Int32,
141+
Int64,
22142
UInt64,
23143
String,
24144
DateTime,
145+
Uri,
146+
/// A JSON object with arbitrary field values.
147+
JsonObject,
148+
/// A regular old list.
149+
List(Box<FieldType>),
25150
/// List with unique items.
26151
Set(Box<FieldType>),
152+
/// A map with a given value type.
153+
///
154+
/// The key type is always `String` in JSON schemas.
155+
Map {
156+
value_ty: Box<FieldType>,
157+
},
27158
SchemaRef(String),
28159
}
29160

@@ -32,40 +163,85 @@ impl FieldType {
32163
let openapi::ParameterSchemaOrContent::Schema(s) = format else {
33164
bail!("found unexpected 'content' data format");
34165
};
35-
Self::from_json_schema(s.json_schema)
166+
Self::from_schema(s.json_schema)
36167
}
37168

38-
fn from_json_schema(s: Schema) -> anyhow::Result<Self> {
169+
fn from_schema(s: Schema) -> anyhow::Result<Self> {
39170
let Schema::Object(obj) = s else {
40171
bail!("found unexpected `true` schema");
41172
};
42173

43-
Ok(match obj.instance_type {
44-
Some(SingleOrVec::Single(ty)) => match *ty {
174+
Self::from_schema_object(obj)
175+
}
176+
177+
fn from_schema_object(obj: SchemaObject) -> anyhow::Result<FieldType> {
178+
Ok(match &obj.instance_type {
179+
Some(SingleOrVec::Single(ty)) => match **ty {
45180
InstanceType::Boolean => Self::Bool,
46181
InstanceType::Integer => match obj.format.as_deref() {
47-
Some("uint64") => Self::UInt64,
182+
Some("int16") => Self::Int16,
183+
Some("uint16") => Self::UInt16,
184+
Some("int32") => Self::Int32,
185+
// FIXME: Why do we have int in the spec?
186+
Some("int" | "int64") => Self::Int64,
187+
// FIXME: Get rid of uint in the spec..
188+
Some("uint" | "uint64") => Self::UInt64,
48189
f => bail!("unsupported integer format: `{f:?}`"),
49190
},
50191
InstanceType::String => match obj.format.as_deref() {
51192
None => Self::String,
52193
Some("date-time") => Self::DateTime,
194+
Some("uri") => Self::Uri,
53195
Some(f) => bail!("unsupported string format: `{f:?}`"),
54196
},
55197
InstanceType::Array => {
56198
let array = obj.array.context("array type must have array props")?;
57199
ensure!(array.additional_items.is_none(), "not supported");
58-
ensure!(
59-
array.unique_items == Some(true),
60-
"non-setlike arrays not currently supported"
61-
);
62200
let inner = match array.items.context("array type must have items prop")? {
63201
SingleOrVec::Single(ty) => ty,
64202
SingleOrVec::Vec(types) => {
65203
bail!("unsupported multi-typed array parameter: `{types:?}`")
66204
}
67205
};
68-
Self::Set(Box::new(Self::from_json_schema(*inner)?))
206+
let inner = Box::new(Self::from_schema(*inner)?);
207+
if array.unique_items == Some(true) {
208+
Self::Set(inner)
209+
} else {
210+
Self::List(inner)
211+
}
212+
}
213+
InstanceType::Object => {
214+
let obj = obj
215+
.object
216+
.context("unsupported: object type without further validation")?;
217+
let additional_properties = obj
218+
.additional_properties
219+
.context("unsupported: object field type without additional_properties")?;
220+
221+
ensure!(obj.max_properties.is_none(), "unsupported: max_properties");
222+
ensure!(obj.min_properties.is_none(), "unsupported: min_properties");
223+
ensure!(
224+
obj.properties.is_empty(),
225+
"unsupported: properties on field type"
226+
);
227+
ensure!(
228+
obj.pattern_properties.is_empty(),
229+
"unsupported: pattern_properties"
230+
);
231+
ensure!(obj.property_names.is_none(), "unsupported: property_names");
232+
ensure!(
233+
obj.required.is_empty(),
234+
"unsupported: required on field type"
235+
);
236+
237+
match *additional_properties {
238+
Schema::Bool(true) => Self::JsonObject,
239+
Schema::Bool(false) => bail!("unsupported `additional_properties: false`"),
240+
Schema::Object(schema_object) => {
241+
let value_ty = Box::new(Self::from_schema_object(schema_object)?);
242+
Self::Map { value_ty }
243+
}
244+
}
69245
}
70246
ty => bail!("unsupported type: `{ty:?}`"),
71247
},
@@ -82,60 +258,91 @@ impl FieldType {
82258
fn to_csharp_typename(&self) -> Cow<'_, str> {
83259
match self {
84260
Self::Bool => "bool".into(),
261+
Self::Int32 |
85262
// FIXME: For backwards compatibility. Should be 'long'.
86-
Self::UInt64 => "int".into(),
263+
Self::Int64 | Self::UInt64 => "int".into(),
87264
Self::String => "string".into(),
88265
Self::DateTime => "DateTime".into(),
89-
Self::Set(field_type) => format!("List<{}>", field_type.to_csharp_typename()).into(),
266+
Self::Int16 | Self::UInt16 | Self::Uri | Self::JsonObject | Self::Map { .. } => todo!(),
267+
// FIXME: Treat set differently?
268+
Self::List(field_type) | Self::Set(field_type) => {
269+
format!("List<{}>", field_type.to_csharp_typename()).into()
270+
}
90271
Self::SchemaRef(name) => name.clone().into(),
91272
}
92273
}
93274

94275
fn to_go_typename(&self) -> Cow<'_, str> {
95276
match self {
96277
Self::Bool => "bool".into(),
278+
Self::Int32 |
97279
// FIXME: Looks like all integers are currently i32
98-
Self::UInt64 => "int32".into(),
280+
Self::Int64 | Self::UInt64 => "int32".into(),
99281
Self::String => "string".into(),
100282
Self::DateTime => "time.Time".into(),
101-
Self::Set(field_type) => format!("[]{}", field_type.to_go_typename()).into(),
283+
Self::Int16 | Self::UInt16 | Self::Uri | Self::JsonObject | Self::Map { .. } => todo!(),
284+
Self::List(field_type) | Self::Set(field_type) => {
285+
format!("[]{}", field_type.to_go_typename()).into()
286+
}
102287
Self::SchemaRef(name) => name.clone().into(),
103288
}
104289
}
105290

106291
fn to_kotlin_typename(&self) -> Cow<'_, str> {
107292
match self {
108293
Self::Bool => "Boolean".into(),
294+
Self::Int32 |
109295
// FIXME: Should be Long..
110-
Self::UInt64 => "Int".into(),
296+
Self::Int64 | Self::UInt64 => "Int".into(),
111297
Self::String => "String".into(),
112298
Self::DateTime => "OffsetDateTime".into(),
113-
Self::Set(field_type) => format!("List<{}>", field_type.to_kotlin_typename()).into(),
299+
Self::Int16 | Self::UInt16 | Self::Uri | Self::JsonObject | Self::Map { .. } => todo!(),
300+
// FIXME: Treat set differently?
301+
Self::List(field_type) | Self::Set(field_type) => {
302+
format!("List<{}>", field_type.to_kotlin_typename()).into()
303+
}
114304
Self::SchemaRef(name) => name.clone().into(),
115305
}
116306
}
117307

118308
fn to_js_typename(&self) -> Cow<'_, str> {
119309
match self {
120310
Self::Bool => "boolean".into(),
121-
Self::UInt64 => "number".into(),
311+
Self::Int16 | Self::UInt16 | Self::Int32 | Self::Int64 | Self::UInt64 => {
312+
"number".into()
313+
}
122314
Self::String => "string".into(),
123315
Self::DateTime => "Date | null".into(),
124-
Self::Set(field_type) => format!("{}[]", field_type.to_js_typename()).into(),
316+
Self::Uri | Self::JsonObject | Self::Map { .. } => todo!(),
317+
Self::List(field_type) | Self::Set(field_type) => {
318+
format!("{}[]", field_type.to_js_typename()).into()
319+
}
125320
Self::SchemaRef(name) => name.clone().into(),
126321
}
127322
}
128323

129324
fn to_rust_typename(&self) -> Cow<'_, str> {
130325
match self {
131326
Self::Bool => "bool".into(),
132-
// FIXME: Looks like all integers are currently i32
133-
Self::UInt64 => "i32".into(),
134-
Self::String => "String".into(),
327+
Self::Int16 => "i16".into(),
328+
Self::UInt16 => "u16".into(),
329+
Self::Int32 |
330+
// FIXME: All integers in query params are currently i32
331+
Self::Int64 | Self::UInt64 => "i32".into(),
332+
// FIXME: Do we want a separate type for Uri?
333+
Self::Uri | Self::String => "String".into(),
135334
// FIXME: Depends on those chrono imports being in scope, not that great..
136335
Self::DateTime => "DateTime<Utc>".into(),
137-
// FIXME: Use BTreeSet
138-
Self::Set(field_type) => format!("Vec<{}>", field_type.to_rust_typename()).into(),
336+
Self::JsonObject => "serde_json::Value".into(),
337+
// FIXME: Treat set differently? (BTreeSet)
338+
Self::List(field_type) | Self::Set(field_type) => {
339+
format!("Vec<{}>", field_type.to_rust_typename()).into()
340+
}
341+
Self::Map { value_ty } => format!(
342+
"std::collections::HashMap<String, {}>",
343+
value_ty.to_rust_typename(),
344+
)
345+
.into(),
139346
Self::SchemaRef(name) => name.clone().into(),
140347
}
141348
}

0 commit comments

Comments
 (0)