Skip to content

Commit f9b9be8

Browse files
ahlsunshowers
andauthored
various improvements to JSON schema processing (#1356)
Co-authored-by: Rain <[email protected]>
1 parent edab589 commit f9b9be8

File tree

8 files changed

+349
-113
lines changed

8 files changed

+349
-113
lines changed

dropshot/src/extractor/common.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub struct ExtractorMetadata {
1515
pub parameters: Vec<ApiEndpointParameter>,
1616
}
1717

18-
/// Extractors that require exclusive access to the underyling `hyper::Request`
18+
/// Extractors that require exclusive access to the underlying `hyper::Request`
1919
///
2020
/// These extractors usually need to read the body of the request or else modify
2121
/// how the server treats the rest of it (e.g., websocket upgrade). There may

dropshot/src/extractor/metadata.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::api_description::ApiSchemaGenerator;
44
use crate::pagination::PAGINATION_PARAM_SENTINEL;
55
use crate::schema_util::schema2struct;
66
use crate::schema_util::schema_extensions;
7-
use crate::schema_util::ReferenceVisitor;
7+
use crate::schema_util::StructMember;
88
use crate::websocket::WEBSOCKET_PARAM_SENTINEL;
99
use crate::ApiEndpointParameter;
1010
use crate::ApiEndpointParameterLocation;
@@ -61,19 +61,19 @@ where
6161
true,
6262
)
6363
.into_iter()
64-
.map(|struct_member| {
65-
let mut s = struct_member.schema;
66-
let mut visitor = ReferenceVisitor::new(&generator);
67-
schemars::visit::visit_schema(&mut visitor, &mut s);
68-
64+
.map(|StructMember { name, description, schema, required }| {
6965
ApiEndpointParameter::new_named(
7066
loc,
71-
struct_member.name,
72-
struct_member.description,
73-
struct_member.required,
67+
name,
68+
description,
69+
required,
7470
ApiSchemaGenerator::Static {
75-
schema: Box::new(s),
76-
dependencies: visitor.dependencies(),
71+
schema: Box::new(schema),
72+
dependencies: generator
73+
.definitions()
74+
.clone()
75+
.into_iter()
76+
.collect(),
7777
},
7878
Vec::new(),
7979
)

dropshot/src/handler.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Oxide Computer Company
1+
// Copyright 2025 Oxide Computer Company
22
//! Interface for implementing HTTP endpoint handler functions.
33
//!
44
//! For information about supported endpoint function signatures, argument types,
@@ -48,7 +48,7 @@ use crate::pagination::PaginationParams;
4848
use crate::router::VariableSet;
4949
use crate::schema_util::make_subschema_for;
5050
use crate::schema_util::schema2struct;
51-
use crate::schema_util::ReferenceVisitor;
51+
use crate::schema_util::StructMember;
5252
use crate::to_map::to_map;
5353

5454
use async_trait::async_trait;
@@ -1403,18 +1403,19 @@ impl<
14031403
true,
14041404
)
14051405
.into_iter()
1406-
.map(|struct_member| {
1407-
let mut s = struct_member.schema;
1408-
let mut visitor = ReferenceVisitor::new(&generator);
1409-
schemars::visit::visit_schema(&mut visitor, &mut s);
1406+
.map(|StructMember { name, description, schema, required }| {
14101407
ApiEndpointHeader {
1411-
name: struct_member.name,
1412-
description: struct_member.description,
1408+
name,
1409+
description,
14131410
schema: ApiSchemaGenerator::Static {
1414-
schema: Box::new(s),
1415-
dependencies: visitor.dependencies(),
1411+
schema: Box::new(schema),
1412+
dependencies: generator
1413+
.definitions()
1414+
.clone()
1415+
.into_iter()
1416+
.collect(),
14161417
},
1417-
required: struct_member.required,
1418+
required,
14181419
}
14191420
})
14201421
.collect::<Vec<_>>();

dropshot/src/schema_util.rs

Lines changed: 86 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -255,50 +255,6 @@ pub(crate) fn schema_extensions(
255255
}
256256
}
257257

258-
/// Used to visit all schemas and collect all dependencies.
259-
pub(crate) struct ReferenceVisitor<'a> {
260-
generator: &'a schemars::gen::SchemaGenerator,
261-
dependencies: indexmap::IndexMap<String, schemars::schema::Schema>,
262-
}
263-
264-
impl<'a> ReferenceVisitor<'a> {
265-
pub fn new(generator: &'a schemars::gen::SchemaGenerator) -> Self {
266-
Self { generator, dependencies: indexmap::IndexMap::new() }
267-
}
268-
269-
pub fn dependencies(
270-
self,
271-
) -> indexmap::IndexMap<String, schemars::schema::Schema> {
272-
self.dependencies
273-
}
274-
}
275-
276-
impl schemars::visit::Visitor for ReferenceVisitor<'_> {
277-
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
278-
if let Some(refstr) = &schema.reference {
279-
let definitions_path = &self.generator.settings().definitions_path;
280-
let name = &refstr[definitions_path.len()..];
281-
282-
if !self.dependencies.contains_key(name) {
283-
let mut refschema = self
284-
.generator
285-
.definitions()
286-
.get(name)
287-
.expect("invalid reference")
288-
.clone();
289-
self.dependencies.insert(
290-
name.to_string(),
291-
schemars::schema::Schema::Bool(false),
292-
);
293-
schemars::visit::visit_schema(self, &mut refschema);
294-
self.dependencies.insert(name.to_string(), refschema);
295-
}
296-
}
297-
298-
schemars::visit::visit_schema_object(self, schema);
299-
}
300-
}
301-
302258
pub(crate) fn schema_extract_description(
303259
schema: &schemars::schema::Schema,
304260
) -> (Option<String>, schemars::schema::Schema) {
@@ -379,14 +335,26 @@ pub(crate) fn j2oas_schema(
379335
// when consumers use a type such as serde_json::Value.
380336
schemars::schema::Schema::Bool(true) => {
381337
openapiv3::ReferenceOr::Item(openapiv3::Schema {
382-
schema_data: openapiv3::SchemaData::default(),
383-
schema_kind: openapiv3::SchemaKind::Any(
384-
openapiv3::AnySchema::default(),
385-
),
338+
schema_data: Default::default(),
339+
schema_kind: openapiv3::SchemaKind::Any(Default::default()),
386340
})
387341
}
342+
// The unsatisfiable, "match nothing" schema. We represent this as
343+
// the `not` of the permissive schema.
388344
schemars::schema::Schema::Bool(false) => {
389-
panic!("We don't expect to see a schema that matches the null set")
345+
openapiv3::ReferenceOr::Item(openapiv3::Schema {
346+
schema_data: Default::default(),
347+
schema_kind: openapiv3::SchemaKind::Not {
348+
not: Box::new(openapiv3::ReferenceOr::Item(
349+
openapiv3::Schema {
350+
schema_data: Default::default(),
351+
schema_kind: openapiv3::SchemaKind::Any(
352+
Default::default(),
353+
),
354+
},
355+
)),
356+
},
357+
})
390358
}
391359
schemars::schema::Schema::Object(obj) => j2oas_schema_object(name, obj),
392360
}
@@ -411,6 +379,73 @@ fn j2oas_schema_object(
411379
};
412380
}
413381

382+
let kind = j2oas_schema_object_kind(obj);
383+
384+
let mut data = openapiv3::SchemaData::default();
385+
386+
if matches!(
387+
&obj.extensions.get("nullable"),
388+
Some(serde_json::Value::Bool(true))
389+
) {
390+
data.nullable = true;
391+
}
392+
393+
if let Some(metadata) = &obj.metadata {
394+
data.title.clone_from(&metadata.title);
395+
data.description.clone_from(&metadata.description);
396+
data.default.clone_from(&metadata.default);
397+
data.deprecated = metadata.deprecated;
398+
data.read_only = metadata.read_only;
399+
data.write_only = metadata.write_only;
400+
}
401+
402+
// Preserve extensions
403+
data.extensions = obj
404+
.extensions
405+
.iter()
406+
.filter(|(key, _)| key.starts_with("x-"))
407+
.map(|(key, value)| (key.clone(), value.clone()))
408+
.collect();
409+
410+
if let Some(name) = name {
411+
data.title = Some(name.clone());
412+
}
413+
if let Some(example) = obj.extensions.get("example") {
414+
data.example = Some(example.clone());
415+
}
416+
417+
openapiv3::ReferenceOr::Item(openapiv3::Schema {
418+
schema_data: data,
419+
schema_kind: kind,
420+
})
421+
}
422+
423+
fn j2oas_schema_object_kind(
424+
obj: &schemars::schema::SchemaObject,
425+
) -> openapiv3::SchemaKind {
426+
// If the JSON schema is attempting to express an unsatisfiable schema
427+
// using an empty enumerated values array, that presents a problem
428+
// translating to the openapiv3 crate's representation. While we might
429+
// like to simply use the schema `false` that is *also* challenging to
430+
// represent via the openapiv3 crate *and* it precludes us from preserving
431+
// extensions, if they happen to be present. Instead we'll represent this
432+
// construction using `{ not: {} }` i.e. the opposite of the permissive
433+
// schema.
434+
if let Some(enum_values) = &obj.enum_values {
435+
if enum_values.is_empty() {
436+
return openapiv3::SchemaKind::Not {
437+
not: Box::new(openapiv3::ReferenceOr::Item(
438+
openapiv3::Schema {
439+
schema_data: Default::default(),
440+
schema_kind: openapiv3::SchemaKind::Any(
441+
Default::default(),
442+
),
443+
},
444+
)),
445+
};
446+
}
447+
}
448+
414449
let ty = match &obj.instance_type {
415450
Some(schemars::schema::SingleOrVec::Single(ty)) => Some(ty.as_ref()),
416451
Some(schemars::schema::SingleOrVec::Vec(_)) => {
@@ -423,7 +458,7 @@ fn j2oas_schema_object(
423458
None => None,
424459
};
425460

426-
let kind = match (ty, &obj.subschemas) {
461+
match (ty, &obj.subschemas) {
427462
(Some(schemars::schema::InstanceType::Null), None) => {
428463
openapiv3::SchemaKind::Type(openapiv3::Type::String(
429464
openapiv3::StringType {
@@ -485,45 +520,7 @@ fn j2oas_schema_object(
485520
openapiv3::SchemaKind::Any(openapiv3::AnySchema::default())
486521
}
487522
(Some(_), Some(_)) => j2oas_any(ty, obj),
488-
};
489-
490-
let mut data = openapiv3::SchemaData::default();
491-
492-
if matches!(
493-
&obj.extensions.get("nullable"),
494-
Some(serde_json::Value::Bool(true))
495-
) {
496-
data.nullable = true;
497-
}
498-
499-
if let Some(metadata) = &obj.metadata {
500-
data.title.clone_from(&metadata.title);
501-
data.description.clone_from(&metadata.description);
502-
data.default.clone_from(&metadata.default);
503-
data.deprecated = metadata.deprecated;
504-
data.read_only = metadata.read_only;
505-
data.write_only = metadata.write_only;
506523
}
507-
508-
// Preserve extensions
509-
data.extensions = obj
510-
.extensions
511-
.iter()
512-
.filter(|(key, _)| key.starts_with("x-"))
513-
.map(|(key, value)| (key.clone(), value.clone()))
514-
.collect();
515-
516-
if let Some(name) = name {
517-
data.title = Some(name.clone());
518-
}
519-
if let Some(example) = obj.extensions.get("example") {
520-
data.example = Some(example.clone());
521-
}
522-
523-
openapiv3::ReferenceOr::Item(openapiv3::Schema {
524-
schema_data: data,
525-
schema_kind: kind,
526-
})
527524
}
528525

529526
fn j2oas_any(
@@ -842,6 +839,7 @@ fn j2oas_string(
842839
let enumeration = enum_values
843840
.iter()
844841
.flat_map(|v| {
842+
assert!(!v.is_empty());
845843
v.iter().map(|vv| match vv {
846844
serde_json::Value::Null => None,
847845
serde_json::Value::String(s) => Some(s.clone()),

0 commit comments

Comments
 (0)